diff --git a/eslint.config.js b/eslint.config.js index 0f337f7..8d43e46 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -22,7 +22,7 @@ export default tsEslint.config( }, }, { - files: ["**/*.spec.*"], + files: ["**/*.spec.*", "**/*.test.*"], rules: { // To make it easier to pass incorrect data to functions under test "@typescript-eslint/no-explicit-any": "off", diff --git a/src/lib/solver.spec.ts b/src/lib/solver.spec.ts index 87a6a83..78c8715 100644 --- a/src/lib/solver.spec.ts +++ b/src/lib/solver.spec.ts @@ -1,8 +1,7 @@ -import t from "tap"; +import t, { type Test } from "tap"; -import { ExpectedOutcome, FinalOutcome, Player } from "./datatypes.ts"; +import { CurrentOutcome, ExpectedOutcome, FinalOutcome, GameRules, Player, SquareState } from "./datatypes.ts"; import { computeAllSolutions, getPreferredNextOutcome } from "./solver.ts"; -import { rules } from "./tictactoe-rules.ts"; void t.test("getPreferredNextOutcome", async (t) => { const nextOutcomes: ExpectedOutcome[] = [ @@ -84,237 +83,359 @@ void t.test("getPreferredNextOutcome", async (t) => { }); void t.test("computeAllSolutions", async (t) => { + // Simple rules: whoever occupies all the squares passed as a parameter, wins + const createRulesForSquares = (...squares: [number, number][]): GameRules => ({ + getBoardOutcome: (board) => { + let hasX = false; + let hasO = false; + let hasUnoccupied = false; + + for (const [row, column] of squares) { + const state = board.get(row, column); + switch (state) { + case SquareState.X: + hasX = true; + break; + case SquareState.O: + hasO = true; + break; + case SquareState.Unoccupied: + hasUnoccupied = true; + break; + } + } + + if (hasX && hasO) { + return CurrentOutcome.Draw; + } + + if (hasUnoccupied) { + return CurrentOutcome.Undecided; + } + + if (hasX) { + return CurrentOutcome.WinX; + } + + if (hasO) { + return CurrentOutcome.WinO; + } + + // Special case: if there are no X, O or unoccupied squares, + // this means that the squares list is empty, + // and in this case we only want to decide the board as Draw when it's full + // and no moves can be made. + if (board.serialize().includes("_")) { + return CurrentOutcome.Undecided; + } + + return CurrentOutcome.Draw; + }, + }); + const checkSolutionsComplete = ( + t: Test, rows: number, columns: number, + squares: [number, number][], expectedSolutions: Record, ) => { - t.matchOnlyStrict(Object.fromEntries(computeAllSolutions(rows, columns, rules).entries()), expectedSolutions); - }; - - const checkSolutionsIncomplete = ( - rows: number, - columns: number, - expectedSolutionsCount: number, - expectedSolutionsIncomplete: Record, - ) => { - const allSolutions = computeAllSolutions(rows, columns, rules); - t.equal(allSolutions.size, expectedSolutionsCount); - t.matchStrict(Object.fromEntries(allSolutions.entries()), expectedSolutionsIncomplete); + const rules = createRulesForSquares(...squares); + const solutions = computeAllSolutions(rows, columns, rules); + t.matchOnlyStrict(Object.fromEntries(solutions.entries()), expectedSolutions); }; - checkSolutionsComplete(0, 0, { - "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + void t.test("empty boards", async (t) => { + void t.test("0x0 board", async (t) => { + checkSolutionsComplete(t, 0, 0, [], { + "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("0x1 board", async (t) => { + checkSolutionsComplete(t, 0, 1, [], { + "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("0x2 board", async (t) => { + checkSolutionsComplete(t, 0, 2, [], { + "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("1x0 board", async (t) => { + checkSolutionsComplete(t, 1, 0, [], { + "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("2x0 board", async (t) => { + checkSolutionsComplete(t, 2, 0, [], { + "|": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); }); - checkSolutionsComplete(1, 0, { - "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - }); - - checkSolutionsComplete(1, 1, { - _: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - X: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + void t.test("1x1 board", async (t) => { + void t.test("no winning squares", async (t) => { + checkSolutionsComplete(t, 1, 1, [], { + _: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + X: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("one winning square", async (t) => { + checkSolutionsComplete(t, 1, 1, [[0, 0]], { + _: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + X: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + }); + }); }); // making sure that we don't have any bugs that would manifest when width > height - checkSolutionsComplete(1, 3, { - ___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - - X__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - __X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - - XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - X_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - - XOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - XXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - OXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + void t.test("1x3 board", async (t) => { + void t.test("no winning squares", async (t) => { + checkSolutionsComplete(t, 1, 3, [], { + ___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + + X__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + __X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + X_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + + XOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + XXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + OXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("one winning square", async (t) => { + checkSolutionsComplete(t, 1, 3, [[0, 1]], { + ___: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + + X__: { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + _X_: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + __X: { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + + XO_: { finalOutcome: FinalOutcome.WinO, movesLeft: 0 }, + X_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + O_X: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + _OX: { finalOutcome: FinalOutcome.WinO, movesLeft: 0 }, + + XXO: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + OXX: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + }); + }); + + void t.test("two winning squares", async (t) => { + checkSolutionsComplete( + t, + 1, + 3, + [ + [0, 0], + [0, 1], + ], + { + ___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + + X__: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + __X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + X_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + _XO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + + XOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + XXO: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + OXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }, + ); + }); }); // making sure that we don't have any bugs that would manifest when height > width - checkSolutionsComplete(3, 1, { - "_|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - - "X|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "_|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "_|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - - "X|O|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "X|_|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "O|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "_|X|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "O|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "_|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - - "X|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - "X|X|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - "O|X|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - }); - - // smallest possible board where X can win - checkSolutionsComplete(1, 5, { - _____: { finalOutcome: FinalOutcome.Draw, movesLeft: 5 }, - - X____: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, - XO___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - X_O__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - X__O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - X___O: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - _X___: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, - OX___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - _XO__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - _X_O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - _X__O: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, - __X__: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, - O_X__: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, - _OX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - __XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - __X_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, - ___X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, - O__X_: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, - _O_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - __OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - ___XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - ____X: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, - O___X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - _O__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - __O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - ___OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - - XOX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - XO_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - XO__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - XXO__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - X_OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - X_O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - XX_O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - X_XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - X__OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - XX__O: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - X_X_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - X__XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - OXX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - OX_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - OX__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _XOX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _XO_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _XXO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _X_OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _XX_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 2 }, - _X_XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - O_XX_: { finalOutcome: FinalOutcome.WinX, movesLeft: 2 }, - O_X_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _OXX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _OX_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - __XOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - __XXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - O__XX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - _O_XX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - __OXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - - XOXO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XOX_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XOOX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XO_XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XOO_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XO_OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XXOO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XXO_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - X_OXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - X_OOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - XX_OO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - X_XOO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - OXXO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - OXX_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - OXOX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - OX_XO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - OXO_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - OX_OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _XOXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _XOOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _XXOO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - OOXX_: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - O_XXO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - OOX_X: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - O_XOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _OXXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _OXOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - OO_XX: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - O_OXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - _OOXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - - XOXOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - XOXXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - XOOXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - XXOOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - XXOXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - XXXOO: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, - OXXOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - OXXXO: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, - OXOXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - OOXXX: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, - }); - - checkSolutionsComplete(2, 2, { - "__|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, - - "X_|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - "XO|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "X_|O_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "X_|_O": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - - "_X|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - "OX|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "_X|O_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "_X|_O": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - - "__|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - "O_|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "_O|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "__|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - - "__|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, - "O_|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "_O|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - "__|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, - - "XO|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "XO|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "XX|O_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "X_|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "XX|_O": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "X_|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "OX|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "OX|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "_X|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "_X|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "O_|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - "_O|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, - - "XO|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - "XO|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - "XX|OO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - "OX|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - "OX|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, - "OO|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + void t.test("3x1 board", async (t) => { + void t.test("no winning squares", async (t) => { + checkSolutionsComplete(t, 3, 1, [], { + "_|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + + "X|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "_|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "_|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "X|O|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "X|_|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "O|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_|X|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "O|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + + "X|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "X|X|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "O|X|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("one winning square", async (t) => { + checkSolutionsComplete(t, 3, 1, [[1, 0]], { + "_|_|_": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + + "X|_|_": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + "_|X|_": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + "_|_|X": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + + "X|O|_": { finalOutcome: FinalOutcome.WinO, movesLeft: 0 }, + "X|_|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + "O|_|X": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + "_|O|X": { finalOutcome: FinalOutcome.WinO, movesLeft: 0 }, + + "X|X|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + "O|X|X": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + }); + }); + + void t.test("two winning squares", async (t) => { + checkSolutionsComplete( + t, + 3, + 1, + [ + [0, 0], + [1, 0], + ], + { + "_|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + + "X|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "X|O|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "X|_|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + "O|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "_|X|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + "O|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + + "X|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "X|X|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + "O|X|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }, + ); + }); }); - // number 5478 taken from https://math.stackexchange.com/a/613505 - checkSolutionsIncomplete(3, 3, 5478, { - "___|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 9 }, - "X__|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, - "_X_|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, - "___|_X_|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, - "XO_|___|___": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, - "X__|___|_O_": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, - "X_O|___|___": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, - "X__|___|__O": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, - "OO_|___|_XX": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, - "OO_|__X|_XX": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + void t.test("2x2 board", async (t) => { + void t.test("no winning squares", async (t) => { + checkSolutionsComplete(t, 2, 2, [], { + "__|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, + + "X_|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + "XO|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "X_|O_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "X_|_O": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "_X|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + "OX|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "_X|O_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "_X|_O": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "__|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + "O_|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "_O|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "__|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "__|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + "O_|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "_O|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + "__|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "XO|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "XO|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "XX|O_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "X_|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "XX|_O": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "X_|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "OX|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "OX|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_X|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_X|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "O_|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_O|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + + "XO|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "XO|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "XX|OO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "OX|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "OX|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "OO|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + }); + }); + + void t.test("two winning squares", async (t) => { + checkSolutionsComplete( + t, + 2, + 2, + [ + [0, 0], + [0, 1], + ], + { + "__|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, + + "X_|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "XO|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "X_|O_": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + "X_|_O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + + "_X|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "OX|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "_X|O_": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + "_X|_O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + + "__|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + "O_|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_O|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "__|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "__|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + "O_|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_O|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "__|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + "XO|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "XO|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "XX|O_": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + "X_|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "XX|_O": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + "X_|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "OX|X_": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "OX|_X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "_X|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "_X|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + "O_|XX": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + "_O|XX": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + + "XO|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "XO|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "OX|XO": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "OX|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + "OO|XX": { finalOutcome: FinalOutcome.WinO, movesLeft: 0 }, + }, + ); + }); }); }); diff --git a/src/lib/tictactoe.test.ts b/src/lib/tictactoe.test.ts new file mode 100644 index 0000000..6866347 --- /dev/null +++ b/src/lib/tictactoe.test.ts @@ -0,0 +1,144 @@ +import t from "tap"; + +import { ExpectedOutcome, FinalOutcome } from "./datatypes.ts"; +import { computeAllSolutions } from "./solver.ts"; +import { rules } from "./tictactoe-rules.ts"; + +void t.test("computeAllSolutions", async (t) => { + const checkSolutionsComplete = ( + rows: number, + columns: number, + expectedSolutions: Record, + ) => { + t.matchOnlyStrict(Object.fromEntries(computeAllSolutions(rows, columns, rules).entries()), expectedSolutions); + }; + + const checkSolutionsIncomplete = ( + rows: number, + columns: number, + expectedSolutionsCount: number, + expectedSolutionsIncomplete: Record, + ) => { + const allSolutions = computeAllSolutions(rows, columns, rules); + t.equal(allSolutions.size, expectedSolutionsCount); + t.matchStrict(Object.fromEntries(allSolutions.entries()), expectedSolutionsIncomplete); + }; + + // smallest possible board where X can win + checkSolutionsComplete(1, 5, { + _____: { finalOutcome: FinalOutcome.Draw, movesLeft: 5 }, + + X____: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, + XO___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + X_O__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + X__O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + X___O: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + _X___: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, + OX___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + _XO__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + _X_O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + _X__O: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, + __X__: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, + O_X__: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, + _OX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + __XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + __X_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, + ___X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, + O__X_: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, + _O_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + __OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + ___XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + ____X: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, + O___X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + _O__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + __O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + ___OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, + + XOX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + XO_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + XO__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + XXO__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + X_OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + X_O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + XX_O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + X_XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + X__OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + XX__O: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + X_X_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + X__XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + OXX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + OX_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + OX__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _XOX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _XO_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _XXO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _X_OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _XX_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 2 }, + _X_XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + O_XX_: { finalOutcome: FinalOutcome.WinX, movesLeft: 2 }, + O_X_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _OXX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _OX_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + __XOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + __XXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + O__XX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + _O_XX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + __OXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, + + XOXO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XOX_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XOOX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XO_XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XOO_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XO_OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XXOO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XXO_O: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + X_OXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + X_OOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + XX_OO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + X_XOO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + OXXO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + OXX_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + OXOX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + OX_XO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + OXO_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + OX_OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _XOXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _XOOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _XXOO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + OOXX_: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + O_XXO: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + OOX_X: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + O_XOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _OXXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _OXOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + OO_XX: { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + O_OXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + _OOXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, + + XOXOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + XOXXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + XOOXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + XXOOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + XXOXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + XXXOO: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + OXXOX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + OXXXO: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + OXOXX: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, + OOXXX: { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, + }); + + // number 5478 taken from https://math.stackexchange.com/a/613505 + checkSolutionsIncomplete(3, 3, 5478, { + "___|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 9 }, + "X__|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, + "_X_|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, + "___|_X_|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, + "XO_|___|___": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, + "X__|___|_O_": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, + "X_O|___|___": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, + "X__|___|__O": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, + "OO_|___|_XX": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, + "OO_|__X|_XX": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, + }); +});