From 036917c30e9350c4743d8f9fa9afcdb421081793 Mon Sep 17 00:00:00 2001 From: Ken Riley Date: Sun, 31 May 2026 11:19:06 -0600 Subject: [PATCH] fix: make debug single-game simulator rule-compliant runDebugSingleGame picked the first legal move and broke on any Game.move error, so the must-use-both-dice / must-use-larger-die enforcement made it strand a die, throw, and loop to the turn limit (stuck=true). Gather candidates across all ready dice and skip rule-rejected moves, matching the EngineRunner driver. --- src/scripts/debugSingleGame.ts | 159 +++++++++++++++------------------ 1 file changed, 72 insertions(+), 87 deletions(-) diff --git a/src/scripts/debugSingleGame.ts b/src/scripts/debugSingleGame.ts index fd06a83..5de4cb9 100644 --- a/src/scripts/debugSingleGame.ts +++ b/src/scripts/debugSingleGame.ts @@ -2,7 +2,6 @@ import { BackgammonGameMoved, BackgammonGameMoving, BackgammonGameRollingForStart, - BackgammonMove, BackgammonMoveSkeleton, } from '@nodots/backgammon-types' import { Board, Game, Player } from '..' @@ -138,104 +137,90 @@ export async function runDebugSingleGame() { return false }) ) { - const nextMove = Array.from(gameMoved.activePlay.moves).find((m: any) => { - if (m.stateKind === 'ready' || (m.stateKind === 'in-progress' && !m.origin)) { - const pm = Board.getPossibleMoves(gameMoved.board, m.player, m.dieValue) as - | BackgammonMoveSkeleton[] - | { moves: BackgammonMoveSkeleton[] } - const movesArr = Array.isArray(pm) ? pm : pm.moves - return movesArr.length > 0 - } - return false - }) as BackgammonMove | undefined - - if (!nextMove) { - console.log('\n⚠️ No next move found - game may be stuck!') - break - } - - // Recalculate possible moves for this die value based on current board state - const pm = Board.getPossibleMoves( - gameMoved.board, - (nextMove as any).player, - (nextMove as any).dieValue - ) as BackgammonMoveSkeleton[] | { moves: BackgammonMoveSkeleton[] } - const possibleMoves = Array.isArray(pm) ? pm : pm.moves - - // Take the first valid move that has checkers - let validMove = null - for (const move of possibleMoves) { - const origin = move.origin - const checkers = origin.checkers - if ( - checkers.length > 0 && - checkers[0].color === gameMoved.activeColor - ) { - validMove = move - break + // Gather candidate (checkerId, dieValue) pairs across all ready dice. + // Game.move enforces the must-use-both-dice / must-use-larger-die rules + // by throwing; skip rejected candidates and try the next so the + // simulation plays a rule-compliant move instead of getting stuck. + const readyDice = Array.from(gameMoved.activePlay.moves).filter( + (m: any) => + m.stateKind === 'ready' || + (m.stateKind === 'in-progress' && !m.origin) + ) + const candidates: { checkerId: string; dieValue: any }[] = [] + for (const rm of readyDice) { + const rpm = Board.getPossibleMoves( + gameMoved.board, + (rm as any).player, + (rm as any).dieValue + ) as BackgammonMoveSkeleton[] | { moves: BackgammonMoveSkeleton[] } + const rmoves = Array.isArray(rpm) ? rpm : rpm.moves + for (const mv of rmoves) { + const checker = mv.origin.checkers.find( + (c: any) => c.color === (gameMoved as any).activeColor + ) + if (checker) { + candidates.push({ + checkerId: checker.id, + dieValue: (rm as any).dieValue, + }) + } } } - if (!validMove) { - console.log( - `\n⚠️ No valid moves found for die value ${nextMove.dieValue} - game may be stuck!` - ) + if (candidates.length === 0) { + if (!FAST) console.log('\n⚠️ No valid moves found - game may be stuck!') break } - const origin = validMove.origin - const destination = validMove.destination - - try { - // Ensure proper state transition before move - const gameToMove = gameMoved - const originChecker = origin.checkers.find( - (c: any) => c.color === (gameMoved as any).activeColor - ) - if (!originChecker) { - if (!FAST) console.log('No checker of active color at chosen origin; breaking') + let moved = false + let lastRuleError: any = null + for (const c of candidates) { + try { + const moveResult = Game.move( + gameMoved as BackgammonGameMoving, + c.checkerId, + c.dieValue + ) + if ((moveResult as any).stateKind === 'moved') { + gameMoved = moveResult as BackgammonGameMoved + } else if ('board' in moveResult) { + gameMoved = moveResult as BackgammonGameMoving + moveCount++ + totalMoves++ + + // Display board after this move (skip in FAST mode) + if (!FAST && gameMoved.stateKind === 'moving') { + displayBoard( + gameMoved, + turnCount, + moveCount, + roll, + gameRolled.activeColor, + playerModels + ) + } + } + moved = true break - } - const moveResult = Game.move( - gameToMove as BackgammonGameMoving, - originChecker.id - ) - if ((moveResult as any).stateKind === 'moved') { - gameMoved = moveResult as BackgammonGameMoved - } else if ('board' in moveResult) { - gameMoved = moveResult as BackgammonGameMoving - moveCount++ - totalMoves++ - - // Display board after this move (skip in FAST mode) - if (!FAST && gameMoved.stateKind === 'moving') { - displayBoard( - gameMoved, - turnCount, - moveCount, - roll, - gameRolled.activeColor, - playerModels - ) + } catch (error: any) { + if ( + error?.name === 'MustUseBothDiceError' || + error?.name === 'MustUseLargerDieError' + ) { + lastRuleError = error + continue } + throw error } - } catch (error) { + } + + if (!moved) { if (!FAST) { - console.log(`\n❌ Error making move: ${error}`) - console.log( - `Origin: ${ - origin.kind === 'point' ? origin.position.clockwise : 'bar' - }` - ) console.log( - `Destination: ${ - destination.kind === 'point' - ? destination.position.clockwise - : 'off' - }` + `\n⚠️ All candidate moves rejected${ + lastRuleError ? ` (${lastRuleError.name})` : '' + } - game may be stuck!` ) - console.log(`Game state: ${gameMoved.stateKind}`) - console.log('\n⚠️ Game stuck due to move error!') } break }