Sets the spawn in the backend when player is created, and other fixes :p

This commit is contained in:
Martin Berg Alstad 2023-07-14 19:14:19 +02:00
parent 63502405e1
commit 47196161ac
22 changed files with 276 additions and 124 deletions

@ -11,27 +11,49 @@ namespace BackendTests.Services;
public class ActionServiceTests public class ActionServiceTests
{ {
private readonly IPlayer _blackPlayer = Players.Create("black"); private readonly Player _blackPlayer = (Player)Players.Create("black");
private readonly IPlayer _redPlayer = Players.Create("red"); private readonly Player _redPlayer = (Player)Players.Create("red");
private readonly IPlayer _whitePlayer = Players.Create("white");
private readonly Player _whitePlayer = (Player)Players.Create("white");
private ActionMessage _blackMessage = null!; private ActionMessage _blackMessage = null!;
private ActionMessage _redMessage = null!; private ActionMessage _redMessage = null!;
private IActionService _service = null!; private IActionService _service = null!;
private Queue<DirectionalPosition> _spawns = null!;
private ActionMessage _whiteMessage = null!; private ActionMessage _whiteMessage = null!;
private IWebSocketService _wssSub = null!; private IWebSocketService _wssSub = null!;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_spawns = CreateQueue();
_whiteMessage = new ActionMessage _whiteMessage = new ActionMessage
{ Action = GameAction.PlayerInfo, Data = JsonSerializer.Serialize(_whitePlayer) }; {
Action = GameAction.PlayerInfo,
Data = JsonSerializer.Serialize(new { Player = _whitePlayer, Spawns = CreateQueue() })
};
_blackMessage = new ActionMessage _blackMessage = new ActionMessage
{ Action = GameAction.PlayerInfo, Data = JsonSerializer.Serialize(_blackPlayer) }; {
_redMessage = new ActionMessage { Action = GameAction.PlayerInfo, Data = JsonSerializer.Serialize(_redPlayer) }; Action = GameAction.PlayerInfo,
Data = JsonSerializer.Serialize(new { Player = _blackPlayer, Spawns = CreateQueue() })
};
_redMessage = new ActionMessage
{
Action = GameAction.PlayerInfo,
Data = JsonSerializer.Serialize(new { Player = _redPlayer, Spawns = CreateQueue() })
};
_wssSub = Substitute.For<WebSocketService>(Substitute.For<ILogger<WebSocketService>>()); _wssSub = Substitute.For<WebSocketService>(Substitute.For<ILogger<WebSocketService>>());
_service = new ActionService(Substitute.For<ILogger<ActionService>>(), _wssSub); _service = new ActionService(Substitute.For<ILogger<ActionService>>(), _wssSub);
} }
private static Queue<DirectionalPosition> CreateQueue() =>
new(new[]
{
new DirectionalPosition { At = new Position { X = 3, Y = 3 }, Direction = Direction.Up },
new() { At = new Position { X = 7, Y = 7 }, Direction = Direction.Down }
});
#region RollDice() #region RollDice()
[Test] [Test]
@ -74,6 +96,9 @@ public class ActionServiceTests
{ {
var players = _service.SetPlayerInfo(_whiteMessage); var players = _service.SetPlayerInfo(_whiteMessage);
var pos = _spawns.Dequeue();
_whitePlayer.PacMan.Position = pos;
_whitePlayer.PacMan.SpawnPosition = pos;
Assert.That(new List<IPlayer> { _whitePlayer }, Is.EqualTo(players)); Assert.That(new List<IPlayer> { _whitePlayer }, Is.EqualTo(players));
} }
@ -140,7 +165,7 @@ public class ActionServiceTests
[Test] [Test]
public void Ready_TwoReady() public void Ready_TwoReady()
{ {
var group = new GameGroup { Players = { _blackPlayer, _whitePlayer } }; var group = new GameGroup(new Queue<DirectionalPosition>()) { Players = { _blackPlayer, _whitePlayer } };
_service.Group = group; _service.Group = group;
_service.Player = _blackPlayer; _service.Player = _blackPlayer;

@ -9,17 +9,34 @@ namespace BackendTests.Services;
public class GameGroupTests public class GameGroupTests
{ {
private readonly DirectionalPosition _spawn3By3Up = new()
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
private readonly DirectionalPosition _spawn7By7Down = new()
{ At = new Position { X = 7, Y = 7 }, Direction = Direction.Down };
private readonly DirectionalPosition _spawn7By7Left = new()
{ At = new Position { X = 7, Y = 7 }, Direction = Direction.Left };
private readonly DirectionalPosition _spawn7By7Right = new()
{ At = new Position { X = 7, Y = 7 }, Direction = Direction.Right };
private IPlayer _bluePlayer = null!; private IPlayer _bluePlayer = null!;
private GameGroup _gameGroup = null!; private GameGroup _gameGroup = null!;
private IPlayer _greenPlayer = null!; private IPlayer _greenPlayer = null!;
private IPlayer _purplePlayer = null!; private IPlayer _purplePlayer = null!;
private IPlayer _redPlayer = null!; private IPlayer _redPlayer = null!;
private Queue<DirectionalPosition> _spawns = null!;
private IPlayer _yellowPlayer = null!; private IPlayer _yellowPlayer = null!;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_gameGroup = new GameGroup(); _spawns = new Queue<DirectionalPosition>(
new[] { _spawn3By3Up, _spawn7By7Left, _spawn7By7Down, _spawn7By7Right });
_gameGroup = new GameGroup(_spawns);
_redPlayer = Players.Create("red"); _redPlayer = Players.Create("red");
_bluePlayer = Players.Create("blue"); _bluePlayer = Players.Create("blue");
_yellowPlayer = Players.Create("yellow"); _yellowPlayer = Players.Create("yellow");
@ -70,6 +87,14 @@ public class GameGroupTests
Assert.That(_redPlayer.State, Is.EqualTo(State.WaitingForPlayers)); Assert.That(_redPlayer.State, Is.EqualTo(State.WaitingForPlayers));
} }
[Test]
public void AddPlayer_AddSpawnPosition()
{
_gameGroup.AddPlayer(_redPlayer);
Assert.That(_redPlayer.PacMan.SpawnPosition, Is.Not.Null);
Assert.That(_redPlayer.PacMan.SpawnPosition, Is.EqualTo(_spawn3By3Up));
}
#endregion #endregion
#region Sendtoall(ArraySegment<byte> segment) #region Sendtoall(ArraySegment<byte> segment)

@ -11,12 +11,25 @@ namespace BackendTests.Services;
public class WebSocketServiceTests public class WebSocketServiceTests
{ {
private readonly DirectionalPosition _spawn3By3Up = new()
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
private IWebSocketService _service = null!; private IWebSocketService _service = null!;
private Queue<DirectionalPosition> _spawns = null!;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
_service = new WebSocketService(Substitute.For<ILogger<WebSocketService>>()); _service = new WebSocketService(Substitute.For<ILogger<WebSocketService>>());
_spawns = new Queue<DirectionalPosition>(new[]
{
_spawn3By3Up,
new DirectionalPosition { At = new Position { X = 7, Y = 7 }, Direction = Direction.Down },
new DirectionalPosition { At = new Position { X = 7, Y = 7 }, Direction = Direction.Down },
new DirectionalPosition { At = new Position { X = 7, Y = 7 }, Direction = Direction.Down }
});
} }
#region Send(Websocket, ArraySegment<byte>) #region Send(Websocket, ArraySegment<byte>)
@ -135,7 +148,7 @@ public class WebSocketServiceTests
public void AddPlayer_ToEmptyGroup() public void AddPlayer_ToEmptyGroup()
{ {
var player = Players.Create("white"); var player = Players.Create("white");
var group = _service.AddPlayer(player); var group = _service.AddPlayer(player, _spawns);
Assert.Multiple(() => Assert.Multiple(() =>
{ {
@ -151,12 +164,12 @@ public class WebSocketServiceTests
for (var i = 0; i < 4; i++) for (var i = 0; i < 4; i++)
{ {
var player = Players.Create(i.ToString()); var player = Players.Create(i.ToString());
_service.AddPlayer(player); _service.AddPlayer(player, _spawns);
} }
var player5 = Players.Create("white"); var player5 = Players.Create("white");
var group = _service.AddPlayer(player5); var group = _service.AddPlayer(player5, new Queue<DirectionalPosition>(new[] { _spawn3By3Up }));
Assert.Multiple(() => Assert.Multiple(() =>
{ {

@ -4,10 +4,9 @@ import findPossiblePositions from "../game/possibleMovesAlgorithm";
import {GameTile} from "./gameTile"; import {GameTile} from "./gameTile";
import {TileType} from "../game/tileType"; import {TileType} from "../game/tileType";
import {useAtomValue} from "jotai"; import {useAtomValue} from "jotai";
import {allCharactersAtom} from "../utils/state"; import {allCharactersAtom, selectedDiceAtom} from "../utils/state";
interface BoardProps extends ComponentProps { interface BoardProps extends ComponentProps {
selectedDice?: SelectedDice,
onMove?: Action<Position[]>, onMove?: Action<Position[]>,
map: GameMap map: GameMap
} }
@ -15,12 +14,12 @@ interface BoardProps extends ComponentProps {
const Board: Component<BoardProps> = ( const Board: Component<BoardProps> = (
{ {
className, className,
selectedDice,
onMove, onMove,
map map
}) => { }) => {
const characters = useAtomValue(allCharactersAtom); const characters = useAtomValue(allCharactersAtom);
const selectedDice = useAtomValue(selectedDiceAtom);
const [selectedCharacter, setSelectedCharacter] = useState<Character>(); const [selectedCharacter, setSelectedCharacter] = useState<Character>();
const [possiblePositions, setPossiblePositions] = useState<Path[]>([]); // TODO reset when other client moves a character const [possiblePositions, setPossiblePositions] = useState<Path[]>([]); // TODO reset when other client moves a character
const [hoveredPosition, setHoveredPosition] = useState<Path>(); const [hoveredPosition, setHoveredPosition] = useState<Path>();
@ -63,15 +62,15 @@ const Board: Component<BoardProps> = (
const pacMan = selectedCharacter as PacMan; const pacMan = selectedCharacter as PacMan;
for (const tile of [...destination.Path ?? [], destination.End]) { for (const tile of [...destination.Path ?? [], destination.End]) {
const currentTile = map[tile.y][tile.x]; const currentTile = map[tile.Y][tile.X];
if (currentTile === TileType.pellet) { if (currentTile === TileType.pellet) {
// pacMan.box.addPellet(new Pellet()); // TODO update to current player // pacMan.box.addPellet(new Pellet()); // TODO update to current player
map[tile.y][tile.x] = TileType.empty; map[tile.Y][tile.X] = TileType.empty;
positions.push(tile); positions.push(tile);
} else if (currentTile === TileType.powerPellet) { } else if (currentTile === TileType.powerPellet) {
// pacMan.box.addPellet(new Pellet(true)); // pacMan.box.addPellet(new Pellet(true));
map[tile.y][tile.x] = TileType.empty; map[tile.Y][tile.X] = TileType.empty;
positions.push(tile); positions.push(tile);
} }
} }
@ -98,9 +97,9 @@ const Board: Component<BoardProps> = (
<GameTile <GameTile
key={colIndex + rowIndex * colIndex} key={colIndex + rowIndex * colIndex}
type={tile} type={tile}
possiblePath={possiblePositions.find(p => p.End.x === colIndex && p.End.y === rowIndex)} possiblePath={possiblePositions.find(p => p.End.X === colIndex && p.End.Y === rowIndex)}
character={characters.find(c => c.isAt({x: colIndex, y: rowIndex}))} character={characters.find(c => c.isAt({X: colIndex, Y: rowIndex}))}
isSelected={selectedCharacter?.isAt({x: colIndex, y: rowIndex})} isSelected={selectedCharacter?.isAt({X: colIndex, Y: rowIndex})}
showPath={hoveredPosition?.Path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined} showPath={hoveredPosition?.Path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined}
handleMoveCharacter={handleMoveCharacter} handleMoveCharacter={handleMoveCharacter}
handleSelectCharacter={handleSelectCharacter} handleSelectCharacter={handleSelectCharacter}

@ -3,11 +3,12 @@ import {AllDice} from "./dice";
import {doAction, GameAction} from "../utils/actions"; import {doAction, GameAction} from "../utils/actions";
import GameBoard from "./gameBoard"; import GameBoard from "./gameBoard";
import WebSocketService from "../websockets/WebSocketService"; import WebSocketService from "../websockets/WebSocketService";
import {testMap} from "../game/map"; import {getCharacterSpawns, testMap} from "../game/map";
import Player, {State} from "../game/player"; import Player, {State} from "../game/player";
import PlayerStats from "../components/playerStats"; import PlayerStats from "../components/playerStats";
import {getDefaultStore, useAtom, useAtomValue} from "jotai"; import {getDefaultStore, useAtom, useAtomValue} from "jotai";
import {currentPlayerAtom, diceAtom, ghostsAtom, playersAtom, selectedDiceAtom} from "../utils/state"; import {currentPlayerAtom, diceAtom, ghostsAtom, playersAtom, selectedDiceAtom} from "../utils/state";
import {CharacterType} from "../game/character";
const wsService = new WebSocketService(import.meta.env.VITE_API); const wsService = new WebSocketService(import.meta.env.VITE_API);
@ -43,7 +44,19 @@ export const GameComponent: Component<{ player: Player }> = ({player}) => {
} }
async function sendPlayer(): Promise<void> { async function sendPlayer(): Promise<void> {
wsService.send({Action: GameAction.playerInfo, Data: player}); // TODO set spawn position and position
wsService.send({
Action: GameAction.playerInfo,
Data: {
Player: player, Spawns: getCharacterSpawns(testMap)
.filter(s => s.type === CharacterType.pacMan)
.map(s => s.position)
}
});
}
function sendReady(): void {
wsService.send({Action: GameAction.ready});
} }
useEffect(() => { useEffect(() => {
@ -55,10 +68,6 @@ export const GameComponent: Component<{ player: Player }> = ({player}) => {
return () => wsService.close(); return () => wsService.close();
}, []); }, []);
function sendReady(): void {
wsService.send({Action: GameAction.ready});
}
return ( return (
<> <>
<div className={"flex-center"}> <div className={"flex-center"}>
@ -70,9 +79,7 @@ export const GameComponent: Component<{ player: Player }> = ({player}) => {
</div> </div>
<AllDice values={dice} selectedDiceIndex={selectedDice?.index}/> <AllDice values={dice} selectedDiceIndex={selectedDice?.index}/>
{players?.map(p => <PlayerStats key={p.Name} player={p} isCurrentPlayer={currentPlayer?.Name === p.Name}/>)} {players?.map(p => <PlayerStats key={p.Name} player={p} isCurrentPlayer={currentPlayer?.Name === p.Name}/>)}
<GameBoard className={"mx-auto my-2"} <GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={testMap}/>
selectedDice={selectedDice}
onMove={onCharacterMove} map={testMap}/>
</> </>
); );
}; };

@ -3,6 +3,7 @@ import {TileType} from "../game/tileType";
import {Character, Dummy} from "../game/character"; import {Character, Dummy} from "../game/character";
import {Direction} from "../game/direction"; import {Direction} from "../game/direction";
import {getCSSColour} from "../utils/colours"; import {getCSSColour} from "../utils/colours";
import {Colour} from "../game/colour";
interface TileWithCharacterProps extends ComponentProps { interface TileWithCharacterProps extends ComponentProps {
possiblePath?: Path, possiblePath?: Path,
@ -113,8 +114,8 @@ const Tile: Component<TileProps> = (
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}> onMouseLeave={onMouseLeave}>
{children} {children}
{type === TileType.pellet && <Circle colour={"yellow"}/>} {type === TileType.pellet && <Circle colour={Colour.Yellow}/>}
{type === TileType.powerPellet && <Circle colour={"red"}/>} {type === TileType.powerPellet && <Circle colour={Colour.Red}/>}
</div> </div>
); );
}; };
@ -146,7 +147,7 @@ const CharacterComponent: Component<CharacterComponentProps> = (
}) => { }) => {
function getSide() { function getSide() {
switch (character?.Position.Direction) { switch (character?.Position?.Direction) {
case Direction.up: case Direction.up:
return "right-1/4 top-0"; return "right-1/4 top-0";
case Direction.down: case Direction.down:

@ -57,7 +57,7 @@ export class Character {
} }
public isAt(position: Position): boolean { public isAt(position: Position): boolean {
return this.Position !== null && this.Position.End.x === position.x && this.Position.End.y === position.y; return this.Position !== null && this.Position.End.X === position.X && this.Position.End.Y === position.Y;
} }
} }
@ -83,7 +83,7 @@ export class Dummy extends Character {
Colour: Colour.Grey, Colour: Colour.Grey,
Position: position, Position: position,
IsEatable: false, IsEatable: false,
SpawnPosition: {At: {x: 0, y: 0}, Direction: Direction.up}, SpawnPosition: {At: {X: 0, Y: 0}, Direction: Direction.up},
Type: CharacterType.dummy, Type: CharacterType.dummy,
}); });
} }

@ -30,10 +30,10 @@ export function getCharacterSpawns(map: GameMap): { type: CharacterType, positio
for (let col = 0; col < map.length; col++) { for (let col = 0; col < map.length; col++) {
// TODO find direction // TODO find direction
if (map[row][col] === 4) { if (map[row][col] === 4) {
result.push({type: CharacterType.ghost, position: {At: {x: col, y: row}, Direction: Direction.up}}); result.push({type: CharacterType.ghost, position: {At: {X: col, Y: row}, Direction: Direction.up}});
} else if (map[row][col] === 5) { } else if (map[row][col] === 5) {
result.push({ result.push({
type: CharacterType.pacMan, position: {At: {x: col, y: row}, Direction: Direction.up} type: CharacterType.pacMan, position: {At: {X: col, Y: row}, Direction: Direction.up}
}); });
} }
} }

@ -108,23 +108,23 @@ function tryMove(board: GameMap, path: Path, direction: Direction, steps: number
switch (direction) { switch (direction) {
case Direction.left: case Direction.left:
return { return {
x: path.End.x - 1, X: path.End.X - 1,
y: path.End.y Y: path.End.Y
}; };
case Direction.up: case Direction.up:
return { return {
x: path.End.x, X: path.End.X,
y: path.End.y - 1 Y: path.End.Y - 1
}; };
case Direction.right: case Direction.right:
return { return {
x: path.End.x + 1, X: path.End.X + 1,
y: path.End.y Y: path.End.Y
}; };
case Direction.down: case Direction.down:
return { return {
x: path.End.x, X: path.End.X,
y: path.End.y + 1 Y: path.End.Y + 1
}; };
} }
} }
@ -150,8 +150,8 @@ function addTeleportationTiles(board: GameMap, currentPath: Path, steps: number,
const possiblePositions = findTeleportationTiles(board); const possiblePositions = findTeleportationTiles(board);
const paths: Path[] = []; const paths: Path[] = [];
for (const pos of possiblePositions) { for (const pos of possiblePositions) {
if (pos.End.x !== interval(0, board.length - 1, currentPath.End.x) || if (pos.End.X !== interval(0, board.length - 1, currentPath.End.X) ||
pos.End.y !== interval(0, board.length - 1, currentPath.End.y)) { pos.End.Y !== interval(0, board.length - 1, currentPath.End.Y)) {
pos.Path = currentPath.Path; pos.Path = currentPath.Path;
paths.push(...findPossibleRecursive(board, pos, steps, character, characters)); paths.push(...findPossibleRecursive(board, pos, steps, character, characters));
@ -193,7 +193,7 @@ function findTeleportationTiles(board: GameMap): Path[] {
*/ */
function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: number): void { function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: number): void {
if (board[x][y] !== TileType.wall) { if (board[x][y] !== TileType.wall) {
possiblePositions.push({End: {x, y}, Direction: findDirection(x, y, board.length)}); possiblePositions.push({End: {X: x, Y: y}, Direction: findDirection(x, y, board.length)});
} }
} }
@ -224,7 +224,7 @@ function findDirection(x: number, y: number, boardSize: number): Direction {
*/ */
function isOutsideBoard(currentPos: Path, boardSize: number): boolean { function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
const pos = currentPos.End; const pos = currentPos.End;
return pos.x < 0 || pos.x >= boardSize || pos.y < 0 || pos.y >= boardSize; return pos.X < 0 || pos.X >= boardSize || pos.Y < 0 || pos.Y >= boardSize;
} }
/** /**
@ -234,7 +234,7 @@ function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
*/ */
function isWall(board: GameMap, currentPos: Path): boolean { function isWall(board: GameMap, currentPos: Path): boolean {
const pos = currentPos.End; const pos = currentPos.End;
return board[pos.y][pos.x] === TileType.wall; // Shouldn't work, but it does return board[pos.Y][pos.X] === TileType.wall; // Shouldn't work, but it does
} }
/** /**
@ -244,7 +244,7 @@ function isWall(board: GameMap, currentPos: Path): boolean {
*/ */
function isSpawn(board: GameMap, currentPos: Path) { function isSpawn(board: GameMap, currentPos: Path) {
const pos = currentPos.End; const pos = currentPos.End;
return board[pos.y][pos.x] === TileType.pacmanSpawn || board[pos.y][pos.x] === TileType.ghostSpawn; return board[pos.Y][pos.X] === TileType.pacmanSpawn || board[pos.Y][pos.X] === TileType.ghostSpawn;
} }
/** /**
@ -255,6 +255,6 @@ function isSpawn(board: GameMap, currentPos: Path) {
function isOwnSpawn(currentPos: Path, character: Character): boolean { function isOwnSpawn(currentPos: Path, character: Character): boolean {
const pos = currentPos.End; const pos = currentPos.End;
const charPos = character.SpawnPosition!.At; const charPos = character.SpawnPosition!.At;
return charPos.x === pos.x && charPos.y === pos.y; return charPos.X === pos.X && charPos.Y === pos.Y;
} }

@ -20,7 +20,7 @@ type SelectedDice = {
index: number index: number
}; };
type Position = { x: number, y: number }; type Position = { X: number, Y: number };
type GameMap = number[][]; type GameMap = number[][];

@ -89,7 +89,7 @@ function removeEatenPellets(data?: MoveCharacterData): void {
const pellets = data?.EatenPellets; const pellets = data?.EatenPellets;
for (const pellet of pellets ?? []) { for (const pellet of pellets ?? []) {
testMap[pellet.y][pellet.x] = TileType.empty; testMap[pellet.Y][pellet.X] = TileType.empty;
} }
} }

@ -8,7 +8,7 @@ let pacMan: Character;
beforeEach(() => { beforeEach(() => {
pacMan = new PacMan({ pacMan = new PacMan({
Colour: "yellow", SpawnPosition: {At: {x: 3, y: 3}, Direction: Direction.up} Colour: "yellow", SpawnPosition: {At: {X: 3, Y: 3}, Direction: Direction.up}
}); });
}); });
@ -29,21 +29,21 @@ test("Pac-Man rolls two from start, should return one position", () => {
test("Pac-Man rolls three from start, should return two positions", () => { test("Pac-Man rolls three from start, should return two positions", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 3, []); const result = possibleMovesAlgorithm(testMap, pacMan, 3, []);
expect(result.length).toBe(2); expect(result.length).toBe(2);
arrayEquals(result, [{End: {x: 2, y: 1}, Direction: Direction.left, Path: [{x: 3, y: 2}, {x: 3, y: 1}]}, arrayEquals(result, [{End: {X: 2, Y: 1}, Direction: Direction.left, Path: [{X: 3, Y: 2}, {X: 3, Y: 1}]},
{End: {x: 4, y: 1}, Direction: Direction.right, Path: [{x: 3, y: 2}, {x: 3, y: 1}]}]); {End: {X: 4, Y: 1}, Direction: Direction.right, Path: [{X: 3, Y: 2}, {X: 3, Y: 1}]}]);
}); });
test("Pac-Man rolls four from start, should return two positions", () => { test("Pac-Man rolls four from start, should return two positions", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 4, []); const result = possibleMovesAlgorithm(testMap, pacMan, 4, []);
expect(result.length).toBe(2); expect(result.length).toBe(2);
arrayEquals(result, [{ arrayEquals(result, [{
End: {x: 1, y: 1}, End: {X: 1, Y: 1},
Direction: Direction.left, Direction: Direction.left,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 2, Y: 1}]
}, { }, {
End: {x: 5, y: 1}, End: {X: 5, Y: 1},
Direction: Direction.right, Direction: Direction.right,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}]
}]); }]);
}); });
@ -51,21 +51,21 @@ test("Pac-Man rolls five from start, should return four positions", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 5, []); const result = possibleMovesAlgorithm(testMap, pacMan, 5, []);
expect(result.length).toBe(4); expect(result.length).toBe(4);
arrayEquals(result, [{ arrayEquals(result, [{
End: {x: 5, y: 0}, End: {X: 5, Y: 0},
Direction: Direction.up, Direction: Direction.up,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}]
}, { }, {
End: {x: 6, y: 1}, End: {X: 6, Y: 1},
Direction: Direction.right, Direction: Direction.right,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}]
}, { }, {
End: {x: 1, y: 2}, End: {X: 1, Y: 2},
Direction: Direction.down, Direction: Direction.down,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}, {x: 1, y: 1}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 2, Y: 1}, {X: 1, Y: 1}]
}, { }, {
End: {x: 5, y: 2}, End: {X: 5, Y: 2},
Direction: Direction.down, Direction: Direction.down,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}]
} }
]); ]);
}); });
@ -75,72 +75,72 @@ test("Pac-Man rolls six from start, should return six positions", () => {
expect(result.length).toBe(6); expect(result.length).toBe(6);
arrayEquals(result, [ arrayEquals(result, [
{ {
End: {x: 1, y: 3}, End: {X: 1, Y: 3},
Direction: Direction.down, Direction: Direction.down,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}, {x: 1, y: 1}, {x: 1, y: 2}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 2, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 2}]
}, { }, {
End: {x: 0, y: 5}, End: {X: 0, Y: 5},
Direction: Direction.right, Direction: Direction.right,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 5, y: 0}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 0}]
}, { }, {
End: {x: 5, y: 3}, End: {X: 5, Y: 3},
Direction: Direction.down, Direction: Direction.down,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 5, y: 2}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 2}]
}, { }, {
End: {x: 7, y: 1}, End: {X: 7, Y: 1},
Direction: Direction.right, Direction: Direction.right,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 6, y: 1}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}, {X: 6, Y: 1}]
}, { }, {
End: {x: 10, y: 5}, End: {X: 10, Y: 5},
Direction: Direction.left, Direction: Direction.left,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 5, y: 0}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 0}]
}, { }, {
End: {x: 5, y: 10}, End: {X: 5, Y: 10},
Direction: Direction.up, Direction: Direction.up,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 5, y: 0}] Path: [{X: 3, Y: 2}, {X: 3, Y: 1}, {X: 4, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 0}]
} }
]); ]);
}); });
test("Pac-Man rolls four from position [5,1] (right), should return 11", () => { test("Pac-Man rolls four from position [5,1] (right), should return 11", () => {
pacMan.follow({End: {x: 5, y: 1}, Direction: Direction.right}); pacMan.follow({End: {X: 5, Y: 1}, Direction: Direction.right});
const result = possibleMovesAlgorithm(testMap, pacMan, 4, []); const result = possibleMovesAlgorithm(testMap, pacMan, 4, []);
expect(result.length).toBe(11); expect(result.length).toBe(11);
}); });
test("Pac-Man rolls four from position [5,1] (left), should return 12", () => { test("Pac-Man rolls four from position [5,1] (left), should return 12", () => {
pacMan.follow({End: {x: 5, y: 1}, Direction: Direction.left}); pacMan.follow({End: {X: 5, Y: 1}, Direction: Direction.left});
const result = possibleMovesAlgorithm(testMap, pacMan, 4, []); const result = possibleMovesAlgorithm(testMap, pacMan, 4, []);
expect(result.length).toBe(12); expect(result.length).toBe(12);
}); });
test("Pac-Man rolls three from position [1,5] (left), should return 5", () => { test("Pac-Man rolls three from position [1,5] (left), should return 5", () => {
pacMan.follow({End: {x: 1, y: 5}, Direction: Direction.left}); pacMan.follow({End: {X: 1, Y: 5}, Direction: Direction.left});
const result = possibleMovesAlgorithm(testMap, pacMan, 3, []); const result = possibleMovesAlgorithm(testMap, pacMan, 3, []);
arrayEquals(result, [ arrayEquals(result, [
{End: {x: 1, y: 2}, Direction: Direction.up, Path: [{x: 1, y: 4}, {x: 1, y: 3}]}, {End: {X: 1, Y: 2}, Direction: Direction.up, Path: [{X: 1, Y: 4}, {X: 1, Y: 3}]},
{End: {x: 1, y: 8}, Direction: Direction.down, Path: [{x: 1, y: 6}, {x: 1, y: 7}]}, {End: {X: 1, Y: 8}, Direction: Direction.down, Path: [{X: 1, Y: 6}, {X: 1, Y: 7}]},
{End: {x: 5, y: 1}, Direction: Direction.down, Path: [{x: 0, y: 5}, {x: 5, y: 0}]}, {End: {X: 5, Y: 1}, Direction: Direction.down, Path: [{X: 0, Y: 5}, {X: 5, Y: 0}]},
{End: {x: 9, y: 5}, Direction: Direction.left, Path: [{x: 0, y: 5}, {x: 10, y: 5}]}, {End: {X: 9, Y: 5}, Direction: Direction.left, Path: [{X: 0, Y: 5}, {X: 10, Y: 5}]},
{End: {x: 5, y: 9}, Direction: Direction.up, Path: [{x: 0, y: 5}, {x: 5, y: 10}]}, {End: {X: 5, Y: 9}, Direction: Direction.up, Path: [{X: 0, Y: 5}, {X: 5, Y: 10}]},
]); ]);
expect(result.length).toBe(5); expect(result.length).toBe(5);
}); });
test("Pac-Man rolls six from position [1,5] (down), should return 17", () => { test("Pac-Man rolls six from position [1,5] (down), should return 17", () => {
pacMan.follow({End: {x: 1, y: 5}, Direction: Direction.down}); pacMan.follow({End: {X: 1, Y: 5}, Direction: Direction.down});
const result = possibleMovesAlgorithm(testMap, pacMan, 6, []); const result = possibleMovesAlgorithm(testMap, pacMan, 6, []);
expect(result.length).toBe(17); expect(result.length).toBe(17);
}); });
test("Pac-Man rolls six from position [7,1] (right), path to [9,5] should be five tiles long", () => { test("Pac-Man rolls six from position [7,1] (right), path to [9,5] should be five tiles long", () => {
pacMan.follow({End: {x: 7, y: 1}, Direction: Direction.right}); pacMan.follow({End: {X: 7, Y: 1}, Direction: Direction.right});
const result = possibleMovesAlgorithm(testMap, pacMan, 6, []); const result = possibleMovesAlgorithm(testMap, pacMan, 6, []);
expect(result[0].Path?.length).toBe(5); expect(result[0].Path?.length).toBe(5);
}); });
test("Pac-Man rolls 5 from position [9,3] (down), should return 5", () => { test("Pac-Man rolls 5 from position [9,3] (down), should return 5", () => {
pacMan.follow({End: {x: 9, y: 3}, Direction: Direction.down}); pacMan.follow({End: {X: 9, Y: 3}, Direction: Direction.down});
const result = possibleMovesAlgorithm(testMap, pacMan, 5, []); const result = possibleMovesAlgorithm(testMap, pacMan, 5, []);
expect(result.length).toBe(5); expect(result.length).toBe(5);
}); });

@ -9,7 +9,7 @@ namespace pacMan.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class GameController : GenericController public class GameController : GenericController // TODO reconnect using player id
{ {
private readonly IActionService _actionService; private readonly IActionService _actionService;

@ -12,15 +12,10 @@ public enum GameAction
public class ActionMessage<T> public class ActionMessage<T>
{ {
public GameAction Action { get; set; } public GameAction Action { get; init; }
public T? Data { get; set; } public T? Data { get; set; }
public static ActionMessage FromJson(string json) public static ActionMessage FromJson(string json) => JsonSerializer.Deserialize<ActionMessage>(json)!;
{
return JsonSerializer.Deserialize<ActionMessage>(json)!;
}
} }
public class ActionMessage : ActionMessage<dynamic> public class ActionMessage : ActionMessage<dynamic> { }
{
}

@ -2,11 +2,11 @@ namespace pacMan.Game;
public class Character : IEquatable<Character> public class Character : IEquatable<Character>
{ {
public required string Colour { get; set; } public required string Colour { get; init; }
public MovePath? Position { get; set; } public MovePath? Position { get; set; }
public bool IsEatable { get; set; } = true; public bool IsEatable { get; set; } = true;
public DirectionalPosition? SpawnPosition { get; set; } public DirectionalPosition? SpawnPosition { get; set; }
public required CharacterType Type { get; set; } public required CharacterType? Type { get; init; }
public bool Equals(Character? other) public bool Equals(Character? other)
{ {

@ -2,7 +2,7 @@ namespace pacMan.Game.Items;
public class Box : IEquatable<Box> public class Box : IEquatable<Box>
{ {
public required List<Pellet>? Pellets { get; init; } = new(); public List<Pellet>? Pellets { get; init; } = new();
public required string Colour { get; init; } public required string Colour { get; init; }
public int CountNormal => Pellets?.Count(pellet => !pellet.IsPowerPellet) ?? 0; public int CountNormal => Pellets?.Count(pellet => !pellet.IsPowerPellet) ?? 0;

@ -5,7 +5,7 @@ public interface IPlayer
string Name { get; init; } string Name { get; init; }
Character PacMan { get; init; } Character PacMan { get; init; }
string Colour { get; init; } string Colour { get; init; }
Box Box { get; init; } Box? Box { get; init; }
State State { get; set; } State State { get; set; }
} }
@ -29,7 +29,7 @@ public class Player : IPlayer, IEquatable<Player>
public required string Name { get; init; } public required string Name { get; init; }
public required Character PacMan { get; init; } public required Character PacMan { get; init; }
public required string Colour { get; init; } public required string Colour { get; init; }
public required Box Box { get; init; } public Box? Box { get; init; }
public State State { get; set; } = State.WaitingForPlayers; public State State { get; set; } = State.WaitingForPlayers;
public override bool Equals(object? obj) public override bool Equals(object? obj)

@ -1,16 +1,55 @@
namespace pacMan.Game; namespace pacMan.Game;
public class MovePath public class MovePath : IEquatable<MovePath>
{ {
public Position[]? Path { get; set; } public Position[]? Path { get; set; }
public required Position End { get; set; } public required Position End { get; init; }
public required Direction Direction { get; set; } public required Direction Direction { get; init; }
public bool Equals(MovePath? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Equals(Path, other.Path) && End.Equals(other.End) && Direction == other.Direction;
}
public static implicit operator MovePath(DirectionalPosition path) =>
new()
{
End = path.At,
Direction = path.Direction
};
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((MovePath)obj);
}
public override int GetHashCode() => HashCode.Combine(Path, End, (int)Direction);
} }
public class Position public class Position : IEquatable<Position>
{ {
public int X { get; set; } = 0; public int X { get; init; }
public int Y { get; set; } = 0; public int Y { get; init; }
public bool Equals(Position? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return X == other.X && Y == other.Y;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((Position)obj);
}
public override int GetHashCode() => HashCode.Combine(X, Y);
} }
public enum Direction public enum Direction
@ -21,8 +60,31 @@ public enum Direction
Down Down
} }
public class DirectionalPosition public class DirectionalPosition : IEquatable<DirectionalPosition>
{ {
public required Position At { get; set; } public required Position At { get; init; }
public required Direction Direction { get; set; } public required Direction Direction { get; init; }
}
public bool Equals(DirectionalPosition? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return At.Equals(other.At) && Direction == other.Direction;
}
public static explicit operator DirectionalPosition(MovePath path) =>
new()
{
At = path.End,
Direction = path.Direction
};
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((DirectionalPosition)obj);
}
public override int GetHashCode() => HashCode.Combine(At, (int)Direction);
}

@ -1,4 +1,5 @@
using System.Net.WebSockets; using System.Net.WebSockets;
using pacMan.Game;
using pacMan.Game.Items; using pacMan.Game.Items;
using pacMan.Services; using pacMan.Services;
@ -13,5 +14,5 @@ public interface IWebSocketService
void SendToAll(ArraySegment<byte> segment); void SendToAll(ArraySegment<byte> segment);
Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer); Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer);
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription); Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription);
GameGroup AddPlayer(IPlayer player); GameGroup AddPlayer(IPlayer player, Queue<DirectionalPosition> spawns);
} }

@ -29,7 +29,7 @@ public class ActionService : IActionService
_wsService = wsService; _wsService = wsService;
} }
public GameGroup Group { get; set; } = new(); public GameGroup? Group { get; set; }
public IPlayer? Player { get; set; } public IPlayer? Player { get; set; }
@ -56,14 +56,16 @@ public class ActionService : IActionService
{ {
try try
{ {
// Receieved JsonElement from frontend // Receieving JsonElement from frontend
Player = JsonSerializer.Deserialize<Player>(message.Data); PlayerInfoData data = JsonSerializer.Deserialize<PlayerInfoData>(message.Data);
Group = _wsService.AddPlayer(Player); Player = data.Player;
Group = _wsService.AddPlayer(Player, data.Spawns);
} }
catch (RuntimeBinderException e) catch (RuntimeBinderException e)
{ {
Console.WriteLine(e); Console.WriteLine(e);
if (message.Data == null) throw new NullReferenceException(); if (message.Data is null) throw new NullReferenceException();
throw; throw;
} }
@ -74,7 +76,7 @@ public class ActionService : IActionService
public object Ready() public object Ready()
{ {
object data; object data;
if (Player != null) if (Player != null && Group != null)
{ {
var players = Group.SetReady(Player).ToArray(); var players = Group.SetReady(Player).ToArray();
if (players.All(p => p.State == State.Ready)) if (players.All(p => p.State == State.Ready))
@ -96,3 +98,9 @@ public class ActionService : IActionService
return data; return data;
} }
} }
public class PlayerInfoData
{
public required Player Player { get; set; }
public required Queue<DirectionalPosition> Spawns { get; set; }
}

@ -8,14 +8,20 @@ namespace pacMan.Services;
public class GameGroup : IEnumerable<IPlayer> public class GameGroup : IEnumerable<IPlayer>
{ {
private readonly Random _random = new(); private readonly Random _random = new();
public GameGroup(Queue<DirectionalPosition> spawns) => Spawns = spawns;
public List<IPlayer> Players { get; } = new(); public List<IPlayer> Players { get; } = new();
private Queue<DirectionalPosition> Spawns { get; }
public IPlayer RandomPlayer => Players[_random.Next(Count)]; public IPlayer RandomPlayer => Players[_random.Next(Count)];
public int Count => Players.Count; public int Count => Players.Count;
public IEnumerator<IPlayer> GetEnumerator() => Players.GetEnumerator(); public IEnumerator<IPlayer> GetEnumerator() => Players.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public event Func<ArraySegment<byte>, Task>? Connections; public event Func<ArraySegment<byte>, Task>? Connections;
public bool AddPlayer(IPlayer player) // TODO if name exists, use that player instead public bool AddPlayer(IPlayer player) // TODO if name exists, use that player instead
@ -25,9 +31,18 @@ public class GameGroup : IEnumerable<IPlayer>
player.State = State.WaitingForPlayers; player.State = State.WaitingForPlayers;
if (Players.Exists(p => p.Name == player.Name)) return true; if (Players.Exists(p => p.Name == player.Name)) return true;
Players.Add(player); Players.Add(player);
if (player.PacMan.SpawnPosition is null) SetSpawn(player);
return true; return true;
} }
private void SetSpawn(IPlayer player)
{
if (player.PacMan.SpawnPosition is not null) return;
var spawn = Spawns.Dequeue();
player.PacMan.SpawnPosition = spawn;
player.PacMan.Position = spawn;
}
public void SendToAll(ArraySegment<byte> segment) => Connections?.Invoke(segment); public void SendToAll(ArraySegment<byte> segment) => Connections?.Invoke(segment);
public IEnumerable<IPlayer> SetReady(IPlayer player) public IEnumerable<IPlayer> SetReady(IPlayer player)

@ -1,4 +1,5 @@
using System.Net.WebSockets; using System.Net.WebSockets;
using pacMan.Game;
using pacMan.Game.Items; using pacMan.Game.Items;
using pacMan.Interfaces; using pacMan.Interfaces;
using pacMan.Utils; using pacMan.Utils;
@ -56,7 +57,7 @@ public class WebSocketService : IWebSocketService
_logger.Log(LogLevel.Information, "WebSocket connection closed"); _logger.Log(LogLevel.Information, "WebSocket connection closed");
} }
public GameGroup AddPlayer(IPlayer player) public GameGroup AddPlayer(IPlayer player, Queue<DirectionalPosition> spawns)
{ {
var index = 0; var index = 0;
try try
@ -65,7 +66,7 @@ public class WebSocketService : IWebSocketService
} }
catch (ArgumentOutOfRangeException) catch (ArgumentOutOfRangeException)
{ {
var game = new GameGroup(); var game = new GameGroup(spawns);
game.AddPlayer(player); game.AddPlayer(player);
Games.Add(game); Games.Add(game);
} }