using System.Text.Json; using MauMau_Server.Mau.GameMessages; using MauMau_Server.Mau.Managers; using MauMau_Server.Websockets; using MauMau_Server.Room; using MauMau_Server.Room.Messages; namespace MauMau_Server.Mau; public class Game : RoomType { // Helpers private readonly Deck _deck = new(); private readonly TurnManager _turnManager = new(); // Game state private readonly List _mauCardBuffer = new(); private CardType? NextAllowedCardType { get; set; } // Variables private const int NumberOfFaultcards = 5; private const int NumberOfStartCards = 8; public Game(Room.Room room, IEnumerable connections) : base(room) { // If the current card is a joker, set the next allowed card type to a random card type if (_deck.CurrentCard.CardType == CardType.JOKER) { // In the case the random card type is a joker, try again until it is not do { var cardTypes = Enum.GetValues(typeof(CardType)); var randomIndex = new Random().Next(cardTypes.Length); NextAllowedCardType = (CardType?)cardTypes.GetValue(randomIndex) ?? CardType.SPADES; } while (NextAllowedCardType == CardType.JOKER); } // Convert all the connections to players List players = new(); foreach (var player in connections.Select(connection => new Player(connection))) { // Give the new player a hand of cards var initialHand = _deck.DrawCards(NumberOfStartCards); player.GiveCards(initialHand); players.Add(player); } // Add all the players to the turn manager _turnManager.Initialize(players); // Broadcast new game state SendGameState(); } /** * */ public override void OnMessage(ConnectionInstance sender, RoomMessage message) { // If the message type is not a game message, ignore the message if (message.Type is not ("CHOOSE" or "DRAW" or "PLAY")) { return; } // Get the player that sent the message var player = _turnManager.Players.FirstOrDefault(x => x.IsMe(sender.Id)); // If the player is not the player that is currently playing, ignore the message if (_turnManager.CurrentPlayer != player) return; // Based on the message intent, handle the message switch (message.Type) { case "CHOOSE": Choose(player, message.Data); break; case "PLAY": Play(player, message.Data); break; default: Draw(player); break; } } /** * */ public override void OnConnect(ConnectionInstance connection) { // Broadcast that a player joined var joinMessage = new JoinMessage(_room.Connections, connection); _room.BroadCast(new RoomMessage("JOIN", joinMessage)); // Create a new player, give them a new hand and add them to the game var player = new Player(connection); var initialHand = _deck.DrawCards(8); player.GiveCards(initialHand); _turnManager.Players.Add(player); // Broadcast new game state SendGameState(); } /** * */ public override void OnDisconnect(ConnectionInstance connection) { // Broadcast that the player left var leaveMessage = new LeaveMessage(connection); _room.BroadCast(new RoomMessage("LEAVE", leaveMessage)); // Get the player that left var player = _turnManager.Players.FirstOrDefault(x => x.IsMe(connection.Id)); if (player is null) return; // Add the player's hand to the used deck var playerHand = player.Hand; _deck.AddCardsToUsedDeck(playerHand); // Change the turn if the player that left was the current player if (player == _turnManager.CurrentPlayer) { _turnManager.ChangeTurnTo(); } // Remove the player from the game _turnManager.Players.Remove(player); // Broadcast new game state SendGameState(); } /** * * The player had to choose a new card type * * The player that chose a new card type * A string that represents a CardType */ private void Choose(Player player, string data) { // TODO: Validate if choosing a card is allowed // Convert the data to a CardType, if it fails, ignore the message if (!Enum.TryParse(data, out CardType cardType)) { return; } // Set the next card type that is allowed to be played NextAllowedCardType = cardType; // Change the turns _turnManager.ChangeTurnTo(); // Broadcast new game state SendGameState(); } /** * * The player either: * * Could not play a card and had to draw a card * Chose to draw a card as a strategic move * * When there are multiple mau cards played, the player has to draw the combined amount of mau cards played. * * The player that drew a card */ private void Draw(Player player) { // If there are cards in the MauCardBuffer, this means there are multiple cards that need to be drawn // Otherwise, just draw a single card if (_mauCardBuffer.Count > 0) { // Count the amount of cards that need to be drawn var totalCards = CountMauCardBuffer(); // Draw the cards from the deck var drawnCards = _deck.DrawCards(totalCards); // Give the cards to the player player.GiveCards(drawnCards); // Change the turn _turnManager.ChangeTurnTo(); } else { // Draw a card from the deck var drawnCard = _deck.DrawCard(); // Give the card to the player player.GiveCard(drawnCard); // Change the player if the drawn card cannot be played if (!CardCanBePlayed(drawnCard)) { _turnManager.ChangeTurnTo(); } } // Broadcast new game state SendGameState(); } /** * * The player plays a card, this will only be possible if: * * The player has the correct state * The player has the card in their hand * The card is playable on the current card * * * The player that played * A string that can be serialized to a playcard instance */ private void Play(Player player, string data) { // Check if the player has the correct state to play a card if (player.State != PlayerState.TURN) { return; } // Convert the data to a Card instance var cardData = JsonSerializer.Deserialize(data).ToCard(); // Check if the player indeed has the card they claim to have var playerCard = player.TakeCardFromHand(cardData); if (playerCard is null) { return; } if (!CardCanBePlayed(playerCard)) return; // Remove the card from the player's hand player.Hand.Remove(playerCard); // If the player's last played card is a special card, give the player 5 fault cards if (player.Hand.Count < 1 && playerCard.IsSpecialCard()) { var faultCards = _deck.DrawCards(NumberOfFaultcards); player.GiveCards(faultCards); } // Add the card to the used deck _deck.AddCardToUsedDeck(playerCard); // Reset the allowed card type, so the next player has the normal same type and same value rules NextAllowedCardType = null; // If the player has no cards left, end the game with player as winner if (player.Hand.Count == 0) { EndGame(player); return; } // Change the turn based on the played card HandleNextPlayer(playerCard); // Broadcast new game state SendGameState(); } /** * * Based on the played card, change the turn to the next player. * * The card that was played */ private void HandleNextPlayer(Card card) { switch (card.CardValue) { case CardValue.RED: case CardValue.BLACK: { _mauCardBuffer.Add(card); _turnManager.CurrentPlayer.State = PlayerState.CHOOSE; break; } case CardValue.TWO: { _mauCardBuffer.Add(card); _turnManager.ChangeTurnTo(); break; } case CardValue.SEVEN: case CardValue.KING: break; case CardValue.EIGHT: _turnManager.ChangeTurnTo(_turnManager.GetNextPlayer(2)); break; case CardValue.ACE: if (_turnManager.Players.Count > 2) { _turnManager.ChangeDirection(); _turnManager.ChangeTurnTo(); } break; case CardValue.JACK: _turnManager.CurrentPlayer.State = PlayerState.CHOOSE; break; case CardValue.THREE: case CardValue.FOUR: case CardValue.FIVE: case CardValue.SIX: case CardValue.NINE: case CardValue.TEN: case CardValue.QUEEN: default: _turnManager.ChangeTurnTo(); break; } } /** * * Check if the card can be played with the current game state. * * The player can only play a mau card if there are pending mau cards (or draw) * If there is a next allowed card type, the player can only play that card type, a joker or a card with the same value as the current card * Otherwise, the player can play a card that has the same type, same value or is a joker * * * True if the given card could be played */ private bool CardCanBePlayed(Card card) { // Check if there are pending mau cards played if (_mauCardBuffer.Count > 0) { // If so, the card must be a mau card return card.IsMauCard(); } // Check if there is a next allowed card type if (NextAllowedCardType != null) { // If so, the card must be the allowed card type, a joker or the same value as the current card return card.CardType == NextAllowedCardType || card.CardType == CardType.JOKER || card.CardValue == _deck.CurrentCard.CardValue; } // Otherwise, use the normal rules return card.CanBePlayedOn(_deck.CurrentCard); } /** * * Count the amount of cards that need to be drawn from the MauCardBuffer. This method also clears the buffer. * * The amount of cards that need to be drawn */ private int CountMauCardBuffer() { var totalCards = 0; foreach (var card in _mauCardBuffer) { if (card.CardType == CardType.JOKER) { totalCards += 5; } else if (card.CardValue == CardValue.TWO) { totalCards += 2; } } _mauCardBuffer.Clear(); return totalCards; } /** * * Create a game state for each player and send it to them. * By making a unique game state for each player we can hide information (like other player's hands). * */ private void SendGameState() { foreach (var player in _turnManager.Players) { var gameState = new GameState(player, _deck.CurrentCard, NextAllowedCardType, _turnManager.CurrentPlayer, _turnManager.Players); player.Connection.SendMessageAsync( JsonSerializer.Serialize(new RoomMessage("GAME", gameState))); } } /** * * Broadcast the winner of the game and change the room's type to a lobby. * * The player that won the game */ private void EndGame(Player winner) { var winMessage = new EndMessage(winner.Connection); _room.BroadCast(new RoomMessage("END", winMessage)); _room.RoomType = new Lobby(_room); } }