feat: sparser robot nudges, typed unread badge, lobby unread bump
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m26s

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.
This commit is contained in:
Ilia Denisov
2026-06-19 16:50:48 +02:00
parent c67a5d51f1
commit 6e77de4c1e
34 changed files with 331 additions and 86 deletions
+13 -5
View File
@@ -446,9 +446,10 @@ disguised robot stays indistinguishable from a person.
**sleeps 00:0007:00** anchored to the **opponent's** profile timezone with a
per-game drift of **±3 h** (fallback UTC), so its night overlaps the human's
rather than running anti-phase; on a daytime nudge it replies near the move's lower
band; it proactively nudges the idle human on a **lengthening, randomized schedule**the
first ~60-90 min into the turn, each later reminder spaced further out toward 1-6 h — so a long
wait gets a handful of increasingly-spaced nudges rather than an hourly stream.
band; it proactively nudges the idle human on a **sparse, randomized schedule**every nudge
waits a uniform random **9-12 h** (the first measured from the turn start, each later one from
the previous nudge), so a neglected turn gets only a handful of widely-spaced reminders. A nudge
that would fall inside the sleep window is skipped and fires at the first scan after wake.
- **Dead-endgame timing**: once the **two most recent moves are both passes**, the board and the
robot's rack are frozen and it is bound to pass again, so the robot drops the long late-game
think time and answers on a **shortened schedule scaled to the human's own last (pass) think
@@ -570,8 +571,15 @@ in either direction (the enqueue excludes the caller's `BlockedWith` set);
recipient answers by moving** (the move path calls a wired `NudgeClearer`). The mask is
inverted so "anything unread" is a plain `unread_seats <> 0`, which the per-viewer
`unread_chat` game-view flag (seeding the lobby and in-game unread **dot**), the admin
unread filter and the unread gauge all use. On each clear the publish-to-read latency is
recorded; the read time itself is not retained.
unread filter and the unread gauge all use. A second per-viewer flag, **`unread_messages`**
(`unread_seats <> 0 AND kind = 'message'`), reports whether any unread entry is a real
message rather than only a nudge, so the **dot is coloured**: the regular danger colour
when a message is unread, a softer amber when only nudges are. Both flags share the
REST-seed-then-event-bump lifecycle (the live-event game-view leaves them false; a nudge
event raises only `unread_chat`, a message event raises both). The lobby additionally
**floats games with any unread entry to the top** of the your-turn and opponent-turn
sections (the finished section keeps its activity order). On each clear the publish-to-read
latency is recorded; the read time itself is not retained.
- **Profile**: `preferred_language` (en/ru, edited in Settings), display name, email
(confirm-code binding, see §4), **timezone**, the daily **away window** and the
block toggles — all editable through `account.UpdateProfile`, which validates them:
+8 -5
View File
@@ -74,8 +74,9 @@ The lobby lists **my games** and offers a bottom tab bar — new game, statistic
**my games** list groups games into three
sections — *your turn*, *opponent's turn* and *finished* (empty sections are hidden) — and
orders them so the games awaiting your move come first, the longest-waiting on top, while
opponent-turn and finished games are most-recent first; it renders as a compact,
line-separated list. While the lobby is open and a listed game **becomes your turn or
opponent-turn and finished games are most-recent first; within the your-turn and
opponent-turn sections a game with any unread notification floats to the top (the finished
section keeps its order). It renders as a compact, line-separated list. While the lobby is open and a listed game **becomes your turn or
finishes**, its status icon **blinks twice** to draw the eye (an opponent's-turn change is
silent, applied in place). You can **remove a finished game from your own list**:
swipe a finished row left (or, on desktop, tap its **⋮**) to reveal a **❌**, then tap it.
@@ -161,7 +162,8 @@ times for most moves, the occasional long one, and a night-time pause that track
player's own day; once a game is clearly decided and both sides are only passing, it stops
dragging it out, answering its forced passes at roughly the player's own pace rather than
after a long deliberation. It answers a nudge
within a few minutes and nudges back when the player has been away a long time. It
within a few minutes and, when the player keeps it waiting, nudges back on a sparse
schedule — roughly every 9-12 hours, and never during its night-time pause. It
carries a human-like, language-appropriate name — a fresh one each game, drawn from a wide
international pool of real names and handles, so the arena feels populated by many different
players (a Russian game shows mostly Russian names, never East-Asian scripts; an international
@@ -216,8 +218,9 @@ Chat and the word-check tool share one **comms screen** with **💬 chat** / **
tabs, reached from the 💬 in the move-history header (with a back to the game). An AI game has no
chat, so its comms screen is the word-check alone — and once an AI game is finished (no chat, the
dictionary closed) the 💬 entry disappears entirely. An unread chat
entry — a message **or a nudge** from an opponent — raises a small **red dot** next to the game
in the **lobby** and on the game's **score bar**. Opening the **move history** counts as reading
entry — a message **or a nudge** from an opponent — raises a small **dot** next to the game
in the **lobby** and on the game's **score bar**, **red when an unread message is waiting and a
softer amber when only nudges are**. Opening the **move history** counts as reading
the chat, even without entering it: the dot clears and the 💬 icon **fade-blinks twice**. A nudge
also clears the moment its recipient **takes their move**.
+9 -6
View File
@@ -75,8 +75,9 @@ nudge) приходят от бота **этой партии** — по язы
Список **мои игры** разбит на три секции —
*твой ход*, *ход соперника* и *завершённые* (пустые секции скрыты) — и упорядочен так,
что игры, ждущие твоего хода, идут первыми, дольше всего ждущие сверху, а игры на ходу
соперника и завершённые — самые свежие сверху; отображается компактным списком с
линиями-разделителями. Пока лобби открыто и партия из списка **переходит на твой ход или
соперника и завершённые — самые свежие сверху; внутри секций «твой ход» и «ход соперника»
игра с любым непрочитанным уведомлением всплывает наверх (секция «завершённые» сохраняет
свой порядок). Отображается компактным списком с линиями-разделителями. Пока лобби открыто и партия из списка **переходит на твой ход или
завершается**, её значок статуса **дважды моргает**, привлекая внимание (переход на ход
соперника — без моргания, применяется на месте). Завершённую партию можно **убрать из своего списка**:
проведи по строке завершённой партии влево (или, на десктопе, нажми её **⋮**), чтобы открыть
@@ -166,8 +167,9 @@ nudge) приходят от бота **этой партии** — по язы
человеческим темпом — чаще короткие раздумья, изредка долгие, и ночная пауза,
подстроенная под день игрока; когда партия уже явно решена и обе стороны только пасуют,
он перестаёт её затягивать — отвечает на вынужденные пасы примерно в темпе самого игрока,
а не после долгого раздумья. На nudge отвечает за несколько минут и
сам шлёт nudge, когда игрок надолго пропал. Носит человекоподобное имя, подходящее
а не после долгого раздумья. На nudge отвечает за несколько минут и, когда игрок
держит его в ожидании, сам шлёт nudge редко — примерно раз в 9-12 часов и никогда во время
своей ночной паузы. Носит человекоподобное имя, подходящее
языку партии — каждую партию новое, из широкого международного пула реальных имён и
никнеймов, так что арена кажется полной разных игроков (в русской партии — в основном
русские имена, без восточноазиатских письменностей; в международной — весь спектр); не общается в чате и
@@ -222,8 +224,9 @@ nudge) приходят от бота **этой партии** — по язы
открываемый по 💬 в шапке истории ходов (с кнопкой «назад» в партию). В ИИ-партии чата нет, поэтому её
экран связи — только проверка слова; а после завершения ИИ-партии (чата нет, словарь закрыт) сам вход
💬 исчезает. Непрочитанная запись чата —
сообщение **или nudge** от соперника — зажигает маленькую **красную точку** рядом с партией в
**лобби** и на **строке счёта** партии. Открытие **истории ходов** считается прочтением чата, даже
сообщение **или nudge** от соперника — зажигает маленькую **точку** рядом с партией в
**лобби** и на **строке счёта** партии, **красную при непрочитанном сообщении и мягкую жёлтую,
когда непрочитаны только nudge'и**. Открытие **истории ходов** считается прочтением чата, даже
без захода в него: точка гаснет, а значок 💬 **дважды мигает**. Nudge также гаснет в момент, когда
его получатель **делает ход**.