diff --git a/src/backend/main/index.tsx b/src/backend/main/index.tsx index bc7bd3f..d759946 100644 --- a/src/backend/main/index.tsx +++ b/src/backend/main/index.tsx @@ -4,15 +4,15 @@ import { handleBoardgame } from "./boardgame-handler.ts"; import { getCounterHtml } from "../components/counter.tsx"; export const mainPageHandler: RequestHandler = (req, res) => { - const board1 = handleBoardgame(req, res, "tictactoe1", "tictactoe"); - if (!board1) { + const boardThree = handleBoardgame(req, res, "tictactoe-three-1", "tictactoe-three"); + if (!boardThree) { // No return value from handleBoardgame means that it redirected user to another URL, // and we no longer need to render anything. return; } - const board2 = handleBoardgame(req, res, "tictactoe2", "tictactoe"); - if (!board2) { + const boardAll = handleBoardgame(req, res, "tictactoe-all-2", "tictactoe-all"); + if (!boardAll) { return; } @@ -42,8 +42,20 @@ export const mainPageHandler: RequestHandler = (req, res) => {
  • {getCounterHtml(req, "b")}
  • -
    {board1}
    -
    {board2}
    +
    +

    + "Three in a row" generalized variant of tictactoe (it is enough to get three adjacent cells on the same row, + on the same column, or on the same 45°/135° line) +

    + {boardThree} +
    +
    +

    + "All in a row" generalized variant of tictactoe (you need to get complete row, complete column, or, on + square boards, complete diagonal) +

    + {boardAll} +
    , ); diff --git a/src/shared/game-variants/index.ts b/src/shared/game-variants/index.ts index 4bfac80..bb7e5c6 100644 --- a/src/shared/game-variants/index.ts +++ b/src/shared/game-variants/index.ts @@ -1,7 +1,9 @@ -import { rules as tictactoeRules } from "./tictactoe-rules.ts"; +import { tictactoeAllRules } from "./tictactoe/tictactoe-all-rules.ts"; +import { tictactoeThreeRules } from "./tictactoe/tictactoe-three-rules.ts"; export const gamesRules = { - tictactoe: tictactoeRules, + "tictactoe-three": tictactoeThreeRules, + "tictactoe-all": tictactoeAllRules, }; export type GameVariantName = keyof typeof gamesRules; diff --git a/src/shared/game-variants/tictactoe-rules.spec.ts b/src/shared/game-variants/tictactoe-rules.spec.ts deleted file mode 100644 index 0caded1..0000000 --- a/src/shared/game-variants/tictactoe-rules.spec.ts +++ /dev/null @@ -1,194 +0,0 @@ -import t from "tap"; - -import { Board } from "../datatypes/board.ts"; -import { CurrentOutcome, SquareState } from "../datatypes/types.ts"; -import { getBoardOutcome, getSequenceOutcome } from "./tictactoe-rules.ts"; - -void t.test("getSequenceOutcome", async (t) => { - void t.test("empty sequence", async (t) => { - t.equal(getSequenceOutcome([]), null); - }); - - void t.test("all sequences of length 1", async (t) => { - t.equal(getSequenceOutcome([SquareState.Unoccupied]), null); - t.equal(getSequenceOutcome([SquareState.X]), CurrentOutcome.WinX); - t.equal(getSequenceOutcome([SquareState.O]), CurrentOutcome.WinO); - }); - - void t.test("all sequences of length 2", async (t) => { - t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.Unoccupied]), null); - t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.X]), null); - t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.O]), null); - t.equal(getSequenceOutcome([SquareState.X, SquareState.Unoccupied]), null); - t.equal(getSequenceOutcome([SquareState.X, SquareState.X]), CurrentOutcome.WinX); - t.equal(getSequenceOutcome([SquareState.X, SquareState.O]), null); - t.equal(getSequenceOutcome([SquareState.O, SquareState.Unoccupied]), null); - t.equal(getSequenceOutcome([SquareState.O, SquareState.X]), null); - t.equal(getSequenceOutcome([SquareState.O, SquareState.O]), CurrentOutcome.WinO); - }); - - void t.test("sequences of length 7", async (t) => { - void t.test("all X except for the first element", async (t) => { - t.equal( - getSequenceOutcome([ - SquareState.Unoccupied, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - ]), - null, - ); - t.equal( - getSequenceOutcome([ - SquareState.O, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - ]), - null, - ); - }); - - void t.test("all X except for the last element", async (t) => { - t.equal( - getSequenceOutcome([ - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.Unoccupied, - ]), - null, - ); - t.equal( - getSequenceOutcome([ - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.O, - ]), - null, - ); - }); - - void t.test("all X except for the middle element", async (t) => { - t.equal( - getSequenceOutcome([ - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.Unoccupied, - SquareState.X, - SquareState.X, - SquareState.X, - ]), - null, - ); - t.equal( - getSequenceOutcome([ - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.O, - SquareState.X, - SquareState.X, - SquareState.X, - ]), - null, - ); - }); - - t.equal( - getSequenceOutcome([ - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - SquareState.X, - ]), - CurrentOutcome.WinX, - ); - }); -}); - -void t.test("getBoardOutcome", async (t) => { - void t.test("1x1 boards", async (t) => { - t.equal(getBoardOutcome(Board.fromSerialized("_")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("X")), CurrentOutcome.Draw); - t.equal(getBoardOutcome(Board.fromSerialized("O")), CurrentOutcome.Draw); - }); - - void t.test("2x2 boards", async (t) => { - t.equal(getBoardOutcome(Board.fromSerialized("__|__")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("XX|X_")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("XX|XX")), CurrentOutcome.Draw); - t.equal(getBoardOutcome(Board.fromSerialized("OO|OO")), CurrentOutcome.Draw); - t.equal(getBoardOutcome(Board.fromSerialized("XO|OX")), CurrentOutcome.Draw); - }); - - void t.test("3x3 boards", async (t) => { - t.equal(getBoardOutcome(Board.fromSerialized("___|___|___")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|___")), CurrentOutcome.Undecided); - - t.equal(getBoardOutcome(Board.fromSerialized("XX_|___|___")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("___|___|_XX")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("X__|X__|___")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("___|__X|__X")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("X__|_X_|___")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|__X")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("__X|_X_|___")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|X__")), CurrentOutcome.Undecided); - - t.equal(getBoardOutcome(Board.fromSerialized("XXX|O_O|O_O")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("O_O|O_O|XXX")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("XOO|X__|XOO")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("OOX|__X|OOX")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("XOO|OXO|OOX")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("OOX|OXO|XOO")), CurrentOutcome.WinX); - - t.equal(getBoardOutcome(Board.fromSerialized("XXO|XOX|OXX")), CurrentOutcome.WinO); - - t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OXO")), CurrentOutcome.Draw); - t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OX_")), CurrentOutcome.Undecided); - }); - - void t.test("5x5 boards", async (t) => { - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____")), CurrentOutcome.Undecided); - t.equal(getBoardOutcome(Board.fromSerialized("XXX__|_____|_____|_____|_____")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("X____|X____|X____|_____|_____")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("X____|_X___|__X__|_____|_____")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("__X__|_X___|X____|_____|_____")), CurrentOutcome.WinX); - - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|__XXX")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|____X|____X|____X")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|__X__|___X_|____X")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|____X|___X_|__X__")), CurrentOutcome.WinX); - }); - - void t.test("6x5 boards", async (t) => { - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____|__XXX")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|____X|____X|____X")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|__X__|___X_|____X")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|____X|___X_|__X__")), CurrentOutcome.WinX); - }); - - void t.test("5x6 boards", async (t) => { - t.equal(getBoardOutcome(Board.fromSerialized("______|______|______|______|___XXX")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("______|______|_____X|_____X|_____X")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("______|______|___X__|____X_|_____X")), CurrentOutcome.WinX); - t.equal(getBoardOutcome(Board.fromSerialized("______|______|_____X|____X_|___X__")), CurrentOutcome.WinX); - }); -}); diff --git a/src/shared/game-variants/tictactoe/tictactoe-all-rules.spec.ts b/src/shared/game-variants/tictactoe/tictactoe-all-rules.spec.ts new file mode 100644 index 0000000..36cd70e --- /dev/null +++ b/src/shared/game-variants/tictactoe/tictactoe-all-rules.spec.ts @@ -0,0 +1,253 @@ +import t from "tap"; + +import { Board } from "../../datatypes/board.ts"; +import { CurrentOutcome } from "../../datatypes/types.ts"; +import { getBoardOutcome } from "./tictactoe-all-rules.ts"; + +void t.test("getBoardOutcome", async (t) => { + void t.test("1x1 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("O")), CurrentOutcome.WinO); + }); + + void t.test("2x2 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("OO|OO")), CurrentOutcome.WinO); + + // Could be WinO as well, but we consider right-down diagonal before right-up one + t.equal(getBoardOutcome(Board.fromSerialized("XO|OX")), CurrentOutcome.WinX); + }); + + void t.test("Enumerate all 2x2 boards without O", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX")), CurrentOutcome.WinX); + }); + + void t.test("Enumerate all 2x3 boards without O", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("___|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__X|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|__X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__X|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|_XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__X|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|X_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__X|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|_X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|_XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|XX_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|__X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|_X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|_XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|X_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|XX_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X__|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|X__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X__|X_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X__|XX_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X__|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|__X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|_XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|X__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|X_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|XX_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|_X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|_XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|X__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|X_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|XX_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|___")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|__X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|_X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|_XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|X__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|X_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|XX_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|XXX")), CurrentOutcome.WinX); + }); + + void t.test("Enumerate all 3x2 boards without O", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("__|__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|__|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|XX")), CurrentOutcome.WinX); + }); + + void t.test("3x3 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("___|___|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|___")), CurrentOutcome.Undecided); + + t.equal(getBoardOutcome(Board.fromSerialized("XX_|___|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|___|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|X__|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|__X|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|_X_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|_X_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|X__")), CurrentOutcome.Undecided); + + t.equal(getBoardOutcome(Board.fromSerialized("XXX|O_O|O_O")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("O_O|O_O|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XOO|X__|XOO")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("OOX|__X|OOX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XOO|OXO|OOX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("OOX|OXO|XOO")), CurrentOutcome.WinX); + + t.equal(getBoardOutcome(Board.fromSerialized("XXO|XOX|OXX")), CurrentOutcome.WinO); + + t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OXO")), CurrentOutcome.Draw); + t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OX_")), CurrentOutcome.Undecided); + }); + + void t.test("5x5 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XXXXX|_____|_____|_____|_____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|XXXXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X____|X____|X____|X____|X____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("____X|____X|____X|____X|____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X____|_X___|__X__|___X_|____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("____X|___X_|__X__|_X___|X____")), CurrentOutcome.WinX); + + t.equal(getBoardOutcome(Board.fromSerialized("XXXX_|_____|_____|_____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_XXXX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X____|X____|X____|X____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____|____X|____X|____X|____X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X____|_X___|__X__|___X_|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____|___X_|__X__|_X___|X____")), CurrentOutcome.Undecided); + }); + + void t.test("6x5 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XXXXX|_____|_____|_____|_____|_____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____|XXXXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X____|X____|X____|X____|X____|X____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("____X|____X|____X|____X|____X|____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X____|_X___|__X__|___X_|____X|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("____X|___X_|__X__|_X___|X____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____|X____|_X___|__X__|___X_|____X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____|____X|___X_|__X__|_X___|X____")), CurrentOutcome.Undecided); + + t.equal(getBoardOutcome(Board.fromSerialized("XXXX_|_____|_____|_____|_____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____|_XXXX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X____|X____|X____|X____|X____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____|____X|____X|____X|____X|____X")), CurrentOutcome.Undecided); + }); + + void t.test("5x6 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("______|______|______|______|______")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XXXXXX|______|______|______|______")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("______|______|______|______|XXXXXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_____|X_____|X_____|X_____|X_____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____X|_____X|_____X|_____X|_____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_____|_X____|__X___|___X__|____X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X____|__X___|___X__|____X_|_____X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("____X_|___X__|__X___|_X____|X_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_____X|____X_|___X__|__X___|_X____")), CurrentOutcome.Undecided); + + t.equal(getBoardOutcome(Board.fromSerialized("XXXXX_|______|______|______|______")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("______|______|______|______|_XXXXX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_____|X_____|X_____|X_____|______")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("______|_____X|_____X|_____X|_____X")), CurrentOutcome.Undecided); + }); +}); diff --git a/src/shared/game-variants/tictactoe/tictactoe-all-rules.ts b/src/shared/game-variants/tictactoe/tictactoe-all-rules.ts new file mode 100644 index 0000000..979271a --- /dev/null +++ b/src/shared/game-variants/tictactoe/tictactoe-all-rules.ts @@ -0,0 +1,66 @@ +import { CurrentOutcome, GameRules, SquareState } from "../../datatypes/types.ts"; +import { getSequenceOutcome } from "./tictactoe-generic.ts"; + +export const getBoardOutcome: GameRules["getBoardOutcome"] = (board) => { + for (let row = 0; board.hasRow(row); row++) { + const rowValues = []; + for (let column = 0; board.hasSquare(row, column); column++) { + rowValues.push(board.get(row, column)); + } + + const outcome = getSequenceOutcome(rowValues); + if (outcome) { + return outcome; + } + } + + for (let column = 0; board.hasSquare(0, column); column++) { + const columnValues = []; + for (let row = 0; board.hasSquare(row, column); row++) { + columnValues.push(board.get(row, column)); + } + + const outcome = getSequenceOutcome(columnValues); + if (outcome) { + return outcome; + } + } + + { + const rightDownValues = []; + for (let index = 0; board.hasSquare(index, index); index++) { + rightDownValues.push(board.get(index, index)); + } + + if (!board.hasRow(rightDownValues.length) && !board.hasSquare(rightDownValues.length - 1, rightDownValues.length)) { + // It only makes sense to check diagonals on square boards + const rightDownOutcome = getSequenceOutcome(rightDownValues); + if (rightDownOutcome) { + return rightDownOutcome; + } + + const rightUpValues = []; + for (let index = 0; board.hasSquare(rightDownValues.length - 1 - index, index); index++) { + rightUpValues.push(board.get(rightDownValues.length - 1 - index, index)); + } + const rightUpOutcome = getSequenceOutcome(rightUpValues); + if (rightUpOutcome) { + return rightUpOutcome; + } + } + } + + for (let row = 0; board.hasRow(row); row++) { + for (let column = 0; board.hasSquare(row, column); column++) { + if (board.get(row, column) === SquareState.Unoccupied) { + return CurrentOutcome.Undecided; + } + } + } + + return CurrentOutcome.Draw; +}; + +export const tictactoeAllRules: GameRules = { + getBoardOutcome, +}; diff --git a/src/shared/game-variants/tictactoe/tictactoe-generic.spec.ts b/src/shared/game-variants/tictactoe/tictactoe-generic.spec.ts new file mode 100644 index 0000000..860aeaa --- /dev/null +++ b/src/shared/game-variants/tictactoe/tictactoe-generic.spec.ts @@ -0,0 +1,124 @@ +import t from "tap"; + +import { CurrentOutcome, SquareState } from "../../datatypes/types.ts"; +import { getSequenceOutcome } from "./tictactoe-generic.ts"; + +void t.test("getSequenceOutcome", async (t) => { + void t.test("empty sequence", async (t) => { + t.equal(getSequenceOutcome([]), null); + }); + + void t.test("all sequences of length 1", async (t) => { + t.equal(getSequenceOutcome([SquareState.Unoccupied]), null); + t.equal(getSequenceOutcome([SquareState.X]), CurrentOutcome.WinX); + t.equal(getSequenceOutcome([SquareState.O]), CurrentOutcome.WinO); + }); + + void t.test("all sequences of length 2", async (t) => { + t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.Unoccupied]), null); + t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.X]), null); + t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.O]), null); + t.equal(getSequenceOutcome([SquareState.X, SquareState.Unoccupied]), null); + t.equal(getSequenceOutcome([SquareState.X, SquareState.X]), CurrentOutcome.WinX); + t.equal(getSequenceOutcome([SquareState.X, SquareState.O]), null); + t.equal(getSequenceOutcome([SquareState.O, SquareState.Unoccupied]), null); + t.equal(getSequenceOutcome([SquareState.O, SquareState.X]), null); + t.equal(getSequenceOutcome([SquareState.O, SquareState.O]), CurrentOutcome.WinO); + }); + + void t.test("sequences of length 7", async (t) => { + void t.test("all X except for the first element", async (t) => { + t.equal( + getSequenceOutcome([ + SquareState.Unoccupied, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + ]), + null, + ); + t.equal( + getSequenceOutcome([ + SquareState.O, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + ]), + null, + ); + }); + + void t.test("all X except for the last element", async (t) => { + t.equal( + getSequenceOutcome([ + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.Unoccupied, + ]), + null, + ); + t.equal( + getSequenceOutcome([ + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.O, + ]), + null, + ); + }); + + void t.test("all X except for the middle element", async (t) => { + t.equal( + getSequenceOutcome([ + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.Unoccupied, + SquareState.X, + SquareState.X, + SquareState.X, + ]), + null, + ); + t.equal( + getSequenceOutcome([ + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.O, + SquareState.X, + SquareState.X, + SquareState.X, + ]), + null, + ); + }); + + t.equal( + getSequenceOutcome([ + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + SquareState.X, + ]), + CurrentOutcome.WinX, + ); + }); +}); diff --git a/src/shared/game-variants/tictactoe/tictactoe-generic.ts b/src/shared/game-variants/tictactoe/tictactoe-generic.ts new file mode 100644 index 0000000..c275a32 --- /dev/null +++ b/src/shared/game-variants/tictactoe/tictactoe-generic.ts @@ -0,0 +1,18 @@ +import { CurrentOutcome, SquareState } from "../../datatypes/types.ts"; + +export const getSequenceOutcome = (sequence: SquareState[]) => { + for (let i = 1; i < sequence.length; i++) { + if (sequence[i - 1] != sequence[i]) { + return null; + } + } + + switch (sequence[0]) { + case SquareState.X: + return CurrentOutcome.WinX; + case SquareState.O: + return CurrentOutcome.WinO; + default: + return null; + } +}; diff --git a/src/shared/game-variants/tictactoe/tictactoe-three-rules.spec.ts b/src/shared/game-variants/tictactoe/tictactoe-three-rules.spec.ts new file mode 100644 index 0000000..1cec347 --- /dev/null +++ b/src/shared/game-variants/tictactoe/tictactoe-three-rules.spec.ts @@ -0,0 +1,227 @@ +import t from "tap"; + +import { Board } from "../../datatypes/board.ts"; +import { CurrentOutcome } from "../../datatypes/types.ts"; +import { getBoardOutcome } from "./tictactoe-three-rules.ts"; + +void t.test("getBoardOutcome", async (t) => { + void t.test("1x1 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X")), CurrentOutcome.Draw); + t.equal(getBoardOutcome(Board.fromSerialized("O")), CurrentOutcome.Draw); + }); + + void t.test("2x2 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX")), CurrentOutcome.Draw); + t.equal(getBoardOutcome(Board.fromSerialized("OO|OO")), CurrentOutcome.Draw); + t.equal(getBoardOutcome(Board.fromSerialized("XO|OX")), CurrentOutcome.Draw); + }); + + void t.test("Enumerate all 2x2 boards without O", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX")), CurrentOutcome.Draw); + }); + + void t.test("Enumerate all 2x3 boards without O", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("___|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__X|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X_|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_XX|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X__|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_X|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|_X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|X__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|X_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|XX_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX_|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|___")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|__X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|_X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|_XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|X__")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|X_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|XX_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XXX|XXX")), CurrentOutcome.WinX); + }); + + void t.test("Enumerate all 3x2 boards without O", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("__|__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|__|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|_X|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|X_|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__|XX|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|__|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|_X|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|X_|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("_X|XX|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|__|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|_X|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|X_|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X_|XX|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|__|XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|X_")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|_X|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|_X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|X_|XX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|__")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|_X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|X_")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XX|XX|XX")), CurrentOutcome.WinX); + }); + + void t.test("3x3 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("___|___|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|___")), CurrentOutcome.Undecided); + + t.equal(getBoardOutcome(Board.fromSerialized("XX_|___|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|___|_XX")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|X__|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|__X|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("X__|_X_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|__X")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("__X|_X_|___")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|X__")), CurrentOutcome.Undecided); + + t.equal(getBoardOutcome(Board.fromSerialized("XXX|O_O|O_O")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("O_O|O_O|XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XOO|X__|XOO")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("OOX|__X|OOX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("XOO|OXO|OOX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("OOX|OXO|XOO")), CurrentOutcome.WinX); + + t.equal(getBoardOutcome(Board.fromSerialized("XXO|XOX|OXX")), CurrentOutcome.WinO); + + t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OXO")), CurrentOutcome.Draw); + t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OX_")), CurrentOutcome.Undecided); + }); + + void t.test("5x5 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____")), CurrentOutcome.Undecided); + t.equal(getBoardOutcome(Board.fromSerialized("XXX__|_____|_____|_____|_____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X____|X____|X____|_____|_____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("X____|_X___|__X__|_____|_____")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("__X__|_X___|X____|_____|_____")), CurrentOutcome.WinX); + + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|__XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|____X|____X|____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|__X__|___X_|____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|____X|___X_|__X__")), CurrentOutcome.WinX); + }); + + void t.test("6x5 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____|__XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|____X|____X|____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|__X__|___X_|____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|____X|___X_|__X__")), CurrentOutcome.WinX); + }); + + void t.test("5x6 boards", async (t) => { + t.equal(getBoardOutcome(Board.fromSerialized("______|______|______|______|___XXX")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("______|______|_____X|_____X|_____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("______|______|___X__|____X_|_____X")), CurrentOutcome.WinX); + t.equal(getBoardOutcome(Board.fromSerialized("______|______|_____X|____X_|___X__")), CurrentOutcome.WinX); + }); +}); diff --git a/src/shared/game-variants/tictactoe-rules.ts b/src/shared/game-variants/tictactoe/tictactoe-three-rules.ts similarity index 79% rename from src/shared/game-variants/tictactoe-rules.ts rename to src/shared/game-variants/tictactoe/tictactoe-three-rules.ts index ed3f108..a2f816b 100644 --- a/src/shared/game-variants/tictactoe-rules.ts +++ b/src/shared/game-variants/tictactoe/tictactoe-three-rules.ts @@ -1,21 +1,5 @@ -import { CurrentOutcome, GameRules, SquareState } from "../datatypes/types.ts"; - -export const getSequenceOutcome = (sequence: SquareState[]) => { - for (let i = 1; i < sequence.length; i++) { - if (sequence[i - 1] != sequence[i]) { - return null; - } - } - - switch (sequence[0]) { - case SquareState.X: - return CurrentOutcome.WinX; - case SquareState.O: - return CurrentOutcome.WinO; - default: - return null; - } -}; +import { CurrentOutcome, GameRules, SquareState } from "../../datatypes/types.ts"; +import { getSequenceOutcome } from "./tictactoe-generic.ts"; export const getBoardOutcome: GameRules["getBoardOutcome"] = (board) => { for (let row = 0; board.hasRow(row); row++) { @@ -80,6 +64,6 @@ export const getBoardOutcome: GameRules["getBoardOutcome"] = (board) => { return CurrentOutcome.Draw; }; -export const rules: GameRules = { +export const tictactoeThreeRules: GameRules = { getBoardOutcome, }; diff --git a/src/shared/integration-tests/test-helpers.ts b/src/shared/integration-tests/test-helpers.ts new file mode 100644 index 0000000..1c123ce --- /dev/null +++ b/src/shared/integration-tests/test-helpers.ts @@ -0,0 +1,40 @@ +import { Test } from "tap"; +import { ExpectedOutcome, GameRules, Opponent, Player, getOccupiedStateByPlayer } from "../datatypes/types.ts"; +import { computeAllSolutions } from "../gameplay/solver.ts"; +import { Board } from "../datatypes/board.ts"; + +export const checkSolutionsComplete = ( + t: Test, + rules: GameRules, + rows: number, + columns: number, + expectedSolutions: Record, +) => { + t.matchOnlyStrict(Object.fromEntries(computeAllSolutions(rows, columns, rules).entries()), expectedSolutions); +}; + +export const checkSolutionsIncomplete = ( + t: Test, + rules: GameRules, + 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); +}; + +export const checkNextMove = ( + t: Test, + opponent: Opponent, + currentBoardSerialized: string, + currentPlayer: Player, + expectedNextBoardSerialized: string, +) => { + 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); +}; diff --git a/src/shared/integration-tests/tictactoe-generic.test.ts b/src/shared/integration-tests/tictactoe-generic.test.ts new file mode 100644 index 0000000..da304b2 --- /dev/null +++ b/src/shared/integration-tests/tictactoe-generic.test.ts @@ -0,0 +1,59 @@ +import t from "tap"; + +import { FinalOutcome, Player } from "../datatypes/types.ts"; +import { createOpponent } from "../gameplay/opponent.ts"; +import { computeAllSolutions } from "../gameplay/solver.ts"; +import { checkNextMove, checkSolutionsIncomplete } from "./test-helpers.ts"; +import { gamesRules } from "../game-variants/index.ts"; + +for (const [gameVariant, rules] of Object.entries(gamesRules)) { + void t.test(`Checking 3x3 game for ${gameVariant}`, async (t) => { + void t.test("computeAllSolutions", async (t) => { + // number 5478 taken from https://math.stackexchange.com/a/613505 + checkSolutionsIncomplete(t, rules, 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("createOpponent", async (t) => { + const outcomes = computeAllSolutions(3, 3, rules); + const opponent = createOpponent(outcomes); + + checkNextMove(t, opponent, "___|___|___", Player.X, "X__|___|___"); + checkNextMove(t, opponent, "X__|___|___", Player.O, "X__|_O_|___"); + checkNextMove(t, opponent, "X__|_O_|___", Player.X, "XX_|_O_|___"); + checkNextMove(t, opponent, "XX_|_O_|___", Player.O, "XXO|_O_|___"); + checkNextMove(t, opponent, "XXO|_O_|___", Player.X, "XXO|_O_|X__"); + checkNextMove(t, opponent, "XXO|_O_|X__", Player.O, "XXO|OO_|X__"); + checkNextMove(t, opponent, "XXO|OO_|X__", Player.X, "XXO|OOX|X__"); + checkNextMove(t, opponent, "XXO|OOX|X__", Player.O, "XXO|OOX|XO_"); + checkNextMove(t, opponent, "XXO|OOX|XO_", Player.X, "XXO|OOX|XOX"); + + checkNextMove(t, opponent, "___|___|__X", Player.O, "___|_O_|__X"); + checkNextMove(t, opponent, "___|_O_|__X", Player.X, "X__|_O_|__X"); + checkNextMove(t, opponent, "X__|_O_|__X", Player.O, "XO_|_O_|__X"); + checkNextMove(t, opponent, "XO_|_O_|__X", Player.X, "XO_|_O_|_XX"); + checkNextMove(t, opponent, "XO_|_O_|_XX", Player.O, "XO_|_O_|OXX"); + checkNextMove(t, opponent, "XO_|_O_|OXX", Player.X, "XOX|_O_|OXX"); + checkNextMove(t, opponent, "XOX|_O_|OXX", Player.O, "XOX|_OO|OXX"); + checkNextMove(t, opponent, "XOX|_OO|OXX", Player.X, "XOX|XOO|OXX"); + + checkNextMove(t, opponent, "XO_|___|___", Player.X, "XO_|X__|___"); + checkNextMove(t, opponent, "XO_|X__|___", Player.O, "XO_|X__|O__"); + checkNextMove(t, opponent, "XO_|X__|O__", Player.X, "XO_|XX_|O__"); + checkNextMove(t, opponent, "XO_|XX_|O__", Player.O, "XOO|XX_|O__"); + checkNextMove(t, opponent, "XOO|XX_|O__", Player.X, "XOO|XXX|O__"); + + checkNextMove(t, opponent, "XO_|XXO|O__", Player.X, "XO_|XXO|O_X"); + }); + }); + }); +} diff --git a/src/shared/game-variants/tictactoe.test.ts b/src/shared/integration-tests/tictactoe-three.test.ts similarity index 63% rename from src/shared/game-variants/tictactoe.test.ts rename to src/shared/integration-tests/tictactoe-three.test.ts index dc23aa1..54a3d66 100644 --- a/src/shared/game-variants/tictactoe.test.ts +++ b/src/shared/integration-tests/tictactoe-three.test.ts @@ -1,33 +1,16 @@ -import t, { Test } from "tap"; +import t from "tap"; -import { Board } from "../datatypes/board.ts"; -import { ExpectedOutcome, FinalOutcome, Opponent, Player, getOccupiedStateByPlayer } from "../datatypes/types.ts"; +import { FinalOutcome, Player } from "../datatypes/types.ts"; import { createOpponent } from "../gameplay/opponent.ts"; import { computeAllSolutions } from "../gameplay/solver.ts"; -import { rules } from "./tictactoe-rules.ts"; +import { tictactoeThreeRules } from "../game-variants/tictactoe/tictactoe-three-rules.ts"; +import { checkNextMove, checkSolutionsComplete } from "./test-helpers.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); - }; +const rules = tictactoeThreeRules; +void t.test("computeAllSolutions", async (t) => { // smallest possible board where X can win - checkSolutionsComplete(1, 5, { + checkSolutionsComplete(t, rules, 1, 5, { _____: { finalOutcome: FinalOutcome.Draw, movesLeft: 5 }, X____: { finalOutcome: FinalOutcome.Draw, movesLeft: 4 }, @@ -129,36 +112,9 @@ void t.test("computeAllSolutions", async (t) => { 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 }, - }); }); void t.test("createOpponent", async (t) => { - const checkNextMove = ( - t: Test, - opponent: Opponent, - currentBoardSerialized: string, - currentPlayer: Player, - expectedNextBoardSerialized: string, - ) => { - 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) => { const outcomes = computeAllSolutions(1, 5, rules); const opponent = createOpponent(outcomes); @@ -178,36 +134,4 @@ void t.test("createOpponent", async (t) => { checkNextMove(t, opponent, "O_XX_", Player.O, "OOXX_"); checkNextMove(t, opponent, "OOXX_", Player.X, "OOXXX"); }); - - void t.test("3x3 board", async (t) => { - const outcomes = computeAllSolutions(3, 3, rules); - const opponent = createOpponent(outcomes); - - checkNextMove(t, opponent, "___|___|___", Player.X, "X__|___|___"); - checkNextMove(t, opponent, "X__|___|___", Player.O, "X__|_O_|___"); - checkNextMove(t, opponent, "X__|_O_|___", Player.X, "XX_|_O_|___"); - checkNextMove(t, opponent, "XX_|_O_|___", Player.O, "XXO|_O_|___"); - checkNextMove(t, opponent, "XXO|_O_|___", Player.X, "XXO|_O_|X__"); - checkNextMove(t, opponent, "XXO|_O_|X__", Player.O, "XXO|OO_|X__"); - checkNextMove(t, opponent, "XXO|OO_|X__", Player.X, "XXO|OOX|X__"); - checkNextMove(t, opponent, "XXO|OOX|X__", Player.O, "XXO|OOX|XO_"); - checkNextMove(t, opponent, "XXO|OOX|XO_", Player.X, "XXO|OOX|XOX"); - - checkNextMove(t, opponent, "___|___|__X", Player.O, "___|_O_|__X"); - checkNextMove(t, opponent, "___|_O_|__X", Player.X, "X__|_O_|__X"); - checkNextMove(t, opponent, "X__|_O_|__X", Player.O, "XO_|_O_|__X"); - checkNextMove(t, opponent, "XO_|_O_|__X", Player.X, "XO_|_O_|_XX"); - checkNextMove(t, opponent, "XO_|_O_|_XX", Player.O, "XO_|_O_|OXX"); - checkNextMove(t, opponent, "XO_|_O_|OXX", Player.X, "XOX|_O_|OXX"); - checkNextMove(t, opponent, "XOX|_O_|OXX", Player.O, "XOX|_OO|OXX"); - checkNextMove(t, opponent, "XOX|_OO|OXX", Player.X, "XOX|XOO|OXX"); - - checkNextMove(t, opponent, "XO_|___|___", Player.X, "XO_|X__|___"); - checkNextMove(t, opponent, "XO_|X__|___", Player.O, "XO_|X__|O__"); - checkNextMove(t, opponent, "XO_|X__|O__", Player.X, "XO_|XX_|O__"); - checkNextMove(t, opponent, "XO_|XX_|O__", Player.O, "XOO|XX_|O__"); - checkNextMove(t, opponent, "XOO|XX_|O__", Player.X, "XOO|XXX|O__"); - - checkNextMove(t, opponent, "XO_|XXO|O__", Player.X, "XO_|XXO|O_X"); - }); }); diff --git a/tsconfig.build.json b/tsconfig.build.json index 5e4070b..337e5a4 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - "exclude": ["src/**/*.spec.*"], + "exclude": ["src/**/*.spec.*", "src/**/*.test.*", "src/**/test-helpers.*"], "compilerOptions": { "module": "Preserve", "noCheck": true,