Two pre-existing offline bugs, both exposed once offline mode became usable (cold boot +
toggling), owner-reported on the contour.
Bug 1 -- offline tiles render weight 0. The rack/board read tile values from the
lib/alphabet cache, populated only by the online wire codec (server-sent alphabet) or the
mock (so the e2e masked it). A local game never goes through the codec, so on a cold
offline boot with no prior online session the cache was empty and every value read 0
(scores stayed correct -- they come from the offline ruleset). Fix: the local source
seeds lib/alphabet from the static offline ruleset (letters + values) when it builds a
game view.
Bug 2 -- the lobby showed (and could open) the other mode's game after a toggle. Two
causes: (a) the lobby cache was not mode-tagged, so getLobby() instant-rendered the last
snapshot regardless of mode; (b) load() wrote the module list after an await with no
generation guard, so a slow online gamesList() finishing after a fast offline list() (or
vice versa) left the wrong mode's games on screen. Fix: tag the snapshot with offline and
gate getLobby() on it; add a load-sequence guard so only the latest load() writes.
- localgame/source.ts: ensureAlphabet from the ruleset in gameView (+ unit test).
- lobbycache.ts: LobbySnapshot.offline + getLobby(offline) mode check (+ tests).
- Lobby.svelte: setLobby tags the mode; load() carries a generation guard.
check 0 / unit 485 / e2e 198 / app entry 113.8/114.
A vs_ai hint is now unlimited and wallet-free, but idle-gated as an anti-frustration
aid: it unlocks only after the player has been stuck ~30 min on a turn (timed from the
robot's last move; the human's first move, before the robot has played, is exempt).
While gated the hint button carries a small lock badge and a tap shows the remaining
minutes ('Available in N min.'); the lock lifts live at the mark.
The gate is enforced CLIENT-SIDE against a MONOTONIC clock (performance.now()), never a
wall-clock timestamp: a device clock the player controls (or an auto-sync) must not be
able to open or freeze it. The wait is therefore session-scoped -- a reload restarts it
(there is no tamper-proof way to carry idle time across a relaunch without a wall clock).
An online vs_ai game will gate the same way but from the server's clock (a follow-up).
- lib/hints.ts: HINT_GATE_MS + pure hintGateRemainingMs (monotonic) + hintLockMinutes;
removed the wall-clock hintRemainingMs. Unit-tested red->green.
- Game.svelte: the monotonic gate (hintGateStart/monoNow, armed on the turn change), a
vs_ai hint button (plain: no confirm, no count; lock badge + gated tap -> toast), a
live 10s tick.
- localgame: removed the wall-clock hint gate and the now-dead robotLastMoveAtUnix field
from source.ts/serialize.ts (the client is authoritative); hint() just serves the top
move.
- i18n game.hintLockedIn.
- offline.spec.ts: assert the first move is un-gated and the lock arms after the robot moves.
- Docs FUNCTIONAL(+_ru) + ARCHITECTURE; bundle budget 114->115 (game-screen feature).
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).