diff --git a/Mau/Deck.cs b/Mau/Deck.cs index ad83aad..e5129e8 100644 --- a/Mau/Deck.cs +++ b/Mau/Deck.cs @@ -17,6 +17,10 @@ public class Deck ShuffleDeck(); CurrentCard = DrawCard(); _usedDeck.Add(CurrentCard); + + if (CurrentCard.CardType != CardType.JOKER) return; + CurrentCard = DrawCard(); + _usedDeck.Add(CurrentCard); } /** diff --git a/Mau/Game.cs b/Mau/Game.cs index fb2f41a..8bd9804 100644 --- a/Mau/Game.cs +++ b/Mau/Game.cs @@ -11,6 +11,8 @@ public class Game : RoomType { private readonly Deck _deck = new(); private readonly TurnManager _turnManager = new(); + public readonly List MauCardBuffer = new(); + public CardType? NextAllowedCardType { get; set; } public Game(Room.Room room, IEnumerable connections) : base(room) { @@ -69,7 +71,7 @@ public class Game : RoomType public override void OnConnect(ConnectionInstance connection) { // Broadcast that a player joined - var joinMessage = new JoinMessage(connection.Id, connection.Name + " has joined the room."); + 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 @@ -88,7 +90,7 @@ public class Game : RoomType public override void OnDisconnect(ConnectionInstance connection) { // Broadcast that the player left - var leaveMessage = new LeaveMessage(connection.Id, connection.Name + " has left the room."); + var leaveMessage = new LeaveMessage(connection); _room.BroadCast(new RoomMessage("LEAVE", leaveMessage)); // Get the player that left @@ -127,24 +129,11 @@ public class Game : RoomType return; } - // Does the current card require the next player to draw cards? - var isCardGivingCard = _deck.CurrentCard.CardType == CardType.JOKER || _deck.CurrentCard.CardValue == CardValue.TWO; - - // Can the player that received a card giving card counter the card? - var skippedPlayerCanPlay = player.CanPlayCard(_deck.CurrentCard); - - // If the card is a card giving card and the player cannot counter it, skip the next player - var shouldSkipPlayer = isCardGivingCard && !skippedPlayerCanPlay; - var nextPlayer = shouldSkipPlayer - ? _turnManager.GetNextPlayer(2) - : _turnManager.GetNextPlayer(); - - // TODO: Not make it a jack, as it would not work when a Joker was played - // Set the new current card - _deck.CurrentCard = new Card(cardType, CardValue.JACK); + // Set the next card type that is allowed to be played + NextAllowedCardType = cardType; // Change the turns - _turnManager.ChangeTurnTo(nextPlayer); + _turnManager.ChangeTurnTo(); // Broadcast new game state SendGameState(); @@ -157,23 +146,43 @@ public class Game : RoomType * 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 * A string that can be serialized to a drawcard instance */ private void Draw(Player player) { - // 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 (!drawnCard.CanBePlayedOn(_deck.CurrentCard)) + // 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 (!drawnCard.CanBePlayedOn(_deck.CurrentCard)) + { + _turnManager.ChangeTurnTo(); + } + } // Broadcast new game state SendGameState(); @@ -208,14 +217,34 @@ public class Game : RoomType { return; } - - // Check if the played card is compatible with the current card - // If not, ignore the play - if (!playerCard.CanBePlayedOn(_deck.CurrentCard)) + + // Check if there is a specific card type that is allowed to be played + if (NextAllowedCardType != null) { - return; + // If the card is not the allowed card type, not the same value or a joker, ignore the message + if (playerCard.CardType != NextAllowedCardType && playerCard.CardType != CardType.JOKER && playerCard.CardValue != _deck.CurrentCard.CardValue) + { + return; + } + // Reset the allowed card type, so the next player has the normal same type and same value rules + NextAllowedCardType = null; + } else if (MauCardBuffer.Count > 0) + { + // If there are queued mau cards, the player can only another mau card (or draw) + if (playerCard.CardType != CardType.JOKER && playerCard.CardValue != CardValue.TWO) + { + return; + } } - + else + { + // Check if the card can be played on the current card, if not, ignore the message + if (!playerCard.CanBePlayedOn(_deck.CurrentCard)) + { + return; + } + } + // Remove the card from the player's hand player.Hand.Remove(playerCard); @@ -251,14 +280,14 @@ public class Game : RoomType case CardValue.RED: case CardValue.BLACK: { - _turnManager.GetNextPlayer().GiveCards(_deck.DrawCards(5)); + MauCardBuffer.Add(card); _turnManager.CurrentPlayer.State = PlayerState.CHOOSE; break; } case CardValue.TWO: { - _turnManager.GetNextPlayer().GiveCards(_deck.DrawCards(2)); - _turnManager.ChangeTurnTo(_turnManager.GetNextPlayer(2)); + MauCardBuffer.Add(card); + _turnManager.ChangeTurnTo(); break; } case CardValue.SEVEN: @@ -290,6 +319,33 @@ public class Game : RoomType } } + /** + * + * 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; + continue; + } + + if (card.CardValue == CardValue.TWO) + { + totalCards += 2; + continue; + } + } + MauCardBuffer.Clear(); + return totalCards; + } + /** * * Create a game state for each player and send it to them. @@ -300,7 +356,7 @@ public class Game : RoomType { foreach (var player in _turnManager.Players) { - var gameState = new GameState(player, _deck.CurrentCard, _turnManager.CurrentPlayer, _turnManager.Players); + var gameState = new GameState(player, _deck.CurrentCard, NextAllowedCardType, _turnManager.CurrentPlayer, _turnManager.Players); player.Connection.SendMessageAsync(JsonConvert.SerializeObject(new RoomMessage("GAME", gameState))); } } diff --git a/Mau/GameState.cs b/Mau/GameState.cs index d544afb..bf113db 100644 --- a/Mau/GameState.cs +++ b/Mau/GameState.cs @@ -6,10 +6,11 @@ public class GameState public string MyState { get; set; } public List Hand { get; set; } = new(); public string CurrentCard { get; set; } + public string? NextAllowedCardType { get; set; } public PlayerDTO CurrentPlayer { get; set; } public List Players { get; set; } = new(); - public GameState(Player me, Card currentCard, Player currentPlayer, List others) + public GameState(Player me, Card currentCard, CardType? nextAllowedCardType, Player currentPlayer, List others) { Me = new PlayerDTO(me); MyState = me.State.ToString(); @@ -23,6 +24,7 @@ public class GameState Players.Add(new PlayerDTO(player)); } + NextAllowedCardType = nextAllowedCardType?.ToString(); CurrentCard = currentCard.ToString(); CurrentPlayer = new PlayerDTO(currentPlayer); } diff --git a/Room/Lobby.cs b/Room/Lobby.cs index 9559eac..cd8d6ac 100644 --- a/Room/Lobby.cs +++ b/Room/Lobby.cs @@ -21,13 +21,13 @@ public class Lobby : RoomType public override void OnConnect(ConnectionInstance connection) { - var joinMessage = new JoinMessage(connection.Id, connection.Name + " has joined the room."); + var joinMessage = new JoinMessage(_room.Connections, connection); _room.BroadCast(new RoomMessage("JOIN", joinMessage)); } public override void OnDisconnect(ConnectionInstance connection) { - var leaveMessage = new LeaveMessage(connection.Id, connection.Name + " has left the room."); + var leaveMessage = new LeaveMessage(connection); _room.BroadCast(new RoomMessage("LEAVE", leaveMessage)); } } \ No newline at end of file diff --git a/Room/Messages/JoinMessage.cs b/Room/Messages/JoinMessage.cs index b2f492e..b838a8e 100644 --- a/Room/Messages/JoinMessage.cs +++ b/Room/Messages/JoinMessage.cs @@ -1,21 +1,15 @@ -namespace MauMau_Server.Room.Messages; +using MauMau_Server.Websockets; + +namespace MauMau_Server.Room.Messages; public class JoinMessage { - public Guid Id { get; set; } - public string ChatMessage { get; set; } - public string? Data { get; set; } + public List Connections { get; set; } + public ConnectionInstance NewConnection { get; set; } - public JoinMessage(Guid id, string chatMessage, string data) + public JoinMessage(List connections, ConnectionInstance newConnection) { - Id = id; - ChatMessage = chatMessage; - Data = data; - } - - public JoinMessage(Guid id, string chatMessage) - { - Id = id; - ChatMessage = chatMessage; + Connections = connections; + NewConnection = newConnection; } } \ No newline at end of file diff --git a/Room/Messages/LeaveMessage.cs b/Room/Messages/LeaveMessage.cs index 32bdd0f..9b181e1 100644 --- a/Room/Messages/LeaveMessage.cs +++ b/Room/Messages/LeaveMessage.cs @@ -1,13 +1,13 @@ -namespace MauMau_Server.Room.Messages; +using MauMau_Server.Websockets; + +namespace MauMau_Server.Room.Messages; public class LeaveMessage { - public Guid Id { get; set; } - public string ChatMessage { get; set; } + public ConnectionInstance Connection { get; set; } - public LeaveMessage(Guid id, string chatMessage) + public LeaveMessage(ConnectionInstance connection) { - Id = id; - ChatMessage = chatMessage; + Connection = connection; } } \ No newline at end of file diff --git a/Room/Room.cs b/Room/Room.cs index b5d6da3..2b5f338 100644 --- a/Room/Room.cs +++ b/Room/Room.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using MauMau_Server.Room.Messages; using MauMau_Server.Websockets; +using Microsoft.AspNet.SignalR.Messaging; namespace MauMau_Server.Room; @@ -24,10 +25,16 @@ public class Room public async Task InstantiateConnection(WebSocket socket, string name) { var connectionId = Guid.NewGuid(); - var connection = new ConnectionInstance(name, connectionId, socket); + + // If the name is empty, set it to "Mau" + the first part of the connection ID, otherwise strip potential HTML from the name and use it + var validatedName = !string.IsNullOrWhiteSpace(name) + ? StripHTML(name) + : "Mau" + connectionId.ToString().Split('-')[0]; + + var connection = new ConnectionInstance(validatedName, connectionId, socket); if (IsEmpty()) Host = connection; Connections.Add(connection); - + RoomType.OnConnect(connection); await HandleConnection(connection); } @@ -38,27 +45,60 @@ public class Room while (!webSocketResponse.Result!.CloseStatus.HasValue) { var message = JsonSerializer.Deserialize>(webSocketResponse.SlicedBuffer); - if (message.Type == "CHAT") + switch (message.Type) { - var cleanedMessage = StripHTML(message.Data); - if (string.IsNullOrWhiteSpace(cleanedMessage)) cleanedMessage = "Mau!"; - var chatMessage = new ChatMessage(connection.Name, cleanedMessage); - BroadCast(new RoomMessage("CHAT", chatMessage)); - } - else - { - RoomType.OnMessage(connection, message); + case "CHAT": + HandleChatMessage(connection, message.Data); + break; + case "KICK": + await HandleKick(Guid.Parse(message.Data)); + break; + default: + RoomType.OnMessage(connection, message); + break; } webSocketResponse = await WebsocketManager.ReceiveAsync(connection.Socket); } + WebsocketManager.CloseAsync(connection.Socket, webSocketResponse.Result); HandleDisconnect(connection); } + private void HandleChatMessage(ConnectionInstance sender, string chatMessage) + { + // Remove HTML from chat message to prevent HTML injection + var cleanedMessage = StripHTML(chatMessage); + + // If the message is empty, set it to "Mau!" + if (string.IsNullOrWhiteSpace(cleanedMessage)) cleanedMessage = "Mau!"; + + // Create a new chat message object with the sender and the message + var envelope = new ChatMessage(sender.Name, cleanedMessage); + + // Broadcast the chat message to all connections in the room + BroadCast(new RoomMessage("CHAT", envelope)); + } + + private async Task HandleKick(Guid connectionId) + { + // Search for the connection with the given ID + var connection = Connections.FirstOrDefault(connection => connection.Id == connectionId); + + // If the connection could not be found, return + if (connection == null) return; + + // Handle the disconnect of the connection + HandleDisconnect(connection); + + // Close the connection with the reason "You have been kicked" + await connection.Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "You have been kicked", + CancellationToken.None); + } + private void HandleDisconnect(ConnectionInstance connection) { - Connections.Remove(connection); + Connections.Remove(connection); RoomType.OnDisconnect(connection); if (IsEmpty()) { @@ -84,7 +124,7 @@ public class Room { return Connections.Count == 0; } - + private static string StripHTML(string input) { return Regex.Replace(input, "<.*?>", string.Empty); diff --git a/Websockets/ConnectionInstance.cs b/Websockets/ConnectionInstance.cs index 9e4fbc1..9a5c58c 100644 --- a/Websockets/ConnectionInstance.cs +++ b/Websockets/ConnectionInstance.cs @@ -1,5 +1,6 @@ using System.Net.WebSockets; using System.Text; +using Newtonsoft.Json; namespace MauMau_Server.Websockets; @@ -7,6 +8,7 @@ public class ConnectionInstance { public Guid Id { get; set; } public string Name { get; set; } + [JsonIgnore] public WebSocket Socket { get; set; } public ConnectionInstance(string name, Guid id, WebSocket socket) diff --git a/Websockets/WebsocketManager.cs b/Websockets/WebsocketManager.cs index 487a16c..6fefe05 100644 --- a/Websockets/WebsocketManager.cs +++ b/Websockets/WebsocketManager.cs @@ -16,6 +16,9 @@ public static class WebsocketManager public static async void CloseAsync(WebSocket webSocket, WebSocketReceiveResult result) { - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + if (webSocket.State is WebSocketState.Open or WebSocketState.CloseReceived or WebSocketState.CloseSent) + { + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } } } \ No newline at end of file