PacMan has a box containing pellets, pellets will be removed from map, when picked up, info and counter at top

This commit is contained in:
martin 2023-05-29 19:14:48 +02:00
parent 69ec14c04c
commit 5cffdf1762
9 changed files with 192 additions and 70 deletions

View File

@ -3,11 +3,13 @@ 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 {NormalPellet, PowerPellet} from "../game/pellet";
interface BoardProps extends ComponentProps {
characters: Character[],
selectedDice?: SelectedDice,
onMove?: (character: Character) => void,
onMove?: Action<Character>,
map: GameMap
}
@ -32,15 +34,34 @@ const Board: Component<BoardProps> = (
setHoveredPosition(path);
}
function handleMoveCharacter(path: Path): void {
function handleMoveCharacter(destination: Path): void {
if (selectedCharacter) {
setHoveredPosition(undefined);
selectedCharacter.follow(path);
selectedCharacter.follow(destination);
pickUpPellets(destination);
onMove?.(selectedCharacter);
setSelectedCharacter(undefined);
}
}
function pickUpPellets(destination: Path): void {
if (selectedCharacter instanceof PacMan) {
const pacMan = selectedCharacter as PacMan;
for (const tile of [...destination.path ?? [], destination.end]) {
const currentTile = map[tile.y][tile.x];
if (currentTile === TileType.pellet) {
pacMan.box.addPellet(new NormalPellet());
map[tile.y][tile.x] = TileType.empty;
} else if (currentTile === TileType.powerPellet) {
pacMan.box.addPellet(new PowerPellet());
map[tile.y][tile.x] = TileType.empty;
}
}
}
}
useEffect(() => {
if (selectedCharacter && selectedDice) {
const possiblePaths = findPossiblePositions(map, selectedCharacter, selectedDice.value);

View File

@ -1,16 +1,21 @@
import React, {useEffect, useRef, useState} from "react";
import React, {useEffect, useState} from "react";
import {AllDice} from "./dice";
import {Action} from "../websockets/actions";
import {GameAction} from "../websockets/actions";
import GameBoard from "./gameBoard";
import {Character, Ghost, PacMan} from "../game/character";
import WebSocketService from "../websockets/WebSocketService";
import {testMap} from "../game/map";
import {Direction} from "../game/direction";
import Box from "../game/box";
const wsService = new WebSocketService("wss://localhost:3000/api/game");
export const GameComponent: Component = () => {
// Better for testing than outside of the component
const characters = useRef([new PacMan("yellow"), new Ghost("purple")]);
// TODO find spawn points
const [characters, setCharacters] = useState([
new PacMan("yellow", {at: {x: 3, y: 3}, direction: Direction.up}),
new Ghost("purple", {at: {x: 8, y: 3}, direction: Direction.up})
]);
const [dice, setDice] = useState<number[]>();
const [selectedDice, setSelectedDice] = useState<SelectedDice>();
@ -20,7 +25,7 @@ export const GameComponent: Component = () => {
}
function rollDice(): void {
wsService.send({Action: Action.rollDice});
wsService.send({Action: GameAction.rollDice});
}
function startGameLoop(): void {
@ -36,13 +41,23 @@ export const GameComponent: Component = () => {
const parsed: ActionMessage = JSON.parse(message.data);
switch (parsed.Action) {
case Action.rollDice:
case GameAction.rollDice:
setDice(parsed.Data as number[]);
break;
case Action.moveCharacter:
case GameAction.moveCharacter:
setDice(parsed.Data?.dice as number[]);
const character = parsed.Data?.character as Character;
characters.current.find(c => c.color === character.color)?.follow(character.position);
const character = parsed.Data?.character satisfies Ghost | PacMan;
const currentCharacter = characters.find(c => c.color === character.color);
if (currentCharacter) {
currentCharacter.position = character.position;
}
// TODO update pellets on other clients (character and on map)
// if (character satisfies PacMan) {
// (characters[currentCharacter] as PacMan).box = new Box(character.box.colour, character.box.pellets);
// console.log(characters[currentCharacter]);
// }
break;
}
}
@ -53,7 +68,7 @@ export const GameComponent: Component = () => {
}
setSelectedDice(undefined);
const data: ActionMessage = {
Action: Action.moveCharacter,
Action: GameAction.moveCharacter,
Data: {
dice: dice?.length ?? 0 > 0 ? dice : null,
character: character
@ -73,11 +88,19 @@ export const GameComponent: Component = () => {
return (
<div>
<h1 className={"w-fit mx-auto"}>Pac-Man The Board Game</h1>
<div className={"flex justify-center"}>
<div className={"flex-center"}>
<button onClick={startGameLoop}>Roll dice</button>
</div>
<AllDice values={dice} onclick={handleDiceClick} selectedDiceIndex={selectedDice?.index}/>
<GameBoard className={"mx-auto my-2"} characters={characters.current} selectedDice={selectedDice}
{
(characters.filter(c => c instanceof PacMan) as PacMan[]).map(c =>
<div key={c.color} className={"mx-auto w-fit m-2"}>
<p>Pac-Man: {c.color}</p>
<p>Pellets: {c.box.count}</p>
<p>PowerPellets: {c.box.countPowerPellets}</p>
</div>)
}
<GameBoard className={"mx-auto my-2"} characters={characters} selectedDice={selectedDice}
onMove={onCharacterMove} map={testMap}/>
</div>
);

View File

@ -2,15 +2,16 @@ 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";
interface TileWithCharacterProps extends ComponentProps {
possiblePath?: Path,
character?: Character,
type?: TileType,
handleMoveCharacter?: (path: Path) => void,
handleSelectCharacter?: (character: Character) => void,
handleStartShowPath?: (path: Path) => void,
handleStopShowPath?: () => void,
handleMoveCharacter?: Action<Path>,
handleSelectCharacter?: Action<Character>,
handleStartShowPath?: Action<Path>,
handleStopShowPath?: VoidFunction,
isSelected?: boolean,
showPath?: boolean
}
@ -26,32 +27,34 @@ export const GameTile: Component<TileWithCharacterProps> = (
handleStopShowPath,
isSelected = false,
showPath = false
}) => {
return (
<Tile className={`${possiblePath?.end ? "border-4 border-white" : ""}`}
type={type}
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}
onMouseLeave={handleStopShowPath}>
<>
{character &&
<div className={"flex-center w-full h-full"}>
<CharacterComponent
character={character}
onClick={handleSelectCharacter}
className={isSelected ? "animate-bounce" : ""}/>
</div>
}
{showPath && <PathSymbol/>}
<AddDummy path={possiblePath}/>
</>
</Tile>
);
};
}) => (
<Tile className={`${possiblePath?.end ? "border-4 border-white" : ""}`}
type={type}
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}
onMouseLeave={handleStopShowPath}>
<>
{character &&
<div className={"flex-center w-full h-full"}>
<CharacterComponent
character={character}
onClick={handleSelectCharacter}
className={isSelected ? "animate-bounce" : ""}/>
</div>
}
{showPath && <Circle/>}
<AddDummy path={possiblePath}/>
</>
</Tile>
);
const PathSymbol: Component = () => ( // TODO sometimes shows up when it shouldn't
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 bg-white"}/>
<div className={`w-1/2 h-1/2 rounded-full ${getCSSColour(colour)}`}/>
</div>
);
@ -79,18 +82,14 @@ const Tile: Component<TileProps> = (
function setColor(): string {
switch (type) {
case TileType.empty:
return "bg-black";
case TileType.wall:
return "bg-blue-500";
case TileType.pellet:
return "bg-yellow-500";
case TileType.powerPellet:
return "bg-orange-500";
case TileType.ghostSpawn:
return "bg-red-500";
case TileType.pacmanSpawn:
return "bg-green-500";
default:
return "bg-black";
}
}
@ -113,6 +112,8 @@ const Tile: Component<TileProps> = (
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
{type === TileType.pellet && <Circle colour={"yellow"}/>}
{type === TileType.powerPellet && <Circle colour={"red"}/>}
{children}
</div>
);
@ -126,7 +127,7 @@ const AddDummy: Component<AddDummyProps> = ({path}) => (
<>
{path &&
<div className={"flex-center w-full h-full"}>
<CharacterComponent character={new Dummy(path)}/>
<CharacterComponent character={new Dummy({at: path.end, direction: path.direction})}/>
</div>
}
</>

View File

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

View File

@ -1,20 +1,15 @@
import {Direction} from "./direction";
type CharacterColor = "red" | "blue" | "yellow" | "green" | "purple" | "grey";
const defaultDirection: Path = {
end: {x: 0, y: 0},
direction: Direction.up
};
import Box from "./box";
export abstract class Character {
public color: CharacterColor;
public color: Colour;
public position: Path;
public isEatable: boolean = false;
public isEatable = false;
public readonly spawnPosition: DirectionalPosition;
protected constructor(color: CharacterColor, startPosition = defaultDirection) {
protected constructor(color: Colour, spawnPosition: DirectionalPosition) {
this.color = color;
this.position = startPosition;
this.position = {end: spawnPosition.at, direction: spawnPosition.direction};
this.spawnPosition = spawnPosition;
}
public follow(path: Path): void {
@ -30,24 +25,27 @@ export abstract class Character {
export class PacMan extends Character {
constructor(color: CharacterColor, startPosition = defaultDirection) {
super(color, startPosition);
public box: Box;
public constructor(color: Colour, spawnPosition: DirectionalPosition) {
super(color, spawnPosition);
this.isEatable = true;
this.box = new Box(color);
}
}
export class Ghost extends Character {
constructor(color: CharacterColor, startPosition = defaultDirection) {
super(color, startPosition);
public constructor(color: Colour, spawnPosition: DirectionalPosition) {
super(color, spawnPosition);
}
}
export class Dummy extends Character {
constructor(path: Path) {
super("grey", path);
public constructor(position: DirectionalPosition) {
super("grey", position);
}
}

View File

@ -0,0 +1,20 @@
export abstract class Pellet {
public readonly colour: Colour;
protected constructor(colour: Colour) {
this.colour = colour;
}
}
export class NormalPellet extends Pellet {
public constructor() {
super("white");
}
}
export class PowerPellet extends Pellet {
public constructor() {
super("yellow");
}
}

View File

@ -5,10 +5,12 @@ type Setter<T> = React.Dispatch<React.SetStateAction<T>>;
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView;
type ActionMessage<T = any> = {
Action: import("../websockets/actions").Action,
Action: import("../websockets/actions").GameAction,
Data?: T
}
type Action<T> = (obj: T) => void;
type SelectedDice = {
value: number,
index: number
@ -28,3 +30,5 @@ type Path = {
end: Position,
direction: import("../game/direction").Direction
}
type Colour = "white" | "red" | "blue" | "yellow" | "green" | "purple" | "grey";

View File

@ -0,0 +1,27 @@
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;
}

View File

@ -1,4 +1,4 @@
export enum Action {
export enum GameAction {
rollDice,
moveCharacter,
}