Added prettier

This commit is contained in:
martin 2023-10-28 15:12:50 +02:00
parent 57c046fc77
commit b0c6641ea2
45 changed files with 1121 additions and 1072 deletions

View File

@ -1,5 +1,5 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<state> <state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> <option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state> </state>
</component> </component>

View File

@ -0,0 +1,9 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false,
"arrowParens": "avoid",
"bracketSpacing": true,
"bracketSameLine": true,
"printWidth": 120
}

View File

@ -23,6 +23,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"happy-dom": "^12.10.3", "happy-dom": "^12.10.3",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0", "vite": "^4.5.0",
@ -41,7 +42,8 @@
"build": "vite build", "build": "vite build",
"serve": "vite preview", "serve": "vite preview",
"test": "cross-env CI=true vitest", "test": "cross-env CI=true vitest",
"coverage": "vitest run --coverage" "coverage": "vitest run --coverage",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@ -67,6 +67,9 @@ importers:
postcss: postcss:
specifier: ^8.4.31 specifier: ^8.4.31
version: 8.4.31 version: 8.4.31
prettier:
specifier: ^3.0.3
version: 3.0.3
tailwindcss: tailwindcss:
specifier: ^3.3.5 specifier: ^3.3.5
version: 3.3.5 version: 3.3.5
@ -2442,6 +2445,7 @@ packages:
chalk: 3.0.0 chalk: 3.0.0
diff-match-patch: 1.0.5 diff-match-patch: 1.0.5
dev: false dev: false
bundledDependencies: []
/lilconfig@2.1.0: /lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
@ -2890,6 +2894,12 @@ packages:
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true dev: true
/prettier@3.0.3:
resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==}
engines: {node: '>=14'}
hasBin: true
dev: true
/pretty-format@29.6.2: /pretty-format@29.6.2:
resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}

View File

@ -1,21 +1,21 @@
import React, {FC} from "react"; import React, { FC } from "react"
import {Navigate, Route, Routes} from "react-router-dom"; import { Navigate, Route, Routes } from "react-router-dom"
import Layout from "./components/layout"; import Layout from "./components/layout"
import AppRoutes from "./AppRoutes"; import AppRoutes from "./AppRoutes"
import "./index.css"; import "./index.css"
import {useAtomValue} from "jotai"; import { useAtomValue } from "jotai"
import {thisPlayerAtom} from "./utils/state"; import { thisPlayerAtom } from "./utils/state"
export const App: FC = () => ( export const App: FC = () => (
<Layout> <Layout>
<Routes> <Routes>
{AppRoutes.map((route, index) => { {AppRoutes.map((route, index) => {
const {element, secured = false, ...rest} = route; const { element, secured = false, ...rest } = route
return <Route key={index} {...rest} element={<Secured secured={secured}>{element}</Secured>}/>; return <Route key={index} {...rest} element={<Secured secured={secured}>{element}</Secured>} />
})} })}
</Routes> </Routes>
</Layout> </Layout>
); )
/** /**
* This component is used to redirect the user to the login page if they are not logged in and the page is secured. * This component is used to redirect the user to the login page if they are not logged in and the page is secured.
@ -23,13 +23,15 @@ export const App: FC = () => (
* @param secured Whether or not the page is secured. * @param secured Whether or not the page is secured.
* @constructor The Secured component. * @constructor The Secured component.
*/ */
const Secured: FC<{ const Secured: FC<
secured: boolean {
} & ChildProps> = ({children, secured}) => { secured: boolean
const player = useAtomValue(thisPlayerAtom); } & ChildProps
> = ({ children, secured }) => {
const player = useAtomValue(thisPlayerAtom)
if (secured && player === undefined) { if (secured && player === undefined) {
return <Navigate to={"/login"} replace/> return <Navigate to={"/login"} replace />
} }
return <>{children}</> return <>{children}</>

View File

@ -1,37 +1,37 @@
import React from "react"; import React from "react"
import {Counter} from "./pages/counter"; import { Counter } from "./pages/counter"
import GamePage from "./pages/game"; import GamePage from "./pages/game"
import LobbyPage from "./pages/lobby"; import LobbyPage from "./pages/lobby"
import LoginPage from "./pages/login"; import LoginPage from "./pages/login"
import HomePage from "./pages/home"; import HomePage from "./pages/home"
const AppRoutes = [ const AppRoutes = [
{ {
index: true, index: true,
element: <HomePage/> element: <HomePage />,
}, },
{ {
path: "/counter", path: "/counter",
element: <Counter/> element: <Counter />,
}, },
{ {
path: "/game/:id", path: "/game/:id",
element: <GamePage/>, element: <GamePage />,
secured: true secured: true,
}, },
{ {
path: "/lobby", path: "/lobby",
element: <LobbyPage/>, element: <LobbyPage />,
secured: true secured: true,
}, },
{ {
path: "/login", path: "/login",
element: <LoginPage/> element: <LoginPage />,
}, },
{ {
path: "*", path: "*",
element: <p>Page not found</p> element: <p>Page not found</p>,
} },
]; ]
export default AppRoutes; export default AppRoutes

View File

@ -1,16 +1,15 @@
import React, {FC} from "react"; import React, { FC } from "react"
export const Button: FC<ButtonProps> = ( export const Button: FC<ButtonProps> = ({
{ className,
className, onClick,
onClick, style,
style, title,
title, id,
id, disabled = false,
disabled = false, children,
children, type = "button",
type = "button", }) => {
}) => {
return ( return (
<button <button
id={id} id={id}
@ -23,4 +22,4 @@ export const Button: FC<ButtonProps> = (
{children} {children}
</button> </button>
) )
} }

View File

@ -1,36 +1,34 @@
import React, {FC} from "react"; import React, { FC } from "react"
import useToggle from "../hooks/useToggle"; import useToggle from "../hooks/useToggle"
import {BugAntIcon} from "@heroicons/react/20/solid"; import { BugAntIcon } from "@heroicons/react/20/solid"
import {selectedMapAtom} from "../utils/state"; import { selectedMapAtom } from "../utils/state"
import {useAtom} from "jotai"; import { useAtom } from "jotai"
const DebugMenu: FC = () => { const DebugMenu: FC = () => {
const [open, toggleOpen] = useToggle()
const [open, toggleOpen] = useToggle();
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
return ( return (
<div> <div>
{open && <DebugOptions/>} {open && <DebugOptions />}
<button className={"fixed bottom-2 right-2 bg-gray-800 text-white p-2 z-50 rounded-full"} <button
title={"Debug menu"} className={"fixed bottom-2 right-2 bg-gray-800 text-white p-2 z-50 rounded-full"}
onClick={() => toggleOpen()}> title={"Debug menu"}
<BugAntIcon className={"w-8 m-1"}/> onClick={() => toggleOpen()}>
<BugAntIcon className={"w-8 m-1"} />
</button> </button>
</div> </div>
)
);
} }
} }
export default DebugMenu; export default DebugMenu
const DebugOptions: FC = () => { const DebugOptions: FC = () => {
const [map, setMap] = useAtom(selectedMapAtom)
const [map, setMap] = useAtom(selectedMapAtom);
function clearSessionStorage(): void { function clearSessionStorage(): void {
sessionStorage.clear(); sessionStorage.clear()
} }
function restartGame(): void { function restartGame(): void {

View File

@ -1,56 +1,49 @@
import React, {FC} from "react"; import React, { FC } from "react"
import {useAtom, useAtomValue} from "jotai"; import { useAtom, useAtomValue } from "jotai"
import {selectedDiceAtom, thisPlayerAtom,} from "../utils/state"; import { selectedDiceAtom, thisPlayerAtom } from "../utils/state"
import {Button} from "./button"; import { Button } from "./button"
export const AllDice: FC<{ values?: number[] } & ComponentProps> = ( export const AllDice: FC<{ values?: number[] } & ComponentProps> = ({ className, values }) => {
{ const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom)
className,
values,
}) => {
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
function handleClick(dice: SelectedDice): void { function handleClick(dice: SelectedDice): void {
setSelectedDice(dice); setSelectedDice(dice)
} }
return ( return (
<div className={"flex gap-5 justify-center"}> <div className={"flex gap-5 justify-center"}>
{values?.map((value, index) => {values?.map((value, index) => (
<Dice key={index} <Dice
className={`${selectedDice?.index === index ? "border-2 border-black" : ""} ${className}`} key={index}
value={value} className={`${selectedDice?.index === index ? "border-2 border-black" : ""} ${className}`}
onClick={(value) => handleClick({index, value})}/>)} value={value}
onClick={value => handleClick({ index, value })}
/>
))}
</div> </div>
); )
};
interface DiceProps extends ComponentProps {
value?: number,
onClick?: (value: number) => void,
} }
export const Dice: FC<DiceProps> = ( interface DiceProps extends ComponentProps {
{ value?: number
className, onClick?: (value: number) => void
value, }
onClick,
}) => {
const thisPlayer = useAtomValue(thisPlayerAtom); export const Dice: FC<DiceProps> = ({ className, value, onClick }) => {
const thisPlayer = useAtomValue(thisPlayerAtom)
function handleClick() { function handleClick() {
if (onClick && value) { if (onClick && value) {
onClick(value); onClick(value)
} }
} }
return ( return (
<Button className={`text-2xl bg-gray-400 px-4 m-1 ${className}`} <Button
disabled={!thisPlayer?.isTurn()} className={`text-2xl bg-gray-400 px-4 m-1 ${className}`}
onClick={handleClick}> disabled={!thisPlayer?.isTurn()}
onClick={handleClick}>
{value?.toString()} {value?.toString()}
</Button> </Button>
); )
}; }

View File

@ -1,24 +1,17 @@
import React, {forwardRef, ReactEventHandler} from "react"; import React, { forwardRef, ReactEventHandler } from "react"
export interface DropdownProps extends ComponentProps { export interface DropdownProps extends ComponentProps {
options?: string[], options?: string[]
onSelect?: ReactEventHandler<HTMLSelectElement>, onSelect?: ReactEventHandler<HTMLSelectElement>
} }
const Dropdown: FRComponent<DropdownProps, HTMLSelectElement> = forwardRef(( const Dropdown: FRComponent<DropdownProps, HTMLSelectElement> = forwardRef(({ className, options, onSelect }, ref) => (
{ <select
className, ref={ref}
options, className={"border-2 border-gray-300 rounded-md py-1 px-2 bg-white " + className}
onSelect onSelect={onSelect}>
}, ref) => ( {options?.map((option, index) => <option key={index}>{option}</option>)}
<select ref={ref} className={"border-2 border-gray-300 rounded-md py-1 px-2 bg-white " + className}
onSelect={onSelect}>
{
options?.map((option, index) => (
<option key={index}>{option}</option>
))
}
</select> </select>
)); ))
export default Dropdown; export default Dropdown

View File

@ -1,147 +1,138 @@
import React, {FC, Fragment, useEffect, useState} from "react"; import React, { FC, Fragment, useEffect, useState } from "react"
import {Character} 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 {atom, useAtom, useAtomValue, useSetAtom} from "jotai"; import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"
import {allCharactersAtom, currentPlayerAtom, playersAtom, selectedDiceAtom} from "../utils/state"; import { allCharactersAtom, currentPlayerAtom, playersAtom, selectedDiceAtom } from "../utils/state"
import {Dialog, Transition} from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react"
interface BoardProps extends ComponentProps { interface BoardProps extends ComponentProps {
onMove?: Action<Position[]>, onMove?: Action<Position[]>
map: GameMap map: GameMap
} }
const modalOpenAtom = atom(false); const modalOpenAtom = atom(false)
const Board: FC<BoardProps> = ( const Board: FC<BoardProps> = ({ className, onMove, map }) => {
{ const currentPlayer = useAtomValue(currentPlayerAtom)
className, const characters = useAtomValue(allCharactersAtom)
onMove, const selectedDice = useAtomValue(selectedDiceAtom)
map const [selectedCharacter, setSelectedCharacter] = useState<Character>()
}) => { const [possiblePositions, setPossiblePositions] = useState<Path[]>([])
const [hoveredPosition, setHoveredPosition] = useState<Path>()
const currentPlayer = useAtomValue(currentPlayerAtom); const setModalOpen = useSetAtom(modalOpenAtom)
const characters = useAtomValue(allCharactersAtom);
const selectedDice = useAtomValue(selectedDiceAtom);
const [selectedCharacter, setSelectedCharacter] = useState<Character>();
const [possiblePositions, setPossiblePositions] = useState<Path[]>([]);
const [hoveredPosition, setHoveredPosition] = useState<Path>();
const setModalOpen = useSetAtom(modalOpenAtom);
function handleSelectCharacter(character: Character): void { function handleSelectCharacter(character: Character): void {
if (character.isPacMan() && currentPlayer?.pacMan.colour !== character.colour) { if (character.isPacMan() && currentPlayer?.pacMan.colour !== character.colour) {
return; return
} }
setSelectedCharacter(character); setSelectedCharacter(character)
} }
function handleShowPath(path: Path): void { function handleShowPath(path: Path): void {
setHoveredPosition(path); setHoveredPosition(path)
} }
function handleMoveCharacter(destination: Path): void { function handleMoveCharacter(destination: Path): void {
if (selectedCharacter) { if (selectedCharacter) {
setHoveredPosition(undefined); setHoveredPosition(undefined)
if (selectedCharacter.isGhost()) { if (selectedCharacter.isGhost()) {
tryMovePacManToSpawn(destination); tryMovePacManToSpawn(destination)
} }
selectedCharacter.follow(destination); selectedCharacter.follow(destination)
const positions = pickUpPellets(destination); const positions = pickUpPellets(destination)
onMove?.(positions); onMove?.(positions)
setSelectedCharacter(undefined); setSelectedCharacter(undefined)
} }
} }
function tryMovePacManToSpawn(destination: Path): void { 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) { if (takenChar) {
takenChar.moveToSpawn(); takenChar.moveToSpawn()
stealFromPlayer(); stealFromPlayer()
} }
} }
function stealFromPlayer(): void { function stealFromPlayer(): void {
setModalOpen(true); setModalOpen(true)
} }
function pickUpPellets(destination: Path): Position[] { function pickUpPellets(destination: Path): Position[] {
const positions: Position[] = []; const positions: Position[] = []
if (selectedCharacter?.isPacMan()) { if (selectedCharacter?.isPacMan()) {
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 { function updateTileAndPlayerBox(isPowerPellet = false): void {
if (isPowerPellet) { if (isPowerPellet) {
currentPlayer?.addPowerPellet(); currentPlayer?.addPowerPellet()
} else { } else {
currentPlayer?.addPellet(); currentPlayer?.addPellet()
} }
map[tile.y][tile.x] = TileType.empty; map[tile.y][tile.x] = TileType.empty
positions.push(tile); positions.push(tile)
} }
if (currentTile === TileType.pellet) { if (currentTile === TileType.pellet) {
updateTileAndPlayerBox(); updateTileAndPlayerBox()
} else if (currentTile === TileType.powerPellet) { } else if (currentTile === TileType.powerPellet) {
updateTileAndPlayerBox(true); updateTileAndPlayerBox(true)
} }
} }
} }
return positions; return positions
} }
useEffect(() => { useEffect(() => {
if (selectedCharacter && selectedDice) { if (selectedCharacter && selectedDice) {
const possiblePaths = findPossiblePositions(map, selectedCharacter, selectedDice.value, characters); const possiblePaths = findPossiblePositions(map, selectedCharacter, selectedDice.value, characters)
setPossiblePositions(possiblePaths); setPossiblePositions(possiblePaths)
} else { } else {
setPossiblePositions([]); setPossiblePositions([])
} }
}, [selectedCharacter, selectedDice]); }, [selectedCharacter, selectedDice])
return ( return (
<div className={`w-fit ${className}`}> <div className={`w-fit ${className}`}>
<SelectPlayerModal/> <SelectPlayerModal />
{ {map.map((row, rowIndex) => (
map.map((row, rowIndex) => <div key={rowIndex} className={"flex"}>
<div key={rowIndex} className={"flex"}> {row.map((tile, colIndex) => (
{ <GameTile
row.map((tile, colIndex) => key={colIndex + rowIndex * colIndex}
<GameTile type={tile}
key={colIndex + rowIndex * colIndex} possiblePath={possiblePositions.find(p => p.end.x === colIndex && p.end.y === rowIndex)}
type={tile} character={characters.find(c => c.isAt({ x: colIndex, y: rowIndex }))}
possiblePath={possiblePositions.find(p => p.end.x === colIndex && p.end.y === rowIndex)} isSelected={selectedCharacter?.isAt({ x: colIndex, y: rowIndex })}
character={characters.find(c => c.isAt({x: colIndex, y: rowIndex}))} showPath={hoveredPosition?.path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined}
isSelected={selectedCharacter?.isAt({x: colIndex, y: rowIndex})} handleMoveCharacter={handleMoveCharacter}
showPath={hoveredPosition?.path?.find(pos => pos.x === colIndex && pos.y === rowIndex) !== undefined} handleSelectCharacter={handleSelectCharacter}
handleMoveCharacter={handleMoveCharacter} handleStartShowPath={handleShowPath}
handleSelectCharacter={handleSelectCharacter} handleStopShowPath={() => setHoveredPosition(undefined)}
handleStartShowPath={handleShowPath} />
handleStopShowPath={() => setHoveredPosition(undefined)}/> ))}
) </div>
} ))}
</div>)
}
</div> </div>
); )
}; }
export default Board; export default Board
const SelectPlayerModal: FC = () => { const SelectPlayerModal: FC = () => {
const [isOpen, setIsOpen] = useAtom(modalOpenAtom); const [isOpen, setIsOpen] = useAtom(modalOpenAtom)
const currentPlayer = useAtomValue(currentPlayerAtom); const currentPlayer = useAtomValue(currentPlayerAtom)
const allPlayers = useAtomValue(playersAtom).filter(p => p !== currentPlayer); const allPlayers = useAtomValue(playersAtom).filter(p => p !== currentPlayer)
if (currentPlayer === undefined) return null; if (currentPlayer === undefined) return null
function close(): void { function close(): void {
setIsOpen(false); setIsOpen(false)
} }
return ( return (
@ -155,9 +146,8 @@ const SelectPlayerModal: FC = () => {
enterTo="opacity-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0">
> <div className="fixed inset-0 bg-black bg-opacity-25" />
<div className="fixed inset-0 bg-black bg-opacity-25"/>
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 overflow-y-auto"> <div className="fixed inset-0 overflow-y-auto">
@ -169,10 +159,8 @@ const SelectPlayerModal: FC = () => {
enterTo="opacity-100 scale-100" enterTo="opacity-100 scale-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95">
> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Panel
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900"> <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
Steal from player Steal from player
</Dialog.Title> </Dialog.Title>
@ -182,29 +170,29 @@ const SelectPlayerModal: FC = () => {
</Dialog.Description> </Dialog.Description>
</div> </div>
{ {allPlayers.map(player => (
allPlayers.map(player => <div key={player.username} className={"border-b pb-1"}>
<div key={player.username} className={"border-b pb-1"}> <span className={"mx-2"}>
<span className={"mx-2"}>{player.username} has {player.box.pellets} pellets</span> {player.username} has {player.box.pellets} pellets
<button className={"text-blue-500 enabled:cursor-pointer disabled:text-gray-500"} </span>
style={{background: "none"}} <button
disabled={player.box.pellets === 0} className={"text-blue-500 enabled:cursor-pointer disabled:text-gray-500"}
onClick={() => { style={{ background: "none" }}
currentPlayer?.stealFrom(player); disabled={player.box.pellets === 0}
close(); onClick={() => {
}}> currentPlayer?.stealFrom(player)
Steal close()
</button> }}>
</div> Steal
) </button>
} </div>
))}
<div className="mt-4"> <div className="mt-4">
<button <button
type="button" type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={close} onClick={close}>
>
Don't steal from anyone Don't steal from anyone
</button> </button>
</div> </div>

View File

@ -1,33 +1,36 @@
import React, {FC, MouseEventHandler} from "react"; import React, { FC, MouseEventHandler } from "react"
import {State} from "../game/player"; import { State } from "../game/player"
import {currentPlayerAtom, playersAtom, rollDiceButtonAtom, thisPlayerAtom} from "../utils/state"; import { currentPlayerAtom, playersAtom, rollDiceButtonAtom, thisPlayerAtom } from "../utils/state"
import {useAtomValue} from "jotai"; import { useAtomValue } from "jotai"
import {Button} from "./button"; import { Button } from "./button"
import rules from "../game/rules"; import rules from "../game/rules"
interface GameButtonProps extends ComponentProps { interface GameButtonProps extends ComponentProps {
onReadyClick?: MouseEventHandler, onReadyClick?: MouseEventHandler
onRollDiceClick?: MouseEventHandler onRollDiceClick?: MouseEventHandler
} }
const GameButton: FC<GameButtonProps> = ( const GameButton: FC<GameButtonProps> = ({ onReadyClick, onRollDiceClick }) => {
{ const currentPlayer = useAtomValue(currentPlayerAtom)
onReadyClick, const thisPlayer = useAtomValue(thisPlayerAtom)
onRollDiceClick, const players = useAtomValue(playersAtom)
}) => { const activeRollDiceButton = useAtomValue(rollDiceButtonAtom)
const currentPlayer = useAtomValue(currentPlayerAtom); if (
const thisPlayer = useAtomValue(thisPlayerAtom); players.length >= rules.minPlayers &&
const players = useAtomValue(playersAtom); (currentPlayer === undefined || currentPlayer.state === State.waitingForPlayers)
const activeRollDiceButton = useAtomValue(rollDiceButtonAtom); ) {
return <Button onClick={onReadyClick}>Ready</Button>
if (players.length >= rules.minPlayers && (currentPlayer === undefined || currentPlayer.state === State.waitingForPlayers)) {
return <Button onClick={onReadyClick}>Ready</Button>;
} }
if (!thisPlayer?.isTurn()) { // TODO also show when waiting for other players if (!thisPlayer?.isTurn()) {
return <Button disabled>Please wait</Button>; // TODO also show when waiting for other players
return <Button disabled>Please wait</Button>
} }
return <Button onClick={onRollDiceClick} disabled={!activeRollDiceButton}>Roll dice</Button>; return (
}; <Button onClick={onRollDiceClick} disabled={!activeRollDiceButton}>
Roll dice
</Button>
)
}
export default GameButton; export default GameButton

View File

@ -1,39 +1,38 @@
import React, {FC, useEffect} from "react"; import React, { FC, useEffect } from "react"
import {AllDice} from "./dice"; 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 Player from "../game/player"; import Player from "../game/player"
import PlayerStats from "../components/playerStats"; import PlayerStats from "../components/playerStats"
import {useAtom, useAtomValue, useSetAtom} from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai"
import {diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedDiceAtom} from "../utils/state"; import { diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedDiceAtom } from "../utils/state"
import GameButton from "./gameButton"; import GameButton from "./gameButton"
import {Button} from "./button"; import { Button } from "./button"
import {useNavigate, useParams} from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom"
import {getData} from "../utils/api"; import { getData } from "../utils/api"
const wsService = new WebSocketService(import.meta.env.VITE_API_WS); const wsService = new WebSocketService(import.meta.env.VITE_API_WS)
export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map}) => { export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, map }) => {
const players = useAtomValue(playersAtom)
const dice = useAtomValue(diceAtom)
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom)
const setActiveRollDiceButton = useSetAtom(rollDiceButtonAtom)
const ghosts = useAtomValue(ghostsAtom)
const players = useAtomValue(playersAtom); const navigate = useNavigate()
const dice = useAtomValue(diceAtom); const { id } = useParams()
const [selectedDice, setSelectedDice] = useAtom(selectedDiceAtom);
const setActiveRollDiceButton = useSetAtom(rollDiceButtonAtom);
const ghosts = useAtomValue(ghostsAtom);
const navigate = useNavigate();
const {id} = useParams();
/** /**
* Rolls the dice for the current player's turn. * Rolls the dice for the current player's turn.
*/ */
function rollDice(): void { function rollDice(): void {
if (!player.isTurn()) return; if (!player.isTurn()) return
setSelectedDice(undefined); setSelectedDice(undefined)
wsService.send({action: GameAction.rollDice}); wsService.send({ action: GameAction.rollDice })
setActiveRollDiceButton(false); setActiveRollDiceButton(false)
} }
/** /**
@ -42,22 +41,22 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map
*/ */
function onCharacterMove(eatenPellets: Position[]): void { function onCharacterMove(eatenPellets: Position[]): void {
if (dice && selectedDice) { if (dice && selectedDice) {
dice.splice(selectedDice.index, 1); dice.splice(selectedDice.index, 1)
} }
setSelectedDice(undefined); setSelectedDice(undefined)
const data: ActionMessage = { const data: ActionMessage = {
action: GameAction.moveCharacter, action: GameAction.moveCharacter,
data: { data: {
dice: dice?.length ?? 0 > 0 ? dice : null, dice: dice?.length ?? 0 > 0 ? dice : null,
players: players, players: players,
ghosts: ghosts, ghosts: ghosts,
eatenPellets: eatenPellets eatenPellets: eatenPellets,
} },
}; }
wsService.send(data); wsService.send(data)
if (dice?.length === 0) { if (dice?.length === 0) {
endTurn(); endTurn()
} }
} }
@ -70,15 +69,15 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map
data: { data: {
username: player.username, username: player.username,
gameId: id, gameId: id,
} as JoinGameData } as JoinGameData,
}); })
} }
/** /**
* Sends a ready action to the WebSocket service. * Sends a ready action to the WebSocket service.
*/ */
function sendReady(): void { function sendReady(): void {
wsService.send({action: GameAction.ready}); wsService.send({ action: GameAction.ready })
} }
/** /**
@ -86,44 +85,40 @@ export const GameComponent: FC<{ player: Player, map: GameMap }> = ({player, map
* to advance to the next player in the game. * to advance to the next player in the game.
*/ */
function endTurn(): void { function endTurn(): void {
wsService.send({action: GameAction.nextPlayer}); wsService.send({ action: GameAction.nextPlayer })
} }
/** /**
* Leaves the current game and navigates to the lobby. * Leaves the current game and navigates to the lobby.
*/ */
function leaveGame(): void { function leaveGame(): void {
wsService.send({action: GameAction.disconnect}); wsService.send({ action: GameAction.disconnect })
navigate("/lobby"); navigate("/lobby")
} }
useEffect(() => { useEffect(() => {
getData(`/game/exists/${id}`).then(res => {
if (!res.ok) {
return navigate("/lobby")
}
wsService.onReceive = doAction
wsService.open()
getData(`/game/exists/${id}`) wsService.waitForOpen().then(() => joinGame())
.then(res => { })
if (!res.ok) {
return navigate("/lobby");
}
wsService.onReceive = doAction;
wsService.open();
wsService.waitForOpen().then(() => joinGame()); return () => wsService.close()
}) }, [])
return () => wsService.close();
}, []);
return ( return (
<> <>
<Button onClick={leaveGame}>Leave game</Button> <Button onClick={leaveGame}>Leave game</Button>
<div className={"flex justify-center"}> <div className={"flex justify-center"}>{players?.map(p => <PlayerStats key={p.username} player={p} />)}</div>
{players?.map(p => <PlayerStats key={p.username} player={p}/>)}
</div>
<div className={"flex-center"}> <div className={"flex-center"}>
<GameButton onReadyClick={sendReady} onRollDiceClick={rollDice}/> <GameButton onReadyClick={sendReady} onRollDiceClick={rollDice} />
</div> </div>
<AllDice values={dice}/> <AllDice values={dice} />
<GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={map}/> <GameBoard className={"mx-auto my-2"} onMove={onCharacterMove} map={map} />
</> </>
); )
}; }

View File

@ -1,165 +1,151 @@
import React, {FC, useEffect, useState} from "react"; import React, { FC, 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 {Colour} from "../game/colour"; import { Colour } from "../game/colour"
interface TileWithCharacterProps extends ComponentProps { interface TileWithCharacterProps extends ComponentProps {
possiblePath?: Path, possiblePath?: Path
character?: Character, character?: Character
type?: TileType, type?: TileType
handleMoveCharacter?: Action<Path>, handleMoveCharacter?: Action<Path>
handleSelectCharacter?: Action<Character>, handleSelectCharacter?: Action<Character>
handleStartShowPath?: Action<Path>, handleStartShowPath?: Action<Path>
handleStopShowPath?: VoidFunction, handleStopShowPath?: VoidFunction
isSelected?: boolean, isSelected?: boolean
showPath?: boolean showPath?: boolean
} }
export const GameTile: FC<TileWithCharacterProps> = ( export const GameTile: FC<TileWithCharacterProps> = ({
{ possiblePath,
possiblePath, character,
character, type,
type, handleMoveCharacter,
handleMoveCharacter, handleSelectCharacter,
handleSelectCharacter, handleStartShowPath,
handleStartShowPath, handleStopShowPath,
handleStopShowPath, isSelected = false,
isSelected = false, showPath = false,
showPath = false }) => (
}) => ( <Tile
<Tile className={`${possiblePath?.end ? "border-4 border-white" : ""}`} className={`${possiblePath?.end ? "border-4 border-white" : ""}`}
type={type} type={type}
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined} onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined} onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}
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/>} )}
<AddDummy path={possiblePath}/> {showPath && <Circle />}
<AddDummy path={possiblePath} />
</> </>
</Tile> </Tile>
); )
const Circle: FC<{ colour?: Colour } & ComponentProps> = ({colour = Colour.white, className}) => ( const Circle: FC<{ colour?: Colour } & ComponentProps> = ({ colour = Colour.white, className }) => (
<div className={`flex-center w-full h-full ${className}`}> <div className={`flex-center w-full h-full ${className}`}>
<div className={`w-1/2 h-1/2 rounded-full`} <div className={`w-1/2 h-1/2 rounded-full`} style={{ backgroundColor: colour }} />
style={{backgroundColor: colour}}/>
</div> </div>
); )
interface TileProps extends ChildProps { interface TileProps extends ChildProps {
type?: TileType, type?: TileType
onClick?: VoidFunction, onClick?: VoidFunction
onMouseEnter?: VoidFunction, onMouseEnter?: VoidFunction
onMouseLeave?: VoidFunction, onMouseLeave?: VoidFunction
character?: Character, character?: Character
onCharacterClick?: Action<Character>, onCharacterClick?: Action<Character>
characterClass?: string, characterClass?: string
} }
const Tile: FC<TileProps> = ( const Tile: FC<TileProps> = ({ type = TileType.empty, onClick, onMouseEnter, onMouseLeave, className, children }) => {
{ const [tileSize, setTileSize] = useState(2)
type = TileType.empty,
onClick,
onMouseEnter,
onMouseLeave,
className,
children
}) => {
const [tileSize, setTileSize] = useState(2);
function setColor(): string { function setColor(): string {
switch (type) { switch (type) {
case TileType.wall: case TileType.wall:
return "bg-blue-500"; return "bg-blue-500"
case TileType.ghostSpawn: case TileType.ghostSpawn:
return "bg-red-500"; return "bg-red-500"
case TileType.pacmanSpawn: case TileType.pacmanSpawn:
return "bg-green-500"; // TODO should be the colour of the player return "bg-green-500" // TODO should be the colour of the player
default: default:
return "bg-black"; return "bg-black"
} }
} }
useEffect(() => { useEffect(() => {
function handleResize(): void { function handleResize(): void {
const newSize = Math.floor(window.innerWidth / 16); const newSize = Math.floor(window.innerWidth / 16)
setTileSize(newSize); setTileSize(newSize)
} }
handleResize(); handleResize()
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize)
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize)
}, []); }, [])
return ( return (
<div className={`${setColor()} hover:border relative max-w-[75px] max-h-[75px] ${className}`} <div
style={{width: `${tileSize}px`, height: `${tileSize}px`}} className={`${setColor()} hover:border relative max-w-[75px] max-h-[75px] ${className}`}
onClick={onClick} style={{ width: `${tileSize}px`, height: `${tileSize}px` }}
onMouseEnter={onMouseEnter} onClick={onClick}
onMouseLeave={onMouseLeave}> onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
{children} {children}
{type === TileType.pellet && <Circle colour={Colour.yellow}/>} {type === TileType.pellet && <Circle colour={Colour.yellow} />}
{type === TileType.powerPellet && <Circle colour={Colour.red}/>} {type === TileType.powerPellet && <Circle colour={Colour.red} />}
</div> </div>
); )
};
const AddDummy: FC<{ path?: Path } & ComponentProps> = ({path}) => (
<>
{path &&
<div className={"flex-center wh-full"}>
<CharacterComponent character={new Dummy(path)}/>
</div>
}
</>
);
interface CharacterComponentProps extends ComponentProps {
character?: Character,
onClick?: Action<Character>,
} }
const CharacterComponent: FC<CharacterComponentProps> = ( const AddDummy: FC<{ path?: Path } & ComponentProps> = ({ path }) => (
{ <>
character, {path && (
onClick, <div className={"flex-center wh-full"}>
className <CharacterComponent character={new Dummy(path)} />
}) => { </div>
)}
</>
)
interface CharacterComponentProps extends ComponentProps {
character?: Character
onClick?: Action<Character>
}
const CharacterComponent: FC<CharacterComponentProps> = ({ character, onClick, className }) => {
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:
return "right-1/4 bottom-0"; return "right-1/4 bottom-0"
case Direction.left: case Direction.left:
return "left-0 top-1/4"; return "left-0 top-1/4"
case Direction.right: case Direction.right:
return "right-0 top-1/4"; return "right-0 top-1/4"
} }
} }
if (character === undefined) return null; if (character === undefined) return null
return ( return (
<div className={`rounded-full w-4/5 h-4/5 cursor-pointer hover:border border-black relative ${className}`} <div
style={{backgroundColor: `${character.colour}`}} className={`rounded-full w-4/5 h-4/5 cursor-pointer hover:border border-black relative ${className}`}
onClick={() => onClick?.(character)}> style={{ backgroundColor: `${character.colour}` }}
onClick={() => onClick?.(character)}>
<div> <div>
<div className={`absolute ${getSide()} w-1/2 h-1/2 rounded-full bg-black`}/> <div className={`absolute ${getSide()} w-1/2 h-1/2 rounded-full bg-black`} />
</div> </div>
</div> </div>
); )
}; }

View File

@ -1,23 +1,18 @@
import React, {forwardRef} from "react"; import React, { forwardRef } from "react"
const Input: FRComponent<InputProps, HTMLInputElement> = forwardRef(( const Input: FRComponent<InputProps, HTMLInputElement> = forwardRef(
{ ({ type = "text", className, id, placeholder, required = false, name, autoComplete = "off" }, ref) => (
type = "text", <input
className, type={type}
id, autoComplete={autoComplete}
placeholder, ref={ref}
required = false, id={id}
name, name={name}
autoComplete = "off", className={"border-2 border-gray-300 rounded-md p-1 " + className}
}, ref) => ( placeholder={placeholder}
<input type={type} required={required}
autoComplete={autoComplete} />
ref={ref} ),
id={id} )
name={name}
className={"border-2 border-gray-300 rounded-md p-1 " + className}
placeholder={placeholder}
required={required}/>
));
export default Input; export default Input

View File

@ -1,13 +1,11 @@
import React, {FC} from "react"; import React, { FC } from "react"
import NavMenu from "./navMenu"; import NavMenu from "./navMenu"
const Layout: FC<ChildProps> = ({children}) => ( const Layout: FC<ChildProps> = ({ children }) => (
<div> <div>
<NavMenu/> <NavMenu />
<main> <main>{children}</main>
{children}
</main>
</div> </div>
); )
export default Layout; export default Layout

View File

@ -1,84 +1,89 @@
import React, {FC, useEffect} from "react"; import React, { FC, useEffect } from "react"
import {Link, useNavigate} from "react-router-dom"; import { Link, useNavigate } from "react-router-dom"
import {useAtom, useAtomValue} from "jotai"; import { useAtom, useAtomValue } from "jotai"
import {thisPlayerAtom} from "../utils/state"; import { thisPlayerAtom } from "../utils/state"
import {UserCircleIcon} from "@heroicons/react/24/outline"; import { UserCircleIcon } from "@heroicons/react/24/outline"
import useToggle from "../hooks/useToggle"; import useToggle from "../hooks/useToggle"
const NavMenu: FC = () => { const NavMenu: FC = () => {
const player = useAtomValue(thisPlayerAtom); const player = useAtomValue(thisPlayerAtom)
return ( return (
<header className={"z-10"}> <header className={"z-10"}>
<nav className="mb-3 flex justify-between border-b-2"> <nav className="mb-3 flex justify-between border-b-2">
<Link to="/"><h2 className={"m-1"}>Pac-Man Board Game</h2></Link> <Link to="/">
<h2 className={"m-1"}>Pac-Man Board Game</h2>
</Link>
<ul className="inline-flex gap-2 items-center mr-5 relative"> <ul className="inline-flex gap-2 items-center mr-5 relative">
<NavItem to="/">Home</NavItem> <NavItem to="/">Home</NavItem>
{ {player === undefined ? (
player === undefined ? <NavItem className={"mx-2"} to={"/login"}>
<NavItem className={"mx-2"} to={"/login"}>Login</NavItem> Login
: </NavItem>
<> ) : (
<NavItem to={"/lobby"}>Lobby</NavItem> <>
<ProfileDropdown className={"mx-2"}/> <NavItem to={"/lobby"}>Lobby</NavItem>
</> <ProfileDropdown className={"mx-2"} />
} </>
)}
</ul> </ul>
</nav> </nav>
</header> </header>
); )
}; }
export default NavMenu; export default NavMenu
const NavItem: FC<LinkProps> = ({to, children, className}) => ( const NavItem: FC<LinkProps> = ({ to, children, className }) => (
<li> <li>
<Link className={`hover:underline ${className}`} to={to}>{children}</Link> <Link className={`hover:underline ${className}`} to={to}>
{children}
</Link>
</li> </li>
); )
const ProfileDropdown: FC<ComponentProps> = ({className}) => { const ProfileDropdown: FC<ComponentProps> = ({ className }) => {
const [player, setPlayer] = useAtom(thisPlayerAtom); const [player, setPlayer] = useAtom(thisPlayerAtom)
const [isOpened, toggleOpen] = useToggle(); const [isOpened, toggleOpen] = useToggle()
const navigate = useNavigate(); const navigate = useNavigate()
async function logout(): Promise<void> { async function logout(): Promise<void> {
setPlayer(undefined); setPlayer(undefined)
navigate("/login"); navigate("/login")
} }
useEffect(() => { useEffect(() => {
if (isOpened) { if (isOpened) {
function closeIfOutsideButton(e: MouseEvent): void { function closeIfOutsideButton(e: MouseEvent): void {
if (isOpened && e.target instanceof HTMLElement) { if (isOpened && e.target instanceof HTMLElement) {
if (e.target.closest("#profile-dropdown") === null) { if (e.target.closest("#profile-dropdown") === null) {
toggleOpen(false); toggleOpen(false)
} }
} }
} }
document.addEventListener("click", closeIfOutsideButton); document.addEventListener("click", closeIfOutsideButton)
return () => document.removeEventListener("click", closeIfOutsideButton); return () => document.removeEventListener("click", closeIfOutsideButton)
} }
}, [isOpened])
}, [isOpened]);
return ( return (
<> <>
<li id={"profile-dropdown"} <li
className={`inline-flex-center cursor-pointer hover:bg-gray-100 h-full px-2 ${className}`} id={"profile-dropdown"}
onClick={() => toggleOpen()}> className={`inline-flex-center cursor-pointer hover:bg-gray-100 h-full px-2 ${className}`}
<UserCircleIcon className={"w-7"}/> onClick={() => toggleOpen()}>
<UserCircleIcon className={"w-7"} />
<span>{player?.username}</span> <span>{player?.username}</span>
</li> </li>
{ {isOpened && (
isOpened && <div className={"absolute right-2 border rounded-b -bottom-9 px-5"}>
<div className={"absolute right-2 border rounded-b -bottom-9 px-5"}> <button onClick={logout} className={"hover:underline py-1"}>
<button onClick={logout} className={"hover:underline py-1"}>Logout</button> Logout
</div> </button>
} </div>
)}
</> </>
) )
} }

View File

@ -1,29 +1,27 @@
import React, {FC} from "react"; import React, { FC } from "react"
import Player, {State} from "../game/player"; import Player, { State } from "../game/player"
import {useAtomValue} from "jotai"; import { useAtomValue } from "jotai"
import {currentPlayerNameAtom} from "../utils/state"; import { currentPlayerNameAtom } from "../utils/state"
const PlayerStats: FC<{ player: Player } & ComponentProps> = ( const PlayerStats: FC<{ player: Player } & ComponentProps> = ({ player, className, id }) => {
{ const currentPlayerName = useAtomValue(currentPlayerNameAtom)
player,
className,
id
}) => {
const currentPlayerName = useAtomValue(currentPlayerNameAtom);
return ( return (
<div key={player.colour} <div
className={`w-fit m-2 ${player.state === State.disconnected ? "text-gray-500" : ""} ${className}`} id={id}> key={player.colour}
className={`w-fit m-2 ${player.state === State.disconnected ? "text-gray-500" : ""} ${className}`}
id={id}>
<p className={player.username === currentPlayerName ? "underline" : ""}>Player: {player.username}</p> <p className={player.username === currentPlayerName ? "underline" : ""}>Player: {player.username}</p>
<p>Colour: {player.colour}</p> <p>Colour: {player.colour}</p>
{player.state === State.inGame || player.state === State.disconnected ? {player.state === State.inGame || player.state === State.disconnected ? (
<> <>
<p>Pellets: {player.box.pellets}</p> <p>Pellets: {player.box.pellets}</p>
<p>PowerPellets: {player.box.powerPellets}</p> <p>PowerPellets: {player.box.powerPellets}</p>
</> </>
: ) : (
<p>{player.state === State.waitingForPlayers ? "Waiting" : "Ready"}</p>} <p>{player.state === State.waitingForPlayers ? "Waiting" : "Ready"}</p>
)}
</div> </div>
); )
}; }
export default PlayerStats; export default PlayerStats

View File

@ -1,32 +1,31 @@
export default class Box implements BoxProps { export default class Box implements BoxProps {
public readonly colour; public readonly colour
public pellets; public pellets
public powerPellets; public powerPellets
public constructor({colour, pellets = 0, powerPellets = 0}: BoxProps) { public constructor({ colour, pellets = 0, powerPellets = 0 }: BoxProps) {
this.colour = colour; this.colour = colour
this.pellets = pellets; this.pellets = pellets
this.powerPellets = powerPellets; this.powerPellets = powerPellets
} }
public addPellet(): void { public addPellet(): void {
this.pellets++; this.pellets++
} }
public removePellet(): boolean { public removePellet(): boolean {
if (this.pellets <= 0) return false; if (this.pellets <= 0) return false
this.pellets--; this.pellets--
return true; return true
} }
public addPowerPellet(): void { public addPowerPellet(): void {
this.powerPellets++; this.powerPellets++
} }
public removePowerPellet(): boolean { public removePowerPellet(): boolean {
if (this.powerPellets <= 0) return false; if (this.powerPellets <= 0) return false
this.powerPellets--; this.powerPellets--
return true; return true
} }
} }

View File

@ -1,5 +1,5 @@
import {Direction} from "./direction"; import { Direction } from "./direction"
import {Colour} from "./colour"; import { Colour } from "./colour"
export enum CharacterType { export enum CharacterType {
pacMan, pacMan,
@ -8,89 +8,106 @@ export enum CharacterType {
} }
export class Character implements CharacterProps { export class Character implements CharacterProps {
public readonly colour; public readonly colour
public position; public position
public isEatable; public isEatable
public readonly spawnPosition; public readonly spawnPosition
public readonly type; public readonly type
public constructor( public constructor({
{ colour,
colour, position = null,
position = null, type = CharacterType.dummy,
type = CharacterType.dummy, isEatable = type === CharacterType.pacMan,
isEatable = type === CharacterType.pacMan, spawnPosition = null,
spawnPosition = null }: CharacterProps) {
}: CharacterProps) { this.colour = colour
this.colour = colour; this.isEatable = isEatable
this.isEatable = isEatable; this.spawnPosition = spawnPosition
this.spawnPosition = spawnPosition;
if (position) { if (position) {
this.position = position; this.position = position
} else { } else {
this.position = spawnPosition ? { this.position = spawnPosition
end: spawnPosition!.at, ? {
direction: spawnPosition!.direction end: spawnPosition!.at,
} : null; direction: spawnPosition!.direction,
}
: null
} }
this.type = type; this.type = type
} }
public follow(path: Path): void { public follow(path: Path): void {
if (!this.position) { if (!this.position) {
this.position = path; this.position = path
} else { } else {
this.position.end = path.end; this.position.end = path.end
this.position.direction = path.direction; this.position.direction = path.direction
this.position.path = undefined; this.position.path = undefined
} }
} }
public isPacMan(): boolean { public isPacMan(): boolean {
return this.type === CharacterType.pacMan; return this.type === CharacterType.pacMan
} }
public isGhost(): boolean { public isGhost(): boolean {
return this.type === CharacterType.ghost; return this.type === CharacterType.ghost
} }
public moveToSpawn(): void { public moveToSpawn(): void {
if (!this.spawnPosition) return; if (!this.spawnPosition) return
this.follow({end: this.spawnPosition.at, direction: this.spawnPosition.direction}); this.follow({
end: this.spawnPosition.at,
direction: this.spawnPosition.direction,
})
} }
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
} }
} }
export class PacMan extends Character implements CharacterProps { export class PacMan extends Character implements CharacterProps {
public constructor({
public constructor({colour, position, isEatable = true, spawnPosition, type = CharacterType.pacMan}: CharacterProps) { colour,
super({colour: colour, position: position, isEatable: isEatable, spawnPosition: spawnPosition, type: type}); position,
isEatable = true,
spawnPosition,
type = CharacterType.pacMan,
}: CharacterProps) {
super({
colour: colour,
position: position,
isEatable: isEatable,
spawnPosition: spawnPosition,
type: type,
})
} }
} }
export class Ghost extends Character implements CharacterProps { export class Ghost extends Character implements CharacterProps {
public constructor({ colour, position, isEatable, spawnPosition, type = CharacterType.ghost }: CharacterProps) {
public constructor({colour, position, isEatable, spawnPosition, type = CharacterType.ghost}: CharacterProps) { super({
super({colour: colour, position: position, isEatable: isEatable, spawnPosition: spawnPosition, type: type}); colour: colour,
position: position,
isEatable: isEatable,
spawnPosition: spawnPosition,
type: type,
})
} }
} }
export class Dummy extends Character implements CharacterProps { export class Dummy extends Character implements CharacterProps {
public constructor(position: Path) { public constructor(position: Path) {
super({ super({
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,
}); })
} }
} }

View File

@ -8,4 +8,4 @@ export enum Colour {
grey = "grey", grey = "grey",
} }
export const getColours = (): Colour[] => Object.values(Colour); export const getColours = (): Colour[] => Object.values(Colour)

View File

@ -2,8 +2,7 @@ export enum Direction {
left, left,
up, up,
right, right,
down down,
} }
export const getDirections = () => Object.values(Direction) export const getDirections = () => Object.values(Direction).filter(d => !isNaN(Number(d))) as Direction[]
.filter(d => !isNaN(Number(d))) as Direction[];

View File

@ -1,5 +1,5 @@
import {CharacterType} from "./character"; import { CharacterType } from "./character"
import {Direction} from "./direction"; import { Direction } from "./direction"
/** /**
* 0 = empty * 0 = empty
@ -25,29 +25,32 @@ export const customMap: GameMap = [
[1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1], [1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1, 0, 1, 2, 1],
[1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1], [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
]; ]
export function getCharacterSpawns(map: GameMap): { type: CharacterType, position: DirectionalPosition }[] { export function getCharacterSpawns(map: GameMap): { type: CharacterType; position: DirectionalPosition }[] {
const result: { type: CharacterType; position: DirectionalPosition }[] = []
const result: { type: CharacterType, position: DirectionalPosition }[] = [];
for (let row = 0; row < map.length; row++) { for (let row = 0; row < map.length; row++) {
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 },
})
} }
} }
} }
return result; return result
} }
export function getPacManSpawns(map: GameMap): DirectionalPosition[] { export function getPacManSpawns(map: GameMap): DirectionalPosition[] {
return getCharacterSpawns(map) return getCharacterSpawns(map)
.filter(s => s.type === CharacterType.pacMan) .filter(s => s.type === CharacterType.pacMan)
.map(s => s.position) .map(s => s.position)
} }

View File

@ -1,54 +1,57 @@
import {Character, CharacterType} from "./character"; import { Character, CharacterType } from "./character"
import Box from "./box"; import Box from "./box"
import {getDefaultStore} from "jotai"; import { getDefaultStore } from "jotai"
import {currentPlayerNameAtom, playersAtom} from "../utils/state"; import { currentPlayerNameAtom, playersAtom } from "../utils/state"
import rules from "./rules"; import rules from "./rules"
export enum State { export enum State {
waitingForPlayers, waitingForPlayers,
ready, ready,
inGame, inGame,
disconnected disconnected,
} }
export default class Player implements PlayerProps { export default class Player implements PlayerProps {
private static store = getDefaultStore(); private static store = getDefaultStore()
public readonly username; public readonly username
public readonly pacMan; public readonly pacMan
public readonly colour; public readonly colour
public readonly box; public readonly box
public state; public state
constructor(props: PlayerProps) { constructor(props: PlayerProps) {
this.username = props.username; this.username = props.username
this.colour = props.colour; this.colour = props.colour
this.box = new Box(props.box ?? {colour: props.colour}); this.box = new Box(props.box ?? { colour: props.colour })
this.pacMan = new Character(props.pacMan ?? { this.pacMan = new Character(
colour: props.colour, props.pacMan ?? {
type: CharacterType.pacMan colour: props.colour,
}); type: CharacterType.pacMan,
this.state = props.state ?? State.waitingForPlayers; },
)
this.state = props.state ?? State.waitingForPlayers
} }
public isTurn(): boolean { public isTurn(): boolean {
return Player.store.get(currentPlayerNameAtom) === this.username; return Player.store.get(currentPlayerNameAtom) === this.username
} }
public addPellet(): void { public addPellet(): void {
this.box.addPellet(); this.box.addPellet()
} }
public addPowerPellet(): void { public addPowerPellet(): void {
this.box.addPowerPellet(); this.box.addPowerPellet()
} }
public stealFrom(other: Player): void { public stealFrom(other: Player): void {
for (let i = 0; i < rules.maxStealPellets; i++) { for (let i = 0; i < rules.maxStealPellets; i++) {
const removed = other.box.removePellet(); const removed = other.box.removePellet()
if (removed) if (removed) this.box.addPellet()
this.box.addPellet();
} }
Player.store.set(playersAtom, Player.store.get(playersAtom).map(player => player)); Player.store.set(
playersAtom,
Player.store.get(playersAtom).map(player => player),
)
} }
} }

View File

@ -1,6 +1,6 @@
import {TileType} from "./tileType"; import { TileType } from "./tileType"
import {Character} from "./character"; import { Character } from "./character"
import {Direction, getDirections} from "./direction"; import { Direction, getDirections } from "./direction"
/** /**
* Finds all the possible positions for the character to move to * Finds all the possible positions for the character to move to
@ -10,10 +10,15 @@ import {Direction, getDirections} from "./direction";
* @param characters All the characters on the map * @param characters All the characters on the map
* @returns An array of paths the character can move to * @returns An array of paths the character can move to
*/ */
export default function findPossiblePositions(board: GameMap, character: Character, steps: number, characters: Character[]): Path[] { export default function findPossiblePositions(
if (!character.position || !character.spawnPosition) throw new Error("Character has no position or spawn position"); board: GameMap,
return findPossibleRecursive(board, character.position, steps, character, characters); character: Character,
}; steps: number,
characters: Character[],
): Path[] {
if (!character.position || !character.spawnPosition) throw new Error("Character has no position or spawn position")
return findPossibleRecursive(board, character.position, steps, character, characters)
}
/** /**
* Uses recursion to move through the map and find all the possible positions * Uses recursion to move through the map and find all the possible positions
@ -24,38 +29,40 @@ export default function findPossiblePositions(board: GameMap, character: Charact
* @param characters All the characters on the map * @param characters All the characters on the map
* @returns {Path[]} An array of paths the character can move to * @returns {Path[]} An array of paths the character can move to
*/ */
function findPossibleRecursive(map: GameMap, currentPath: Path, steps: number, character: Character, characters: Character[]): Path[] { function findPossibleRecursive(
map: GameMap,
const paths: Path[] = []; currentPath: Path,
steps: number,
character: Character,
characters: Character[],
): Path[] {
const paths: Path[] = []
if (isOutsideBoard(currentPath, map.length)) { if (isOutsideBoard(currentPath, map.length)) {
if (character.isPacMan()) { if (character.isPacMan()) {
return addTeleportationTiles(map, currentPath, steps, character, characters); return addTeleportationTiles(map, currentPath, steps, character, characters)
} }
} else if (!isWall(map, currentPath)) { } else if (!isWall(map, currentPath)) {
if (!characterHitsAnotherCharacter(character, currentPath, characters)) { if (!characterHitsAnotherCharacter(character, currentPath, characters)) {
if (steps <= 0) { if (steps <= 0) {
if (!(isSpawn(map, currentPath) && !isOwnSpawn(currentPath, character))) { if (!(isSpawn(map, currentPath) && !isOwnSpawn(currentPath, character))) {
paths.push(currentPath); paths.push(currentPath)
} }
} else { } else {
tryAddToPath(currentPath)
tryAddToPath(currentPath); steps--
steps--;
for (const direction of getDirections()) { for (const direction of getDirections()) {
paths.push(...tryMove(map, currentPath, direction, steps, character, characters)); paths.push(...tryMove(map, currentPath, direction, steps, character, characters))
} }
} }
} else { } else {
const pacMan = ghostHitsPacMan(character, currentPath, characters); const pacMan = ghostHitsPacMan(character, currentPath, characters)
if (pacMan instanceof Character && !isCharactersSpawn(currentPath, pacMan)) { if (pacMan instanceof Character && !isCharactersSpawn(currentPath, pacMan)) {
paths.push(currentPath); paths.push(currentPath)
} }
} }
} }
return paths; return paths
} }
/** /**
@ -65,7 +72,7 @@ function findPossibleRecursive(map: GameMap, currentPath: Path, steps: number, c
* @returns {boolean} True if the character is on its spawn, otherwise false * @returns {boolean} True if the character is on its spawn, otherwise false
*/ */
function isCharactersSpawn(currentPath: Path, character: Character): boolean { function isCharactersSpawn(currentPath: Path, character: Character): boolean {
return character.spawnPosition?.at.x === currentPath.end.x && character.spawnPosition.at.y === currentPath.end.y; return character.spawnPosition?.at.x === currentPath.end.x && character.spawnPosition.at.y === currentPath.end.y
} }
/** /**
@ -75,8 +82,12 @@ function isCharactersSpawn(currentPath: Path, character: Character): boolean {
* @param characters All the characters on the board * @param characters All the characters on the board
* @returns {boolean} True if the character is a ghost and hits Pac-Man, otherwise false * @returns {boolean} True if the character is a ghost and hits Pac-Man, otherwise false
*/ */
function ghostHitsPacMan(character: Character, currentPath: Path, characters: Character[]): Character | undefined | false { function ghostHitsPacMan(
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.end)); character: Character,
currentPath: Path,
characters: Character[],
): Character | undefined | false {
return character.isGhost() && characters.find(c => c.isPacMan() && c.isAt(currentPath.end))
} }
/** /**
@ -87,7 +98,7 @@ function ghostHitsPacMan(character: Character, currentPath: Path, characters: Ch
* @returns {boolean} True if the character hits another character, otherwise false * @returns {boolean} True if the character hits another character, otherwise false
*/ */
function characterHitsAnotherCharacter(character: Character, currentPath: Path, characters: Character[]): boolean { 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
} }
/** /**
@ -96,9 +107,9 @@ function characterHitsAnotherCharacter(character: Character, currentPath: Path,
*/ */
function tryAddToPath(currentPos: Path): void { function tryAddToPath(currentPos: Path): void {
if (!currentPos.path) { if (!currentPos.path) {
currentPos.path = []; currentPos.path = []
} else if (!currentPos.path.includes(currentPos.end)) { } else if (!currentPos.path.includes(currentPos.end)) {
currentPos.path = [...currentPos.path, currentPos.end]; currentPos.path = [...currentPos.path, currentPos.end]
} }
} }
@ -113,39 +124,53 @@ function tryAddToPath(currentPos: Path): void {
* @param characters All the characters on the board * @param characters All the characters on the board
* @returns An array of paths the character can move to * @returns An array of paths the character can move to
*/ */
function tryMove(board: GameMap, path: Path, direction: Direction, steps: number, character: Character, characters: Character[]): Path[] { function tryMove(
board: GameMap,
path: Path,
direction: Direction,
steps: number,
character: Character,
characters: Character[],
): Path[] {
function getNewPosition(): Position { function getNewPosition(): Position {
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,
}; }
} }
} }
if (path.direction !== (direction + 2) % 4) { if (path.direction !== (direction + 2) % 4) {
return findPossibleRecursive(board, { return findPossibleRecursive(
end: getNewPosition(), direction: direction, path: path.path board,
}, steps, character, characters); {
end: getNewPosition(),
direction: direction,
path: path.path,
},
steps,
character,
characters,
)
} }
return []; return []
} }
/** /**
@ -157,22 +182,26 @@ function tryMove(board: GameMap, path: Path, direction: Direction, steps: number
* @param characters All the characters on the map * @param characters All the characters on the map
* @returns {Path[]} An array of paths the character can move to * @returns {Path[]} An array of paths the character can move to
*/ */
function addTeleportationTiles(board: GameMap, currentPath: Path, steps: number, character: Character, characters: Character[]): Path[] { function addTeleportationTiles(
const possiblePositions = findTeleportationTiles(board); board: GameMap,
const paths: Path[] = []; currentPath: Path,
steps: number,
character: Character,
characters: Character[],
): Path[] {
const possiblePositions = findTeleportationTiles(board)
const paths: Path[] = []
for (const pos of possiblePositions) { for (const pos of possiblePositions) {
function inInterval(coordinate: "x" | "y"): boolean { function inInterval(coordinate: "x" | "y"): boolean {
return pos.end[coordinate] !== interval(0, board.length - 1, currentPath.end[coordinate]) return pos.end[coordinate] !== interval(0, board.length - 1, currentPath.end[coordinate])
} }
if (inInterval("x") || inInterval("y")) { if (inInterval("x") || inInterval("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));
} }
} }
return paths; return paths
} }
/** /**
@ -183,7 +212,7 @@ function addTeleportationTiles(board: GameMap, currentPath: Path, steps: number,
* @returns {number} The value if it's between the lower and upper bounds, otherwise it returns the lower or upper bound * @returns {number} The value if it's between the lower and upper bounds, otherwise it returns the lower or upper bound
*/ */
function interval(lower: number, upper: number, value: number): number { function interval(lower: number, upper: number, value: number): number {
return Math.max(Math.min(value, upper), lower); return Math.max(Math.min(value, upper), lower)
} }
/** /**
@ -192,18 +221,17 @@ function interval(lower: number, upper: number, value: number): number {
* @returns An array of paths containing the teleportation tiles * @returns An array of paths containing the teleportation tiles
*/ */
function findTeleportationTiles(map: GameMap): Path[] { function findTeleportationTiles(map: GameMap): Path[] {
const possiblePositions: Path[] = []; const possiblePositions: Path[] = []
const edge = [0, map.length - 1]; const edge = [0, map.length - 1]
for (const e of edge) { for (const e of edge) {
for (let i = 0; i < map[e].length; i++) { for (let i = 0; i < map[e].length; i++) {
pushPath(map, possiblePositions, i, e)
pushPath(map, possiblePositions, i, e); pushPath(map, possiblePositions, e, i)
pushPath(map, possiblePositions, e, i);
} }
} }
return possiblePositions; return possiblePositions
} }
/** /**
@ -215,7 +243,10 @@ function findTeleportationTiles(map: 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[y] && board[y][x] !== TileType.wall) { if (board[y] && board[y][x] !== TileType.wall) {
possiblePositions.push({end: {x: x, y: y}, direction: findDirection(x, y, board.length)}); possiblePositions.push({
end: { x: x, y: y },
direction: findDirection(x, y, board.length),
})
} }
} }
@ -226,17 +257,17 @@ function pushPath(board: GameMap, possiblePositions: Path[], x: number, y: numbe
* @param boardSize The length of the board * @param boardSize The length of the board
*/ */
function findDirection(x: number, y: number, boardSize: number): Direction { function findDirection(x: number, y: number, boardSize: number): Direction {
let direction: Direction; let direction: Direction
if (x === 0) { if (x === 0) {
direction = Direction.right; direction = Direction.right
} else if (y === 0) { } else if (y === 0) {
direction = Direction.down; direction = Direction.down
} else if (x === boardSize - 1) { } else if (x === boardSize - 1) {
direction = Direction.left; direction = Direction.left
} else { } else {
direction = Direction.up; direction = Direction.up
} }
return direction; return direction
} }
/** /**
@ -245,8 +276,8 @@ function findDirection(x: number, y: number, boardSize: number): Direction {
* @param boardSize The size of the board * @param boardSize The size of the board
*/ */
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
} }
/** /**
@ -255,8 +286,8 @@ function isOutsideBoard(currentPos: Path, boardSize: number): boolean {
* @param currentPos The current position of the character * @param currentPos The current position of the character
*/ */
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
} }
/** /**
@ -265,8 +296,8 @@ function isWall(board: GameMap, currentPos: Path): boolean {
* @param currentPos The current position of the character * @param currentPos The current position of the character
*/ */
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
} }
/** /**
@ -275,8 +306,7 @@ function isSpawn(board: GameMap, currentPos: Path) {
* @param character The current character * @param character The current character
*/ */
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
} }

View File

@ -4,4 +4,4 @@ const rules = {
maxStealPellets: 2, maxStealPellets: 2,
} }
export default rules; export default rules

View File

@ -5,4 +5,4 @@ export enum TileType {
powerPellet, powerPellet,
ghostSpawn, ghostSpawn,
pacmanSpawn, pacmanSpawn,
} }

View File

@ -1,4 +1,4 @@
import {useState} from "react"; import { useState } from "react"
/** /**
* A hook that returns a boolean value and a function to toggle it. The function can optionally be passed a boolean * A hook that returns a boolean value and a function to toggle it. The function can optionally be passed a boolean
@ -6,7 +6,7 @@ import {useState} from "react";
* @returns A tuple containing the boolean value and a function to toggle it. * @returns A tuple containing the boolean value and a function to toggle it.
*/ */
export default function useToggle(defaultValue = false): [boolean, (value?: boolean) => void] { export default function useToggle(defaultValue = false): [boolean, (value?: boolean) => void] {
const [value, setValue] = useState(defaultValue); const [value, setValue] = useState(defaultValue)
const toggleValue = (newValue?: boolean) => newValue ? setValue(newValue) : setValue(!value); const toggleValue = (newValue?: boolean) => (newValue ? setValue(newValue) : setValue(!value))
return [value, toggleValue]; return [value, toggleValue]
} }

View File

@ -3,35 +3,37 @@
@tailwind utilities; @tailwind utilities;
.debug { .debug {
@apply border border-red-500; @apply border border-red-500;
@apply after:content-['debug'] after:absolute; @apply after:content-['debug'] after:absolute;
} }
.flex-center { .flex-center {
@apply flex justify-center items-center; @apply flex justify-center items-center;
} }
.inline-flex-center { .inline-flex-center {
@apply inline-flex justify-center items-center; @apply inline-flex justify-center items-center;
} }
.wh-full { .wh-full {
@apply w-full h-full; @apply w-full h-full;
} }
h1 { h1 {
@apply text-4xl; @apply text-4xl;
} }
h2 { h2 {
@apply text-3xl; @apply text-3xl;
} }
br { br {
@apply my-2; @apply my-2;
} }
.button-default, button[type=submit], input[type=submit] { .button-default,
@apply bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded; button[type="submit"],
@apply disabled:bg-gray-500; input[type="submit"] {
@apply bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded;
@apply disabled:bg-gray-500;
} }

View File

@ -1,25 +1,26 @@
import React from 'react'; import React from "react"
import {createRoot} from 'react-dom/client'; import { createRoot } from "react-dom/client"
import {BrowserRouter} from 'react-router-dom'; import { BrowserRouter } from "react-router-dom"
import {App} from './App'; import { App } from "./App"
// @ts-ignore // @ts-ignore
import reportWebVitals from './reportWebVitals'; import reportWebVitals from "./reportWebVitals"
import {DevTools} from "jotai-devtools"; import { DevTools } from "jotai-devtools"
import DebugMenu from "./components/debugMenu"; import DebugMenu from "./components/debugMenu"
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href'); const baseUrl = document.getElementsByTagName("base")[0].getAttribute("href")
const rootElement = document.getElementById('root'); const rootElement = document.getElementById("root")
if (rootElement === null) throw new Error("Root element is null"); if (rootElement === null) throw new Error("Root element is null")
const root = createRoot(rootElement); const root = createRoot(rootElement)
root.render( root.render(
<BrowserRouter basename={baseUrl ?? undefined}> <BrowserRouter basename={baseUrl ?? undefined}>
<DevTools/> <DevTools />
<DebugMenu/> <DebugMenu />
<App/> <App />
</BrowserRouter>); </BrowserRouter>,
)
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals(); reportWebVitals()

View File

@ -1,29 +1,27 @@
import React, {FC} from "react"; import React, { FC } from "react"
import WebSocketService from "../websockets/WebSocketService"; import WebSocketService from "../websockets/WebSocketService"
const ws = new WebSocketService("wss://localhost:3000/api/ws"); const ws = new WebSocketService("wss://localhost:3000/api/ws")
export const Counter: FC = () => { export const Counter: FC = () => {
const [currentCount, setCurrentCount] = React.useState(0)
const [currentCount, setCurrentCount] = React.useState(0); function incrementCounterAndSend() {
async function incrementCounterAndSend() {
if (ws.isOpen()) { if (ws.isOpen()) {
await ws.send((currentCount + 1).toString()); ws.send((currentCount + 1).toString())
} }
} }
function receiveMessage(data: MessageEvent<string>) { function receiveMessage(data: MessageEvent<string>) {
const count = parseInt(data.data); const count = parseInt(data.data)
if (!isNaN(count)) if (!isNaN(count)) setCurrentCount(count)
setCurrentCount(count);
} }
React.useEffect(() => { React.useEffect(() => {
ws.onReceive = receiveMessage; ws.onReceive = receiveMessage
ws.open(); ws.open()
return () => ws.close(); return () => ws.close()
}, []); }, [])
return ( return (
<div> <div>
@ -31,9 +29,13 @@ export const Counter: FC = () => {
<p>This is a simple example of a React component.</p> <p>This is a simple example of a React component.</p>
<p aria-live="polite">Current count: <strong>{currentCount}</strong></p> <p aria-live="polite">
Current count: <strong>{currentCount}</strong>
</p>
<button className="btn btn-primary" onClick={incrementCounterAndSend}>Increment</button> <button className="btn btn-primary" onClick={incrementCounterAndSend}>
Increment
</button>
</div> </div>
); )
}; }

View File

@ -1,17 +1,16 @@
import React, {FC} from "react"; import React, { FC } from "react"
import {GameComponent} from "../components/gameComponent"; import { GameComponent } from "../components/gameComponent"
import {useAtomValue} from "jotai"; import { useAtomValue } from "jotai"
import {selectedMapAtom, thisPlayerAtom} from "../utils/state"; import { selectedMapAtom, thisPlayerAtom } from "../utils/state"
const GamePage: FC = () => { const GamePage: FC = () => {
const player = useAtomValue(thisPlayerAtom); const player = useAtomValue(thisPlayerAtom)
const map = useAtomValue(selectedMapAtom); const map = useAtomValue(selectedMapAtom)
if (player && map) { if (player && map) {
return <GameComponent player={player} map={map}/>; return <GameComponent player={player} map={map} />
} else {
return null;
} }
}; return null
}
export default GamePage; export default GamePage

View File

@ -1,27 +1,35 @@
import React, {FC} from "react"; import React, { FC } from "react"
import {Link} from "react-router-dom"; import { Link } from "react-router-dom"
import {useAtomValue} from "jotai"; import { useAtomValue } from "jotai"
import {thisPlayerAtom} from "../utils/state"; import { thisPlayerAtom } from "../utils/state"
const HomePage: FC = () => { const HomePage: FC = () => {
const player = useAtomValue(thisPlayerAtom); const player = useAtomValue(thisPlayerAtom)
return ( return (
<div className={"container max-w-[800px] mx-auto px-2"}> <div className={"container max-w-[800px] mx-auto px-2"}>
<h1 className={"w-fit mx-auto"}>Hello {player?.username ?? "Player"}. Do you want to play a game?</h1> <h1 className={"w-fit mx-auto"}>Hello {player?.username ?? "Player"}. Do you want to play a game?</h1>
<p className={"text-center mt-5"}> <p className={"text-center mt-5"}>
{!player ? {!player ? (
<>Start by {" "} <>
<Link to={"/login"} className={"text-blue-600"}>logging in</Link>. Start by{" "}
<Link to={"/login"} className={"text-blue-600"}>
logging in
</Link>
.
</> </>
: ) : (
<>Go to the {" "} <>
<Link to={"/lobby"} className={"text-blue-600"}>lobby</Link> to select a game. Go to the{" "}
<Link to={"/lobby"} className={"text-blue-600"}>
lobby
</Link>{" "}
to select a game.
</> </>
} )}
</p> </p>
</div> </div>
); )
}; }
export default HomePage; export default HomePage

View File

@ -1,66 +1,65 @@
import React, {FC, Suspense} from "react"; import React, { FC, Suspense } from "react"
import {atom, useAtomValue} from "jotai"; import { atom, useAtomValue } from "jotai"
import {Button} from "../components/button"; import { Button } from "../components/button"
import {selectedMapAtom, thisPlayerAtom} from "../utils/state"; import { selectedMapAtom, thisPlayerAtom } from "../utils/state"
import {getData, postData} from "../utils/api"; import { getData, postData } from "../utils/api"
import {getPacManSpawns} from "../game/map"; import { getPacManSpawns } from "../game/map"
import {useNavigate} from "react-router-dom"; import { useNavigate } from "react-router-dom"
const fetchAtom = atom(async () => { const fetchAtom = atom(async () => {
const response = await getData("/game/all"); const response = await getData("/game/all")
return await response.json() as Game[]; return (await response.json()) as Game[]
}); })
const LobbyPage: FC = () => { const LobbyPage: FC = () => {
const thisPlayer = useAtomValue(thisPlayerAtom)
const thisPlayer = useAtomValue(thisPlayerAtom); const navigate = useNavigate()
const navigate = useNavigate(); const map = useAtomValue(selectedMapAtom)
const map = useAtomValue(selectedMapAtom);
async function createGame(): Promise<void> { async function createGame(): Promise<void> {
const response = await postData("/game/create", { const response = await postData("/game/create", {
body: {player: thisPlayer, spawns: getPacManSpawns(map)} as CreateGameData body: {
}); player: thisPlayer,
spawns: getPacManSpawns(map),
} as CreateGameData,
})
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json()
navigate("/game/" + data.id) navigate("/game/" + data.id)
} else { } else {
const data = await response.text(); const data = await response.text()
console.error("Error: ", data); console.error("Error: ", data)
// TODO display error // TODO display error
} }
} }
return ( return (
<> <>
<Button onClick={createGame}>New game</Button> <Button onClick={createGame}>New game</Button>
<Suspense fallback={"Please wait"}> <Suspense fallback={"Please wait"}>
<GameTable className={"mx-auto"}/> <GameTable className={"mx-auto"} />
</Suspense> </Suspense>
</> </>
); )
} }
export default LobbyPage; export default LobbyPage
const GameTable: FC<ComponentProps> = ({className}) => { const GameTable: FC<ComponentProps> = ({ className }) => {
const data = useAtomValue(fetchAtom)
const data = useAtomValue(fetchAtom); const thisPlayer = useAtomValue(thisPlayerAtom)
const thisPlayer = useAtomValue(thisPlayerAtom); const navigate = useNavigate()
const navigate = useNavigate();
async function joinGame(gameId: string): Promise<void> { async function joinGame(gameId: string): Promise<void> {
if (thisPlayer === undefined) throw new Error("Player is undefined"); if (thisPlayer === undefined) throw new Error("Player is undefined")
const result = await postData("/game/join/" + gameId, {body: thisPlayer}); const result = await postData("/game/join/" + gameId, { body: thisPlayer })
if (result.ok) { if (result.ok) {
navigate("/game/" + gameId); navigate("/game/" + gameId)
} else { } else {
console.error("Failed to join game " + gameId, await result.text()); console.error("Failed to join game " + gameId, await result.text())
// TODO show error message // TODO show error message
} }
} }
@ -68,33 +67,34 @@ const GameTable: FC<ComponentProps> = ({className}) => {
return ( return (
<table className={`rounded overflow-hidden ${className}`}> <table className={`rounded overflow-hidden ${className}`}>
<thead className={"bg-gray-500 text-white"}> <thead className={"bg-gray-500 text-white"}>
<tr className={"my-5"}> <tr className={"my-5"}>
<th className={"p-2"}>Id</th> <th className={"p-2"}>Id</th>
<th className={"p-2"}>Count</th> <th className={"p-2"}>Count</th>
<th className={"p-2"}>State</th> <th className={"p-2"}>State</th>
<th className={"p-2"}>Join</th> <th className={"p-2"}>Join</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.map(game => {data?.map(game => (
<tr key={game.id} className={"even:bg-gray-200"}> <tr key={game.id} className={"even:bg-gray-200"}>
<td className={"p-2"}>{game.id}</td> <td className={"p-2"}>{game.id}</td>
<td className={"text-center"}>{game.count}</td> <td className={"text-center"}>{game.count}</td>
<td>{game.isGameStarted ? "Closed" : "Open"}</td> <td>{game.isGameStarted ? "Closed" : "Open"}</td>
<td className={"p-2"}> <td className={"p-2"}>
<Button disabled={game.isGameStarted} onClick={() => joinGame(game.id)}> <Button disabled={game.isGameStarted} onClick={() => joinGame(game.id)}>
Join Join
</Button> </Button>
</td> </td>
</tr>
)}
{
data?.length === 0 &&
<tr>
<td colSpan={4} className={"text-center"}>No games found</td>
</tr> </tr>
} ))}
{data?.length === 0 && (
<tr>
<td colSpan={4} className={"text-center"}>
No games found
</td>
</tr>
)}
</tbody> </tbody>
</table> </table>
); )
} }

View File

@ -1,23 +1,22 @@
import React, {FC, FormEvent, useState} from "react"; import React, { FC, FormEvent, useState } from "react"
import {Button} from "../components/button"; import { Button } from "../components/button"
import Input from "../components/input"; import Input from "../components/input"
import {useSetAtom} from "jotai"; import { useSetAtom } from "jotai"
import {thisPlayerAtom} from "../utils/state"; import { thisPlayerAtom } from "../utils/state"
import Player from "../game/player"; import Player from "../game/player"
import {useNavigate} from "react-router-dom"; import { useNavigate } from "react-router-dom"
import {postData} from "../utils/api"; import { postData } from "../utils/api"
const LoginPage: FC = () => { const LoginPage: FC = () => {
const setThisPlayer = useSetAtom(thisPlayerAtom)
const setThisPlayer = useSetAtom(thisPlayerAtom); const navigate = useNavigate()
const navigate = useNavigate(); const [error, setError] = useState<string | undefined>()
const [error, setError] = useState<string | undefined>();
async function handleLogin(e: FormEvent<HTMLFormElement>): Promise<void> { async function handleLogin(e: FormEvent<HTMLFormElement>): Promise<void> {
e.preventDefault(); e.preventDefault()
const fields = e.currentTarget.querySelectorAll("input"); const fields = e.currentTarget.querySelectorAll("input")
let user: User = {username: "", password: ""}; let user: User = { username: "", password: "" }
for (const field of fields) { for (const field of fields) {
user = { user = {
...user, ...user,
@ -26,36 +25,41 @@ const LoginPage: FC = () => {
} }
const response = await postData("/player/login", { const response = await postData("/player/login", {
body: {username: user.username, password: user.password} as User body: { username: user.username, password: user.password } as User,
}) })
if (response.ok) { if (response.ok) {
const data = await response.json() as PlayerProps; const data = (await response.json()) as PlayerProps
setThisPlayer(new Player(data)); setThisPlayer(new Player(data))
navigate("/lobby"); navigate("/lobby")
} else { } else {
const data = await response.text(); const data = await response.text()
console.error(data); console.error(data)
setError(data); setError(data)
} }
} }
const username = "username", password = "password"; const username = "username",
password = "password"
return ( return (
<form onSubmit={handleLogin} className={"container w-fit mx-auto flex flex-col gap-2"}> <form onSubmit={handleLogin} className={"container w-fit mx-auto flex flex-col gap-2"}>
<h1 className={"my-5"}>Login</h1> <h1 className={"my-5"}>Login</h1>
{error && <p className={"text-red-500"}>{error}</p>} {error && <p className={"text-red-500"}>{error}</p>}
<label htmlFor={username}>Username:</label> <label htmlFor={username}>Username:</label>
<Input id={username} name={username} placeholder={"Username"} autoComplete={"username"} required/> <Input id={username} name={username} placeholder={"Username"} autoComplete={"username"} required />
<label htmlFor={password}>Password:</label> <label htmlFor={password}>Password:</label>
<Input id={password} name={password} type={"password"} placeholder={"Password"} <Input
autoComplete={"current-password"} required/> id={password}
name={password}
type={"password"}
placeholder={"Password"}
autoComplete={"current-password"}
required
/>
<Button type={"submit"}>Login</Button> <Button type={"submit"}>Login</Button>
</form> </form>
); )
} }
export default LoginPage; export default LoginPage

View File

@ -1,13 +1,13 @@
const reportWebVitals = (onPerfEntry) => { const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry)
getFID(onPerfEntry); getFID(onPerfEntry)
getFCP(onPerfEntry); getFCP(onPerfEntry)
getLCP(onPerfEntry); getLCP(onPerfEntry)
getTTFB(onPerfEntry); getTTFB(onPerfEntry)
}); })
} }
}; }
export default reportWebVitals; export default reportWebVitals

View File

@ -1,25 +1,27 @@
type FRComponent<T = ComponentProps, HTML extends HTMLElement = HTMLElement> = React.ForwardRefExoticComponent<React.PropsWithoutRef<T> & React.RefAttributes<HTML>>; type FRComponent<T = ComponentProps, HTML extends HTMLElement = HTMLElement> = React.ForwardRefExoticComponent<
React.PropsWithoutRef<T> & React.RefAttributes<HTML>
>
interface ComponentProps { interface ComponentProps {
className?: string, className?: string
style?: React.CSSProperties, style?: React.CSSProperties
id?: string, id?: string
title?: string, title?: string
} }
interface ChildProps extends ComponentProps { interface ChildProps extends ComponentProps {
children?: React.JSX.Element | string, children?: React.JSX.Element | string
} }
interface LinkProps extends ChildProps { interface LinkProps extends ChildProps {
to: string, to: string
newTab?: boolean, newTab?: boolean
} }
interface ButtonProps extends ChildProps { interface ButtonProps extends ChildProps {
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void, onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
disabled?: boolean, disabled?: boolean
type?: "button" | "submit" | "reset", type?: "button" | "submit" | "reset"
} }
interface InputProps extends ComponentProps { interface InputProps extends ComponentProps {
@ -31,23 +33,23 @@ interface InputProps extends ComponentProps {
} }
interface CharacterProps { interface CharacterProps {
colour: import("../game/colour").Colour, colour: import("../game/colour").Colour
position?: Path | null, position?: Path | null
isEatable?: boolean, isEatable?: boolean
spawnPosition?: DirectionalPosition | null, spawnPosition?: DirectionalPosition | null
type?: import("../game/character").CharacterType, type?: import("../game/character").CharacterType
} }
interface BoxProps { interface BoxProps {
pellets?: number, pellets?: number
powerPellets?: number, powerPellets?: number
readonly colour: import("../game/colour").Colour, readonly colour: import("../game/colour").Colour
} }
interface PlayerProps { interface PlayerProps {
readonly username: string, readonly username: 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

@ -1,68 +1,68 @@
type MessageEventFunction<T = any> = (data: MessageEvent<T>) => void; type MessageEventFunction<T = any> = (data: MessageEvent<T>) => void
type Setter<T> = React.Dispatch<React.SetStateAction<T>>; type Setter<T> = React.Dispatch<React.SetStateAction<T>>
type GUID = `${string}-${string}-${string}-${string}-${string}`; type GUID = `${string}-${string}-${string}-${string}-${string}`
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView; type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView
type ActionMessage<T = any> = { type ActionMessage<T = any> = {
readonly action: import("../utils/actions").GameAction, readonly action: import("../utils/actions").GameAction
readonly data?: T readonly data?: T
} }
type Action<T> = (obj: T) => void; type Action<T> = (obj: T) => void
type BiAction<T1, T2> = (obj1: T1, obj2: T2) => void; type BiAction<T1, T2> = (obj1: T1, obj2: T2) => void
type Predicate<T> = (obj: T) => boolean; type Predicate<T> = (obj: T) => boolean
type SelectedDice = { type SelectedDice = {
value: number, value: number
index: number index: number
}; }
type Position = { x: number, y: number }; type Position = { x: number; y: number }
type GameMap = number[][]; type GameMap = number[][]
type DirectionalPosition = { type DirectionalPosition = {
at: Position, at: Position
direction: import("../game/direction").Direction direction: import("../game/direction").Direction
} }
type Path = { type Path = {
path?: Position[] | null, path?: Position[] | null
// TODO replace with DirectionalPosition // TODO replace with DirectionalPosition
end: Position, end: Position
direction: import("../game/direction").Direction direction: import("../game/direction").Direction
} }
type Game = { type Game = {
readonly id: string, readonly id: string
readonly count: number, readonly count: number
readonly isGameStarted: boolean, readonly isGameStarted: boolean
} }
type User = { type User = {
readonly username: string, readonly username: string
readonly password: string, readonly password: string
readonly colour?: import("../game/colour").Colour readonly colour?: import("../game/colour").Colour
} }
type Api<T = ApiRequest> = (path: string, data?: ApiRequest & T) => Promise<Response>; type Api<T = ApiRequest> = (path: string, data?: ApiRequest & T) => Promise<Response>
type ApiRequest = { type ApiRequest = {
headers?: HeadersInit, headers?: HeadersInit
body?: any body?: any
} }
type JoinGameData = { type JoinGameData = {
readonly username: string, readonly username: string
readonly gameId: GUID, readonly gameId: GUID
} }
type CreateGameData = { type CreateGameData = {
readonly player: PlayerProps, readonly player: PlayerProps
readonly spawns: DirectionalPosition[], readonly spawns: DirectionalPosition[]
} }

View File

@ -1,10 +1,10 @@
import Player from "../game/player"; import Player from "../game/player"
import {CharacterType, Ghost} from "../game/character"; import { CharacterType, Ghost } from "../game/character"
import {getCharacterSpawns} from "../game/map"; import { getCharacterSpawns } from "../game/map"
import {TileType} from "../game/tileType"; import { TileType } from "../game/tileType"
import {getDefaultStore} from "jotai"; import { getDefaultStore } from "jotai"
import {currentPlayerNameAtom, diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedMapAtom} from "./state"; import { currentPlayerNameAtom, diceAtom, ghostsAtom, playersAtom, rollDiceButtonAtom, selectedMapAtom } from "./state"
import {Colour} from "../game/colour"; import { Colour } from "../game/colour"
export enum GameAction { export enum GameAction {
error, error,
@ -17,107 +17,112 @@ export enum GameAction {
// TODO add updatePellets // TODO add updatePellets
} }
const store = getDefaultStore(); const store = getDefaultStore()
const map = store.get(selectedMapAtom); const map = store.get(selectedMapAtom)
const ghostsProps: CharacterProps[] = [ const ghostsProps: CharacterProps[] = [{ colour: Colour.purple }, { colour: Colour.purple }]
{colour: Colour.purple}, let spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.ghost)
{colour: Colour.purple},
];
let spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.ghost);
ghostsProps.forEach(ghost => { ghostsProps.forEach(ghost => {
ghost.spawnPosition = spawns.pop()?.position; ghost.spawnPosition = spawns.pop()?.position
})
const ghosts = ghostsProps.map(props => new Ghost(props))
}); store.set(ghostsAtom, ghosts)
const ghosts = ghostsProps.map(props => new Ghost(props));
store.set(ghostsAtom, ghosts);
export const doAction: MessageEventFunction<string> = (event): void => { export const doAction: MessageEventFunction<string> = (event): void => {
const message: ActionMessage = JSON.parse(event.data); const message: ActionMessage = JSON.parse(event.data)
console.debug("Received message:", message); console.debug("Received message:", message)
switch (message.action as GameAction) { switch (message.action as GameAction) {
case GameAction.error: case GameAction.error:
console.error("Error:", message.data); console.error("Error:", message.data)
break; break
case GameAction.rollDice: case GameAction.rollDice:
setDice(message.data); setDice(message.data)
break; break
case GameAction.moveCharacter: case GameAction.moveCharacter:
moveCharacter(message.data); moveCharacter(message.data)
break; break
case GameAction.joinGame: case GameAction.joinGame:
joinGame(message.data); joinGame(message.data)
break; break
case GameAction.ready: case GameAction.ready:
ready(message.data); ready(message.data)
break; break
case GameAction.nextPlayer: case GameAction.nextPlayer:
nextPlayer(message.data); nextPlayer(message.data)
break; break
case GameAction.disconnect: case GameAction.disconnect:
updatePlayers(message.data); updatePlayers(message.data)
break; break
} }
};
function setDice(data?: number[]): void {
store.set(diceAtom, data);
} }
type MoveCharacterData = { dice: number[], players: PlayerProps[], ghosts: CharacterProps[], eatenPellets: Position[] }; function setDice(data?: number[]): void {
store.set(diceAtom, data)
}
type MoveCharacterData = {
dice: number[]
players: PlayerProps[]
ghosts: CharacterProps[]
eatenPellets: Position[]
}
function moveCharacter(data?: MoveCharacterData): void { function moveCharacter(data?: MoveCharacterData): void {
store.set(diceAtom, data?.dice); store.set(diceAtom, data?.dice)
updatePlayers(data?.players); updatePlayers(data?.players)
updateGhosts(data); updateGhosts(data)
removeEatenPellets(data); removeEatenPellets(data)
} }
function updatePlayers(updatedPlayers?: PlayerProps[]): void { function updatePlayers(updatedPlayers?: PlayerProps[]): void {
if (updatedPlayers) { if (updatedPlayers) {
const newList: Player[] = updatedPlayers.map(p => new Player(p)); const newList: Player[] = updatedPlayers.map(p => new Player(p))
store.set(playersAtom, newList); store.set(playersAtom, newList)
} }
} }
function updateGhosts(data?: MoveCharacterData): void { function updateGhosts(data?: MoveCharacterData): void {
const updatedGhosts = data?.ghosts; const updatedGhosts = data?.ghosts
if (updatedGhosts) { if (updatedGhosts) {
const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g)); const newList: Ghost[] = updatedGhosts.map(g => new Ghost(g))
store.set(ghostsAtom, newList); store.set(ghostsAtom, newList)
} }
} }
function removeEatenPellets(data?: MoveCharacterData): void { function removeEatenPellets(data?: MoveCharacterData): void {
const pellets = data?.eatenPellets; const pellets = data?.eatenPellets
for (const pellet of pellets ?? []) { for (const pellet of pellets ?? []) {
map[pellet.y][pellet.x] = TileType.empty; map[pellet.y][pellet.x] = TileType.empty
} }
} }
function joinGame(data?: PlayerProps[]): void { // TODO missing data when refreshing page function joinGame(data?: PlayerProps[]): void {
const playerProps = data ?? []; // TODO missing data when refreshing page
spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.pacMan); const playerProps = data ?? []
store.set(playersAtom, playerProps.map(p => new Player(p))); spawns = getCharacterSpawns(map).filter(spawn => spawn.type === CharacterType.pacMan)
store.set(
playersAtom,
playerProps.map(p => new Player(p)),
)
} }
type ReadyData = { allReady: boolean, players: PlayerProps[] }; type ReadyData = { allReady: boolean; players: PlayerProps[] }
function ready(data?: ReadyData): void { function ready(data?: ReadyData): void {
if (data) { if (data) {
const players = data.players.map(p => new Player(p)); const players = data.players.map(p => new Player(p))
store.set(playersAtom, players); store.set(playersAtom, players)
if (data.allReady) { if (data.allReady) {
store.set(currentPlayerNameAtom, data.players[0].username); store.set(currentPlayerNameAtom, data.players[0].username)
} }
} }
} }
function nextPlayer(currentPlayerName?: string): void { function nextPlayer(currentPlayerName?: string): void {
store.set(currentPlayerNameAtom, currentPlayerName); store.set(currentPlayerNameAtom, currentPlayerName)
store.set(rollDiceButtonAtom, true); store.set(rollDiceButtonAtom, true)
} }

View File

@ -1,12 +1,12 @@
export const getData: Api = async (path, {headers} = {}) => { export const getData: Api = async (path, { headers } = {}) => {
if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([]))); if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([])))
return await fetch(import.meta.env.VITE_API_HTTP + path, { return await fetch(import.meta.env.VITE_API_HTTP + path, {
method: "GET", method: "GET",
headers: headers headers: headers,
}); })
} }
export const postData: Api = async (path, {body, headers} = {}) => { export const postData: Api = async (path, { body, headers } = {}) => {
return await fetch(import.meta.env.VITE_API_HTTP + path, { return await fetch(import.meta.env.VITE_API_HTTP + path, {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -1,74 +1,75 @@
import Player from "../game/player"; import Player from "../game/player"
import {atom} from "jotai"; import { atom } from "jotai"
import {Ghost} from "../game/character"; import { Ghost } from "../game/character"
import {customMap} from "../game/map"; import { customMap } from "../game/map"
const playerStorage = "player"; const playerStorage = "player"
/** /**
* All players in the game. * All players in the game.
*/ */
export const playersAtom = atom<Player[]>([]); export const playersAtom = atom<Player[]>([])
/** /**
* All player characters (Pac-Man) in the game. * All player characters (Pac-Man) in the game.
*/ */
export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.pacMan)); export const playerCharactersAtom = atom(get => get(playersAtom).map(player => player.pacMan))
/** /**
* All ghosts in the game. * All ghosts in the game.
*/ */
export const ghostsAtom = atom<Ghost[]>([]); export const ghostsAtom = atom<Ghost[]>([])
/** /**
* All characters in the game. * All characters in the game.
*/ */
export const allCharactersAtom = atom(get => [...get(playerCharactersAtom), ...get(ghostsAtom)]); export const allCharactersAtom = atom(get => [...get(playerCharactersAtom), ...get(ghostsAtom)])
/** /**
* The player that is currently logged in. * The player that is currently logged in.
*/ */
const playerAtom = atom<Player | undefined>(undefined); const playerAtom = atom<Player | undefined>(undefined)
/** /**
* Gets a getter and setter to get or set the player that is currently logged in. * Gets a getter and setter to get or set the player that is currently logged in.
* Returns A tuple containing a getter and setter to get or set the player that is currently logged in. * Returns A tuple containing a getter and setter to get or set the player that is currently logged in.
*/ */
export const thisPlayerAtom = atom(get => { export const thisPlayerAtom = atom(
const atomValue = get(playerAtom); get => {
if (!atomValue) { const atomValue = get(playerAtom)
const item = sessionStorage.getItem(playerStorage); if (!atomValue) {
if (item) { const item = sessionStorage.getItem(playerStorage)
const playerProps = JSON.parse(item) as PlayerProps; if (item) {
return new Player(playerProps); const playerProps = JSON.parse(item) as PlayerProps
return new Player(playerProps)
}
} }
} return atomValue
return atomValue; },
}, (_get, set, player: Player | undefined) => { (_get, set, player: Player | undefined) => {
if (player) if (player) sessionStorage.setItem(playerStorage, JSON.stringify(player))
sessionStorage.setItem(playerStorage, JSON.stringify(player)); else sessionStorage.removeItem(playerStorage)
else set(playerAtom, player)
sessionStorage.removeItem(playerStorage); },
set(playerAtom, player); )
});
/** /**
* All dice that have been rolled. * All dice that have been rolled.
*/ */
export const diceAtom = atom<number[] | undefined>(undefined); export const diceAtom = atom<number[] | undefined>(undefined)
/** /**
* The dice that have been selected by the player. * The dice that have been selected by the player.
*/ */
export const selectedDiceAtom = atom<SelectedDice | undefined>(undefined); export const selectedDiceAtom = atom<SelectedDice | undefined>(undefined)
/** /**
* The name of the player whose turn it is. * The name of the player whose turn it is.
*/ */
export const currentPlayerNameAtom = atom<string | undefined>(undefined); export const currentPlayerNameAtom = atom<string | undefined>(undefined)
/** /**
* The player whose turn it is. * The player whose turn it is.
*/ */
export const currentPlayerAtom = atom<Player | undefined>(get => { export const currentPlayerAtom = atom<Player | undefined>(get => {
const currentPlayerName = get(currentPlayerNameAtom); const currentPlayerName = get(currentPlayerNameAtom)
return get(playersAtom).find(player => player.username === currentPlayerName); return get(playersAtom).find(player => player.username === currentPlayerName)
}); })
/** /**
* Whether the roll dice button should be enabled. * Whether the roll dice button should be enabled.
*/ */
export const rollDiceButtonAtom = atom(true); export const rollDiceButtonAtom = atom(true)
/** /**
* The map that is currently selected. * The map that is currently selected.
*/ */
export const selectedMapAtom = atom(customMap); export const selectedMapAtom = atom(customMap)

View File

@ -5,14 +5,14 @@
* @returns A promise that resolves when the predicate is true. * @returns A promise that resolves when the predicate is true.
*/ */
export function wait(predicate: Predicate<void>, timeout: number = 50): Promise<void> { export function wait(predicate: Predicate<void>, timeout: number = 50): Promise<void> {
return new Promise<void>((resolve) => { return new Promise<void>(resolve => {
const f = () => { const f = () => {
if (predicate()) { if (predicate()) {
return resolve(); return resolve()
} }
setTimeout(f, timeout); setTimeout(f, timeout)
}; }
f(); f()
}); })
} }

View File

@ -1,11 +1,11 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_API_URI: string, readonly VITE_API_URI: string
readonly VITE_API_HTTP: string, readonly VITE_API_HTTP: string
readonly VITE_API_WS: string, readonly VITE_API_WS: string
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv
} }

View File

@ -1,9 +1,9 @@
import {wait} from "../utils/utils"; import { wait } from "../utils/utils"
interface IWebSocket { interface IWebSocket {
onOpen?: VoidFunction, onOpen?: VoidFunction
onReceive?: MessageEventFunction, onReceive?: MessageEventFunction
onClose?: VoidFunction, onClose?: VoidFunction
onError?: VoidFunction onError?: VoidFunction
} }
@ -11,59 +11,59 @@ interface IWebSocket {
* WebSocketService class provides a WebSocket client interface for easy communication with a WebSocket server. * WebSocketService class provides a WebSocket client interface for easy communication with a WebSocket server.
*/ */
export default class WebSocketService { export default class WebSocketService {
private ws?: WebSocket; private ws?: WebSocket
private readonly _url: string; private readonly _url: string
constructor(url: string, {onOpen, onReceive, onClose, onError}: IWebSocket = {}) { constructor(url: string, { onOpen, onReceive, onClose, onError }: IWebSocket = {}) {
this._url = url; this._url = url
this._onOpen = onOpen; this._onOpen = onOpen
this._onReceive = onReceive; this._onReceive = onReceive
this._onClose = onClose; this._onClose = onClose
this._onError = onError; this._onError = onError
} }
private _onOpen?: VoidFunction; private _onOpen?: VoidFunction
set onOpen(onOpen: VoidFunction) { set onOpen(onOpen: VoidFunction) {
this._onOpen = onOpen; this._onOpen = onOpen
if (!this.ws) return; if (!this.ws) return
this.ws.onopen = onOpen; this.ws.onopen = onOpen
} }
private _onReceive?: MessageEventFunction; private _onReceive?: MessageEventFunction
set onReceive(onReceive: MessageEventFunction) { set onReceive(onReceive: MessageEventFunction) {
this._onReceive = onReceive; this._onReceive = onReceive
if (!this.ws) return; if (!this.ws) return
this.ws.onmessage = onReceive; this.ws.onmessage = onReceive
} }
private _onClose?: VoidFunction; private _onClose?: VoidFunction
set onClose(onClose: VoidFunction) { set onClose(onClose: VoidFunction) {
this._onClose = onClose; this._onClose = onClose
if (!this.ws) return; if (!this.ws) return
this.ws.onclose = onClose; this.ws.onclose = onClose
} }
private _onError?: VoidFunction; private _onError?: VoidFunction
set onError(onError: VoidFunction) { set onError(onError: VoidFunction) {
this._onError = onError; this._onError = onError
if (!this.ws) return; if (!this.ws) return
this.ws.onerror = onError; this.ws.onerror = onError
} }
/** /**
* Opens a WebSocket connection with the specified URL and sets the event callbacks. * Opens a WebSocket connection with the specified URL and sets the event callbacks.
*/ */
public open(): void { public open(): void {
if (typeof WebSocket === "undefined" || this.isConnecting()) return; if (typeof WebSocket === "undefined" || this.isConnecting()) return
this.ws = new WebSocket(this._url); this.ws = new WebSocket(this._url)
if (this._onOpen) this.ws.onopen = this._onOpen; if (this._onOpen) this.ws.onopen = this._onOpen
if (this._onReceive) this.ws.onmessage = this._onReceive; if (this._onReceive) this.ws.onmessage = this._onReceive
if (this._onClose) this.ws.onclose = this._onClose; if (this._onClose) this.ws.onclose = this._onClose
if (this._onError) this.ws.onerror = this._onError; if (this._onError) this.ws.onerror = this._onError
} }
/** /**
@ -72,8 +72,8 @@ export default class WebSocketService {
* @returns {Promise<void>} - A promise that resolves when the "isOpen" condition is met. * @returns {Promise<void>} - A promise that resolves when the "isOpen" condition is met.
*/ */
public async waitForOpen(): Promise<void> { public async waitForOpen(): Promise<void> {
await wait(() => this.isOpen()); await wait(() => this.isOpen())
if (this._onOpen) this.onOpen = this._onOpen; if (this._onOpen) this.onOpen = this._onOpen
} }
/** /**
@ -83,16 +83,16 @@ export default class WebSocketService {
*/ */
public send(data: ActionMessage | string): void { public send(data: ActionMessage | string): void {
if (typeof data !== "string") { if (typeof data !== "string") {
data = JSON.stringify(data); data = JSON.stringify(data)
} }
this.ws?.send(data); this.ws?.send(data)
} }
/** /**
* Closes the WebSocket connection. * Closes the WebSocket connection.
*/ */
public close(): void { public close(): void {
this.ws?.close(); this.ws?.close()
} }
/** /**
@ -100,7 +100,7 @@ export default class WebSocketService {
* @returns {boolean} Returns true if the WebSocket is open, otherwise false. * @returns {boolean} Returns true if the WebSocket is open, otherwise false.
*/ */
public isOpen(): boolean { public isOpen(): boolean {
return this.ws?.readyState === WebSocket?.OPEN; return this.ws?.readyState === WebSocket?.OPEN
} }
/** /**
@ -109,7 +109,7 @@ export default class WebSocketService {
* @returns {boolean} - Returns 'true' if the WebSocket is connecting, otherwise 'false'. * @returns {boolean} - Returns 'true' if the WebSocket is connecting, otherwise 'false'.
*/ */
public isConnecting(): boolean { public isConnecting(): boolean {
return this.ws?.readyState === WebSocket?.CONNECTING; return this.ws?.readyState === WebSocket?.CONNECTING
} }
/** /**
@ -118,6 +118,6 @@ export default class WebSocketService {
* @returns {boolean} Returns true if the WebSocket connection is closed, false otherwise. * @returns {boolean} Returns true if the WebSocket connection is closed, false otherwise.
*/ */
public isClosed(): boolean { public isClosed(): boolean {
return this.ws?.readyState === WebSocket?.CLOSED; return this.ws?.readyState === WebSocket?.CLOSED
} }
} }

View File

@ -69,7 +69,7 @@ public class GameController : GenericController
} }
catch (Exception e) catch (Exception e)
{ {
return BadRequest(e.Message); // TODO not necessary? return BadRequest(e.Message);
} }
} }