From 968dda538a580519cf723f8fa820d39e3a133f71 Mon Sep 17 00:00:00 2001 From: Ken Riley Date: Wed, 15 Jul 2026 10:43:23 -0600 Subject: [PATCH] refactor(game): remove/annotate as-any casts in cube + snapshot paths (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 --- src/Game/cube.ts | 15 ++++++++++----- src/Game/index.ts | 2 ++ src/Game/turnFlow.ts | 16 ++++++++-------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Game/cube.ts b/src/Game/cube.ts index 40981e6..58da8db 100644 --- a/src/Game/cube.ts +++ b/src/Game/cube.ts @@ -35,7 +35,7 @@ export function canOfferDouble( game.cube.stateKind !== 'offered' && (!game.cube.owner || game.cube.owner.id === player.id) && // Disallow repeat doubles in same turn unless Beaver is implemented - (game.cube as any).offeredThisTurnBy?.id !== player.id + game.cube.offeredThisTurnBy?.id !== player.id ) } @@ -107,13 +107,16 @@ export function acceptDouble( ...offeringPlayer, stateKind: 'rolling', dice: Dice.initialize(offeringPlayer.color, 'rolling'), - rollForStartValue: (offeringPlayer as any).rollForStartValue, + // Non-null: an active player mid-game has a rollForStartValue; the base + // player type declares it optional, the rolling player type requires it. + rollForStartValue: offeringPlayer.rollForStartValue!, } const updatedInactivePlayer: BackgammonPlayerInactive = { ...player, stateKind: 'inactive', dice: Dice.initialize(player.color, 'inactive'), - rollForStartValue: (player as any).rollForStartValue, + // Non-null: see above; inactive player type also requires rollForStartValue. + rollForStartValue: player.rollForStartValue!, } const updatedPlayers = game.players.map((p) => { @@ -130,8 +133,10 @@ export function acceptDouble( activePlayer: updatedActivePlayer, inactivePlayer: updatedInactivePlayer, activeColor: updatedActivePlayer.color, - activePlay: undefined as any, - } as any) + activePlay: undefined, + // as unknown as BackgammonGameRolling: the spread yields a generic players + // array (not the rolling-game tuple) that TS can't narrow structurally. + } as unknown as BackgammonGameRolling) } export function canRefuseDouble( diff --git a/src/Game/index.ts b/src/Game/index.ts index 2f0996d..d260306 100644 --- a/src/Game/index.ts +++ b/src/Game/index.ts @@ -88,6 +88,8 @@ export class Game { */ get gnuPositionId(): string { try { + // as any: the Game class instance is structurally a BackgammonGame but + // TS does not treat the class as assignable to the discriminated union. return exportToGnuPositionId(this as any) } catch (error) { logger.warn('Failed to generate gnuPositionId:', error) diff --git a/src/Game/turnFlow.ts b/src/Game/turnFlow.ts index aea86ea..1443e91 100644 --- a/src/Game/turnFlow.ts +++ b/src/Game/turnFlow.ts @@ -752,14 +752,14 @@ export function move( ): BackgammonGameMoving | BackgammonGameMoved | BackgammonGameCompleted { // Push a pre-move snapshot to the turn-local undo stack try { - const ap: any = (game as any).activePlay + const ap = game.activePlay if (ap) { - if (!ap.undo) ap.undo = { frames: [] } + const undo = ap.undo ?? (ap.undo = { frames: [] }) const snapshot = typeof structuredClone === 'function' ? structuredClone(game) - : (JSON.parse(JSON.stringify(game)) as any) - ap.undo.frames.push(snapshot) + : (JSON.parse(JSON.stringify(game)) as BackgammonGameMoving) + undo.frames.push(snapshot) } } catch (e) { logger?.warn?.('Failed to push undo snapshot in Game.move', e) @@ -1207,14 +1207,14 @@ export function executeAndRecalculate( // Push a pre-move snapshot try { - const ap: any = (game as any).activePlay + const ap = game.activePlay if (ap) { - if (!ap.undo) ap.undo = { frames: [] } + const undo = ap.undo ?? (ap.undo = { frames: [] }) const snapshot = typeof structuredClone === 'function' ? structuredClone(game) - : (JSON.parse(JSON.stringify(game)) as any) - ap.undo.frames.push(snapshot) + : (JSON.parse(JSON.stringify(game)) as BackgammonGameMoving) + undo.frames.push(snapshot) } } catch (e) { logger?.warn?.('Failed to push undo snapshot before move', e)