4aa6174968
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
Add the "Кошелёк" section to the settings hub: context-visible chip balances, active benefits (no-ads term/forever, hints) and a storefront of chip-priced values and money-priced chip packs. Guests have no wallet; the Google Play build hides the money purchases behind a RuStore stub; a web purchase that would draw VK/Telegram chips warns first. Add the catalog read path the storefront needs — a context-projected GET /api/v1/user/wallet/catalog (payments service + store, gateway op wallet.catalog, FBS Catalog/CatalogProduct/CatalogAtom, client decode) — plus the client leg for the existing wallet.get/buy ops. Value spends reuse the existing spend path; the chip-pack purchase (money order flow) arrives with payment intake, so its action is a disabled placeholder for now. Covered by Go unit (catalog projection) + integration (/wallet/catalog over Postgres), vitest (formatting, spendable selection, web-spend warning, GP flag, codec + gateway encode round-trips) and Playwright mock e2e (render, guest-hidden, GP stub, warning) on Chromium + WebKit.
590 lines
80 KiB
Markdown
590 lines
80 KiB
Markdown
# Scrabble Game — Функциональная спецификация
|
||
|
||
Пользовательские сценарии по доменам: что делает каждая видимая пользователю
|
||
операция. Это зеркало [`FUNCTIONAL.md`](FUNCTIONAL.md) для владельца проекта;
|
||
**авторитетна английская версия**. Любую точечную правку переносим в том же
|
||
патче (переводим только изменённые абзацы).
|
||
|
||
## Домены
|
||
|
||
### Клиентское приложение
|
||
Веб/приложение-клиент (Svelte + Vite) воплощает эти истории. Он
|
||
покрывает вход (гость или email), лобби «мои игры», старт авто-подбора,
|
||
игру на доске (постановка фишек перетаскиванием или тапом, пас, обмен, сдача),
|
||
top-1 подсказку, безлимитную проверку слова с жалобой, чат и nudge в партии,
|
||
обновления в реальном времени, переключение языка интерфейса (en/ru) и темы и
|
||
профиль только для чтения. Он также включает управление друзьями (в т.ч.
|
||
одноразовые коды-приглашения) и блоками, дружеские приглашения в игру,
|
||
редактирование профиля и привязку email, экран статистики и просмотр истории
|
||
партии с экспортом в GCG / PNG-картинку. В настройках также выбирается стиль подписей бонус-клеток
|
||
(новичок / классика / без текста). Подсказка **выставляет предложенные фишки на
|
||
доску** — игрок сам решает сделать ход, и подсказка не тратится, если ходов нет.
|
||
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
|
||
и ограничивает частоту повторов.
|
||
Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и
|
||
тему и ведёт в Telegram-канал игры, в VK Mini App и в **веб-версию** (`/app/`) — каждая под
|
||
подписью (Telegram / VK / Веб-версия); сама игра живёт по адресам
|
||
`/app/` (веб), `/telegram/` (Telegram Mini App) и `/vk/` (VK Mini App). Тема на странице эфемерна (берётся из
|
||
системной настройки, а не из сохранённой), выбор языка сохраняется. Страница всегда
|
||
открывается на **русском** — язык браузера не определяется автоматически (роботы
|
||
рендерят страницу с произвольным языком, и автоопределение сделало бы
|
||
проиндексированный контент недетерминированным); сохранённый выбор 🌐 по-прежнему
|
||
главнее. Страница несёт статическую русскую SEO-«шапку» (title/description, карточка
|
||
Open Graph, которую используют превью ссылок в Telegram/VK, канонический адрес
|
||
продакшен-домена, JSON-LD, набор favicon и `robots.txt`), а оболочка SPA помечена
|
||
`noindex` — индексируется только посадочная страница.
|
||
|
||
В обычном вебе клиент — **устанавливаемое PWA**: незалогиненный игрок видит призыв к установке
|
||
под формой входа (и внизу «Настроек»), который в один тап ставит приложение на рабочий стол
|
||
(домашний экран) там, где браузер это умеет (Chromium), либо показывает ручные шаги
|
||
«На экран „Домой“» в Safari на iOS; он скрыт после установки и внутри Mini App.
|
||
|
||
### Первый запуск: онбординг
|
||
При первом открытии приложения игрока один раз проводят по интерфейсу лёгким
|
||
**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.
|
||
**Email**-вход (в вебе) отправляет на адрес брендированный шестизначный код подтверждения —
|
||
локализованный (ru/en), без картинок; после ввода игрок входит. Новый адрес заводится при первом
|
||
запросе, но становится постоянным аккаунтом только после подтверждения кода — так брошенная,
|
||
неподтверждённая попытка вычищается, а адрес освобождается. Отправки ограничены по частоте (короткая
|
||
пауза между кодами и небольшой часовой лимит на адрес), чтобы опечатка в адресе или нетерпеливый тап
|
||
не завалили почтовый ящик. В письме также есть **ссылка одного нажатия**, которая подтверждает
|
||
адрес — или сразу выполняет вход — в один тап; открытие её в другом браузере тут же отражается в
|
||
приложении, а почтовый сканер, который просто загружает ссылку, до токена не дотянется —
|
||
он едет в URL-фрагменте, который не уходит на сервер. Вход, запрошенный из **установленного
|
||
PWA**, эту ссылку не содержит: в письме только код, который вводится в том же окне (ссылка
|
||
открылась бы в отдельном браузере, недоступном приложению).
|
||
Telegram держит **единого бота**: все игроки пользуются одним
|
||
и тем же ботом, а весь его чат и внеприложенческие уведомления пишутся на **языке
|
||
интерфейса** самого игрока (en/ru). Рядом с основным может работать отдельный опциональный
|
||
**промо-бот** — его единственная задача отвечать на `/start` коротким сообщением и кнопкой,
|
||
открывающей приложение **основного** бота, где игрок выбирает нужный вариант игры; это точка входа
|
||
для онбординга, не затрагивающая больше ничего. Гость — только сессия, с урезанными функциями (только
|
||
авто-подбор; без друзей, статистики и истории); заброшенный гость, не вошедший ни
|
||
в одну игру и простаивавший дольше окна удержания, удаляется сборщиком. Пока приложение открыто, клиент
|
||
держит живой стрим и получает обновления в реальном времени — ход соперника, ваш ход,
|
||
чат, nudge и подключение соперника к игре, в которой вы ждёте. Каждое обновление приходит самим событием и применяется на месте без
|
||
перезагрузки — доска обновляется бесшовно, а приглашённая игра открывается мгновенно. Когда приложение **закрыто**, выбранные внеприложенческие
|
||
события (ваш ход, конец партии, nudge, приглашение или заявка в друзья)
|
||
приходят вместо этого **уведомлением в Telegram** — если только игрок не оставил
|
||
уведомления только в приложении (настройка профиля, **включена по умолчанию**).
|
||
Уведомление «ваш ход» называет соперника и пересказывает его последний ход — слово и
|
||
текущий счёт для результативного хода либо что он поменял фишки или пропустил, — а по
|
||
завершении партии приходит уведомление «конец партии» с твоим результатом и финальным
|
||
счётом (счёт читается, твой первым). Если связь пропадает или сервер ограничивает частоту
|
||
запросов, приложение не донимает ошибками: в шапке тихо крутится спиннер **«Подключение…»**,
|
||
пока идёт переподключение, действия, отправляющие данные на сервер, приостанавливаются до
|
||
восстановления связи (экран с серверными данными всё равно открывается — со спиннером — и
|
||
подгружается при реконнекте), а незавершённые чтения возобновляются сами — интерфейс остаётся
|
||
рабочим вместо красного баннера каждый раз.
|
||
|
||
### Аккаунты, привязка и слияние
|
||
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
|
||
привязывает email (по confirm-коду) или свой Telegram (через веб-вход); гость,
|
||
привязавший первую личность, становится постоянным аккаунтом. Факт «личность уже
|
||
занята» не раскрывается до проверки кода/входа. Если привязываемая личность уже
|
||
принадлежит другому аккаунту, игроку показывают явное **необратимое**
|
||
подтверждение, и два аккаунта сливаются в тот, под которым он сейчас работает
|
||
(статистика суммируется, игры и друзья переносятся, дубликаты убираются), — кроме
|
||
случая, когда гость привязывает личность с уже существующим постоянным аккаунтом:
|
||
тогда сохраняется постоянный аккаунт, а игры гостя переходят в него. Слияние
|
||
запрещено, только пока у аккаунтов есть общая незавершённая игра.
|
||
|
||
В профиле перечислены **способы входа** аккаунта. В вебе игрок может добавить
|
||
Telegram (попап логин-виджета) или VK (веб-вход VK ID — редирект на страницу входа VK и
|
||
обратно); внутри Mini App платформа-хозяин уже привязана, и её собственная плашка способа
|
||
входа скрыта — платформу, через которую игрок сейчас вошёл, отсюда отвязать нельзя,
|
||
показывается только плашка другого провайдера. Привязка провайдера, уже
|
||
принадлежащего другому аккаунту, предлагает то же необратимое **слияние**, что и привязка
|
||
email. Привязанного провайдера можно **отвязать** — кроме последнего
|
||
оставшегося способа входа: он не отвязывается, чтобы аккаунт оставался достижимым.
|
||
**Email не отвязывают — его меняют**: игрок вводит новый адрес, подтверждает код,
|
||
отправленный на него, и аккаунт атомарно переключается на новый адрес, освобождая
|
||
старый. Новый адрес, уже принадлежащий другому аккаунту, отклоняется нейтральным
|
||
«проверьте правильность e-mail или обратитесь в поддержку» — смена никогда не сливает
|
||
аккаунты и не раскрывает чужой.
|
||
|
||
Игрок может **удалить аккаунт**. Это удаление с юридическим удержанием, а не стирание:
|
||
аккаунт деактивируется, живые поверхности обезличиваются (соперники видят «[Deleted]»),
|
||
способы входа освобождаются под повторную регистрацию, сессии отзываются — но досье
|
||
(какие креды были привязаны, последний вход, сообщения игрока) сохраняется для оператора и
|
||
чистится через два года. Подтверждение удаления — код на почту (если у аккаунта есть
|
||
e-mail) либо ввод фразы. Активные игры форфейтятся, чтобы не бросать соперников; одиночные
|
||
игры против ИИ удаляются, а игры с людьми сохраняются под обезличенным именем, чат игрока
|
||
остаётся. Если снова открыть приложение после удаления — создаётся новый аккаунт.
|
||
|
||
### Лобби и подбор
|
||
При холодном запуске лобби встречает игрока короткой **заставкой загрузки** — фишки 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 для английских),
|
||
чтобы его посмотреть. Подсказки управляются настройками
|
||
партии — разрешены ли они и сколько их у каждого игрока на старте — и расходуют
|
||
личный кошелёк подсказок после исчерпания внутриигрового лимита. Против робота
|
||
(**vs_ai**) подсказки, наоборот, **безлимитны и без кошелька**, но с idle-гейтом как
|
||
анти-фрустрация: подсказка открывается, только когда игрок **застрял на ходу ~30 минут**
|
||
(отсчёт от последнего хода робота; самый первый ход, до хода робота, исключён). Пока гейт
|
||
закрыт, кнопка подсказки несёт маленький **🔒 замок**, а тап показывает, сколько осталось;
|
||
замок снимается вживую в нужный момент. Один и тот же гейт работает **онлайн и офлайн**: отсчёт
|
||
идёт по **ровному внутриигровому таймеру**, а не по системным часам, поэтому их перевод его не
|
||
собьёт, а повторный вход перечитывает остаток — застрявший ход не забывается. Онлайн гейт
|
||
**принуждает сервер** по своим часам (их игрок не тронет); vs_ai-подсказка не учитывается ни в
|
||
одной статистике подсказок. Партия
|
||
завершается, когда мешок пуст и игрок выложил стойку, после 6 подряд бесплодных
|
||
ходов, по сдаче, либо по таймауту хода (от 5 минут до 24 часов, дефолт 24 часа):
|
||
пропущенный ход означает авто-сдачу, кроме как когда игрок внутри своего
|
||
суточного окна отсутствия (away). В партии на двоих сдача или таймаут отдают
|
||
победу другому игроку, а вышедший сохраняет свои очки. В партии на троих-четверых
|
||
место вышедшего убирается, остальные играют дальше, и партия завершается, когда
|
||
остаётся один активный игрок; что делать с фишками вышедшего (вернуть в мешок или
|
||
убрать из игры) выбирается при создании партии, а его стойка никогда не
|
||
показывается остальным. **Композиция на доске сохраняется по партии**: расположение
|
||
фишек на стойке и выложенные, но не отправленные фишки сохраняются по мере составления
|
||
хода и восстанавливаются при возврате (в том числе на другом устройстве); игрок может
|
||
**раскладывать фишки и в ход соперника**, но такой черновик только позиционный —
|
||
предпросмотр счёта и отправка доступны лишь в собственный ход.
|
||
|
||
### Робот-соперник
|
||
Если авто-подбор не находит человека за время ожидания (1,5–3 минуты), свободное место в игре,
|
||
в которой вы уже ждёте, занимает робот-соперник. Он задуман неотличимым от человека:
|
||
один раз за партию решает, играть ли на победу (примерно в 40% случаев, так что
|
||
человек выигрывает большинство партий), целится в близкий счёт, а не в разгром или
|
||
поддавки, время от времени делает один ход вопреки этому плану — неожиданный отрыв или
|
||
слабый ход, — но к концу партии (когда мешок пуст) держится плана, и ходит с
|
||
человеческим темпом — чаще короткие раздумья, изредка долгие, и ночная пауза,
|
||
подстроенная под день игрока; когда партия уже явно решена и обе стороны только пасуют,
|
||
он перестаёт её затягивать — отвечает на вынужденные пасы примерно в темпе самого игрока,
|
||
а не после долгого раздумья. На nudge отвечает за несколько минут и, когда игрок
|
||
держит его в ожидании, сам шлёт nudge редко — примерно раз в 9-12 часов и никогда во время
|
||
своей ночной паузы. Носит человекоподобное имя, подходящее
|
||
языку партии — каждую партию новое, из широкого международного пула реальных имён и
|
||
никнеймов, так что арена кажется полной разных игроков (в русской партии — в основном
|
||
русские имена, без восточноазиатских письменностей; в международной — весь спектр); не общается в чате и
|
||
**молча игнорирует заявки в друзья** — заявка роботу остаётся в ожидании и истекает,
|
||
ровно как у человека, который не отвечает.
|
||
|
||
Тот же робот стоит и за **честной игрой против ИИ**, которую игрок выбирает напрямую (выше). Там он
|
||
не притворяется: везде показан как **🤖**, присоединяется и ходит сразу (без раздумий и ночной
|
||
паузы), сохраняет ту же силу (по-прежнему играет на победу лишь примерно в 40% партий, с теми же
|
||
редкими ходами вопреки плану, затухающими к эндшпилю), а чат, nudge и «добавить в друзья» выключены. Партии с ИИ — это **тренировка**: они не идут в статистику игрока.
|
||
|
||
### Офлайн-режим
|
||
Установленный веб-PWA со входом по подтверждённой почте может переключиться в осознанный
|
||
**офлайн-режим** (Настройки → Режим игры). В офлайне приложение не обращается к сети: шапка синеет с
|
||
меткой *Офлайн*, лобби показывает только сохранённые на устройстве игры, а сетевые поверхности
|
||
отключены или скрыты (вкладка Статистика; вариант «случайный соперник» в Новой игре).
|
||
Новая игра против робота создаёт локальную **vs_ai**-игру — он ходит
|
||
целиком в браузере без бэкенда. Локальные игры хранятся на устройстве, видны только в офлайн-режиме и
|
||
никогда не синхронизируются с аккаунтом. Чтобы игру можно было создать и сыграть без связи, приложение
|
||
заранее, пока ещё онлайн, подгружает словари включённых игроком вариантов и (после установки)
|
||
запускается из прекешированного шелла даже без сети. Переключение **в** офлайн сначала проверяет, что
|
||
словари включённых вариантов есть на устройстве — если каких-то не хватает, оно их подгружает и недолго
|
||
ждёт, а если подготовить их не удаётся, остаётся онлайн с короткой заметкой *нужен интернет* (загрузка
|
||
продолжается в фоне, так что следующее переключение мгновенно). Режим привязан к устройству и
|
||
сохраняется между запусками.
|
||
|
||
В офлайне «с друзьями» в Новой игре запускает **локальную игру по очереди (hotseat)** на 2–4 человек
|
||
за одним устройством. Сначала задаётся **пароль ведущего** — 4-значный PIN на клавиатуре в стиле
|
||
экрана блокировки; пока он не задан, строки игроков заблокированы. Затем приложение спрашивает,
|
||
играете ли вы сами — если да, вы занимаете первое место под именем из профиля. Вы называете каждого
|
||
игрока (от 2 до 4; строки можно добавлять и удалять, удаление спрашивает пароль ведущего) и можете
|
||
дать любому месту **свой необязательный PIN**. Во время игры доска видна всегда, но место с PIN
|
||
вместо стойки показывает кнопку **«Разблокировать»**, пока владелец не введёт PIN, — так устройство
|
||
передаётся из рук в руки, и только защищённое место прячет свои буквы; на каждом ходу замок
|
||
возвращается. **Ведущий** (кнопка ведущего в игре) может по паролю ведущего **пропустить** ход
|
||
текущего игрока, **исключить** игрока или **завершить игру досрочно** — досрочное завершение стирает
|
||
партию без победителей и проигравших. Подсказки и чат в игре по очереди отключены. Нормально
|
||
завершённая партия сохраняется в офлайн-лобби как любая другая; удаление любой игры по очереди —
|
||
идущей или завершённой — из лобби спрашивает пароль ведущего, чтобы никто не стёр партию сразу после
|
||
своего хода.
|
||
|
||
Приложение также **само включает офлайн-режим**, когда не может достучаться до сети. При холодном
|
||
запуске без связи оно переходит в офлайн на текущую сессию; когда устройство онлайн, но шлюз молчит
|
||
несколько секунд, оно сперва спрашивает — *Нет связи. Включить офлайн-режим?*, с выбором **Включить**
|
||
(уйти в офлайн) или **Ждать сеть** (продолжить попытки онлайн). Пока приложение открыто, потеря сети —
|
||
режим полёта — переключает в офлайн автоматически, а её восстановление само возвращает онлайн.
|
||
Самостоятельно включённый офлайн временный: он возвращается в онлайн, как только сеть появилась, и
|
||
следующий запуск проверяет заново. **Осознанный** офлайн — тумблер в Настройках или **Включить** в том
|
||
диалоге — это выбор игрока: он сохраняется и никогда не отменяется автоматически.
|
||
|
||
### Социальное: друзья, блок, чат, 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 и отрисованная **PNG-картинка**
|
||
финальной позиции; экспорт доступен **только после завершения партии** и никогда —
|
||
для тренировочной партии с ИИ (экспорт идущей партии раскрыл бы журнал ходов, а
|
||
партия с ИИ одноразовая). Выбор формата — **нативный попап Telegram** на
|
||
Telegram Android/desktop (там это безопасно: цепочка целиком из bridge-вызовов,
|
||
которым user activation не нужна) и собственный модал приложения в остальных
|
||
случаях — на Telegram iOS доставка идёт через системную share-шторку, которую из
|
||
коллбека нативного попапа не открыть, а у VK нативного чузера нет.
|
||
|
||
**Картинка** рендерится на сервере (внутренний рендер-сайдкар исполняет тот же
|
||
модуль отрисовки, что тестирует веб-клиент; всегда светлая тема): финальная
|
||
доска с классическими осями координат A..O / 1..15 слева — бонус-клетки чистой
|
||
цветовой заливкой, без текстовых подписей — и компактная таблица ходов по местам
|
||
справа: в шапке имя и финальный счёт каждого места (🏆 у победителя), далее по
|
||
строке на ход: классическая координата главного слова (горизонтальный ход —
|
||
цифра первой, `8G`; вертикальный — буква первой, `H8` — конвенция GCG), слово и
|
||
его очки; дополнительные слова мультисловного хода — второй мелкой строкой,
|
||
не-игровые ходы — локализованными пометками (пас/обмен/сдался/таймаут), а
|
||
замыкающая строка ± показывает концовочную поправку за рэк, когда она была
|
||
(финальные счета авторитетны — суммы ходов её не содержат). В подвале — хост сайта
|
||
и дата завершения в локали устройства. Типографика таблицы фиксирована; длинная
|
||
партия растягивает доску (не ниже минимума), так что пустот на картинке нет.
|
||
|
||
Доставка — **одна подписанная короткоживущая ссылка для обоих форматов на любой
|
||
платформе**, отданная лучшему механизму, который у платформы реально есть (каждая
|
||
ветка проверена на устройстве): Telegram Android/desktop — диалог загрузки Telegram
|
||
(из его превью файл шерится дальше); **Telegram iOS — системная share-шторка** со
|
||
скачанным файлом; VK iOS ведёт оба формата через загрузку VK в её нативный
|
||
share-поток, а на **VK Android** — чей загрузчик виснет — картинка открывается в
|
||
нативном просмотрщике фото VK (сохранение его кнопками), GCG копируется в буфер
|
||
(десктопный iframe VK — обычный браузер — скачивает оба); **мобильный браузер
|
||
получает системную share-шторку** (ничего не оседает в Загрузках); десктопный
|
||
браузер скачивает файл. Ссылка не требует логина при
|
||
скачивании (родные загрузчики платформ его не несут) и живёт минуты. Единственное
|
||
исключение — устаревший клиент Telegram без диалога загрузки (до Bot API 8.0): там
|
||
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`, отличная от полной блокировки аккаунта:
|
||
останавливает только отправку обратной связи). Роли перечислены и выдаются/снимаются на карточке
|
||
пользователя. Открытие сообщения не помечает его прочитанным — это делают только явные действия; тело
|
||
сообщения и вложения показываются защищённо (текст экранируется, вложения отдаются на скачивание, а не
|
||
рендерятся).
|
||
|
||
### Кошелёк
|
||
|
||
У постоянных (не гостевых) игроков есть **Кошелёк** — вкладка в разделе настроек, между «Друзьями» и
|
||
«О программе»; у гостей его нет. Он показывает **фишки** (внутриигровую валюту, разделённую по тому,
|
||
где она была пополнена — VK, Telegram или веб), **активные блага** (реклама выключена до даты или
|
||
навсегда, и число доступных подсказок) и **магазин**. Что видно и что можно потратить, зависит от
|
||
того, **где запущено приложение**, по правилам соответствия магазинам из [`PAYMENTS.md`](PAYMENTS.md):
|
||
внутри магазина доступны только его фишки; в открытом вебе — все привязанные, списываются в порядке
|
||
веб → VK → Telegram; на VK-iOS баланс показан, но трата заморожена.
|
||
|
||
**Магазин** перечисляет **ценности**, покупаемые за фишки (дополнительные подсказки, дни без рекламы),
|
||
по фиксированной цене в фишках, одинаковой во всех контекстах, и **наборы фишек**, покупаемые за
|
||
настоящие деньги, в валюте текущего контекста (голоса VK, звёзды Telegram или рубли в вебе). Покупка
|
||
ценности сразу применяет её благо. Перед покупкой в вебе, которая списала бы фишки VK/Telegram,
|
||
приложение **предупреждает**, что благо будет работать только в вебе и в приложении — это правило
|
||
магазинов — и просит подтверждения. В сборке для **Google Play** покупки за деньги скрыты за подсказкой
|
||
установить сборку из RuStore (правило Google о внутриигровой валюте); трата уже заработанных фишек
|
||
по-прежнему работает. Кошелёк **не хранит историю покупок** — только текущие балансы, блага и магазин.
|
||
|
||
### Рекламный баннер
|
||
|
||
Однострочный баннер под навбаром показывает короткие рекламные или служебные сообщения **бесплатным
|
||
игрокам**: всем, кто **не** оплатил пожизненный аккаунт, **не** имеет купленных подсказок и **не**
|
||
несёт роль **`no_banner`** (она отключает баннер безусловно). Покупка платного аккаунта или подсказок —
|
||
либо выдача `no_banner` — убирает его; гости как самые «бесплатные» пользователи баннер видят. Когда
|
||
оператор меняет одно из этих свойств, баннер появляется или исчезает **на месте**, без перезагрузки.
|
||
|
||
Сообщения берутся из управляемых оператором **кампаний**, каждая из которых — заказ на размещение со
|
||
**своим весом показа** (процент) и необязательным **окном** действия (начало/конец). Одновременно
|
||
идущие кампании **конкурируют** за баннер пропорционально весам; постоянная **дефолтная (домашняя)**
|
||
кампания добивает остаток, не выкупленный платными, и полностью уходит из ротации, когда те выкупают
|
||
все 100%. Каждое сообщение написано на **двух языках** (английский + русский); игрок видит вариант для
|
||
**бота, через которого играет**, независимо от языка интерфейса. Клиент ротирует подходящие кампании
|
||
**честно** — каждая получает свою долю по весу за цикл, равномерно размазанную, а не случайно — а
|
||
кампания с несколькими сообщениями показывает их по очереди.
|
||
|
||
Недефолтная кампания может нести собственные **цвета** — фон, текст и ссылку — чтобы рекламное или
|
||
служебное сообщение выделялось на фоне нейтральной ленты. Цвета задаются двумя наборами: один
|
||
применяется на **любой теме**, а необязательный второй переопределяет только **тёмную тему**, чтобы
|
||
кампания хорошо читалась на обеих; рамка ленты выводится из фона автоматически. Кампанию можно также
|
||
пометить как **срочную (urgent)**: срочная кампания показывается **всем** — даже платным игрокам,
|
||
владельцам подсказок и пользователям с ролью `no_banner` — и, пока она активна, лента показывает
|
||
**только её** (обычные кампании и домашняя дефолтная уступают место). Срочность — для настоящих
|
||
системных уведомлений (тех.работы, сбои), а не для рекламы.
|
||
|
||
Всем этим оператор управляет в админ-консоли по адресу **`/_gm/banners`**: создаёт, редактирует,
|
||
включает/выключает и планирует кампании, пишет двуязычные сообщения кампании (переставляет и удаляет
|
||
их), выбирает необязательные цвета недефолтной кампании (нативный color-picker с живым превью ленты
|
||
на светлой и тёмной темах) и помечает её срочной, а также задаёт общие **тайминги** показа (сколько
|
||
держится сообщение, прокрутка слишком длинного, и переход fade-out → пауза → fade-in между
|
||
сообщениями). Дефолтную кампанию нельзя удалить, в ней всегда остаётся хотя бы одно сообщение, и она
|
||
остаётся простой (без цветов, никогда не срочная).
|