Files
scrabble-game/docs/FUNCTIONAL.md
T
Ilia Denisov cf66ed7e26
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Stage 9: Telegram integration (connector side-service, Mini App, out-of-app push)
New platform/telegram connector (own container, bot token only there):
- go-telegram/bot long-poll loop: /start deep-links + Mini App launch button.
- gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify
  (renders a localized message + deep-link button), SendToUser/SendToGameChannel
  (admin, wired in Stage 10). Generic methods are platform-agnostic (external_id).
- Bot API base override for Telegram's test environment; Dockerfile + compose
  (VPN sidecar, no public ingress); README.

Gateway:
- initData validation relocated from the gateway into the connector; the gateway
  calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token,
  and deletes internal/auth.
- Out-of-app push: runPushPump routes events whose recipient has no live in-app
  stream to connector.Notify, gated by /internal/push-target + the in-app-only
  flag (race-free de-dup); HasSubscribers added to the push hub.

Backend:
- Migration 00007 accounts.notifications_in_app_only (default true) + jetgen.
- ProvisionTelegram seeds a new account's language/display name from the launch
  fields; IdentityExternalID reverse lookup; /internal/push-target handler.

UI:
- Telegram Mini App launch: detect initData, apply themeParams, authTelegram,
  route the deep-link start_param (g/i/f); /telegram/ guard redirects outside
  Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle;
  share-to-Telegram link for a friend code. Vitest + Playwright coverage.

Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only
(Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN,
ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated.
2026-06-04 07:10:21 +02:00

7.4 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.

Identity & sessions (Stage 1 / 6 / 9)

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. Guests are session-only with restricted features (auto-match only; no friends, stats or history). 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, 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).

Accounts, linking & merge (Stage 1 / 10)

First platform contact auto-provisions a durable account. From the profile a player links additional platform identities or an email via a confirm flow; 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 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.

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 name and neither chats nor accepts friend requests.

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 (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 / 8)

Edit the display name (letters joined by single space / "." / "_" separators, up to 32 characters), 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, 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 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)

Admin (Basic Auth at the gateway) reviews word complaints, manages dictionary versions, and inspects users/games.