In a local game the human seat's account id was a synthetic 'local:human:0',
so seatName's `accountId === session.userId` check never matched: the game
header rendered BOTH seats as 🤖 (the vs_ai fallback), and the lobby's
groupGames could not find the viewer's seat, so a human's turn read as
'Their turn' with the hourglass. Carry the real account id on the human seat
(create -> record -> GameView); the robot keeps its synthetic id.
Local games created before this fix keep the old display (no migration).
In offline mode the lobby now shows only the device-local games and its
New-vs-AI entry creates one through the in-browser engine — the visible
payoff of the offline mode.
- LocalSource.list() reconstructs a lobby GameView per stored local game by
replay, exposed through the lazy gamesource proxy; unit-tested via an
in-memory store.
- Lobby.load() branches on offlineMode: lists local games and skips every
gateway call (no online games/invitations/incoming); the Stats tab is
disabled offline.
- NewGame offline: find() creates a device-local vs_ai game via
LocalSource.create using the profile's advertised dict version + a local
seed; the friends flow and the random-opponent option are hidden, and the
variant picker / Start are enabled offline (were gated on connection).
- id.ts: newLocalGameId + randomSeed (tested).
Docs: ARCHITECTURE + FUNCTIONAL(+ru) offline-mode section.
Deferred to fast-follow: the Settings Friends/Profile/Feedback affordance
gating, the flip-to-offline readiness wait, the offline mock e2e (needs
mock-dawg support), and the local-hint UI. The offline flow is verified on
the test contour — the mock e2e cannot enter offline mode (the toggle is
gated to an installed PWA).
The game screen now drives a local vs_ai game through the offline engine, dispatched by
game id — completing the playable local game (on top of the source, #193). Online play
is unchanged.
- gamesource.ts: gameSource(id) returns the local source for a `local:` id, else the
gateway (the same game-loop interface). The offline engine stays OUT of the app entry
bundle — it is dynamically imported on first use (a separate chunk), so online-only
users never pay for it (the app entry stays within its size budget).
- localgame/id.ts: the tiny id helper (no engine imports) the dispatcher branches on.
- Game.svelte: the game-loop calls (state/history/submit/pass/exchange/resign/hint/
evaluate/draft) go through gameSource(id) instead of the gateway directly; a local
game's robot-reply events route through the same app event hub the network stream
feeds, so the screen reacts to opponent_moved / game_over identically.
Behaviour-preserving for network games (gameSource returns the gateway for them). Local
verify green: check + test:unit + build + bundle-size gate + e2e (196 passed).
A GatewayClient-shaped facade over the offline engine, so the same game screen can drive
a local vs_ai game with no backend (the wiring into Game.svelte is B3.2).
- source.ts: LocalSource implements the game-loop subset (GameLoopSource) for a local game
id — gameState/gameHistory via replay, submitPlay/pass/exchange/resign apply the human
move then run the robot (decide(generateMoves)) synchronously, persisting both and
delivering the robot's move through a per-game event emitter (no live stream). hint is
gated to >30 min since the robot's last move; evaluate/checkWord are local. It translates
the UI's glyph space to the engine's index space with the static letters table.
- ruleset.ts: add the static per-variant letters (glyphs), pinned to the Go alphabet —
offline is now fully self-contained (no reliance on a warm server alphabet cache).
- engine.ts: submitPlay (infers the direction like the server SubmitPlay), evaluatePlay +
dictionaryHas for the move preview / word check, and record the main-word coordinate +
the words on a play (for the history MoveRecord).
- source.test.ts: create -> human pass -> synchronous robot reply via the event, the hint
gate, decoded history, and a whole game driven to completion.
Pure additive library code; no runtime behavior change (bundle unchanged).