From b47cfb8916c6437c9e84c29b935511e50c6a6d9b Mon Sep 17 00:00:00 2001 From: Inga Date: Sat, 16 Nov 2024 17:06:03 +0000 Subject: [PATCH] implemented derived boards creation --- src/lib/board.spec.ts | 157 +++++++++++++++++++++++++++--------------- src/lib/board.ts | 53 ++++++++------ src/lib/utils.ts | 2 - 3 files changed, 136 insertions(+), 76 deletions(-) diff --git a/src/lib/board.spec.ts b/src/lib/board.spec.ts index 7cac5c2..1406ffa 100644 --- a/src/lib/board.spec.ts +++ b/src/lib/board.spec.ts @@ -1,63 +1,112 @@ import t, { type Test } from "tap"; + import { Board } from "./board.ts"; -import { SquareState } from "./types.js"; - -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" }); -}); +import { SquareState } from "./types.ts"; -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("Serialize / deserialize", async (t) => { + const createAndCheckBoard = (t: Test, serialized: string) => { + const board = Board.fromSerialized(serialized); + t.equal(board.serialize(), serialized); + return board; + }; -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("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) => { + 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) => { - 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("Validation", async (t) => { + t.throws(() => Board.fromSerialized("abc"), { + 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("Throws error on incorrect serialized value", async (t) => { - t.throws(() => new Board("abc"), { - message: "Unsupported square character: a", +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", }); }); diff --git a/src/lib/board.ts b/src/lib/board.ts index 3b1dbe2..155e61f 100644 --- a/src/lib/board.ts +++ b/src/lib/board.ts @@ -2,24 +2,7 @@ import { SquareState } from "./types.ts"; import { unreachable } from "./utils.ts"; export class Board { - 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}`); - } - }), - ); - } + constructor(private readonly state: SquareState[][]) {} get(row: number, column: number) { const result = this.state[row]?.[column]; @@ -30,6 +13,38 @@ export class Board { 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() { return this.state .map((row) => @@ -42,12 +57,10 @@ export class Board { return "O"; case SquareState.X: return "X"; - /* c8 ignore start */ default: throw new Error( `Unsupported square state: ${unreachable(squareState)}`, ); - /* c8 ignore stop */ } }) .join(""), diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 52ea880..237cc75 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,2 @@ // To simplify switches and provide some typecheck-time guarantees -/* c8 ignore start */ export const unreachable = (value: never) => value as unknown as string; -/* c8 ignore stop */