implemented autoplay configuration

main
Inga 🏳‍🌈 2 days ago
parent 0b77ff5a1d
commit ce53388d26
  1. 39
      src/backend/components/boardgame.tsx
  2. 4
      src/backend/main/boardgame-handler.ts
  3. 4
      src/shared/array-utils.ts
  4. 202
      src/shared/boardgame-state.spec.ts
  5. 45
      src/shared/boardgame-state.ts
  6. 3
      src/shared/datatypes.ts
  7. 6
      src/shared/display.ts

@ -3,6 +3,7 @@ import {
BoardgameStateType, BoardgameStateType,
CurrentOutcome, CurrentOutcome,
GameRules, GameRules,
Player,
SquareState, SquareState,
formatSquareState, formatSquareState,
} from "../../shared/datatypes.ts"; } from "../../shared/datatypes.ts";
@ -17,6 +18,12 @@ const getClassAndDisplayAttributes = (
style: getDisplayStates(gameState, currentOutcome)[className] ? {} : { display: "none" }, style: getDisplayStates(gameState, currentOutcome)[className] ? {} : { display: "none" },
}); });
const getSubmitAttributes = (key: string, targetState: BoardgameStateType) => ({
type: "submit" as const,
name: key,
value: targetState.serialize(),
});
const getCellHtml = ({ const getCellHtml = ({
key, key,
gameState, gameState,
@ -32,7 +39,7 @@ const getCellHtml = ({
}) => { }) => {
if (!gameState.board) { if (!gameState.board) {
return ( return (
<button type="submit" name={key} value={gameState.serialize()} disabled> <button {...getSubmitAttributes(key, gameState)} disabled>
{" "} {" "}
</button> </button>
); );
@ -44,7 +51,7 @@ const getCellHtml = ({
? gameState.withMove(row, column) ? gameState.withMove(row, column)
: gameState; : gameState;
return ( return (
<button type="submit" name={key} value={nextGameState.serialize()} disabled={nextGameState === gameState}> <button {...getSubmitAttributes(key, nextGameState)} disabled={nextGameState === gameState}>
{formatSquareState(squareState)} {formatSquareState(squareState)}
</button> </button>
); );
@ -76,6 +83,34 @@ export const getBoardgameHtml = (key: string, gameState: BoardgameStateType, rul
<span {...getClassAndDisplayAttributes("outcome-draw", gameState, currentOutcome)}>Draw</span> <span {...getClassAndDisplayAttributes("outcome-draw", gameState, currentOutcome)}>Draw</span>
</p> </p>
<p {...getClassAndDisplayAttributes("autoplayer-x-disabled", gameState, currentOutcome)}>
Currently X moves are made manually.{" "}
<button class="autoplayer-x-enable" {...getSubmitAttributes(key, gameState.withAutoPlayer(Player.X))}>
Make computer play for X.
</button>
</p>
<p {...getClassAndDisplayAttributes("autoplayer-x-enabled", gameState, currentOutcome)}>
Currently X moves are made by computer.{" "}
<button class="autoplayer-x-disable" {...getSubmitAttributes(key, gameState.withoutAutoPlayer(Player.X))}>
Make them manually.
</button>
</p>
<p {...getClassAndDisplayAttributes("autoplayer-o-disabled", gameState, currentOutcome)}>
Currently O moves are made manually.{" "}
<button class="autoplayer-o-enable" {...getSubmitAttributes(key, gameState.withAutoPlayer(Player.O))}>
Make computer play for O.
</button>
</p>
<p {...getClassAndDisplayAttributes("autoplayer-o-enabled", gameState, currentOutcome)}>
Currently O moves are made by computer.{" "}
<button class="autoplayer-o-disable" {...getSubmitAttributes(key, gameState.withoutAutoPlayer(Player.O))}>
Make them manually.
</button>
</p>
<p> <p>
<button type="submit" name={key} value={gameState.withEmptyBoard().serialize()}> <button type="submit" name={key} value={gameState.withEmptyBoard().serialize()}>
<span {...getClassAndDisplayAttributes("start-new-game", gameState, currentOutcome)}>Start game</span> <span {...getClassAndDisplayAttributes("start-new-game", gameState, currentOutcome)}>Start game</span>

@ -4,7 +4,7 @@ import { BoardgameState } from "../../shared/boardgame-state.ts";
import { getBoardgameHtml } from "../components/boardgame.tsx"; import { getBoardgameHtml } from "../components/boardgame.tsx";
import { gamesRules } from "../../shared/rules.ts"; import { gamesRules } from "../../shared/rules.ts";
import { getAllSolutions } from "../../shared/solver-cache.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"; import { createOpponent } from "../../shared/opponent.ts";
// Returns nothing if query parameter is uninitialized and a redirect was made, // 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; return;
} }
if (state.board && state.currentPlayer === Player.O) { if (state.board && state.currentPlayer && state.autoPlayers.has(state.currentPlayer)) {
const currentOutcome = rules.getBoardOutcome(state.board); const currentOutcome = rules.getBoardOutcome(state.board);
if (currentOutcome === CurrentOutcome.Undecided) { if (currentOutcome === CurrentOutcome.Undecided) {
const solutions = getAllSolutions(state.rows, state.columns, rules); const solutions = getAllSolutions(state.rows, state.columns, rules);

@ -1,3 +1,7 @@
export const repeat = <T>(value: T, size: number) => [...(new Array(size) as unknown[])].map(() => value); export const repeat = <T>(value: T, size: number) => [...(new Array(size) as unknown[])].map(() => value);
export const sequence = (size: number) => [...(new Array(size) as unknown[])].map((_, index) => index); export const sequence = (size: number) => [...(new Array(size) as unknown[])].map((_, index) => index);
const isNotEmpty = <T>(value: T): value is Exclude<T, null | undefined> => !!value;
export const compact = <T>(array: T[]) => array.filter(isNotEmpty);

@ -14,33 +14,57 @@ void t.test("Serialize / deserialize", async (t) => {
void t.test("Empty state", async (t) => { void t.test("Empty state", async (t) => {
t.equal(BoardgameState.fromSerialized(""), null); t.equal(BoardgameState.fromSerialized(""), null);
t.equal(BoardgameState.fromSerialized(null), 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) => { void t.test("1x1 state (incompletely serialized)", async (t) => {
const state = BoardgameState.fromSerialized("1x1"); const state = BoardgameState.fromSerialized("1x1");
t.ok(state); t.ok(state);
t.equal(state?.serialize(), "1x1.."); t.equal(state?.serialize(), "1x1...");
t.equal(state?.rows, 1); t.equal(state?.rows, 1);
t.equal(state?.columns, 1); t.equal(state?.columns, 1);
t.matchOnlyStrict(state?.autoPlayers, new Set());
t.equal(state?.currentPlayer, null); t.equal(state?.currentPlayer, null);
t.equal(state?.currentPlayerName, ""); t.equal(state?.currentPlayerName, "");
t.equal(state?.board, null); t.equal(state?.board, null);
}); });
void t.test("1x1 state with empty board", async (t) => { 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?.rows, 1);
t.equal(state?.columns, 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?.currentPlayer, null);
t.equal(state?.currentPlayerName, ""); t.equal(state?.currentPlayerName, "");
t.equal(state?.board, null); t.equal(state?.board, null);
}); });
void t.test("1x1 board with started game", async (t) => { 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?.rows, 1);
t.equal(state?.columns, 1); t.equal(state?.columns, 1);
t.matchOnlyStrict(state?.autoPlayers, new Set());
t.equal(state?.currentPlayer, Player.X); t.equal(state?.currentPlayer, Player.X);
t.equal(state?.currentPlayerName, "X"); t.equal(state?.currentPlayerName, "X");
t.equal(state?.board?.hasRow(0), true); 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) => { 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?.rows, 1);
t.equal(state?.columns, 2); t.equal(state?.columns, 2);
t.matchOnlyStrict(state?.autoPlayers, new Set());
t.equal(state?.currentPlayer, Player.O); t.equal(state?.currentPlayer, Player.O);
t.equal(state?.currentPlayerName, "O"); t.equal(state?.currentPlayerName, "O");
t.equal(state?.board?.get(0, 0), SquareState.Unoccupied); 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) => { 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?.rows, 2);
t.equal(state?.columns, 1); t.equal(state?.columns, 1);
t.matchOnlyStrict(state?.autoPlayers, new Set());
t.equal(state?.currentPlayer, Player.O); t.equal(state?.currentPlayer, Player.O);
t.equal(state?.currentPlayerName, "O"); t.equal(state?.currentPlayerName, "O");
t.equal(state?.board?.get(0, 0), SquareState.Unoccupied); 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?.hasSquare(0, 1), false);
t.equal(state?.board?.hasRow(2), 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("Empty state creation", async (t) => {
void t.test("0x0 board", async (t) => { void t.test("0x0 board", async (t) => {
const state = BoardgameState.createWithoutBoard(0, 0); const state = BoardgameState.createWithoutBoard(0, 0);
t.equal(state.serialize(), "0x0.."); t.equal(state.serialize(), "0x0...");
t.equal(state.rows, 0); t.equal(state.rows, 0);
t.equal(state.columns, 0); t.equal(state.columns, 0);
t.equal(state.currentPlayer, null); t.equal(state.currentPlayer, null);
@ -88,7 +139,7 @@ void t.test("Empty state creation", async (t) => {
void t.test("1x0 board", async (t) => { void t.test("1x0 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 0); const state = BoardgameState.createWithoutBoard(1, 0);
t.equal(state.serialize(), "1x0.."); t.equal(state.serialize(), "1x0...");
t.equal(state.rows, 1); t.equal(state.rows, 1);
t.equal(state.columns, 0); t.equal(state.columns, 0);
t.equal(state.currentPlayer, null); t.equal(state.currentPlayer, null);
@ -98,7 +149,7 @@ void t.test("Empty state creation", async (t) => {
void t.test("1x1 board", async (t) => { void t.test("1x1 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 1); const state = BoardgameState.createWithoutBoard(1, 1);
t.equal(state.serialize(), "1x1.."); t.equal(state.serialize(), "1x1...");
t.equal(state.rows, 1); t.equal(state.rows, 1);
t.equal(state.columns, 1); t.equal(state.columns, 1);
t.equal(state.currentPlayer, null); t.equal(state.currentPlayer, null);
@ -108,26 +159,20 @@ void t.test("Empty state creation", async (t) => {
void t.test("1x2 board", async (t) => { void t.test("1x2 board", async (t) => {
const state = BoardgameState.createWithoutBoard(1, 2); const state = BoardgameState.createWithoutBoard(1, 2);
t.equal(state.serialize(), "1x2.."); t.equal(state.serialize(), "1x2...");
t.equal(state.rows, 1); t.equal(state.rows, 1);
t.equal(state.columns, 2); t.equal(state.columns, 2);
t.equal(state.currentPlayer, null); t.equal(state.currentPlayer, null);
t.equal(state.currentPlayerName, ""); t.equal(state.currentPlayerName, "");
t.equal(state.board, null); 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("withEmptyBoard", async (t) => {
void t.test("On 0x0 board", async (t) => { void t.test("On 0x0 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "0x0.."); const oldState = createAndCheckBoardgameState(t, "0x0...");
const newState = oldState?.withEmptyBoard(); const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "0x0.X."); t.equal(newState?.serialize(), "0x0..X.");
t.equal(newState?.rows, 0); t.equal(newState?.rows, 0);
t.equal(newState?.columns, 0); t.equal(newState?.columns, 0);
t.equal(newState?.currentPlayer, Player.X); t.equal(newState?.currentPlayer, Player.X);
@ -136,9 +181,9 @@ void t.test("withEmptyBoard", async (t) => {
}); });
void t.test("On 0x1 board", async (t) => { void t.test("On 0x1 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "0x1.."); const oldState = createAndCheckBoardgameState(t, "0x1...");
const newState = oldState?.withEmptyBoard(); const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "0x1.X."); t.equal(newState?.serialize(), "0x1..X.");
t.equal(newState?.rows, 0); t.equal(newState?.rows, 0);
t.equal(newState?.columns, 1); t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X); t.equal(newState?.currentPlayer, Player.X);
@ -147,9 +192,9 @@ void t.test("withEmptyBoard", async (t) => {
}); });
void t.test("On 1x0 board", async (t) => { void t.test("On 1x0 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "1x0.."); const oldState = createAndCheckBoardgameState(t, "1x0...");
const newState = oldState?.withEmptyBoard(); const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "1x0.X."); t.equal(newState?.serialize(), "1x0..X.");
t.equal(newState?.rows, 1); t.equal(newState?.rows, 1);
t.equal(newState?.columns, 0); t.equal(newState?.columns, 0);
t.equal(newState?.currentPlayer, Player.X); t.equal(newState?.currentPlayer, Player.X);
@ -158,9 +203,9 @@ void t.test("withEmptyBoard", async (t) => {
}); });
void t.test("On 1x1 board", async (t) => { void t.test("On 1x1 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "1x1.."); const oldState = createAndCheckBoardgameState(t, "1x1...");
const newState = oldState?.withEmptyBoard(); const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "1x1.X._"); t.equal(newState?.serialize(), "1x1..X._");
t.equal(newState?.rows, 1); t.equal(newState?.rows, 1);
t.equal(newState?.columns, 1); t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X); t.equal(newState?.currentPlayer, Player.X);
@ -169,9 +214,9 @@ void t.test("withEmptyBoard", async (t) => {
}); });
void t.test("On 1x2 board", async (t) => { void t.test("On 1x2 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "1x2.."); const oldState = createAndCheckBoardgameState(t, "1x2...");
const newState = oldState?.withEmptyBoard(); const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "1x2.X.__"); t.equal(newState?.serialize(), "1x2..X.__");
t.equal(newState?.rows, 1); t.equal(newState?.rows, 1);
t.equal(newState?.columns, 2); t.equal(newState?.columns, 2);
t.equal(newState?.currentPlayer, Player.X); t.equal(newState?.currentPlayer, Player.X);
@ -180,9 +225,9 @@ void t.test("withEmptyBoard", async (t) => {
}); });
void t.test("On 2x1 board", async (t) => { void t.test("On 2x1 board", async (t) => {
const oldState = createAndCheckBoardgameState(t, "2x1.."); const oldState = createAndCheckBoardgameState(t, "2x1...");
const newState = oldState?.withEmptyBoard(); const newState = oldState?.withEmptyBoard();
t.equal(newState?.serialize(), "2x1.X._|_"); t.equal(newState?.serialize(), "2x1..X._|_");
t.equal(newState?.rows, 2); t.equal(newState?.rows, 2);
t.equal(newState?.columns, 1); t.equal(newState?.columns, 1);
t.equal(newState?.currentPlayer, Player.X); 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("withMove", async (t) => {
void t.test("On 2x3 board", 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); 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?.rows, 2);
t.equal(firstMoveState?.columns, 3); t.equal(firstMoveState?.columns, 3);
t.equal(firstMoveState?.currentPlayer, Player.O); t.equal(firstMoveState?.currentPlayer, Player.O);
@ -204,7 +332,7 @@ void t.test("withMove", async (t) => {
t.equal(firstMoveState?.board?.serialize(), "_X_|___"); t.equal(firstMoveState?.board?.serialize(), "_X_|___");
const secondMoveState = firstMoveState?.withMove(1, 2); 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?.rows, 2);
t.equal(secondMoveState?.columns, 3); t.equal(secondMoveState?.columns, 3);
t.equal(secondMoveState?.currentPlayer, Player.X); t.equal(secondMoveState?.currentPlayer, Player.X);
@ -213,7 +341,7 @@ void t.test("withMove", async (t) => {
}); });
void t.test("Error handling", async () => { void t.test("Error handling", async () => {
const unstartedBoard = createAndCheckBoardgameState(t, "1x1.."); const unstartedBoard = createAndCheckBoardgameState(t, "1x1...");
t.throws(() => unstartedBoard?.withMove(0, 0), { t.throws(() => unstartedBoard?.withMove(0, 0), {
message: "Game is not started", message: "Game is not started",
}); });
@ -222,17 +350,17 @@ void t.test("withMove", async (t) => {
void t.test("Sample usage scenario", async (t) => { void t.test("Sample usage scenario", async (t) => {
const noBoardState = BoardgameState.createWithoutBoard(2, 3); const noBoardState = BoardgameState.createWithoutBoard(2, 3);
t.equal(noBoardState.serialize(), "2x3.."); t.equal(noBoardState.serialize(), "2x3...");
const gameStartedState = noBoardState.withEmptyBoard(); const gameStartedState = noBoardState.withEmptyBoard();
t.equal(gameStartedState.serialize(), "2x3.X.___|___"); t.equal(gameStartedState.serialize(), "2x3..X.___|___");
const firstMoveState = gameStartedState.withMove(0, 1); 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); 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); const thirdMoveState = secondMoveState.withMove(1, 0);
t.equal(thirdMoveState.serialize(), "2x3.O._X_|X_O"); t.equal(thirdMoveState.serialize(), "2x3..O._X_|X_O");
}); });

@ -1,3 +1,4 @@
import { compact } from "./array-utils.ts";
import { Board } from "./board.ts"; import { Board } from "./board.ts";
import { import {
BoardType, BoardType,
@ -25,6 +26,16 @@ const parseDimensions = (dimensionsSerialized: string | undefined) => {
return [parseInt(rowsSerialized, 10), parseInt(columnsSerialized, 10)] as const; return [parseInt(rowsSerialized, 10), parseInt(columnsSerialized, 10)] as const;
}; };
const parsePlayers = (playersSerialized: string | undefined) => {
if (!playersSerialized) {
return new Set<Player>();
}
return new Set(compact(playersSerialized.split("").map(parsePlayer)));
};
const serializePlayers = (players: ReadonlySet<Player>) => [...players].map(formatPlayer).join("");
const parseBoard = (boardSerialized: string | undefined) => { const parseBoard = (boardSerialized: string | undefined) => {
if (!boardSerialized) { if (!boardSerialized) {
return null; return null;
@ -45,6 +56,7 @@ export class BoardgameState implements BoardgameStateType {
constructor( constructor(
readonly rows: number, readonly rows: number,
readonly columns: number, readonly columns: number,
readonly autoPlayers: ReadonlySet<Player>,
readonly currentPlayer: Player | null, readonly currentPlayer: Player | null,
readonly board: BoardType | null, readonly board: BoardType | null,
) {} ) {}
@ -54,11 +66,29 @@ export class BoardgameState implements BoardgameStateType {
} }
static createWithoutBoard(rows: number, columns: number) { static createWithoutBoard(rows: number, columns: number) {
return new BoardgameState(rows, columns, null, null); return new BoardgameState(rows, columns, new Set<Player>(), null, null);
} }
withEmptyBoard() { 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) { 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 nextBoard = this.board.with(row, column, getOccupiedStateByPlayer(this.currentPlayer));
const nextPlayer = getNextPlayer(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) { static fromSerialized(serialized: string | null) {
const [dimensionsSerialized, currentPlayerSerialized, boardSerialized] = (serialized ?? "").split("."); const [dimensionsSerialized, autoPlayersSerialized, currentPlayerSerialized, boardSerialized] = (
serialized ?? ""
).split(".");
const dimensions = parseDimensions(dimensionsSerialized); const dimensions = parseDimensions(dimensionsSerialized);
if (!dimensions) { if (!dimensions) {
@ -80,13 +112,14 @@ export class BoardgameState implements BoardgameStateType {
} }
const [rows, columns] = dimensions; const [rows, columns] = dimensions;
const autoPlayers = parsePlayers(autoPlayersSerialized);
const currentPlayer = currentPlayerSerialized ? parsePlayer(currentPlayerSerialized) : null; const currentPlayer = currentPlayerSerialized ? parsePlayer(currentPlayerSerialized) : null;
const board = parseBoard(boardSerialized); const board = parseBoard(boardSerialized);
return new BoardgameState(rows, columns, currentPlayer, board); return new BoardgameState(rows, columns, autoPlayers, currentPlayer, board);
} }
serialize() { 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)}`;
} }
} }

@ -42,12 +42,15 @@ export type ExpectedOutcome = {
export type BoardgameStateType = { export type BoardgameStateType = {
readonly rows: number; readonly rows: number;
readonly columns: number; readonly columns: number;
readonly autoPlayers: ReadonlySet<Player>;
readonly currentPlayer: Player | null; readonly currentPlayer: Player | null;
readonly board: BoardType | null; readonly board: BoardType | null;
readonly currentPlayerName: string; readonly currentPlayerName: string;
withEmptyBoard(): BoardgameStateType; withEmptyBoard(): BoardgameStateType;
withAutoPlayer(player: Player): BoardgameStateType;
withoutAutoPlayer(player: Player): BoardgameStateType;
withMove(row: number, column: number): BoardgameStateType; withMove(row: number, column: number): BoardgameStateType;
serialize(): string; serialize(): string;
}; };

@ -1,9 +1,13 @@
import { BoardgameStateType, CurrentOutcome } from "./datatypes.ts"; import { BoardgameStateType, CurrentOutcome, Player } from "./datatypes.ts";
export const getDisplayStates = (gameState: BoardgameStateType, currentOutcome: CurrentOutcome) => ({ export const getDisplayStates = (gameState: BoardgameStateType, currentOutcome: CurrentOutcome) => ({
"outcome-winx": currentOutcome === CurrentOutcome.WinX, "outcome-winx": currentOutcome === CurrentOutcome.WinX,
"outcome-wino": currentOutcome === CurrentOutcome.WinO, "outcome-wino": currentOutcome === CurrentOutcome.WinO,
"outcome-draw": currentOutcome === CurrentOutcome.Draw, "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-new-game": !gameState.board,
"start-clear-game": gameState.board, "start-clear-game": gameState.board,
}); });

Loading…
Cancel
Save