diff --git a/Controllers/RoomController.cs b/Controllers/RoomController.cs index 85fe489..7d5af5e 100644 --- a/Controllers/RoomController.cs +++ b/Controllers/RoomController.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using MauMau_Server.Websockets; using Microsoft.AspNetCore.Mvc; @@ -24,26 +25,27 @@ public class RoomController : ControllerBase return Ok(rooms); } - [HttpGet("{id}")] - public async Task ConnectToRoom(string id) + [HttpGet("{id}/{name}")] + public async Task ConnectToRoom(string id, string name) { - if (HttpContext.WebSockets.IsWebSocketRequest) + var response = HttpContext.Response; + if (!HttpContext.WebSockets.IsWebSocketRequest) { - if (_roomManager.RoomExists(id)) - { - using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - var room = _roomManager.GetRoom(id); - await room.InstantiateConnection(webSocket); - } - else - { - HttpContext.Response.StatusCode = 404; - } + response.StatusCode = 400; + await response.BodyWriter.WriteAsync("Request is not a websocket request"u8.ToArray()); + return; } - else + + if (!_roomManager.RoomExists(id)) { - HttpContext.Response.StatusCode = 400; + response.StatusCode = 404; + await response.BodyWriter.WriteAsync("Room could not be found"u8.ToArray()); + return; } + + using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); + var room = _roomManager.GetRoom(id); + await room.InstantiateConnection(webSocket, name); } [HttpPost] diff --git a/Mau/Card.cs b/Mau/Card.cs index 7e5ad7e..b9b48e4 100644 --- a/Mau/Card.cs +++ b/Mau/Card.cs @@ -30,7 +30,8 @@ public enum CardType SPADES, HEARTS, DIAMONDS, - CLUBS + CLUBS, + JOKER } public enum CardValue @@ -47,5 +48,7 @@ public enum CardValue JACK, QUEEN, KING, - ACE + ACE, + RED, + BLACK } \ No newline at end of file diff --git a/Mau/Deck.cs b/Mau/Deck.cs index b2b6ff5..50ffedc 100644 --- a/Mau/Deck.cs +++ b/Mau/Deck.cs @@ -6,15 +6,27 @@ public class Deck public List UsedDeck = new(); public Deck() + { + CreateSet(); + ShuffleDeck(); + } + + private void CreateSet() { foreach (CardType cardType in Enum.GetValues(typeof(CardType))) { + if (cardType == CardType.JOKER) + { + UnusedDeck.Add(new Card(cardType, CardValue.RED)); + UnusedDeck.Add(new Card(cardType, CardValue.BLACK)); + continue; + } foreach (CardValue cardValue in Enum.GetValues(typeof(CardValue))) { + if (cardValue is CardValue.RED or CardValue.BLACK) continue; UnusedDeck.Add(new Card(cardType, cardValue)); } } - ShuffleDeck(); } public List GetUnusedDeck() @@ -49,6 +61,12 @@ public class Deck { UnusedDeck.AddRange(UsedDeck); UsedDeck.Clear(); + + if (UnusedDeck.Count == 0) + { + CreateSet(); + } + ShuffleDeck(); } diff --git a/Mau/Game.cs b/Mau/Game.cs index 4477b5f..3f15e0f 100644 --- a/Mau/Game.cs +++ b/Mau/Game.cs @@ -1,5 +1,4 @@ -using System.Net.WebSockets; -using System.Text.Json; +using System.Text.Json; using MauMau_Server.Websockets; namespace MauMau_Server.Mau; @@ -11,9 +10,11 @@ public class Game public List Players = new(); public Player CurrentPlayer; public int TurnDirection = 1; + private readonly Room _room; - public Game() + public Game(Room room) { + _room = room; CurrentCard = Deck.DrawCard(); Deck.AddCardToUsedDeck(CurrentCard); } @@ -25,7 +26,9 @@ public class Game Hand = Deck.DrawCards(8) }; Players.Add(player); - if (Players.Count == 1) CurrentPlayer = player; + if (Players.Count > 1) return; + CurrentPlayer = player; + CurrentPlayer.State = PlayerState.TURN; } public void RemovePlayer(string playerId) @@ -42,11 +45,30 @@ public class Game { case "PLAYCARD": { + if (player.State != PlayerState.TURN) + { + break; + } var card = JsonSerializer.Deserialize(action.Data).ToCard(); PlayCard(player, card); break; } + case "CHOOSE": + var choice = action.Data; + if (!Enum.TryParse(choice, out CardType cardType)) + { + break; + } + CurrentPlayer.State = PlayerState.WAIT; + CurrentPlayer = CurrentCard.CardType == CardType.JOKER ? GetNextPlayer(2) : GetNextPlayer(); + CurrentPlayer.State = PlayerState.TURN; + CurrentCard = new Card(cardType, CardValue.JACK); + break; case "DRAW": + if (player.State != PlayerState.TURN) + { + break; + } DrawCard(player); break; } @@ -59,6 +81,11 @@ public class Game Deck.AddCardToUsedDeck(card); hand.Remove(GetSameCardFromHand(hand, card)); CurrentCard = card; + if (hand.Count == 0) + { + _room.EndGame(player); + return; + } HandleNextPlayer(card); } @@ -66,6 +93,18 @@ public class Game { switch (card.CardValue) { + case CardValue.RED: + case CardValue.BLACK: + { + var nextPlayer = GetNextPlayer(); + var cardsToDraw = Deck.DrawCards(5); + foreach (var drawnCard in cardsToDraw) + { + nextPlayer.Hand.Add(drawnCard); + } + CurrentPlayer.State = PlayerState.CHOOSE; + break; + } case CardValue.TWO: { var nextPlayer = GetNextPlayer(); @@ -74,40 +113,49 @@ public class Game { nextPlayer.Hand.Add(drawnCard); } - CurrentPlayer = GetNextPlayer(2); + HandleNextPlayer(CurrentPlayer, GetNextPlayer(2)); break; } case CardValue.SEVEN: case CardValue.KING: break; case CardValue.EIGHT: - CurrentPlayer = GetNextPlayer(2); + HandleNextPlayer(CurrentPlayer, GetNextPlayer(2)); break; case CardValue.ACE: if (Players.Count > 2) { TurnDirection *= -1; - CurrentPlayer = GetNextPlayer(); + HandleNextPlayer(CurrentPlayer, GetNextPlayer()); } break; + case CardValue.JACK: + 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.JACK: case CardValue.QUEEN: default: - CurrentPlayer = GetNextPlayer(); + HandleNextPlayer(CurrentPlayer, GetNextPlayer()); break; } } + private void HandleNextPlayer(Player current, Player next) + { + current.State = PlayerState.WAIT; + next.State = PlayerState.TURN; + CurrentPlayer = next; + } + private void DrawCard(Player player) { player.Hand.Add(Deck.DrawCard()); - CurrentPlayer = GetNextPlayer(); + HandleNextPlayer(player, GetNextPlayer()); } private Player GetNextPlayer(int numberOfPlayers = 1) @@ -135,7 +183,7 @@ public class Game private static bool IsCardPlayable(Card currentCard, Card playedCard) { - return IsSameCardType(currentCard, playedCard) || IsSameCardValue(currentCard, playedCard); + return IsSameCardType(currentCard, playedCard) || IsSameCardValue(currentCard, playedCard) || playedCard.CardType == CardType.JOKER; } private static bool IsCardInHand(IEnumerable hand, Card card) diff --git a/Mau/GameState.cs b/Mau/GameState.cs index 5f510c1..91cac2e 100644 --- a/Mau/GameState.cs +++ b/Mau/GameState.cs @@ -2,16 +2,18 @@ public class GameState { - public string PlayerName { get; set; } + public PlayerDTO Me { get; set; } + public string CurrentState { get; set; } public List Hand { get; set; } = new(); public string CurrentCard { get; set; } - public string CurrentPlayer { get; set; } - public List Players { get; set; } = new(); + public PlayerDTO CurrentPlayer { get; set; } + public List Players { get; set; } = new(); public GameState(Game game, string playerId) { var p = game.GetPlayer(playerId); - PlayerName = p.Connection.ConnectionId; + Me = new PlayerDTO(game.GetPlayer(playerId)); + CurrentState = p.State.ToString(); foreach (var card in p.Hand) { Hand.Add(card.ToString()); @@ -19,10 +21,24 @@ public class GameState foreach (var player in game.Players) { - Players.Add(player.Connection.ConnectionId); + Players.Add(new PlayerDTO(player)); } CurrentCard = game.CurrentCard.ToString(); - CurrentPlayer = game.CurrentPlayer.Connection.ConnectionId;; + CurrentPlayer = new PlayerDTO(game.CurrentPlayer); + } +} + +public class PlayerDTO +{ + public string Name { get; set; } + public string Id { get; set; } + public int CardsLeft { get; set; } + + public PlayerDTO(Player player) + { + Name = player.Connection.Name; + Id = player.Connection.ConnectionId; + CardsLeft = player.Hand.Count; } } \ No newline at end of file diff --git a/Mau/Player.cs b/Mau/Player.cs index 0fbec58..0c269c3 100644 --- a/Mau/Player.cs +++ b/Mau/Player.cs @@ -1,11 +1,11 @@ -using System.Net.WebSockets; -using MauMau_Server.Websockets; +using MauMau_Server.Websockets; namespace MauMau_Server.Mau; public class Player { public ConnectionInstance Connection { get; set; } + public PlayerState State { get; set; } = PlayerState.WAIT; public List Hand { get; set; } = new(); public Player(ConnectionInstance connection) diff --git a/Mau/PlayerState.cs b/Mau/PlayerState.cs new file mode 100644 index 0000000..a52669c --- /dev/null +++ b/Mau/PlayerState.cs @@ -0,0 +1,8 @@ +namespace MauMau_Server.Mau; + +public enum PlayerState +{ + TURN, + CHOOSE, + WAIT +} \ No newline at end of file diff --git a/MauMau-Server.csproj b/MauMau-Server.csproj index 476e59d..d1cec05 100644 --- a/MauMau-Server.csproj +++ b/MauMau-Server.csproj @@ -7,6 +7,8 @@ + + diff --git a/Program.cs b/Program.cs index a45afb6..16382e9 100644 --- a/Program.cs +++ b/Program.cs @@ -1,3 +1,5 @@ +using Hangfire; +using Hangfire.MemoryStorage; using MauMau_Server.Websockets; var builder = WebApplication.CreateBuilder(args); @@ -9,6 +11,14 @@ services.AddControllers(); services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); services.AddScoped(); +// var roomManager = services.BuildServiceProvider().GetRequiredService(); +// +// services.AddHangfire((sp, config) => +// { +// config.UseRecommendedSerializerSettings(); +// config.UseMemoryStorage(); +// }); +// services.AddHangfireServer(); var app = builder.Build(); @@ -28,6 +38,9 @@ var webSocketOptions = new WebSocketOptions() app.UseWebSockets(webSocketOptions); +// var recurringJobManager = app.Services.GetRequiredService(); +// recurringJobManager.AddOrUpdate("1", () => roomManager.ClearGhostRooms(), Cron.Hourly); + app.UseCors(policyBuilder => { policyBuilder.AllowAnyOrigin(); diff --git a/Room/Chat/Chat.cs b/Room/Chat/Chat.cs index 0e22051..338321f 100644 --- a/Room/Chat/Chat.cs +++ b/Room/Chat/Chat.cs @@ -13,10 +13,22 @@ public class Chat _room = room; } - public void SendChatMessage(string connectionId, string message) + public void SendChatMessage(ConnectionInstance connection, string message) { - var chatMessage = new ChatOutput(connectionId, message); + var chatMessage = new ChatMessage(connection.Name, message); var formattedMessage = new MessageDTO("CHAT", JsonSerializer.Serialize(chatMessage)); WebsocketManager.BroadcastAsync(_room.GetWebsockets(), JsonSerializer.Serialize(formattedMessage)); } +} + +public class ChatMessage +{ + public string Sender { get; set; } + public string Message { get; set; } + + public ChatMessage(string sender, string message) + { + Sender = sender; + Message = message; + } } \ No newline at end of file diff --git a/Room/Room.cs b/Room/Room.cs index b89e655..c6bda50 100644 --- a/Room/Room.cs +++ b/Room/Room.cs @@ -1,5 +1,6 @@ using System.Net.WebSockets; using System.Text.Json; +using System.Text.RegularExpressions; using MauMau_Server.Mau; namespace MauMau_Server.Websockets; @@ -9,9 +10,10 @@ public class Room private readonly IRoomManager _roomManager; private readonly string _roomId; private readonly List _connections = new(); - private ConnectionInstance _host; + private ConnectionInstance? _host; private readonly Chat _chat; - private readonly Game _game = new(); + private Game? _game; + private RoomState _state = RoomState.LOBBY; public Room(IRoomManager roomManager, string roomId) { @@ -20,17 +22,16 @@ public class Room _roomId = roomId; } - public async Task InstantiateConnection(WebSocket socket) + public async Task InstantiateConnection(WebSocket socket, string name) { - var connection = AddConnection(socket); - if (IsEmpty()) _host = connection; - _game.AddPlayerToGame(connection); + var connection = AddConnection(socket, name); + _game?.AddPlayerToGame(connection); await HandleConnection(connection); } private async Task HandleConnection(ConnectionInstance connection) { - BroadcastGameState(); + BroadcastState(); var webSocketResponse = await WebsocketManager.ReceiveAsync(connection.Socket); while (!webSocketResponse.Result!.CloseStatus.HasValue) { @@ -39,28 +40,43 @@ public class Room { case "GAME": { + if (_state != RoomState.GAME) break; var gameInput = JsonSerializer.Deserialize(message.Payload); _game.HandleAction(connection.ConnectionId, gameInput); break; } case "CHAT": { - _chat.SendChatMessage(connection.ConnectionId, message.Payload); + var cleanedMessage = StripHTML(message.Payload); + if (string.IsNullOrWhiteSpace(cleanedMessage)) + { + cleanedMessage = "Mau!"; + }; + _chat.SendChatMessage(connection, cleanedMessage); + break; + } + case "LOBBY": + { + if (connection.ConnectionId == _host?.ConnectionId) + { + ChangeLobbyState(RoomState.GAME); + } break; } } - BroadcastGameState(); + BroadcastState(); webSocketResponse = await WebsocketManager.ReceiveAsync(connection.Socket); } WebsocketManager.CloseAsync(connection.Socket, webSocketResponse.Result); HandleDisconnect(connection.ConnectionId); } - private ConnectionInstance AddConnection(WebSocket socket) + private ConnectionInstance AddConnection(WebSocket socket, string name) { var connectionId = Guid.NewGuid().ToString(); - var connection = new ConnectionInstance(connectionId, socket); + var connection = new ConnectionInstance(name, connectionId, socket); + if (IsEmpty()) _host = connection; _connections.Add(connection); return connection; } @@ -72,6 +88,10 @@ public class Room private void BroadcastGameState() { + if (_game == null) + { + return; + } foreach (var connection in _connections) { var gameState = new GameState(_game, connection.ConnectionId); @@ -80,10 +100,50 @@ public class Room } } + private void BroadcastState() + { + switch (_state) + { + case RoomState.LOBBY: + var message = new MessageDTO("LOBBY", JsonSerializer.Serialize("a")); + WebsocketManager.BroadcastAsync(GetWebsockets(), JsonSerializer.Serialize(message)); + break; + case RoomState.GAME: + BroadcastGameState(); + break; + default: + // + break; + } + } + + private void ChangeLobbyState(RoomState targetState) + { + switch (targetState) + { + case RoomState.LOBBY: + _state = RoomState.LOBBY; + _game = null; + break; + case RoomState.GAME: + { + _state = RoomState.GAME; + _game = new Game(this); + foreach (var connectionInstance in _connections) + { + _game.AddPlayerToGame(connectionInstance); + } + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(targetState), targetState, null); + } + } + private void HandleDisconnect(string socketId) { RemoveConnection(socketId); - _game.RemovePlayer(socketId); + _game?.RemovePlayer(socketId); if (IsEmpty()) { _roomManager.RemoveRoom(_roomId); @@ -99,8 +159,26 @@ public class Room return _connections.Select(connection => connection.Socket).ToList(); } - private bool IsEmpty() + public bool IsEmpty() { return _connections.Count == 0; } + + private static string StripHTML(string input) + { + return Regex.Replace(input, "<.*?>", string.Empty); + } + + public void EndGame(Player player) + { + var message = new MessageDTO("END", JsonSerializer.Serialize(new PlayerDTO(player))); + WebsocketManager.BroadcastAsync(GetWebsockets(), JsonSerializer.Serialize(message)); + ChangeLobbyState(RoomState.LOBBY); + } +} + +public enum RoomState +{ + LOBBY, + GAME } \ No newline at end of file diff --git a/Room/RoomManager.cs b/Room/RoomManager.cs index 2280be2..4b9dab2 100644 --- a/Room/RoomManager.cs +++ b/Room/RoomManager.cs @@ -31,6 +31,16 @@ public class RoomManager : IRoomManager { return Rooms.ContainsKey(roomId); } + + public void ClearGhostRooms() + { + var ghostRooms = Rooms.Where(room => room.Value.IsEmpty()); + foreach (var room in ghostRooms) + { + GC.Collect(GC.GetGeneration(room.Value)); + RemoveRoom(room.Key); + } + } } public interface IRoomManager @@ -40,4 +50,5 @@ public interface IRoomManager public List GetAllRooms(); public void RemoveRoom(string roomId); public bool RoomExists(string roomId); + public void ClearGhostRooms(); } \ No newline at end of file diff --git a/Websockets/ConnectionInstance.cs b/Websockets/ConnectionInstance.cs index 64357d9..8b1884d 100644 --- a/Websockets/ConnectionInstance.cs +++ b/Websockets/ConnectionInstance.cs @@ -4,11 +4,13 @@ namespace MauMau_Server.Websockets; public class ConnectionInstance { + public string Name { get; set; } public string ConnectionId { get; set; } public WebSocket Socket { get; set; } - public ConnectionInstance(string connectionId, WebSocket socket) + public ConnectionInstance(string name, string connectionId, WebSocket socket) { + Name = name; ConnectionId = connectionId; Socket = socket; }