diff --git a/src/backend/components/boardgame.tsx b/src/backend/components/boardgame.tsx
index ca46281..ef237c3 100644
--- a/src/backend/components/boardgame.tsx
+++ b/src/backend/components/boardgame.tsx
@@ -3,6 +3,7 @@ import {
BoardgameStateType,
CurrentOutcome,
GameRules,
+ Player,
SquareState,
formatSquareState,
} from "../../shared/datatypes.ts";
@@ -17,6 +18,12 @@ const getClassAndDisplayAttributes = (
style: getDisplayStates(gameState, currentOutcome)[className] ? {} : { display: "none" },
});
+const getSubmitAttributes = (key: string, targetState: BoardgameStateType) => ({
+ type: "submit" as const,
+ name: key,
+ value: targetState.serialize(),
+});
+
const getCellHtml = ({
key,
gameState,
@@ -32,7 +39,7 @@ const getCellHtml = ({
}) => {
if (!gameState.board) {
return (
-
+
+ Currently X moves are made manually.{" "}
+
+ Make computer play for X.
+
+
+
+
+ Currently X moves are made by computer.{" "}
+
+ Make them manually.
+
+
+
+
+ Currently O moves are made manually.{" "}
+
+ Make computer play for O.
+
+
+
+
+ Currently O moves are made by computer.{" "}
+
+ Make them manually.
+
+
+
Start game
diff --git a/src/backend/main/boardgame-handler.ts b/src/backend/main/boardgame-handler.ts
index cfada31..52792f3 100644
--- a/src/backend/main/boardgame-handler.ts
+++ b/src/backend/main/boardgame-handler.ts
@@ -4,7 +4,7 @@ import { BoardgameState } from "../../shared/boardgame-state.ts";
import { getBoardgameHtml } from "../components/boardgame.tsx";
import { gamesRules } from "../../shared/rules.ts";
import { getAllSolutions } from "../../shared/solver-cache.ts";
-import { CurrentOutcome, Player } from "../../shared/datatypes.ts";
+import { CurrentOutcome } from "../../shared/datatypes.ts";
import { createOpponent } from "../../shared/opponent.ts";
// Returns nothing if query parameter is uninitialized and a redirect was made,
@@ -21,7 +21,7 @@ export const handleBoardgame = (req: Request, res: Response, key: string) => {
return;
}
- if (state.board && state.currentPlayer === Player.O) {
+ if (state.board && state.currentPlayer && state.autoPlayers.has(state.currentPlayer)) {
const currentOutcome = rules.getBoardOutcome(state.board);
if (currentOutcome === CurrentOutcome.Undecided) {
const solutions = getAllSolutions(state.rows, state.columns, rules);
diff --git a/src/shared/array-utils.ts b/src/shared/array-utils.ts
index a0740e0..6e7af69 100644
--- a/src/shared/array-utils.ts
+++ b/src/shared/array-utils.ts
@@ -1,3 +1,7 @@
export const repeat = (value: T, size: number) => [...(new Array(size) as unknown[])].map(() => value);
export const sequence = (size: number) => [...(new Array(size) as unknown[])].map((_, index) => index);
+
+const isNotEmpty = (value: T): value is Exclude => !!value;
+
+export const compact = (array: T[]) => array.filter(isNotEmpty);
diff --git a/src/shared/boardgame-state.spec.ts b/src/shared/boardgame-state.spec.ts
index dc9acb5..b056cee 100644
--- a/src/shared/boardgame-state.spec.ts
+++ b/src/shared/boardgame-state.spec.ts
@@ -14,33 +14,57 @@ 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);
+ 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?.serialize(), "1x1...");
t.equal(state?.rows, 1);
t.equal(state?.columns, 1);
+ t.matchOnlyStrict(state?.autoPlayers, new Set());
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..");
+ const state = createAndCheckBoardgameState(t, "1x1...");
t.equal(state?.rows, 1);
t.equal(state?.columns, 1);
+ t.matchOnlyStrict(state?.autoPlayers, new Set());
+ t.equal(state?.currentPlayer, null);
+ t.equal(state?.currentPlayerName, "");
+ t.equal(state?.board, null);
+ });
+
+ void t.test("1x1 state with empty board and one autoplayer", async (t) => {
+ const state = createAndCheckBoardgameState(t, "1x1.X..");
+ t.equal(state?.rows, 1);
+ t.equal(state?.columns, 1);
+ t.matchOnlyStrict(state?.autoPlayers, new Set([Player.X]));
+ t.equal(state?.currentPlayer, null);
+ t.equal(state?.currentPlayerName, "");
+ t.equal(state?.board, null);
+ });
+
+ void t.test("1x1 state with empty board and one duplicated autoplayer", async (t) => {
+ const state = BoardgameState.fromSerialized("1x1.OOO..");
+ t.equal(state?.serialize(), "1x1.O..");
+ t.equal(state?.rows, 1);
+ t.equal(state?.columns, 1);
+ t.matchOnlyStrict(state?.autoPlayers, new Set([Player.O]));
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._");
+ const state = createAndCheckBoardgameState(t, "1x1..X._");
t.equal(state?.rows, 1);
t.equal(state?.columns, 1);
+ t.matchOnlyStrict(state?.autoPlayers, new Set());
t.equal(state?.currentPlayer, Player.X);
t.equal(state?.currentPlayerName, "X");
t.equal(state?.board?.hasRow(0), true);
@@ -51,9 +75,10 @@ void t.test("Serialize / deserialize", async (t) => {
});
void t.test("1x2 board with first move", async (t) => {
- const state = createAndCheckBoardgameState(t, "1x2.O._X");
+ const state = createAndCheckBoardgameState(t, "1x2..O._X");
t.equal(state?.rows, 1);
t.equal(state?.columns, 2);
+ t.matchOnlyStrict(state?.autoPlayers, new Set());
t.equal(state?.currentPlayer, Player.O);
t.equal(state?.currentPlayerName, "O");
t.equal(state?.board?.get(0, 0), SquareState.Unoccupied);
@@ -63,9 +88,10 @@ void t.test("Serialize / deserialize", async (t) => {
});
void t.test("2x1 board with first move", async (t) => {
- const state = createAndCheckBoardgameState(t, "2x1.O._|X");
+ const state = createAndCheckBoardgameState(t, "2x1..O._|X");
t.equal(state?.rows, 2);
t.equal(state?.columns, 1);
+ t.matchOnlyStrict(state?.autoPlayers, new Set());
t.equal(state?.currentPlayer, Player.O);
t.equal(state?.currentPlayerName, "O");
t.equal(state?.board?.get(0, 0), SquareState.Unoccupied);
@@ -73,12 +99,37 @@ void t.test("Serialize / deserialize", async (t) => {
t.equal(state?.board?.hasSquare(0, 1), false);
t.equal(state?.board?.hasRow(2), false);
});
+
+ void t.test("2x1 board with first move and two autoplayers", async (t) => {
+ const state = createAndCheckBoardgameState(t, "2x1.XO.O._|X");
+ t.equal(state?.rows, 2);
+ t.equal(state?.columns, 1);
+ t.matchOnlyStrict(state?.autoPlayers, new Set([Player.X, Player.O]));
+ 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("Error handling", async (t) => {
+ t.throws(() => BoardgameState.fromSerialized("0..."), {
+ message: "Incorrect dimensions",
+ });
+
+ {
+ const state = BoardgameState.fromSerialized("1x1.abc..");
+ t.equal(state?.serialize(), "1x1...");
+ t.matchOnlyStrict(state?.autoPlayers, new Set());
+ }
+ });
});
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.serialize(), "0x0...");
t.equal(state.rows, 0);
t.equal(state.columns, 0);
t.equal(state.currentPlayer, null);
@@ -88,7 +139,7 @@ void t.test("Empty state creation", async (t) => {
void t.test("1x0 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 0);
- t.equal(state.serialize(), "1x0..");
+ t.equal(state.serialize(), "1x0...");
t.equal(state.rows, 1);
t.equal(state.columns, 0);
t.equal(state.currentPlayer, null);
@@ -98,7 +149,7 @@ void t.test("Empty state creation", async (t) => {
void t.test("1x1 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 1);
- t.equal(state.serialize(), "1x1..");
+ t.equal(state.serialize(), "1x1...");
t.equal(state.rows, 1);
t.equal(state.columns, 1);
t.equal(state.currentPlayer, null);
@@ -108,26 +159,20 @@ void t.test("Empty state creation", async (t) => {
void t.test("1x2 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 2);
- t.equal(state.serialize(), "1x2..");
+ 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 oldState = createAndCheckBoardgameState(t, "0x0...");
const newState = oldState?.withEmptyBoard();
- t.equal(newState?.serialize(), "0x0.X.");
+ t.equal(newState?.serialize(), "0x0..X.");
t.equal(newState?.rows, 0);
t.equal(newState?.columns, 0);
t.equal(newState?.currentPlayer, Player.X);
@@ -136,9 +181,9 @@ void t.test("withEmptyBoard", async (t) => {
});
void t.test("On 0x1 board", async (t) => {
- const oldState = createAndCheckBoardgameState(t, "0x1..");
+ const oldState = createAndCheckBoardgameState(t, "0x1...");
const newState = oldState?.withEmptyBoard();
- t.equal(newState?.serialize(), "0x1.X.");
+ t.equal(newState?.serialize(), "0x1..X.");
t.equal(newState?.rows, 0);
t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X);
@@ -147,9 +192,9 @@ void t.test("withEmptyBoard", async (t) => {
});
void t.test("On 1x0 board", async (t) => {
- const oldState = createAndCheckBoardgameState(t, "1x0..");
+ const oldState = createAndCheckBoardgameState(t, "1x0...");
const newState = oldState?.withEmptyBoard();
- t.equal(newState?.serialize(), "1x0.X.");
+ t.equal(newState?.serialize(), "1x0..X.");
t.equal(newState?.rows, 1);
t.equal(newState?.columns, 0);
t.equal(newState?.currentPlayer, Player.X);
@@ -158,9 +203,9 @@ void t.test("withEmptyBoard", async (t) => {
});
void t.test("On 1x1 board", async (t) => {
- const oldState = createAndCheckBoardgameState(t, "1x1..");
+ const oldState = createAndCheckBoardgameState(t, "1x1...");
const newState = oldState?.withEmptyBoard();
- t.equal(newState?.serialize(), "1x1.X._");
+ t.equal(newState?.serialize(), "1x1..X._");
t.equal(newState?.rows, 1);
t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X);
@@ -169,9 +214,9 @@ void t.test("withEmptyBoard", async (t) => {
});
void t.test("On 1x2 board", async (t) => {
- const oldState = createAndCheckBoardgameState(t, "1x2..");
+ const oldState = createAndCheckBoardgameState(t, "1x2...");
const newState = oldState?.withEmptyBoard();
- t.equal(newState?.serialize(), "1x2.X.__");
+ t.equal(newState?.serialize(), "1x2..X.__");
t.equal(newState?.rows, 1);
t.equal(newState?.columns, 2);
t.equal(newState?.currentPlayer, Player.X);
@@ -180,9 +225,9 @@ void t.test("withEmptyBoard", async (t) => {
});
void t.test("On 2x1 board", async (t) => {
- const oldState = createAndCheckBoardgameState(t, "2x1..");
+ const oldState = createAndCheckBoardgameState(t, "2x1...");
const newState = oldState?.withEmptyBoard();
- t.equal(newState?.serialize(), "2x1.X._|_");
+ t.equal(newState?.serialize(), "2x1..X._|_");
t.equal(newState?.rows, 2);
t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X);
@@ -191,12 +236,95 @@ void t.test("withEmptyBoard", async (t) => {
});
});
+void t.test("withAutoPlayer", async (t) => {
+ void t.test("On uninitialized board", async (t) => {
+ const oldState = createAndCheckBoardgameState(t, "2x3...");
+
+ const newStateX = oldState?.withAutoPlayer(Player.X);
+ t.equal(newStateX?.serialize(), "2x3.X..");
+ t.matchOnlyStrict(newStateX?.autoPlayers, new Set([Player.X]));
+
+ const newStateO = oldState?.withAutoPlayer(Player.O);
+ t.equal(newStateO?.serialize(), "2x3.O..");
+ t.matchOnlyStrict(newStateO?.autoPlayers, new Set([Player.O]));
+
+ const newStateXO = newStateX?.withAutoPlayer(Player.O);
+ t.equal(newStateXO?.serialize(), "2x3.XO..");
+ t.matchOnlyStrict(newStateXO?.autoPlayers, new Set([Player.X, Player.O]));
+
+ const newStateOO = newStateO?.withAutoPlayer(Player.O);
+ t.equal(newStateOO?.serialize(), "2x3.O..");
+ t.matchOnlyStrict(newStateOO?.autoPlayers, new Set([Player.O]));
+ });
+ void t.test("On 2x3 board", async (t) => {
+ const oldState = createAndCheckBoardgameState(t, "2x3..X.__X|O__");
+
+ const newStateX = oldState?.withAutoPlayer(Player.X);
+ t.equal(newStateX?.serialize(), "2x3.X.X.__X|O__");
+ t.matchOnlyStrict(newStateX?.autoPlayers, new Set([Player.X]));
+
+ const newStateO = oldState?.withAutoPlayer(Player.O);
+ t.equal(newStateO?.serialize(), "2x3.O.X.__X|O__");
+ t.matchOnlyStrict(newStateO?.autoPlayers, new Set([Player.O]));
+
+ const newStateXO = newStateX?.withAutoPlayer(Player.O);
+ t.equal(newStateXO?.serialize(), "2x3.XO.X.__X|O__");
+ t.matchOnlyStrict(newStateXO?.autoPlayers, new Set([Player.X, Player.O]));
+
+ const newStateOO = newStateO?.withAutoPlayer(Player.O);
+ t.equal(newStateOO?.serialize(), "2x3.O.X.__X|O__");
+ t.matchOnlyStrict(newStateOO?.autoPlayers, new Set([Player.O]));
+ });
+});
+
+void t.test("withoutAutoPlayer", async (t) => {
+ void t.test("On uninitialized board", async (t) => {
+ const oldState = createAndCheckBoardgameState(t, "2x3.XO..");
+
+ const newStateX = oldState?.withoutAutoPlayer(Player.O);
+ t.equal(newStateX?.serialize(), "2x3.X..");
+ t.matchOnlyStrict(newStateX?.autoPlayers, new Set([Player.X]));
+
+ const newStateO = oldState?.withoutAutoPlayer(Player.X);
+ t.equal(newStateO?.serialize(), "2x3.O..");
+ t.matchOnlyStrict(newStateO?.autoPlayers, new Set([Player.O]));
+
+ const newStateNone = newStateX?.withoutAutoPlayer(Player.X);
+ t.equal(newStateNone?.serialize(), "2x3...");
+ t.matchOnlyStrict(newStateNone?.autoPlayers, new Set());
+
+ const newStateOO = newStateO?.withoutAutoPlayer(Player.X);
+ t.equal(newStateOO?.serialize(), "2x3.O..");
+ t.matchOnlyStrict(newStateOO?.autoPlayers, new Set([Player.O]));
+ });
+
+ void t.test("On 2x3 board", async (t) => {
+ const oldState = createAndCheckBoardgameState(t, "2x3.XO.X.__X|O__");
+
+ const newStateX = oldState?.withoutAutoPlayer(Player.O);
+ t.equal(newStateX?.serialize(), "2x3.X.X.__X|O__");
+ t.matchOnlyStrict(newStateX?.autoPlayers, new Set([Player.X]));
+
+ const newStateO = oldState?.withoutAutoPlayer(Player.X);
+ t.equal(newStateO?.serialize(), "2x3.O.X.__X|O__");
+ t.matchOnlyStrict(newStateO?.autoPlayers, new Set([Player.O]));
+
+ const newStateNone = newStateX?.withoutAutoPlayer(Player.X);
+ t.equal(newStateNone?.serialize(), "2x3..X.__X|O__");
+ t.matchOnlyStrict(newStateNone?.autoPlayers, new Set());
+
+ const newStateOO = newStateO?.withoutAutoPlayer(Player.X);
+ t.equal(newStateOO?.serialize(), "2x3.O.X.__X|O__");
+ t.matchOnlyStrict(newStateOO?.autoPlayers, new Set([Player.O]));
+ });
+});
+
void t.test("withMove", async (t) => {
void t.test("On 2x3 board", async (t) => {
- const oldState = createAndCheckBoardgameState(t, "2x3.X.___|___");
+ const oldState = createAndCheckBoardgameState(t, "2x3..X.___|___");
const firstMoveState = oldState?.withMove(0, 1);
- t.equal(firstMoveState?.serialize(), "2x3.O._X_|___");
+ t.equal(firstMoveState?.serialize(), "2x3..O._X_|___");
t.equal(firstMoveState?.rows, 2);
t.equal(firstMoveState?.columns, 3);
t.equal(firstMoveState?.currentPlayer, Player.O);
@@ -204,7 +332,7 @@ void t.test("withMove", async (t) => {
t.equal(firstMoveState?.board?.serialize(), "_X_|___");
const secondMoveState = firstMoveState?.withMove(1, 2);
- t.equal(secondMoveState?.serialize(), "2x3.X._X_|__O");
+ t.equal(secondMoveState?.serialize(), "2x3..X._X_|__O");
t.equal(secondMoveState?.rows, 2);
t.equal(secondMoveState?.columns, 3);
t.equal(secondMoveState?.currentPlayer, Player.X);
@@ -213,7 +341,7 @@ void t.test("withMove", async (t) => {
});
void t.test("Error handling", async () => {
- const unstartedBoard = createAndCheckBoardgameState(t, "1x1..");
+ const unstartedBoard = createAndCheckBoardgameState(t, "1x1...");
t.throws(() => unstartedBoard?.withMove(0, 0), {
message: "Game is not started",
});
@@ -222,17 +350,17 @@ void t.test("withMove", async (t) => {
void t.test("Sample usage scenario", async (t) => {
const noBoardState = BoardgameState.createWithoutBoard(2, 3);
- t.equal(noBoardState.serialize(), "2x3..");
+ t.equal(noBoardState.serialize(), "2x3...");
const gameStartedState = noBoardState.withEmptyBoard();
- t.equal(gameStartedState.serialize(), "2x3.X.___|___");
+ t.equal(gameStartedState.serialize(), "2x3..X.___|___");
const firstMoveState = gameStartedState.withMove(0, 1);
- t.equal(firstMoveState.serialize(), "2x3.O._X_|___");
+ t.equal(firstMoveState.serialize(), "2x3..O._X_|___");
const secondMoveState = firstMoveState.withMove(1, 2);
- t.equal(secondMoveState.serialize(), "2x3.X._X_|__O");
+ t.equal(secondMoveState.serialize(), "2x3..X._X_|__O");
const thirdMoveState = secondMoveState.withMove(1, 0);
- t.equal(thirdMoveState.serialize(), "2x3.O._X_|X_O");
+ t.equal(thirdMoveState.serialize(), "2x3..O._X_|X_O");
});
diff --git a/src/shared/boardgame-state.ts b/src/shared/boardgame-state.ts
index 61012ed..13c5553 100644
--- a/src/shared/boardgame-state.ts
+++ b/src/shared/boardgame-state.ts
@@ -1,3 +1,4 @@
+import { compact } from "./array-utils.ts";
import { Board } from "./board.ts";
import {
BoardType,
@@ -25,6 +26,16 @@ const parseDimensions = (dimensionsSerialized: string | undefined) => {
return [parseInt(rowsSerialized, 10), parseInt(columnsSerialized, 10)] as const;
};
+const parsePlayers = (playersSerialized: string | undefined) => {
+ if (!playersSerialized) {
+ return new Set();
+ }
+
+ return new Set(compact(playersSerialized.split("").map(parsePlayer)));
+};
+
+const serializePlayers = (players: ReadonlySet) => [...players].map(formatPlayer).join("");
+
const parseBoard = (boardSerialized: string | undefined) => {
if (!boardSerialized) {
return null;
@@ -45,6 +56,7 @@ export class BoardgameState implements BoardgameStateType {
constructor(
readonly rows: number,
readonly columns: number,
+ readonly autoPlayers: ReadonlySet,
readonly currentPlayer: Player | null,
readonly board: BoardType | null,
) {}
@@ -54,11 +66,29 @@ export class BoardgameState implements BoardgameStateType {
}
static createWithoutBoard(rows: number, columns: number) {
- return new BoardgameState(rows, columns, null, null);
+ return new BoardgameState(rows, columns, new Set(), null, null);
}
withEmptyBoard() {
- return new BoardgameState(this.rows, this.columns, FIRST_PLAYER, Board.createEmpty(this.rows, this.columns));
+ return new BoardgameState(
+ this.rows,
+ this.columns,
+ this.autoPlayers,
+ FIRST_PLAYER,
+ Board.createEmpty(this.rows, this.columns),
+ );
+ }
+
+ withAutoPlayer(player: Player) {
+ const autoPlayers = new Set(this.autoPlayers);
+ autoPlayers.add(player);
+ return new BoardgameState(this.rows, this.columns, autoPlayers, this.currentPlayer, this.board);
+ }
+
+ withoutAutoPlayer(player: Player) {
+ const autoPlayers = new Set(this.autoPlayers);
+ autoPlayers.delete(player);
+ return new BoardgameState(this.rows, this.columns, autoPlayers, this.currentPlayer, this.board);
}
withMove(row: number, column: number) {
@@ -68,11 +98,13 @@ export class BoardgameState implements BoardgameStateType {
const nextBoard = this.board.with(row, column, getOccupiedStateByPlayer(this.currentPlayer));
const nextPlayer = getNextPlayer(this.currentPlayer);
- return new BoardgameState(this.rows, this.columns, nextPlayer, nextBoard);
+ return new BoardgameState(this.rows, this.columns, this.autoPlayers, nextPlayer, nextBoard);
}
static fromSerialized(serialized: string | null) {
- const [dimensionsSerialized, currentPlayerSerialized, boardSerialized] = (serialized ?? "").split(".");
+ const [dimensionsSerialized, autoPlayersSerialized, currentPlayerSerialized, boardSerialized] = (
+ serialized ?? ""
+ ).split(".");
const dimensions = parseDimensions(dimensionsSerialized);
if (!dimensions) {
@@ -80,13 +112,14 @@ export class BoardgameState implements BoardgameStateType {
}
const [rows, columns] = dimensions;
+ const autoPlayers = parsePlayers(autoPlayersSerialized);
const currentPlayer = currentPlayerSerialized ? parsePlayer(currentPlayerSerialized) : null;
const board = parseBoard(boardSerialized);
- return new BoardgameState(rows, columns, currentPlayer, board);
+ return new BoardgameState(rows, columns, autoPlayers, currentPlayer, board);
}
serialize() {
- return `${this.rows}x${this.columns}.${this.currentPlayerName}.${serializeBoard(this.board)}`;
+ return `${this.rows}x${this.columns}.${serializePlayers(this.autoPlayers)}.${this.currentPlayerName}.${serializeBoard(this.board)}`;
}
}
diff --git a/src/shared/datatypes.ts b/src/shared/datatypes.ts
index c6ee123..78120d2 100644
--- a/src/shared/datatypes.ts
+++ b/src/shared/datatypes.ts
@@ -42,12 +42,15 @@ export type ExpectedOutcome = {
export type BoardgameStateType = {
readonly rows: number;
readonly columns: number;
+ readonly autoPlayers: ReadonlySet;
readonly currentPlayer: Player | null;
readonly board: BoardType | null;
readonly currentPlayerName: string;
withEmptyBoard(): BoardgameStateType;
+ withAutoPlayer(player: Player): BoardgameStateType;
+ withoutAutoPlayer(player: Player): BoardgameStateType;
withMove(row: number, column: number): BoardgameStateType;
serialize(): string;
};
diff --git a/src/shared/display.ts b/src/shared/display.ts
index 0a08d3e..c8e78a5 100644
--- a/src/shared/display.ts
+++ b/src/shared/display.ts
@@ -1,9 +1,13 @@
-import { BoardgameStateType, CurrentOutcome } from "./datatypes.ts";
+import { BoardgameStateType, CurrentOutcome, Player } from "./datatypes.ts";
export const getDisplayStates = (gameState: BoardgameStateType, currentOutcome: CurrentOutcome) => ({
"outcome-winx": currentOutcome === CurrentOutcome.WinX,
"outcome-wino": currentOutcome === CurrentOutcome.WinO,
"outcome-draw": currentOutcome === CurrentOutcome.Draw,
+ "autoplayer-x-enabled": gameState.autoPlayers.has(Player.X),
+ "autoplayer-x-disabled": !gameState.autoPlayers.has(Player.X),
+ "autoplayer-o-enabled": gameState.autoPlayers.has(Player.O),
+ "autoplayer-o-disabled": !gameState.autoPlayers.has(Player.O),
"start-new-game": !gameState.board,
"start-clear-game": gameState.board,
});