implemented autoplay for O

main
Inga 🏳‍🌈 3 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 { 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);
};

@ -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 = (

@ -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");

@ -25,7 +25,7 @@ export const createOpponent = (outcomesByBoard: Map<string, ExpectedOutcome>): O
nextExpectedOutcome.finalOutcome === currentExpectedOutcome.finalOutcome &&
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 { 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) => {

Loading…
Cancel
Save