Fixed moving in frontend, as well as a number of other things that previously broke

This commit is contained in:
Martin Berg Alstad 2023-07-15 14:55:16 +02:00
parent 969f3fcbc8
commit e894aab4f4
14 changed files with 100 additions and 82 deletions

View File

@ -1,10 +1,11 @@
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {Character, PacMan} from "../game/character"; import {Character} from "../game/character";
import findPossiblePositions from "../game/possibleMovesAlgorithm"; 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, selectedDiceAtom} from "../utils/state"; import {allCharactersAtom, currentPlayerAtom, selectedDiceAtom} from "../utils/state";
import Pellet from "../game/pellet";
interface BoardProps extends ComponentProps { interface BoardProps extends ComponentProps {
onMove?: Action<Position[]>, onMove?: Action<Position[]>,
@ -18,6 +19,7 @@ const Board: Component<BoardProps> = (
map map
}) => { }) => {
const currentPlayer = useAtomValue(currentPlayerAtom);
const characters = useAtomValue(allCharactersAtom); const characters = useAtomValue(allCharactersAtom);
const selectedDice = useAtomValue(selectedDiceAtom); const selectedDice = useAtomValue(selectedDiceAtom);
const [selectedCharacter, setSelectedCharacter] = useState<Character>(); const [selectedCharacter, setSelectedCharacter] = useState<Character>();
@ -52,26 +54,27 @@ const Board: Component<BoardProps> = (
const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.End)); const takenChar = characters.find(c => c.isPacMan() && c.isAt(destination.End));
if (takenChar) { if (takenChar) {
takenChar.moveToSpawn(); takenChar.moveToSpawn();
// TODO steal from player // TODO steal from other player
} }
} }
function pickUpPellets(destination: Path): Position[] { function pickUpPellets(destination: Path): Position[] {
const positions: Position[] = []; const positions: Position[] = [];
if (selectedCharacter instanceof PacMan) { if (selectedCharacter?.isPacMan()) {
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];
function updateTileAndPlayerBox(isPowerPellet = false): void {
currentPlayer?.addPellet(new Pellet(isPowerPellet));
map[tile.Y][tile.X] = TileType.empty;
positions.push(tile);
}
if (currentTile === TileType.pellet) { if (currentTile === TileType.pellet) {
// pacMan.box.addPellet(new Pellet()); // TODO update to current player updateTileAndPlayerBox();
map[tile.Y][tile.X] = TileType.empty;
positions.push(tile);
} else if (currentTile === TileType.powerPellet) { } else if (currentTile === TileType.powerPellet) {
// pacMan.box.addPellet(new Pellet(true)); updateTileAndPlayerBox(true);
map[tile.Y][tile.X] = TileType.empty;
positions.push(tile);
} }
} }
} }

View File

@ -7,21 +7,23 @@ import {getCharacterSpawns, testMap} from "../game/map";
import Player from "../game/player"; import Player 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 {diceAtom, ghostsAtom, playersAtom, selectedDiceAtom} from "../utils/state";
import {CharacterType} from "../game/character"; import {CharacterType} from "../game/character";
import GameButton from "./gameButton"; import GameButton from "./gameButton";
const wsService = new WebSocketService(import.meta.env.VITE_API); const wsService = new WebSocketService(import.meta.env.VITE_API);
export const GameComponent: Component<{ player: Player }> = ({player}) => { // TODO players not moving // TODO do not allow players to move other players' characters
// TODO do not allow players to roll dice multiple times
export const GameComponent: Component<{ player: Player }> = ({player}) => {
const players = useAtomValue(playersAtom); const players = useAtomValue(playersAtom);
const dice = useAtomValue(diceAtom); const dice = useAtomValue(diceAtom);
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom); const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
const currentPlayer = useAtomValue(currentPlayerAtom);
function startGameLoop(): void { function rollDice(): void {
if (currentPlayer?.Name !== player.Name) return; if (!player.isTurn()) return;
setSelectedDice(undefined); setSelectedDice(undefined);
wsService.send({Action: GameAction.rollDice}); wsService.send({Action: GameAction.rollDice});
@ -71,10 +73,12 @@ export const GameComponent: Component<{ player: Player }> = ({player}) => { // T
return ( return (
<> <>
<div className={"flex-center"}> <div className={"flex-center"}>
<GameButton onReadyClick={sendReady} onRollDiceClick={startGameLoop}/> <GameButton onReadyClick={sendReady} onRollDiceClick={rollDice}/>
</div> </div>
<AllDice values={dice}/> <AllDice values={dice}/>
{players?.map(p => <PlayerStats key={p.Name} player={p} isCurrentPlayer={currentPlayer?.Name === p.Name}/>)} <div className={"flex justify-center"}>
{players?.map(p => <PlayerStats key={p.Name} player={p}/>)}
</div>
<GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={testMap}/> <GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={testMap}/>
</> </>
); );

View File

@ -2,7 +2,7 @@ import React, {useEffect, useState} from "react";
import {TileType} from "../game/tileType"; 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 {getBgCSSColour} from "../utils/colours";
import {Colour} from "../game/colour"; import {Colour} from "../game/colour";
interface TileWithCharacterProps extends ComponentProps { interface TileWithCharacterProps extends ComponentProps {
@ -36,12 +36,12 @@ export const GameTile: Component<TileWithCharacterProps> = (
onMouseLeave={handleStopShowPath}> onMouseLeave={handleStopShowPath}>
<> <>
{character && {character &&
<div className={"flex-center wh-full"}> <div className={"flex-center wh-full"}>
<CharacterComponent <CharacterComponent
character={character} character={character}
onClick={handleSelectCharacter} onClick={handleSelectCharacter}
className={isSelected ? "animate-bounce" : ""}/> className={isSelected ? "animate-bounce" : ""}/>
</div> </div>
} }
{showPath && <Circle/>} {showPath && <Circle/>}
<AddDummy path={possiblePath}/> <AddDummy path={possiblePath}/>
@ -49,13 +49,9 @@ export const GameTile: Component<TileWithCharacterProps> = (
</Tile> </Tile>
); );
interface CircleProps extends ComponentProps { const Circle: Component<{ colour?: Colour } & ComponentProps> = ({colour = Colour.White, className}) => (
colour?: Colour, <div className={`flex-center w-full h-full ${className}`}>
} <div className={`w-1/2 h-1/2 rounded-full ${getBgCSSColour(colour)}`}/>
const Circle: Component<CircleProps> = ({colour = "white"}) => (
<div className={"flex-center w-full h-full"}>
<div className={`w-1/2 h-1/2 rounded-full ${getCSSColour(colour)}`}/>
</div> </div>
); );
@ -120,16 +116,12 @@ const Tile: Component<TileProps> = (
); );
}; };
interface AddDummyProps extends ComponentProps { const AddDummy: Component<{ path?: Path } & ComponentProps> = ({path}) => (
path?: Path;
}
const AddDummy: Component<AddDummyProps> = ({path}) => (
<> <>
{path && {path &&
<div className={"flex-center wh-full"}> <div className={"flex-center wh-full"}>
<CharacterComponent character={new Dummy(path)}/> <CharacterComponent character={new Dummy(path)}/>
</div> </div>
} }
</> </>
); );

View File

@ -1,20 +1,14 @@
import React from "react"; import React from "react";
import Player, {State} from "../game/player"; import Player, {State} from "../game/player";
export interface PlayerStatsProps extends ComponentProps { const PlayerStats: Component<{ player: Player } & ComponentProps> = (
player: Player,
isCurrentPlayer?: boolean,
}
const PlayerStats: Component<PlayerStatsProps> = (
{ {
player, player,
isCurrentPlayer = false,
className, className,
id id
}) => ( }) => (
<div key={player.Colour} className={`mx-auto w-fit m-2 ${className}`} id={id}> <div key={player.Colour} className={`w-fit m-2 ${className}`} id={id}>
<p className={isCurrentPlayer ? "underline" : ""}>Player: {player.Name}</p> <p className={player.isTurn() ? "underline" : ""}>Player: {player.Name}</p>
<p>Colour: {player.Colour}</p> <p>Colour: {player.Colour}</p>
{player.State === State.inGame ? {player.State === State.inGame ?
<> <>
@ -22,7 +16,6 @@ const PlayerStats: Component<PlayerStatsProps> = (
<p>PowerPellets: {player.Box.countPowerPellets}</p> <p>PowerPellets: {player.Box.countPowerPellets}</p>
</> : </> :
<p>{player.State === State.waitingForPlayers ? "Waiting" : "Ready"}</p>} <p>{player.State === State.waitingForPlayers ? "Waiting" : "Ready"}</p>}
</div> </div>
); );

View File

@ -26,10 +26,15 @@ export class Character {
this.IsEatable = IsEatable; this.IsEatable = IsEatable;
this.SpawnPosition = SpawnPosition; this.SpawnPosition = SpawnPosition;
this.Position = Position ?? SpawnPosition ? { if (Position) {
End: SpawnPosition!.At, this.Position = Position;
Direction: SpawnPosition!.Direction } else {
} : null; this.Position = SpawnPosition ? {
End: SpawnPosition!.At,
Direction: SpawnPosition!.Direction
} : null;
}
this.Type = Type; this.Type = Type;
} }

View File

@ -3,6 +3,7 @@ import Box from "./box";
import {Colour} from "./colour"; import {Colour} from "./colour";
import {getDefaultStore} from "jotai"; import {getDefaultStore} from "jotai";
import {currentPlayerAtom} from "../utils/state"; import {currentPlayerAtom} from "../utils/state";
import Pellet from "./pellet";
export enum State { export enum State {
waitingForPlayers, waitingForPlayers,
@ -33,6 +34,10 @@ export default class Player {
return store.get(currentPlayerAtom)?.Name === this.Name; return store.get(currentPlayerAtom)?.Name === this.Name;
} }
public addPellet(pellet: Pellet): void {
this.Box.addPellet(pellet);
}
public stealFrom(other: Player): void { public stealFrom(other: Player): void {
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
const pellet = other.Box.Pellets.pop(); const pellet = other.Box.Pellets.pop();

View File

@ -34,7 +34,7 @@ interface BoxProps {
interface PlayerProps { interface PlayerProps {
readonly Name: string, readonly Name: string,
readonly PacMan: CharacterProps, readonly PacMan?: CharacterProps,
readonly Colour: import("../game/colour").Colour, readonly Colour: import("../game/colour").Colour,
readonly Box?: BoxProps, readonly Box?: BoxProps,
State?: import("../game/player").State, State?: import("../game/player").State,

View File

@ -65,10 +65,7 @@ function updatePlayers(data?: MoveCharacterData): void {
const updatedPlayers = data?.Players; const updatedPlayers = data?.Players;
if (updatedPlayers) { if (updatedPlayers) {
const newList: Player[] = []; const newList: Player[] = updatedPlayers.map(p => new Player(p));
for (const player of updatedPlayers) {
newList.push(new Player(player));
}
store.set(playersAtom, newList); store.set(playersAtom, newList);
} }
} }
@ -77,10 +74,7 @@ function updateGhosts(data?: MoveCharacterData): void {
const updatedGhosts = data?.Ghosts; const updatedGhosts = data?.Ghosts;
if (updatedGhosts) { if (updatedGhosts) {
const newList: Ghost[] = []; const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g));
for (const ghost of updatedGhosts) {
newList.push(new Ghost(ghost));
}
store.set(ghostsAtom, newList); store.set(ghostsAtom, newList);
} }
} }
@ -96,12 +90,7 @@ function removeEatenPellets(data?: MoveCharacterData): void {
function playerInfo(data?: PlayerProps[]): void { function playerInfo(data?: PlayerProps[]): void {
const playerProps = data ?? []; const playerProps = data ?? [];
spawns = getCharacterSpawns(testMap).filter(spawn => spawn.type === CharacterType.pacMan); spawns = getCharacterSpawns(testMap).filter(spawn => spawn.type === CharacterType.pacMan);
store.set(playersAtom, playerProps.map(p => { store.set(playersAtom, playerProps.map(p => new Player(p)));
if (!p.PacMan.SpawnPosition) {
p.PacMan.SpawnPosition = spawns.pop()?.position;
}
return new Player(p);
}));
} }
type ReadyData = type ReadyData =
@ -111,9 +100,10 @@ type ReadyData =
function ready(data?: ReadyData): void { function ready(data?: ReadyData): void {
if (data && typeof data !== "string") { if (data && typeof data !== "string") {
const players = data.Players.map(p => new Player(p));
store.set(playersAtom, players);
if (data.AllReady) { if (data.AllReady) {
store.set(currentPlayerAtom, new Player(data.Starter)); store.set(currentPlayerAtom, players.find(p => p.Name === data.Starter.Name));
} }
store.set(playersAtom, data.Players.map(p => new Player(p)));
} }
} }

View File

@ -1,3 +1,10 @@
export function getCSSColour(colour: Colour): string { import {Colour} from "../game/colour";
return `bg-${colour}${colour === "white" ? "-500" : ""}`;
/**
* Converts the given enum Colour to a Tailwind CSS class name.
* @param colour The colour to convert
* @returns The Tailwind CSS class name
*/
export function getBgCSSColour(colour: Colour): string {
return `bg-${colour}${colour !== Colour.White ? "-500" : ""}`;
} }

View File

@ -5,7 +5,6 @@ import {Ghost} from "../game/character";
const playerStorage = createJSONStorage<Player | undefined>(() => sessionStorage); const playerStorage = createJSONStorage<Player | undefined>(() => sessionStorage);
// TODO derived from playersAtom
export const playersAtom = atom<Player[]>([]); export const playersAtom = atom<Player[]>([]);
export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.PacMan)); export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.PacMan));
export const ghostsAtom = atom<Ghost[]>([]); export const ghostsAtom = atom<Ghost[]>([]);

View File

@ -3,12 +3,13 @@ import possibleMovesAlgorithm from "../../src/game/possibleMovesAlgorithm";
import {testMap} from "../../src/game/map"; import {testMap} from "../../src/game/map";
import {Character, PacMan} from "../../src/game/character"; import {Character, PacMan} from "../../src/game/character";
import {Direction} from "../../src/game/direction"; import {Direction} from "../../src/game/direction";
import {Colour} from "../../src/game/colour";
let pacMan: Character; let pacMan: Character;
beforeEach(() => { beforeEach(() => {
pacMan = new PacMan({ pacMan = new PacMan({
Colour: "yellow", SpawnPosition: {At: {X: 3, Y: 3}, Direction: Direction.up} Colour: Colour.Yellow, SpawnPosition: {At: {X: 3, Y: 3}, Direction: Direction.up}
}); });
}); });
@ -16,14 +17,14 @@ test("Pac-Man rolls one from start, should return one position", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 1, []); const result = possibleMovesAlgorithm(testMap, pacMan, 1, []);
expect(result.length).toBe(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: []}]); expect(result).toEqual([{End: {X: 3, Y: 2}, Direction: Direction.up, Path: []}] as Path[]);
}); });
test("Pac-Man rolls two from start, should return one position", () => { test("Pac-Man rolls two from start, should return one position", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 2, []); const result = possibleMovesAlgorithm(testMap, pacMan, 2, []);
expect(result.length).toBe(1); 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}]}]); expect(result).toEqual([{End: {X: 3, Y: 1}, Direction: Direction.up, Path: [{X: 3, Y: 2}]}] as Path[]);
}); });
test("Pac-Man rolls three from start, should return two positions", () => { test("Pac-Man rolls three from start, should return two positions", () => {
@ -130,7 +131,7 @@ test("Pac-Man rolls three from position [1,5] (left), should return 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(21);
}); });
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", () => {
@ -139,10 +140,10 @@ test("Pac-Man rolls six from position [7,1] (right), path to [9,5] should be fiv
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 7", () => {
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(7);
}); });
function arrayEquals<T extends any[]>(result: T, expected: T, message?: string): void { function arrayEquals<T extends any[]>(result: T, expected: T, message?: string): void {

View File

@ -0,0 +1,18 @@
import {expect, test} from "vitest"
import {getBgCSSColour} from "../../src/utils/colours";
import {Colour} from "../../src/game/colour";
test('white should not use -500', () => {
const cssColour = getBgCSSColour(Colour.White);
expect(cssColour).toBe("bg-white");
});
test('purple should use -500', () => {
const cssColour = getBgCSSColour(Colour.Purple);
expect(cssColour).toBe("bg-purple-500");
});
test("yellow should use -500", () => {
const cssColour = getBgCSSColour(Colour.Yellow);
expect(cssColour).toBe("bg-yellow-500");
});

View File

@ -13,7 +13,8 @@ public enum State
{ {
WaitingForPlayers, WaitingForPlayers,
Ready, Ready,
InGame InGame,
Disconnected
} }
public class Player : IPlayer, IEquatable<Player> public class Player : IPlayer, IEquatable<Player>

View File

@ -5,7 +5,7 @@ using pacMan.Game.Items;
namespace pacMan.Services; namespace pacMan.Services;
public class GameGroup : IEnumerable<IPlayer> public class GameGroup : IEnumerable<IPlayer> // TODO handle disconnects and reconnects
{ {
private readonly Random _random = new(); private readonly Random _random = new();