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", |
||||
"include": ["src/**/*"], |
||||
"compilerOptions": { |
||||
"target": "ES2020", |
||||
"module": "Preserve", |
||||
"forceConsistentCasingInFileNames": true |
||||
} |
||||
"extends": "@tsconfig/strictest", |
||||
"include": ["src/**/*"], |
||||
"compilerOptions": { |
||||
"allowImportingTsExtensions": true, |
||||
"noEmit": true, |
||||
"target": "ES2020", |
||||
"module": "NodeNext", |
||||
"forceConsistentCasingInFileNames": true |
||||
} |
||||
|
||||
} |
||||
|
Loading…
Reference in new issue