Files
scrabble-game/docs/FUNCTIONAL_ru.md
T
Ilia Denisov 5a80696fe8
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 53s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m31s
feat(ui): friends list as lobby-style rows with kebab + confirm modals
Settings -> Friends previously rendered each friend as a bordered card with
two always-visible text buttons (Remove / Block) that fired immediately. Rework
the whole screen to the lobby's visual language: one-line rows split by
hairline separators across all three sections (friends, incoming requests,
blocked).

Each friend row gains a right-hand kebab that slides the row open to reveal two
icon actions split by a vertical divider -- block (no-entry) and remove (cross)
-- mirroring the lobby's slide-to-reveal. Both actions now require a
confirmation modal; since the slide moves a short name off-screen, the modal
keeps a generic title and shows the friend's name in the body, above the
buttons, so a long name cannot stretch the sheet. Incoming keeps its
accept/decline buttons and blocked keeps unblock, inline on their rows.

Add the friends.actions / friends.blockConfirm / friends.unfriendConfirm keys
to both i18n catalogs and document the flow in FUNCTIONAL (+_ru).
2026-06-23 00:09:39 +02:00

394 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scrabble Game — Функциональная спецификация
Пользовательские сценарии по доменам: что делает каждая видимая пользователю
операция. Это зеркало [`FUNCTIONAL.md`](FUNCTIONAL.md) для владельца проекта;
**авторитетна английская версия**. Любую точечную правку переносим в том же
патче (переводим только изменённые абзацы).
## Домены
### Клиентское приложение
Веб/приложение-клиент (Svelte + Vite) воплощает эти истории. Он
покрывает вход (гость или email), лобби «мои игры», старт авто-подбора,
игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача),
top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии,
обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и
профиль только для чтения. Он также включает управление друзьями (в т.ч.
одноразовые коды-приглашения) и блоками, дружеские приглашения в игру,
редактирование профиля и привязку email, экран статистики и просмотр истории
партии с экспортом GCG. В настройках также выбирается стиль подписей бонус-клеток
(новичок / классика / без текста). Подсказка **выставляет предложенные фишки на
доску** — игрок сам решает сделать ход, и подсказка не тратится, если ходов нет.
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
и ограничивает частоту повторов.
Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и
тему и ведёт в соответствующий по-язычный Telegram-канал; сама игра живёт по адресам
`/app/` (веб) и `/telegram/` (Telegram Mini App). Тема на странице эфемерна (берётся из
системной настройки, а не из сохранённой), выбор языка сохраняется.
### Личность и сессии
Игрок приходит с платформы (сначала Telegram), через email-вход или как
эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий
session-токен; backend сопоставляет его с внутренним `user_id`. Запуск **Telegram
Mini App** авторизует по подписанным `initData` платформы, перекрашивает интерфейс
в цвета Telegram и — при первом контакте — задаёт язык интерфейса нового аккаунта по
языку Telegram-клиента. Если запуск не может достучаться до бэкенда (например, во время
деплоя), Mini App тихо повторяет попытки, а затем показывает небольшой экран «не удалось
загрузить» с кнопкой **Повторить**, вместо того чтобы сбрасывать на веб-вход, которому внутри
Telegram не место. 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,53 минуты** свободное место занимает робот, так что игра всегда
стартует, и экран новой игры подсказывает, что можно закрыть приложение на время ожидания и вернуться
позже. Для русских игр (авто-подбор или приглашение) на экране
новой игры есть опция **«Несколько слов за ход»** (по умолчанию **выключена**): выключена —
упрощённое **правило одного слова**: настоящим словом должно быть только слово, выложенное
вдоль линии хода (и оно должно пересекать или продолжать уже стоящие на доске буквы вдоль
этой линии), а случайные перпендикулярные слова игнорируются и не засчитываются;
включена — обычный скрэббл. Английские игры всегда по стандартным правилам и тоггл не
показывают. В авто-подборе выбор входит в ключ подбора, поэтому игрок сводится только с теми,
кто выбрал то же правило. Игры с друзьями (2–4)
формируются приглашением игроков из списка друзей (приглашение, как и код друга,
можно отправить deep-link'ом в Telegram, который откроет его сразу): инициатор
выбирает настройки, и партия стартует, когда приняли все приглашённые — любой отказ отменяет приглашение, а без
ответа приглашение протухает через семь дней.
**Лимит одновременных партий.** Игрок может держать одновременно не более **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 также гаснет в момент, когда
его получатель **делает ход**.
### Профиль и настройки
Редактирование отображаемого имени (буквы, разделённые одиночным пробелом / «.» /
«_», с необязательной завершающей «.» или хвостом до пяти цифр, до 32 символов и не
более 5 спецсимволов — пунктуации «.» / «_», пробелы и цифры не в счёт), таймзоны (выбор смещения от
UTC; при создании аккаунта она подставляется из определённого смещения устройства — чтобы
игры с роботом таймились правильно ещё до открытия этой формы), суточного окна отсутствия
(away; сетка по 10 минут, не более 12 часов, с переходом через полночь) и переключателей блокировок. Форма профиля редактируется
сразу (без отдельного режима редактирования). Привязка email и Telegram, а также
слияние аккаунтов вынесены в раздел «Аккаунты, привязка и слияние».
**Предпочтения (в какие варианты тебя можно подбирать).** Настройка профиля задаёт варианты
игры — Эрудит, русский Scrabble и английский Scrabble, показанные **сначала Эрудит**, — в
которые ты разрешаешь себя подбирать; **новый аккаунт стартует только с Эрудитом**, и нужно
оставить выбранным **хотя бы один**. Именно этот список предлагает **Новая игра**, когда ты
запускаешь партию (авто-подбор, игра с ИИ или приглашение друга, которое ты создаёшь), — не
включённый вариант не предлагается, и сервер его отклоняет. На партии, в которые тебя
**приглашают**, это не влияет: приглашённый друг может принять приглашение в **любом** варианте,
а открывать и играть существующие игры любого варианта можно всегда.
### Обратная связь
Зарегистрированный игрок обращается к операторам из Settings → Info: экран **«Обратная связь»** с
сообщением (до 1024 символов) и необязательным вложением (один файл, до ~1 МБ — изображения, PDF,
текст/лог, офисные документы, RTF или архивы; неподходящий файл отклоняется на форме без перечисления
допустимых типов). После отправки форма очищается и подтверждает «Ваше сообщение отправлено», а
повторная отправка блокируется, пока оператор не разобрал обращение — при повторном входе на экране
«Ожидаем рассмотрения вашего последнего обращения». Ответ оператора показывается под формой как
«Ответ на ваше последнее сообщение» (ссылки в нём открываются в новой вкладке); он помечается
прочитанным, как только экран его показал, и исчезает через неделю. Бейдж на вкладке Settings (и на Info внутри неё) сигналит о непрочитанном
ответе. Гость отправлять обратную связь не может (пункт скрыт). Игрок, которому оператор запретил
обратную связь (роль, а не полная блокировка аккаунта), видит кнопку отправки недоступной.
### История и статистика
Завершённые партии архивируются в независимом от словаря виде и экспортируются
в GCG; экспорт доступен **только после завершения партии** и никогда — для
тренировочной партии с ИИ (экспорт идущей партии раскрыл бы журнал ходов, а партия
с ИИ одноразовая). Клиент делится файлом `.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**;
заблокированный игрок видит причину на своём языке. Блокировка мгновенно засчитывает **поражение**
во всех активных партиях игрока (каждый соперник побеждает, как при сдаче) и убирает его из
матчмейкинга. Заблокированный игрок — при открытии приложения или любом действии — видит
терминальный экран: *«Ваша учётная запись заблокирована»* (постоянная) или *«…заблокирована до
&lt;дата, время&gt;»* (временная, в его часовом поясе), плюс причину, если она была указана, и
приложение прекращает любое фоновое общение с сервером. Временная блокировка снимается сама по
истечении срока; оператор также может **разблокировать** с карточки пользователя в любой момент
(уже проигранные партии не возвращаются).
Там, где бот ведёт **привязанный к каналу чат-обсуждение**, по умолчанию писать может каждый, а бот
**глушит** игрока, который **не зарегистрирован** или **заблокирован**, и снимает мьют, как только тот
зарегистрируется или будет разблокирован. То есть незарегистрированного новичка, написавшего в чат,
бот глушит (промо-бот направляет его в игру зарегистрироваться, после чего бот возвращает голос), а
зарегистрированный незаблокированный игрок просто пишет. Оператор также может **замьютить игрока только
в чате** — роль `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 между сообщениями). Дефолтную кампанию нельзя удалить, и в ней
всегда остаётся хотя бы одно сообщение.