uncoupled tictactoe rules and solver logic

main
Inga 🏳‍🌈 1 week ago
parent e424772020
commit 2d03fc4006
  1. 12
      src/lib/datatypes.ts
  2. 365
      src/lib/solver.spec.ts
  3. 97
      src/lib/solver.ts
  4. 345
      src/lib/tictactoe-rules.spec.ts
  5. 89
      src/lib/tictactoe-rules.ts

@ -4,6 +4,14 @@ export enum SquareState {
O, 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 { export enum Player {
X = 101, // so that values of different enums never overlap X = 101, // so that values of different enums never overlap
O, O,
@ -27,6 +35,10 @@ export type ExpectedOutcome = {
movesLeft: number; movesLeft: number;
}; };
export type GameRules = {
getBoardOutcome(board: BoardType): CurrentOutcome;
};
export const getExpectedOutcomeByCurrentOutcome = ( export const getExpectedOutcomeByCurrentOutcome = (
currentOutcome: Exclude<CurrentOutcome, CurrentOutcome.Undecided>, currentOutcome: Exclude<CurrentOutcome, CurrentOutcome.Undecided>,
): ExpectedOutcome => { ): ExpectedOutcome => {

@ -1,359 +1,8 @@
import t from "tap"; import t from "tap";
import { import { ExpectedOutcome, FinalOutcome, Player } from "./datatypes.ts";
computeAllSolutions, import { computeAllSolutions, getPreferredNextOutcome } from "./solver.ts";
getBoardOutcome, import { getBoardOutcome } from "./tictactoe-rules.ts";
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,
);
});
});
void t.test("getPreferredNextOutcome", async (t) => { void t.test("getPreferredNextOutcome", async (t) => {
const nextOutcomes: ExpectedOutcome[] = [ const nextOutcomes: ExpectedOutcome[] = [
@ -453,7 +102,9 @@ void t.test("computeAllSolutions", async (t) => {
expectedSolutions: Record<string, ExpectedOutcome>, expectedSolutions: Record<string, ExpectedOutcome>,
) => { ) => {
t.matchOnlyStrict( t.matchOnlyStrict(
Object.fromEntries(computeAllSolutions(rows, columns).entries()), Object.fromEntries(
computeAllSolutions(rows, columns, { getBoardOutcome }).entries(),
),
expectedSolutions, expectedSolutions,
); );
}; };
@ -464,7 +115,9 @@ void t.test("computeAllSolutions", async (t) => {
expectedSolutionsCount: number, expectedSolutionsCount: number,
expectedSolutionsIncomplete: Record<string, ExpectedOutcome>, expectedSolutionsIncomplete: Record<string, ExpectedOutcome>,
) => { ) => {
const allSolutions = computeAllSolutions(rows, columns); const allSolutions = computeAllSolutions(rows, columns, {
getBoardOutcome,
});
t.equal(allSolutions.size, expectedSolutionsCount); t.equal(allSolutions.size, expectedSolutionsCount);
t.matchStrict( t.matchStrict(
Object.fromEntries(allSolutions.entries()), Object.fromEntries(allSolutions.entries()),

@ -3,6 +3,7 @@ import {
CurrentOutcome, CurrentOutcome,
ExpectedOutcome, ExpectedOutcome,
FinalOutcome, FinalOutcome,
GameRules,
Player, Player,
SquareState, SquareState,
getDesiredFinalOutcomeByPlayer, getDesiredFinalOutcomeByPlayer,
@ -12,94 +13,6 @@ import {
getUndesiredFinalOutcomeByPlayer, getUndesiredFinalOutcomeByPlayer,
} from "./datatypes.ts"; } 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 = ( export const getPreferredNextOutcome = (
possibleNextOutcomes: ExpectedOutcome[], possibleNextOutcomes: ExpectedOutcome[],
currentPlayer: Player, 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<string, ExpectedOutcome>(); const expectedOutcomesByBoard = new Map<string, ExpectedOutcome>();
const getExpectedOutcomeForBoard = ( const getExpectedOutcomeForBoard = (
@ -178,7 +95,7 @@ export const computeAllSolutions = (rows: number, columns: number) => {
// Short-circuit if this board is full // Short-circuit if this board is full
{ {
const currentOutcome = getBoardOutcome(board); const currentOutcome = rules.getBoardOutcome(board);
if (currentOutcome !== CurrentOutcome.Undecided) { if (currentOutcome !== CurrentOutcome.Undecided) {
const expectedOutcome = const expectedOutcome =
getExpectedOutcomeByCurrentOutcome(currentOutcome); getExpectedOutcomeByCurrentOutcome(currentOutcome);

@ -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,
);
});
});

@ -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;
};
Loading…
Cancel
Save