implemented derived boards creation

main
Inga 🏳‍🌈 1 week ago
parent b582f6c118
commit b47cfb8916
  1. 157
      src/lib/board.spec.ts
  2. 53
      src/lib/board.ts
  3. 2
      src/lib/utils.ts

@ -1,63 +1,112 @@
import t, { type Test } from "tap"; import t, { type Test } from "tap";
import { Board } from "./board.ts"; import { Board } from "./board.ts";
import { SquareState } from "./types.js"; import { SquareState } from "./types.ts";
const createAndCheckBoard = (t: Test, serialized: string) => {
const board = new Board(serialized);
t.equal(board.serialize(), serialized);
return board;
};
void t.test("0x0 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" });
});
void t.test("Empty 1x1 board", async (t) => { void t.test("Serialize / deserialize", async (t) => {
const board = createAndCheckBoard(t, "_"); const createAndCheckBoard = (t: Test, serialized: string) => {
t.equal(board.get(0, 0), SquareState.Unoccupied); const board = Board.fromSerialized(serialized);
t.throws(() => board.get(0, 1), { message: "Out of bounds: 0:1" }); t.equal(board.serialize(), serialized);
t.throws(() => board.get(1, 0), { message: "Out of bounds: 1:0" }); return board;
t.throws(() => board.get(1, 1), { message: "Out of bounds: 1:1" }); };
});
void t.test("1x1 board with X", async (t) => { void t.test("0x0 board", async (t) => {
const board = createAndCheckBoard(t, "X"); const board = createAndCheckBoard(t, "");
t.equal(board.get(0, 0), SquareState.X); 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(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, 0), { message: "Out of bounds: 1:0" });
t.throws(() => board.get(1, 1), { message: "Out of bounds: 1:1" }); t.throws(() => board.get(1, 1), { message: "Out of bounds: 1:1" });
}); });
void t.test("Empty 1x1 board", async (t) => {
const board = createAndCheckBoard(t, "_");
t.equal(board.get(0, 0), SquareState.Unoccupied);
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" });
});
void t.test("1x1 board with X", async (t) => {
const board = createAndCheckBoard(t, "X");
t.equal(board.get(0, 0), SquareState.X);
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" });
});
void t.test("Half-full 3x4 board", async (t) => {
const board = createAndCheckBoard(t, "XO_|O_X|OX_|_XO");
t.equal(board.get(0, 0), SquareState.X);
t.equal(board.get(0, 1), SquareState.O);
t.equal(board.get(0, 2), SquareState.Unoccupied);
t.throws(() => board.get(0, 3), { message: "Out of bounds: 0:3" });
t.equal(board.get(1, 0), SquareState.O);
t.equal(board.get(1, 1), SquareState.Unoccupied);
t.equal(board.get(1, 2), SquareState.X);
t.throws(() => board.get(1, 3), { message: "Out of bounds: 1:3" });
t.equal(board.get(2, 0), SquareState.O);
t.equal(board.get(2, 1), SquareState.X);
t.equal(board.get(2, 2), SquareState.Unoccupied);
t.throws(() => board.get(2, 3), { message: "Out of bounds: 2:3" });
t.equal(board.get(3, 0), SquareState.Unoccupied);
t.equal(board.get(3, 1), SquareState.X);
t.equal(board.get(3, 2), SquareState.O);
t.throws(() => board.get(3, 3), { message: "Out of bounds: 3:3" });
t.throws(() => board.get(4, 0), { message: "Out of bounds: 4:0" });
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" });
});
void t.test("Half-full 3x4 board", async (t) => { void t.test("Validation", async (t) => {
const board = createAndCheckBoard(t, "XO_|O_X|OX_|_XO"); t.throws(() => Board.fromSerialized("abc"), {
t.equal(board.get(0, 0), SquareState.X); message: "Unsupported square character: a",
t.equal(board.get(0, 1), SquareState.O); });
t.equal(board.get(0, 2), SquareState.Unoccupied);
t.throws(() => board.get(0, 3), { message: "Out of bounds: 0:3" }); {
t.equal(board.get(1, 0), SquareState.O); const board = new Board([[undefined as unknown as SquareState]]);
t.equal(board.get(1, 1), SquareState.Unoccupied); t.throws(() => board.serialize(), {
t.equal(board.get(1, 2), SquareState.X); message: "Unsupported square state: undefined",
t.throws(() => board.get(1, 3), { message: "Out of bounds: 1:3" }); });
t.equal(board.get(2, 0), SquareState.O); }
t.equal(board.get(2, 1), SquareState.X);
t.equal(board.get(2, 2), SquareState.Unoccupied); {
t.throws(() => board.get(2, 3), { message: "Out of bounds: 2:3" }); const board = new Board([["test" as unknown as SquareState]]);
t.equal(board.get(3, 0), SquareState.Unoccupied); t.throws(() => board.serialize(), {
t.equal(board.get(3, 1), SquareState.X); message: "Unsupported square state: test",
t.equal(board.get(3, 2), SquareState.O); });
t.throws(() => board.get(3, 3), { message: "Out of bounds: 3:3" }); }
t.throws(() => board.get(4, 0), { message: "Out of bounds: 4:0" }); });
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" });
}); });
void t.test("Throws error on incorrect serialized value", async (t) => { void t.test("Derived boards creation", async (t) => {
t.throws(() => new Board("abc"), { const board = Board.fromSerialized("_X|O_|_O");
message: "Unsupported square character: a",
t.equal(board.with(0, 0, SquareState.Unoccupied).serialize(), "_X|O_|_O");
t.equal(board.with(0, 0, SquareState.O).serialize(), "OX|O_|_O");
t.equal(board.with(0, 0, SquareState.X).serialize(), "XX|O_|_O");
t.throws(() => board.with(0, 1, SquareState.X), {
message: "Cannot update occupied square: 0:1",
});
t.throws(() => board.with(0, 2, SquareState.X), {
message: "Out of bounds: 0:2",
});
t.throws(() => board.with(1, 0, SquareState.X), {
message: "Cannot update occupied square: 1:0",
});
t.equal(board.with(1, 1, SquareState.X).serialize(), "_X|OX|_O");
t.throws(() => board.with(1, 2, SquareState.X), {
message: "Out of bounds: 1:2",
});
t.equal(board.with(2, 0, SquareState.X).serialize(), "_X|O_|XX");
t.throws(() => board.with(2, 1, SquareState.X), {
message: "Cannot update occupied square: 2:1",
});
t.throws(() => board.with(2, 2, SquareState.X), {
message: "Out of bounds: 2:2",
});
t.throws(() => board.with(3, 0, SquareState.X), {
message: "Out of bounds: 3:0",
}); });
}); });

@ -2,24 +2,7 @@ import { SquareState } from "./types.ts";
import { unreachable } from "./utils.ts"; import { unreachable } from "./utils.ts";
export class Board { export class Board {
private readonly state: SquareState[][]; constructor(private readonly state: SquareState[][]) {}
constructor(serialized: string) {
this.state = serialized.split("|").map((line) =>
line.split("").map((char) => {
switch (char) {
case "_":
return SquareState.Unoccupied;
case "O":
return SquareState.O;
case "X":
return SquareState.X;
default:
throw new Error(`Unsupported square character: ${char}`);
}
}),
);
}
get(row: number, column: number) { get(row: number, column: number) {
const result = this.state[row]?.[column]; const result = this.state[row]?.[column];
@ -30,6 +13,38 @@ export class Board {
return result; return result;
} }
with(row: number, column: number, newSquareState: SquareState) {
if (this.get(row, column) !== SquareState.Unoccupied) {
throw new Error(`Cannot update occupied square: ${row}:${column}`);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guaranteed to exist at this stage because otherwise `get` would throw an error
const newRowState = [...this.state[column]!];
newRowState[column] = newSquareState;
const newState = [...this.state];
newState[row] = newRowState;
return new Board(newState);
}
static fromSerialized(serialized: string) {
return new Board(
serialized.split("|").map((line) =>
line.split("").map((char) => {
switch (char) {
case "_":
return SquareState.Unoccupied;
case "O":
return SquareState.O;
case "X":
return SquareState.X;
default:
throw new Error(`Unsupported square character: ${char}`);
}
}),
),
);
}
serialize() { serialize() {
return this.state return this.state
.map((row) => .map((row) =>
@ -42,12 +57,10 @@ export class Board {
return "O"; return "O";
case SquareState.X: case SquareState.X:
return "X"; return "X";
/* c8 ignore start */
default: default:
throw new Error( throw new Error(
`Unsupported square state: ${unreachable(squareState)}`, `Unsupported square state: ${unreachable(squareState)}`,
); );
/* c8 ignore stop */
} }
}) })
.join(""), .join(""),

@ -1,4 +1,2 @@
// To simplify switches and provide some typecheck-time guarantees // To simplify switches and provide some typecheck-time guarantees
/* c8 ignore start */
export const unreachable = (value: never) => value as unknown as string; export const unreachable = (value: never) => value as unknown as string;
/* c8 ignore stop */

Loading…
Cancel
Save