From 6f758bf767187db52b7cc80d02a48644768a25e8 Mon Sep 17 00:00:00 2001 From: Ken Riley Date: Wed, 15 Jul 2026 09:08:08 -0600 Subject: [PATCH] refactor(game): extract guards.ts pure predicates (Phase 1 of #132) Move the pure turn/roll predicates canRoll, canRollForStart, canPlayerRoll, and canGetPossibleMoves into src/Game/guards.ts as free functions. The public Game.can* static methods remain as thin delegating wrappers so external callers (ai/api/client) are unchanged. canPlayerRoll calls the module-local canRoll directly (Rule A). Cube predicates (canOffer/Accept/RefuseDouble) and canUndoActivePlay stay behind for cube.ts / undo.ts phases. No behavior change. Core: 448 passed / 12 skipped, tsc -b clean. Refs #132, #133 --- src/Game/guards.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/Game/index.ts | 32 ++++++++++---------------------- 2 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 src/Game/guards.ts diff --git a/src/Game/guards.ts b/src/Game/guards.ts new file mode 100644 index 0000000..263991d --- /dev/null +++ b/src/Game/guards.ts @@ -0,0 +1,45 @@ +import { BackgammonGame } from '@nodots/backgammon-types' + +/** + * Validates if rolling is allowed in the current game state. + */ +export function canRoll(game: BackgammonGame): boolean { + return ( + game.stateKind === 'rolled-for-start' || + game.stateKind === 'rolling' || + game.stateKind === 'doubled' + ) +} + +/** + * Validates if rolling for start is allowed in the current game state. + */ +export function canRollForStart(game: BackgammonGame): boolean { + return game.stateKind === 'rolling-for-start' +} + +/** + * Validates if the specified player can roll in the current game state. + */ +export function canPlayerRoll(game: BackgammonGame, playerId: string): boolean { + if (!canRoll(game)) { + return false + } + + // Check if the player is the active player + if (game.activeColor) { + const activePlayer = game.players.find((p) => p.color === game.activeColor) + if (!activePlayer || activePlayer.id !== playerId) { + return false + } + } + + return true +} + +/** + * Validates if moves can be calculated for the current game state. + */ +export function canGetPossibleMoves(game: BackgammonGame): boolean { + return game.stateKind === 'moving' +} diff --git a/src/Game/index.ts b/src/Game/index.ts index 0e56237..2e37af2 100644 --- a/src/Game/index.ts +++ b/src/Game/index.ts @@ -37,6 +37,12 @@ import { Dice } from '../Dice' import { BackgammonMoveDirection, Play } from '../Play' import type { MoveExecutionOptions } from '../Play' import { debug, logger } from '../utils/logger' +import { + canGetPossibleMoves, + canPlayerRoll, + canRoll, + canRollForStart, +} from './guards' import { createBaseGameProperties, incrementStateVersion } from './shared' export * from '../index' @@ -2004,46 +2010,28 @@ export class Game { * Validates if rolling is allowed in the current game state */ public static canRoll(game: BackgammonGame): boolean { - return ( - game.stateKind === 'rolled-for-start' || - game.stateKind === 'rolling' || - game.stateKind === 'doubled' - ) + return canRoll(game) } /** * Validates if rolling for start is allowed in the current game state */ public static canRollForStart(game: BackgammonGame): boolean { - return game.stateKind === 'rolling-for-start' + return canRollForStart(game) } /** * Validates if the specified player can roll in the current game state */ public static canPlayerRoll(game: BackgammonGame, playerId: string): boolean { - if (!Game.canRoll(game)) { - return false - } - - // Check if the player is the active player - if (game.activeColor) { - const activePlayer = game.players.find( - (p) => p.color === game.activeColor - ) - if (!activePlayer || activePlayer.id !== playerId) { - return false - } - } - - return true + return canPlayerRoll(game, playerId) } /** * Validates if moves can be calculated for the current game state */ public static canGetPossibleMoves(game: BackgammonGame): boolean { - return game.stateKind === 'moving' + return canGetPossibleMoves(game) } // --- Checker Management ---