parent
830b6734a1
commit
b582f6c118
@ -0,0 +1,63 @@ |
|||||||
|
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" }); |
||||||
|
}); |
||||||
|
|
||||||
|
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("Throws error on incorrect serialized value", async (t) => { |
||||||
|
t.throws(() => new Board("abc"), { |
||||||
|
message: "Unsupported square character: a", |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,57 @@ |
|||||||
|
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}`); |
||||||
|
} |
||||||
|
}), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
get(row: number, column: number) { |
||||||
|
const result = this.state[row]?.[column]; |
||||||
|
if (!result) { |
||||||
|
throw new Error(`Out of bounds: ${row}:${column}`); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
serialize() { |
||||||
|
return this.state |
||||||
|
.map((row) => |
||||||
|
row |
||||||
|
.map((squareState) => { |
||||||
|
switch (squareState) { |
||||||
|
case SquareState.Unoccupied: |
||||||
|
return "_"; |
||||||
|
case SquareState.O: |
||||||
|
return "O"; |
||||||
|
case SquareState.X: |
||||||
|
return "X"; |
||||||
|
/* c8 ignore start */ |
||||||
|
default: |
||||||
|
throw new Error( |
||||||
|
`Unsupported square state: ${unreachable(squareState)}`, |
||||||
|
); |
||||||
|
/* c8 ignore stop */ |
||||||
|
} |
||||||
|
}) |
||||||
|
.join(""), |
||||||
|
) |
||||||
|
.join("|"); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
export enum SquareState { |
||||||
|
Unoccupied = 1, // so that all SquareState values are truthy
|
||||||
|
X, |
||||||
|
O, |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
// 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 */ |
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"exclude": ["src/**/*.spec.*"], |
||||||
|
"compilerOptions": { |
||||||
|
"module": "Preserve" |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -1,10 +1,11 @@ |
|||||||
{ |
{ |
||||||
"extends": "@tsconfig/strictest", |
"extends": "@tsconfig/strictest", |
||||||
"include": ["src/**/*"], |
"include": ["src/**/*"], |
||||||
"compilerOptions": { |
"compilerOptions": { |
||||||
"target": "ES2020", |
"allowImportingTsExtensions": true, |
||||||
"module": "Preserve", |
"noEmit": true, |
||||||
"forceConsistentCasingInFileNames": true |
"target": "ES2020", |
||||||
} |
"module": "NodeNext", |
||||||
|
"forceConsistentCasingInFileNames": true |
||||||
} |
} |
||||||
|
} |
||||||
|
Loading…
Reference in new issue