diff --git a/src/backend/components/boardgame.tsx b/src/backend/components/boardgame.tsx index 8e15226..b0ecd76 100644 --- a/src/backend/components/boardgame.tsx +++ b/src/backend/components/boardgame.tsx @@ -110,6 +110,16 @@ export const getBoardgameHtml = (key: string, gameState: BoardgameStateType, gam Restart game

+

+ + {" / "} + +

+

+ + {" / "} + +

); diff --git a/src/frontend/components/board-game.ts b/src/frontend/components/board-game.ts index 4a4eeff..899b1bc 100644 --- a/src/frontend/components/board-game.ts +++ b/src/frontend/components/board-game.ts @@ -48,18 +48,46 @@ export class BoardGameComponent extends HTMLElement { for (const tbodyUntyped of this.querySelectorAll("tbody.game-board")) { const tbody = tbodyUntyped as HTMLTableSectionElement; + + for (let rowNumberToRemove = gameState.rows; rowNumberToRemove < tbody.rows.length; rowNumberToRemove++) { + tbody.rows[rowNumberToRemove]?.remove(); + } + + for (let rowNumberToAdd = tbody.rows.length; rowNumberToAdd < gameState.rows; rowNumberToAdd++) { + tbody.insertRow(); + } + for (let rowNumber = 0; rowNumber < tbody.rows.length; rowNumber++) { const row = tbody.rows[rowNumber]; if (!row) { continue; } + for ( + let columnNumberToRemove = gameState.columns; + columnNumberToRemove < row.cells.length; + columnNumberToRemove++ + ) { + row.cells[columnNumberToRemove]?.remove(); + } + + for (let columnNumberToAdd = row.cells.length; columnNumberToAdd < gameState.columns; columnNumberToAdd++) { + row.insertCell(); + } + for (let columnNumber = 0; columnNumber < row.cells.length; columnNumber++) { const cell = row.cells[columnNumber]; if (!cell) { continue; } + if (!cell.childNodes.length) { + const button = document.createElement("button"); + button.type = "submit"; + button.name = key; + cell.append(button); + } + const { isDisabled, nextGameState, text } = getCellDisplayData({ gameState, currentOutcome, diff --git a/src/shared/datatypes/boardgame-state.spec.ts b/src/shared/datatypes/boardgame-state.spec.ts index a516120..dfc56a5 100644 --- a/src/shared/datatypes/boardgame-state.spec.ts +++ b/src/shared/datatypes/boardgame-state.spec.ts @@ -236,6 +236,122 @@ void t.test("withEmptyBoard", async (t) => { }); }); +void t.test("withAdditionalRow", async (t) => { + void t.test("On uninitialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20..."); + + const newState = oldState?.withAdditionalRow(); + t.equal(newState?.serialize(), "11x20..."); + t.equal(newState?.rows, 11); + t.equal(newState?.columns, 20); + }); + + void t.test("On initialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20.XO.X.___"); + + const newState = oldState?.withAdditionalRow(); + t.equal(newState?.serialize(), "11x20.XO.."); + t.equal(newState?.rows, 11); + t.equal(newState?.columns, 20); + }); +}); + +void t.test("withoutAdditionalRow", async (t) => { + void t.test("On uninitialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20..."); + + const newState = oldState?.withoutAdditionalRow(); + t.equal(newState?.serialize(), "9x20..."); + t.equal(newState?.rows, 9); + t.equal(newState?.columns, 20); + }); + + void t.test("On initialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20.XO.X.___"); + + const newState = oldState?.withoutAdditionalRow(); + t.equal(newState?.serialize(), "9x20.XO.."); + t.equal(newState?.rows, 9); + t.equal(newState?.columns, 20); + }); + + void t.test("On initialized board with 1 row", async (t) => { + const oldState = createAndCheckBoardgameState(t, "0x20.XO.X.___"); + + const newState = oldState?.withoutAdditionalRow(); + t.equal(newState?.serialize(), "0x20.XO.."); + t.equal(newState?.rows, 0); + t.equal(newState?.columns, 20); + }); + + void t.test("On initialized board with 0 rows", async (t) => { + const oldState = createAndCheckBoardgameState(t, "0x20.XO.X.___"); + + const newState = oldState?.withoutAdditionalRow(); + t.equal(newState?.serialize(), "0x20.XO.."); + t.equal(newState?.rows, 0); + t.equal(newState?.columns, 20); + }); +}); + +void t.test("withAdditionalColumn", async (t) => { + void t.test("On uninitialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20..."); + + const newState = oldState?.withAdditionalColumn(); + t.equal(newState?.serialize(), "10x21..."); + t.equal(newState?.rows, 10); + t.equal(newState?.columns, 21); + }); + + void t.test("On initialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20.XO.X.___"); + + const newState = oldState?.withAdditionalColumn(); + t.equal(newState?.serialize(), "10x21.XO.."); + t.equal(newState?.rows, 10); + t.equal(newState?.columns, 21); + }); +}); + +void t.test("withoutAdditionalColumn", async (t) => { + void t.test("On uninitialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20..."); + + const newState = oldState?.withoutAdditionalColumn(); + t.equal(newState?.serialize(), "10x19..."); + t.equal(newState?.rows, 10); + t.equal(newState?.columns, 19); + }); + + void t.test("On initialized board", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x20.XO.X.___"); + + const newState = oldState?.withoutAdditionalColumn(); + t.equal(newState?.serialize(), "10x19.XO.."); + t.equal(newState?.rows, 10); + t.equal(newState?.columns, 19); + }); + + void t.test("On initialized board with 1 columns", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x1.XO.X.___"); + + const newState = oldState?.withoutAdditionalColumn(); + t.equal(newState?.serialize(), "10x0.XO.."); + t.equal(newState?.rows, 10); + t.equal(newState?.columns, 0); + }); + + void t.test("On initialized board with 0 columns", async (t) => { + const oldState = createAndCheckBoardgameState(t, "10x0.XO.X.___"); + + const newState = oldState?.withoutAdditionalColumn(); + t.equal(newState?.serialize(), "10x0.XO.."); + t.equal(newState?.rows, 10); + t.equal(newState?.columns, 0); + }); +}); + void t.test("withAutoPlayer", async (t) => { void t.test("On uninitialized board", async (t) => { const oldState = createAndCheckBoardgameState(t, "2x3..."); diff --git a/src/shared/datatypes/boardgame-state.ts b/src/shared/datatypes/boardgame-state.ts index 71ca590..5b031f5 100644 --- a/src/shared/datatypes/boardgame-state.ts +++ b/src/shared/datatypes/boardgame-state.ts @@ -79,6 +79,22 @@ export class BoardgameState implements BoardgameStateType { ); } + withAdditionalRow() { + return new BoardgameState(this.rows + 1, this.columns, this.autoPlayers, null, null); + } + + withoutAdditionalRow() { + return new BoardgameState(Math.max(0, this.rows - 1), this.columns, this.autoPlayers, null, null); + } + + withAdditionalColumn() { + return new BoardgameState(this.rows, this.columns + 1, this.autoPlayers, null, null); + } + + withoutAdditionalColumn() { + return new BoardgameState(this.rows, Math.max(0, this.columns - 1), this.autoPlayers, null, null); + } + withAutoPlayer(player: Player) { const autoPlayers = new Set(this.autoPlayers); autoPlayers.add(player); diff --git a/src/shared/datatypes/types.ts b/src/shared/datatypes/types.ts index 36c0953..6dbcb72 100644 --- a/src/shared/datatypes/types.ts +++ b/src/shared/datatypes/types.ts @@ -49,6 +49,10 @@ export type BoardgameStateType = { readonly currentPlayerName: string; withEmptyBoard(): BoardgameStateType; + withAdditionalRow(): BoardgameStateType; + withoutAdditionalRow(): BoardgameStateType; + withAdditionalColumn(): BoardgameStateType; + withoutAdditionalColumn(): BoardgameStateType; withAutoPlayer(player: Player): BoardgameStateType; withoutAutoPlayer(player: Player): BoardgameStateType; withMove(row: number, column: number): BoardgameStateType; diff --git a/src/shared/display.ts b/src/shared/display.ts index a75028d..2f7ece4 100644 --- a/src/shared/display.ts +++ b/src/shared/display.ts @@ -50,6 +50,10 @@ export const getButtonValues = (gameState: BoardgameStateType) => ({ "autoplayer-o-enable": gameState.withAutoPlayer(Player.O), "autoplayer-o-disable": gameState.withoutAutoPlayer(Player.O), "game-start": gameState.withEmptyBoard(), + "add-row": gameState.withAdditionalRow(), + "remove-row": gameState.withoutAdditionalRow(), + "add-column": gameState.withAdditionalColumn(), + "remove-column": gameState.withoutAdditionalColumn(), }); export type ButtonValues = ReturnType; diff --git a/src/shared/gameplay/solver.spec.ts b/src/shared/gameplay/solver.spec.ts index 4f907d6..03d995e 100644 --- a/src/shared/gameplay/solver.spec.ts +++ b/src/shared/gameplay/solver.spec.ts @@ -96,6 +96,12 @@ void t.test("computeAllSolutions", async (t) => { t.matchOnlyStrict(Object.fromEntries(solutions.entries()), expectedSolutions); }; + void t.test("too large boards", async (t) => { + t.throws(() => computeAllSolutions(4, 5, createRulesForSquares()), { + message: "Board is too large, solving requires too many computational resources", + }); + }); + void t.test("empty boards", async (t) => { void t.test("0x0 board", async (t) => { checkSolutionsComplete(t, 0, 0, [], { diff --git a/src/shared/gameplay/solver.ts b/src/shared/gameplay/solver.ts index 33e8c4e..85b0f16 100644 --- a/src/shared/gameplay/solver.ts +++ b/src/shared/gameplay/solver.ts @@ -60,6 +60,10 @@ export const getPreferredNextOutcome = ( }; export const computeAllSolutions = (rows: number, columns: number, rules: GameRules) => { + if (rows * columns > 9) { + throw new Error("Board is too large, solving requires too many computational resources"); + } + const expectedOutcomesByBoard = new Map(); const getExpectedOutcomeForBoard = (board: BoardType, currentPlayer: Player): ExpectedOutcome => {