And here is the code Claude created in one prompt without error (though my guardrails demand unit tests and those are in a second file):
/**
* Pure-function game engine for Othello.
*
* Public interface: createInitialState, makeMove, getValidMoves, isValidMove
* Owner: game engine (src/game/)
*
* Invariants:
* - Board is always 8×8.
* - Black moves first.
* - A move must flip at least one opponent piece.
* - When neither player can move, the game ends.
*/
import type { Board, CellState, GameState, Player, Position, GameStatus } from './types';
const BOARD_SIZE = 8;
/** Eight compass directions as [rowDelta, colDelta]. */
const DIRECTIONS: readonly [number, number][] = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1],
];
/**
* Return the opponent of the given player.
* @param player - Current player
* @returns The opposing player
*/
export function opponent(player: Player): Player {
return player === 'black' ? 'white' : 'black';
}
/**
* Create a fresh 8×8 board with the four centre pieces placed.
* @returns A new Board
*/
function createEmptyBoard(): Board {
const board: Board = Array.from({ length: BOARD_SIZE }, () =>
Array.from<CellState>({ length: BOARD_SIZE }).fill('empty'),
);
// Standard Othello opening: white on d4/e5 diagonal, black on d5/e4.
board[3][3] = 'white';
board[3][4] = 'black';
board[4][3] = 'black';
board[4][4] = 'white';
return board;
}
/**
* Deep-clone a board so mutations don't leak.
* @param board - Board to clone
* @returns Independent copy
*/
function cloneBoard(board: Board): Board {
return board.map(row => [...row]);
}
/**
* Return positions of opponent pieces that would be flipped
* if `player` places a piece at (row, col) along one direction.
* Returns empty array if no flips occur in that direction.
*/
function getFlipsInDirection(
board: Board,
row: number,
col: number,
dRow: number,
dCol: number,
player: Player,
): Position[] {
const opp = opponent(player);
const flips: Position[] = [];
let r = row + dRow;
let c = col + dCol;
// Walk outward, collecting opponent pieces.
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === opp) {
flips.push({ row: r, col: c });
r += dRow;
c += dCol;
}
// The line must terminate with our own piece to count.
if (
flips.length > 0 &&
r >= 0 && r < BOARD_SIZE &&
c >= 0 && c < BOARD_SIZE &&
board[r][c] === player
) {
return flips;
}
return [];
}
/**
* Return all opponent pieces that would be flipped if `player` places at (row, col).
* @param board - Current board state
* @param row - Target row
* @param col - Target column
* @param player - Player making the move
* @returns Array of positions that would be flipped (empty if move is invalid)
*/
export function getAllFlips(board: Board, row: number, col: number, player: Player): Position[] {
if (board[row][col] !== 'empty') return [];
const flips: Position[] = [];
for (const [dRow, dCol] of DIRECTIONS) {
flips.push(...getFlipsInDirection(board, row, col, dRow, dCol, player));
}
return flips;
}
/**
* Check whether placing at (row, col) is a valid move for `player`.
* @param board - Current board state
* @param row - Target row
* @param col - Target column
* @param player - Player to check
* @returns True if the move is legal
*/
export function isValidMove(board: Board, row: number, col: number, player: Player): boolean {
return getAllFlips(board, row, col, player).length > 0;
}
/**
* Return all valid moves for `player` on the given board.
* @param board - Current board state
* @param player - Player to find moves for
* @returns Array of valid positions
*/
export function getValidMoves(board: Board, player: Player): Position[] {
const moves: Position[] = [];
for (let r = 0; r < BOARD_SIZE; r++) {
for (let c = 0; c < BOARD_SIZE; c++) {
if (isValidMove(board, r, c, player)) {
moves.push({ row: r, col: c });
}
}
}
return moves;
}
/**
* Count pieces on the board for each player.
* @param board - Board to count
* @returns Tuple [blackCount, whiteCount]
*/
function countPieces(board: Board): [number, number] {
let black = 0;
let white = 0;
for (let r = 0; r < BOARD_SIZE; r++) {
for (let c = 0; c < BOARD_SIZE; c++) {
if (board[r][c] === 'black') black++;
else if (board[r][c] === 'white') white++;
}
}
return [black, white];
}
/**
* Determine game status from scores and whether any moves remain.
*/
function determineStatus(
blackScore: number,
whiteScore: number,
hasMovesForEither: boolean,
): GameStatus {
if (hasMovesForEither) return 'playing';
if (blackScore > whiteScore) return 'black_wins';
if (whiteScore > blackScore) return 'white_wins';
return 'draw';
}
/**
* Create the initial game state with the standard Othello opening.
* @returns Fresh GameState ready for Black's first move
*/
export function createInitialState(): GameState {
const board = createEmptyBoard();
const validMoves = getValidMoves(board, 'black');
return {
board,
currentPlayer: 'black',
status: 'playing',
blackScore: 2,
whiteScore: 2,
validMoves,
lastMoveWasPass: false,
};
}
/**
* Apply a move and return the resulting game state.
*
* DOES: Places `player`'s piece at (row, col), flips captured pieces,
* advances the turn (skipping if next player has no moves),
* and ends the game if neither player can move.
* WHEN: Called with a valid move position for the current player.
* BECAUSE: This is the core state-transition function — every game
* action flows through it.
* REJECTS WHEN: The position is not a valid move (returns null).
*
* @param state - Current game state
* @param row - Row to place piece
* @param col - Column to place piece
* @returns New GameState after the move, or null if the move is invalid
*/
export function makeMove(state: GameState, row: number, col: number): GameState | null {
const { board, currentPlayer } = state;
const flips = getAllFlips(board, row, col, currentPlayer);
if (flips.length === 0) return null;
// Apply the move on a cloned board.
const newBoard = cloneBoard(board);
newBoard[row][col] = currentPlayer;
for (const { row: fr, col: fc } of flips) {
newBoard[fr][fc] = currentPlayer;
}
// Determine next player, handling pass.
const next = opponent(currentPlayer);
const nextMoves = getValidMoves(newBoard, next);
if (nextMoves.length > 0) {
// Normal turn advance.
const [b, w] = countPieces(newBoard);
return {
board: newBoard,
currentPlayer: next,
status: 'playing',
blackScore: b,
whiteScore: w,
validMoves: nextMoves,
lastMoveWasPass: false,
};
}
// Next player has no moves — check if current player can still go.
const currentMoves = getValidMoves(newBoard, currentPlayer);
const [b, w] = countPieces(newBoard);
if (currentMoves.length > 0) {
// Pass: turn stays with current player.
return {
board: newBoard,
currentPlayer,
status: 'playing',
blackScore: b,
whiteScore: w,
validMoves: currentMoves,
lastMoveWasPass: true,
};
}
// Neither player can move — game over.
return {
board: newBoard,
currentPlayer: next,
status: determineStatus(b, w, false),
blackScore: b,
whiteScore: w,
validMoves: [],
lastMoveWasPass: false,
};
}