Files
scrabble-game/docs/FUNCTIONAL.md
T
Ilia Denisov 7e75c32d07
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 36s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
R3: dashboards, docs and tracker bake-back
- Edge/UX dashboard: aggregate request-rate vs rejection-rate panel
  (gateway_rate_limited_total by class; no per-user labels).
- ARCHITECTURE §2/§11/§12/§13: body cap + explicit h2c sizing, the rate-limit
  observability pipeline and auto-flag policy, the admin-limiter note (and the
  caddy-path gap), the landing container topology; fixed the stale 120/min
  per-user figure.
- FUNCTIONAL (+_ru): the Throttled view and the reversible high-rate flag.
- gateway/backend/deploy READMEs, TESTING.md, root CLAUDE.md updated.
- PRERELEASE.md: R3 interview decisions + implementation refinements logged;
  tracker R3 -> done (this PR implements it; CI gates the merge).
2026-06-10 05:12:30 +02:00

13 KiB
Raw Blame History

Scrabble Game — Functional spec

Per-domain user stories: what each user-visible operation does. This is the starting point for any change request that touches behaviour. The English version is authoritative; FUNCTIONAL_ru.md is a mirror for the project owner — mirror every point edit in the same patch (translate only the changed paragraphs). Sections deepen as stages land; (Stage N) marks where the detail is authored.

Domains

Client app (Stage 7 / 8)

The web/app client (Svelte + Vite) realizes these stories. The playable slice (Stage 7) covers signing in (guest or email), the "my games" lobby, starting an 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. 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. A public landing page at the site root introduces the game, switches language and theme, and links to the matching per-language Telegram channel; the game itself runs at /app/ (web) and /telegram/ (the Telegram Mini App). The landing's theme is ephemeral (it follows the system scheme, not the saved preference); its language choice is saved.

Identity & sessions (Stage 1 / 6 / 9 / 15)

A player arrives from a platform (Telegram first), via email login, or as an ephemeral guest. The gateway validates the credential once and mints a thin session token; the backend resolves it to an internal user_id. A Telegram Mini App launch authenticates from the platform's signed initData, themes the UI to the Telegram colours, and — on first contact — seeds the new account's interface language from the Telegram client. The sign-in service also declares the game languages it offers (a set of en/ru, at least one), which gate the New Game variant choice in the lobby. Telegram runs a separate bot per language (an English bot and a Russian bot, the same player spanning both); the bot a player signed in through sets their offered languages, and their non-game notifications come from it. A game's notifications (your turn, game over, a nudge), though, always come from that game's bot — by the game's language, not whichever bot the player signed in through last. Guests are session-only with restricted features (auto-match only; no friends, stats or history); an abandoned guest that never joined a game and has been idle past the retention window is garbage-collected. While the app is open the client keeps a live stream and receives in-app updates in real time — the opponent's move, your turn, chat, nudges and a found match. When the app is closed, the chosen out-of-app events (your turn, game over, nudge, a found match, an invitation or friend request) arrive as a Telegram notification instead — unless the player keeps notifications in the app only (a profile setting, on by default). The "your turn" notification names the opponent and recaps their last move — the word and the running score for a scoring play, or that they swapped or passed — and a finished game sends a "game over" notification with your result and the final score (scores read with yours first). If the connection drops or the server is rate-limiting, the app does not nag with errors: the header shows a quiet "Connecting…" spinner while it reconnects, actions that send to the server pause until it is back (a server-data screen still opens, with the spinner, and fills in on reconnect), and pending reads resume on their own — the interface stays usable instead of flashing a red banner each time.

Accounts, linking & merge (Stage 1 / 11)

First platform contact auto-provisions a durable account. From the profile a player links an email (via a confirm code) or their Telegram (via the web sign-in); a guest who links their first identity becomes a durable account. The "already taken" status of an identity is never revealed before the code/sign-in is verified. If the linked identity already belongs to another account, the player is shown an explicit, irreversible confirmation and the two accounts are merged into the one they are using (statistics summed, games and friends transferred, duplicates removed) — except when a guest links an identity that already has a durable account, where the durable account is kept and the guest's games move into it. A merge is blocked only while the two accounts share a game still in progress.

Lobby & matchmaking (Stage 4 / 15)

Bottom tab menu: my games, profile. The 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 (Stage 17). 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. The removal is per-account and permanent — the game disappears only from your list and stays in the other players' lists, and there is no undo. The game types offered on New Game are limited to the languages the player's sign-in service supports (English → Scrabble; Russian → Scrabble + Erudite; a bilingual service shows all three, and the web client is unrestricted). Variants are shown by their display name — both Scrabble variants read "Scrabble"/"Скрэббл" and Erudit reads "Erudite"/"Эрудит" (by the interface language), and the same name titles the in-game screen. This gates only starting a new game — both auto-match and a friend invitation — so a player still sees and plays existing games of any language. 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 (24) are formed by inviting players from the friend list (an invitation, like a friend code, is shareable as a Telegram deep link that opens it directly): 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 dictionary at submit time and scored; an unlimited preview reports what a tentative move would score and whether it is legal. The dictionary check tool is unlimited and offers a complaint on any result. Hints are governed per game — whether they are allowed and how many each player starts with — and draw on a personal hint wallet once the per-game allowance is spent. The game ends when the 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. 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. A player's board composition is kept per game: the rack arrangement and the tiles laid but not yet submitted are saved as they compose and restored on return (including on another device); a player may arrange tiles during the opponent's turn, but that draft is position-only — the score preview and submission stay available only on the player's own turn.

Robot opponent (Stage 5)

When auto-match finds no human within ten seconds, a robot opponent takes the empty seat so the game starts without waiting. It is meant to feel like a person: it decides once per game whether to play to win (about 40% of the time, so the human wins most games), aims for a close score rather than crushing or throwing the game, and plays at a human pace — short thinking times for most moves, the occasional long 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, language-appropriate name (a Russian game draws mostly Russian names); it does not chat, and silently ignores friend requests — a request to a robot stays pending and expires, exactly like a human who never responds.

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. In a game, an add to friends item for each opponent mirrors the live relationship: it reads request sent (disabled) while a request is pending or was declined, and in friends once accepted — updating in place the moment the opponent answers, and staying correct across reloads (Stage 17). 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. Chat and the word-check tool open as their own screens (with a back to the game), and a new chat message raises an unread badge on the game's menu until the chat is opened.

Profile & settings (Stage 4 / 8)

Edit the display name (letters joined by a single space / "." / "" separator, with an optional trailing ".", up to 32 characters and at most 5 special characters — the "." / "" punctuation, spaces aside), the timezone (chosen as a UTC offset), the daily away window (on a 10-minute grid, at most 12 hours, wrapping midnight) and the block toggles. The profile form is edited inline (no separate edit mode). Linking an email or Telegram and merging accounts are covered under "Accounts, linking & merge" (Stage 11).

History & statistics (Stage 3 / 8)

Finished games are archived in a dictionary-independent form and exportable to 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)

Operators reach a server-rendered admin console at ${DOMAIN}/_gm — the backend renders it; the gateway gates it with HTTP Basic Auth on its public listener and proxies it verbatim. The console lists and inspects users (profile, statistics, identities, their games) and games (summary + seats), works the word-complaint review queue — resolving each as reject / accept-add / accept-remove — and exposes the dictionary: the resident versions per variant, a hot-reload of a new version from BACKEND_DICT_DIR/<version>/, and the pending wordlist changes derived from accepted complaints (which feed the offline rebuild and are marked applied after a reload). When a Telegram connector is configured an operator can also message a user (by their Telegram identity) or post to the game channel. State-changing actions are protected by a same-origin check; the console tracks no operator identity.

The console also surfaces rate-limit abuse (R3): a Throttled page lists the recently throttled users/IPs the gateway reported (an in-memory window — it resets on a backend restart) and the accounts currently carrying the soft high-rate flag. An account sustaining rejections past a tunable threshold is flagged automatically — the marker is reversible, shown as a badge in the user list and on the user card, and never blocks play; the operator reviews and clears it from the user card. There is no automatic ban.