Added groups in backend, refactored property names

This commit is contained in:
martin 2023-06-24 19:43:03 +02:00
parent fbe9594192
commit c469b92739
27 changed files with 393 additions and 254 deletions

View File

@ -1,10 +1,8 @@
import React, {useEffect, useState} from "react";
import {Character, PacMan} from "../game/character";
import findPossiblePositions from "../game/possibleMovesAlgorithm";
import {Direction} from "../game/direction";
import {GameTile} from "./gameTile";
import {TileType} from "../game/tileType";
import Pellet from "../game/pellet";
interface BoardProps extends ComponentProps {
characters: Character[],
@ -49,9 +47,9 @@ const Board: Component<BoardProps> = (
setSelectedCharacter(undefined);
}
}
function tryMovePacManToSpawn(destination: Path): void {
const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.end));
const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.End));
if (takenChar) {
takenChar.moveToSpawn();
// TODO steal from player
@ -63,15 +61,15 @@ const Board: Component<BoardProps> = (
if (selectedCharacter instanceof 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];
if (currentTile === TileType.pellet) {
pacMan.box.addPellet(new Pellet());
// pacMan.box.addPellet(new Pellet()); // TODO update to current player
map[tile.y][tile.x] = TileType.empty;
positions.push(tile);
} else if (currentTile === TileType.powerPellet) {
pacMan.box.addPellet(new Pellet(true));
// pacMan.box.addPellet(new Pellet(true));
map[tile.y][tile.x] = TileType.empty;
positions.push(tile);
}
@ -99,10 +97,10 @@ const Board: Component<BoardProps> = (
<GameTile
key={colIndex + rowIndex * colIndex}
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}))}
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}
handleSelectCharacter={handleSelectCharacter}
handleStartShowPath={handleShowPath}

View File

@ -11,20 +11,20 @@ import Player from "../game/player";
const wsService = new WebSocketService("wss://localhost:3000/api/game");
export const GameComponent: Component<{ player: Player }> = ({player = new Player({colour: "yellow"})}) => {
export const GameComponent: Component<{ player: Player }> = (
{
player = new Player({
name: "Martin",
colour: "yellow",
})
}) => {
// TODO find spawn points
const [characters, setCharacters] = useState([
new PacMan({
colour: "yellow", spawnPosition: {at: {x: 3, y: 3}, direction: Direction.up}
}),
new PacMan({
colour: "blue", spawnPosition: {at: {x: 7, y: 7}, direction: Direction.down}
new Ghost({
colour: "purple", spawnPosition: {At: {x: 7, y: 3}, Direction: Direction.up}
}),
new Ghost({
colour: "purple", spawnPosition: {at: {x: 7, y: 3}, direction: Direction.up}
}),
new Ghost({
colour: "purple", spawnPosition: {at: {x: 3, y: 7}, direction: Direction.down}
colour: "purple", spawnPosition: {At: {x: 3, y: 7}, Direction: Direction.down}
})
]);
@ -63,6 +63,10 @@ export const GameComponent: Component<{ player: Player }> = ({player = new Playe
updateCharacters(parsed);
removeEatenPellets(parsed);
break;
case GameAction.playerInfo:
const players = parsed.Data as Player[];
// TODO set all characters
break;
}
}
@ -102,11 +106,16 @@ export const GameComponent: Component<{ player: Player }> = ({player = new Playe
wsService.send(data);
}
async function sendPlayer(): Promise<void> {
await wsService.waitForOpen();
wsService.send({Action: GameAction.playerInfo, Data: player});
}
useEffect(() => {
wsService.onReceive = doAction;
wsService.open();
// TODO send player info to backend
void sendPlayer();
// TODO send action to backend when all players are ready
// The backend should then send the first player as current player
return () => wsService.close();
@ -121,10 +130,10 @@ export const GameComponent: Component<{ player: Player }> = ({player = new Playe
<AllDice values={dice} onclick={handleDiceClick} selectedDiceIndex={selectedDice?.index}/>
{
(characters.filter(c => c instanceof PacMan) as PacMan[]).map(c =>
<div key={c.colour} className={"mx-auto w-fit m-2"}>
<p>Player: {player.colour}</p>
<p>Pellets: {player.box.count}</p>
<p>PowerPellets: {player.box.countPowerPellets}</p>
<div key={c.Colour} className={"mx-auto w-fit m-2"}>
<p className={currentPlayer === player ? "underline" : ""}>Player: {player.Colour}</p>
<p>Pellets: {player.Box.count}</p>
<p>PowerPellets: {player.Box.countPowerPellets}</p>
</div>)
}
<GameBoard className={"mx-auto my-2"} characters={characters} selectedDice={selectedDice}

View File

@ -28,7 +28,7 @@ export const GameTile: Component<TileWithCharacterProps> = (
isSelected = false,
showPath = false
}) => (
<Tile className={`${possiblePath?.end ? "border-4 border-white" : ""}`}
<Tile className={`${possiblePath?.End ? "border-4 border-white" : ""}`}
type={type}
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}
@ -146,7 +146,7 @@ const CharacterComponent: Component<CharacterComponentProps> = (
}) => {
function getSide() {
switch (character?.position.direction) {
switch (character?.Position.Direction) {
case Direction.up:
return "right-1/4 top-0";
case Direction.down:
@ -162,7 +162,7 @@ const CharacterComponent: Component<CharacterComponentProps> = (
return (
<div className={`rounded-full w-4/5 h-4/5 cursor-pointer hover:border border-black relative ${className}`}
style={{backgroundColor: `${character.colour}`}}
style={{backgroundColor: `${character.Colour}`}}
onClick={() => onClick?.(character)}>
<div>
<div className={`absolute ${getSide()} w-1/2 h-1/2 rounded-full bg-black`}/>

View File

@ -1,28 +1,28 @@
import Pellet from "./pellet";
export default class Box {
public pellets: Pellet[];
public readonly colour: Colour;
public Pellets: Pellet[];
public readonly Colour: Colour;
public constructor({colour, pellets = []}: BoxProps) {
this.colour = colour;
this.pellets = pellets;
}
public addPellet(pellet: Pellet): void {
this.pellets.push(pellet);
this.Colour = colour;
this.Pellets = pellets;
}
get powerPellet(): Pellet | undefined {
return this.pellets.find(pellet => pellet.isPowerPellet);
return this.Pellets.find(pellet => pellet.isPowerPellet);
}
get count(): number {
return this.pellets.filter(pellet => !pellet.isPowerPellet).length;
return this.Pellets.filter(pellet => !pellet.isPowerPellet).length;
}
get countPowerPellets(): number {
return this.pellets.filter(pellet => pellet.isPowerPellet).length;
return this.Pellets.filter(pellet => pellet.isPowerPellet).length;
}
public addPellet(pellet: Pellet): void {
this.Pellets.push(pellet);
}
}

View File

@ -1,53 +1,62 @@
import {Direction} from "./direction";
export enum CharacterType {
pacMan = "pacMan",
ghost = "ghost",
dummy = "dummy",
pacMan,
ghost,
dummy,
}
export class Character {
public readonly colour: Colour;
public position: Path;
public isEatable: boolean;
public readonly spawnPosition: DirectionalPosition;
public readonly type: CharacterType;
public readonly Colour: Colour;
public Position: Path | null;
public IsEatable: boolean;
public readonly SpawnPosition: DirectionalPosition | null;
public readonly Type: CharacterType;
public constructor(
{
colour,
position,
position = null,
type = CharacterType.dummy,
isEatable = type === CharacterType.pacMan,
spawnPosition
spawnPosition = null
}: CharacterProps) {
this.colour = colour;
this.position = position ?? {end: spawnPosition.at, direction: spawnPosition.direction};
this.isEatable = isEatable;
this.spawnPosition = spawnPosition;
this.type = type;
this.Colour = colour;
this.IsEatable = isEatable;
this.SpawnPosition = spawnPosition;
this.Position = position ?? spawnPosition ? {
End: spawnPosition!.At,
Direction: spawnPosition!.Direction
} : null;
this.Type = type;
}
public follow(path: Path): void {
this.position.end = path.end;
this.position.direction = path.direction;
this.position.path = undefined;
if (!this.Position) {
this.Position = path;
} else {
this.Position.End = path.End;
this.Position.Direction = path.Direction;
this.Position.Path = undefined;
}
}
public isPacMan(): boolean {
return this.type === CharacterType.pacMan;
return this.Type === CharacterType.pacMan;
}
public isGhost(): boolean {
return this.type === CharacterType.ghost;
return this.Type === CharacterType.ghost;
}
public moveToSpawn(): void {
this.follow({end: this.spawnPosition.at, direction: this.spawnPosition.direction});
if (!this.SpawnPosition) return;
this.follow({End: this.SpawnPosition.At, Direction: this.SpawnPosition.Direction});
}
public isAt(position: Position): boolean {
return 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;
}
}
@ -73,7 +82,7 @@ export class Dummy extends Character {
colour: "grey",
position,
isEatable: false,
spawnPosition: {at: {x: 0, y: 0}, direction: Direction.up},
spawnPosition: {At: {x: 0, y: 0}, Direction: Direction.up},
type: CharacterType.dummy,
});
}

View File

@ -1,27 +1,27 @@
import {Character, CharacterType} from "./character";
import Box from "./box";
import {Direction} from "./direction";
export default class Player {
public readonly pacMan: Character;
public readonly colour: Colour;
public readonly box: Box;
public readonly Name: string;
public readonly PacMan: Character;
public readonly Colour: Colour;
public readonly Box: Box;
constructor(props: PlayerProps) {
this.colour = props.colour;
this.box = new Box(props.box ?? {colour: props.colour});
this.pacMan = new Character(props.pacMan ?? {
this.Name = props.name;
this.Colour = props.colour;
this.Box = new Box(props.box ?? {colour: props.colour});
this.PacMan = new Character(props.pacMan ?? {
colour: props.colour,
spawnPosition: {at: {x: 0, y: 0}, direction: Direction.up},
type: CharacterType.pacMan
});
}
public stealFrom(other: Player): void {
for (let i = 0; i < 2; i++) {
const pellet = other.box.pellets.pop();
const pellet = other.Box.Pellets.pop();
if (pellet)
this.box.addPellet(pellet);
this.Box.addPellet(pellet);
}
}

View File

@ -13,7 +13,7 @@ import {Direction, getDirections} from "./direction";
* @returns An array of paths the character can move to
*/
export default function findPossiblePositions(board: GameMap, character: Character, steps: number, characters: Character[]): Path[] {
return findPossibleRecursive(board, character.position, steps, character, characters);
return findPossibleRecursive(board, character.Position, steps, character, characters);
}
/**
@ -64,7 +64,7 @@ function findPossibleRecursive(board: GameMap, currentPath: Path, steps: number,
* @returns True if the character is a ghost and hits Pac-Man
*/
function ghostHitsPacMan(character: Character, currentPath: Path, characters: Character[]): boolean {
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.end)) !== undefined;
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.End)) !== undefined;
}
/**
@ -75,7 +75,7 @@ function ghostHitsPacMan(character: Character, currentPath: Path, characters: Ch
* @returns True if the character hits another character
*/
function characterHitsAnotherCharacter(character: Character, currentPath: Path, characters: Character[]): boolean {
return characters.find(c => c !== character && c.isAt(currentPath.end)) !== undefined;
return characters.find(c => c !== character && c.isAt(currentPath.End)) !== undefined;
}
/**
@ -83,10 +83,10 @@ function characterHitsAnotherCharacter(character: Character, currentPath: Path,
* @param currentPos The current path the character is on
*/
function addToPath(currentPos: Path): void {
if (!currentPos.path) {
currentPos.path = [];
} else if (!currentPos.path.includes(currentPos.end)) {
currentPos.path = [...currentPos.path, currentPos.end];
if (!currentPos.Path) {
currentPos.Path = [];
} else if (!currentPos.Path.includes(currentPos.End)) {
currentPos.Path = [...currentPos.Path, currentPos.End];
}
}
@ -107,31 +107,31 @@ function tryMove(board: GameMap, path: Path, direction: Direction, steps: number
switch (direction) {
case Direction.left:
return {
x: path.end.x - 1,
y: path.end.y
x: path.End.x - 1,
y: path.End.y
};
case Direction.up:
return {
x: path.end.x,
y: path.end.y - 1
x: path.End.x,
y: path.End.y - 1
};
case Direction.right:
return {
x: path.end.x + 1,
y: path.end.y
x: path.End.x + 1,
y: path.End.y
};
case Direction.down:
return {
x: path.end.x,
y: path.end.y + 1
x: path.End.x,
y: path.End.y + 1
};
}
}
if (path.direction !== (direction + 2) % 4) {
if (path.Direction !== (direction + 2) % 4) {
// TODO getNewPosition() and check if a character is on the new position
return findPossibleRecursive(board, {
end: getNewPosition(), direction: direction, path: path.path
End: getNewPosition(), Direction: direction, Path: path.Path
}, steps, character, characters);
}
return [];
@ -149,10 +149,10 @@ function addTeleportationTiles(board: GameMap, currentPath: Path, steps: number,
const possiblePositions = findTeleportationTiles(board);
const paths: Path[] = [];
for (const pos of possiblePositions) {
if (pos.end.x !== interval(0, board.length - 1, currentPath.end.x) ||
pos.end.y !== interval(0, board.length - 1, currentPath.end.y)) {
if (pos.End.x !== interval(0, board.length - 1, currentPath.End.x) ||
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));
}
}
@ -192,7 +192,7 @@ function findTeleportationTiles(board: GameMap): Path[] {
*/
function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: number): void {
if (board[x][y] !== TileType.wall) {
possiblePositions.push({end: {x, y}, direction: findDirection(x, y, board.length)});
possiblePositions.push({End: {x, y}, Direction: findDirection(x, y, board.length)});
}
}
@ -222,7 +222,7 @@ function findDirection(x: number, y: number, boardSize: number): Direction {
* @param boardSize The size of the board
*/
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;
}
@ -232,7 +232,7 @@ function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
* @param currentPos The current position of the character
*/
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
}
@ -242,7 +242,7 @@ function isWall(board: GameMap, currentPos: Path): boolean {
* @param currentPos The current position of the character
*/
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;
}
@ -252,8 +252,8 @@ function isSpawn(board: GameMap, currentPos: Path) {
* @param character The current character
*/
function isOwnSpawn(currentPos: Path, character: Character): boolean {
const pos = currentPos.end;
const charPos = character.spawnPosition.at;
const pos = currentPos.End;
const charPos = character.SpawnPosition.At;
return charPos.x === pos.x && charPos.y === pos.y;
}

View File

@ -13,9 +13,9 @@ interface ChildProps extends ComponentProps {
interface CharacterProps {
colour: Colour,
position?: Path,
position?: Path | null,
isEatable?: boolean,
spawnPosition: DirectionalPosition,
spawnPosition?: DirectionalPosition | null,
type?: import("../game/character").CharacterType,
}
@ -25,6 +25,7 @@ interface BoxProps {
}
interface PlayerProps {
readonly name: string,
readonly pacMan?: CharacterProps,
readonly colour: Colour,
readonly box?: BoxProps,

View File

@ -25,14 +25,14 @@ type Position = { x: number, y: number };
type GameMap = number[][];
type DirectionalPosition = {
at: Position,
direction: import("../game/direction").Direction
At: Position,
Direction: import("../game/direction").Direction
}
type Path = {
path?: Position[],
end: Position,
direction: import("../game/direction").Direction
Path?: Position[] | null,
End: Position,
Direction: import("../game/direction").Direction
}
type Colour = "white" | "red" | "blue" | "yellow" | "green" | "purple" | "grey";

View File

@ -1,27 +1,3 @@
export function getCSSColour(colour: Colour): string {
let tailwindColour: string;
switch (colour) {
case "red":
tailwindColour = "bg-red-500";
break;
case "blue":
tailwindColour = "bg-blue-500";
break;
case "yellow":
tailwindColour = "bg-yellow-500";
break;
case "green":
tailwindColour = "bg-green-500";
break;
case "purple":
tailwindColour = "bg-purple-500";
break;
case "grey":
tailwindColour = "bg-gray-500";
break;
default:
tailwindColour = "bg-white";
break;
}
return tailwindColour;
return `bg-${colour}${colour === "white" ? "-500" : ""}`;
}

View File

@ -8,10 +8,6 @@ interface IWebSocket {
export default class WebSocketService {
private ws?: WebSocket;
private readonly _url: string;
private _onOpen?: VoidFunction;
private _onReceive?: MessageEventFunction;
private _onClose?: VoidFunction;
private _onError?: VoidFunction;
constructor(url: string, {onOpen, onReceive, onClose, onError}: IWebSocket = {}) {
this._url = url;
@ -21,8 +17,40 @@ export default class WebSocketService {
this._onError = onError;
}
private _onOpen?: VoidFunction;
set onOpen(onOpen: VoidFunction) {
this._onOpen = onOpen;
if (!this.ws) return;
this.ws.onopen = onOpen;
}
private _onReceive?: MessageEventFunction;
set onReceive(onReceive: MessageEventFunction) {
this._onReceive = onReceive;
if (!this.ws) return;
this.ws.onmessage = onReceive;
}
private _onClose?: VoidFunction;
set onClose(onClose: VoidFunction) {
this._onClose = onClose;
if (!this.ws) return;
this.ws.onclose = onClose;
}
private _onError?: VoidFunction;
set onError(onError: VoidFunction) {
this._onError = onError;
if (!this.ws) return;
this.ws.onerror = onError;
}
public open(): void {
if (typeof WebSocket === "undefined") return;
if (typeof WebSocket === "undefined" || this.isConnecting()) return;
this.ws = new WebSocket(this._url);
if (this._onOpen) this.ws.onopen = this._onOpen;
if (this._onReceive) this.ws.onmessage = this._onReceive;
@ -30,13 +58,27 @@ export default class WebSocketService {
if (this._onError) this.ws.onerror = this._onError;
}
public waitForOpen(): Promise<void> {
return new Promise<void>((resolve) => {
const f = () => {
if (this.isOpen()) {
if (this._onOpen) this.onOpen = this._onOpen;
return resolve();
}
setTimeout(f, 50);
};
f();
});
}
public send(data: ActionMessage | string): void {
if (typeof data !== "string") {
data = JSON.stringify(data);
}
this.ws?.send(data);
}
public async sendAndReceive<R>(data: ActionMessage): Promise<R> {
if (!this.isOpen()) return Promise.reject("WebSocket is not open");
@ -69,27 +111,11 @@ export default class WebSocketService {
return this.ws?.readyState === WebSocket?.OPEN;
}
set onOpen(onOpen: VoidFunction) {
this._onOpen = onOpen;
if (!this.ws) return;
this.ws.onopen = onOpen;
public isConnecting(): boolean {
return this.ws?.readyState === WebSocket?.CONNECTING;
}
set onReceive(onReceive: MessageEventFunction) {
this._onReceive = onReceive;
if (!this.ws) return;
this.ws.onmessage = onReceive;
}
set onClose(onClose: VoidFunction) {
this._onClose = onClose;
if (!this.ws) return;
this.ws.onclose = onClose;
}
set onError(onError: VoidFunction) {
this._onError = onError;
if (!this.ws) return;
this.ws.onerror = onError;
public isClosed(): boolean {
return this.ws?.readyState === WebSocket?.CLOSED;
}
}

View File

@ -1,4 +1,6 @@
export enum GameAction {
rollDice,
moveCharacter,
playerInfo,
ready,
}

View File

@ -8,42 +8,42 @@ let pacMan: Character;
beforeEach(() => {
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}
});
});
test("Pac-Man rolls one from start, should return one position", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 1, []);
expect(result.length).toBe(1);
expect(result[0].path?.length).toBe(0);
expect(result[0].Path?.length).toBe(0);
expect(result).toEqual([{end: {x: 3, y: 2}, direction: Direction.up, path: []}]);
});
test("Pac-Man rolls two from start, should return one position", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 2, []);
expect(result.length).toBe(1);
expect(result[0].path?.length).toBe(1);
expect(result[0].Path?.length).toBe(1);
expect(result).toEqual([{end: {x: 3, y: 1}, direction: Direction.up, path: [{x: 3, y: 2}]}]);
});
test("Pac-Man rolls three from start, should return two positions", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 3, []);
expect(result.length).toBe(2);
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}]}]);
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}]}]);
});
test("Pac-Man rolls four from start, should return two positions", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 4, []);
expect(result.length).toBe(2);
arrayEquals(result, [{
end: {x: 1, y: 1},
direction: Direction.left,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}]
End: {x: 1, y: 1},
Direction: Direction.left,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}]
}, {
end: {x: 5, y: 1},
direction: Direction.right,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}]
End: {x: 5, y: 1},
Direction: Direction.right,
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, []);
expect(result.length).toBe(4);
arrayEquals(result, [{
end: {x: 5, y: 0},
direction: Direction.up,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}]
End: {x: 5, y: 0},
Direction: Direction.up,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}]
}, {
end: {x: 6, y: 1},
direction: Direction.right,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}]
End: {x: 6, y: 1},
Direction: Direction.right,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}]
}, {
end: {x: 1, y: 2},
direction: Direction.down,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}, {x: 1, y: 1}]
End: {x: 1, y: 2},
Direction: Direction.down,
Path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}, {x: 1, y: 1}]
}, {
end: {x: 5, y: 2},
direction: Direction.down,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}]
End: {x: 5, y: 2},
Direction: Direction.down,
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);
arrayEquals(result, [
{
end: {x: 1, y: 3},
direction: Direction.down,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 2, y: 1}, {x: 1, y: 1}, {x: 1, y: 2}]
End: {x: 1, y: 3},
Direction: Direction.down,
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},
direction: Direction.right,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 5, y: 0}]
End: {x: 0, y: 5},
Direction: Direction.right,
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},
direction: Direction.down,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 5, y: 2}]
End: {x: 5, y: 3},
Direction: Direction.down,
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},
direction: Direction.right,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 6, y: 1}]
End: {x: 7, y: 1},
Direction: Direction.right,
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},
direction: Direction.left,
path: [{x: 3, y: 2}, {x: 3, y: 1}, {x: 4, y: 1}, {x: 5, y: 1}, {x: 5, y: 0}]
End: {x: 10, y: 5},
Direction: Direction.left,
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},
direction: Direction.up,
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},
Direction: Direction.up,
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", () => {
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, []);
expect(result.length).toBe(11);
});
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, []);
expect(result.length).toBe(12);
});
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, []);
arrayEquals(result, [
{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: 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: 5, y: 9}, direction: Direction.up, path: [{x: 0, y: 5}, {x: 5, y: 10}]},
{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: 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: 5, y: 9}, Direction: Direction.up, Path: [{x: 0, y: 5}, {x: 5, y: 10}]},
]);
expect(result.length).toBe(5);
});
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, []);
expect(result.length).toBe(17);
});
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, []);
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", () => {
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, []);
expect(result.length).toBe(5);
});

View File

@ -1,4 +1,5 @@
using System.Net.WebSockets;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using pacMan.Game;
using pacMan.Game.Interfaces;
@ -13,19 +14,17 @@ namespace pacMan.Controllers;
public class GameController : GenericController
{
private readonly IDiceCup _diceCup;
private readonly IPlayer _player; // TODO recieve player from client and choose a starter
public GameController(ILogger<GameController> logger, IWebSocketService wsService) : base(logger, wsService)
{
_diceCup = new DiceCup();
_player = new Player
{
Box = new Box()
};
}
[HttpGet]
public override async Task Accept() => await base.Accept();
public override async Task Accept()
{
await base.Accept();
}
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
{
@ -48,10 +47,14 @@ public class GameController : GenericController
message.Data = rolls;
break;
case GameAction.AppendBox:
// TODO
// Add pellets to box
// Forward box to all clients
case GameAction.PlayerInfo:
Player player = JsonSerializer.Deserialize<Player>(message.Data);
var group = WsService.AddPlayer(player); // TODO missing some data?
message.Data = group.Players;
break;
case GameAction.Ready:
// TODO select starter player
break;
default:
Logger.Log(LogLevel.Information, "Forwarding message to all clients");

View File

@ -6,15 +6,15 @@ namespace pacMan.Controllers;
public abstract class GenericController : ControllerBase
{
protected readonly ILogger<GenericController> Logger;
private readonly IWebSocketService _wsService;
private WebSocket? _webSocket;
private const int BufferSize = 1024 * 4;
protected readonly ILogger<GenericController> Logger;
protected readonly IWebSocketService WsService;
private WebSocket? _webSocket;
protected GenericController(ILogger<GenericController> logger, IWebSocketService wsService)
{
Logger = logger;
_wsService = wsService;
WsService = wsService;
Logger.Log(LogLevel.Debug, "WebSocket Controller created");
}
@ -25,7 +25,7 @@ public abstract class GenericController : ControllerBase
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
Logger.Log(LogLevel.Information, "WebSocket connection established to {}", HttpContext.Connection.Id);
_webSocket = webSocket;
_wsService.Connections += WsServiceOnFire;
WsService.Connections += WsServiceOnFire;
await Echo();
}
else
@ -37,7 +37,7 @@ public abstract class GenericController : ControllerBase
private async Task WsServiceOnFire(ArraySegment<byte> segment)
{
if (_webSocket == null) return;
await _wsService.Send(_webSocket, segment);
await WsService.Send(_webSocket, segment);
}
@ -50,23 +50,23 @@ public abstract class GenericController : ControllerBase
do
{
var buffer = new byte[BufferSize];
result = await _wsService.Receive(_webSocket, buffer);
result = await WsService.Receive(_webSocket, buffer);
if (result.CloseStatus.HasValue) break;
var segment = Run(result, buffer);
_wsService.SendToAll(segment);
WsService.SendToAll(segment);
} while (true);
await _wsService.Close(_webSocket, result.CloseStatus.Value, result.CloseStatusDescription ?? "No reason");
await WsService.Close(_webSocket, result.CloseStatus.Value, result.CloseStatusDescription ?? "No reason");
}
catch (WebSocketException e)
{
Logger.Log(LogLevel.Error, "{}", e.Message);
}
_wsService.Connections -= WsServiceOnFire;
WsService.Connections -= WsServiceOnFire;
}
protected abstract ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data);

View File

@ -6,7 +6,8 @@ public enum GameAction
{
RollDice,
MoveCharacter,
AppendBox
PlayerInfo,
Ready
}
public class ActionMessage<T>
@ -14,7 +15,10 @@ public class ActionMessage<T>
public GameAction Action { get; set; }
public T? Data { get; set; }
public static ActionMessage FromJson(string json) => JsonSerializer.Deserialize<ActionMessage>(json)!;
public static ActionMessage FromJson(string json)
{
return JsonSerializer.Deserialize<ActionMessage>(json)!;
}
}
public class ActionMessage : ActionMessage<dynamic>

View File

@ -0,0 +1,17 @@
namespace pacMan.Game;
public class Character
{
public required string Colour { get; set; }
public MovePath? Position { get; set; }
public required bool IsEatable { get; set; }
public DirectionalPosition? SpawnPosition { get; set; }
public required CharacterType Type { get; set; }
}
public enum CharacterType
{
PacMan,
Ghost,
Dummy
}

View File

@ -1,8 +1,9 @@
using pacMan.Game.Items;
namespace pacMan.Game.Interfaces;
public interface IBox : IEnumerable<IPellet>
{
void Add(IPellet pellet);
int CountNormal { get; }
void Add(Pellet pellet);
}

View File

@ -1,12 +1,6 @@
namespace pacMan.Game.Interfaces;
public enum PelletType
{
Normal,
PowerPellet
}
public interface IPellet
{
PelletType Get { get; set; }
bool IsPowerPellet { get; init; }
}

View File

@ -1,6 +1,11 @@
using pacMan.Game.Items;
namespace pacMan.Game.Interfaces;
public interface IPlayer
{
IBox Box { get; init; }
string Name { get; init; }
Character PacMan { get; init; }
string Colour { get; init; }
Box Box { get; init; }
}

View File

@ -1,17 +1,26 @@
using System.Collections;
using pacMan.Game.Interfaces;
namespace pacMan.Game.Items;
public class Box : IBox
public class Box
{
private readonly IList<IPellet> _pellets = new List<IPellet>();
public int CountNormal => _pellets.Count(pellet => pellet.Get == PelletType.Normal);
public required List<Pellet>? Pellets { get; init; } = new();
public required string Colour { get; init; }
public void Add(IPellet pellet) => _pellets.Add(pellet);
public int CountNormal => Pellets?.Count(pellet => !pellet.IsPowerPellet) ?? 0;
public IEnumerator<IPellet> GetEnumerator() => _pellets.GetEnumerator();
public IEnumerator<IPellet> GetEnumerator()
{
return Pellets?.GetEnumerator() ?? new List<Pellet>.Enumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// IEnumerator IEnumerable.GetEnumerator()
// {
// return GetEnumerator();
// }
public void Add(Pellet pellet)
{
Pellets?.Add(pellet);
}
}

View File

@ -4,5 +4,5 @@ namespace pacMan.Game.Items;
public class Pellet : IPellet
{
public PelletType Get { get; set; }
public bool IsPowerPellet { get; init; }
}

View File

@ -4,5 +4,8 @@ namespace pacMan.Game.Items;
public class Player : IPlayer
{
public required IBox Box { get; init; }
public required string Name { get; init; }
public required Character PacMan { get; init; }
public required string Colour { get; init; }
public required Box Box { get; init; }
}

View File

@ -0,0 +1,28 @@
namespace pacMan.Game;
public class MovePath
{
public Position[]? Path { get; set; }
public required Position End { get; set; }
public required Direction Direction { get; set; }
}
public class Position
{
public int X { get; set; } = 0;
public int Y { get; set; } = 0;
}
public enum Direction
{
Left,
Up,
Right,
Down
}
public class DirectionalPosition
{
public required Position At { get; set; }
public required Direction Direction { get; set; }
}

View File

@ -1,4 +1,6 @@
using System.Net.WebSockets;
using pacMan.Game.Interfaces;
using pacMan.Services;
namespace pacMan.Interfaces;
@ -10,4 +12,5 @@ public interface IWebSocketService
Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer);
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string closeStatusDescription);
int CountConnected();
GameGroup AddPlayer(IPlayer player);
}

View File

@ -0,0 +1,24 @@
using pacMan.Game;
using pacMan.Game.Interfaces;
namespace pacMan.Services;
public class GameGroup
{
public List<IPlayer> Players { get; } = new();
public event Func<ArraySegment<byte>, Task>? Connections;
public bool AddPlayer(IPlayer player)
{
if (Players.Count >= Rules.MaxPlayers) return false;
if (Players.Exists(p => p.Name == player.Name)) return false;
Players.Add(player);
return true;
}
public void SendToAll(ArraySegment<byte> segment)
{
Connections?.Invoke(segment);
}
}

View File

@ -1,4 +1,5 @@
using System.Net.WebSockets;
using pacMan.Game.Interfaces;
using pacMan.Interfaces;
using pacMan.Utils;
@ -7,7 +8,6 @@ namespace pacMan.Services;
public class WebSocketService : IWebSocketService
{
private readonly ILogger<WebSocketService> _logger;
public event Func<ArraySegment<byte>, Task>? Connections; // TODO separate connections into groups (1 event per game)
public WebSocketService(ILogger<WebSocketService> logger)
{
@ -15,6 +15,10 @@ public class WebSocketService : IWebSocketService
logger.Log(LogLevel.Debug, "WebSocket Service created");
}
public SynchronizedCollection<GameGroup> Games { get; } = new();
public event Func<ArraySegment<byte>, Task>? Connections;
public async Task Send(WebSocket webSocket, ArraySegment<byte> segment)
{
await webSocket.SendAsync(
@ -26,7 +30,10 @@ public class WebSocketService : IWebSocketService
_logger.Log(LogLevel.Trace, "Message sent to WebSocket");
}
public void SendToAll(ArraySegment<byte> segment) => Connections?.Invoke(segment);
public void SendToAll(ArraySegment<byte> segment)
{
Connections?.Invoke(segment);
}
public async Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer)
{
@ -47,5 +54,25 @@ public class WebSocketService : IWebSocketService
_logger.Log(LogLevel.Information, "WebSocket connection closed");
}
public int CountConnected() => Connections?.GetInvocationList().Length ?? 0;
public int CountConnected()
{
return Connections?.GetInvocationList().Length ?? 0;
}
public GameGroup AddPlayer(IPlayer player)
{
var index = 0;
try
{
while (!Games[index].AddPlayer(player)) index++;
}
catch (ArgumentOutOfRangeException)
{
var game = new GameGroup();
game.AddPlayer(player);
Games.Add(game);
}
return Games[index];
}
}