diff --git a/src/lib/board.spec.ts b/src/lib/board.spec.ts index 1406ffa..6084b3d 100644 --- a/src/lib/board.spec.ts +++ b/src/lib/board.spec.ts @@ -10,12 +10,19 @@ void t.test("Serialize / deserialize", async (t) => { return board; }; - void t.test("0x0 board", async (t) => { + void t.test("1x0 board", async (t) => { const board = createAndCheckBoard(t, ""); t.throws(() => board.get(0, 0), { message: "Out of bounds: 0:0" }); t.throws(() => board.get(0, 1), { message: "Out of bounds: 0:1" }); t.throws(() => board.get(1, 0), { message: "Out of bounds: 1:0" }); t.throws(() => board.get(1, 1), { message: "Out of bounds: 1:1" }); + + t.equal(board.hasRow(0), true); + t.equal(board.hasRow(1), false); + t.equal(board.hasSquare(0, 0), false); + t.equal(board.hasSquare(0, 1), false); + t.equal(board.hasSquare(1, 0), false); + t.equal(board.hasSquare(1, 1), false); }); void t.test("Empty 1x1 board", async (t) => { @@ -24,6 +31,13 @@ void t.test("Serialize / deserialize", async (t) => { t.throws(() => board.get(0, 1), { message: "Out of bounds: 0:1" }); t.throws(() => board.get(1, 0), { message: "Out of bounds: 1:0" }); t.throws(() => board.get(1, 1), { message: "Out of bounds: 1:1" }); + + t.equal(board.hasRow(0), true); + t.equal(board.hasRow(1), false); + t.equal(board.hasSquare(0, 0), true); + t.equal(board.hasSquare(0, 1), false); + t.equal(board.hasSquare(1, 0), false); + t.equal(board.hasSquare(1, 1), false); }); void t.test("1x1 board with X", async (t) => { @@ -32,6 +46,13 @@ void t.test("Serialize / deserialize", async (t) => { t.throws(() => board.get(0, 1), { message: "Out of bounds: 0:1" }); t.throws(() => board.get(1, 0), { message: "Out of bounds: 1:0" }); t.throws(() => board.get(1, 1), { message: "Out of bounds: 1:1" }); + + t.equal(board.hasRow(0), true); + t.equal(board.hasRow(1), false); + t.equal(board.hasSquare(0, 0), true); + t.equal(board.hasSquare(0, 1), false); + t.equal(board.hasSquare(1, 0), false); + t.equal(board.hasSquare(1, 1), false); }); void t.test("Half-full 3x4 board", async (t) => { @@ -56,6 +77,12 @@ void t.test("Serialize / deserialize", async (t) => { t.throws(() => board.get(4, 1), { message: "Out of bounds: 4:1" }); t.throws(() => board.get(4, 2), { message: "Out of bounds: 4:2" }); t.throws(() => board.get(4, 3), { message: "Out of bounds: 4:3" }); + + t.equal(board.hasRow(3), true); + t.equal(board.hasRow(4), false); + t.equal(board.hasSquare(3, 2), true); + t.equal(board.hasSquare(3, 3), false); + t.equal(board.hasSquare(4, 0), false); }); void t.test("Validation", async (t) => { diff --git a/src/lib/board.ts b/src/lib/board.ts index 155e61f..da17a9c 100644 --- a/src/lib/board.ts +++ b/src/lib/board.ts @@ -4,6 +4,14 @@ import { unreachable } from "./utils.ts"; export class Board { constructor(private readonly state: SquareState[][]) {} + hasRow(row: number) { + return !!this.state[row]; + } + + hasSquare(row: number, column: number) { + return !!this.state[row]?.[column]; + } + get(row: number, column: number) { const result = this.state[row]?.[column]; if (!result) { diff --git a/src/lib/solver.spec.ts b/src/lib/solver.spec.ts new file mode 100644 index 0000000..853e46a --- /dev/null +++ b/src/lib/solver.spec.ts @@ -0,0 +1,282 @@ +import t from "tap"; + +import { getBoardOutcome, getSequenceOutcome } from "./solver.ts"; +import { CurrentOutcome, SquareState } from "./types.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("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, + ); + }); +}); diff --git a/src/lib/solver.ts b/src/lib/solver.ts new file mode 100644 index 0000000..f5e3c47 --- /dev/null +++ b/src/lib/solver.ts @@ -0,0 +1,90 @@ +import { Board } from "./board.ts"; +import { CurrentOutcome, SquareState } from "./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; + } +}; + +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; +}; diff --git a/src/lib/types.ts b/src/lib/types.ts index 048c923..a4f63bd 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,3 +3,10 @@ export enum SquareState { X, O, } + +export enum CurrentOutcome { + Undecided = 1, + Draw, + WinX, + WinO, +}