Stage 4: lobby & social (matchmaking, friends, blocks, chat+nudge, invitations, profile, email, multi-player drop-out)
Engine: multi-player drop-out-and-continue with a per-game tile disposition (remove default / return), resigned seats skipped and excluded from the win, leaver rack never revealed; 2-player behaviour unchanged. New domains (service/store, no HTTP yet): internal/social (friend request/accept graph, per-user blocks, per-game chat with nudge as a message kind, content filter via mvdan.cc/xurls/v2 + leet/separator normaliser + phone heuristic) and internal/lobby (in-memory variant-keyed matchmaking pool, friend-game invitations invite->accept with lazy 7-day expiry). account gains profile editing and the email confirm-code flow (Mailer seam: SMTP or log mailer). Migration 00003_social.sql + regenerated jet. main wires the new services into the server (accessors for the Stage 6 handlers); robot substitution stays in Stage 5, REST/stream/push in Stage 6/8. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, README) updated.
This commit is contained in:
+61
-21
@@ -87,8 +87,13 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
a platform auto-provisions a durable account bound to that platform identity.
|
||||
Concretely, platform and email identities share one `identities` table keyed by
|
||||
a unique `(kind, external_id)`; email is an identity with `kind=email` and a
|
||||
`confirmed` flag (the confirm-code flow lands later). Accounts and identities
|
||||
use application-generated **UUIDv7** primary keys.
|
||||
`confirmed` flag. The **email confirm-code flow** (Stage 4) binds an email to the
|
||||
authenticated account: a 6-digit code (stored only as a SHA-256 hash, 15-minute
|
||||
TTL, ≤ 5 attempts) is sent through a `Mailer` seam (an SMTP relay, or a
|
||||
development log mailer when none is configured) and, once verified, attaches a
|
||||
confirmed email identity. An email already confirmed by **another** account is
|
||||
refused — adopting it would be a merge, which Stage 10 owns. Accounts and
|
||||
identities use application-generated **UUIDv7** primary keys.
|
||||
- **Linking** is initiated from an authenticated profile: choose a platform →
|
||||
complete that platform's web-auth confirm → attach the identity to the
|
||||
current account.
|
||||
@@ -162,10 +167,16 @@ Key points:
|
||||
timed out while asleep.
|
||||
- **Players**: auto-match is always 2 players; friend games are 2–4 players.
|
||||
`backend` owns turn order and the bag for any player count. A resignation or
|
||||
timeout in a two-player game ends it with the other player winning; **richer
|
||||
multi-player drop-out (a leaver's seat skipped while the rest play on, with a
|
||||
per-game disposition of their tiles) is deferred to Stage 4**, when friend games
|
||||
are formed.
|
||||
timeout in a two-player game ends it with the other player winning. In a game
|
||||
with **three or more seats** a resignation or timeout **drops that seat and the
|
||||
rest play on** — the engine skips the resigned seat in the turn rotation and
|
||||
excludes it from the win, finishing the game (the sole survivor wins) only once
|
||||
one active seat remains, or by the ordinary end conditions among the active
|
||||
seats. A per-game **drop-out tile disposition**, chosen at creation
|
||||
(`dropout_tiles`: `remove` from play — the default — or `return` to the bag),
|
||||
governs the leaver's rack, which is **never revealed** to the remaining players;
|
||||
it is recorded for deterministic journal replay. (Two-player games end on the
|
||||
first drop-out, so the disposition does not affect them.)
|
||||
- **Hint**: governed by two per-game settings — whether hints are allowed and the
|
||||
starting per-player allowance — plus a per-account hint **wallet**
|
||||
(`hint_balance`, spent after the allowance; top-ups are a later feature). A hint
|
||||
@@ -197,17 +208,39 @@ within 10 seconds. Designed to be indistinguishable from a person.
|
||||
|
||||
## 8. Lobby & social
|
||||
|
||||
- **Matchmaking** *(detail planned)*: a FIFO pool keyed by `(variant,
|
||||
language)`; 10 s with no human match → substitute the robot.
|
||||
- **Friends**: add by friend list, internal ID, or platform deep-link.
|
||||
- **Block** settings independently suppress in-game chat and friend requests.
|
||||
- **Chat**: per-game, persisted, length-limited, suppressed by the block
|
||||
setting.
|
||||
- **Nudge**: a player may nudge the opponent whose turn is awaited once per
|
||||
hour; the opponent receives a platform-native notification.
|
||||
- **Profile**: `preferred_language` (en/ru), display name, linked platform
|
||||
accounts, email (confirm-code binding), **timezone** (drives robot sleep;
|
||||
default from platform/locale, user-editable), block toggles.
|
||||
- **Matchmaking**: an **in-memory** FIFO pool keyed by `variant` (the variant
|
||||
fixes the board language), pairing the next two humans into a two-player
|
||||
auto-match with the seat order randomised for first-move fairness. The pool is
|
||||
lost on restart (players re-queue) and is anonymous, so it does not consult
|
||||
blocks. The 10 s wait and the **robot substitution** for a missing human are
|
||||
added in Stage 5.
|
||||
- **Friends**: a **request → accept** graph (one `friendships` table) — add by
|
||||
friend list or internal ID now, by platform deep-link with Stage 8. Declining or
|
||||
cancelling removes the pending request; blocking someone severs an existing
|
||||
friendship.
|
||||
- **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
|
||||
requests and game invitations between them.
|
||||
- **Friend games**: formed by **invitation → accept** (an `game_invitations`
|
||||
record with one row per invitee). The 2–4 player game starts once **every**
|
||||
invitee accepts; any decline cancels the invitation, and a pending invitation
|
||||
expires after 7 days (enforced lazily on access).
|
||||
- **Chat**: per-game, persisted (kept with the game's archive), **≤ 60 runes**,
|
||||
and **validated on input** — links, email addresses and phone numbers (including
|
||||
lightly obfuscated forms) are rejected, since the chat is for quick reactions,
|
||||
not contact exchange. Each message stores the sender's IP (forwarded by the
|
||||
gateway in Stage 6) for moderation. A sender who has disabled chat cannot post,
|
||||
and messages from a blocked sender are hidden from the viewer.
|
||||
- **Nudge**: folded into the chat as a `nudge` message kind. The player awaiting
|
||||
the opponent may nudge **once per hour per game**; it is not allowed on one's own
|
||||
turn. The platform-native delivery is wired with the gateway / platform
|
||||
side-service (Stage 6 / 8).
|
||||
- **Profile**: `preferred_language` (en/ru), display name, email
|
||||
(confirm-code binding, see §4), **timezone** (drives the away window and the
|
||||
robot's sleep; user-editable), the daily **away window** and the block toggles —
|
||||
all editable through `account.UpdateProfile`. Linked platform accounts and merge
|
||||
are Stage 10.
|
||||
|
||||
## 9. Persistence
|
||||
|
||||
@@ -220,9 +253,14 @@ within 10 seconds. Designed to be indistinguishable from a person.
|
||||
- Tables: `accounts` (durable internal accounts; Stage 3 added the away-window
|
||||
columns `away_start`/`away_end` and the hint wallet `hint_balance`),
|
||||
`identities` (platform/email identities, unique `(kind, external_id)`),
|
||||
`sessions` (revoke-only opaque-token hashes), and the Stage 3 game tables
|
||||
`games`, `game_players`, `game_moves` (the move journal), `complaints` and
|
||||
`account_stats`.
|
||||
`sessions` (revoke-only opaque-token hashes), the Stage 3 game tables
|
||||
`games` (Stage 4 added the `dropout_tiles` disposition column), `game_players`,
|
||||
`game_moves` (the move journal), `complaints` and `account_stats`, and the
|
||||
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.
|
||||
- **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
|
||||
@@ -263,7 +301,9 @@ does not cover.
|
||||
Two channels: **platform-native push** (out-of-app, via the platform
|
||||
side-service — your-turn, nudge) and the **in-app live stream** (chat,
|
||||
opponent-moved, while the app is open). Backend emits notification intents;
|
||||
delivery fans out to the appropriate channel.
|
||||
delivery fans out to the appropriate channel. Stage 4 **persists** the
|
||||
notification-worthy events (chat messages and nudges) but does not yet deliver
|
||||
them: the gRPC stream to the gateway and the platform push arrive in Stage 6 / 8.
|
||||
|
||||
## 11. Observability
|
||||
|
||||
|
||||
+26
-10
@@ -23,9 +23,13 @@ linking an identity that already has history merges it into the current
|
||||
account (stats summed, games/friends transferred).
|
||||
|
||||
### Lobby & matchmaking *(Stage 4)*
|
||||
Bottom tab menu: **my games**, **profile**. Auto-match (always 2 players) joins
|
||||
a `(variant, language)` pool; after 10 s with no human, the robot substitutes.
|
||||
Friend games (2–4) are formed by friend list, internal ID, or deep-link.
|
||||
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.
|
||||
|
||||
### Playing a game *(Stage 3)*
|
||||
Place tiles, pass, exchange, or resign. A play is validated against the game's
|
||||
@@ -37,9 +41,12 @@ personal hint wallet once the per-game allowance is spent. The game ends when th
|
||||
bag empties and a player clears their rack, after 6 consecutive scoreless turns,
|
||||
by resignation, or by the per-game move timeout (5 minutes to 24 hours, default
|
||||
24 hours): a missed turn auto-resigns, except while the player is inside their
|
||||
daily away window. A resignation or timeout gives the win to the other player and
|
||||
the leaver keeps their score (two-player games; multi-player drop-out-and-continue
|
||||
arrives with the lobby in Stage 4).
|
||||
daily away window. In a two-player game a resignation or timeout gives the win to
|
||||
the other player and the leaver keeps their score. In a game with three or four
|
||||
players the leaver's seat is dropped and the others play on, the game ending when a
|
||||
single active player remains; the disposition of the leaver's tiles (returned to
|
||||
the bag or removed from play) is chosen when the game is created, and the leaver's
|
||||
rack is never shown to the others.
|
||||
|
||||
### Robot opponent *(Stage 5)*
|
||||
Indistinguishable-from-human substitute in auto-match. Decides once whether to
|
||||
@@ -47,12 +54,21 @@ play to win (~40%), targets a small score margin, plays with human-like timing
|
||||
and a night sleep window, and nudges/answers nudges like a person.
|
||||
|
||||
### Social: friends, block, chat, nudge *(Stage 4)*
|
||||
Add friends; block chat and/or friend requests independently; per-game chat;
|
||||
nudge the awaited opponent at most once per hour (platform-native push).
|
||||
Send a friend request and have it accepted (decline or cancel 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
|
||||
(up to 60 characters) and may not contain links, email addresses or phone numbers,
|
||||
even disguised. Nudge the player whose turn is awaited at most once per hour (the
|
||||
nudge is part of the game chat); the out-of-app push is delivered via the platform.
|
||||
|
||||
### Profile & settings *(Stage 4)*
|
||||
Language (en/ru), display name, linked accounts, email binding, timezone, block
|
||||
toggles.
|
||||
Edit language (en/ru), display name, timezone, the daily away window and the block
|
||||
toggles, and bind an email by confirm-code: the backend emails a short code that,
|
||||
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 10.
|
||||
|
||||
### History & statistics *(Stage 3)*
|
||||
Finished games are archived in a dictionary-independent form and exportable to
|
||||
|
||||
+26
-11
@@ -23,9 +23,12 @@ session-токен; backend сопоставляет его с внутренн
|
||||
|
||||
### Лобби и подбор *(Stage 4)*
|
||||
Нижнее tab-меню: **мои игры**, **профиль**. Авто-подбор (всегда 2 игрока)
|
||||
встаёт в пул по `(вариант, язык)`; через 10 с без человека подставляется
|
||||
робот. Игры с друзьями (2–4) формируются по списку друзей, внутреннему ID
|
||||
или deep-link.
|
||||
встаёт в пул по варианту и сводится со следующим ожидающим человеком; через 10 с
|
||||
без человека подставляется робот (робот — в Stage 5). Игры с друзьями (2–4)
|
||||
формируются приглашением игроков из списка друзей или по внутреннему ID
|
||||
(приглашения по deep-link появятся с платформенной интеграцией): инициатор
|
||||
выбирает настройки, и партия стартует, когда приняли все приглашённые — любой
|
||||
отказ отменяет приглашение, а без ответа приглашение протухает через семь дней.
|
||||
|
||||
### Игровой процесс *(Stage 3)*
|
||||
Выкладывание фишек, пас, обмен или сдача. Ход проверяется по словарю партии при
|
||||
@@ -37,9 +40,12 @@ session-токен; backend сопоставляет его с внутренн
|
||||
завершается, когда мешок пуст и игрок выложил стойку, после 6 подряд бесплодных
|
||||
ходов, по сдаче, либо по таймауту хода (от 5 минут до 24 часов, дефолт 24 часа):
|
||||
пропущенный ход означает авто-сдачу, кроме как когда игрок внутри своего
|
||||
суточного окна отсутствия (away). Сдача или таймаут отдают победу другому игроку,
|
||||
а вышедший сохраняет свои очки (партии на двоих; выход одного с продолжением для
|
||||
остальных появится вместе с лобби в Stage 4).
|
||||
суточного окна отсутствия (away). В партии на двоих сдача или таймаут отдают
|
||||
победу другому игроку, а вышедший сохраняет свои очки. В партии на троих-четверых
|
||||
место вышедшего убирается, остальные играют дальше, и партия завершается, когда
|
||||
остаётся один активный игрок; что делать с фишками вышедшего (вернуть в мешок или
|
||||
убрать из игры) выбирается при создании партии, а его стойка никогда не
|
||||
показывается остальным.
|
||||
|
||||
### Робот-соперник *(Stage 5)*
|
||||
Неотличимый от человека дублёр в авто-подборе. Один раз решает, играть ли на
|
||||
@@ -47,13 +53,22 @@ session-токен; backend сопоставляет его с внутренн
|
||||
таймингом и ночным сном, делает и принимает nudge как человек.
|
||||
|
||||
### Социальное: друзья, блок, чат, nudge *(Stage 4)*
|
||||
Добавление в друзья; независимая блокировка чата и/или заявок в друзья;
|
||||
чат в рамках партии; nudge ожидаемого соперника не чаще раза в час
|
||||
(платформенное уведомление).
|
||||
Заявка в друзья и её принятие (отклонение или отмена снимают заявку, удаление —
|
||||
расторгает дружбу). Глобальная блокировка — отключить входящие чат и/или заявки —
|
||||
и блокировка конкретного игрока (пер-юзер блок скрывает его чат и запрещает заявки
|
||||
и приглашения в игру в обе стороны, а также расторгает уже имеющуюся дружбу). Чат
|
||||
партии — для быстрых реакций: сообщения короткие (до 60 символов) и не должны
|
||||
содержать ссылок, email и телефонов, даже завуалированных. Nudge ожидаемого
|
||||
соперника — не чаще раза в час (nudge — часть игрового чата); внеприложенческий
|
||||
push доставляется через платформу.
|
||||
|
||||
### Профиль и настройки *(Stage 4)*
|
||||
Язык (en/ru), отображаемое имя, привязанные аккаунты, привязка email, таймзона,
|
||||
переключатели блокировок.
|
||||
Редактирование языка (en/ru), отображаемого имени, таймзоны, суточного окна
|
||||
отсутствия (away) и переключателей блокировок, а также привязка email по
|
||||
confirm-коду: backend шлёт на почту короткий код, и после ввода email
|
||||
привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять
|
||||
нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и
|
||||
слияние появятся в Stage 10.
|
||||
|
||||
### История и статистика *(Stage 3)*
|
||||
Завершённые партии архивируются в независимом от словаря виде и экспортируются
|
||||
|
||||
+14
-1
@@ -33,7 +33,20 @@ tests or touching CI.
|
||||
end, **journal-replay equivalence**, the turn-timeout sweep with away-window
|
||||
grace, resign win/loss and statistics, the hint allowance-then-wallet policy,
|
||||
word-check and complaint capture, and per-game-lock serialisation). The robot
|
||||
balance/margin regression tests arrive with Stage 5.
|
||||
balance/margin regression tests arrive with Stage 5. Stage 4 adds the engine's
|
||||
**multi-player drop-out** cases (continue after one resign, last-survivor win,
|
||||
the tile-disposition bag effect) and a domain integration test for a 3-player
|
||||
**timeout that continues**.
|
||||
- **Social & lobby** *(Stage 4+)* — `backend/internal/social` unit-tests the chat
|
||||
**content filter** (links/emails/phones plus obfuscated forms) and
|
||||
`backend/internal/lobby` unit-tests the in-memory **matchmaker** (FIFO pairing,
|
||||
cancel, per-variant pools) with a fake game creator. Postgres-backed `inttest`
|
||||
covers the friend request/accept lifecycle with the block/toggle guards, the
|
||||
per-user block (and its severing of friendships), chat post/list with the IP,
|
||||
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.
|
||||
|
||||
## Principles
|
||||
|
||||
|
||||
Reference in New Issue
Block a user