implemented derived boards creation

main
Inga 🏳‍🌈 5 days ago
parent b582f6c118
commit b47cfb8916
  1. 77
      src/lib/board.spec.ts
  2. 41
      src/lib/board.ts
  3. 2
      src/lib/utils.ts

@ -1,38 +1,40 @@
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) => { void t.test("Serialize / deserialize", async (t) => {
const board = new Board(serialized); const createAndCheckBoard = (t: Test, serialized: string) => {
const board = Board.fromSerialized(serialized);
t.equal(board.serialize(), serialized); t.equal(board.serialize(), serialized);
return board; return board;
}; };
void t.test("0x0 board", async (t) => { void t.test("0x0 board", async (t) => {
const board = createAndCheckBoard(t, ""); const board = createAndCheckBoard(t, "");
t.throws(() => board.get(0, 0), { message: "Out of bounds: 0:0" }); 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) => { void t.test("Empty 1x1 board", async (t) => {
const board = createAndCheckBoard(t, "_"); const board = createAndCheckBoard(t, "_");
t.equal(board.get(0, 0), SquareState.Unoccupied); t.equal(board.get(0, 0), SquareState.Unoccupied);
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("1x1 board with X", async (t) => { void t.test("1x1 board with X", async (t) => {
const board = createAndCheckBoard(t, "X"); const board = createAndCheckBoard(t, "X");
t.equal(board.get(0, 0), SquareState.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(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("Half-full 3x4 board", async (t) => { void t.test("Half-full 3x4 board", async (t) => {
const board = createAndCheckBoard(t, "XO_|O_X|OX_|_XO"); const board = createAndCheckBoard(t, "XO_|O_X|OX_|_XO");
t.equal(board.get(0, 0), SquareState.X); t.equal(board.get(0, 0), SquareState.X);
t.equal(board.get(0, 1), SquareState.O); t.equal(board.get(0, 1), SquareState.O);
@ -54,10 +56,57 @@ void t.test("Half-full 3x4 board", async (t) => {
t.throws(() => board.get(4, 1), { message: "Out of bounds: 4:1" }); 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, 2), { message: "Out of bounds: 4:2" });
t.throws(() => board.get(4, 3), { message: "Out of bounds: 4:3" }); 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("Validation", async (t) => {
t.throws(() => new Board("abc"), { t.throws(() => Board.fromSerialized("abc"), {
message: "Unsupported square character: a", message: "Unsupported square character: a",
}); });
{
const board = new Board([[undefined as unknown as SquareState]]);
t.throws(() => board.serialize(), {
message: "Unsupported square state: undefined",
});
}
{
const board = new Board([["test" as unknown as SquareState]]);
t.throws(() => board.serialize(), {
message: "Unsupported square state: test",
});
}
});
});
void t.test("Derived boards creation", async (t) => {
const board = Board.fromSerialized("_X|O_|_O");
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,10 +2,33 @@ 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) { get(row: number, column: number) {
this.state = serialized.split("|").map((line) => const result = this.state[row]?.[column];
if (!result) {
throw new Error(`Out of bounds: ${row}:${column}`);
}
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) => { line.split("").map((char) => {
switch (char) { switch (char) {
case "_": case "_":
@ -18,18 +41,10 @@ export class Board {
throw new Error(`Unsupported square character: ${char}`); throw new Error(`Unsupported square character: ${char}`);
} }
}), }),
),
); );
} }
get(row: number, column: number) {
const result = this.state[row]?.[column];
if (!result) {
throw new Error(`Out of bounds: ${row}:${column}`);
}
return result;
}
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