Files
MauMau-Server/Mau/Game.cs
DTieman 681cfa13bd
Some checks failed
Build Mau & Deploy Mau / build (push) Failing after 1m2s
Build Mau & Deploy Mau / deploy (push) Has been skipped
Dotnet version upgrade
2024-07-13 00:01:43 +02:00

412 lines
13 KiB
C#

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<Card> _mauCardBuffer = new();
private CardType? NextAllowedCardType { get; set; }
// Variables
private const int NumberOfFaultcards = 5;
private const int NumberOfStartCards = 8;
public Game(Room.Room room, IEnumerable<ConnectionInstance> 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<Player> 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();
}
/**
* <inheritdoc cref="RoomType.OnMessage"/>
*/
public override void OnMessage(ConnectionInstance sender, RoomMessage<string> 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;
}
}
/**
* <inheritdoc cref="RoomType.OnConnect"/>
*/
public override void OnConnect(ConnectionInstance connection)
{
// Broadcast that a player joined
var joinMessage = new JoinMessage(_room.Connections, connection);
_room.BroadCast(new RoomMessage<JoinMessage>("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();
}
/**
* <inheritdoc cref="RoomType.OnDisconnect"/>
*/
public override void OnDisconnect(ConnectionInstance connection)
{
// Broadcast that the player left
var leaveMessage = new LeaveMessage(connection);
_room.BroadCast(new RoomMessage<LeaveMessage>("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();
}
/**
* <summary>
* The player had to choose a new card type
* </summary>
* <param name="player">The player that chose a new card type</param>
* <param name="data">A string that represents a CardType</param>
*/
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();
}
/**
* <summary>
* The player either:
* <list type="bullet">
* <item>Could not play a card and had to draw a card</item>
* <item>Chose to draw a card as a strategic move</item>
* </list>
* When there are multiple mau cards played, the player has to draw the combined amount of mau cards played.
* </summary>
* <param name="player">The player that drew a card</param>
*/
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();
}
/**
* <summary>
* The player plays a card, this will only be possible if:
* <list type="bullet">
* <item>The player has the correct state</item>
* <item>The player has the card in their hand</item>
* <item>The card is playable on the current card</item>
* </list>
* </summary>
* <param name="player">The player that played</param>
* <param name="data">A string that can be serialized to a playcard instance</param>
*/
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<PlayCard>(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();
}
/**
* <summary>
* Based on the played card, change the turn to the next player.
* </summary>
* <param name="card">The card that was played</param>
*/
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;
}
}
/**
* <summary>
* Check if the card can be played with the current game state.
* <list type="bullet">
* <item>The player can only play a mau card if there are pending mau cards (or draw)</item>
* <item>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</item>
* <item>Otherwise, the player can play a card that has the same type, same value or is a joker</item>
* </list>
* </summary>
* <returns>True if the given card could be played</returns>
*/
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);
}
/**
* <summary>
* Count the amount of cards that need to be drawn from the MauCardBuffer. This method also clears the buffer.
* </summary>
* <returns>The amount of cards that need to be drawn</returns>
*/
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;
}
/**
* <summary>
* 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).
* </summary>
*/
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<GameState>("GAME", gameState)));
}
}
/**
* <summary>
* Broadcast the winner of the game and change the room's type to a lobby.
* </summary>
* <param name="winner">The player that won the game</param>
*/
private void EndGame(Player winner)
{
var winMessage = new EndMessage(winner.Connection);
_room.BroadCast(new RoomMessage<EndMessage>("END", winMessage));
_room.RoomType = new Lobby(_room);
}
}