Refactored setPlayerInfo to joinGame and some other refactoring and comments

This commit is contained in:
martin 2023-08-05 23:32:25 +02:00
parent 697a4ddd6d
commit 5ee2d99d42
9 changed files with 111 additions and 67 deletions

View File

@ -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<DirectionalPosition> _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<GameService>(Substitute.For<ILogger<GameService>>());
_service = new ActionService(Substitute.For<ILogger<ActionService>>(), _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<DirectionalPosition> 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<JsonException>(() => _service.SetPlayerInfo(serialized.RootElement));
Assert.Throws<JsonException>(() => _service.FindGame(serialized.RootElement));
message.Data = null;
Assert.Throws<NullReferenceException>(() => _service.SetPlayerInfo(message.Data));
Assert.Throws<NullReferenceException>(() => _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<JsonException>(() => _service.SetPlayerInfo(message.Data));
Assert.Throws<JsonException>(() => _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<IEnumerable<Player>>());
Assert.That(new List<Player> { _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;

View File

@ -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<GameNotFoundException>(() => _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");

View File

@ -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();
}, []);

View File

@ -20,7 +20,7 @@ const LobbyPage: FC = () => {
async function createGame(): Promise<void> {
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) {

View File

@ -2,6 +2,8 @@ type MessageEventFunction<T = any> = (data: MessageEvent<T>) => void;
type Setter<T> = React.Dispatch<React.SetStateAction<T>>;
type GUID = `${string}-${string}-${string}-${string}-${string}`;
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView;
type ActionMessage<T = any> = {
@ -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[],
}

View File

@ -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<string> = (event): void => { // TODO divide into smaller functions
export const doAction: MessageEventFunction<string> = (event): void => {
const message: ActionMessage = JSON.parse(event.data);
console.debug("Received message:", message);
@ -43,8 +43,8 @@ export const doAction: MessageEventFunction<string> = (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)));

View File

@ -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

View File

@ -7,7 +7,7 @@ public enum GameAction
{
RollDice,
MoveCharacter,
PlayerInfo,
JoinGame,
Ready,
NextPlayer,
Disconnect

View File

@ -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<int> RollDice();
List<Player> SetPlayerInfo(JsonElement? jsonElement);
List<Player> 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<Player> SetPlayerInfo(JsonElement? jsonElement) // TODO split up into two actions, join and create
public List<Player> FindGame(JsonElement? jsonElement)
{
var data = jsonElement?.Deserialize<PlayerInfoData>() ?? throw new NullReferenceException("Data is null");
Player = data.Player;
// TODO Receive Username and GameId
var data = jsonElement?.Deserialize<JoinGameData>() ?? 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")]