implemented detection of win/lose/draw configurations

main
Inga 🏳‍🌈 1 week ago
parent b47cfb8916
commit 1c9175e4a9
  1. 29
      src/lib/board.spec.ts
  2. 8
      src/lib/board.ts
  3. 282
      src/lib/solver.spec.ts
  4. 90
      src/lib/solver.ts
  5. 7
      src/lib/types.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) => {

@ -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) {

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

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

@ -3,3 +3,10 @@ export enum SquareState {
X,
O,
}
export enum CurrentOutcome {
Undecided = 1,
Draw,
WinX,
WinO,
}

Loading…
Cancel
Save