diff --git a/src/backend/main/boardgame-handler.ts b/src/backend/main/boardgame-handler.ts index f04931c..cfada31 100644 --- a/src/backend/main/boardgame-handler.ts +++ b/src/backend/main/boardgame-handler.ts @@ -2,11 +2,16 @@ import type { Request, Response } from "express"; import { rewriteQueryParamsWith, safeGetQueryValue } from "../utils.ts"; import { BoardgameState } from "../../shared/boardgame-state.ts"; import { getBoardgameHtml } from "../components/boardgame.tsx"; -import { gamesRules } from "../../shared/rules.spec.ts"; +import { gamesRules } from "../../shared/rules.ts"; +import { getAllSolutions } from "../../shared/solver-cache.ts"; +import { CurrentOutcome, Player } from "../../shared/datatypes.ts"; +import { createOpponent } from "../../shared/opponent.ts"; // Returns nothing if query parameter is uninitialized and a redirect was made, // or component if query parameter is initialized. export const handleBoardgame = (req: Request, res: Response, key: string) => { + const rules = gamesRules.tictactoe; + const serializedState = safeGetQueryValue(req, key); const state = BoardgameState.fromSerialized(serializedState); @@ -16,5 +21,16 @@ export const handleBoardgame = (req: Request, res: Response, key: string) => { return; } - return getBoardgameHtml(key, state, gamesRules.tictactoe); + if (state.board && state.currentPlayer === Player.O) { + const currentOutcome = rules.getBoardOutcome(state.board); + if (currentOutcome === CurrentOutcome.Undecided) { + const solutions = getAllSolutions(state.rows, state.columns, rules); + const nextMove = createOpponent(solutions).getNextMove(state.board, state.currentPlayer); + const newState = state.withMove(nextMove.row, nextMove.column); + rewriteQueryParamsWith(req, res, { [key]: newState.serialize() }); + return; + } + } + + return getBoardgameHtml(key, state, rules); }; diff --git a/src/shared/datatypes.ts b/src/shared/datatypes.ts index 74ffa0d..c6ee123 100644 --- a/src/shared/datatypes.ts +++ b/src/shared/datatypes.ts @@ -57,7 +57,7 @@ export type GameRules = { }; export type Opponent = { - getNextMove(board: BoardType, currentPlayer: Player): BoardType; + getNextMove(board: BoardType, currentPlayer: Player): { row: number; column: number }; }; export const getExpectedOutcomeByCurrentOutcome = ( diff --git a/src/shared/opponent.spec.ts b/src/shared/opponent.spec.ts index 14d3aae..10891a7 100644 --- a/src/shared/opponent.spec.ts +++ b/src/shared/opponent.spec.ts @@ -1,6 +1,6 @@ import t from "tap"; import { Board } from "./board.ts"; -import { ExpectedOutcome, FinalOutcome, Player } from "./datatypes.ts"; +import { ExpectedOutcome, FinalOutcome, Player, getOccupiedStateByPlayer } from "./datatypes.ts"; import { createOpponent } from "./opponent.ts"; void t.test("createOpponent", async (t) => { @@ -34,10 +34,10 @@ void t.test("createOpponent", async (t) => { currentPlayer: Player, expectedNextBoardSerialized: string, ) => { - t.equal( - opponent.getNextMove(Board.fromSerialized(currentBoardSerialized), currentPlayer).serialize(), - expectedNextBoardSerialized, - ); + const currentBoard = Board.fromSerialized(currentBoardSerialized); + const nextMove = opponent.getNextMove(currentBoard, currentPlayer); + const nextBoard = currentBoard.with(nextMove.row, nextMove.column, getOccupiedStateByPlayer(currentPlayer)); + t.equal(nextBoard.serialize(), expectedNextBoardSerialized); }; checkNextMove("X_|__|__", Player.O, "X_|__|_O"); diff --git a/src/shared/opponent.ts b/src/shared/opponent.ts index 949a21f..be7601d 100644 --- a/src/shared/opponent.ts +++ b/src/shared/opponent.ts @@ -25,7 +25,7 @@ export const createOpponent = (outcomesByBoard: Map): O nextExpectedOutcome.finalOutcome === currentExpectedOutcome.finalOutcome && nextExpectedOutcome.movesLeft === currentExpectedOutcome.movesLeft - 1 ) { - return nextBoard; + return { row, column }; } } } diff --git a/src/shared/rules.spec.ts b/src/shared/rules.ts similarity index 100% rename from src/shared/rules.spec.ts rename to src/shared/rules.ts diff --git a/src/shared/solver-cache.ts b/src/shared/solver-cache.ts new file mode 100644 index 0000000..2f613be --- /dev/null +++ b/src/shared/solver-cache.ts @@ -0,0 +1,20 @@ +import { ExpectedOutcome, GameRules } from "./datatypes.ts"; +import { computeAllSolutions } from "./solver.ts"; + +const solverCache = new Map>>(); + +export const getAllSolutions = (rows: number, columns: number, rules: GameRules) => { + if (!solverCache.has(rules)) { + solverCache.set(rules, new Map>()); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we have just ensured that it exists. + const solverCacheForRules = solverCache.get(rules)!; + const dimensionsKey = `${rows}x${columns}`; + if (!solverCacheForRules.has(dimensionsKey)) { + solverCacheForRules.set(dimensionsKey, computeAllSolutions(rows, columns, rules)); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we have just ensured that it exists. + return solverCacheForRules.get(dimensionsKey)!; +}; diff --git a/src/shared/tictactoe.test.ts b/src/shared/tictactoe.test.ts index 7892896..546f9e2 100644 --- a/src/shared/tictactoe.test.ts +++ b/src/shared/tictactoe.test.ts @@ -1,7 +1,7 @@ import t, { Test } from "tap"; import { Board } from "./board.ts"; -import { ExpectedOutcome, FinalOutcome, Opponent, Player } from "./datatypes.ts"; +import { ExpectedOutcome, FinalOutcome, Opponent, Player, getOccupiedStateByPlayer } from "./datatypes.ts"; import { createOpponent } from "./opponent.ts"; import { computeAllSolutions } from "./solver.ts"; import { rules } from "./tictactoe-rules.ts"; @@ -153,10 +153,10 @@ void t.test("createOpponent", async (t) => { currentPlayer: Player, expectedNextBoardSerialized: string, ) => { - t.equal( - opponent.getNextMove(Board.fromSerialized(currentBoardSerialized), currentPlayer).serialize(), - expectedNextBoardSerialized, - ); + const currentBoard = Board.fromSerialized(currentBoardSerialized); + const nextMove = opponent.getNextMove(currentBoard, currentPlayer); + const nextBoard = currentBoard.with(nextMove.row, nextMove.column, getOccupiedStateByPlayer(currentPlayer)); + t.equal(nextBoard.serialize(), expectedNextBoardSerialized); }; void t.test("1x5 board", async (t) => {