bf7dca0a09
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
Two owner-reported defects from a live contour game. A. Frequency: the robot's proactive nudge fired hourly for 12h+ (a 12h idle threshold then the 1h cooldown, uncapped). Replaced with a lengthening, randomized schedule (proactiveNudgeGap): the first nudge ~60-90 min into the human's turn, each later gap growing toward 1-6h (uniform sample in [60min, ceil], ceil ramping 90min->6h over 12h of idle, measured from the previous nudge), so a long wait gets a handful of increasingly-spaced reminders instead of a stream. B. Language: out-of-app push routed by the recipient's GLOBAL service_language (last-login-wins), so after re-logging via the RU bot an English game's nudges came from the RU bot. Now a game push (your_turn, game_over, nudge, match_found) carries the game's own language (engine.Variant.Language) on push.Event, and the gateway routes by it (falling back to service_language for non-game pushes). The New-Game variant-gating guarantees the game's bot is one the player has started, so delivery is never blocked. Tests: proactiveNudgeGap unit + retimed TestRobotProactiveNudge; TestVariantLanguage; emit your_turn/game_over language; TestNudgeRoutedByGameLanguage integration. Docs: ARCHITECTURE (§7 nudge, §10/§13 routing), FUNCTIONAL (+ _ru), PLAN tracker.
178 lines
22 KiB
Markdown
178 lines
22 KiB
Markdown
# Scrabble Game — Функциональная спецификация
|
||
|
||
Пользовательские сценарии по доменам: что делает каждая видимая пользователю
|
||
операция. Это зеркало [`FUNCTIONAL.md`](FUNCTIONAL.md) для владельца проекта;
|
||
**авторитетна английская версия**. Любую точечную правку переносим в том же
|
||
патче (переводим только изменённые абзацы). Разделы наполняются по мере этапов;
|
||
*(Stage N)* помечает, где пишется детализация.
|
||
|
||
## Домены
|
||
|
||
### Клиентское приложение *(Stage 7 / 8)*
|
||
Веб/приложение-клиент (Svelte + Vite) воплощает эти истории. **Играбельный срез**
|
||
(Stage 7) покрывает вход (гость или email), лобби «мои игры», старт авто-подбора,
|
||
игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача),
|
||
top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии,
|
||
обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и
|
||
профиль только для чтения. **Stage 8** добавляет управление друзьями (в т.ч.
|
||
одноразовые коды-приглашения) и блоками, дружеские приглашения в игру,
|
||
редактирование профиля и привязку email, экран статистики и просмотр истории
|
||
партии с экспортом GCG. В настройках также выбирается стиль подписей бонус-клеток
|
||
(новичок / классика / без текста). Подсказка **выставляет предложенные фишки на
|
||
доску** — игрок сам решает сделать ход, и подсказка не тратится, если ходов нет.
|
||
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
|
||
и ограничивает частоту повторов.
|
||
Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и
|
||
тему и ведёт в соответствующий по-язычный Telegram-канал; сама игра живёт по адресам
|
||
`/app/` (веб) и `/telegram/` (Telegram Mini App). Тема на странице эфемерна (берётся из
|
||
системной настройки, а не из сохранённой), выбор языка сохраняется.
|
||
|
||
### Личность и сессии *(Stage 1 / 6 / 9 / 15)*
|
||
Игрок приходит с платформы (сначала Telegram), через email-вход или как
|
||
эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий
|
||
session-токен; backend сопоставляет его с внутренним `user_id`. Запуск **Telegram
|
||
Mini App** авторизует по подписанным `initData` платформы, перекрашивает интерфейс
|
||
в цвета Telegram и — при первом контакте — задаёт язык интерфейса нового аккаунта по
|
||
языку Telegram-клиента. Сервис входа также объявляет **языки игры**, которые он
|
||
предлагает (набор из en/ru, минимум один), и они ограничивают выбор типа партии в
|
||
лобби. Telegram держит отдельного бота на язык (английский и русский, один игрок
|
||
охватывает обоих); бот, через которого игрок вошёл, задаёт его доступные языки, и от него
|
||
приходят его **внеигровые** уведомления. А уведомления по **партии** (ваш ход, конец партии,
|
||
nudge) приходят от бота **этой партии** — по языку партии, а не по тому боту, через которого
|
||
игрок входил последним. Гость — только сессия, с урезанными функциями (только
|
||
авто-подбор; без друзей, статистики и истории); заброшенный гость, не вошедший ни
|
||
в одну игру и простаивавший дольше окна удержания, удаляется сборщиком. Пока приложение открыто, клиент
|
||
держит живой стрим и получает обновления в реальном времени — ход соперника, ваш ход,
|
||
чат, nudge и найденный матч. Когда приложение **закрыто**, выбранные внеприложенческие
|
||
события (ваш ход, конец партии, nudge, найденный матч, приглашение или заявка в друзья)
|
||
приходят вместо этого **уведомлением в Telegram** — если только игрок не оставил
|
||
уведомления только в приложении (настройка профиля, **включена по умолчанию**).
|
||
Уведомление «ваш ход» называет соперника и пересказывает его последний ход — слово и
|
||
текущий счёт для результативного хода либо что он поменял фишки или пропустил, — а по
|
||
завершении партии приходит уведомление «конец партии» с твоим результатом и финальным
|
||
счётом (счёт читается, твой первым). Если связь пропадает или сервер ограничивает частоту
|
||
запросов, приложение не донимает ошибками: в шапке тихо крутится спиннер **«Подключение…»**,
|
||
пока идёт переподключение, действия, отправляющие данные на сервер, приостанавливаются до
|
||
восстановления связи (экран с серверными данными всё равно открывается — со спиннером — и
|
||
подгружается при реконнекте), а незавершённые чтения возобновляются сами — интерфейс остаётся
|
||
рабочим вместо красного баннера каждый раз.
|
||
|
||
### Аккаунты, привязка и слияние *(Stage 1 / 11)*
|
||
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
|
||
привязывает email (по confirm-коду) или свой Telegram (через веб-вход); гость,
|
||
привязавший первую личность, становится постоянным аккаунтом. Факт «личность уже
|
||
занята» не раскрывается до проверки кода/входа. Если привязываемая личность уже
|
||
принадлежит другому аккаунту, игроку показывают явное **необратимое**
|
||
подтверждение, и два аккаунта сливаются в тот, под которым он сейчас работает
|
||
(статистика суммируется, игры и друзья переносятся, дубликаты убираются), — кроме
|
||
случая, когда гость привязывает личность с уже существующим постоянным аккаунтом:
|
||
тогда сохраняется постоянный аккаунт, а игры гостя переходят в него. Слияние
|
||
запрещено, только пока у аккаунтов есть общая незавершённая игра.
|
||
|
||
### Лобби и подбор *(Stage 4 / 15)*
|
||
Нижнее tab-меню: **мои игры**, **профиль**. Список **мои игры** разбит на три секции —
|
||
*твой ход*, *ход соперника* и *завершённые* (пустые секции скрыты) — и упорядочен так,
|
||
что игры, ждущие твоего хода, идут первыми, дольше всего ждущие сверху, а игры на ходу
|
||
соперника и завершённые — самые свежие сверху; отображается компактным списком с
|
||
линиями-разделителями (Stage 17). Завершённую партию можно **убрать из своего списка**:
|
||
проведи по строке завершённой партии влево (или, на десктопе, нажми её **⋮**), чтобы открыть
|
||
**❌**, и нажми её. Удаление действует только для твоего аккаунта и необратимо — партия
|
||
исчезает лишь из твоего списка и остаётся в списках других игроков, отмены нет. Типы партий
|
||
на экране **Новая игра**
|
||
ограничены языками, которые поддерживает сервис входа игрока (английский → Scrabble;
|
||
русский → Scrabble + Erudite; двуязычный сервис показывает все три, а веб-клиент не
|
||
ограничен). Варианты показываются под **отображаемым именем** — оба варианта Scrabble
|
||
читаются как «Scrabble»/«Скрэббл», а Erudit — «Erudite»/«Эрудит» (по языку интерфейса),
|
||
и это же имя выносится в заголовок экрана игры. Это ограничивает только **старт** новой игры — и авто-подбор, и
|
||
приглашение друга, — поэтому игрок по-прежнему видит и играет существующие игры на
|
||
любом языке. Авто-подбор (всегда 2 игрока)
|
||
встаёт в пул по варианту и сводится со следующим ожидающим человеком; через 10 с
|
||
без человека подставляется робот (робот — в Stage 5). Игры с друзьями (2–4)
|
||
формируются приглашением игроков из списка друзей (приглашение, как и код друга,
|
||
можно отправить deep-link'ом в Telegram, который откроет его сразу): инициатор
|
||
выбирает настройки, и партия стартует, когда приняли все приглашённые — любой отказ отменяет приглашение, а без
|
||
ответа приглашение протухает через семь дней.
|
||
|
||
### Игровой процесс *(Stage 3)*
|
||
Выкладывание фишек, пас, обмен или сдача. Ход проверяется по словарю партии при
|
||
сдаче и считается; безлимитный предпросмотр сообщает, сколько принёс бы
|
||
предполагаемый ход и легален ли он. Инструмент проверки слова безлимитный и
|
||
предлагает пожаловаться на любой результат. Подсказки управляются настройками
|
||
партии — разрешены ли они и сколько их у каждого игрока на старте — и расходуют
|
||
личный кошелёк подсказок после исчерпания внутриигрового лимита. Партия
|
||
завершается, когда мешок пуст и игрок выложил стойку, после 6 подряд бесплодных
|
||
ходов, по сдаче, либо по таймауту хода (от 5 минут до 24 часов, дефолт 24 часа):
|
||
пропущенный ход означает авто-сдачу, кроме как когда игрок внутри своего
|
||
суточного окна отсутствия (away). В партии на двоих сдача или таймаут отдают
|
||
победу другому игроку, а вышедший сохраняет свои очки. В партии на троих-четверых
|
||
место вышедшего убирается, остальные играют дальше, и партия завершается, когда
|
||
остаётся один активный игрок; что делать с фишками вышедшего (вернуть в мешок или
|
||
убрать из игры) выбирается при создании партии, а его стойка никогда не
|
||
показывается остальным. **Композиция на доске сохраняется по партии**: расположение
|
||
фишек на стойке и выложенные, но не отправленные фишки сохраняются по мере составления
|
||
хода и восстанавливаются при возврате (в том числе на другом устройстве); игрок может
|
||
**раскладывать фишки и в ход соперника**, но такой черновик только позиционный —
|
||
предпросмотр счёта и отправка доступны лишь в собственный ход.
|
||
|
||
### Робот-соперник *(Stage 5)*
|
||
Если авто-подбор не находит человека за десять секунд, свободное место занимает
|
||
робот-соперник, и партия стартует без ожидания. Он задуман неотличимым от человека:
|
||
один раз за партию решает, играть ли на победу (примерно в 40% случаев, так что
|
||
человек выигрывает большинство партий), целится в близкий счёт, а не в разгром или
|
||
поддавки, и ходит с человеческим темпом — чаще короткие раздумья, изредка долгие, и
|
||
ночная пауза, подстроенная под день игрока. На nudge отвечает за несколько минут и
|
||
сам шлёт nudge, когда игрок надолго пропал. Носит человекоподобное имя, подходящее
|
||
языку партии (в русской партии — в основном русские имена); не общается в чате и
|
||
**молча игнорирует заявки в друзья** — заявка роботу остаётся в ожидании и истекает,
|
||
ровно как у человека, который не отвечает.
|
||
|
||
### Социальное: друзья, блок, чат, nudge *(Stage 4 / 8)*
|
||
Подружиться можно двумя способами: погасить **одноразовый код**, который выпускает
|
||
другой игрок (шесть цифр, действует двенадцать часов), либо отправить **заявку
|
||
тому, с кем вы играли** — он принимает, игнорирует (заявка истекает через тридцать
|
||
дней, после чего её можно отправить снова) или отклоняет (отказ блокирует ваши
|
||
повторные заявки, пока он сам не передаст вам код). Отмена своей висящей заявки
|
||
снимает её; удаление расторгает дружбу. В партии пункт меню **в друзья** для каждого
|
||
соперника отражает живое отношение: он показывает *заявка отправлена* (неактивный),
|
||
пока заявка висит или была отклонена, и *в друзьях* после принятия — обновляясь на месте
|
||
в момент ответа соперника и оставаясь верным после перезагрузки (Stage 17). Глобальная блокировка — отключить входящие
|
||
чат и/или заявки —
|
||
и блокировка конкретного игрока (пер-юзер блок скрывает его чат и запрещает заявки
|
||
и приглашения в игру в обе стороны, а также расторгает уже имеющуюся дружбу). Чат
|
||
партии — для быстрых реакций: сообщения короткие (до 60 символов) и не должны
|
||
содержать ссылок, email и телефонов, даже завуалированных. Nudge ожидаемого
|
||
соперника — не чаще раза в час (nudge — часть игрового чата); внеприложенческий
|
||
push доставляется через платформу.
|
||
Чат и инструмент проверки слова открываются **отдельными экранами** (с кнопкой «назад» в
|
||
партию), а новое сообщение в чате рисует **бейдж непрочитанного** на меню партии до открытия чата.
|
||
|
||
### Профиль и настройки *(Stage 4 / 8)*
|
||
Редактирование отображаемого имени (буквы, разделённые одиночным пробелом / «.» /
|
||
«_», с необязательной завершающей «.», до 32 символов и не более 5 спецсимволов —
|
||
пунктуации «.» / «_», пробелы не в счёт), таймзоны (выбор смещения от
|
||
UTC), суточного окна отсутствия (away; сетка по 10 минут, не более 12 часов, с
|
||
переходом через полночь) и переключателей блокировок. Форма профиля редактируется
|
||
сразу (без отдельного режима редактирования). Привязка email и Telegram, а также
|
||
слияние аккаунтов вынесены в раздел «Аккаунты, привязка и слияние» (Stage 11).
|
||
|
||
### История и статистика *(Stage 3 / 8)*
|
||
Завершённые партии архивируются в независимом от словаря виде и экспортируются
|
||
в GCG; экспорт доступен **только после завершения партии** (экспорт идущей партии
|
||
раскрыл бы журнал ходов), и клиент делится файлом `.gcg` там, где платформа это
|
||
поддерживает, иначе скачивает его. Статистика (только у постоянных аккаунтов):
|
||
победы, поражения, ничьи, макс. очков за партию и макс. очков за один ход (лучший
|
||
ход, уже включающий все образованные им слова и бонус за все фишки).
|
||
|
||
### Администрирование *(Stage 10)*
|
||
Оператор открывает серверную админ-консоль по адресу `${DOMAIN}/_gm` — её рендерит
|
||
backend; gateway закрывает её HTTP Basic Auth на публичном порту и проксирует
|
||
один-в-один. В консоли можно смотреть **пользователей** (профиль, статистика,
|
||
identity, их игры) и **игры** (сводка + места), разбирать **очередь жалоб на слова** —
|
||
закрывая каждую как reject / accept-add / accept-remove — и управлять **словарём**:
|
||
резидентные версии по вариантам, **горячая перезагрузка** новой версии из
|
||
`BACKEND_DICT_DIR/<version>/` и **список ожидающих правок**, выведенный из принятых
|
||
жалоб (он питает офлайн-пересборку и отмечается применённым после перезагрузки). Если
|
||
подключён Telegram-коннектор, оператор также может **написать пользователю** (по его
|
||
Telegram-identity) или **отправить пост в игровой канал**. Изменяющие действия
|
||
защищены проверкой same-origin; личность оператора не отслеживается.
|