From 5ee2d99d42f74506ae34ef6aadaed8fe53e81281 Mon Sep 17 00:00:00 2001 From: martin Date: Sat, 5 Aug 2023 23:32:25 +0200 Subject: [PATCH] Refactored setPlayerInfo to joinGame and some other refactoring and comments --- BackendTests/Services/ActionServiceTests.cs | 57 +++++++++++-------- BackendTests/Services/GameServiceTests.cs | 8 +-- .../src/components/gameComponent.tsx | 41 ++++++++++--- .../ClientApp/src/pages/lobby.tsx | 2 +- .../ClientApp/src/types/types.d.ts | 9 ++- .../ClientApp/src/utils/actions.ts | 10 ++-- .../Controllers/GameController.cs | 2 +- pac-man-board-game/GameStuff/Actions.cs | 2 +- pac-man-board-game/Services/ActionService.cs | 47 ++++++++------- 9 files changed, 111 insertions(+), 67 deletions(-) diff --git a/BackendTests/Services/ActionServiceTests.cs b/BackendTests/Services/ActionServiceTests.cs index 73734d0..31ef4fa 100644 --- a/BackendTests/Services/ActionServiceTests.cs +++ b/BackendTests/Services/ActionServiceTests.cs @@ -12,43 +12,45 @@ public class ActionServiceTests { private readonly Player _blackPlayer = Players.Create("black"); private readonly Player _redPlayer = Players.Create("red"); - private readonly Player _whitePlayer = Players.Create("white"); private ActionMessage _blackMessage = null!; + private pacMan.Services.Game _game = null!; private GameService _gameService = null!; private ActionMessage _redMessage = null!; private IActionService _service = null!; private Queue _spawns = null!; - private ActionMessage _whiteMessage = null!; + [SetUp] public void Setup() { _spawns = CreateQueue(); + _game = new pacMan.Services.Game(_spawns); _whiteMessage = new ActionMessage { - Action = GameAction.PlayerInfo, - Data = SerializeData(_whitePlayer) + Action = GameAction.JoinGame, + Data = SerializeData(_whitePlayer.Username) }; _blackMessage = new ActionMessage { - Action = GameAction.PlayerInfo, - Data = SerializeData(_blackPlayer) + Action = GameAction.JoinGame, + Data = SerializeData(_blackPlayer.Username) }; _redMessage = new ActionMessage { - Action = GameAction.PlayerInfo, - Data = SerializeData(_redPlayer) + Action = GameAction.JoinGame, + Data = SerializeData(_redPlayer.Username) }; _gameService = Substitute.For(Substitute.For>()); _service = new ActionService(Substitute.For>(), _gameService); } - private static JsonElement SerializeData(Player player) => + private JsonElement SerializeData(string username) => JsonDocument.Parse(JsonSerializer.Serialize( - new PlayerInfoData { Player = player, Spawns = CreateQueue() }) + new JoinGameData { Username = username, GameId = _game.Id } + ) ).RootElement; private static Queue CreateQueue() => @@ -81,34 +83,34 @@ public class ActionServiceTests [Test] public void PlayerInfo_DataIsNull() { - var message = new ActionMessage { Action = GameAction.PlayerInfo, Data = "null" }; + var message = new ActionMessage { Action = GameAction.JoinGame, Data = "null" }; var serialized = JsonDocument.Parse(JsonSerializer.Serialize(message.Data)); - Assert.Throws(() => _service.SetPlayerInfo(serialized.RootElement)); + Assert.Throws(() => _service.FindGame(serialized.RootElement)); message.Data = null; - Assert.Throws(() => _service.SetPlayerInfo(message.Data)); + Assert.Throws(() => _service.FindGame(message.Data)); } [Test] - public void PlayerInfo_DataIsNotPlayer() + public void PlayerInfo_DataIsNotJoinGameData() { var serialized = JsonDocument.Parse(JsonSerializer.Serialize(new Box { Colour = "white" })); var message = new ActionMessage { - Action = GameAction.PlayerInfo, + Action = GameAction.JoinGame, Data = serialized.RootElement }; - Assert.Throws(() => _service.SetPlayerInfo(message.Data)); + Assert.Throws(() => _service.FindGame(message.Data)); } [Test] - public void PlayerInfo_DataIsPlayer() + public void PlayerInfo_DataIsUsernameAndGameId() { - var players = _service.SetPlayerInfo(_whiteMessage.Data); + _game.AddPlayer(_whitePlayer); + _gameService.Games.Add(_game); + var players = _service.FindGame(_whiteMessage.Data); - var pos = _spawns.Dequeue(); - _whitePlayer.PacMan.Position = pos; - _whitePlayer.PacMan.SpawnPosition = pos; + Assert.That(players, Is.InstanceOf>()); Assert.That(new List { _whitePlayer }, Is.EqualTo(players)); } @@ -148,9 +150,11 @@ public class ActionServiceTests [Test] public void Ready_NotAllReady() { - var game = _gameService.CreateAndJoin(_whitePlayer, _spawns); + _gameService.Games.Add(_game); + var game = _gameService.JoinById(_game.Id, _whitePlayer); _gameService.JoinById(game.Id, _blackPlayer); - _service.SetPlayerInfo(_whiteMessage.Data); + + _service.FindGame(_whiteMessage.Data); // Sets white to ready var result = _service.Ready(); if (result is ReadyData r1) @@ -159,7 +163,7 @@ public class ActionServiceTests Assert.Fail("Result should be ReadyData"); _gameService.JoinById(game.Id, _redPlayer); - _service.SetPlayerInfo(_redMessage.Data); + _service.FindGame(_redMessage.Data); // Sets red to ready result = _service.Ready(); if (result is ReadyData r2) @@ -171,7 +175,10 @@ public class ActionServiceTests [Test] public void Ready_OneReady() { - _service.SetPlayerInfo(_whiteMessage.Data); + _gameService.Games.Add(_game); + _gameService.JoinById(_game.Id, _whitePlayer); + _service.FindGame(_whiteMessage.Data); // Sets white to ready + var result = _service.Ready(); // If selected the state is changed to InGame _whitePlayer.State = State.InGame; diff --git a/BackendTests/Services/GameServiceTests.cs b/BackendTests/Services/GameServiceTests.cs index f79ca0f..80e5759 100644 --- a/BackendTests/Services/GameServiceTests.cs +++ b/BackendTests/Services/GameServiceTests.cs @@ -60,7 +60,7 @@ public class GameServiceTests public void JoinById_WhenIdNotExists() { var player = Players.Create("white"); - _service.AddPlayer(player, _spawns); + _service.AddPlayer(player, _spawns); // TODO obsolete Assert.Throws(() => _service.JoinById(Guid.NewGuid(), player)); } @@ -69,7 +69,7 @@ public class GameServiceTests public void JoinById_WhenIdExists() { var player = Players.Create("white"); - var group = _service.AddPlayer(player, _spawns); + var group = _service.AddPlayer(player, _spawns); // TODO obsolete var player2 = Players.Create("black"); var result = _service.JoinById(group.Id, player2); @@ -90,7 +90,7 @@ public class GameServiceTests public void AddPlayer_ToEmptyGroup() { var player = Players.Create("white"); - var group = _service.AddPlayer(player, _spawns); + var group = _service.AddPlayer(player, _spawns); // TODO obsolete Assert.Multiple(() => { @@ -106,7 +106,7 @@ public class GameServiceTests for (var i = 0; i < 4; i++) { var player = Players.Create(i.ToString()); - _service.AddPlayer(player, _spawns); + _service.AddPlayer(player, _spawns); // TODO obsolete } var player5 = Players.Create("white"); diff --git a/pac-man-board-game/ClientApp/src/components/gameComponent.tsx b/pac-man-board-game/ClientApp/src/components/gameComponent.tsx index fbab39a..bf970d7 100644 --- a/pac-man-board-game/ClientApp/src/components/gameComponent.tsx +++ b/pac-man-board-game/ClientApp/src/components/gameComponent.tsx @@ -3,14 +3,13 @@ import {AllDice} from "./dice"; import {doAction, GameAction} from "../utils/actions"; import GameBoard from "./gameBoard"; import WebSocketService from "../websockets/WebSocketService"; -import {getPacManSpawns} from "../game/map"; import Player from "../game/player"; import PlayerStats from "../components/playerStats"; import {useAtom, useAtomValue, useSetAtom} from "jotai"; import {diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedDiceAtom} from "../utils/state"; import GameButton from "./gameButton"; import {Button} from "./button"; -import {useNavigate} from "react-router-dom"; +import {useNavigate, useParams} from "react-router-dom"; const wsService = new WebSocketService(import.meta.env.VITE_API_WS); @@ -19,6 +18,7 @@ const wsService = new WebSocketService(import.meta.env.VITE_API_WS); // TODO bug, when refreshing page, some data is missing until other clients make a move // TODO bug, stolen pellets are only updated on the client that stole them // TODO bug, when navigating to lobby from the navbar while not logged in, the page is blank instead of redirecting to login +// TODO bug, when refreshing page, the player's button show ready, instead of roll dice or waiting // TODO spawns should be the same color as the player // TODO better front page @@ -29,6 +29,9 @@ const wsService = new WebSocketService(import.meta.env.VITE_API_WS); // TODO sign up player page // TODO show box with collected pellets // TODO layout +// TODO end game when all pellets are eaten +// TODO store stats in backend +// TODO check if game exists on load, if not redirect to lobby export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map}) => { @@ -39,7 +42,11 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map const ghosts = useAtomValue(ghostsAtom); const navigate = useNavigate(); + const {id} = useParams(); + /** + * Rolls the dice for the current player's turn. + */ function rollDice(): void { if (!player.isTurn()) return; @@ -48,6 +55,10 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map setActiveRollDiceButton(false); } + /** + * Handles the event when the character moves. + * @param {Position[]} eatenPellets - An array of positions where the pellets have been eaten. + */ function onCharacterMove(eatenPellets: Position[]): void { if (dice && selectedDice) { dice.splice(selectedDice.index, 1); @@ -69,23 +80,37 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map } } - function sendPlayer(): void { - wsService.send({ - action: GameAction.playerInfo, + /** + * Joins a game by sending a WebSocket request to the server. + */ + function joinGame(): void { + wsService.send({ // TODO if returns exception, navigate to lobby + action: GameAction.joinGame, data: { - player: player, spawns: getPacManSpawns(map) - } as PlayerInfoData + username: player.username, + gameId: id, + } as JoinGameData }); } + /** + * Sends a ready action to the WebSocket service. + */ function sendReady(): void { wsService.send({action: GameAction.ready}); } + /** + * Ends the current turn and sends a message to the web socket service + * to advance to the next player in the game. + */ function endTurn(): void { wsService.send({action: GameAction.nextPlayer}); } + /** + * Leaves the current game and navigates to the lobby. + */ function leaveGame(): void { wsService.send({action: GameAction.disconnect}); navigate("/lobby"); @@ -95,7 +120,7 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map wsService.onReceive = doAction; wsService.open(); - wsService.waitForOpen().then(() => sendPlayer()); + wsService.waitForOpen().then(() => joinGame()); return () => wsService.close(); }, []); diff --git a/pac-man-board-game/ClientApp/src/pages/lobby.tsx b/pac-man-board-game/ClientApp/src/pages/lobby.tsx index 4fed86e..22bbd9a 100644 --- a/pac-man-board-game/ClientApp/src/pages/lobby.tsx +++ b/pac-man-board-game/ClientApp/src/pages/lobby.tsx @@ -20,7 +20,7 @@ const LobbyPage: FC = () => { async function createGame(): Promise { const response = await postData("/game/create", { - body: {player: thisPlayer, spawns: getPacManSpawns(map)} as PlayerInfoData + body: {player: thisPlayer, spawns: getPacManSpawns(map)} as CreateGameData }); if (response.ok) { diff --git a/pac-man-board-game/ClientApp/src/types/types.d.ts b/pac-man-board-game/ClientApp/src/types/types.d.ts index 288346c..d02e173 100644 --- a/pac-man-board-game/ClientApp/src/types/types.d.ts +++ b/pac-man-board-game/ClientApp/src/types/types.d.ts @@ -2,6 +2,8 @@ type MessageEventFunction = (data: MessageEvent) => void; type Setter = React.Dispatch>; +type GUID = `${string}-${string}-${string}-${string}-${string}`; + type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView; type ActionMessage = { @@ -54,7 +56,12 @@ type ApiRequest = { body?: any } -type PlayerInfoData = { +type JoinGameData = { + readonly username: string, + readonly gameId: GUID, +} + +type CreateGameData = { readonly player: PlayerProps, readonly spawns: DirectionalPosition[], } diff --git a/pac-man-board-game/ClientApp/src/utils/actions.ts b/pac-man-board-game/ClientApp/src/utils/actions.ts index 17a1010..e211e97 100644 --- a/pac-man-board-game/ClientApp/src/utils/actions.ts +++ b/pac-man-board-game/ClientApp/src/utils/actions.ts @@ -9,7 +9,7 @@ import {Colour} from "../game/colour"; export enum GameAction { rollDice, moveCharacter, - playerInfo, // TODO rename to joinGame + joinGame, ready, nextPlayer, disconnect, @@ -32,7 +32,7 @@ const ghosts = ghostsProps.map(props => new Ghost(props)); store.set(ghostsAtom, ghosts); -export const doAction: MessageEventFunction = (event): void => { // TODO divide into smaller functions +export const doAction: MessageEventFunction = (event): void => { const message: ActionMessage = JSON.parse(event.data); console.debug("Received message:", message); @@ -43,8 +43,8 @@ export const doAction: MessageEventFunction = (event): void => { // TODO case GameAction.moveCharacter: moveCharacter(message.data); break; - case GameAction.playerInfo: - playerInfo(message.data); + case GameAction.joinGame: + joinGame(message.data); break; case GameAction.ready: ready(message.data); @@ -95,7 +95,7 @@ function removeEatenPellets(data?: MoveCharacterData): void { } } -function playerInfo(data?: PlayerProps[]): void { // TODO missing data when refreshing page +function joinGame(data?: PlayerProps[]): void { // TODO missing data when refreshing page const playerProps = data ?? []; spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.pacMan); store.set(playersAtom, playerProps.map(p => new Player(p))); diff --git a/pac-man-board-game/Controllers/GameController.cs b/pac-man-board-game/Controllers/GameController.cs index 51afb34..9f1b4f4 100644 --- a/pac-man-board-game/Controllers/GameController.cs +++ b/pac-man-board-game/Controllers/GameController.cs @@ -52,7 +52,7 @@ public class GameController : GenericController } [HttpPost("create")] - public IActionResult CreateGame([FromBody] PlayerInfoData data) + public IActionResult CreateGame([FromBody] CreateGameData data) { Logger.Log(LogLevel.Debug, "Creating game"); try diff --git a/pac-man-board-game/GameStuff/Actions.cs b/pac-man-board-game/GameStuff/Actions.cs index 94d1e41..8841a48 100644 --- a/pac-man-board-game/GameStuff/Actions.cs +++ b/pac-man-board-game/GameStuff/Actions.cs @@ -7,7 +7,7 @@ public enum GameAction { RollDice, MoveCharacter, - PlayerInfo, + JoinGame, Ready, NextPlayer, Disconnect diff --git a/pac-man-board-game/Services/ActionService.cs b/pac-man-board-game/Services/ActionService.cs index 4cd5cd5..d98c488 100644 --- a/pac-man-board-game/Services/ActionService.cs +++ b/pac-man-board-game/Services/ActionService.cs @@ -1,6 +1,7 @@ using System.Net.WebSockets; using System.Text.Json; using System.Text.Json.Serialization; +using pacMan.Exceptions; using pacMan.GameStuff; using pacMan.GameStuff.Items; @@ -13,7 +14,7 @@ public interface IActionService WebSocket? WebSocket { set; } void DoAction(ActionMessage message); List RollDice(); - List SetPlayerInfo(JsonElement? jsonElement); + List FindGame(JsonElement? jsonElement); object? HandleMoveCharacter(JsonElement? jsonElement); object Ready(); string FindNextPlayer(); @@ -45,7 +46,7 @@ public class ActionService : IActionService { GameAction.RollDice => RollDice(), GameAction.MoveCharacter => HandleMoveCharacter(message.Data), - GameAction.PlayerInfo => SetPlayerInfo(message.Data), + GameAction.JoinGame => FindGame(message.Data), GameAction.Ready => Ready(), GameAction.NextPlayer => FindNextPlayer(), GameAction.Disconnect => LeaveGame(), @@ -75,29 +76,22 @@ public class ActionService : IActionService return jsonElement; } - public List SetPlayerInfo(JsonElement? jsonElement) // TODO split up into two actions, join and create + public List FindGame(JsonElement? jsonElement) { - var data = jsonElement?.Deserialize() ?? throw new NullReferenceException("Data is null"); - Player = data.Player; + // TODO Receive Username and GameId + var data = jsonElement?.Deserialize() ?? throw new NullReferenceException("Data is null"); - Game? game; - if ((game = _gameService.FindGameByUsername(Player.Username)) != null) - { - var player = game.Players.Find(p => p.Username == Player.Username); - if (player is null) throw new NullReferenceException("Player is null"); + var game = _gameService.Games.FirstOrDefault(game => game.Id == data.GameId) ?? + throw new GameNotFoundException(); - player.State = game.IsGameStarted ? State.InGame : State.WaitingForPlayers; // TODO doesn't work anymore - Player = player; - Game = game; - // TODO send missing data: Dices, CurrentPlayer, Ghosts - } - else - { - Game = _gameService.CreateAndJoin(Player, data.Spawns); - } + var player = game.Players.Find(p => p.Username == data.Username) + ?? throw new PlayerNotFoundException("Player was not found in game"); + player.State = game.IsGameStarted ? State.InGame : State.WaitingForPlayers; // TODO doesn't work anymore + Player = player; + Game = game; + // TODO send missing data: Dices, CurrentPlayer, Ghosts | Return Game instead? Game.Connections += SendSegment; - return Game.Players; } @@ -147,7 +141,18 @@ public class ActionService : IActionService } } -public struct PlayerInfoData +public struct JoinGameData +{ + [JsonInclude] + [JsonPropertyName("username")] + public required string Username { get; init; } + + [JsonInclude] + [JsonPropertyName("gameId")] + public required Guid GameId { get; init; } +} + +public struct CreateGameData { [JsonInclude] [JsonPropertyName("player")]