Skip to content

TypeScript source code review & clean-up (all 10 modules)#427

Merged
ejfn merged 16 commits into
mainfrom
ejfn/ts-review
Jun 12, 2026
Merged

TypeScript source code review & clean-up (all 10 modules)#427
ejfn merged 16 commits into
mainfrom
ejfn/ts-review

Conversation

@ejfn

@ejfn ejfn commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Systematic quality audit of all ~16,250 lines across 60 .ts files in src/. Changes are purely non-functional: dead code removal, type guard fixes, style improvements, and one logic bug fix.

Modules

Module Key Fixes
src/types/ Type guard for deserialization, AIGameContext hardening, PlayableSuit constraint
src/utils/ Fisher-Yates shuffle (replaced biased sort-shuffle), infinite retry bug fix, type-safe sort helpers
src/game/ Dead nullish coalescing, redundant guards, tractor detection precision
src/hooks/ Stale closure fixes, animation leak, persistence logic
src/ai/ core Dead branches, unsafe casts, memory context correctness
src/ai/following/ Filter bug, in-place mutation, unsafe casts, dead routing branches
src/ai/leading/ Shadowing variable, hoisted memory context, PlayableSuit tightening
src/ai/kittySwap/ + trumpDeclaration/ Dead variable, redundant guards, in-place sort mutation
src/ai/llm/ Dead .replace() calls, unused interface field, redundant guard
src/locales/ gameLogger convention, redundant cast

Verification

  • All 723 tests pass (npm run qualitycheck)
  • Zero functional changes – purely type safety, dead code, and style fixes

ejfn added 16 commits June 6, 2026 18:26
- card.ts: deserializeCard validates input via isCardLike before casting;
  isTrump guards Suit.None trumpSuit (joker-pair declarations)
- ai.ts: trumpInfo required (always present when AI invoked);
  suitDistribution typed as Partial<Record<Suit,number>>;
  suitVoids narrowed to Set<PlayableSuit>
- trumpDeclaration.ts: detectPossibleDeclarations returns DeclarationOpportunity[];
  card.suit check uses !== Suit.None instead of truthiness
- Ripple fix: add PlayableSuit import + cast at all suitVoids call sites;
  remove now-redundant trumpInfo || fallbacks in teammateAnalysis;
  add trumpInfo to test helper createAIGameContext
- cardSorting: type getSuitOrderValue/getRankOrderValue with Suit/Rank enums
  instead of string; remove always-true card field truthiness guards
- suitHelpers: remove dead joker ternary (both branches were identical);
  document that no distinct Unicode emoji exists for big/small joker
- cardAutoSelection: remove redundant inner && trumpInfo check (already
  guarded by the outer condition on line 92)
- translationHelpers: fix dead assignment (both ternary branches identical);
  narrow TranslationFunction from any to minimal typed interface
- gameInitialization: replace biased sort-shuffle with Fisher-Yates for
  uniform bot name index distribution
- gameStatePersistence: fix infinite retry loop in getItem (was passing
  retries instead of retries+1, so count never incremented)
- cardValue: remove misleading || 0 fallback on exhaustive Record<Rank, number>
  (suppressed bugs silently; direct indexing is correct and safe)
- tractorLogic: type getSuitOffset param as Suit instead of string; update
  switch cases from raw string literals to Suit.Spades/Hearts/Clubs/Diamonds
- multiComboAnalysis: remove dead else branch in groupCardsBySuitOrTrump
  (non-trump cards always have a valid suit; the fallback to Suit.None was
  unreachable dead code)
- comboDetection: extract getLeadingSuit as exported module-level function;
  remove always-true card.suit guard; delete two in-file local duplicates
- playValidation: remove third local copy of getLeadingSuit; import the
  single source of truth from comboDetection instead
…o run.sh, and clean up workspace script paths
Bugs fixed:
- useGameState: fix stale-closure on restore path — restoredState declared
  before completed-trick block; currentTrick cleared inline instead of via
  setTimeout(handleTrickResultComplete) which captured null gameState
- useGameStatePersistence: settings={} default created new object ref each
  render causing useMemo/triggerAutoSave to recompute every render; fixed by
  destructuring individual boolean fields with primitive defaults
- useProgressiveDealing: replace 3× magic string "dealing" with GamePhase.Dealing

Dead code removed:
- useGameState: remove initGame no-op and its call-site in GameScreenController
- useGameState: remove redundant inner if(result.completedTrick) guard (already
  asserted by outer condition)
- useGameStatePersistence: remove unused checkForSavedGame, useGameRestoration,
  RestorationData interface, and imports (hasSavedGame, getSavedGameMetadata,
  PersistedGameState); move MIN_SAVE_INTERVAL to module scope

Style / type safety:
- useAITurns: waitingPlayerId PlayerId|null instead of "" as PlayerId cast;
  de-duplicate trickComplete expression; remove redundant sub-field deps from
  effect dependency array
- useTranslation: remove unnecessary ?. and || fallback on changeLanguage (×5)
- useTrickResults: fix wrong comment on onTrickResultCompleteRef; tighten
  auto-hide effect deps to [showTrickResult]
Dead code removed:
- aiGameContext: remove isOpponentWinning from TrickWinnerAnalysis (never read
  anywhere; trivially derivable as !isTeammateWinning)
- aiGameContext: remove isLeadWinning from TrickWinnerAnalysis (never read
  anywhere in src/ or tests)
- aiGameContext: remove dead trumpInfo fallback in getRemainingUnseenCards —
  context.trumpInfo is required (hardened in module 1); or-fallback was dead
- aiGameContext: remove unreachable if(!trumpInfo) guard directly below

Type/style fixes:
- aiGameContext: un-export 4 internal-only helpers (isPlayerOnAttackingTeam,
  getCurrentAttackingPoints, calculateCardsRemaining, calculatePointPressure)
  that have no external callers; keep analyzeTrickWinner and getTrickPosition
  exported (used in tests)
- aiGameContext: add post-switch return to getTrickPosition to satisfy tsc
  (switch on number is not exhaustively provable); remove dead default case
- aiCardMemory: remove redundant if(suitKey !== undefined) guard — card.suit
  is typed as Suit enum, never undefined

Test updates:
- Remove isOpponentWinning/isLeadWinning from TrickWinnerAnalysis fixtures
- Replace isOpponentWinning assertions with !isTeammateWinning equivalents
- [BUG] validCombosDecision.ts: Fix dead filter body in attemptToBeatWithThreshold
  - strongBeatingCombos.filter used block-body arrow without return, so the .some()
    result was always discarded (implicit undefined → falsy). Every combo was excluded,
    making the threshold-gated beat path completely unreachable. Fixed with expression body.

- [BUG] multiComboFollowingStrategy.ts: Avoid mutating playerHand via in-place .sort()
  - selectCrossSuitDisposal called playerHand.sort() which reorders the caller's actual
    hand array. Changed to [...playerHand].sort() to copy before sorting.

- [TYPE] teammateAnalysis.ts: Remove unnecessary 'as PlayerId' casts (x2)
  - gameState.players[idx]?.id yields string|undefined. The existing && nextPlayerId
    guard already narrows it; casting it to PlayerId earlier was redundant and unsafe.
    playerMemories is Record<string, PlayerMemory> so no cast needed at the lookup site.

- [STYLE/DEAD] routingLogic.ts: Make trumpInfo required in fallbackSelection
  - trumpInfo was optional but always provided by routeToDecision. The else branch
    returning [playerHand[0]] was unreachable dead code. Made required, removed branch.

- [STYLE] suitAvailabilityAnalysis.ts: Add block scope to case const declarations
  - Wrapped ComboType.Pair and ComboType.Tractor case bodies in { } to eliminate
    'lexical declaration in case clause' lint warnings.
- [TYPE] candidateLeadDetection.ts: Rename local 'isTrump' to 'isTrumpSuit'
  - const isTrump = suit === Suit.None shadowed the imported isTrump function
    within createMetadata. Renamed to isTrumpSuit to eliminate the hazard.

- [PERF] candidateLeadDetection.ts: Hoist createMemoryContext out of for-loop
  - getAllUnbeatableCardsInSuit recreated an identical MemoryContext on every
    combo iteration. gameState is constant across iterations so hoisting is safe.

- [TYPE] leadingContext.ts + leadingScoring.ts: Tighten voidSuits to Set<PlayableSuit>
  - LeadingContext.teammate.voidSuits and opponents.voidSuits were typed Set<Suit>
    despite never containing Suit.None (trump voids tracked separately via isTrumpVoid).
  - Changed interface fields and constructors to Set<PlayableSuit>.
  - Added as PlayableSuit casts at the opponentVoids.add() site and the two
    scoreNonTrumpLead voidSuits.has() call sites.
  - Added PlayableSuit to leadingScoring.ts import.
…mpDeclaration/

- [DEAD] trumpDeclarationStrategy.ts: Remove always-true shouldDeclare variable
  - const shouldDeclare = true followed by shouldDeclare ? x : undefined made the
    false branch unreachable. Inlined shouldDeclare: true and declaration directly.

- [TYPE] trumpDeclarationStrategy.ts: Remove redundant if (card.rank) guard
  - rank is a required Rank enum field — always truthy. Guard removed in
    calculateSuitStrength.

- [STYLE] trumpDeclarationStrategy.ts: Copy options before .sort() in evaluateAllPossibleTrumpSuits
  - options.sort() mutated the array stored in the suitGroups Map in-place.
    Changed to [...options].sort().

- [TYPE] kittySwapStrategy.ts: Remove redundant pair.cards[0].rank && guard
  - rank is a required Rank enum field — the leading truthy check before
    bigRanks.includes() was always true. Guard removed in applyBasicExclusions.

- [TYPE] kittySwapStrategy.ts: Remove redundant if (card.suit) guard
  - card.suit is a required Suit enum field — always truthy. Guard removed in
    selectFromDisposableCards.

- [STYLE] kittySwapStrategy.ts: Fix module header comment rank > 7 -> rank >= 5
  - bigRanks array starts at Rank.Five, not Rank.Eight. Header now matches code.
- [DEAD] llmGamePrompt.ts: Remove 4 no-op .replace() calls in buildLLMSystemPrompt
  - STATIC_LLM_GAME_RULES contains none of the searched placeholder strings.
    All 4 regex replacements were dead. Trump values are already injected via
    buildUserPromptTemplate args. Now returns STATIC_LLM_GAME_RULES directly.

- [TYPE] llmPromptTemplates.ts + llmGamePrompt.ts: Remove unused handCardsCount
  - handCardsCount was declared in UserPromptTemplateArgs and populated at the
    call site (handCards.length) but never read inside buildUserPromptTemplate
    or any block builder. Removed from interface and call site.

- [TYPE] llmAIStrategy.ts: Remove redundant config.applyToPlayers && guard
  - applyToPlayers is a required string[] field initialized in DEFAULT_LLM_CONFIG.
    The leading truthy guard before .includes() was never needed.

- [STYLE] llmGamePrompt.ts: Refactor allCategories mutation to declarative concat
  - localBuildHandDisplay spread categoryOrder into a mutable array then pushed
    extras. Replaced with filter + spread concat pattern.
…les/

- [STYLE] index.ts: Replace console.warn/console.error with gameLogger
  - getStoredLanguage used console.warn; changeLanguageCustom used console.error.
    Both replaced with gameLogger.warn/gameLogger.error per codebase convention.
    Added gameLogger import.

- [STYLE] index.ts: Remove redundant 'as Language' cast in getStoredLanguage
  - The condition (stored === 'en' || stored === 'zh') already narrows stored
    to Language. The explicit cast was redundant; removed to let TS narrow.
@ejfn ejfn merged commit 42dc926 into main Jun 12, 2026
1 check passed
@ejfn ejfn deleted the ejfn/ts-review branch June 12, 2026 10:21
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