Skip to content

refactor(game): land decomp phases 2-9 on development (recovery for #132/#134/#96)#145

Merged
nodots merged 13 commits into
developmentfrom
refactor/game-decomp-phase7-facade
Jul 15, 2026
Merged

refactor(game): land decomp phases 2-9 on development (recovery for #132/#134/#96)#145
nodots merged 13 commits into
developmentfrom
refactor/game-decomp-phase7-facade

Conversation

@nodots

@nodots nodots commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What happened

The Game/index.ts decomposition (#132) landed as a stacked PR chain #135-#144. All ten show MERGED, but only #135 (phase 0) and #136 (phase 1) had base = development. PRs #137-#144 had base = the branch below them in the stack, so merging them only advanced the intermediate feature branches. Phases 2-9 never reached development.

State before this PR on development: Game/index.ts still 2448 lines; only shared.ts + guards.ts extracted.

What this PR does

Lands the remaining commits (phases 2-9) onto development. This is not new work; it is the already-reviewed content of the merged-but-stranded PRs #137-#144. This branch's tree is identical to the intended final decomposition tip.

Net effect:

Recovers stranded PRs

#137 phase 2 undo, #138 phase 3 cube, #139 phase 4 lifecycle, #140 phase 5 turnFlow, #141 phase 6 robot, #142 phase 7 facade, #143 phase 8 coverage, #144 phase 9 casts.

Merge instructions

Use a merge commit (do NOT squash) to preserve the individual phase commits.

nodots added 13 commits July 15, 2026 09:10
Lock in current behavior of Game.canUndoActivePlay and
Game.undoLastInActivePlay before refactoring: defensive/guard branches
and the real-move happy path (snapshot push -> undo pop). 10 tests.

Refs #132, #134
Move undoLastInActivePlay and canUndoActivePlay into src/Game/undo.ts as
free functions. Now that types promotes undo.frames to a typed
BackgammonGameMoving[] on BasePlay, the functions read
game.activePlay.undo.frames directly with no 'as any' casts (removes 5
casts from the undo slice of #96). The public Game.* statics remain as
thin delegating wrappers; canUndoActivePlay (deferred from guards) lands
here with its concern.

Depends on backgammon-types refactor/undo-frames-typed.
Characterization tests (10) added in the prior commit stay green.

No behavior change. Core: 458 passed / 12 skipped, tsc -b clean.

Refs #132, #96
Cover the gaps not already exercised by crawford-jacoby-rules.test.ts:
Game.double output shape, canAcceptDouble/acceptDouble cube transfer,
and Game.resign (simple/gammon scoring, Jacoby reduction, disabled-resign
and already-completed guards). 10 tests.

Refs #132, #134
Move the cube/doubling and resign logic into src/Game/cube.ts as free
functions: canOfferDouble, canAcceptDouble, acceptDouble, canRefuseDouble,
refuseDouble, resign, double. The public Game.* statics remain thin
delegating wrappers so external callers (ai/api/client) are unchanged.
Internal cross-calls go module-local (Rule A): acceptDouble->canAcceptDouble,
canRefuseDouble->canAcceptDouble, refuseDouble->canRefuseDouble,
double->canOfferDouble.

The cube-slice predicates (canOfferDouble/canAcceptDouble/canRefuseDouble)
land here with their concern, as deferred from the guards phase.

Faithful move: pre-existing casts preserved verbatim (cube-slice #96
cast removal is out of scope for this phase). Characterization tests (10)
added in the prior commit stay green.

No behavior change. Core: 468 passed / 12 skipped, tsc -b clean.

Refs #132, #134
restoreState had no coverage. Lock in all validation guard branches
(null state, missing stateKind/board/cube, wrong player count,
non-restorable stateKind) and the valid-state passthrough. 7 tests.
rollForStart is already well-covered by existing suites.

Refs #132, #134
#132)

Move the standalone lifecycle transitions rollForStart and restoreState
into src/Game/lifecycle.ts as free functions. Public Game.* statics
remain thin delegating wrappers (external ai/api/client callers
unchanged). rollForStart self-recurses on a tie via the module-local
function (Rule A). Removed two now-dead imports from index.ts
(RESTORABLE_GAME_STATE_KINDS, BackgammonPlayerRollingForStart).

Game.initialize and Game.createNewGame stay on the class: they drive the
new Game() constructor path (PositionReconstructor, gnuPositionId getter)
and the overloaded initialize signature.

Faithful move; pre-existing casts preserved. Characterization tests for
restoreState (7) added in the prior commit stay green.

No behavior change. Core: 475 passed / 12 skipped, tsc -b clean.

Refs #132, #134
Move the turn-progression logic into src/Game/turnFlow.ts as free
functions: roll, switchDice, move, moveAndFinalize, toMoved,
executeAndRecalculate, checkAndCompleteTurn, confirmTurn, startMove,
and getPlayersForColor (used by roll). The public Game.* statics now
delegate via 'public static X = X' to the imported free functions, so
external callers (ai/api/client) and their type signatures are
unchanged. Internal cross-calls go module-local (Rule A):
roll->getPlayersForColor, moveAndFinalize->move/checkAndCompleteTurn,
executeAndRecalculate->move/checkAndCompleteTurn/confirmTurn,
checkAndCompleteTurn->toMoved.

The player accessors activePlayer/inactivePlayer and the robot helpers
(handleRobotMovedState, executeRobotTurn) stay on the class;
handleRobotMovedState still reaches confirmTurn through the facade.

Byte-exact relocation (mechanical extraction script) preserves behavior;
the test suite is identical before and after. Removed now-dead value
imports (Dice, Play, MoveExecutionOptions) from index.ts. index.ts drops
~1500 lines (~2050 -> 586).

Note: switchDice coverage (weak, ~#134) is left as a follow-up coverage
pass; this phase is a behavior-preserving move.

No behavior change. Core: 475 passed / 12 skipped, tsc -b clean.

Refs #132
Move handleRobotMovedState and confirmTurnWithRobotAutomation into
src/Game/robot.ts as free functions. Both call confirmTurn imported from
turnFlow (Rule A: no facade round-trip). Public Game.* statics delegate
via 'public static X = X'. executeRobotTurn stays as its existing
re-export from ./executeRobotTurn.

Removed now-dead imports from index.ts (debug, BackgammonGameMoved).

Faithful move; behavior unchanged. Core: 475 passed / 12 skipped,
tsc -b clean.

Refs #132
…132)

After the module extractions, index.ts is a thin facade over
shared/guards/undo/cube/lifecycle/turnFlow/robot. Remove type imports
left dead by the moves: BackgammonCubeValue, BackgammonDieValue,
BackgammonMoveSkeleton, BackgammonPlayerDoubled, BackgammonPlayerWinner,
BackgammonRoll, BackgammonPlayersMovingTuple, BackgammonPlayersRollingTuple.

Downstream guard: rebuilt core + typechecked consumers against local
core -- ai (tsc -b) clean, api (tsc --noEmit) clean; client does not
consume core at the type level. Game.* public API + signatures preserved
throughout the epic.

No behavior change. Core: 475 passed / 12 skipped, tsc -b clean.
index.ts: 2491 -> ~530 lines across the epic.

Refs #132, #133
Add targeted tests for the decomposed modules and a COVERAGE=1-gated
threshold scoped to src/Game/:
- guards.ts -> 100% (canRoll/canRollForStart/canPlayerRoll/canGetPossibleMoves)
- robot.ts -> 88% (handleRobotMovedState, confirmTurnWithRobotAutomation)
- game-accessors: activePlayer/inactivePlayer/getPlayersForColor/findChecker + createNewGame rules merge
- turnFlow: roll (rolled-for-start/rolling/doubled), switchDice, executeAndRecalculate, checkAndCompleteTurn, toMoved, moveAndFinalize
- cube: acceptDouble-at-64 (maxxed) and refuseDouble non-first-double

Game/ aggregate coverage: ~59% -> 80.5% stmts, 71.8% branch, 89% funcs,
82.5% lines. Gate uses a directory-path key (aggregate, not per-file) at
75/70/75/75; branch floor ratchets toward 75 as turnFlow no-move/blocked
paths and the gnuPositionId getter gain coverage. Enforced only under
COVERAGE=1, so normal jest runs are unaffected.

Core: 518 passed / 12 skipped; COVERAGE=1 exit 0; normal exit 0.

Refs #134
… (Phase 9 of #96)

cube.ts: drop all 5 'as any' casts.
- offeredThisTurnBy is on BaseCube, so the cast was unnecessary.
- rollForStartValue is on BasePlayerProps (optional); the two player-copy
  casts become documented non-null assertions (rolling/inactive player
  types require the field).
- the acceptDouble return 'as any' narrows to a commented
  'as unknown as BackgammonGameRolling' (spread yields a generic players
  array TS can't narrow to the tuple).

turnFlow.ts: remove the undo-snapshot casts in move() and
executeAndRecalculate() -- game.activePlay and undo.frames are typed now
(since the Phase 2 types fix), so 'const ap: any = (game as any)...' plus
the JSON fallback 'as any' are gone.

index.ts: comment the gnuPositionId getter's 'this as any' (Game class
vs BackgammonGame union).

Deferred: turnFlow still has ~30 '(move as any).x =' casts in the
sanitization/recalculation loops. Removing those requires rebuilding move
objects immutably instead of mutating in place -- a behavior-sensitive
change left for a dedicated pass now that turnFlow has ~76% coverage.

No behavior change. Core: 518 passed / 12 skipped, tsc -b clean.

Refs #96
test(game): coverage gate for decomposed Game modules (Phase 8 of #134)
refactor(game): remove as-any casts in cube + snapshot paths (Phase 9 of #96)
@nodots
nodots merged commit 61fd785 into development Jul 15, 2026
1 check passed
@nodots
nodots deleted the refactor/game-decomp-phase7-facade branch July 15, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant