Stage 8: UI social/account/history surfaces
Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode -> backend REST -> existing domain services): friends (incl. one-time friend codes), per-user blocks, friend-game invitations, profile editing + email binding, the statistics screen, and the in-game history + GCG export. Friends gain two add paths (interview decision, a deliberate plan change): one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited redeem); and play-gated requests (shared game required) where an explicit decline is permanent, an ignored request lapses after 30 days, and a code bypasses a decline. Migration 00006 widens friendships_status_chk and adds friend_codes. Lobby notification badge is poll + push: a new generic `notify` event drives it live; the client polls on open/focus. Language stays a single Settings control that writes through to the durable account's preferred_language. GCG export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file. Tests: backend unit + inttest (friend gate/decline/code, ListInvitations, GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN (Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN, TESTING, module READMEs.
This commit is contained in:
+28
-13
@@ -269,10 +269,17 @@ requires (there is no DM surface; chat is per-game).
|
||||
robot (§7) and starts the game. On a pairing or substitution the matchmaker
|
||||
emits a **match-found** notification (§10), delivered over the live stream;
|
||||
`Poll` remains as a fallback for a client that is not currently streaming.
|
||||
- **Friends**: a **request → accept** graph (one `friendships` table) — add by
|
||||
friend list or internal ID now, by platform deep-link with Stage 9. Declining or
|
||||
cancelling removes the pending request; blocking someone severs an existing
|
||||
friendship.
|
||||
- **Friends** (Stage 8): two add paths over one `friendships` table. A **one-time
|
||||
code** the to-be-added player issues (a `friend_codes` row: 6-digit numeric,
|
||||
SHA-256-hashed, **12 h** TTL, one live code per issuer, single-use, redeem
|
||||
rate-limited) is redeemed by the other player to become friends immediately.
|
||||
Alternatively a **request → accept** is sent to someone you **share a game with**
|
||||
(active or finished); the recipient may accept, ignore (the pending row lazily
|
||||
expires after **30 days** and may be re-sent), or **decline** — a decline is
|
||||
remembered (`status='declined'`) and blocks further requests from that sender,
|
||||
unless they hand them a code, which overrides it. The requester's own cancel still
|
||||
deletes the row; blocking someone severs an existing friendship. (Discovery by
|
||||
friend list or platform deep-link arrives with Stage 9 / TODO-5.)
|
||||
- **Block**: two independent **global** account toggles (`block_chat`,
|
||||
`block_friend_requests`) **plus** a **per-user block list**. A per-user block is
|
||||
applied mutually: it hides the pair's chat from each other and refuses friend
|
||||
@@ -316,8 +323,9 @@ requires (there is no DM surface; chat is per-game).
|
||||
Stage 4 social/lobby tables `friendships` (the request/accept graph), `blocks`
|
||||
(per-user blocks), `chat_messages` (per-game chat and nudges), `email_confirmations`
|
||||
(pending confirm-codes) and `game_invitations` / `game_invitation_invitees`
|
||||
(friend-game invitations). The matchmaking pool is **in-memory** and persists
|
||||
nothing.
|
||||
(friend-game invitations). Stage 8's migration `00006` widened the `friendships`
|
||||
status to admit `declined` and added `friend_codes` (one-time add-a-friend codes).
|
||||
The matchmaking pool is **in-memory** and persists nothing.
|
||||
- **Active games are event-sourced.** A game is a `games` row (pinned
|
||||
`variant`/`dict_version`, bag `seed`, the per-game settings, and a denormalised
|
||||
turn cursor) plus an append-only, decoded move journal (`game_moves`); the live
|
||||
@@ -352,7 +360,9 @@ the same rows and is likewise self-contained — we ship our own writer (the sol
|
||||
exposes none): the standard Poslfit dialect (UTF-8, `#player`/`#lexicon`
|
||||
pragmas, `8G`/`H8` coordinates, lower-case blanks, `.` pass-throughs, `-TILES`
|
||||
exchanges), plus `#note` lines for resignations and timeouts, which the standard
|
||||
does not cover.
|
||||
does not cover. **GCG export is offered only on a finished game** (`game.ErrGameActive`
|
||||
otherwise, Stage 8), so an in-progress journal is never leaked mid-play; the client
|
||||
shares the `.gcg` file via the Web Share API where available, else downloads it.
|
||||
|
||||
## 10. Notifications
|
||||
|
||||
@@ -365,12 +375,17 @@ services); a single backend→gateway **gRPC server-stream** (`Push.Subscribe`,
|
||||
`user_id` to each client's Connect `Subscribe` stream while the app is open. The
|
||||
catalog is **your-turn** and **opponent-moved** (emitted from the game commit, so
|
||||
robot-driver and timeout-sweeper moves emit too), **chat-message** and **nudge**
|
||||
(from the social service), and **match-found** (from the matchmaker, §8). Event
|
||||
payloads are FlatBuffers-encoded by the backend and forwarded verbatim. A client
|
||||
that is not currently streaming falls back to the matchmaker's `Poll` for
|
||||
match-found. Out-of-app platform push (your-turn, nudge) is wired in Stage 9;
|
||||
session-revocation events and cursor-based stream resume are deferred
|
||||
(single-instance MVP).
|
||||
(from the social service), **match-found** (from the matchmaker, §8), and **notify**
|
||||
(Stage 8 — a lightweight "re-poll" signal carrying a sub-kind: friend-request,
|
||||
friend-added, invitation or game-started; emitted on a friend-request and invitation
|
||||
create and on an invitation's game start). Event payloads are FlatBuffers-encoded by
|
||||
the backend and forwarded verbatim. A client that is not currently streaming falls
|
||||
back to the matchmaker's `Poll` for match-found and, for the lobby **notification
|
||||
badge** (incoming friend requests + open invitations), the client polls on lobby
|
||||
open and on focus as well as re-polling on the `notify` event — covering a push
|
||||
missed while the app was hidden. Out-of-app platform push (your-turn, nudge) is
|
||||
wired in Stage 9; session-revocation events and cursor-based stream resume are
|
||||
deferred (single-instance MVP).
|
||||
|
||||
A separate **announcements channel** feeds the client's one-line banner (UI_DESIGN.md).
|
||||
It is a client-side **mock** rotation today; a server-driven source (operational notices,
|
||||
|
||||
+21
-15
@@ -15,10 +15,10 @@ The web/app client (Svelte + Vite) realizes these stories. The **playable slice*
|
||||
auto-match, playing the board (place tiles by drag or tap, pass, exchange, resign),
|
||||
the top-1 hint, the unlimited word-check with complaint, per-game chat and nudge,
|
||||
real-time in-app updates, switching interface language (en/ru) and theme, and a
|
||||
read-only profile. Managing friends and blocks, creating friend games (invitations),
|
||||
editing the profile, the statistics screen and the history/GCG viewer arrive in
|
||||
Stage 8. Settings also pick the board's bonus-label style (beginner / classic /
|
||||
none). A hint **lays the suggested tiles on the board** for the player to confirm and
|
||||
read-only profile. **Stage 8** adds managing friends (including one-time friend
|
||||
codes) and blocks, friend-game invitations, editing the profile and binding an
|
||||
email, the statistics screen, and the in-game history viewer with GCG export.
|
||||
Settings also pick the board's bonus-label style (beginner / classic / none). A hint **lays the suggested tiles on the board** for the player to confirm and
|
||||
costs nothing when the rack has no legal move. The word-check accepts only the
|
||||
variant's alphabet, remembers answers within the session and rate-limits repeats.
|
||||
|
||||
@@ -42,10 +42,10 @@ account (stats summed, games/friends transferred).
|
||||
Bottom tab menu: **my games**, **profile**. Auto-match (always 2 players) joins a
|
||||
per-variant pool and is paired with the next waiting human; after 10 s with no
|
||||
human the robot substitutes (the robot arrives in Stage 5). Friend games (2–4) are
|
||||
formed by inviting players from the friend list or by internal ID (deep-link
|
||||
invites arrive with the platform integration): the inviter chooses the settings
|
||||
and the game starts once every invitee has accepted — any decline cancels it, and
|
||||
an unanswered invitation expires after seven days.
|
||||
formed by inviting players from the friend list (deep-link invites arrive with the
|
||||
platform integration): the inviter chooses the settings and the game starts once
|
||||
every invitee has accepted — any decline cancels it, and an unanswered invitation
|
||||
expires after seven days.
|
||||
|
||||
### Playing a game *(Stage 3)*
|
||||
Place tiles, pass, exchange, or resign. A play is validated against the game's
|
||||
@@ -74,9 +74,13 @@ one, and a night-time pause that tracks the player's own day. It answers a nudge
|
||||
within a few minutes and nudges back when the player has been away a long time. It
|
||||
carries a human-like name and neither chats nor accepts friend requests.
|
||||
|
||||
### Social: friends, block, chat, nudge *(Stage 4)*
|
||||
Send a friend request and have it accepted (decline or cancel withdraws it,
|
||||
unfriending removes the friendship). Block globally — switch off incoming chat
|
||||
### Social: friends, block, chat, nudge *(Stage 4 / 8)*
|
||||
Become friends in two ways: redeem a **one-time code** the other player issues (six
|
||||
digits, valid for twelve hours), or send a **request to someone you have played
|
||||
with** — they accept, ignore it (a request lapses after thirty days and can then be
|
||||
re-sent), or decline (a decline blocks further requests from you until they hand you
|
||||
a code). Cancelling your own pending request withdraws it; unfriending removes the
|
||||
friendship. Block globally — switch off incoming chat
|
||||
and/or friend requests — and block individual players (a per-user block hides that
|
||||
person's chat and stops requests and game invitations both ways; it also ends any
|
||||
existing friendship). Per-game chat is for quick reactions: messages are short
|
||||
@@ -91,11 +95,13 @@ once entered, attaches the email to the account (an email already confirmed by
|
||||
another account cannot be taken — that is a merge, a later stage). Linked platform
|
||||
accounts and merge arrive in Stage 11.
|
||||
|
||||
### History & statistics *(Stage 3)*
|
||||
### History & statistics *(Stage 3 / 8)*
|
||||
Finished games are archived in a dictionary-independent form and exportable to
|
||||
GCG. Statistics (durable accounts only): wins, losses, draws, max points in a
|
||||
game, and max points for a single move (the best play, which already includes
|
||||
every word it formed plus the all-tiles bonus).
|
||||
GCG; the export is offered **only once a game is finished** (exporting a live game
|
||||
would leak the move journal), and the client shares the `.gcg` file where the
|
||||
platform supports it, otherwise downloads it. Statistics (durable accounts only):
|
||||
wins, losses, draws, max points in a game, and max points for a single move (the
|
||||
best play, which already includes every word it formed plus the all-tiles bonus).
|
||||
|
||||
### Administration *(Stage 10)*
|
||||
Admin (Basic Auth at the gateway) reviews word complaints, manages dictionary
|
||||
|
||||
+22
-14
@@ -14,9 +14,10 @@
|
||||
игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача),
|
||||
top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии,
|
||||
обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и
|
||||
профиль только для чтения. Управление друзьями и блоками, создание дружеских игр
|
||||
(приглашения), редактирование профиля, экран статистики и просмотр истории/GCG
|
||||
появятся в Stage 8. В настройках также выбирается стиль подписей бонус-клеток
|
||||
профиль только для чтения. **Stage 8** добавляет управление друзьями (в т.ч.
|
||||
одноразовые коды-приглашения) и блоками, дружеские приглашения в игру,
|
||||
редактирование профиля и привязку email, экран статистики и просмотр истории
|
||||
партии с экспортом GCG. В настройках также выбирается стиль подписей бонус-клеток
|
||||
(новичок / классика / без текста). Подсказка **выставляет предложенные фишки на
|
||||
доску** — игрок сам решает сделать ход, и подсказка не тратится, если ходов нет.
|
||||
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
|
||||
@@ -42,10 +43,10 @@ session-токен; backend сопоставляет его с внутренн
|
||||
Нижнее tab-меню: **мои игры**, **профиль**. Авто-подбор (всегда 2 игрока)
|
||||
встаёт в пул по варианту и сводится со следующим ожидающим человеком; через 10 с
|
||||
без человека подставляется робот (робот — в Stage 5). Игры с друзьями (2–4)
|
||||
формируются приглашением игроков из списка друзей или по внутреннему ID
|
||||
(приглашения по deep-link появятся с платформенной интеграцией): инициатор
|
||||
выбирает настройки, и партия стартует, когда приняли все приглашённые — любой
|
||||
отказ отменяет приглашение, а без ответа приглашение протухает через семь дней.
|
||||
формируются приглашением игроков из списка друзей (приглашения по deep-link
|
||||
появятся с платформенной интеграцией): инициатор выбирает настройки, и партия
|
||||
стартует, когда приняли все приглашённые — любой отказ отменяет приглашение, а без
|
||||
ответа приглашение протухает через семь дней.
|
||||
|
||||
### Игровой процесс *(Stage 3)*
|
||||
Выкладывание фишек, пас, обмен или сдача. Ход проверяется по словарю партии при
|
||||
@@ -74,9 +75,14 @@ session-токен; backend сопоставляет его с внутренн
|
||||
сам шлёт nudge, когда игрок надолго пропал. Носит человекоподобное имя, не общается
|
||||
в чате и не принимает заявки в друзья.
|
||||
|
||||
### Социальное: друзья, блок, чат, nudge *(Stage 4)*
|
||||
Заявка в друзья и её принятие (отклонение или отмена снимают заявку, удаление —
|
||||
расторгает дружбу). Глобальная блокировка — отключить входящие чат и/или заявки —
|
||||
### Социальное: друзья, блок, чат, nudge *(Stage 4 / 8)*
|
||||
Подружиться можно двумя способами: погасить **одноразовый код**, который выпускает
|
||||
другой игрок (шесть цифр, действует двенадцать часов), либо отправить **заявку
|
||||
тому, с кем вы играли** — он принимает, игнорирует (заявка истекает через тридцать
|
||||
дней, после чего её можно отправить снова) или отклоняет (отказ блокирует ваши
|
||||
повторные заявки, пока он сам не передаст вам код). Отмена своей висящей заявки
|
||||
снимает её; удаление расторгает дружбу. Глобальная блокировка — отключить входящие
|
||||
чат и/или заявки —
|
||||
и блокировка конкретного игрока (пер-юзер блок скрывает его чат и запрещает заявки
|
||||
и приглашения в игру в обе стороны, а также расторгает уже имеющуюся дружбу). Чат
|
||||
партии — для быстрых реакций: сообщения короткие (до 60 символов) и не должны
|
||||
@@ -92,11 +98,13 @@ confirm-коду: backend шлёт на почту короткий код, и
|
||||
нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и
|
||||
слияние появятся в Stage 11.
|
||||
|
||||
### История и статистика *(Stage 3)*
|
||||
### История и статистика *(Stage 3 / 8)*
|
||||
Завершённые партии архивируются в независимом от словаря виде и экспортируются
|
||||
в GCG. Статистика (только у постоянных аккаунтов): победы, поражения, ничьи,
|
||||
макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все
|
||||
образованные им слова и бонус за все фишки).
|
||||
в GCG; экспорт доступен **только после завершения партии** (экспорт идущей партии
|
||||
раскрыл бы журнал ходов), и клиент делится файлом `.gcg` там, где платформа это
|
||||
поддерживает, иначе скачивает его. Статистика (только у постоянных аккаунтов):
|
||||
победы, поражения, ничьи, макс. очков за партию и макс. очков за один ход (лучший
|
||||
ход, уже включающий все образованные им слова и бонус за все фишки).
|
||||
|
||||
### Администрирование *(Stage 10)*
|
||||
Админ (Basic Auth на gateway) разбирает жалобы на слова, управляет версиями
|
||||
|
||||
+14
-3
@@ -15,7 +15,12 @@ tests or touching CI.
|
||||
`go test -tags=integration -count=1 -p=1 ./backend/...` (needs Docker), guarded
|
||||
by a separate CI workflow (`integration.yaml`; Ryuk disabled, serial). Slow.
|
||||
- **UI** *(introduced with the UI in Stage 7)* — Vitest (unit) + Playwright
|
||||
(e2e), mirroring the chosen plain-Svelte + Vite toolchain.
|
||||
(e2e), mirroring the chosen plain-Svelte + Vite toolchain. Stage 8 adds Vitest for
|
||||
the new FlatBuffers codecs (friend list, invitation, stats), the win-rate
|
||||
derivation and the GCG share/download choice, plus Playwright specs against the
|
||||
mock for the friends screen (code issue/redeem, accept a request), the lobby
|
||||
invitations section, the stats screen, profile editing, and the GCG export's
|
||||
finished-only visibility.
|
||||
- **Engine** *(Stage 2+)* — correctness of scoring and move generation is owned
|
||||
by `scrabble-solver`'s own GCG-backed tests. `backend/internal/engine` adds, on
|
||||
top of the embedded solver: per-variant smoke tests (load all three committed
|
||||
@@ -48,7 +53,11 @@ tests or touching CI.
|
||||
content and block-visibility rules, the nudge turn/rate-limit rules, the
|
||||
invitation flow (all-accept starts the game, decline cancels, lazy expiry,
|
||||
inviter-only cancel), and the email confirm-code flow (request/confirm, taken
|
||||
email, expiry and attempt-cap) with a fixture mailer.
|
||||
email, expiry and attempt-cap) with a fixture mailer. Stage 8 adds the
|
||||
**befriend-an-opponent** gate (a request needs a shared game), the **permanent
|
||||
decline** and 30-day re-send rule, the **one-time friend code** (issue/redeem,
|
||||
self/single-use, decline-bypass), `ListInvitations`, the zero-value `GetStats`, and
|
||||
the GCG **finished-only** gate.
|
||||
- **Robot** *(Stage 5+)* — `backend/internal/robot` unit-tests the pure strategy:
|
||||
the ≈ 40% play-to-win split over many seeds, the right-skewed move-delay
|
||||
(bounds, ~10-min median, determinism), the margin selection (win/lose, in-band
|
||||
@@ -70,7 +79,9 @@ tests or touching CI.
|
||||
(guest auth, unauthenticated rejection, unknown message type). The backend gains
|
||||
the **guest** lifecycle (a guest plays an auto-match to a natural end yet accrues
|
||||
no statistics) and the **email-as-login** flow (request/verify, returning user)
|
||||
in `inttest`.
|
||||
in `inttest`. Stage 8 adds gateway transcode round-trips for the new social/account
|
||||
operations (friends list, friend code issue/redeem, invitation create, stats, GCG,
|
||||
the profile-update away round-trip) and a `notify`-event constructor round-trip.
|
||||
|
||||
## Principles
|
||||
|
||||
|
||||
+27
-1
@@ -22,7 +22,11 @@ Login uses `Screen`.
|
||||
- **Back**: a thin, compact `<` drawn from two rotated CSS borders (`Header.svelte`
|
||||
`.chev`) — lighter than a glyph.
|
||||
- **Hamburger**: a CSS three-bar (`Menu.svelte`), deliberately larger; opens a dropdown
|
||||
of items (lobby: Profile/Settings/About; game: History/Chat/Check word/Drop game).
|
||||
of items (lobby: Friends/Profile/Settings/About; game: History/Chat/Check word, plus
|
||||
*Export GCG* on a finished game and *Add to friends* per opponent, then Drop game). A
|
||||
red count **badge** rides the hamburger (and the lobby *Friends* item) for pending
|
||||
incoming friend requests + invitations; the same dot style serves any future
|
||||
notification count.
|
||||
- **Tab bar** (`TabBar.svelte`): square, borderless, evenly distributed buttons — a large
|
||||
emoji icon over a tiny truncated label. A press highlights a rounded **square** behind
|
||||
the icon (slightly larger than it) until release; spacing keeps adjacent labels from
|
||||
@@ -77,6 +81,28 @@ Lobby rows show two lines (opponents, then result + score) with a large place-ba
|
||||
on the right: Victory 🏆 / Defeat 🥈 / Draw 🏅, and for 3–4-player games II 🥈 / III 🥉 /
|
||||
IV 🏅; active games show Your move 🟢 / Opponent's move ⏳; invitations use 💌.
|
||||
|
||||
## Social, account & history surfaces (Stage 8)
|
||||
|
||||
- **Friends** (`screens/Friends.svelte`, from the lobby menu): an "add a friend" block
|
||||
pairing a code **input** with a **Show my code** action that reveals a large 6-digit
|
||||
code + its expiry; then the incoming **requests** (Accept / Decline), the **friends**
|
||||
list (Remove / Block), and a **blocked** list (Unblock). Durable accounts only — a
|
||||
guest sees a sign-in prompt.
|
||||
- **Invitations**: a lobby **section** (a 💌 row per open invitation) with Accept /
|
||||
Decline for an invitee and a waiting/Cancel state for the inviter; creating one is the
|
||||
**"Play with friends"** mode in `NewGame.svelte` (pick invitees, then variant / move
|
||||
time / hints).
|
||||
- **Statistics** (`screens/Stats.svelte`, the lobby 📊 tab): a 2-column grid of stat
|
||||
cards (wins / losses / draws / games / win-rate / best game / best move) — pure
|
||||
numbers, no charts.
|
||||
- **Profile editing** (`screens/Profile.svelte`): an inline form (display name, timezone,
|
||||
the away-window time pickers, block toggles) and an email-binding sub-flow (enter email
|
||||
→ enter the confirm code). Interface language stays in **Settings** (it writes through
|
||||
to the account for durable users).
|
||||
- **History / GCG**: the in-game slide-down history gains the running total per move;
|
||||
*Export GCG* shares or downloads the `.gcg` file and appears only once the game is
|
||||
finished.
|
||||
|
||||
## Caveat
|
||||
|
||||
Emoji are rendered by the platform's system emoji font, so their exact look varies across
|
||||
|
||||
Reference in New Issue
Block a user