diff --git a/interface/index.ts b/interface/index.ts index 17a354e..cacd699 100644 --- a/interface/index.ts +++ b/interface/index.ts @@ -14,6 +14,7 @@ export * from "./types/PlacedPiece"; export * from "./BoardBuilder"; export * from "./server-events/ServerError"; export * from "./server-events/ServerEvent"; +export * from "./server-events/StartRoundEvent"; export * from "./server-events/UpdateLobbyEvent"; export * from "./client-events/ClientEvent"; export * from "./client-events/CreateLobbyEvent"; diff --git a/interface/server-events/StartRoundEvent.ts b/interface/server-events/StartRoundEvent.ts new file mode 100644 index 0000000..7b870ec --- /dev/null +++ b/interface/server-events/StartRoundEvent.ts @@ -0,0 +1,22 @@ +import { Socket as ServerSocket } from "socket.io"; +import { Socket as ClientSocket } from "socket.io-client"; +import { ServerEvent } from "./ServerEvent"; +import { PieceId } from "../constants/Pieces"; + +export type StartRoundEvent = { + pieceIds: PieceId[]; +}; + +export const emitStartRoundEvent = ( + socket: ServerSocket, + payload: StartRoundEvent, +) => { + socket.emit(ServerEvent.START_ROUND, payload); +}; + +export function attachHandlerToStartRoundEvent( + socket: ClientSocket, + handler: (event: StartRoundEvent) => void, +): void { + socket.once(ServerEvent.START_ROUND, handler); +} diff --git a/interface/server-events/UpdateLobbyEvent.ts b/interface/server-events/UpdateLobbyEvent.ts index 90f696a..9cbbb79 100644 --- a/interface/server-events/UpdateLobbyEvent.ts +++ b/interface/server-events/UpdateLobbyEvent.ts @@ -1,14 +1,22 @@ -import { Socket } from "socket.io-client"; +import { Socket as ServerSocket } from "socket.io"; +import { Socket as ClientSocket } from "socket.io-client"; import { ServerEvent } from "./ServerEvent"; export type UpdateLobbyEvent = { playerNames: Array; + gameCode: string; +}; + +export const emitUpdateLobbyEvent = ( + socket: ServerSocket, + payload: UpdateLobbyEvent, +) => { + socket.emit(ServerEvent.LOBBY_UPDATE, payload); }; export function attachHandlerToUpdateLobbyEvent( - socket: Socket, + socket: ClientSocket, handler: (event: UpdateLobbyEvent) => void, -): () => void { - socket.on(ServerEvent.LOBBY_UPDATE, handler); - return () => socket.off(ServerEvent.LOBBY_UPDATE, handler); +): void { + socket.once(ServerEvent.LOBBY_UPDATE, handler); } diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 6412cc5..cd12587 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -63,7 +63,7 @@ export class Cell { trackType: track.type, }; }) - .some(({ trackExternalNodes, trackType }) => { + .reduce((isConnected, { trackExternalNodes, trackType }) => { let isTrackConnected: boolean = false; trackExternalNodes .filter((node) => node.traverseBorder() instanceof Exit) @@ -98,11 +98,15 @@ export class Cell { } }); - return isTrackConnected; - }); + return isConnected || isTrackConnected; + }, false); if (!hasAnyConnection) { throw Error("No adjacent exit or piece available to connect to"); } } + + public removePiece() { + this.placedPiece = undefined; + } } diff --git a/web/package-lock.json b/web/package-lock.json index 936fa4a..6028a80 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,7 +13,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", - "@types/node": "^16.18.119", + "@types/node": "^17.0.29", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "interface": "file:../interface", @@ -4202,9 +4202,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "16.18.119", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.119.tgz", - "integrity": "sha512-ia7V9a2FnhUFfetng4/sRPBMTwHZUkPFY736rb1cg9AgG7MZdR97q7/nLR9om+sq5f1la9C857E0l/nrI0RiFQ==", + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.29.tgz", + "integrity": "sha512-tx5jMmMFwx7wBwq/V7OohKDVb/JwJU5qCVkeLMh1//xycAJ/ESuw9aJ9SEtlCZDYi2pBfe4JkisSoAtbOsBNAA==", "license": "MIT" }, "node_modules/@types/node-forge": { diff --git a/web/package.json b/web/package.json index f240e25..b5e64b2 100644 --- a/web/package.json +++ b/web/package.json @@ -8,7 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", - "@types/node": "^16.18.119", + "@types/node": "^17.0.29", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "interface": "file:../interface", diff --git a/web/public/clean-icon.png b/web/public/clean-icon.png new file mode 100644 index 0000000..c06ba90 Binary files /dev/null and b/web/public/clean-icon.png differ diff --git a/web/public/commit-icon.png b/web/public/commit-icon.png new file mode 100644 index 0000000..d254d8d Binary files /dev/null and b/web/public/commit-icon.png differ diff --git a/web/public/factory-icon.png b/web/public/factory-icon.png new file mode 100644 index 0000000..e7c3ad8 Binary files /dev/null and b/web/public/factory-icon.png differ diff --git a/web/public/house-icon.png b/web/public/house-icon.png new file mode 100644 index 0000000..6d5b496 Binary files /dev/null and b/web/public/house-icon.png differ diff --git a/web/public/university-icon.png b/web/public/university-icon.png new file mode 100644 index 0000000..e55f06a Binary files /dev/null and b/web/public/university-icon.png differ diff --git a/web/src/App.tsx b/web/src/App.tsx index b1fc803..93d4015 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,19 +1,55 @@ -import React from "react"; +import React, { useCallback, useState } from "react"; import { Socket } from "socket.io-client"; import LandingPage from "./pages/landing/LandingPage"; import GamePage from "./pages/game/GamePage"; +import LobbyPage from "./pages/lobby/LobbyPage"; +import { + attachHandlerToStartRoundEvent, + attachHandlerToUpdateLobbyEvent, + PieceId, +} from "interface"; export interface AppProps { socket: Socket; } const App = (props: AppProps) => { - const renderedPage: string = "LANDING"; + const { socket } = props; + const [renderedPage, setRenderedPage] = useState("LANDING"); + const [initialPlayerNames, setInitialPlayerNames] = useState([] as string[]); + const [gameCode, setGameCode] = useState(""); + const [initialPieceIds, setInitialPieceIds] = useState([] as PieceId[]); + + const setupNextPageTransition = useCallback(() => { + if (renderedPage === "LANDING") { + attachHandlerToUpdateLobbyEvent(socket, (event) => { + setInitialPlayerNames(event.playerNames); + setGameCode(event.gameCode); + setRenderedPage("LOBBY"); + }); + } else if (renderedPage === "LOBBY") { + attachHandlerToStartRoundEvent(socket, (event) => { + setInitialPieceIds(event.pieceIds); + setRenderedPage("GAME"); + }); + } + }, [renderedPage]); + + setupNextPageTransition(); return (
{renderedPage === "LANDING" && } - {renderedPage === "GAME" && } + {renderedPage === "LOBBY" && ( + + )} + {renderedPage === "GAME" && ( + + )}
); }; diff --git a/web/src/pages/game/GamePage.scss b/web/src/pages/game/GamePage.scss index 9847b6c..87b1eba 100644 --- a/web/src/pages/game/GamePage.scss +++ b/web/src/pages/game/GamePage.scss @@ -12,5 +12,6 @@ h1 { .right-panel { display: flex; flex-direction: column; + padding: 50px; } } diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 4b06b3d..c66ee9e 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -2,32 +2,129 @@ import React, { useState } from "react"; import GameBoard from "./components/GameBoard"; import DiceSet from "./components/DiceSet"; import "./GamePage.scss"; -import { PieceId } from "interface"; +import { buildBoard, PieceId } from "interface"; +import { DieViewProps } from "./types/DieViewProps"; +import { PlacePieceActionStack } from "./types/PlacePieceActionStack"; -const GamePage = () => { - const getRandomPieceId = () => { - const randomId = Math.floor(Math.random() * Object.keys(PieceId).length); - return Object.values(PieceId)[randomId]; - }; +export interface GamePageProps { + initialPieceIds: PieceId[]; +} - const [dice, setDice] = useState( - [1, 2, 3, 4].map(() => { +const GamePage = (props: GamePageProps) => { + const { initialPieceIds } = props; + + const [pieceIds] = useState(initialPieceIds); + + const getDiceSet: () => DieViewProps[] = () => + pieceIds.map((pieceId) => { return { - pieceId: getRandomPieceId(), + pieceId: pieceId, isSelected: false, isDisabled: false, + isSpecial: false, + rotation: 0, + }; + }); + + const [dice, setDice] = useState(getDiceSet()); + const [specialDice, setSpecialDice] = useState( + [ + PieceId.P03, + PieceId.P08, + PieceId.P13, + PieceId.P14, + PieceId.P15, + PieceId.P19, + ].map((pieceId) => { + return { + pieceId: pieceId, + isSelected: false, + isDisabled: false, + isSpecial: true, rotation: 0, }; }), ); + const [specialDieUsedInRound, setSpecialDieUsedInRound] = useState(false); + + const modifyDieState = ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => { + setDice( + dice.map((die) => { + if (!matcher(die)) return die; + return { + ...die, + ...newStateComputer(die), + }; + }), + ); + setSpecialDice( + specialDice.map((die) => { + if (!matcher(die)) return die; + return { + ...die, + ...newStateComputer(die), + }; + }), + ); + }; + + const fetchDie = (matcher: (die: DieViewProps) => boolean) => { + return dice.concat(specialDice).find((die) => matcher(die)); + }; + + const [board, setBoard] = useState(buildBoard()); + const [id, setId] = useState(1); + const refreshBoardRender = () => { + setBoard(board); + setId(id + 1); + }; + + const [placePieceActionStack, setPlacePieceActionStack] = useState( + new PlacePieceActionStack(), + ); + const resetBoard = () => { + placePieceActionStack.resetActions(board); + setBoard(board); + modifyDieState( + () => true, + () => { + return { isSelected: false, isDisabled: false }; + }, + ); + setId(id + 1); + }; + const commitBoard = () => { + if (dice.some((die) => !die.isDisabled)) return; + placePieceActionStack.commitActions(); + setDice(getDiceSet()); + setSpecialDieUsedInRound(false); + }; return (

Game Page Title

-
- -
- +
+ +
+
diff --git a/web/src/pages/game/components/BoardCell.scss b/web/src/pages/game/components/BoardCell.scss index 1250a30..d1e612c 100644 --- a/web/src/pages/game/components/BoardCell.scss +++ b/web/src/pages/game/components/BoardCell.scss @@ -3,18 +3,12 @@ height: 100px; border: 2px solid black; border-radius: 10%; + position: relative; .piece { border-radius: 10%; - position: relative; - - &:nth-child(2) { - top: -20px; - } - - &:nth-child(3) { - top: -40px; - } + position: absolute; + top: 0; &.rotate-90 { transform: rotate(-90deg); @@ -29,33 +23,23 @@ } } - &.house { - background-color: blue; + .cell-type-icon { + width: 25px; + height: 25px; + padding: 2px; + margin: 2px; + position: absolute; + top: 0; + left: 0; } +} - &.university { - background-color: orange; - } +.cell .exit { + position: absolute; + width: 20px; + height: 20px; - &.factory { - background-color: grey; - } - - .exit:has(.exit-road) { - border: solid black; - border-width: 0 1px; - - .exit-road { - margin: auto; - width: 0; - height: 100%; - border: dashed black; - border-width: 0 1px; - transform: translate(calc(50% - 1px)); - } - } - - .exit:has(.exit-road) { + &:has(.exit-road) { border: solid black; border-width: 0 1px; @@ -69,7 +53,7 @@ } } - .exit:has(.exit-rail) { + &:has(.exit-rail) { border-width: 0 1px; .exit-rail { @@ -82,7 +66,7 @@ } } - .exit:has(.exit-ambivalent) { + &:has(.exit-ambivalent) { border-width: 0 1px; border-style: dotted; @@ -95,47 +79,30 @@ transform: translate(calc(50%)); } } -} - -.exit { - position: relative; - width: 20px; - height: 20px; + &:has(.exit-north), &:has(.exit-south) { + left: 50%; + } &:has(.exit-north) { - left: 50%; transform: translate(calc(-50% - 1px), -100%); } &:has(.exit-south) { - left: 50%; top: 100%; transform: translate(calc(-50% - 1px), 0%); } + &:has(.exit-east), &:has(.exit-west) { + top: 50%; + } + &:has(.exit-east) { left: 100%; - top: 50%; transform: rotate(90deg) translate(-50%, 0); } &:has(.exit-west) { left: 0%; - top: 50%; transform: rotate(90deg) translate(-50%, 100%); } - - &:has(.exit-north), &:has(.exit-south) { - + .exit:has(.exit-east) { - left: 100%; - transform: rotate(90deg) translate(-150%, 0); - } - } - - &:has(.exit-north), &:has(.exit-south) { - + .exit:has(.exit-west) { - top: 50%; - transform: rotate(90deg) translate(-150%, 100%); - } - } } \ No newline at end of file diff --git a/web/src/pages/game/components/BoardCell.tsx b/web/src/pages/game/components/BoardCell.tsx index 3170438..426020b 100644 --- a/web/src/pages/game/components/BoardCell.tsx +++ b/web/src/pages/game/components/BoardCell.tsx @@ -1,63 +1,58 @@ -import { Cell, directions, Exit, PieceId } from "interface"; +import { Cell, CellType, directions, Exit, PieceId } from "interface"; import "./BoardCell.scss"; import { useState } from "react"; +import { DieViewProps } from "../types/DieViewProps"; export interface BoardCellProps { cell: Cell; - dice: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[]; - setDice: React.Dispatch< - React.SetStateAction< - { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[] - > - >; refreshBoardRender: () => void; + modifyDieState: ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => void; + fetchDie: ( + matcher: (die: DieViewProps) => boolean, + ) => DieViewProps | undefined; + setSpecialDieUsedInRound: React.Dispatch>; + placePieceActionHandler: (pieceId: PieceId, rotation: number) => void; } const BoardCell = (props: BoardCellProps) => { - const { cell, dice, setDice, refreshBoardRender } = props; + const { + cell, + refreshBoardRender, + modifyDieState, + fetchDie, + setSpecialDieUsedInRound, + placePieceActionHandler, + } = props; const [pieceRotationAngle, setPieceRotationAngle] = useState(0); const handleBoardCellClick = () => { - const selectedDie = dice.find((die) => die.isSelected); + const selectedDie = fetchDie((die) => die.isSelected); if (!selectedDie) return; try { - cell.placePiece( - selectedDie.pieceId, - selectedDie.rotation as 0 | 90 | 180 | 270, - ); + placePieceActionHandler(selectedDie.pieceId, selectedDie.rotation); } catch (error) { console.log(error); return; } - setDice( - dice.map((die) => { - return die !== selectedDie - ? die - : { - ...selectedDie, - isSelected: false, - isDisabled: true, - }; - }), + modifyDieState( + (die) => die === selectedDie, + () => { + return { + isSelected: false, + isDisabled: true, + }; + }, ); + if (selectedDie.isSpecial) setSpecialDieUsedInRound(true); + // Set rotation to the piece in the board, not the die setPieceRotationAngle(selectedDie.rotation); refreshBoardRender(); }; return ( -
+
{directions.map((direction) => { const traversedNode = cell.getNodeAt(direction).traverseBorder(); const isExit = traversedNode instanceof Exit; @@ -77,6 +72,12 @@ const BoardCell = (props: BoardCellProps) => { src={`pieces/${cell.placedPiece.id}.jpeg`} > )} + {cell.cellType !== CellType.NORMAL && ( + + )}
); }; diff --git a/web/src/pages/game/components/DiceSet.scss b/web/src/pages/game/components/DiceSet.scss index 87776c6..114a733 100644 --- a/web/src/pages/game/components/DiceSet.scss +++ b/web/src/pages/game/components/DiceSet.scss @@ -1,22 +1,25 @@ .dice-set-actions { display: flex; flex-direction: row; - padding: 50px 50px 0; width: max-content; - margin: auto; + margin: auto auto 50px auto; img { border: 3px solid green; box-shadow: 0 0 10px green; border-radius: 20%; padding: 5px; - width: 80px; - height: 80px; + width: 70px; + height: 70px; margin-right: 10px; &.icon-inverted { transform: scaleX(-1); } + + &:last-child { + margin-right: 0; + } } } @@ -24,5 +27,14 @@ display: grid; grid-template-columns: repeat(2, auto); gap: 20px; - padding: 50px; + margin: 0 0 50px 0; +} + +.special-dice-set { + display: grid; + grid-template-columns: repeat(3, auto); + margin: 0 0 50px 0; + column-gap: 12%; + row-gap: 20px; + width: 100%; } \ No newline at end of file diff --git a/web/src/pages/game/components/DiceSet.tsx b/web/src/pages/game/components/DiceSet.tsx index 1779190..bf0b5a2 100644 --- a/web/src/pages/game/components/DiceSet.tsx +++ b/web/src/pages/game/components/DiceSet.tsx @@ -1,55 +1,51 @@ -import { PieceId } from "interface"; import "./DiceSet.scss"; import Die from "./Die"; import React from "react"; +import { DieViewProps } from "../types/DieViewProps"; export interface DiceSetProps { - dice: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[]; - setDice: React.Dispatch< - React.SetStateAction< - { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[] - > - >; + dice: DieViewProps[]; + specialDice: DieViewProps[]; + modifyDieState: ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => void; + specialDieUsedInRound: boolean; + resetBoard: () => void; + commitBoard: () => void; } const DiceSet = (props: DiceSetProps) => { - const { dice, setDice } = props; - const handleDieClick = (die: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }) => { - if (die.isDisabled) return; - const newDiceState = dice.map((oldDie) => { - const isSelected = die === oldDie; - return { - ...oldDie, - isSelected: isSelected, - }; - }); - setDice(newDiceState); + const { + dice, + specialDice, + modifyDieState, + specialDieUsedInRound, + resetBoard, + commitBoard, + } = props; + const handleDieClick = (clickedDie: DieViewProps) => { + if (clickedDie.isDisabled) return; + const isSpecialDie = clickedDie.isSpecial; + if (isSpecialDie && specialDieUsedInRound) return; + modifyDieState( + () => true, + (die) => { + return { + isSelected: die === clickedDie, + }; + }, + ); }; const handleRotateButton = (rotation: number) => { - const newDiceState = dice.map((die) => { - if (!die.isSelected) return die; - const rotationAngle = (die.rotation + rotation + 360) % 360; - return { - ...die, - rotation: rotationAngle, - }; - }); - setDice(newDiceState); + modifyDieState( + (die) => die.isSelected, + (die) => { + return { + rotation: (die.rotation + rotation + 360) % 360, + }; + }, + ); }; return ( @@ -64,12 +60,19 @@ const DiceSet = (props: DiceSetProps) => { className="icon-inverted" onClick={() => handleRotateButton(90)} > + +
{dice.map((die) => ( ))}
+
+ {specialDice.map((die) => ( + + ))} +
); }; diff --git a/web/src/pages/game/components/Die.scss b/web/src/pages/game/components/Die.scss index b3f4195..675582d 100644 --- a/web/src/pages/game/components/Die.scss +++ b/web/src/pages/game/components/Die.scss @@ -29,4 +29,9 @@ } transition: transform 0.5s cubic-bezier(.47,1.64,.41,.8); +} + +.special-dice-set .dice { + height: 80px; + width: 80px; } \ No newline at end of file diff --git a/web/src/pages/game/components/Die.tsx b/web/src/pages/game/components/Die.tsx index ba8474f..c039c39 100644 --- a/web/src/pages/game/components/Die.tsx +++ b/web/src/pages/game/components/Die.tsx @@ -1,19 +1,9 @@ -import { PieceId } from "interface"; import "./Die.scss"; +import { DieViewProps } from "../types/DieViewProps"; interface DieProps { - die: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }; - handleDieClick: (die: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }) => void; + die: DieViewProps; + handleDieClick: (die: DieViewProps) => void; } const Die = (props: DieProps) => { diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index 0997041..94a94f2 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -1,43 +1,44 @@ -import { buildBoard, PieceId } from "interface"; +import { Cell } from "interface"; import "./GameBoard.scss"; -import { useState } from "react"; import BoardCell from "./BoardCell"; +import { DieViewProps } from "../types/DieViewProps"; +import { PlacePieceActionStack } from "../types/PlacePieceActionStack"; +import { PlacePieceAction } from "../types/PlacePieceAction"; export interface GameBoardProps { - dice: { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[]; - setDice: React.Dispatch< - React.SetStateAction< - { - pieceId: PieceId; - isSelected: boolean; - isDisabled: boolean; - rotation: number; - }[] - > + modifyDieState: ( + matcher: (die: DieViewProps) => boolean, + newStateComputer: (die: DieViewProps) => Partial, + ) => void; + fetchDie: ( + matcher: (die: DieViewProps) => boolean, + ) => DieViewProps | undefined; + setSpecialDieUsedInRound: React.Dispatch>; + refreshBoardRender: () => void; + board: Cell[][]; + placePieceActionStack: PlacePieceActionStack; + setPlacePieceActionStack: React.Dispatch< + React.SetStateAction >; } const GameBoard = (props: GameBoardProps) => { - const [board, setBoard] = useState(buildBoard()); - const [id, setId] = useState(1); - const refreshBoardRender = () => { - setBoard(board); - setId(id + 1); - }; + const { board, placePieceActionStack, setPlacePieceActionStack } = props; return ( -
- {board.flatMap((row) => - row.map((cell) => ( +
+ {board.flatMap((row, rowIndex) => + row.map((cell, colIndex) => ( { + placePieceActionStack.executeAction( + new PlacePieceAction(pieceId, rotation, rowIndex, colIndex), + board, + ); + setPlacePieceActionStack(placePieceActionStack); + }} /> )), )} diff --git a/web/src/pages/game/types/DieViewProps.ts b/web/src/pages/game/types/DieViewProps.ts new file mode 100644 index 0000000..71914ed --- /dev/null +++ b/web/src/pages/game/types/DieViewProps.ts @@ -0,0 +1,9 @@ +import { PieceId } from "interface"; + +export interface DieViewProps { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + readonly isSpecial: boolean; + rotation: number; +} diff --git a/web/src/pages/game/types/PlacePieceAction.ts b/web/src/pages/game/types/PlacePieceAction.ts new file mode 100644 index 0000000..6fe46d3 --- /dev/null +++ b/web/src/pages/game/types/PlacePieceAction.ts @@ -0,0 +1,32 @@ +import { Cell, PieceId } from "interface"; + +export class PlacePieceAction { + pieceId: PieceId; + rotation: number; + cell: { + row: number; + col: number; + }; + + constructor(pieceId: PieceId, rotation: number, row: number, col: number) { + this.pieceId = pieceId; + this.rotation = rotation; + this.cell = { + row: row, + col: col, + }; + } + + do(board: Cell[][]) { + const cell = board[this.cell.row][this.cell.col]; + cell.placePiece(this.pieceId, this.rotation as 0 | 90 | 180 | 270); + } + + undo(board: Cell[][]) { + const cell = board[this.cell.row][this.cell.col]; + if (!cell.placedPiece || cell.placedPiece.id !== this.pieceId) { + throw Error("Un-doing action error"); + } + cell.removePiece(); + } +} diff --git a/web/src/pages/game/types/PlacePieceActionStack.ts b/web/src/pages/game/types/PlacePieceActionStack.ts new file mode 100644 index 0000000..4a6841f --- /dev/null +++ b/web/src/pages/game/types/PlacePieceActionStack.ts @@ -0,0 +1,40 @@ +import { Cell } from "interface"; +import { PlacePieceAction } from "./PlacePieceAction"; + +export class PlacePieceActionStack { + readonly committedActions: PlacePieceAction[] = []; + inProgressActions: PlacePieceAction[] = []; + + executeAction(action: PlacePieceAction, board: Cell[][]) { + action.do(board); + this.inProgressActions.push(action); + } + + undoLastAction(board: Cell[][]) { + const lastAction = this.inProgressActions.pop(); + if (!lastAction) { + throw Error("No in progress action to undo"); + } + lastAction.undo(board); + } + + resetActions(board: Cell[][]) { + try { + while (true) { + this.undoLastAction(board); + } + } catch (error) { + if ( + !(error instanceof Error) || + error.message !== "No in progress action to undo" + ) { + throw error; + } + } + } + + commitActions() { + this.committedActions.push(...this.inProgressActions); + this.inProgressActions = []; + } +} diff --git a/web/src/pages/lobby/LobbyPage.scss b/web/src/pages/lobby/LobbyPage.scss new file mode 100644 index 0000000..5a935f7 --- /dev/null +++ b/web/src/pages/lobby/LobbyPage.scss @@ -0,0 +1,50 @@ +.lobby-page { + height: 100%; + width: 100%; + padding: 2%; + margin: 0 auto; + text-align: center; + + background: linear-gradient(-45deg, #03444a, #00a8a8, #f1bc52, #ff8f4b); + background-size: 400% 400%; + animation: gradient 15s ease infinite; + height: 100vh; + + @keyframes gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } + + .lobby-page-body { + margin: auto; + width: 20%; + min-width: 300px; + + .lobby-user-name { + margin: 0 auto 10px; + padding: 10px; + width: 60%; + border: 2px solid cyan; + border-radius: 10px; + } + + .game-code { + margin: 0 auto 10px; + padding: 10px; + width: 60%; + border: 2px solid blue; + border-radius: 10px; + } + + .start-game-button { + margin: 30px 0; + } + } +} diff --git a/web/src/pages/lobby/LobbyPage.tsx b/web/src/pages/lobby/LobbyPage.tsx new file mode 100644 index 0000000..a336d29 --- /dev/null +++ b/web/src/pages/lobby/LobbyPage.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import "./LobbyPage.scss"; +import { Socket } from "socket.io-client"; +import { attachHandlerToUpdateLobbyEvent, ClientEvent } from "interface"; + +export interface LobbyPageProps { + initialPlayerNames: string[]; + gameCode: string; + socket: Socket; +} + +const LobbyPage = (props: LobbyPageProps) => { + const { initialPlayerNames, gameCode, socket } = props; + const [playerNames, setPlayerNames] = useState(initialPlayerNames); + + attachHandlerToUpdateLobbyEvent(socket, (event) => { + setPlayerNames(event.playerNames); + }); + const startGame = () => socket.emit(ClientEvent.START_GAME); + + return ( +
+
+

Trains And Roads

+
+
+ {playerNames.map((name) => ( +
{name}
+ ))} +
+ +
+
Game code: {gameCode}
+
+
+ ); +}; + +export default LobbyPage;