d-sports-engage-native. Game code ships in the app binary. The playfield never computes a result.
Architecture
One dynamic route plus shared chrome:lib/games/index.tsside-effect imports every shipped game soregisterGameruns.resolveGame(key)looks up the definition. A miss is Update required, not an exception.GameEngineScreenstartsuseGameSessionand mountsGameShell.GameHudshows title, attempt pips, and running Rep.- The playfield renders inside the shell. It does not draw its own chrome or
SafeAreaView.
launchRouteForGame → /(tabs)/locker-room/games/{gameKey} for engine games.
You can still open the legacy Daily Spin and Guess the Player modal routes. Engine also registers prize-wheel-spin and guess-the-player on the shared [gameKey] route. How-to-Play walkthroughs overlay the live playfield against a fake session. Do not call the server from a walkthrough.
Registry
lib/games/registry.ts is an open Map of gameKey → GameDefinition. Keys are strings. You do not edit a shared union to add a game.
Each definition supplies:
key— same slug asGame.keyon the serverparseConfig/parseScenario— runtime-validate server JSON and reject unknown shapesComponent— playfield only- optional
presentOutcome— cosmetic mapping (animation, HUD copy). It must not decide what happened.
air-hockey, and locker-singularity in lib/games/register-shipped.ts. locker-singularity uses sessionMode: "idle-ledger" and talks to /api/v1/engage/games/locker-singularity/ledger instead of the session verbs.
Catalog join
The client merges two public catalogs:lib/join-game-catalog.ts overlays engine rows on the public catalog. Shipped engine tiles stay visible even if the legacy catalog omits them. The player-pickem coming-soon tile hides once daily-pickem is present.
Unplayable engine rows stay in the list:
reason: UPDATE_REQUIRED— live-looking card, subtitle Update required- Registry miss → treat as Not in this build
- Offline or empty → static local tiles from
GAME_CATALOGUE
Session hook
useGameSession owns lifecycle: idle → loading → active → submitting → resolved, plus forfeited | expired | pendingResolution.
Forfeit is centralized. The hook forfeits when the app backgrounds, the tab hides, you navigate away, board/team context changes, or the session expires. Game authors do not write that branch. Web vs native focus-loss lives in lib/games/focus-loss.ts.
Engine calls go through useGamesEngineApi(), not useApi(). Every request sends X-Client-Version.
HUD rules
- Summary Rep uses awarded (
awarded ?? totalPoints). Caps can bind. Do not showrequestedas the banked amount. ResultTierOverlayplays after each server outcome (elite|clean|partial|miss).- Pick’em locks on submit and shows Locked in while
PENDING_RESOLUTION. - Trivia resolves immediately. The correct key appears only after the server responds.
- On bank, native updates Fan Rep and invalidates leaderboard caches.
Error UX
GAME_UPDATE_REQUIREDand a registry miss share the sameGameUpdateRequiredscreen.GAME_DAILY_NOT_ELIGIBLEmaps to the already-played state.- An unknown session status is treated as finished (
EXPIRED), never as playable. - A failed submit does not consume the next
sequenceNumber.
Related files
components/locker-room/GameEngineScreen.tsxcomponents/locker-room/game-hud/{GameShell,GameHud,ResultTierOverlay}.tsxhooks/use-game-session.ts,hooks/use-games-engine-api.tsapp/(tabs)/locker-room/games/[gameKey].tsxconstants/game-routes.ts
