Stage 4: lobby & social (matchmaking, friends, blocks, chat+nudge, invitations, profile, email, multi-player drop-out)
Tests · Go / test (push) Successful in 6s
Tests · Integration / integration (push) Successful in 9s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 9s

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:
Ilia Denisov
2026-06-02 19:29:30 +02:00
parent 571bc8c9f2
commit bfa8797f8c
54 changed files with 4270 additions and 81 deletions
+51 -1
View File
@@ -37,7 +37,7 @@ independent (see ARCHITECTURE §9.1).
| 1 | Backend foundation (config, server, Postgres+goose, sessions, accounts) | **done** |
| 2 | Engine package over scrabble-solver | **done** |
| 3 | Game domain (lifecycle, rules, hint, word-check, history+GCG, stats) | **done** |
| 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | todo |
| 4 | Lobby & social (matchmaking, friends, block, chat, profile, nudge) | **done** |
| 5 | Robot opponent | todo |
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | todo |
| 7 | UI (plain Svelte + Vite, board, lobby, chat, i18n) | todo |
@@ -261,6 +261,56 @@ Open details: deployment target/host; dashboards; load expectations.
`BACKEND_DICT_DIR`. `accounts` gained `away_start`/`away_end`/`hint_balance`
and the `account` package gained `SpendHint` (it owns its table).
- **Stage 4** (interview + implementation):
- Scope, as in Stages 13: **domain service/store layer, no HTTP** — REST/stream
is Stage 6. Chat and nudges are **persisted** now; live delivery (push /
in-app stream) is Stage 6/8. New packages `internal/social` (friends, blocks,
chat+nudge) and `internal/lobby` (matchmaking + invitations); profile editing
and the email confirm-code extend `internal/account`. The services have no
active driver this stage, so `main` builds them and hands them to the server,
which exposes them via accessors (the Stage 1 scaffolding-accessor pattern) for
the Stage 6 handlers.
- **Friends** (interview): request → accept on a single `friendships` table;
decline/cancel delete the pending row; **blocking severs** any friendship.
- **Blocks** (interview): the existing global toggles **plus** a per-user
`blocks` table; block effects are **mutual** (a block either way suppresses
chat visibility and prevents requests/invitations between the pair).
- **Friend games** (interview): invitation → accept; the game starts only when
**all** invitees accept, any decline cancels it, and a pending invitation
**lazily expires after 7 days** (checked on access — no new sweeper).
- **Chat** (interview): ≤ **60 runes**, stored with the game forever, the
sender **IP** kept for moderation (as `text`, following Stage 1's no-`bytea`
precedent; the gateway forwards it in Stage 6), input **content-filtered**
(links/emails/phone numbers incl. obfuscated forms) via `mvdan.cc/xurls/v2`
plus a compact leet/separator normaliser and a ≥7-digit phone heuristic — the
one new dependency. **Nudge is a chat message** (`kind='nudge'`), rate-limited
to once per hour per game per sender.
- **Matchmaking** (interview): an **in-memory** FIFO pool keyed by **variant**
only (variant fixes the board language), pairing two humans (seat order
randomised). The 10 s wait and **robot substitution are deferred to Stage 5**.
The pool does **not** consult blocks (auto-match is anonymous) — a deliberate
simplification of the plan's optional block-skip that also avoids a DB call
under the pool lock.
- **Email confirm-code** (interview): 6-digit code, 15-min TTL, ≤ 5 attempts,
stored as a **SHA-256 hash**; a `Mailer` seam with an SMTP relay
(`BACKEND_SMTP_*`) and a default **log mailer**. It binds an email to the
current account; an email already confirmed by another account → `ErrEmailTaken`
(**merge is Stage 10**); email-as-login is Stage 6 and reuses this mechanism.
- **Multi-player drop-out** (interview; discharges the Stage 3 deferral): the
engine's `Resign` now drops a seat and the rest **play on** while ≥ 2 are
active, finishing (last-survivor wins) when one remains; `winner` excludes all
resigned seats. A per-game **`dropout_tiles`** setting (`remove` default |
`return`) governs the leaver's rack, which is **never revealed** to the others.
Timeout reuses `Resign`, so a multi-player timeout drops one seat and play
continues; `game.commit`/`timeoutGame` were already keyed on `g.Over()`, so they
only needed the setting threaded through create/replay.
- **Build/deps**: `go mod tidy` is not run — the bare-path `scrabble-solver`
replace lives only in `go.work`, so `tidy`/`go get` cannot resolve it; the
`xurls` dependency was added with `go mod edit -require` + `go mod download`,
its checksums recorded in the committed **`go.work.sum`**. No CI workflow change
(both Go workflows already clone the solver sibling and export
`BACKEND_DICT_DIR`).
## Deferred TODOs (cross-stage)
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,