diff --git a/src/lib/opponent.spec.ts b/src/lib/opponent.spec.ts new file mode 100644 index 0000000..22a8a7a --- /dev/null +++ b/src/lib/opponent.spec.ts @@ -0,0 +1,67 @@ +import t from "tap"; +import { ExpectedOutcome, FinalOutcome, Player } from "./datatypes.ts"; +import { createOpponent } from "./opponent.ts"; +import { Board } from "./board.ts"; + +void t.test("createOpponent", async (t) => { + const outcomesByBoard: Record = { + "X_|__|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, + "XO|__|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 9 }, + "X_|O_|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 20 }, + "X_|_O|__": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, + "X_|__|O_": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, + "X_|__|_O": { finalOutcome: FinalOutcome.WinO, movesLeft: 9 }, + "XX|__|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 9 }, + "X_|X_|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 20 }, + "X_|_X|__": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, + "X_|__|X_": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, + "X_|__|_X": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, + "XO|XO|XO": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + + // Incorrect scenarios + // movesLeft is not zero, but there is no information about next boards + "XX|XX|X_": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, + // movesLeft is not zero, and information about next boards + // is inconsistent with information about current board + "OO|OO|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, + "OO|OO|O_": { finalOutcome: FinalOutcome.WinO, movesLeft: 0 }, + "OO|OO|_O": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, + }; + const opponent = createOpponent(new Map(Object.entries(outcomesByBoard))); + + const checkNextMove = ( + currentBoardSerialized: string, + currentPlayer: Player, + expectedNextBoardSerialized: string, + ) => { + t.equal( + opponent + .getNextMove( + Board.fromSerialized(currentBoardSerialized), + currentPlayer, + ) + .serialize(), + expectedNextBoardSerialized, + ); + }; + + checkNextMove("X_|__|__", Player.O, "X_|__|_O"); + checkNextMove("X_|__|__", Player.X, "XX|__|__"); + + t.throws( + () => opponent.getNextMove(Board.fromSerialized("XO|XO|XO"), Player.X), + { message: "There are no possible moves left: XO|XO|XO" }, + ); + t.throws( + () => opponent.getNextMove(Board.fromSerialized("XX|XX|XX"), Player.X), + { message: "Board is not solved: XX|XX|XX" }, + ); + t.throws( + () => opponent.getNextMove(Board.fromSerialized("XX|XX|X_"), Player.X), + { message: "Next board is not solved: XX|XX|XX" }, + ); + t.throws( + () => opponent.getNextMove(Board.fromSerialized("OO|OO|__"), Player.O), + { message: "Cannot find appropriate next move: OO|OO|__" }, + ); +}); diff --git a/src/lib/opponent.ts b/src/lib/opponent.ts new file mode 100644 index 0000000..a0c644d --- /dev/null +++ b/src/lib/opponent.ts @@ -0,0 +1,52 @@ +import { + BoardType, + ExpectedOutcome, + Player, + SquareState, + getOccupiedStateByPlayer, +} from "./datatypes.ts"; + +export const createOpponent = ( + outcomesByBoard: Map, +) => { + const getNextMove = (board: BoardType, currentPlayer: Player) => { + const currentExpectedOutcome = outcomesByBoard.get(board.serialize()); + if (!currentExpectedOutcome) { + throw new Error(`Board is not solved: ${board.serialize()}`); + } + + if (!currentExpectedOutcome.movesLeft) { + throw new Error(`There are no possible moves left: ${board.serialize()}`); + } + + const occupiedState = getOccupiedStateByPlayer(currentPlayer); + for (let row = 0; board.hasRow(row); row++) { + for (let column = 0; board.hasSquare(row, column); column++) { + if (board.get(row, column) === SquareState.Unoccupied) { + const nextBoard = board.with(row, column, occupiedState); + const nextExpectedOutcome = outcomesByBoard.get( + nextBoard.serialize(), + ); + if (!nextExpectedOutcome) { + throw new Error( + `Next board is not solved: ${nextBoard.serialize()}`, + ); + } + + if ( + nextExpectedOutcome.finalOutcome === + currentExpectedOutcome.finalOutcome && + nextExpectedOutcome.movesLeft === + currentExpectedOutcome.movesLeft - 1 + ) { + return nextBoard; + } + } + } + } + + throw new Error(`Cannot find appropriate next move: ${board.serialize()}`); + }; + + return { getNextMove }; +}; diff --git a/src/lib/solver.ts b/src/lib/solver.ts index 409071a..c404e57 100644 --- a/src/lib/solver.ts +++ b/src/lib/solver.ts @@ -1,5 +1,6 @@ import { Board } from "./board.ts"; import { + BoardType, CurrentOutcome, ExpectedOutcome, FinalOutcome, @@ -77,7 +78,7 @@ export const computeAllSolutions = ( const expectedOutcomesByBoard = new Map(); const getExpectedOutcomeForBoard = ( - board: Board, + board: BoardType, currentPlayer: Player, ): ExpectedOutcome => { // assuming that currentPlayer is always the same for the same board