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

View File

@ -7,21 +7,23 @@ import {getCharacterSpawns, testMap} from "../game/map";
import Player from "../game/player";
import PlayerStats from "../components/playerStats";
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 GameButton from "./gameButton";
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 dice = useAtomValue(diceAtom);
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
const currentPlayer = useAtomValue(currentPlayerAtom);
function startGameLoop(): void {
if (currentPlayer?.Name !== player.Name) return;
function rollDice(): void {
if (!player.isTurn()) return;
setSelectedDice(undefined);
wsService.send({Action: GameAction.rollDice});
@ -71,10 +73,12 @@ export const GameComponent: Component<{ player: Player }> = ({player}) => { // T
return (
<>
<div className={"flex-center"}>
<GameButton onReadyClick={sendReady} onRollDiceClick={startGameLoop}/>
<GameButton onReadyClick={sendReady} onRollDiceClick={rollDice}/>
</div>
<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}/>
</>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -65,10 +65,7 @@ function updatePlayers(data?: MoveCharacterData): void {
const updatedPlayers = data?.Players;
if (updatedPlayers) {
const newList: Player[] = [];
for (const player of updatedPlayers) {
newList.push(new Player(player));
}
const newList: Player[] = updatedPlayers.map(p => new Player(p));
store.set(playersAtom, newList);
}
}
@ -77,10 +74,7 @@ function updateGhosts(data?: MoveCharacterData): void {
const updatedGhosts = data?.Ghosts;
if (updatedGhosts) {
const newList: Ghost[] = [];
for (const ghost of updatedGhosts) {
newList.push(new Ghost(ghost));
}
const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g));
store.set(ghostsAtom, newList);
}
}
@ -96,12 +90,7 @@ function removeEatenPellets(data?: MoveCharacterData): void {
function playerInfo(data?: PlayerProps[]): void {
const playerProps = data ?? [];
spawns = getCharacterSpawns(testMap).filter(spawn => spawn.type === CharacterType.pacMan);
store.set(playersAtom, playerProps.map(p => {
if (!p.PacMan.SpawnPosition) {
p.PacMan.SpawnPosition = spawns.pop()?.position;
}
return new Player(p);
}));
store.set(playersAtom, playerProps.map(p => new Player(p)));
}
type ReadyData =
@ -111,9 +100,10 @@ type ReadyData =
function ready(data?: ReadyData): void {
if (data && typeof data !== "string") {
const players = data.Players.map(p => new Player(p));
store.set(playersAtom, players);
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 {
return `bg-${colour}${colour === "white" ? "-500" : ""}`;
import {Colour} from "../game/colour";
/**
* 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);
// TODO derived from playersAtom
export const playersAtom = atom<Player[]>([]);
export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.PacMan));
export const ghostsAtom = atom<Ghost[]>([]);

View File

@ -3,12 +3,13 @@ import possibleMovesAlgorithm from "../../src/game/possibleMovesAlgorithm";
import {testMap} from "../../src/game/map";
import {Character, PacMan} from "../../src/game/character";
import {Direction} from "../../src/game/direction";
import {Colour} from "../../src/game/colour";
let pacMan: Character;
beforeEach(() => {
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, []);
expect(result.length).toBe(1);
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", () => {
const result = possibleMovesAlgorithm(testMap, pacMan, 2, []);
expect(result.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", () => {
@ -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", () => {
pacMan.follow({End: {X: 1, Y: 5}, Direction: Direction.down});
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", () => {
@ -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);
});
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});
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 {

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,
Ready,
InGame
InGame,
Disconnected
}
public class Player : IPlayer, IEquatable<Player>

View File

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