Fix commit and reset actions crossing pointers by creating an action stack (will be used to send data to backend)

game-page
MiguelMLorente 2024-12-05 10:46:38 +01:00
parent 6a483b1bd7
commit d69c8944c3
9 changed files with 117 additions and 17 deletions

View File

@ -105,4 +105,8 @@ export class Cell {
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;
}
} }

8
web/package-lock.json generated
View File

@ -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": {

View File

@ -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",

View File

@ -8,7 +8,7 @@ export interface AppProps {
} }
const App = (props: AppProps) => { const App = (props: AppProps) => {
const renderedPage: string = "LANDING"; const renderedPage: string = "GAME";
return ( return (
<div className="app"> <div className="app">

View File

@ -4,6 +4,7 @@ import DiceSet from "./components/DiceSet";
import "./GamePage.scss"; import "./GamePage.scss";
import { buildBoard, PieceId } from "interface"; import { buildBoard, PieceId } from "interface";
import { DieViewProps } from "./types/DieViewProps"; import { DieViewProps } from "./types/DieViewProps";
import { PlacePieceActionStack } from "./types/PlacePieceActionStack";
const GamePage = () => { const GamePage = () => {
const getRandomPieceId = () => { const getRandomPieceId = () => {
@ -71,15 +72,19 @@ const GamePage = () => {
return dice.concat(specialDice).find((die) => matcher(die)); return dice.concat(specialDice).find((die) => matcher(die));
}; };
const [storedBoard, setStoredBoard] = useState(buildBoard());
const [board, setBoard] = useState(buildBoard()); const [board, setBoard] = useState(buildBoard());
const [id, setId] = useState(1); const [id, setId] = useState(1);
const refreshBoardRender = () => { const refreshBoardRender = () => {
setBoard(board); setBoard(board);
setId(id + 1); setId(id + 1);
}; };
const [placePieceActionStack, setPlacePieceActionStack] = useState(
new PlacePieceActionStack(),
);
const resetBoard = () => { const resetBoard = () => {
setBoard(storedBoard); placePieceActionStack.resetActions(board);
setBoard(board);
modifyDieState( modifyDieState(
() => true, () => true,
() => { () => {
@ -90,7 +95,7 @@ const GamePage = () => {
}; };
const commitBoard = () => { const commitBoard = () => {
if (dice.some((die) => !die.isDisabled)) return; if (dice.some((die) => !die.isDisabled)) return;
setStoredBoard(board); placePieceActionStack.commitActions();
setDice(getRandomDiceSet()); setDice(getRandomDiceSet());
setSpecialDieUsedInRound(false); setSpecialDieUsedInRound(false);
}; };
@ -105,6 +110,8 @@ const GamePage = () => {
setSpecialDieUsedInRound={setSpecialDieUsedInRound} setSpecialDieUsedInRound={setSpecialDieUsedInRound}
refreshBoardRender={refreshBoardRender} refreshBoardRender={refreshBoardRender}
board={board} board={board}
placePieceActionStack={placePieceActionStack}
setPlacePieceActionStack={setPlacePieceActionStack}
/> />
<div className="right-panel"> <div className="right-panel">
<DiceSet <DiceSet

View File

@ -1,4 +1,4 @@
import { Cell, CellType, directions, Exit } 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"; import { DieViewProps } from "../types/DieViewProps";
@ -14,6 +14,7 @@ export interface BoardCellProps {
matcher: (die: DieViewProps) => boolean, matcher: (die: DieViewProps) => boolean,
) => DieViewProps | undefined; ) => DieViewProps | undefined;
setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>; setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>;
placePieceActionHandler: (pieceId: PieceId, rotation: number) => void;
} }
const BoardCell = (props: BoardCellProps) => { const BoardCell = (props: BoardCellProps) => {
@ -23,16 +24,14 @@ const BoardCell = (props: BoardCellProps) => {
modifyDieState, modifyDieState,
fetchDie, fetchDie,
setSpecialDieUsedInRound, setSpecialDieUsedInRound,
placePieceActionHandler,
} = props; } = props;
const [pieceRotationAngle, setPieceRotationAngle] = useState(0); const [pieceRotationAngle, setPieceRotationAngle] = useState(0);
const handleBoardCellClick = () => { const handleBoardCellClick = () => {
const selectedDie = fetchDie((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;

View File

@ -2,6 +2,8 @@ import { Cell } from "interface";
import "./GameBoard.scss"; import "./GameBoard.scss";
import BoardCell from "./BoardCell"; import BoardCell from "./BoardCell";
import { DieViewProps } from "../types/DieViewProps"; import { DieViewProps } from "../types/DieViewProps";
import { PlacePieceActionStack } from "../types/PlacePieceActionStack";
import { PlacePieceAction } from "../types/PlacePieceAction";
export interface GameBoardProps { export interface GameBoardProps {
modifyDieState: ( modifyDieState: (
@ -14,15 +16,31 @@ export interface GameBoardProps {
setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>; setSpecialDieUsedInRound: React.Dispatch<React.SetStateAction<boolean>>;
refreshBoardRender: () => void; refreshBoardRender: () => void;
board: Cell[][]; board: Cell[][];
placePieceActionStack: PlacePieceActionStack;
setPlacePieceActionStack: React.Dispatch<
React.SetStateAction<PlacePieceActionStack>
>;
} }
const GameBoard = (props: GameBoardProps) => { const GameBoard = (props: GameBoardProps) => {
const { board } = props; const { board, placePieceActionStack, setPlacePieceActionStack } = props;
return ( return (
<div className="game-board"> <div className="game-board">
{board.flatMap((row) => {board.flatMap((row, rowIndex) =>
row.map((cell) => <BoardCell {...props} cell={cell} />), row.map((cell, colIndex) => (
<BoardCell
{...props}
cell={cell}
placePieceActionHandler={(pieceId, rotation) => {
placePieceActionStack.executeAction(
new PlacePieceAction(pieceId, rotation, rowIndex, colIndex),
board,
);
setPlacePieceActionStack(placePieceActionStack);
}}
/>
)),
)} )}
</div> </div>
); );

View File

@ -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();
}
}

View File

@ -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 = [];
}
}