diff --git a/README.md b/README.md index dc5e17b..734f7c7 100644 --- a/README.md +++ b/README.md @@ -29,5 +29,5 @@ Source: https://www.programmfabrik.de/en/assignment-frontend-backend-developer-j ## Time spent * ~0.5 hours to set up the project; -* ~4.5 hours to implement game board serialization, tic-tac-toe rules and game solver (with tests); +* ~5 hours to implement game board serialization, tic-tac-toe rules and game solver (with tests); * ... diff --git a/src/lib/board.ts b/src/lib/board.ts index eff0082..6a0d4a0 100644 --- a/src/lib/board.ts +++ b/src/lib/board.ts @@ -1,7 +1,7 @@ -import { SquareState } from "./datatypes.ts"; +import { BoardType, SquareState } from "./datatypes.ts"; import { unreachable } from "./utils.ts"; -export class Board { +export class Board implements BoardType { // State should be immutable constructor(private readonly state: SquareState[][]) {} diff --git a/src/lib/datatypes.ts b/src/lib/datatypes.ts index 04c64d2..d86dab2 100644 --- a/src/lib/datatypes.ts +++ b/src/lib/datatypes.ts @@ -39,6 +39,10 @@ export type GameRules = { getBoardOutcome(board: BoardType): CurrentOutcome; }; +export type Opponent = { + getNextMove(board: BoardType, currentPlayer: Player): BoardType; +}; + export const getExpectedOutcomeByCurrentOutcome = ( currentOutcome: Exclude, ): ExpectedOutcome => { diff --git a/src/lib/opponent.spec.ts b/src/lib/opponent.spec.ts index 4fd5ee2..14d3aae 100644 --- a/src/lib/opponent.spec.ts +++ b/src/lib/opponent.spec.ts @@ -1,7 +1,7 @@ import t from "tap"; +import { Board } from "./board.ts"; import { ExpectedOutcome, FinalOutcome, Player } from "./datatypes.ts"; import { createOpponent } from "./opponent.ts"; -import { Board } from "./board.ts"; void t.test("createOpponent", async (t) => { const outcomesByBoard: Record = { diff --git a/src/lib/opponent.ts b/src/lib/opponent.ts index e278636..949a21f 100644 --- a/src/lib/opponent.ts +++ b/src/lib/opponent.ts @@ -1,7 +1,7 @@ -import { BoardType, ExpectedOutcome, Player, SquareState, getOccupiedStateByPlayer } from "./datatypes.ts"; +import { ExpectedOutcome, Opponent, SquareState, getOccupiedStateByPlayer } from "./datatypes.ts"; -export const createOpponent = (outcomesByBoard: Map) => { - const getNextMove = (board: BoardType, currentPlayer: Player) => { +export const createOpponent = (outcomesByBoard: Map): Opponent => { + const getNextMove: Opponent["getNextMove"] = (board, currentPlayer) => { const currentExpectedOutcome = outcomesByBoard.get(board.serialize()); if (!currentExpectedOutcome) { throw new Error(`Board is not solved: ${board.serialize()}`); diff --git a/src/lib/tictactoe.test.ts b/src/lib/tictactoe.test.ts index 6866347..7892896 100644 --- a/src/lib/tictactoe.test.ts +++ b/src/lib/tictactoe.test.ts @@ -1,6 +1,8 @@ -import t from "tap"; +import t, { Test } from "tap"; -import { ExpectedOutcome, FinalOutcome } from "./datatypes.ts"; +import { Board } from "./board.ts"; +import { ExpectedOutcome, FinalOutcome, Opponent, Player } from "./datatypes.ts"; +import { createOpponent } from "./opponent.ts"; import { computeAllSolutions } from "./solver.ts"; import { rules } from "./tictactoe-rules.ts"; @@ -142,3 +144,70 @@ void t.test("computeAllSolutions", async (t) => { "OO_|__X|_XX": { finalOutcome: FinalOutcome.WinO, movesLeft: 1 }, }); }); + +void t.test("createOpponent", async (t) => { + const checkNextMove = ( + t: Test, + opponent: Opponent, + currentBoardSerialized: string, + currentPlayer: Player, + expectedNextBoardSerialized: string, + ) => { + t.equal( + opponent.getNextMove(Board.fromSerialized(currentBoardSerialized), currentPlayer).serialize(), + expectedNextBoardSerialized, + ); + }; + + void t.test("1x5 board", async (t) => { + const outcomes = computeAllSolutions(1, 5, rules); + const opponent = createOpponent(outcomes); + + checkNextMove(t, opponent, "_____", Player.X, "X____"); + checkNextMove(t, opponent, "X____", Player.O, "XO___"); + checkNextMove(t, opponent, "XO___", Player.X, "XOX__"); + checkNextMove(t, opponent, "XOX__", Player.O, "XOXO_"); + checkNextMove(t, opponent, "XOXO_", Player.X, "XOXOX"); + + checkNextMove(t, opponent, "__X__", Player.O, "_OX__"); + checkNextMove(t, opponent, "_OX__", Player.X, "XOX__"); + checkNextMove(t, opponent, "XOX__", Player.O, "XOXO_"); + checkNextMove(t, opponent, "XOXO_", Player.X, "XOXOX"); + + checkNextMove(t, opponent, "O_X__", Player.X, "O_XX_"); + checkNextMove(t, opponent, "O_XX_", Player.O, "OOXX_"); + checkNextMove(t, opponent, "OOXX_", Player.X, "OOXXX"); + }); + + void t.test("3x3 board", async (t) => { + const outcomes = computeAllSolutions(3, 3, rules); + const opponent = createOpponent(outcomes); + + checkNextMove(t, opponent, "___|___|___", Player.X, "X__|___|___"); + checkNextMove(t, opponent, "X__|___|___", Player.O, "X__|_O_|___"); + checkNextMove(t, opponent, "X__|_O_|___", Player.X, "XX_|_O_|___"); + checkNextMove(t, opponent, "XX_|_O_|___", Player.O, "XXO|_O_|___"); + checkNextMove(t, opponent, "XXO|_O_|___", Player.X, "XXO|_O_|X__"); + checkNextMove(t, opponent, "XXO|_O_|X__", Player.O, "XXO|OO_|X__"); + checkNextMove(t, opponent, "XXO|OO_|X__", Player.X, "XXO|OOX|X__"); + checkNextMove(t, opponent, "XXO|OOX|X__", Player.O, "XXO|OOX|XO_"); + checkNextMove(t, opponent, "XXO|OOX|XO_", Player.X, "XXO|OOX|XOX"); + + checkNextMove(t, opponent, "___|___|__X", Player.O, "___|_O_|__X"); + checkNextMove(t, opponent, "___|_O_|__X", Player.X, "X__|_O_|__X"); + checkNextMove(t, opponent, "X__|_O_|__X", Player.O, "XO_|_O_|__X"); + checkNextMove(t, opponent, "XO_|_O_|__X", Player.X, "XO_|_O_|_XX"); + checkNextMove(t, opponent, "XO_|_O_|_XX", Player.O, "XO_|_O_|OXX"); + checkNextMove(t, opponent, "XO_|_O_|OXX", Player.X, "XOX|_O_|OXX"); + checkNextMove(t, opponent, "XOX|_O_|OXX", Player.O, "XOX|_OO|OXX"); + checkNextMove(t, opponent, "XOX|_OO|OXX", Player.X, "XOX|XOO|OXX"); + + checkNextMove(t, opponent, "XO_|___|___", Player.X, "XO_|X__|___"); + checkNextMove(t, opponent, "XO_|X__|___", Player.O, "XO_|X__|O__"); + checkNextMove(t, opponent, "XO_|X__|O__", Player.X, "XO_|XX_|O__"); + checkNextMove(t, opponent, "XO_|XX_|O__", Player.O, "XOO|XX_|O__"); + checkNextMove(t, opponent, "XOO|XX_|O__", Player.X, "XOO|XXX|O__"); + + checkNextMove(t, opponent, "XO_|XXO|O__", Player.X, "XO_|XXO|O_X"); + }); +});