From 2d03fc4006886ebc7529743f0a5b4e11e94d9918 Mon Sep 17 00:00:00 2001 From: Inga Date: Sun, 17 Nov 2024 00:37:13 +0000 Subject: [PATCH] uncoupled tictactoe rules and solver logic --- src/lib/datatypes.ts | 12 ++ src/lib/solver.spec.ts | 365 +------------------------------- src/lib/solver.ts | 97 +-------- src/lib/tictactoe-rules.spec.ts | 345 ++++++++++++++++++++++++++++++ src/lib/tictactoe-rules.ts | 89 ++++++++ 5 files changed, 462 insertions(+), 446 deletions(-) create mode 100644 src/lib/tictactoe-rules.spec.ts create mode 100644 src/lib/tictactoe-rules.ts diff --git a/src/lib/datatypes.ts b/src/lib/datatypes.ts index 765f08d..04c64d2 100644 --- a/src/lib/datatypes.ts +++ b/src/lib/datatypes.ts @@ -4,6 +4,14 @@ export enum SquareState { O, } +export type BoardType = { + hasRow(row: number): boolean; + hasSquare(row: number, column: number): boolean; + get(row: number, column: number): SquareState; + with(row: number, column: number, newSquareState: SquareState): BoardType; + serialize(): string; +}; + export enum Player { X = 101, // so that values of different enums never overlap O, @@ -27,6 +35,10 @@ export type ExpectedOutcome = { movesLeft: number; }; +export type GameRules = { + getBoardOutcome(board: BoardType): CurrentOutcome; +}; + export const getExpectedOutcomeByCurrentOutcome = ( currentOutcome: Exclude, ): ExpectedOutcome => { diff --git a/src/lib/solver.spec.ts b/src/lib/solver.spec.ts index b4c679b..1dad996 100644 --- a/src/lib/solver.spec.ts +++ b/src/lib/solver.spec.ts @@ -1,359 +1,8 @@ import t from "tap"; -import { - computeAllSolutions, - getBoardOutcome, - getPreferredNextOutcome, - getSequenceOutcome, -} from "./solver.ts"; -import { - CurrentOutcome, - ExpectedOutcome, - FinalOutcome, - Player, - SquareState, -} from "./datatypes.ts"; -import { Board } from "./board.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, - ); - }); -}); +import { ExpectedOutcome, FinalOutcome, Player } from "./datatypes.ts"; +import { computeAllSolutions, getPreferredNextOutcome } from "./solver.ts"; +import { getBoardOutcome } from "./tictactoe-rules.ts"; void t.test("getPreferredNextOutcome", async (t) => { const nextOutcomes: ExpectedOutcome[] = [ @@ -453,7 +102,9 @@ void t.test("computeAllSolutions", async (t) => { expectedSolutions: Record, ) => { t.matchOnlyStrict( - Object.fromEntries(computeAllSolutions(rows, columns).entries()), + Object.fromEntries( + computeAllSolutions(rows, columns, { getBoardOutcome }).entries(), + ), expectedSolutions, ); }; @@ -464,7 +115,9 @@ void t.test("computeAllSolutions", async (t) => { expectedSolutionsCount: number, expectedSolutionsIncomplete: Record, ) => { - const allSolutions = computeAllSolutions(rows, columns); + const allSolutions = computeAllSolutions(rows, columns, { + getBoardOutcome, + }); t.equal(allSolutions.size, expectedSolutionsCount); t.matchStrict( Object.fromEntries(allSolutions.entries()), diff --git a/src/lib/solver.ts b/src/lib/solver.ts index 5d1f90b..409071a 100644 --- a/src/lib/solver.ts +++ b/src/lib/solver.ts @@ -3,6 +3,7 @@ import { CurrentOutcome, ExpectedOutcome, FinalOutcome, + GameRules, Player, SquareState, getDesiredFinalOutcomeByPlayer, @@ -12,94 +13,6 @@ import { getUndesiredFinalOutcomeByPlayer, } from "./datatypes.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; - } -}; - -export const getBoardOutcome = (board: Board) => { - for (let row = 0; board.hasRow(row); row++) { - for (let column = 2; board.hasSquare(row, column); column++) { - const tripleLeft = [ - board.get(row, column - 2), - board.get(row, column - 1), - board.get(row, column - 0), - ]; - - const outcome = getSequenceOutcome(tripleLeft); - if (outcome) { - return outcome; - } - } - } - - for (let row = 2; board.hasRow(row); row++) { - for (let column = 0; board.hasSquare(row, column); column++) { - const tripleUp = [ - board.get(row - 2, column), - board.get(row - 1, column), - board.get(row - 0, column), - ]; - - const outcome = getSequenceOutcome(tripleUp); - if (outcome) { - return outcome; - } - } - } - - for (let row = 2; board.hasRow(row); row++) { - for (let column = 2; board.hasSquare(row, column); column++) { - { - const tripleUpLeft = [ - board.get(row - 2, column - 2), - board.get(row - 1, column - 1), - board.get(row - 0, column - 0), - ]; - const upLeftOutcome = getSequenceOutcome(tripleUpLeft); - if (upLeftOutcome) { - return upLeftOutcome; - } - } - - { - const tripleCross = [ - board.get(row - 0, column - 2), - board.get(row - 1, column - 1), - board.get(row - 2, column - 0), - ]; - - const crossOutcome = getSequenceOutcome(tripleCross); - if (crossOutcome) { - return crossOutcome; - } - } - } - } - - 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 getPreferredNextOutcome = ( possibleNextOutcomes: ExpectedOutcome[], currentPlayer: Player, @@ -156,7 +69,11 @@ export const getPreferredNextOutcome = ( ); }; -export const computeAllSolutions = (rows: number, columns: number) => { +export const computeAllSolutions = ( + rows: number, + columns: number, + rules: GameRules, +) => { const expectedOutcomesByBoard = new Map(); const getExpectedOutcomeForBoard = ( @@ -178,7 +95,7 @@ export const computeAllSolutions = (rows: number, columns: number) => { // Short-circuit if this board is full { - const currentOutcome = getBoardOutcome(board); + const currentOutcome = rules.getBoardOutcome(board); if (currentOutcome !== CurrentOutcome.Undecided) { const expectedOutcome = getExpectedOutcomeByCurrentOutcome(currentOutcome); diff --git a/src/lib/tictactoe-rules.spec.ts b/src/lib/tictactoe-rules.spec.ts new file mode 100644 index 0000000..7e6f15a --- /dev/null +++ b/src/lib/tictactoe-rules.spec.ts @@ -0,0 +1,345 @@ +import t from "tap"; + +import { Board } from "./board.ts"; +import { CurrentOutcome, SquareState } from "./datatypes.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/lib/tictactoe-rules.ts b/src/lib/tictactoe-rules.ts new file mode 100644 index 0000000..31b79f1 --- /dev/null +++ b/src/lib/tictactoe-rules.ts @@ -0,0 +1,89 @@ +import { CurrentOutcome, GameRules, SquareState } from "./datatypes.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; + } +}; + +export const getBoardOutcome: GameRules["getBoardOutcome"] = (board) => { + for (let row = 0; board.hasRow(row); row++) { + for (let column = 2; board.hasSquare(row, column); column++) { + const tripleLeft = [ + board.get(row, column - 2), + board.get(row, column - 1), + board.get(row, column - 0), + ]; + + const outcome = getSequenceOutcome(tripleLeft); + if (outcome) { + return outcome; + } + } + } + + for (let row = 2; board.hasRow(row); row++) { + for (let column = 0; board.hasSquare(row, column); column++) { + const tripleUp = [ + board.get(row - 2, column), + board.get(row - 1, column), + board.get(row - 0, column), + ]; + + const outcome = getSequenceOutcome(tripleUp); + if (outcome) { + return outcome; + } + } + } + + for (let row = 2; board.hasRow(row); row++) { + for (let column = 2; board.hasSquare(row, column); column++) { + { + const tripleUpLeft = [ + board.get(row - 2, column - 2), + board.get(row - 1, column - 1), + board.get(row - 0, column - 0), + ]; + const upLeftOutcome = getSequenceOutcome(tripleUpLeft); + if (upLeftOutcome) { + return upLeftOutcome; + } + } + + { + const tripleCross = [ + board.get(row - 0, column - 2), + board.get(row - 1, column - 1), + board.get(row - 2, column - 0), + ]; + + const crossOutcome = getSequenceOutcome(tripleCross); + if (crossOutcome) { + return crossOutcome; + } + } + } + } + + 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; +};