main #12
|
|
@ -14,6 +14,7 @@ export * from "./types/PlacedPiece";
|
||||||
export * from "./BoardBuilder";
|
export * from "./BoardBuilder";
|
||||||
export * from "./server-events/ServerError";
|
export * from "./server-events/ServerError";
|
||||||
export * from "./server-events/ServerEvent";
|
export * from "./server-events/ServerEvent";
|
||||||
|
export * from "./server-events/StartRoundEvent";
|
||||||
export * from "./server-events/UpdateLobbyEvent";
|
export * from "./server-events/UpdateLobbyEvent";
|
||||||
export * from "./client-events/ClientEvent";
|
export * from "./client-events/ClientEvent";
|
||||||
export * from "./client-events/CreateLobbyEvent";
|
export * from "./client-events/CreateLobbyEvent";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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";
|
import { ServerEvent } from "./ServerEvent";
|
||||||
|
|
||||||
export type UpdateLobbyEvent = {
|
export type UpdateLobbyEvent = {
|
||||||
playerNames: Array<string>;
|
playerNames: Array<string>;
|
||||||
|
gameCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emitUpdateLobbyEvent = (
|
||||||
|
socket: ServerSocket,
|
||||||
|
payload: UpdateLobbyEvent,
|
||||||
|
) => {
|
||||||
|
socket.emit(ServerEvent.LOBBY_UPDATE, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function attachHandlerToUpdateLobbyEvent(
|
export function attachHandlerToUpdateLobbyEvent(
|
||||||
socket: Socket,
|
socket: ClientSocket,
|
||||||
handler: (event: UpdateLobbyEvent) => void,
|
handler: (event: UpdateLobbyEvent) => void,
|
||||||
): () => void {
|
): void {
|
||||||
socket.on(ServerEvent.LOBBY_UPDATE, handler);
|
socket.once(ServerEvent.LOBBY_UPDATE, handler);
|
||||||
return () => socket.off(ServerEvent.LOBBY_UPDATE, handler);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export class Cell {
|
||||||
trackType: track.type,
|
trackType: track.type,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.some(({ trackExternalNodes, trackType }) => {
|
.reduce((isConnected, { trackExternalNodes, trackType }) => {
|
||||||
let isTrackConnected: boolean = false;
|
let isTrackConnected: boolean = false;
|
||||||
trackExternalNodes
|
trackExternalNodes
|
||||||
.filter((node) => node.traverseBorder() instanceof Exit)
|
.filter((node) => node.traverseBorder() instanceof Exit)
|
||||||
|
|
@ -98,11 +98,15 @@ export class Cell {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return isTrackConnected;
|
return isConnected || isTrackConnected;
|
||||||
});
|
}, false);
|
||||||
|
|
||||||
if (!hasAnyConnection) {
|
if (!hasAnyConnection) {
|
||||||
throw Error("No adjacent exit or piece available to connect to");
|
throw Error("No adjacent exit or piece available to connect to");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removePiece() {
|
||||||
|
this.placedPiece = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.119",
|
"@types/node": "^17.0.29",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"interface": "file:../interface",
|
"interface": "file:../interface",
|
||||||
|
|
@ -4202,9 +4202,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.18.119",
|
"version": "17.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.119.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.29.tgz",
|
||||||
"integrity": "sha512-ia7V9a2FnhUFfetng4/sRPBMTwHZUkPFY736rb1cg9AgG7MZdR97q7/nLR9om+sq5f1la9C857E0l/nrI0RiFQ==",
|
"integrity": "sha512-tx5jMmMFwx7wBwq/V7OohKDVb/JwJU5qCVkeLMh1//xycAJ/ESuw9aJ9SEtlCZDYi2pBfe4JkisSoAtbOsBNAA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-forge": {
|
"node_modules/@types/node-forge": {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.119",
|
"@types/node": "^17.0.29",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"interface": "file:../interface",
|
"interface": "file:../interface",
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -1,19 +1,55 @@
|
||||||
import React from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import LandingPage from "./pages/landing/LandingPage";
|
import LandingPage from "./pages/landing/LandingPage";
|
||||||
import GamePage from "./pages/game/GamePage";
|
import GamePage from "./pages/game/GamePage";
|
||||||
|
import LobbyPage from "./pages/lobby/LobbyPage";
|
||||||
|
import {
|
||||||
|
attachHandlerToStartRoundEvent,
|
||||||
|
attachHandlerToUpdateLobbyEvent,
|
||||||
|
PieceId,
|
||||||
|
} from "interface";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = (props: AppProps) => {
|
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 (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
{renderedPage === "LANDING" && <LandingPage {...props} />}
|
{renderedPage === "LANDING" && <LandingPage {...props} />}
|
||||||
{renderedPage === "GAME" && <GamePage />}
|
{renderedPage === "LOBBY" && (
|
||||||
|
<LobbyPage
|
||||||
|
{...props}
|
||||||
|
initialPlayerNames={initialPlayerNames}
|
||||||
|
gameCode={gameCode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renderedPage === "GAME" && (
|
||||||
|
<GamePage initialPieceIds={initialPieceIds} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,6 @@ h1 {
|
||||||
.right-panel {
|
.right-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,129 @@ import React, { useState } from "react";
|
||||||
import GameBoard from "./components/GameBoard";
|
import GameBoard from "./components/GameBoard";
|
||||||
import DiceSet from "./components/DiceSet";
|
import DiceSet from "./components/DiceSet";
|
||||||
import "./GamePage.scss";
|
import "./GamePage.scss";
|
||||||
import { PieceId } from "interface";
|
import { buildBoard, PieceId } from "interface";
|
||||||
|
import { DieViewProps } from "./types/DieViewProps";
|
||||||
|
import { PlacePieceActionStack } from "./types/PlacePieceActionStack";
|
||||||
|
|
||||||
const GamePage = () => {
|
export interface GamePageProps {
|
||||||
const getRandomPieceId = () => {
|
initialPieceIds: PieceId[];
|
||||||
const randomId = Math.floor(Math.random() * Object.keys(PieceId).length);
|
}
|
||||||
return Object.values(PieceId)[randomId];
|
|
||||||
};
|
|
||||||
|
|
||||||
const [dice, setDice] = useState(
|
const GamePage = (props: GamePageProps) => {
|
||||||
[1, 2, 3, 4].map(() => {
|
const { initialPieceIds } = props;
|
||||||
|
|
||||||
|
const [pieceIds] = useState(initialPieceIds);
|
||||||
|
|
||||||
|
const getDiceSet: () => DieViewProps[] = () =>
|
||||||
|
pieceIds.map((pieceId) => {
|
||||||
return {
|
return {
|
||||||
pieceId: getRandomPieceId(),
|
pieceId: pieceId,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isDisabled: 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,
|
rotation: 0,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const [specialDieUsedInRound, setSpecialDieUsedInRound] = useState(false);
|
||||||
|
|
||||||
|
const modifyDieState = (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
|
) => {
|
||||||
|
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<h1>Game Page Title</h1>
|
<h1>Game Page Title</h1>
|
||||||
<div className="game-panel">
|
<div className="game-panel" id={id.toString()}>
|
||||||
<GameBoard dice={dice} setDice={setDice} />
|
<GameBoard
|
||||||
<div className="rigth-panel">
|
modifyDieState={modifyDieState}
|
||||||
<DiceSet dice={dice} setDice={setDice} />
|
fetchDie={fetchDie}
|
||||||
|
setSpecialDieUsedInRound={setSpecialDieUsedInRound}
|
||||||
|
refreshBoardRender={refreshBoardRender}
|
||||||
|
board={board}
|
||||||
|
placePieceActionStack={placePieceActionStack}
|
||||||
|
setPlacePieceActionStack={setPlacePieceActionStack}
|
||||||
|
/>
|
||||||
|
<div className="right-panel">
|
||||||
|
<DiceSet
|
||||||
|
dice={dice}
|
||||||
|
specialDice={specialDice}
|
||||||
|
modifyDieState={modifyDieState}
|
||||||
|
specialDieUsedInRound={specialDieUsedInRound}
|
||||||
|
resetBoard={resetBoard}
|
||||||
|
commitBoard={commitBoard}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,12 @@
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
border-radius: 10%;
|
border-radius: 10%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.piece {
|
.piece {
|
||||||
border-radius: 10%;
|
border-radius: 10%;
|
||||||
position: relative;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
&:nth-child(2) {
|
|
||||||
top: -20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
top: -40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rotate-90 {
|
&.rotate-90 {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
|
|
@ -29,33 +23,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.house {
|
.cell-type-icon {
|
||||||
background-color: blue;
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.university {
|
.cell .exit {
|
||||||
background-color: orange;
|
position: absolute;
|
||||||
}
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
&.factory {
|
&:has(.exit-road) {
|
||||||
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) {
|
|
||||||
border: solid black;
|
border: solid black;
|
||||||
border-width: 0 1px;
|
border-width: 0 1px;
|
||||||
|
|
||||||
|
|
@ -69,7 +53,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.exit:has(.exit-rail) {
|
&:has(.exit-rail) {
|
||||||
border-width: 0 1px;
|
border-width: 0 1px;
|
||||||
|
|
||||||
.exit-rail {
|
.exit-rail {
|
||||||
|
|
@ -82,7 +66,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.exit:has(.exit-ambivalent) {
|
&:has(.exit-ambivalent) {
|
||||||
border-width: 0 1px;
|
border-width: 0 1px;
|
||||||
border-style: dotted;
|
border-style: dotted;
|
||||||
|
|
||||||
|
|
@ -95,47 +79,30 @@
|
||||||
transform: translate(calc(50%));
|
transform: translate(calc(50%));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
&:has(.exit-north), &:has(.exit-south) {
|
||||||
|
left: 50%;
|
||||||
.exit {
|
}
|
||||||
position: relative;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
|
|
||||||
&:has(.exit-north) {
|
&:has(.exit-north) {
|
||||||
left: 50%;
|
|
||||||
transform: translate(calc(-50% - 1px), -100%);
|
transform: translate(calc(-50% - 1px), -100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(.exit-south) {
|
&:has(.exit-south) {
|
||||||
left: 50%;
|
|
||||||
top: 100%;
|
top: 100%;
|
||||||
transform: translate(calc(-50% - 1px), 0%);
|
transform: translate(calc(-50% - 1px), 0%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(.exit-east), &:has(.exit-west) {
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
&:has(.exit-east) {
|
&:has(.exit-east) {
|
||||||
left: 100%;
|
left: 100%;
|
||||||
top: 50%;
|
|
||||||
transform: rotate(90deg) translate(-50%, 0);
|
transform: rotate(90deg) translate(-50%, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(.exit-west) {
|
&:has(.exit-west) {
|
||||||
left: 0%;
|
left: 0%;
|
||||||
top: 50%;
|
|
||||||
transform: rotate(90deg) translate(-50%, 100%);
|
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%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,63 +1,58 @@
|
||||||
import { Cell, directions, Exit, PieceId } from "interface";
|
import { Cell, CellType, directions, Exit, PieceId } from "interface";
|
||||||
import "./BoardCell.scss";
|
import "./BoardCell.scss";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
|
||||||
export interface BoardCellProps {
|
export interface BoardCellProps {
|
||||||
cell: Cell;
|
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;
|
refreshBoardRender: () => void;
|
||||||
|
modifyDieState: (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
|
) => void;
|
||||||
|
fetchDie: (
|
||||||
|
matcher: (die: DieViewProps) => boolean,
|
||||||
|
) => DieViewProps | undefined;
|
||||||
|
setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
placePieceActionHandler: (pieceId: PieceId, rotation: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BoardCell = (props: BoardCellProps) => {
|
const BoardCell = (props: BoardCellProps) => {
|
||||||
const { cell, dice, setDice, refreshBoardRender } = props;
|
const {
|
||||||
|
cell,
|
||||||
|
refreshBoardRender,
|
||||||
|
modifyDieState,
|
||||||
|
fetchDie,
|
||||||
|
setSpecialDieUsedInRound,
|
||||||
|
placePieceActionHandler,
|
||||||
|
} = props;
|
||||||
const [pieceRotationAngle, setPieceRotationAngle] = useState(0);
|
const [pieceRotationAngle, setPieceRotationAngle] = useState(0);
|
||||||
const handleBoardCellClick = () => {
|
const handleBoardCellClick = () => {
|
||||||
const selectedDie = dice.find((die) => die.isSelected);
|
const selectedDie = fetchDie((die) => die.isSelected);
|
||||||
if (!selectedDie) return;
|
if (!selectedDie) return;
|
||||||
try {
|
try {
|
||||||
cell.placePiece(
|
placePieceActionHandler(selectedDie.pieceId, selectedDie.rotation);
|
||||||
selectedDie.pieceId,
|
|
||||||
selectedDie.rotation as 0 | 90 | 180 | 270,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDice(
|
modifyDieState(
|
||||||
dice.map((die) => {
|
(die) => die === selectedDie,
|
||||||
return die !== selectedDie
|
() => {
|
||||||
? die
|
return {
|
||||||
: {
|
isSelected: false,
|
||||||
...selectedDie,
|
isDisabled: true,
|
||||||
isSelected: false,
|
};
|
||||||
isDisabled: true,
|
},
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
if (selectedDie.isSpecial) setSpecialDieUsedInRound(true);
|
||||||
|
// Set rotation to the piece in the board, not the die
|
||||||
setPieceRotationAngle(selectedDie.rotation);
|
setPieceRotationAngle(selectedDie.rotation);
|
||||||
refreshBoardRender();
|
refreshBoardRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={"cell"} onClick={handleBoardCellClick}>
|
||||||
className={"cell " + cell.cellType.toLowerCase()}
|
|
||||||
onClick={handleBoardCellClick}
|
|
||||||
>
|
|
||||||
{directions.map((direction) => {
|
{directions.map((direction) => {
|
||||||
const traversedNode = cell.getNodeAt(direction).traverseBorder();
|
const traversedNode = cell.getNodeAt(direction).traverseBorder();
|
||||||
const isExit = traversedNode instanceof Exit;
|
const isExit = traversedNode instanceof Exit;
|
||||||
|
|
@ -77,6 +72,12 @@ const BoardCell = (props: BoardCellProps) => {
|
||||||
src={`pieces/${cell.placedPiece.id}.jpeg`}
|
src={`pieces/${cell.placedPiece.id}.jpeg`}
|
||||||
></img>
|
></img>
|
||||||
)}
|
)}
|
||||||
|
{cell.cellType !== CellType.NORMAL && (
|
||||||
|
<img
|
||||||
|
className="cell-type-icon"
|
||||||
|
src={`./${cell.cellType.toLowerCase()}-icon.png`}
|
||||||
|
></img>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
.dice-set-actions {
|
.dice-set-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding: 50px 50px 0;
|
|
||||||
width: max-content;
|
width: max-content;
|
||||||
margin: auto;
|
margin: auto auto 50px auto;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border: 3px solid green;
|
border: 3px solid green;
|
||||||
box-shadow: 0 0 10px green;
|
box-shadow: 0 0 10px green;
|
||||||
border-radius: 20%;
|
border-radius: 20%;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
width: 80px;
|
width: 70px;
|
||||||
height: 80px;
|
height: 70px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|
||||||
&.icon-inverted {
|
&.icon-inverted {
|
||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,5 +27,14 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, auto);
|
grid-template-columns: repeat(2, auto);
|
||||||
gap: 20px;
|
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%;
|
||||||
}
|
}
|
||||||
|
|
@ -1,55 +1,51 @@
|
||||||
import { PieceId } from "interface";
|
|
||||||
import "./DiceSet.scss";
|
import "./DiceSet.scss";
|
||||||
import Die from "./Die";
|
import Die from "./Die";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
|
||||||
export interface DiceSetProps {
|
export interface DiceSetProps {
|
||||||
dice: {
|
dice: DieViewProps[];
|
||||||
pieceId: PieceId;
|
specialDice: DieViewProps[];
|
||||||
isSelected: boolean;
|
modifyDieState: (
|
||||||
isDisabled: boolean;
|
matcher: (die: DieViewProps) => boolean,
|
||||||
rotation: number;
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
}[];
|
) => void;
|
||||||
setDice: React.Dispatch<
|
specialDieUsedInRound: boolean;
|
||||||
React.SetStateAction<
|
resetBoard: () => void;
|
||||||
{
|
commitBoard: () => void;
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}[]
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
const DiceSet = (props: DiceSetProps) => {
|
const DiceSet = (props: DiceSetProps) => {
|
||||||
const { dice, setDice } = props;
|
const {
|
||||||
const handleDieClick = (die: {
|
dice,
|
||||||
pieceId: PieceId;
|
specialDice,
|
||||||
isSelected: boolean;
|
modifyDieState,
|
||||||
isDisabled: boolean;
|
specialDieUsedInRound,
|
||||||
rotation: number;
|
resetBoard,
|
||||||
}) => {
|
commitBoard,
|
||||||
if (die.isDisabled) return;
|
} = props;
|
||||||
const newDiceState = dice.map((oldDie) => {
|
const handleDieClick = (clickedDie: DieViewProps) => {
|
||||||
const isSelected = die === oldDie;
|
if (clickedDie.isDisabled) return;
|
||||||
return {
|
const isSpecialDie = clickedDie.isSpecial;
|
||||||
...oldDie,
|
if (isSpecialDie && specialDieUsedInRound) return;
|
||||||
isSelected: isSelected,
|
modifyDieState(
|
||||||
};
|
() => true,
|
||||||
});
|
(die) => {
|
||||||
setDice(newDiceState);
|
return {
|
||||||
|
isSelected: die === clickedDie,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRotateButton = (rotation: number) => {
|
const handleRotateButton = (rotation: number) => {
|
||||||
const newDiceState = dice.map((die) => {
|
modifyDieState(
|
||||||
if (!die.isSelected) return die;
|
(die) => die.isSelected,
|
||||||
const rotationAngle = (die.rotation + rotation + 360) % 360;
|
(die) => {
|
||||||
return {
|
return {
|
||||||
...die,
|
rotation: (die.rotation + rotation + 360) % 360,
|
||||||
rotation: rotationAngle,
|
};
|
||||||
};
|
},
|
||||||
});
|
);
|
||||||
setDice(newDiceState);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -64,12 +60,19 @@ const DiceSet = (props: DiceSetProps) => {
|
||||||
className="icon-inverted"
|
className="icon-inverted"
|
||||||
onClick={() => handleRotateButton(90)}
|
onClick={() => handleRotateButton(90)}
|
||||||
></img>
|
></img>
|
||||||
|
<img src="./clean-icon.png" onClick={resetBoard}></img>
|
||||||
|
<img src="./commit-icon.png" onClick={commitBoard}></img>
|
||||||
</div>
|
</div>
|
||||||
<div className="dice-set">
|
<div className="dice-set">
|
||||||
{dice.map((die) => (
|
{dice.map((die) => (
|
||||||
<Die die={die} handleDieClick={handleDieClick} />
|
<Die die={die} handleDieClick={handleDieClick} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="special-dice-set">
|
||||||
|
{specialDice.map((die) => (
|
||||||
|
<Die die={die} handleDieClick={handleDieClick} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
transition: transform 0.5s cubic-bezier(.47,1.64,.41,.8);
|
transition: transform 0.5s cubic-bezier(.47,1.64,.41,.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-dice-set .dice {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,9 @@
|
||||||
import { PieceId } from "interface";
|
|
||||||
import "./Die.scss";
|
import "./Die.scss";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
|
||||||
interface DieProps {
|
interface DieProps {
|
||||||
die: {
|
die: DieViewProps;
|
||||||
pieceId: PieceId;
|
handleDieClick: (die: DieViewProps) => void;
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
};
|
|
||||||
handleDieClick: (die: {
|
|
||||||
pieceId: PieceId;
|
|
||||||
isSelected: boolean;
|
|
||||||
isDisabled: boolean;
|
|
||||||
rotation: number;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Die = (props: DieProps) => {
|
const Die = (props: DieProps) => {
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,44 @@
|
||||||
import { buildBoard, PieceId } from "interface";
|
import { Cell } from "interface";
|
||||||
import "./GameBoard.scss";
|
import "./GameBoard.scss";
|
||||||
import { useState } from "react";
|
|
||||||
import BoardCell from "./BoardCell";
|
import BoardCell from "./BoardCell";
|
||||||
|
import { DieViewProps } from "../types/DieViewProps";
|
||||||
|
import { PlacePieceActionStack } from "../types/PlacePieceActionStack";
|
||||||
|
import { PlacePieceAction } from "../types/PlacePieceAction";
|
||||||
|
|
||||||
export interface GameBoardProps {
|
export interface GameBoardProps {
|
||||||
dice: {
|
modifyDieState: (
|
||||||
pieceId: PieceId;
|
matcher: (die: DieViewProps) => boolean,
|
||||||
isSelected: boolean;
|
newStateComputer: (die: DieViewProps) => Partial<DieViewProps>,
|
||||||
isDisabled: boolean;
|
) => void;
|
||||||
rotation: number;
|
fetchDie: (
|
||||||
}[];
|
matcher: (die: DieViewProps) => boolean,
|
||||||
setDice: React.Dispatch<
|
) => DieViewProps | undefined;
|
||||||
React.SetStateAction<
|
setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
{
|
refreshBoardRender: () => void;
|
||||||
pieceId: PieceId;
|
board: Cell[][];
|
||||||
isSelected: boolean;
|
placePieceActionStack: PlacePieceActionStack;
|
||||||
isDisabled: boolean;
|
setPlacePieceActionStack: React.Dispatch<
|
||||||
rotation: number;
|
React.SetStateAction<PlacePieceActionStack>
|
||||||
}[]
|
|
||||||
>
|
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GameBoard = (props: GameBoardProps) => {
|
const GameBoard = (props: GameBoardProps) => {
|
||||||
const [board, setBoard] = useState(buildBoard());
|
const { board, placePieceActionStack, setPlacePieceActionStack } = props;
|
||||||
const [id, setId] = useState(1);
|
|
||||||
const refreshBoardRender = () => {
|
|
||||||
setBoard(board);
|
|
||||||
setId(id + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="game-board" id={id.toString()}>
|
<div className="game-board">
|
||||||
{board.flatMap((row) =>
|
{board.flatMap((row, rowIndex) =>
|
||||||
row.map((cell) => (
|
row.map((cell, colIndex) => (
|
||||||
<BoardCell
|
<BoardCell
|
||||||
{...props}
|
{...props}
|
||||||
cell={cell}
|
cell={cell}
|
||||||
refreshBoardRender={refreshBoardRender}
|
placePieceActionHandler={(pieceId, rotation) => {
|
||||||
|
placePieceActionStack.executeAction(
|
||||||
|
new PlacePieceAction(pieceId, rotation, rowIndex, colIndex),
|
||||||
|
board,
|
||||||
|
);
|
||||||
|
setPlacePieceActionStack(placePieceActionStack);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { PieceId } from "interface";
|
||||||
|
|
||||||
|
export interface DieViewProps {
|
||||||
|
pieceId: PieceId;
|
||||||
|
isSelected: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
readonly isSpecial: boolean;
|
||||||
|
rotation: number;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
<div className="lobby-page">
|
||||||
|
<div className="landing-page-title">
|
||||||
|
<h1>Trains And Roads</h1>
|
||||||
|
</div>
|
||||||
|
<div className="lobby-page-body">
|
||||||
|
{playerNames.map((name) => (
|
||||||
|
<article className="lobby-user-name">{name}</article>
|
||||||
|
))}
|
||||||
|
<div className="start-game-button">
|
||||||
|
<button onClick={startGame}>Start Game</button>
|
||||||
|
</div>
|
||||||
|
<article className="game-code">Game code: {gameCode}</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LobbyPage;
|
||||||
Loading…
Reference in New Issue