Three owner-requested polish changes:
- robot: replace the lengthening 60-90 min -> 6 h proactive-nudge ramp with a
flat uniform 9-12 h wait before every nudge; the existing sleep-window gate
still skips and defers a nudge that would land in the robot's night.
- ui: colour the lobby/in-game unread dot by type -- the regular danger colour
when a chat message is unread, a softer amber (--warn) when only nudges are.
Adds a per-viewer unread_messages flag (chat_messages.kind='message') across
the backend DTO, FlatBuffers wire, gateway transcode and the UI store.
- ui: float games with any unread notification to the top of the lobby's
your-turn and opponent-turn sections (finished keeps its order), reusing the
existing unread_chat flag.
Docs (ARCHITECTURE 7, FUNCTIONAL + _ru) updated. No DB migration; the new wire
field is backward-compatible.
In a dead-drawn endgame — the two most recent journal moves are both
passes, so the board and the robot's rack are frozen and the robot is
bound to pass again — the robot still waited out its long late-game think
time (up to 90 min) before passing, needlessly dragging out a decided game.
Shorten that delay to a [0.8, 1.5]x band around the human's last-move think
time (the gap between the last two journal entries), clamped to [30s, 8min]
and taken as a min with the normal schedule, so the robot never moves
slower. A slow human collapses to the 8-min cap; a fast human is tracked,
with the floor keeping the robot from passing suspiciously instantly. The
anchor reads the move journal only (no schema change), stays deterministic
from the seed, and still defers to the sleep window.
RobotTurns now carries EndgamePass + OppLastMove, filled by one batched
journal query on the scan; the honest-AI single-game trigger keeps the
normal path (it moves at once). NextMoveAt (admin ETA) is left as the
normal-schedule upper bound.
The robot followed its per-game playToWin/lose intent on every move, which made
the outcome too predictable. It now flips that intent for a single move on ~20%
of opening/midgame turns (a winning robot eases off, a losing one surges ahead),
so the chosen strategy may not pan out — which favours the human. The chance
tapers linearly to 0 over the last 14 tiles in the bag and is 0 once the bag is
empty, so the endgame follows the chosen strategy strictly.
The decision is deterministic from the seed (mix(seed,"deviate",moveCount)) and
applies to both robot paths via the shared selectMove; the per-game play-to-win
intent the admin card shows is unchanged. Adds deviateProb/deviates helpers and
unit tests (taper bounds + monotonicity, never-in-endgame, determinism, ~20%
distribution); bakes the behaviour into ARCHITECTURE §7, FUNCTIONAL (+_ru),
backend/README, PRERELEASE and PLAN Stage 5.
Two owner-reported defects from a live contour game.
A. Frequency: the robot's proactive nudge fired hourly for 12h+ (a 12h idle threshold
then the 1h cooldown, uncapped). Replaced with a lengthening, randomized schedule
(proactiveNudgeGap): the first nudge ~60-90 min into the human's turn, each later gap
growing toward 1-6h (uniform sample in [60min, ceil], ceil ramping 90min->6h over 12h
of idle, measured from the previous nudge), so a long wait gets a handful of
increasingly-spaced reminders instead of a stream.
B. Language: out-of-app push routed by the recipient's GLOBAL service_language
(last-login-wins), so after re-logging via the RU bot an English game's nudges came
from the RU bot. Now a game push (your_turn, game_over, nudge, match_found) carries
the game's own language (engine.Variant.Language) on push.Event, and the gateway
routes by it (falling back to service_language for non-game pushes). The New-Game
variant-gating guarantees the game's bot is one the player has started, so delivery is
never blocked.
Tests: proactiveNudgeGap unit + retimed TestRobotProactiveNudge; TestVariantLanguage;
emit your_turn/game_over language; TestNudgeRoutedByGameLanguage integration. Docs:
ARCHITECTURE (§7 nudge, §10/§13 routing), FUNCTIONAL (+ _ru), PLAN tracker.
The admin game detail now shows, per robot seat, the game's deterministic play-to-win
decision (from the bag seed) and — while it is that robot's turn — its scheduled next-move
ETA (sampled think-time delay, deferred past the sleep window), plus a caption with the
~40% global target. Wiring: robot.PlayToWin/NextMoveAt/PlayToWinTargetPercent exports,
account.IsRobot, game RobotSchedule (seed + turn-start). Tests: NextMoveAt invariants
(never early, never in the sleep window), PlayToWin export, and an admin render integration
test asserting the intent + ETA + target appear.
Owner-review follow-up on the Stage 8 branch:
- Friend code is copyable (📋 + toast). The lobby notification badge is fixed —
it had inherited the hamburger-bar style — into a proper round count dot.
- Safari: min-width:0 on flex text inputs (friend code, profile, chat) so they
shrink instead of pushing the adjacent button off-screen.
- Profile editing is validated on both the UI and the backend: display-name format
(letters joined by single space/./_ separators, no leading/trailing/adjacent
separators, <=32 runes), a UTC-offset timezone picker (account.ResolveZone parses
±HH:MM or a legacy IANA name), a 10-minute away grid capped at 12h (wrap-aware),
and email format; Save is disabled and invalid fields red-bordered until valid.
Language stays in Settings.
- In a game, an "add to friends" menu item flips to a disabled "request sent"; chat
send/nudge became ⬆️/🛎️ icon buttons.
- A finished game drops its last-word highlight, hides Check word / Drop game,
disables zoom, and draws an inert (greyed) footer instead of hiding it.
Tests: account validators (name/away/zone), UI profileValidation, e2e for the
finished-game footer/menu and the copy control. Docs (PLAN, ARCHITECTURE,
FUNCTIONAL +ru, UI_DESIGN) updated for the display-name rule, UTC-offset timezone
and the 12h away window.