improved formatting (line width set to 120)

main
Inga 🏳‍🌈 5 days ago
parent d823eab61f
commit ca311400ea
  1. 1
      .prettierrc
  2. 5
      eslint.config.js
  3. 8
      src/lib/board.ts
  4. 9
      src/lib/datatypes.spec.ts
  5. 35
      src/lib/opponent.spec.ts
  6. 26
      src/lib/opponent.ts
  7. 56
      src/lib/solver.spec.ts
  8. 46
      src/lib/solver.ts
  9. 251
      src/lib/tictactoe-rules.spec.ts
  10. 12
      src/lib/tictactoe-rules.ts

@ -1,4 +1,5 @@
{ {
"printWidth": 120,
"tabWidth": 2, "tabWidth": 2,
"useTabs": false "useTabs": false
} }

@ -18,10 +18,7 @@ export default tsEslint.config(
{ {
rules: { rules: {
"@typescript-eslint/consistent-type-definitions": ["error", "type"], "@typescript-eslint/consistent-type-definitions": ["error", "type"],
"@typescript-eslint/restrict-template-expressions": [ "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true, allowNever: true }],
"error",
{ allowNumber: true, allowNever: true },
],
}, },
}, },
{ {

@ -36,9 +36,7 @@ export class Board {
} }
static createEmpty(rows: number, columns: number) { static createEmpty(rows: number, columns: number) {
const row = [...(new Array(columns) as unknown[])].map( const row = [...(new Array(columns) as unknown[])].map(() => SquareState.Unoccupied);
() => SquareState.Unoccupied,
);
const boardState = [...(new Array(rows) as unknown[])].map(() => row); const boardState = [...(new Array(rows) as unknown[])].map(() => row);
return new Board(boardState); return new Board(boardState);
} }
@ -75,9 +73,7 @@ export class Board {
case SquareState.X: case SquareState.X:
return "X"; return "X";
default: default:
throw new Error( throw new Error(`Unsupported square state: ${unreachable(squareState)}`);
`Unsupported square state: ${unreachable(squareState)}`,
);
} }
}) })
.join(""), .join(""),

@ -31,12 +31,9 @@ void t.test("getExpectedOutcomeByCurrentOutcome", async (t) => {
movesLeft: 0, movesLeft: 0,
}); });
t.throws( t.throws(() => getExpectedOutcomeByCurrentOutcome(CurrentOutcome.Undecided as any), {
() => getExpectedOutcomeByCurrentOutcome(CurrentOutcome.Undecided as any), message: "Unsupported current outcome: 201",
{ });
message: "Unsupported current outcome: 201",
},
);
t.throws(() => getExpectedOutcomeByCurrentOutcome("test" as any), { t.throws(() => getExpectedOutcomeByCurrentOutcome("test" as any), {
message: "Unsupported current outcome: test", message: "Unsupported current outcome: test",

@ -35,12 +35,7 @@ void t.test("createOpponent", async (t) => {
expectedNextBoardSerialized: string, expectedNextBoardSerialized: string,
) => { ) => {
t.equal( t.equal(
opponent opponent.getNextMove(Board.fromSerialized(currentBoardSerialized), currentPlayer).serialize(),
.getNextMove(
Board.fromSerialized(currentBoardSerialized),
currentPlayer,
)
.serialize(),
expectedNextBoardSerialized, expectedNextBoardSerialized,
); );
}; };
@ -48,20 +43,16 @@ void t.test("createOpponent", async (t) => {
checkNextMove("X_|__|__", Player.O, "X_|__|_O"); checkNextMove("X_|__|__", Player.O, "X_|__|_O");
checkNextMove("X_|__|__", Player.X, "XX|__|__"); checkNextMove("X_|__|__", Player.X, "XX|__|__");
t.throws( t.throws(() => opponent.getNextMove(Board.fromSerialized("XO|XO|XO"), Player.X), {
() => opponent.getNextMove(Board.fromSerialized("XO|XO|XO"), Player.X), message: "There are no possible moves left: XO|XO|XO",
{ message: "There are no possible moves left: XO|XO|XO" }, });
); t.throws(() => opponent.getNextMove(Board.fromSerialized("XX|XX|XX"), Player.X), {
t.throws( message: "Board is not solved: XX|XX|XX",
() => opponent.getNextMove(Board.fromSerialized("XX|XX|XX"), Player.X), });
{ message: "Board is not solved: XX|XX|XX" }, t.throws(() => opponent.getNextMove(Board.fromSerialized("XX|XX|X_"), Player.X), {
); message: "Next board is not solved: XX|XX|XX",
t.throws( });
() => opponent.getNextMove(Board.fromSerialized("XX|XX|X_"), Player.X), t.throws(() => opponent.getNextMove(Board.fromSerialized("OO|OO|__"), Player.O), {
{ message: "Next board is not solved: XX|XX|XX" }, message: "Cannot find appropriate next move: OO|OO|__",
); });
t.throws(
() => opponent.getNextMove(Board.fromSerialized("OO|OO|__"), Player.O),
{ message: "Cannot find appropriate next move: OO|OO|__" },
);
}); });

@ -1,14 +1,6 @@
import { import { BoardType, ExpectedOutcome, Player, SquareState, getOccupiedStateByPlayer } from "./datatypes.ts";
BoardType,
ExpectedOutcome,
Player,
SquareState,
getOccupiedStateByPlayer,
} from "./datatypes.ts";
export const createOpponent = ( export const createOpponent = (outcomesByBoard: Map<string, ExpectedOutcome>) => {
outcomesByBoard: Map<string, ExpectedOutcome>,
) => {
const getNextMove = (board: BoardType, currentPlayer: Player) => { const getNextMove = (board: BoardType, currentPlayer: Player) => {
const currentExpectedOutcome = outcomesByBoard.get(board.serialize()); const currentExpectedOutcome = outcomesByBoard.get(board.serialize());
if (!currentExpectedOutcome) { if (!currentExpectedOutcome) {
@ -24,20 +16,14 @@ export const createOpponent = (
for (let column = 0; board.hasSquare(row, column); column++) { for (let column = 0; board.hasSquare(row, column); column++) {
if (board.get(row, column) === SquareState.Unoccupied) { if (board.get(row, column) === SquareState.Unoccupied) {
const nextBoard = board.with(row, column, occupiedState); const nextBoard = board.with(row, column, occupiedState);
const nextExpectedOutcome = outcomesByBoard.get( const nextExpectedOutcome = outcomesByBoard.get(nextBoard.serialize());
nextBoard.serialize(),
);
if (!nextExpectedOutcome) { if (!nextExpectedOutcome) {
throw new Error( throw new Error(`Next board is not solved: ${nextBoard.serialize()}`);
`Next board is not solved: ${nextBoard.serialize()}`,
);
} }
if ( if (
nextExpectedOutcome.finalOutcome === nextExpectedOutcome.finalOutcome === currentExpectedOutcome.finalOutcome &&
currentExpectedOutcome.finalOutcome && nextExpectedOutcome.movesLeft === currentExpectedOutcome.movesLeft - 1
nextExpectedOutcome.movesLeft ===
currentExpectedOutcome.movesLeft - 1
) { ) {
return nextBoard; return nextBoard;
} }

@ -60,35 +60,23 @@ void t.test("getPreferredNextOutcome", async (t) => {
movesLeft: 140, movesLeft: 140,
}); });
t.matchOnlyStrict( t.matchOnlyStrict(getPreferredNextOutcome(nextOutcomes.slice(0, 5), Player.X), {
getPreferredNextOutcome(nextOutcomes.slice(0, 5), Player.X), finalOutcome: FinalOutcome.WinX,
{ movesLeft: 60,
finalOutcome: FinalOutcome.WinX, });
movesLeft: 60, t.matchOnlyStrict(getPreferredNextOutcome(nextOutcomes.slice(0, 5), Player.O), {
}, finalOutcome: FinalOutcome.Draw,
); movesLeft: 150,
t.matchOnlyStrict( });
getPreferredNextOutcome(nextOutcomes.slice(0, 5), Player.O),
{ t.matchOnlyStrict(getPreferredNextOutcome(nextOutcomes.slice(0, 3), Player.X), {
finalOutcome: FinalOutcome.Draw, finalOutcome: FinalOutcome.Draw,
movesLeft: 150, movesLeft: 150,
}, });
); t.matchOnlyStrict(getPreferredNextOutcome(nextOutcomes.slice(0, 3), Player.O), {
finalOutcome: FinalOutcome.Draw,
t.matchOnlyStrict( movesLeft: 150,
getPreferredNextOutcome(nextOutcomes.slice(0, 3), Player.X), });
{
finalOutcome: FinalOutcome.Draw,
movesLeft: 150,
},
);
t.matchOnlyStrict(
getPreferredNextOutcome(nextOutcomes.slice(0, 3), Player.O),
{
finalOutcome: FinalOutcome.Draw,
movesLeft: 150,
},
);
t.throws(() => getPreferredNextOutcome([], Player.X), { t.throws(() => getPreferredNextOutcome([], Player.X), {
message: "Could not find the best outcome out of 0 possible next outcomes", message: "Could not find the best outcome out of 0 possible next outcomes",
@ -101,10 +89,7 @@ void t.test("computeAllSolutions", async (t) => {
columns: number, columns: number,
expectedSolutions: Record<string, ExpectedOutcome>, expectedSolutions: Record<string, ExpectedOutcome>,
) => { ) => {
t.matchOnlyStrict( t.matchOnlyStrict(Object.fromEntries(computeAllSolutions(rows, columns, rules).entries()), expectedSolutions);
Object.fromEntries(computeAllSolutions(rows, columns, rules).entries()),
expectedSolutions,
);
}; };
const checkSolutionsIncomplete = ( const checkSolutionsIncomplete = (
@ -115,10 +100,7 @@ void t.test("computeAllSolutions", async (t) => {
) => { ) => {
const allSolutions = computeAllSolutions(rows, columns, rules); const allSolutions = computeAllSolutions(rows, columns, rules);
t.equal(allSolutions.size, expectedSolutionsCount); t.equal(allSolutions.size, expectedSolutionsCount);
t.matchStrict( t.matchStrict(Object.fromEntries(allSolutions.entries()), expectedSolutionsIncomplete);
Object.fromEntries(allSolutions.entries()),
expectedSolutionsIncomplete,
);
}; };
checkSolutionsComplete(0, 0, { checkSolutionsComplete(0, 0, {

@ -24,10 +24,7 @@ export const getPreferredNextOutcome = (
for (const nextOutcome of possibleNextOutcomes) { for (const nextOutcome of possibleNextOutcomes) {
if (nextOutcome.finalOutcome === desiredFinalOutcome) { if (nextOutcome.finalOutcome === desiredFinalOutcome) {
// If there are multiple winning options, give preference to the one that wins in less moves // If there are multiple winning options, give preference to the one that wins in less moves
if ( if (!bestDesiredNextOutcome || bestDesiredNextOutcome.movesLeft > nextOutcome.movesLeft) {
!bestDesiredNextOutcome ||
bestDesiredNextOutcome.movesLeft > nextOutcome.movesLeft
) {
bestDesiredNextOutcome = nextOutcome; bestDesiredNextOutcome = nextOutcome;
} }
} }
@ -39,21 +36,14 @@ export const getPreferredNextOutcome = (
} }
{ {
const leastDesiredFinalOutcome = const leastDesiredFinalOutcome = getUndesiredFinalOutcomeByPlayer(currentPlayer);
getUndesiredFinalOutcomeByPlayer(currentPlayer); for (const undesiredFinalOutcome of [FinalOutcome.Draw, leastDesiredFinalOutcome]) {
for (const undesiredFinalOutcome of [
FinalOutcome.Draw,
leastDesiredFinalOutcome,
]) {
let leastBadUndesiredNextOutcome: ExpectedOutcome | null = null; let leastBadUndesiredNextOutcome: ExpectedOutcome | null = null;
for (const nextOutcome of possibleNextOutcomes) { for (const nextOutcome of possibleNextOutcomes) {
if (nextOutcome.finalOutcome === undesiredFinalOutcome) { if (nextOutcome.finalOutcome === undesiredFinalOutcome) {
// If there are multiple draw options, or multiple losing options, // If there are multiple draw options, or multiple losing options,
// give preference to the one that protracts for most moves // give preference to the one that protracts for most moves
if ( if (!leastBadUndesiredNextOutcome || leastBadUndesiredNextOutcome.movesLeft < nextOutcome.movesLeft) {
!leastBadUndesiredNextOutcome ||
leastBadUndesiredNextOutcome.movesLeft < nextOutcome.movesLeft
) {
leastBadUndesiredNextOutcome = nextOutcome; leastBadUndesiredNextOutcome = nextOutcome;
} }
} }
@ -65,22 +55,13 @@ export const getPreferredNextOutcome = (
} }
} }
throw new Error( throw new Error(`Could not find the best outcome out of ${possibleNextOutcomes.length} possible next outcomes`);
`Could not find the best outcome out of ${possibleNextOutcomes.length} possible next outcomes`,
);
}; };
export const computeAllSolutions = ( export const computeAllSolutions = (rows: number, columns: number, rules: GameRules) => {
rows: number,
columns: number,
rules: GameRules,
) => {
const expectedOutcomesByBoard = new Map<string, ExpectedOutcome>(); const expectedOutcomesByBoard = new Map<string, ExpectedOutcome>();
const getExpectedOutcomeForBoard = ( const getExpectedOutcomeForBoard = (board: BoardType, currentPlayer: Player): ExpectedOutcome => {
board: BoardType,
currentPlayer: Player,
): ExpectedOutcome => {
// assuming that currentPlayer is always the same for the same board // assuming that currentPlayer is always the same for the same board
// (which is true, because for correct board states, currentPlayer is determined by // (which is true, because for correct board states, currentPlayer is determined by
// parity of the number of occupied cells) // parity of the number of occupied cells)
@ -98,8 +79,7 @@ export const computeAllSolutions = (
{ {
const currentOutcome = rules.getBoardOutcome(board); const currentOutcome = rules.getBoardOutcome(board);
if (currentOutcome !== CurrentOutcome.Undecided) { if (currentOutcome !== CurrentOutcome.Undecided) {
const expectedOutcome = const expectedOutcome = getExpectedOutcomeByCurrentOutcome(currentOutcome);
getExpectedOutcomeByCurrentOutcome(currentOutcome);
expectedOutcomesByBoard.set(key, expectedOutcome); expectedOutcomesByBoard.set(key, expectedOutcome);
return expectedOutcome; return expectedOutcome;
} }
@ -114,20 +94,14 @@ export const computeAllSolutions = (
if (board.get(row, column) === SquareState.Unoccupied) { if (board.get(row, column) === SquareState.Unoccupied) {
// Recursion is not a problem here, because every time the number of occupied squares increases, // Recursion is not a problem here, because every time the number of occupied squares increases,
// therefore the depth of recursion is limited by the number of originally free squares // therefore the depth of recursion is limited by the number of originally free squares
const nextOutcome = getExpectedOutcomeForBoard( const nextOutcome = getExpectedOutcomeForBoard(board.with(row, column, occupiedState), nextPlayer);
board.with(row, column, occupiedState),
nextPlayer,
);
possibleNextOutcomes.push(nextOutcome); possibleNextOutcomes.push(nextOutcome);
} }
} }
} }
} }
const preferredNextOutcome = getPreferredNextOutcome( const preferredNextOutcome = getPreferredNextOutcome(possibleNextOutcomes, currentPlayer);
possibleNextOutcomes,
currentPlayer,
);
const expectedOutcome: ExpectedOutcome = { const expectedOutcome: ExpectedOutcome = {
finalOutcome: preferredNextOutcome.finalOutcome, finalOutcome: preferredNextOutcome.finalOutcome,
movesLeft: preferredNextOutcome.movesLeft + 1, movesLeft: preferredNextOutcome.movesLeft + 1,

@ -16,24 +16,15 @@ void t.test("getSequenceOutcome", async (t) => {
}); });
void t.test("all sequences of length 2", async (t) => { void t.test("all sequences of length 2", async (t) => {
t.equal( t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.Unoccupied]), null);
getSequenceOutcome([SquareState.Unoccupied, SquareState.Unoccupied]),
null,
);
t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.X]), null); t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.X]), null);
t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.O]), null); t.equal(getSequenceOutcome([SquareState.Unoccupied, SquareState.O]), null);
t.equal(getSequenceOutcome([SquareState.X, SquareState.Unoccupied]), null); t.equal(getSequenceOutcome([SquareState.X, SquareState.Unoccupied]), null);
t.equal( t.equal(getSequenceOutcome([SquareState.X, SquareState.X]), CurrentOutcome.WinX);
getSequenceOutcome([SquareState.X, SquareState.X]),
CurrentOutcome.WinX,
);
t.equal(getSequenceOutcome([SquareState.X, SquareState.O]), null); t.equal(getSequenceOutcome([SquareState.X, SquareState.O]), null);
t.equal(getSequenceOutcome([SquareState.O, SquareState.Unoccupied]), null); t.equal(getSequenceOutcome([SquareState.O, SquareState.Unoccupied]), null);
t.equal(getSequenceOutcome([SquareState.O, SquareState.X]), null); t.equal(getSequenceOutcome([SquareState.O, SquareState.X]), null);
t.equal( t.equal(getSequenceOutcome([SquareState.O, SquareState.O]), CurrentOutcome.WinO);
getSequenceOutcome([SquareState.O, SquareState.O]),
CurrentOutcome.WinO,
);
}); });
void t.test("sequences of length 7", async (t) => { void t.test("sequences of length 7", async (t) => {
@ -135,211 +126,69 @@ void t.test("getSequenceOutcome", async (t) => {
void t.test("getBoardOutcome", async (t) => { void t.test("getBoardOutcome", async (t) => {
void t.test("1x1 boards", async (t) => { void t.test("1x1 boards", async (t) => {
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("_")), CurrentOutcome.Undecided);
getBoardOutcome(Board.fromSerialized("_")),
CurrentOutcome.Undecided,
);
t.equal(getBoardOutcome(Board.fromSerialized("X")), CurrentOutcome.Draw); t.equal(getBoardOutcome(Board.fromSerialized("X")), CurrentOutcome.Draw);
t.equal(getBoardOutcome(Board.fromSerialized("O")), CurrentOutcome.Draw); t.equal(getBoardOutcome(Board.fromSerialized("O")), CurrentOutcome.Draw);
}); });
void t.test("2x2 boards", async (t) => { void t.test("2x2 boards", async (t) => {
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("__|__")), CurrentOutcome.Undecided);
getBoardOutcome(Board.fromSerialized("__|__")), t.equal(getBoardOutcome(Board.fromSerialized("XX|X_")), CurrentOutcome.Undecided);
CurrentOutcome.Undecided, t.equal(getBoardOutcome(Board.fromSerialized("XX|XX")), CurrentOutcome.Draw);
); t.equal(getBoardOutcome(Board.fromSerialized("OO|OO")), CurrentOutcome.Draw);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("XO|OX")), CurrentOutcome.Draw);
getBoardOutcome(Board.fromSerialized("XX|X_")),
CurrentOutcome.Undecided,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XX|XX")),
CurrentOutcome.Draw,
);
t.equal(
getBoardOutcome(Board.fromSerialized("OO|OO")),
CurrentOutcome.Draw,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XO|OX")),
CurrentOutcome.Draw,
);
}); });
void t.test("3x3 boards", async (t) => { void t.test("3x3 boards", async (t) => {
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("___|___|___")), CurrentOutcome.Undecided);
getBoardOutcome(Board.fromSerialized("___|___|___")), t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|___")), CurrentOutcome.Undecided);
CurrentOutcome.Undecided,
); t.equal(getBoardOutcome(Board.fromSerialized("XX_|___|___")), CurrentOutcome.Undecided);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("___|___|_XX")), CurrentOutcome.Undecided);
getBoardOutcome(Board.fromSerialized("___|_X_|___")), t.equal(getBoardOutcome(Board.fromSerialized("X__|X__|___")), CurrentOutcome.Undecided);
CurrentOutcome.Undecided, t.equal(getBoardOutcome(Board.fromSerialized("___|__X|__X")), CurrentOutcome.Undecided);
); t.equal(getBoardOutcome(Board.fromSerialized("X__|_X_|___")), CurrentOutcome.Undecided);
t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|__X")), CurrentOutcome.Undecided);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("__X|_X_|___")), CurrentOutcome.Undecided);
getBoardOutcome(Board.fromSerialized("XX_|___|___")), t.equal(getBoardOutcome(Board.fromSerialized("___|_X_|X__")), CurrentOutcome.Undecided);
CurrentOutcome.Undecided,
); t.equal(getBoardOutcome(Board.fromSerialized("XXX|O_O|O_O")), CurrentOutcome.WinX);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("O_O|O_O|XXX")), CurrentOutcome.WinX);
getBoardOutcome(Board.fromSerialized("___|___|_XX")), t.equal(getBoardOutcome(Board.fromSerialized("XOO|X__|XOO")), CurrentOutcome.WinX);
CurrentOutcome.Undecided, t.equal(getBoardOutcome(Board.fromSerialized("OOX|__X|OOX")), CurrentOutcome.WinX);
); t.equal(getBoardOutcome(Board.fromSerialized("XOO|OXO|OOX")), CurrentOutcome.WinX);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("OOX|OXO|XOO")), CurrentOutcome.WinX);
getBoardOutcome(Board.fromSerialized("X__|X__|___")),
CurrentOutcome.Undecided, t.equal(getBoardOutcome(Board.fromSerialized("XXO|XOX|OXX")), CurrentOutcome.WinO);
);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OXO")), CurrentOutcome.Draw);
getBoardOutcome(Board.fromSerialized("___|__X|__X")), t.equal(getBoardOutcome(Board.fromSerialized("XOX|XXO|OX_")), CurrentOutcome.Undecided);
CurrentOutcome.Undecided,
);
t.equal(
getBoardOutcome(Board.fromSerialized("X__|_X_|___")),
CurrentOutcome.Undecided,
);
t.equal(
getBoardOutcome(Board.fromSerialized("___|_X_|__X")),
CurrentOutcome.Undecided,
);
t.equal(
getBoardOutcome(Board.fromSerialized("__X|_X_|___")),
CurrentOutcome.Undecided,
);
t.equal(
getBoardOutcome(Board.fromSerialized("___|_X_|X__")),
CurrentOutcome.Undecided,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XXX|O_O|O_O")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("O_O|O_O|XXX")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XOO|X__|XOO")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("OOX|__X|OOX")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XOO|OXO|OOX")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("OOX|OXO|XOO")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XXO|XOX|OXX")),
CurrentOutcome.WinO,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XOX|XXO|OXO")),
CurrentOutcome.Draw,
);
t.equal(
getBoardOutcome(Board.fromSerialized("XOX|XXO|OX_")),
CurrentOutcome.Undecided,
);
}); });
void t.test("5x5 boards", async (t) => { void t.test("5x5 boards", async (t) => {
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____")), CurrentOutcome.Undecided);
getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____")), t.equal(getBoardOutcome(Board.fromSerialized("XXX__|_____|_____|_____|_____")), CurrentOutcome.WinX);
CurrentOutcome.Undecided, t.equal(getBoardOutcome(Board.fromSerialized("X____|X____|X____|_____|_____")), CurrentOutcome.WinX);
); t.equal(getBoardOutcome(Board.fromSerialized("X____|_X___|__X__|_____|_____")), CurrentOutcome.WinX);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("__X__|_X___|X____|_____|_____")), CurrentOutcome.WinX);
getBoardOutcome(Board.fromSerialized("XXX__|_____|_____|_____|_____")),
CurrentOutcome.WinX, t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|__XXX")), CurrentOutcome.WinX);
); t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|____X|____X|____X")), CurrentOutcome.WinX);
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|__X__|___X_|____X")), CurrentOutcome.WinX);
getBoardOutcome(Board.fromSerialized("X____|X____|X____|_____|_____")), t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|____X|___X_|__X__")), CurrentOutcome.WinX);
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("X____|_X___|__X__|_____|_____")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("__X__|_X___|X____|_____|_____")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|__XXX")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("_____|_____|____X|____X|____X")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("_____|_____|__X__|___X_|____X")),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(Board.fromSerialized("_____|_____|____X|___X_|__X__")),
CurrentOutcome.WinX,
);
}); });
void t.test("6x5 boards", async (t) => { void t.test("6x5 boards", async (t) => {
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|_____|_____|__XXX")), CurrentOutcome.WinX);
getBoardOutcome( t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|____X|____X|____X")), CurrentOutcome.WinX);
Board.fromSerialized("_____|_____|_____|_____|_____|__XXX"), t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|__X__|___X_|____X")), CurrentOutcome.WinX);
), t.equal(getBoardOutcome(Board.fromSerialized("_____|_____|_____|____X|___X_|__X__")), CurrentOutcome.WinX);
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(
Board.fromSerialized("_____|_____|_____|____X|____X|____X"),
),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(
Board.fromSerialized("_____|_____|_____|__X__|___X_|____X"),
),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(
Board.fromSerialized("_____|_____|_____|____X|___X_|__X__"),
),
CurrentOutcome.WinX,
);
}); });
void t.test("5x6 boards", async (t) => { void t.test("5x6 boards", async (t) => {
t.equal( t.equal(getBoardOutcome(Board.fromSerialized("______|______|______|______|___XXX")), CurrentOutcome.WinX);
getBoardOutcome( t.equal(getBoardOutcome(Board.fromSerialized("______|______|_____X|_____X|_____X")), CurrentOutcome.WinX);
Board.fromSerialized("______|______|______|______|___XXX"), t.equal(getBoardOutcome(Board.fromSerialized("______|______|___X__|____X_|_____X")), CurrentOutcome.WinX);
), t.equal(getBoardOutcome(Board.fromSerialized("______|______|_____X|____X_|___X__")), CurrentOutcome.WinX);
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(
Board.fromSerialized("______|______|_____X|_____X|_____X"),
),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(
Board.fromSerialized("______|______|___X__|____X_|_____X"),
),
CurrentOutcome.WinX,
);
t.equal(
getBoardOutcome(
Board.fromSerialized("______|______|_____X|____X_|___X__"),
),
CurrentOutcome.WinX,
);
}); });
}); });

@ -20,11 +20,7 @@ export const getSequenceOutcome = (sequence: SquareState[]) => {
export const getBoardOutcome: GameRules["getBoardOutcome"] = (board) => { export const getBoardOutcome: GameRules["getBoardOutcome"] = (board) => {
for (let row = 0; board.hasRow(row); row++) { for (let row = 0; board.hasRow(row); row++) {
for (let column = 2; board.hasSquare(row, column); column++) { for (let column = 2; board.hasSquare(row, column); column++) {
const tripleLeft = [ const tripleLeft = [board.get(row, column - 2), board.get(row, column - 1), board.get(row, column - 0)];
board.get(row, column - 2),
board.get(row, column - 1),
board.get(row, column - 0),
];
const outcome = getSequenceOutcome(tripleLeft); const outcome = getSequenceOutcome(tripleLeft);
if (outcome) { if (outcome) {
@ -35,11 +31,7 @@ export const getBoardOutcome: GameRules["getBoardOutcome"] = (board) => {
for (let row = 2; board.hasRow(row); row++) { for (let row = 2; board.hasRow(row); row++) {
for (let column = 0; board.hasSquare(row, column); column++) { for (let column = 0; board.hasSquare(row, column); column++) {
const tripleUp = [ const tripleUp = [board.get(row - 2, column), board.get(row - 1, column), board.get(row - 0, column)];
board.get(row - 2, column),
board.get(row - 1, column),
board.get(row - 0, column),
];
const outcome = getSequenceOutcome(tripleUp); const outcome = getSequenceOutcome(tripleUp);
if (outcome) { if (outcome) {

Loading…
Cancel
Save