implemented autoplay for O

main
Inga 🏳‍🌈 6 days ago
parent 7ed91f3621
commit 0b77ff5a1d
  1. 20
      src/backend/main/boardgame-handler.ts
  2. 2
      src/shared/datatypes.ts
  3. 10
      src/shared/opponent.spec.ts
  4. 2
      src/shared/opponent.ts
  5. 0
      src/shared/rules.ts
  6. 20
      src/shared/solver-cache.ts
  7. 10
      src/shared/tictactoe.test.ts

@ -2,11 +2,16 @@ import type { Request, Response } from "express";
import { rewriteQueryParamsWith, safeGetQueryValue } from "../utils.ts"; import { rewriteQueryParamsWith, safeGetQueryValue } from "../utils.ts";
import { BoardgameState } from "../../shared/boardgame-state.ts"; import { BoardgameState } from "../../shared/boardgame-state.ts";
import { getBoardgameHtml } from "../components/boardgame.tsx"; 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, // Returns nothing if query parameter is uninitialized and a redirect was made,
// or component if query parameter is initialized. // or component if query parameter is initialized.
export const handleBoardgame = (req: Request, res: Response, key: string) => { export const handleBoardgame = (req: Request, res: Response, key: string) => {
const rules = gamesRules.tictactoe;
const serializedState = safeGetQueryValue(req, key); const serializedState = safeGetQueryValue(req, key);
const state = BoardgameState.fromSerialized(serializedState); const state = BoardgameState.fromSerialized(serializedState);
@ -16,5 +21,16 @@ export const handleBoardgame = (req: Request, res: Response, key: string) => {
return; 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);
}; };

@ -57,7 +57,7 @@ export type GameRules = {
}; };
export type Opponent = { export type Opponent = {
getNextMove(board: BoardType, currentPlayer: Player): BoardType; getNextMove(board: BoardType, currentPlayer: Player): { row: number; column: number };
}; };
export const getExpectedOutcomeByCurrentOutcome = ( export const getExpectedOutcomeByCurrentOutcome = (

@ -1,6 +1,6 @@
import t from "tap"; import t from "tap";
import { Board } from "./board.ts"; 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"; import { createOpponent } from "./opponent.ts";
void t.test("createOpponent", async (t) => { void t.test("createOpponent", async (t) => {
@ -34,10 +34,10 @@ void t.test("createOpponent", async (t) => {
currentPlayer: Player, currentPlayer: Player,
expectedNextBoardSerialized: string, expectedNextBoardSerialized: string,
) => { ) => {
t.equal( const currentBoard = Board.fromSerialized(currentBoardSerialized);
opponent.getNextMove(Board.fromSerialized(currentBoardSerialized), currentPlayer).serialize(), const nextMove = opponent.getNextMove(currentBoard, currentPlayer);
expectedNextBoardSerialized, const nextBoard = currentBoard.with(nextMove.row, nextMove.column, getOccupiedStateByPlayer(currentPlayer));
); t.equal(nextBoard.serialize(), expectedNextBoardSerialized);
}; };
checkNextMove("X_|__|__", Player.O, "X_|__|_O"); checkNextMove("X_|__|__", Player.O, "X_|__|_O");

@ -25,7 +25,7 @@ export const createOpponent = (outcomesByBoard: Map<string, ExpectedOutcome>): O
nextExpectedOutcome.finalOutcome === currentExpectedOutcome.finalOutcome && nextExpectedOutcome.finalOutcome === currentExpectedOutcome.finalOutcome &&
nextExpectedOutcome.movesLeft === currentExpectedOutcome.movesLeft - 1 nextExpectedOutcome.movesLeft === currentExpectedOutcome.movesLeft - 1
) { ) {
return nextBoard; return { row, column };
} }
} }
} }

@ -0,0 +1,20 @@
import { ExpectedOutcome, GameRules } from "./datatypes.ts";
import { computeAllSolutions } from "./solver.ts";
const solverCache = new Map<GameRules, Map<string, Map<string, ExpectedOutcome>>>();
export const getAllSolutions = (rows: number, columns: number, rules: GameRules) => {
if (!solverCache.has(rules)) {
solverCache.set(rules, new Map<string, Map<string, ExpectedOutcome>>());
}
// 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)!;
};

@ -1,7 +1,7 @@
import t, { Test } from "tap"; import t, { Test } from "tap";
import { Board } from "./board.ts"; 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 { createOpponent } from "./opponent.ts";
import { computeAllSolutions } from "./solver.ts"; import { computeAllSolutions } from "./solver.ts";
import { rules } from "./tictactoe-rules.ts"; import { rules } from "./tictactoe-rules.ts";
@ -153,10 +153,10 @@ void t.test("createOpponent", async (t) => {
currentPlayer: Player, currentPlayer: Player,
expectedNextBoardSerialized: string, expectedNextBoardSerialized: string,
) => { ) => {
t.equal( const currentBoard = Board.fromSerialized(currentBoardSerialized);
opponent.getNextMove(Board.fromSerialized(currentBoardSerialized), currentPlayer).serialize(), const nextMove = opponent.getNextMove(currentBoard, currentPlayer);
expectedNextBoardSerialized, const nextBoard = currentBoard.with(nextMove.row, nextMove.column, getOccupiedStateByPlayer(currentPlayer));
); t.equal(nextBoard.serialize(), expectedNextBoardSerialized);
}; };
void t.test("1x5 board", async (t) => { void t.test("1x5 board", async (t) => {

Loading…
Cancel
Save