diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts
index c71bb2b..cd12587 100644
--- a/interface/types/Cell.ts
+++ b/interface/types/Cell.ts
@@ -105,4 +105,8 @@ export class Cell {
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 ec0ff54..701bdeb 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",
@@ -4201,9 +4201,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/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx
index 4ad8707..529308b 100644
--- a/web/src/pages/game/GamePage.tsx
+++ b/web/src/pages/game/GamePage.tsx
@@ -4,6 +4,7 @@ import DiceSet from "./components/DiceSet";
import "./GamePage.scss";
import { buildBoard, PieceId } from "interface";
import { DieViewProps } from "./types/DieViewProps";
+import { PlacePieceActionStack } from "./types/PlacePieceActionStack";
const GamePage = () => {
const getRandomPieceId = () => {
@@ -71,15 +72,19 @@ const GamePage = () => {
return dice.concat(specialDice).find((die) => matcher(die));
};
- const [storedBoard, setStoredBoard] = useState(buildBoard());
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 = () => {
- setBoard(storedBoard);
+ placePieceActionStack.resetActions(board);
+ setBoard(board);
modifyDieState(
() => true,
() => {
@@ -90,7 +95,7 @@ const GamePage = () => {
};
const commitBoard = () => {
if (dice.some((die) => !die.isDisabled)) return;
- setStoredBoard(board);
+ placePieceActionStack.commitActions();
setDice(getRandomDiceSet());
setSpecialDieUsedInRound(false);
};
@@ -105,6 +110,8 @@ const GamePage = () => {
setSpecialDieUsedInRound={setSpecialDieUsedInRound}
refreshBoardRender={refreshBoardRender}
board={board}
+ placePieceActionStack={placePieceActionStack}
+ setPlacePieceActionStack={setPlacePieceActionStack}
/>
boolean,
) => DieViewProps | undefined;
setSpecialDieUsedInRound: React.Dispatch>;
+ placePieceActionHandler: (pieceId: PieceId, rotation: number) => void;
}
const BoardCell = (props: BoardCellProps) => {
@@ -23,16 +24,14 @@ const BoardCell = (props: BoardCellProps) => {
modifyDieState,
fetchDie,
setSpecialDieUsedInRound,
+ placePieceActionHandler,
} = props;
const [pieceRotationAngle, setPieceRotationAngle] = useState(0);
const handleBoardCellClick = () => {
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;
diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx
index 2ddfb81..94a94f2 100644
--- a/web/src/pages/game/components/GameBoard.tsx
+++ b/web/src/pages/game/components/GameBoard.tsx
@@ -2,6 +2,8 @@ import { Cell } from "interface";
import "./GameBoard.scss";
import BoardCell from "./BoardCell";
import { DieViewProps } from "../types/DieViewProps";
+import { PlacePieceActionStack } from "../types/PlacePieceActionStack";
+import { PlacePieceAction } from "../types/PlacePieceAction";
export interface GameBoardProps {
modifyDieState: (
@@ -14,15 +16,31 @@ export interface GameBoardProps {
setSpecialDieUsedInRound: React.Dispatch>;
refreshBoardRender: () => void;
board: Cell[][];
+ placePieceActionStack: PlacePieceActionStack;
+ setPlacePieceActionStack: React.Dispatch<
+ React.SetStateAction
+ >;
}
const GameBoard = (props: GameBoardProps) => {
- const { board } = props;
+ 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/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 = [];
+ }
+}