1ed624eaf1
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 8s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
The landing now always opens in Russian (saved 🌐 choice still wins) —
browser-language detection made the indexed content nondeterministic
(Googlebot renders with en-US). landing.html gains the static Russian
SEO head: title/description, canonical pinned to the production origin,
Open Graph card (Telegram/VK link previews), twitter:card, JSON-LD,
theme-color and the favicon set; the SPA shell turns noindex and its
tab title becomes «Эрудит (Скрэббл)». New assets/icons generator
(same tile design as the VK loader) produces favicon.svg/ico,
apple-touch-icon.png and og-image.png into ui/public/, plus robots.txt.
443 lines
60 KiB
Markdown
443 lines
60 KiB
Markdown
# Scrabble Game — Функциональная спецификация
|
||
|
||
Пользовательские сценарии по доменам: что делает каждая видимая пользователю
|
||
операция. Это зеркало [`FUNCTIONAL.md`](FUNCTIONAL.md) для владельца проекта;
|
||
**авторитетна английская версия**. Любую точечную правку переносим в том же
|
||
патче (переводим только изменённые абзацы).
|
||
|
||
## Домены
|
||
|
||
### Клиентское приложение
|
||
Веб/приложение-клиент (Svelte + Vite) воплощает эти истории. Он
|
||
покрывает вход (гость или email), лобби «мои игры», старт авто-подбора,
|
||
игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача),
|
||
top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии,
|
||
обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и
|
||
профиль только для чтения. Он также включает управление друзьями (в т.ч.
|
||
одноразовые коды-приглашения) и блоками, дружеские приглашения в игру,
|
||
редактирование профиля и привязку email, экран статистики и просмотр истории
|
||
партии с экспортом GCG. В настройках также выбирается стиль подписей бонус-клеток
|
||
(новичок / классика / без текста). Подсказка **выставляет предложенные фишки на
|
||
доску** — игрок сам решает сделать ход, и подсказка не тратится, если ходов нет.
|
||
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
|
||
и ограничивает частоту повторов.
|
||
Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и
|
||
тему и ведёт в Telegram-канал игры и в VK Mini App; сама игра живёт по адресам
|
||
`/app/` (веб), `/telegram/` (Telegram Mini App) и `/vk/` (VK Mini App). Тема на странице эфемерна (берётся из
|
||
системной настройки, а не из сохранённой), выбор языка сохраняется. Страница всегда
|
||
открывается на **русском** — язык браузера не определяется автоматически (роботы
|
||
рендерят страницу с произвольным языком, и автоопределение сделало бы
|
||
проиндексированный контент недетерминированным); сохранённый выбор 🌐 по-прежнему
|
||
главнее. Страница несёт статическую русскую SEO-«шапку» (title/description, карточка
|
||
Open Graph, которую используют превью ссылок в Telegram/VK, канонический адрес
|
||
продакшен-домена, JSON-LD, набор favicon и `robots.txt`), а оболочка SPA помечена
|
||
`noindex` — индексируется только посадочная страница.
|
||
|
||
### Первый запуск: онбординг
|
||
При первом открытии приложения игрока один раз проводят по интерфейсу лёгким
|
||
**coachmark-оверлеем**: затемнённый слой показывает по одной подсказке-«облачку» за раз, каждое
|
||
«хвостиком» указывает на реальный элемент управления, а **тап в любом месте** переходит к
|
||
следующей; после последней подсказки оверлей убирается навсегда. Серий две. Серия **лобби**
|
||
указывает на вкладки ⚙️ настроек, ✏️ статистики и 🎲 новой игры. Серия **игры**, на первой партии
|
||
игрока, указывает на шапку со счётом (история ходов / чат / проверка слова), кнопки 🔄 пас-и-обмен,
|
||
🛟 подсказок и 🔀 перемешивания, и на стойку с фишками. Серия считается просмотренной только
|
||
**после последней подсказки**, поэтому выход на середине в следующий раз покажет её с начала;
|
||
запоминается она по устройству. Триггер серии лобби — попадание в лобби, так что игрок, пришедший
|
||
по deeplink сразу в Настройки → Друзья, всё равно получит проводку по лобби при первом возврате.
|
||
Обе серии одинаково работают в portrait и landscape (облачка переустанавливаются туда, куда
|
||
переезжают элементы), тексты подсказок локализованы (en/ru). Рекламный баннер скрывается, пока
|
||
оверлей показан, и возвращается по закрытию.
|
||
|
||
### Личность и сессии
|
||
Игрок приходит с платформы (сначала Telegram), через email-вход или как
|
||
эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий
|
||
session-токен; backend сопоставляет его с внутренним `user_id`. Запуск **Telegram
|
||
Mini App** авторизует по подписанным `initData` платформы, перекрашивает интерфейс
|
||
в цвета Telegram (перекрашиваясь вживую при смене светлой/тёмной темы Telegram) и
|
||
вписывается в безопасные зоны экрана (safe-area), а — при первом контакте — задаёт язык
|
||
интерфейса нового аккаунта по
|
||
языку Telegram-клиента. Если запуск не может достучаться до бэкенда (например, во время
|
||
деплоя), Mini App тихо повторяет попытки, а затем показывает небольшой экран «не удалось
|
||
загрузить» с кнопкой **Повторить**, вместо того чтобы сбрасывать на веб-вход, которому внутри
|
||
Telegram не место. Запуск **VK Mini App** работает так же: авторизует по подписанным
|
||
launch-параметрам VK (их проверяет gateway), и при первом контакте задаёт язык интерфейса
|
||
нового аккаунта по `vk_language`, а отображаемое имя — по профилю VK (читается на клиенте,
|
||
так как VK не кладёт имя в подписанный запуск). Пока тема в режиме «авто», приложение следует
|
||
светлой/тёмной схеме VK-клиента, а раскладка обходит нижнюю home-bar VK на мобильных. Тот же экран
|
||
тихого повтора «не удалось
|
||
загрузить» действует и внутри VK. Telegram держит **единого бота**: все игроки пользуются одним
|
||
и тем же ботом, а весь его чат и внеприложенческие уведомления пишутся на **языке
|
||
интерфейса** самого игрока (en/ru). Рядом с основным может работать отдельный опциональный
|
||
**промо-бот** — его единственная задача отвечать на `/start` коротким сообщением и кнопкой,
|
||
открывающей приложение **основного** бота, где игрок выбирает нужный вариант игры; это точка входа
|
||
для онбординга, не затрагивающая больше ничего. Гость — только сессия, с урезанными функциями (только
|
||
авто-подбор; без друзей, статистики и истории); заброшенный гость, не вошедший ни
|
||
в одну игру и простаивавший дольше окна удержания, удаляется сборщиком. Пока приложение открыто, клиент
|
||
держит живой стрим и получает обновления в реальном времени — ход соперника, ваш ход,
|
||
чат, nudge и подключение соперника к игре, в которой вы ждёте. Каждое обновление приходит самим событием и применяется на месте без
|
||
перезагрузки — доска обновляется бесшовно, а приглашённая игра открывается мгновенно. Когда приложение **закрыто**, выбранные внеприложенческие
|
||
события (ваш ход, конец партии, nudge, приглашение или заявка в друзья)
|
||
приходят вместо этого **уведомлением в Telegram** — если только игрок не оставил
|
||
уведомления только в приложении (настройка профиля, **включена по умолчанию**).
|
||
Уведомление «ваш ход» называет соперника и пересказывает его последний ход — слово и
|
||
текущий счёт для результативного хода либо что он поменял фишки или пропустил, — а по
|
||
завершении партии приходит уведомление «конец партии» с твоим результатом и финальным
|
||
счётом (счёт читается, твой первым). Если связь пропадает или сервер ограничивает частоту
|
||
запросов, приложение не донимает ошибками: в шапке тихо крутится спиннер **«Подключение…»**,
|
||
пока идёт переподключение, действия, отправляющие данные на сервер, приостанавливаются до
|
||
восстановления связи (экран с серверными данными всё равно открывается — со спиннером — и
|
||
подгружается при реконнекте), а незавершённые чтения возобновляются сами — интерфейс остаётся
|
||
рабочим вместо красного баннера каждый раз.
|
||
|
||
### Аккаунты, привязка и слияние
|
||
_Вход сейчас только через провайдера, поэтому UI привязки в профиле временно скрыт; он
|
||
вернётся, когда появится анонимный `/app/`-гость (для апгрейда которого он и нужен). Описание
|
||
ниже — на этот случай._
|
||
|
||
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
|
||
привязывает email (по confirm-коду) или свой Telegram (через веб-вход); гость,
|
||
привязавший первую личность, становится постоянным аккаунтом. Факт «личность уже
|
||
занята» не раскрывается до проверки кода/входа. Если привязываемая личность уже
|
||
принадлежит другому аккаунту, игроку показывают явное **необратимое**
|
||
подтверждение, и два аккаунта сливаются в тот, под которым он сейчас работает
|
||
(статистика суммируется, игры и друзья переносятся, дубликаты убираются), — кроме
|
||
случая, когда гость привязывает личность с уже существующим постоянным аккаунтом:
|
||
тогда сохраняется постоянный аккаунт, а игры гостя переходят в него. Слияние
|
||
запрещено, только пока у аккаунтов есть общая незавершённая игра.
|
||
|
||
### Лобби и подбор
|
||
При холодном запуске лобби встречает игрока короткой **заставкой загрузки** — фишки Scrabble
|
||
складывают небольшой кроссворд из слов **ЭРУДИТ / ЗАГРУЗКА / ОЖИДАНИЕ** — и она исчезает, как
|
||
только список игр готов, поэтому на медленном соединении список не мигает «пустым» состоянием.
|
||
|
||
В лобби — список **мои игры** и нижний tab-bar (новая игра, статистика и вкладка
|
||
**⚙️ настройки**, открывающая хаб настроек — настройки, профиль, друзья, о программе).
|
||
Список **мои игры** разбит на три секции —
|
||
*твой ход*, *ход соперника* и *завершённые* (пустые секции скрыты) — и упорядочен так,
|
||
что игры, ждущие твоего хода, идут первыми, дольше всего ждущие сверху, а игры на ходу
|
||
соперника и завершённые — самые свежие сверху; внутри секций «твой ход» и «ход соперника»
|
||
игра с любым непрочитанным уведомлением всплывает наверх (секция «завершённые» сохраняет
|
||
свой порядок). Отображается компактным списком с линиями-разделителями. Пока лобби открыто и партия из списка **переходит на твой ход или
|
||
завершается**, её значок статуса **дважды моргает**, привлекая внимание (переход на ход
|
||
соперника — без моргания, применяется на месте). Завершённую партию можно **убрать из своего списка**:
|
||
проведи по строке завершённой партии влево (или, на десктопе, нажми её **⋮**), чтобы открыть
|
||
**❌**, и нажми её. Удаление действует только для твоего аккаунта и необратимо — партия
|
||
исчезает лишь из твоего списка и остаётся в списках других игроков, отмены нет. Завершённая
|
||
**игра с ИИ (🤖), покинутая тобой** — через сдачу или истечение 7-дневного таймаута — убирается
|
||
из *завершённых* автоматически, без свайпа; нормально доигранная игра с ИИ остаётся, пока ты не
|
||
уберёшь её, и никакие другие типы партий автоматически не убираются. Типы партий
|
||
на экране **Новая игра**
|
||
ограничены выбранными игроком **предпочтениями вариантов** (см. «Профиль и
|
||
настройки») — сначала Эрудит, по умолчанию только Эрудит. Варианты показываются под **отображаемым именем** — оба варианта Scrabble
|
||
читаются как «Scrabble»/«Скрэббл», а Erudit — «Erudite»/«Эрудит» (по языку интерфейса),
|
||
и это же имя выносится в заголовок экрана игры. Это ограничивает только **старт** игры, которую ты
|
||
инициируешь, — авто-подбор, игру с ИИ и приглашение друга, которое ты **создаёшь**, — поэтому игрок
|
||
по-прежнему видит и играет существующие игры любого варианта, а при **приглашении** в партию её
|
||
можно принять в любом варианте.
|
||
|
||
**Быстрая игра** даёт выбрать соперника — **ИИ** (по умолчанию) или **случайного игрока**. С **ИИ**
|
||
вы сразу начинаете против 🤖, который присоединяется и отвечает мгновенно: ожидания нет, чат и nudge
|
||
нет — только проверка слова, — «добавить в друзья» не показывается; вместо лимита на ход вы
|
||
проигрываете только после **7 дней без хода** (так что ИИ-партию можно спокойно забросить, а сам ИИ
|
||
никогда не «просрочит» ход). Выбор **случайного игрока** — это авто-подбор (всегда 2 игрока), который
|
||
сразу **помещает вас в игру, и вы ждёте соперника прямо
|
||
в ней**: если ваш ход — вы уже можете ходить, иначе просто рассматриваете свои фишки. Пока соперник не
|
||
присоединился, на карточке соперника (и в строке игры в лобби) написано **«Поиск соперника...»**, а
|
||
сдача, чат и nudge недоступны. Другой игрок, ищущий тот же вариант и правило, присоединяется к вашей
|
||
игре; если такого нет — через **1,5–3 минуты** свободное место занимает робот, так что игра всегда
|
||
стартует, и экран новой игры подсказывает, что можно закрыть приложение на время ожидания и вернуться
|
||
позже. Для русских игр (авто-подбор или приглашение) на экране
|
||
новой игры есть опция **«Несколько слов за ход»** (по умолчанию **выключена**): выключена —
|
||
упрощённое **правило одного слова**: настоящим словом должно быть только слово, выложенное
|
||
вдоль линии хода (и оно должно пересекать или продолжать уже стоящие на доске буквы вдоль
|
||
этой линии), а случайные перпендикулярные слова игнорируются и не засчитываются;
|
||
включена — обычный скрэббл. Английские игры всегда по стандартным правилам и тоггл не
|
||
показывают. В авто-подборе выбор входит в ключ подбора, поэтому игрок сводится только с теми,
|
||
кто выбрал то же правило. Игры с друзьями (2–4)
|
||
формируются приглашением игроков из списка друзей (приглашение, как и код друга,
|
||
можно отправить deep-link'ом в Telegram, который откроет его сразу — на VK, который не передаёт Mini App
|
||
никакой нагрузки, ссылка лишь открывает приложение, а скопированный код получатель вводит вручную): инициатор
|
||
выбирает настройки, и партия стартует, когда приняли все приглашённые — любой отказ отменяет приглашение, а без
|
||
ответа приглашение протухает через семь дней.
|
||
|
||
**Лимит одновременных партий.** Игрок может держать одновременно не более **10 активных
|
||
быстрых партий** — считаются и идущие, и ещё ищущие соперника авто-подбор/AI-партии, но **не**
|
||
игры с друзьями, созданные приглашением. На лимите кнопка **«Новая игра»** становится неактивной
|
||
(серой), а под списками появляется ненавязчивая строка *«Вы достигли лимита одновременных
|
||
партий»*; и кнопка, и надпись снимаются автоматически, как только активная партия завершается и
|
||
счётчик опускается ниже десяти. Лимит запрещает **создавать** любую партию (быструю или
|
||
приглашение в игру с друзьями — у них общий вход «Новая игра»), но **принимать входящее
|
||
приглашение никогда не запрещено** — так игры с друзьями ограничиваются «с другого конца» (на
|
||
лимите их нельзя инициировать), а присоединиться к партии по приглашению можно всегда.
|
||
|
||
### Игровой процесс
|
||
Выкладывание фишек, пас, обмен или сдача. Пас и обмен — один элемент управления:
|
||
без выбранных фишек — пас, с выбранными — обмен. Фишки кладутся без выбора
|
||
направления — игра сама определяет ориентацию хода, поэтому одна фишка, продолжающая
|
||
уже лежащее
|
||
слово (по столбцу или по строке), принимается. Ход проверяется по словарю партии при
|
||
сдаче и считается; безлимитный предпросмотр показывает слово (или слова), которое
|
||
образует предполагаемый ход, и его очки — либо что ход недопустим, — и ход можно
|
||
отправить только после подтверждения, что он допустим. Предпросмотр считается **на устройстве**
|
||
и отвечает мгновенно, как только словарь партии загружен — иначе при первом открытии показывается
|
||
короткий прогрев, — и уходит на сервер, если локальный словарь недоступен. Инструмент проверки слова безлимитный и
|
||
предлагает пожаловаться на любой результат; для найденного слова он также даёт ссылку на
|
||
внешний справочный словарь (gramota.ru для русских игр, scrabblewordfinder.org для английских),
|
||
чтобы его посмотреть. Подсказки управляются настройками
|
||
партии — разрешены ли они и сколько их у каждого игрока на старте — и расходуют
|
||
личный кошелёк подсказок после исчерпания внутриигрового лимита. Партия
|
||
завершается, когда мешок пуст и игрок выложил стойку, после 6 подряд бесплодных
|
||
ходов, по сдаче, либо по таймауту хода (от 5 минут до 24 часов, дефолт 24 часа):
|
||
пропущенный ход означает авто-сдачу, кроме как когда игрок внутри своего
|
||
суточного окна отсутствия (away). В партии на двоих сдача или таймаут отдают
|
||
победу другому игроку, а вышедший сохраняет свои очки. В партии на троих-четверых
|
||
место вышедшего убирается, остальные играют дальше, и партия завершается, когда
|
||
остаётся один активный игрок; что делать с фишками вышедшего (вернуть в мешок или
|
||
убрать из игры) выбирается при создании партии, а его стойка никогда не
|
||
показывается остальным. **Композиция на доске сохраняется по партии**: расположение
|
||
фишек на стойке и выложенные, но не отправленные фишки сохраняются по мере составления
|
||
хода и восстанавливаются при возврате (в том числе на другом устройстве); игрок может
|
||
**раскладывать фишки и в ход соперника**, но такой черновик только позиционный —
|
||
предпросмотр счёта и отправка доступны лишь в собственный ход.
|
||
|
||
### Робот-соперник
|
||
Если авто-подбор не находит человека за время ожидания (1,5–3 минуты), свободное место в игре,
|
||
в которой вы уже ждёте, занимает робот-соперник. Он задуман неотличимым от человека:
|
||
один раз за партию решает, играть ли на победу (примерно в 40% случаев, так что
|
||
человек выигрывает большинство партий), целится в близкий счёт, а не в разгром или
|
||
поддавки, время от времени делает один ход вопреки этому плану — неожиданный отрыв или
|
||
слабый ход, — но к концу партии (когда мешок пуст) держится плана, и ходит с
|
||
человеческим темпом — чаще короткие раздумья, изредка долгие, и ночная пауза,
|
||
подстроенная под день игрока; когда партия уже явно решена и обе стороны только пасуют,
|
||
он перестаёт её затягивать — отвечает на вынужденные пасы примерно в темпе самого игрока,
|
||
а не после долгого раздумья. На nudge отвечает за несколько минут и, когда игрок
|
||
держит его в ожидании, сам шлёт nudge редко — примерно раз в 9-12 часов и никогда во время
|
||
своей ночной паузы. Носит человекоподобное имя, подходящее
|
||
языку партии — каждую партию новое, из широкого международного пула реальных имён и
|
||
никнеймов, так что арена кажется полной разных игроков (в русской партии — в основном
|
||
русские имена, без восточноазиатских письменностей; в международной — весь спектр); не общается в чате и
|
||
**молча игнорирует заявки в друзья** — заявка роботу остаётся в ожидании и истекает,
|
||
ровно как у человека, который не отвечает.
|
||
|
||
Тот же робот стоит и за **честной игрой против ИИ**, которую игрок выбирает напрямую (выше). Там он
|
||
не притворяется: везде показан как **🤖**, присоединяется и ходит сразу (без раздумий и ночной
|
||
паузы), сохраняет ту же силу (по-прежнему играет на победу лишь примерно в 40% партий, с теми же
|
||
редкими ходами вопреки плану, затухающими к эндшпилю), а чат, nudge и «добавить в друзья» выключены. Партии с ИИ — это **тренировка**: они не идут в статистику игрока.
|
||
|
||
### Социальное: друзья, блок, чат, nudge
|
||
Подружиться можно двумя способами: погасить **одноразовый код**, который выпускает
|
||
другой игрок (шесть цифр, действует двенадцать часов), либо отправить **заявку
|
||
тому, с кем вы играли** — он принимает, игнорирует (заявка истекает через тридцать
|
||
дней, после чего её можно отправить снова) или отклоняет (отказ блокирует ваши
|
||
повторные заявки, пока он сам не передаст вам код). Отмена своей висящей заявки
|
||
снимает её; удаление расторгает дружбу. В партии карточка счёта каждого соперника (при открытой
|
||
истории ходов) несёт два контрола: **в друзья 🤝** справа и **блокировка ✖️** слева. Каждый
|
||
подтверждается тапом по затухающей ✅ — на месте счёта карточка показывает *В друзья?* (или
|
||
*Блокируем?* красным) во время подтверждения, — и пока подтверждается один, второй скрыт, чтобы
|
||
они не пересекались. 🤝 становится **неактивным**, пока заявка висит или была отклонена, и
|
||
**исчезает** после принятия; **оба контрола исчезают, а имя соперника становится зачёркнутым**
|
||
после блокировки. Они обновляются на месте в момент изменения отношения и остаются верны после
|
||
перезагрузки. Блокировка (как и заявка в друзья) применяется немедленно и подтверждается живым
|
||
событием; при сбое транспорта контрол возвращается в прежнее состояние.
|
||
|
||
Глобальная блокировка — отключить входящие чат и/или заявки — либо блокировка **конкретного
|
||
игрока**. Пер-юзер блок **односторонний и незаметный**: вы перестаёте получать от этого человека
|
||
всё — чат, nudge, заявки в друзья и приглашения в игру, — и матчмейкер никогда не сводит вас с
|
||
ним, а он **ничего не замечает**. Его сообщения и nudge по-прежнему отправляются по обычным
|
||
правилам, но не доходят до вас (всё, что было бы для вас непрочитанным, сразу помечается
|
||
прочитанным), а присланные им заявка или приглашение сохраняются, но никогда не всплывают. Блок
|
||
**перекрывает, но не удаляет** имеющуюся дружбу (поэтому можно заблокировать друга, а он
|
||
продолжает видеть вас в друзьях); активные игры не прерываются — их можно доигрывать, при этом у
|
||
заблокированного соперника «подвал» чата скрыт (остаётся только лог). Блокировка с карточки в
|
||
партии повторяет блокировку в **Настройках → Друзья**; **разблокировка** и **удаление из друзей**
|
||
есть только там.
|
||
В **Настройках → Друзья** каждый друг — однострочник, чей правый кебаб (⋮) выдвигает
|
||
иконки-действия **заблокировать 🚫** и **удалить ✖️**, и каждое действие подтверждается
|
||
диалогом с именем друга (*Заблокировать?* / *Удалить из друзей?*).
|
||
Блокировка **авто-матч соперника, который втайне робот**, в этой партии ведёт себя так же
|
||
(зачёркнутое имя, скрытый «подвал») и в списке заблокированных показывается под тем именем,
|
||
которое ты видел, но записывается только для этой партии — маскировка сохраняется, общий
|
||
аккаунт робота глобально не блокируется, и матчмейкер продолжает давать роботов (так что
|
||
заблокировать себя без соперников нельзя). Отправка 🤝 такому **замаскированному роботу** так же
|
||
записывает заявку только для этой партии (привязанной к месту, поэтому она читается как
|
||
*отправленная* здесь, не затрагивая этого робота в твоих других играх); робот её **игнорирует**,
|
||
заявка не показывается в **Настройки → Друзья** и автоматически удаляется примерно через неделю
|
||
после завершения партии. Чат
|
||
партии — для быстрых реакций: сообщения короткие (до 60 символов) и не должны
|
||
содержать ссылок, email и телефонов, даже завуалированных. В свой ход можно отправить
|
||
**одно сообщение за ход**; после отправки поле сменяется короткой подписью до следующего
|
||
хода. Nudge ожидаемого
|
||
соперника — не чаще раза в час (nudge — часть игрового чата); и всплывашка в приложении,
|
||
и внеприложенческий push (доставляется через платформу) **называют отправителя**
|
||
(«<соперник>: жду вашего хода»).
|
||
Чат и инструмент проверки слова — один **экран связи** со вкладками **💬 чат** / **🔎 словарь**,
|
||
открываемый по 💬 в шапке истории ходов (с кнопкой «назад» в партию). В ИИ-партии чата нет, поэтому её
|
||
экран связи — только проверка слова; а после завершения ИИ-партии (чата нет, словарь закрыт) сам вход
|
||
💬 исчезает. Непрочитанная запись чата —
|
||
сообщение **или nudge** от соперника — зажигает маленькую **точку** рядом с партией в
|
||
**лобби** и на **строке счёта** партии, **красную при непрочитанном сообщении и мягкую жёлтую,
|
||
когда непрочитаны только nudge'и**. Открытие **истории ходов** считается прочтением чата, даже
|
||
без захода в него: точка гаснет, а значок 💬 **дважды мигает**. Nudge также гаснет в момент, когда
|
||
его получатель **делает ход**, и **все nudge'и партии гаснут по её завершении** (после конца
|
||
партии бейдж неактуален) — но непрочитанные сообщения чата остаются непрочитанными.
|
||
|
||
### Профиль и настройки
|
||
Редактирование отображаемого имени (буквы, разделённые одиночным пробелом / «.» /
|
||
«_», с необязательной завершающей «.» или хвостом до пяти цифр, до 32 символов и не
|
||
более 5 спецсимволов — пунктуации «.» / «_», пробелы и цифры не в счёт), таймзоны (выбор смещения от
|
||
UTC; при создании аккаунта она подставляется из определённого смещения устройства — чтобы
|
||
игры с роботом таймились правильно ещё до открытия этой формы), суточного окна отсутствия
|
||
(away; сетка по 10 минут, не более 12 часов, с переходом через полночь) и переключателей блокировок. Форма профиля редактируется
|
||
сразу (без отдельного режима редактирования). Привязка email и Telegram, а также
|
||
слияние аккаунтов вынесены в раздел «Аккаунты, привязка и слияние». Внутри Telegram
|
||
Mini App пункт **Settings** в системном меню «⋮» Telegram также открывает этот экран, а
|
||
ваши настройки отображения (тема, стиль подписей клеток и reduce-motion — кроме языка
|
||
интерфейса, который следует за аккаунтом) синхронизируются между вашими устройствами в
|
||
Telegram.
|
||
|
||
**Предпочтения (в какие варианты тебя можно подбирать).** Настройка профиля задаёт варианты
|
||
игры — Эрудит, русский Scrabble и английский Scrabble, показанные **сначала Эрудит**, — в
|
||
которые ты разрешаешь себя подбирать; **новый аккаунт стартует только с Эрудитом** — если только
|
||
он не создан по **диплинку промо-бота**, который дополнительно включает **английский Scrabble**, —
|
||
и нужно оставить выбранным **хотя бы один**. Именно этот список предлагает **Новая игра**, когда ты
|
||
запускаешь партию (авто-подбор, игра с ИИ или приглашение друга, которое ты создаёшь), — не
|
||
включённый вариант не предлагается, и сервер его отклоняет. На партии, в которые тебя
|
||
**приглашают**, это не влияет: приглашённый друг может принять приглашение в **любом** варианте,
|
||
а открывать и играть существующие игры любого варианта можно всегда.
|
||
|
||
### Обратная связь
|
||
Зарегистрированный игрок обращается к операторам из Settings → Info: экран **«Обратная связь»** с
|
||
сообщением (до 1024 символов) и необязательным вложением (один файл, до ~1 МБ — изображения, PDF,
|
||
текст/лог, офисные документы, RTF или архивы; неподходящий файл отклоняется на форме без перечисления
|
||
допустимых типов). После отправки форма очищается и подтверждает «Ваше сообщение отправлено», а
|
||
повторная отправка блокируется, пока оператор не разобрал обращение — при повторном входе на экране
|
||
«Ожидаем рассмотрения вашего последнего обращения». Ответ оператора показывается под формой как
|
||
«Ответ на ваше последнее сообщение» (ссылки в нём открываются в новой вкладке); он помечается
|
||
прочитанным, как только экран его показал, и исчезает через неделю. Бейдж на вкладке Settings (и на Info внутри неё) сигналит о непрочитанном
|
||
ответе. Гость отправлять обратную связь не может (пункт скрыт). Игрок, которому оператор запретил
|
||
обратную связь (роль, а не полная блокировка аккаунта), видит кнопку отправки недоступной.
|
||
|
||
### Чат поддержки в Telegram
|
||
С операторами можно поговорить и прямо из Telegram-бота: всё, что пользователь присылает боту,
|
||
кроме `/start` — текст, фото, голосовое, файл, — пересылается в закрытую группу поддержки операторов
|
||
и собирается в отдельную ветку на пользователя. Пользователь не получает автоответа; оператор
|
||
отвечает из этой ветки, и бот доставляет ответ обратно обычным сообщением — для пользователя это
|
||
тихий диалог один на один. Операторы могут заблокировать пользователя (тогда бот молча игнорирует
|
||
его сообщения) или очистить сообщения ветки. Это независимо от встроенной «Обратной связи» выше —
|
||
канал для тех, кто пишет боту напрямую, а не через приложение.
|
||
|
||
### История и статистика
|
||
Завершённые партии архивируются в независимом от словаря виде и экспортируются
|
||
в GCG; экспорт доступен **только после завершения партии** и никогда — для
|
||
тренировочной партии с ИИ (экспорт идущей партии раскрыл бы журнал ходов, а партия
|
||
с ИИ одноразовая). Клиент делится файлом `.gcg` там, где платформа это поддерживает;
|
||
в Android-приложении (Telegram / VK), где нет ни Web Share, ни рабочей загрузки файла, копирует GCG
|
||
в буфер обмена (с подтверждающим тостом); иначе скачивает файл. Статистика (только у постоянных аккаунтов):
|
||
победы, поражения, ничьи, макс. очков за партию и макс. очков за один ход (лучший
|
||
ход, уже включающий все образованные им слова и бонус за все фишки). Также
|
||
показываются **число ходов** игрока (его выкладки — пасы и обмены не считаются) и
|
||
**доля подсказок** (процент таких ходов, на которых бралась подсказка).
|
||
Лучший ход также даётся **с разбивкой по вариантам игры** — **само слово**,
|
||
нарисованное игровыми фишками (wildcard показывает свою букву без очков), по одной
|
||
строке на каждый сыгранный вариант; варианты без ходов не выводятся.
|
||
Партия, которую больше нельзя продолжить — из-за изменения правил более ранний ход
|
||
стал бы недопустимым, — закрывается **ничьёй** в момент открытия её игроком, а не
|
||
остаётся висеть с ошибкой: в конце истории ходов показывается обезличенная заметка
|
||
организатора (партию не удалось продолжить), и та же заметка попадает в экспорт GCG.
|
||
|
||
### Администрирование
|
||
Оператор открывает серверную админ-консоль по адресу `${DOMAIN}/_gm` — её рендерит
|
||
backend; gateway закрывает её HTTP Basic Auth на публичном порту и проксирует
|
||
один-в-один. В консоли можно смотреть **пользователей** (профиль, статистика,
|
||
identity, их игры) и **игры** — сводка, места, записанная **жеребьёвка первого хода** (кто что
|
||
вытащил и кто ходит первым) и простой **пошаговый реплей доски** всей партии, — разбирать
|
||
**очередь жалоб на слова** —
|
||
закрывая каждую как reject / accept-add / accept-remove — и управлять **словарём**:
|
||
активная версия, на которую опираются новые партии, резидентные версии по вариантам,
|
||
онлайн-**обновление словаря** (загрузить релизный архив `scrabble-dawg-vX.Y.Z.tar.gz`,
|
||
посмотреть предпросмотр добавленных и удалённых слов по каждому варианту относительно
|
||
активного словаря, затем установить — версия записывается, загружается и становится
|
||
активной; версии неизменяемы, а идущие партии остаются на своей) и **список ожидающих
|
||
правок**, выведенный из принятых жалоб (он питает офлайн-пересборку и отмечается
|
||
применённым после обновления). Если
|
||
подключён Telegram-бот, оператор также может **написать пользователю** (по его
|
||
Telegram-identity) или **отправить пост в игровой канал**. Изменяющие действия
|
||
защищены проверкой same-origin; личность оператора не отслеживается.
|
||
|
||
Консоль также показывает **злоупотребление лимитами**: страница **Throttled**
|
||
перечисляет недавно затроттленных пользователей/IP по отчётам gateway (окно в памяти —
|
||
сбрасывается при рестарте backend) и аккаунты с действующим мягким **high-rate
|
||
флагом**. Аккаунт, устойчиво превышающий настраиваемый порог отказов, помечается
|
||
автоматически — маркер обратим, виден бейджем в списке пользователей и на карточке
|
||
аккаунта и **никогда не блокирует игру**; оператор рассматривает и снимает его с
|
||
карточки пользователя. Сам флаг аккаунта баном не является. В **проде** та же
|
||
страница дополнительно перечисляет **активные баны по IP**, которые применяет gateway:
|
||
временную блокировку IP клиента, превысившего порог наплыва, либо задевшего скрытую
|
||
**honeypot**-ловушку или подброшенный **honeytoken** — высокодостоверный признак
|
||
сканера или враждебного бота, но не нормального игрока. У каждого бана показаны причина
|
||
и срок, рядом действие **Unban**; баны истекают сами, а оператор может снять бан раньше.
|
||
Баны по IP — защита только для прода: общий тестовый контур не различает своих клиентов,
|
||
поэтому там не применяется.
|
||
|
||
Консоль также позволяет оператору **вручную заблокировать** аккаунт — жёсткий аналог мягкого
|
||
high-rate флага. С карточки пользователя оператор блокирует аккаунт **навсегда** или **до даты**
|
||
(пресеты: 1 день / 3 дня / 1 неделя / 1 месяц либо произвольная дата в UTC), при желании указав
|
||
**причину** из редактируемого **списка en+ru**, который ведётся на странице **Reasons**;
|
||
заблокированный игрок видит причину на своём языке. Блокировка мгновенно засчитывает **поражение**
|
||
во всех активных партиях игрока (каждый соперник побеждает, как при сдаче) и убирает его из
|
||
матчмейкинга. Заблокированный игрок — при открытии приложения или любом действии — видит
|
||
терминальный экран: *«Ваша учётная запись заблокирована»* (постоянная) или *«…заблокирована до
|
||
<дата, время>»* (временная, в его часовом поясе), плюс причину, если она была указана, и
|
||
приложение прекращает любое фоновое общение с сервером. Временная блокировка снимается сама по
|
||
истечении срока; оператор также может **разблокировать** с карточки пользователя в любой момент
|
||
(уже проигранные партии не возвращаются).
|
||
|
||
Там, где бот ведёт **привязанный к каналу чат-обсуждение**, по умолчанию писать может каждый, а бот
|
||
**глушит** игрока, который **не зарегистрирован** или **заблокирован**, и снимает мьют, как только тот
|
||
зарегистрируется или будет разблокирован. То есть незарегистрированного новичка, написавшего в чат,
|
||
бот глушит (промо-бот направляет его в игру зарегистрироваться, после чего бот возвращает голос), а
|
||
зарегистрированный незаблокированный игрок просто пишет. Оператор также может **замьютить игрока только
|
||
в чате** — роль `chat_muted` на карточке пользователя — без полной блокировки аккаунта; блокировка
|
||
аккаунта всё равно мьютит его в чате. Мьют и размьют срабатывают для игрока, уже находящегося в чате;
|
||
того, кого в чате нет, это не затрагивает до его следующего входа.
|
||
|
||
С карточки пользователя оператор также может **пополнить кошелёк подсказок** игрока: аддитивное
|
||
начисление (1–100 подсказок за раз), которое **только увеличивает** баланс на карточке. Начисления
|
||
**только в плюс** — понизить кошелёк из консоли нельзя (игрок теряет подсказки только тратя их в
|
||
партии), поэтому ошибочно начисленное через консоль там не отнять.
|
||
|
||
Консоль ведёт и очередь **обратной связи** (`/_gm/feedback`): присланные игроками сообщения с фильтром
|
||
**непрочитанные / прочитанные / архив** и поиском по пользователю, каждое — с отправителем, источником,
|
||
каналом (и языком бота — en/ru — для сообщения из Telegram), языком интерфейса отправителя,
|
||
**версией приложения**, с которой отправлено, IP, временем подачи (в трёх зонах — UTC, зоне браузера
|
||
на момент отправки и сохранённой зоне отправителя, каждая — «N/A», если неизвестна) и вложением. Оператор может пометить сообщение прочитанным, **ответить** игроку (доставка
|
||
в приложение), отправить в архив, удалить или удалить все сообщения этого игрока — и вместе с удалением
|
||
**запретить игроку обратную связь** (роль `feedback_banned`, отличная от полной блокировки аккаунта:
|
||
останавливает только отправку обратной связи). Роли перечислены и выдаются/снимаются на карточке
|
||
пользователя. Открытие сообщения не помечает его прочитанным — это делают только явные действия; тело
|
||
сообщения и вложения показываются защищённо (текст экранируется, вложения отдаются на скачивание, а не
|
||
рендерятся).
|
||
|
||
### Рекламный баннер
|
||
|
||
Однострочный баннер под навбаром показывает короткие рекламные или служебные сообщения **бесплатным
|
||
игрокам**: всем, кто **не** оплатил пожизненный аккаунт, **не** имеет купленных подсказок и **не**
|
||
несёт роль **`no_banner`** (она отключает баннер безусловно). Покупка платного аккаунта или подсказок —
|
||
либо выдача `no_banner` — убирает его; гости как самые «бесплатные» пользователи баннер видят. Когда
|
||
оператор меняет одно из этих свойств, баннер появляется или исчезает **на месте**, без перезагрузки.
|
||
|
||
Сообщения берутся из управляемых оператором **кампаний**, каждая из которых — заказ на размещение со
|
||
**своим весом показа** (процент) и необязательным **окном** действия (начало/конец). Одновременно
|
||
идущие кампании **конкурируют** за баннер пропорционально весам; постоянная **дефолтная (домашняя)**
|
||
кампания добивает остаток, не выкупленный платными, и полностью уходит из ротации, когда те выкупают
|
||
все 100%. Каждое сообщение написано на **двух языках** (английский + русский); игрок видит вариант для
|
||
**бота, через которого играет**, независимо от языка интерфейса. Клиент ротирует подходящие кампании
|
||
**честно** — каждая получает свою долю по весу за цикл, равномерно размазанную, а не случайно — а
|
||
кампания с несколькими сообщениями показывает их по очереди.
|
||
|
||
Всем этим оператор управляет в админ-консоли по адресу **`/_gm/banners`**: создаёт, редактирует,
|
||
включает/выключает и планирует кампании, пишет двуязычные сообщения кампании (переставляет и удаляет
|
||
их) и задаёт общие **тайминги** показа (сколько держится сообщение, прокрутка слишком длинного, и
|
||
переход fade-out → пауза → fade-in между сообщениями). Дефолтную кампанию нельзя удалить, и в ней
|
||
всегда остаётся хотя бы одно сообщение.
|