feature/modern-browsers
Inga 🏳‍🌈 2 weeks ago
parent 24fcb0ca1b
commit a30663b268
  1. 14
      src/shared/array-utils.spec.ts
  2. 14
      src/shared/board.spec.ts
  3. 17
      src/shared/board.ts
  4. 238
      src/shared/boardgame-state.spec.ts
  5. 36
      src/shared/datatypes.spec.ts
  6. 4
      src/shared/datatypes.ts
  7. 3
      src/shared/utils.ts

@ -0,0 +1,14 @@
import t from "tap";
import { repeat, sequence } from "./array-utils.ts";
void t.test("repeat", async (t) => {
t.matchOnlyStrict(repeat("x", 0), []);
t.matchOnlyStrict(repeat("x", 1), ["x"]);
t.matchOnlyStrict(repeat("x", 5), ["x", "x", "x", "x", "x"]);
});
void t.test("sequence", async (t) => {
t.matchOnlyStrict(sequence(0), []);
t.matchOnlyStrict(sequence(1), [0]);
t.matchOnlyStrict(sequence(5), [0, 1, 2, 3, 4]);
});

@ -89,20 +89,6 @@ void t.test("Serialize / deserialize", async (t) => {
t.throws(() => Board.fromSerialized("abc"), {
message: "Unsupported square character: a",
});
{
const board = new Board([[undefined as any]]);
t.throws(() => board.serialize(), {
message: "Unsupported square state: undefined",
});
}
{
const board = new Board([["test" as any]]);
t.throws(() => board.serialize(), {
message: "Unsupported square state: test",
});
}
});
});

@ -3,7 +3,7 @@ import { BoardType, SquareState, formatSquareState, parseSquareState } from "./d
export class Board implements BoardType {
// State should be immutable
constructor(
private constructor(
private readonly state: SquareState[][],
private serialized?: string,
) {}
@ -61,20 +61,7 @@ export class Board implements BoardType {
}
private forceSerialize() {
return this.state
.map((row) =>
row
.map((squareState) => {
const char = formatSquareState(squareState);
if (!char) {
throw new Error(`Unsupported square state: ${squareState}`);
}
return char;
})
.join(""),
)
.join("|");
return this.state.map((row) => row.map(formatSquareState).join("")).join("|");
}
serialize() {

@ -0,0 +1,238 @@
import t, { type Test } from "tap";
import { BoardgameState } from "./boardgame-state.ts";
import { Player, SquareState } from "./datatypes.ts";
const createAndCheckBoardgameState = (t: Test, serialized: string) => {
const boardgameState = BoardgameState.fromSerialized(serialized);
t.ok(boardgameState);
t.equal(boardgameState?.serialize(), serialized);
return boardgameState;
};
void t.test("Serialize / deserialize", async (t) => {
void t.test("Empty state", async (t) => {
t.equal(BoardgameState.fromSerialized(""), null);
t.equal(BoardgameState.fromSerialized(null), null);
t.equal(BoardgameState.fromSerialized(".."), null);
});
void t.test("1x1 state (incompletely serialized)", async (t) => {
const state = BoardgameState.fromSerialized("1x1");
t.ok(state);
t.equal(state?.serialize(), "1x1..");
t.equal(state?.rows, 1);
t.equal(state?.columns, 1);
t.equal(state?.currentPlayer, null);
t.equal(state?.currentPlayerName, "");
t.equal(state?.board, null);
});
void t.test("1x1 state with empty board", async (t) => {
const state = createAndCheckBoardgameState(t, "1x1..");
t.equal(state?.rows, 1);
t.equal(state?.columns, 1);
t.equal(state?.currentPlayer, null);
t.equal(state?.currentPlayerName, "");
t.equal(state?.board, null);
});
void t.test("1x1 board with started game", async (t) => {
const state = createAndCheckBoardgameState(t, "1x1.X._");
t.equal(state?.rows, 1);
t.equal(state?.columns, 1);
t.equal(state?.currentPlayer, Player.X);
t.equal(state?.currentPlayerName, "X");
t.equal(state?.board?.hasRow(0), true);
t.equal(state?.board?.hasSquare(0, 0), true);
t.equal(state?.board?.get(0, 0), SquareState.Unoccupied);
t.equal(state?.board?.hasSquare(0, 1), false);
t.equal(state?.board?.hasRow(1), false);
});
void t.test("1x2 board with first move", async (t) => {
const state = createAndCheckBoardgameState(t, "1x2.O._X");
t.equal(state?.rows, 1);
t.equal(state?.columns, 2);
t.equal(state?.currentPlayer, Player.O);
t.equal(state?.currentPlayerName, "O");
t.equal(state?.board?.get(0, 0), SquareState.Unoccupied);
t.equal(state?.board?.get(0, 1), SquareState.X);
t.equal(state?.board?.hasSquare(0, 2), false);
t.equal(state?.board?.hasRow(1), false);
});
void t.test("2x1 board with first move", async (t) => {
const state = createAndCheckBoardgameState(t, "2x1.O._|X");
t.equal(state?.rows, 2);
t.equal(state?.columns, 1);
t.equal(state?.currentPlayer, Player.O);
t.equal(state?.currentPlayerName, "O");
t.equal(state?.board?.get(0, 0), SquareState.Unoccupied);
t.equal(state?.board?.get(1, 0), SquareState.X);
t.equal(state?.board?.hasSquare(0, 1), false);
t.equal(state?.board?.hasRow(2), false);
});
});
void t.test("Empty state creation", async (t) => {
void t.test("0x0 board", async (t) => {
const state = BoardgameState.createWithoutBoard(0, 0);
t.equal(state.serialize(), "0x0..");
t.equal(state.rows, 0);
t.equal(state.columns, 0);
t.equal(state.currentPlayer, null);
t.equal(state.currentPlayerName, "");
t.equal(state.board, null);
});
void t.test("1x0 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 0);
t.equal(state.serialize(), "1x0..");
t.equal(state.rows, 1);
t.equal(state.columns, 0);
t.equal(state.currentPlayer, null);
t.equal(state.currentPlayerName, "");
t.equal(state.board, null);
});
void t.test("1x1 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 1);
t.equal(state.serialize(), "1x1..");
t.equal(state.rows, 1);
t.equal(state.columns, 1);
t.equal(state.currentPlayer, null);
t.equal(state.currentPlayerName, "");
t.equal(state.board, null);
});
void t.test("1x2 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 2);
t.equal(state.serialize(), "1x2..");
t.equal(state.rows, 1);
t.equal(state.columns, 2);
t.equal(state.currentPlayer, null);
t.equal(state.currentPlayerName, "");
t.equal(state.board, null);
});
void t.test("Error handling", async (t) => {
t.throws(() => BoardgameState.fromSerialized("0.."), {
message: "Incorrect dimensions",
});
});
});
void t.test("withEmptyBoard", async (t) => {
void t.test("On 0x0 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "0x0..");
const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "0x0.X.");
t.equal(newState?.rows, 0);
t.equal(newState?.columns, 0);
t.equal(newState?.currentPlayer, Player.X);
t.equal(newState?.currentPlayerName, "X");
t.equal(newState?.board?.serialize(), "");
});
void t.test("On 0x1 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "0x1..");
const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "0x1.X.");
t.equal(newState?.rows, 0);
t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X);
t.equal(newState?.currentPlayerName, "X");
t.equal(newState?.board?.serialize(), "");
});
void t.test("On 1x0 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "1x0..");
const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "1x0.X.");
t.equal(newState?.rows, 1);
t.equal(newState?.columns, 0);
t.equal(newState?.currentPlayer, Player.X);
t.equal(newState?.currentPlayerName, "X");
t.equal(newState?.board?.serialize(), "");
});
void t.test("On 1x1 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "1x1..");
const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "1x1.X._");
t.equal(newState?.rows, 1);
t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X);
t.equal(newState?.currentPlayerName, "X");
t.equal(newState?.board?.serialize(), "_");
});
void t.test("On 1x2 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "1x2..");
const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "1x2.X.__");
t.equal(newState?.rows, 1);
t.equal(newState?.columns, 2);
t.equal(newState?.currentPlayer, Player.X);
t.equal(newState?.currentPlayerName, "X");
t.equal(newState?.board?.serialize(), "__");
});
void t.test("On 2x1 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "2x1..");
const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "2x1.X._|_");
t.equal(newState?.rows, 2);
t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X);
t.equal(newState?.currentPlayerName, "X");
t.equal(newState?.board?.serialize(), "_|_");
});
});
void t.test("withMove", async (t) => {
void t.test("On 2x3 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "2x3.X.___|___");
const firstMoveState = oldState?.withMove(0, 1);
t.equal(firstMoveState?.serialize(), "2x3.O._X_|___");
t.equal(firstMoveState?.rows, 2);
t.equal(firstMoveState?.columns, 3);
t.equal(firstMoveState?.currentPlayer, Player.O);
t.equal(firstMoveState?.currentPlayerName, "O");
t.equal(firstMoveState?.board?.serialize(), "_X_|___");
const secondMoveState = firstMoveState?.withMove(1, 2);
t.equal(secondMoveState?.serialize(), "2x3.X._X_|__O");
t.equal(secondMoveState?.rows, 2);
t.equal(secondMoveState?.columns, 3);
t.equal(secondMoveState?.currentPlayer, Player.X);
t.equal(secondMoveState?.currentPlayerName, "X");
t.equal(secondMoveState?.board?.serialize(), "_X_|__O");
});
void t.test("Error handling", async () => {
const unstartedBoard = createAndCheckBoardgameState(t, "1x1..");
t.throws(() => unstartedBoard?.withMove(0, 0), {
message: "Game is not started",
});
});
});
void t.test("Sample usage scenario", async (t) => {
const noBoardState = BoardgameState.createWithoutBoard(2, 3);
t.equal(noBoardState.serialize(), "2x3..");
const gameStartedState = noBoardState.withEmptyBoard();
t.equal(gameStartedState.serialize(), "2x3.X.___|___");
const firstMoveState = gameStartedState.withMove(0, 1);
t.equal(firstMoveState.serialize(), "2x3.O._X_|___");
const secondMoveState = firstMoveState.withMove(1, 2);
t.equal(secondMoveState.serialize(), "2x3.X._X_|__O");
const thirdMoveState = secondMoveState.withMove(1, 0);
t.equal(thirdMoveState.serialize(), "2x3.O._X_|X_O");
});

@ -4,11 +4,15 @@ import {
FinalOutcome,
Player,
SquareState,
formatPlayer,
formatSquareState,
getDesiredFinalOutcomeByPlayer,
getExpectedOutcomeByCurrentOutcome,
getNextPlayer,
getOccupiedStateByPlayer,
getUndesiredFinalOutcomeByPlayer,
parsePlayer,
parseSquareState,
} from "./datatypes.ts";
// These unit tests are mostly useless because they're basically a tautology,
@ -71,3 +75,35 @@ void t.test("getUndesiredFinalOutcomeByPlayer", async (t) => {
message: "Unsupported player: test",
});
});
void t.test("formatSquareState", async (t) => {
t.equal(formatSquareState(SquareState.Unoccupied), "_");
t.equal(formatSquareState(SquareState.X), "X");
t.equal(formatSquareState(SquareState.O), "O");
t.throws(() => formatSquareState("test" as unknown as SquareState), {
message: "Unsupported square state: test",
});
});
void t.test("parseSquareState", async (t) => {
t.equal(parseSquareState("_"), SquareState.Unoccupied);
t.equal(parseSquareState("X"), SquareState.X);
t.equal(parseSquareState("O"), SquareState.O);
t.equal(parseSquareState("test"), null);
t.equal(parseSquareState(undefined as unknown as string), null);
});
void t.test("formatPlayer", async (t) => {
t.equal(formatPlayer(Player.X), "X");
t.equal(formatPlayer(Player.O), "O");
t.throws(() => formatPlayer("test" as unknown as Player), {
message: "Unsupported player: test",
});
});
void t.test("parsePlayer", async (t) => {
t.equal(parsePlayer("X"), Player.X);
t.equal(parsePlayer("O"), Player.O);
t.equal(parsePlayer("test"), null);
t.equal(parsePlayer(undefined as unknown as string), null);
});

@ -1,4 +1,4 @@
import { unreachableNull, unreachableString } from "./utils.ts";
import { unreachableString } from "./utils.ts";
export enum SquareState {
Unoccupied = 1, // so that all SquareState values are truthy
@ -128,7 +128,7 @@ export const formatSquareState = (squareState: SquareState) => {
case SquareState.X:
return "X";
default:
return unreachableNull(squareState);
throw new Error(`Unsupported square state: ${unreachableString(squareState)}`);
}
};

@ -1,5 +1,2 @@
// To simplify switches and provide some typecheck-time guarantees
export const unreachableString = (value: never) => value as unknown as string;
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- We don't care about the value here, but we do care that from TS point of view it has type `never`
export const unreachableNull = (_value: never) => null;

Loading…
Cancel
Save