implemented generic solver tests; extracted tictactoe-specific solver tests to integration tests file

feature/modern-browsers
Inga 🏳‍🌈 1 month ago
parent ca311400ea
commit 4ab4f19070
  1. 2
      eslint.config.js
  2. 393
      src/lib/solver.spec.ts
  3. 144
      src/lib/tictactoe.test.ts

@ -22,7 +22,7 @@ export default tsEslint.config(
}, },
}, },
{ {
files: ["**/*.spec.*"], files: ["**/*.spec.*", "**/*.test.*"],
rules: { rules: {
// To make it easier to pass incorrect data to functions under test // To make it easier to pass incorrect data to functions under test
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",

@ -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 { computeAllSolutions, getPreferredNextOutcome } from "./solver.ts";
import { rules } from "./tictactoe-rules.ts";
void t.test("getPreferredNextOutcome", async (t) => { void t.test("getPreferredNextOutcome", async (t) => {
const nextOutcomes: ExpectedOutcome[] = [ const nextOutcomes: ExpectedOutcome[] = [
@ -84,40 +83,120 @@ void t.test("getPreferredNextOutcome", async (t) => {
}); });
void t.test("computeAllSolutions", 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 = ( const checkSolutionsComplete = (
t: Test,
rows: number, rows: number,
columns: number, columns: number,
squares: [number, number][],
expectedSolutions: Record<string, ExpectedOutcome>, expectedSolutions: Record<string, ExpectedOutcome>,
) => { ) => {
t.matchOnlyStrict(Object.fromEntries(computeAllSolutions(rows, columns, rules).entries()), expectedSolutions); const rules = createRulesForSquares(...squares);
const solutions = computeAllSolutions(rows, columns, rules);
t.matchOnlyStrict(Object.fromEntries(solutions.entries()), expectedSolutions);
}; };
const checkSolutionsIncomplete = ( void t.test("empty boards", async (t) => {
rows: number, void t.test("0x0 board", async (t) => {
columns: number, checkSolutionsComplete(t, 0, 0, [], {
expectedSolutionsCount: number, "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
expectedSolutionsIncomplete: Record<string, ExpectedOutcome>, });
) => { });
const allSolutions = computeAllSolutions(rows, columns, rules);
t.equal(allSolutions.size, expectedSolutionsCount);
t.matchStrict(Object.fromEntries(allSolutions.entries()), expectedSolutionsIncomplete);
};
checkSolutionsComplete(0, 0, { void t.test("0x1 board", async (t) => {
checkSolutionsComplete(t, 0, 1, [], {
"": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
}); });
});
checkSolutionsComplete(1, 0, { void t.test("0x2 board", async (t) => {
checkSolutionsComplete(t, 0, 2, [], {
"": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, "": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
}); });
});
checkSolutionsComplete(1, 1, { 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 },
});
});
});
void t.test("1x1 board", async (t) => {
void t.test("no winning squares", async (t) => {
checkSolutionsComplete(t, 1, 1, [], {
_: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 }, _: { finalOutcome: FinalOutcome.Draw, movesLeft: 1 },
X: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, 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 // making sure that we don't have any bugs that would manifest when width > height
checkSolutionsComplete(1, 3, { void t.test("1x3 board", async (t) => {
void t.test("no winning squares", async (t) => {
checkSolutionsComplete(t, 1, 3, [], {
___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, ___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 },
X__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, X__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 },
@ -135,9 +214,61 @@ void t.test("computeAllSolutions", async (t) => {
XXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, XXO: { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
OXX: { 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 // making sure that we don't have any bugs that would manifest when height > width
checkSolutionsComplete(3, 1, { void t.test("3x1 board", async (t) => {
void t.test("no winning squares", async (t) => {
checkSolutionsComplete(t, 3, 1, [], {
"_|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "_|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 },
"X|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "X|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 },
@ -155,112 +286,60 @@ void t.test("computeAllSolutions", async (t) => {
"X|X|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, "X|X|O": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
"O|X|X": { 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 },
// smallest possible board where X can win "X|_|_": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 },
checkSolutionsComplete(1, 5, { "_|X|_": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 },
_____: { finalOutcome: FinalOutcome.Draw, movesLeft: 5 }, "_|_|X": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 },
X____: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, "X|O|_": { finalOutcome: FinalOutcome.WinO, movesLeft: 0 },
XO___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "X|_|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 },
X_O__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "O|_|X": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 },
X__O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "_|O|X": { finalOutcome: FinalOutcome.WinO, movesLeft: 0 },
X___O: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 },
_X___: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, "X|X|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 },
OX___: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "O|X|X": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 },
_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 }, void t.test("two winning squares", async (t) => {
O_X__: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, checkSolutionsComplete(
_OX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, t,
__XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, 3,
__X_O: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, 1,
___X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, [
O__X_: { finalOutcome: FinalOutcome.WinX, movesLeft: 3 }, [0, 0],
_O_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, [1, 0],
__OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, ],
___XO: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, {
____X: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, "_|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 },
O___X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 },
_O__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "X|_|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 },
__O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "_|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 },
___OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "_|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 2 },
XOX__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "X|O|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
XO_X_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "X|_|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 },
XO__X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "O|X|_": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
XXO__: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "_|X|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 },
X_OX_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "O|_|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 },
X_O_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "_|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 1 },
XX_O_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 },
X_XO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "X|O|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
X__OX: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "X|X|O": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 },
XX__O: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, "O|X|X": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
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 }, void t.test("2x2 board", async (t) => {
_XO_X: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, void t.test("no winning squares", async (t) => {
_XXO_: { finalOutcome: FinalOutcome.Draw, movesLeft: 2 }, checkSolutionsComplete(t, 2, 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 }, "__|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 4 },
"X_|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 }, "X_|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 3 },
@ -303,18 +382,60 @@ void t.test("computeAllSolutions", async (t) => {
"OX|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, "OX|OX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
"OO|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 }, "OO|XX": { finalOutcome: FinalOutcome.Draw, movesLeft: 0 },
}); });
});
// number 5478 taken from https://math.stackexchange.com/a/613505 void t.test("two winning squares", async (t) => {
checkSolutionsIncomplete(3, 3, 5478, { checkSolutionsComplete(
"___|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 9 }, t,
"X__|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, 2,
"_X_|___|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, 2,
"___|_X_|___": { finalOutcome: FinalOutcome.Draw, movesLeft: 8 }, [
"XO_|___|___": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, [0, 0],
"X__|___|_O_": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, [0, 1],
"X_O|___|___": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, ],
"X__|___|__O": { finalOutcome: FinalOutcome.WinX, movesLeft: 5 }, {
"OO_|___|_XX": { finalOutcome: FinalOutcome.WinX, movesLeft: 1 }, "__|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 4 },
"OO_|__X|_XX": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 },
"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 },
},
);
});
}); });
}); });

@ -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<string, ExpectedOutcome>,
) => {
t.matchOnlyStrict(Object.fromEntries(computeAllSolutions(rows, columns, rules).entries()), expectedSolutions);
};
const checkSolutionsIncomplete = (
rows: number,
columns: number,
expectedSolutionsCount: number,
expectedSolutionsIncomplete: Record<string, ExpectedOutcome>,
) => {
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 },
});
});
Loading…
Cancel
Save