docs(offline): document hotseat pass-and-play
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) Failing after 12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped

ARCHITECTURE (offline section) + FUNCTIONAL (+_ru mirror): the local
pass-and-play mode — host/referee master PIN, per-seat optional PIN
locks (social lock, salted SHA-256, client-only), the board-visible seat
unlock, host overrides (skip/exclude/terminate), no hints/chat, and the
master-PIN-gated lobby deletion.
This commit is contained in:
Ilia Denisov
2026-07-07 11:56:40 +02:00
parent 66278e34b7
commit baa9961c3e
3 changed files with 49 additions and 6 deletions
+16 -2
View File
@@ -1251,8 +1251,22 @@ toggle — distinct from the transient gateway-reachability signal — that tint
an *Offline* chip and confines play to on-device `vs_ai` games. The **offline lobby lists only those an *Offline* chip and confines play to on-device `vs_ai` games. The **offline lobby lists only those
device-local games** (reconstructed by replaying the IndexedDB move journal) and its New-vs-AI entry device-local games** (reconstructed by replaying the IndexedDB move journal) and its New-vs-AI entry
creates one through the in-browser engine — the same game screen then drives it, the robot replying creates one through the in-browser engine — the same game screen then drives it, the robot replying
locally; online-only affordances (the Stats tab, the friend/random options in New Game) are disabled locally; online-only affordances (the Stats tab, the random-opponent option) are disabled
or hidden. A `vs_ai` hint (online and offline alike) is unlimited and wallet-free but idle-gated or hidden, and New Game's *with friends* becomes the entry to **local pass-and-play (hotseat)**.
A hotseat game is a device-local **2-4 human** game built through the same engine (`hotseat` +
per-seat/host PIN locks on the record; the seats carry names but no accounts). A **mandatory host
(referee) PIN** gates the roster at creation and, in-game, the referee overrides — **skip** the
current turn, **exclude** a player (a forced seat resign, `resignSeat`) or **terminate** the game
(deleted, no winner/loser); each seat may also carry its own **optional PIN** that withholds its rack
(the board stays visible — only the tray is gated) until it is entered, so players pass the device
around freely and lock a seat only against a distrusted tablemate. The unlock is **per turn**
(re-locks on advance). PINs are a **social lock, not cryptography** — a salted SHA-256 kept only in
the device record (`lib/pin`); a 4-digit PIN is trivially brute-forceable and the racks live in
client memory regardless, so they defeat a casual glance, not a determined peek. A hotseat game is
**not `vs_ai`**: hints (the freed control becomes the host button) and chat/self-resign are gone, and
its lobby card — active *and* finished — is master-PIN-gated to delete (so the last mover cannot
instantly wipe it); a naturally finished game is saved with its result like any local game. A `vs_ai`
hint (online and offline alike) is unlimited and wallet-free but idle-gated
(unlocked ~30 min into a stuck turn), and counts toward no hint statistic. The **source** reports the (unlocked ~30 min into a stuck turn), and counts toward no hint statistic. The **source** reports the
**seconds left** (`hint_unlock_left_seconds` on the game view) — computed by the **backend from the **seconds left** (`hint_unlock_left_seconds` on the game view) — computed by the **backend from the
server clock** online (which also enforces the gate, returning `hint_locked` for an early request), server clock** online (which also enforces the gate, returning `hint_locked` for an early request),
+16 -2
View File
@@ -269,8 +269,8 @@ statistics.
An installed web PWA signed in with a confirmed email can switch to a deliberate **offline mode** An installed web PWA signed in with a confirmed email can switch to a deliberate **offline mode**
(Settings → Play mode). Offline, the app never touches the network: the header turns blue with an (Settings → Play mode). Offline, the app never touches the network: the header turns blue with an
*Offline* chip, the lobby lists only the games stored on the device, and online-only surfaces are *Offline* chip, the lobby lists only the games stored on the device, and online-only surfaces are
disabled or hidden (the Stats tab; the *with friends* and *random opponent* options in New Game). disabled or hidden (the Stats tab; the *random opponent* option in New Game).
Only **vs_ai** games are playable — New Game creates a device-local game against the robot, which A New Game against the robot creates a device-local **vs_ai** game — it
plays entirely in the browser with no backend. Local games are kept on the device, visible only in plays entirely in the browser with no backend. Local games are kept on the device, visible only in
offline mode, and never sync to the account. So a game can be created and played with no connection, offline mode, and never sync to the account. So a game can be created and played with no connection,
the app quietly preloads the dictionaries for the player's enabled variants while still online, and the app quietly preloads the dictionaries for the player's enabled variants while still online, and
@@ -280,6 +280,20 @@ and waits briefly, and if they cannot be readied it stays online with a short *n
(the download keeps running in the background, so a later switch is instant). The mode is (the download keeps running in the background, so a later switch is instant). The mode is
device-scoped and sticky across launches. device-scoped and sticky across launches.
Offline, New Game's **with friends** starts a **local pass-and-play** game for 2-4 people sharing one
device. You first set a **host (leader) password** — a 4-digit PIN entered on a lock-screen-style
keypad; until it is set the player rows stay locked. The app then asks whether you are playing too —
if so you take the first seat with your profile name. You name each player (2 to 4; add or remove
rows, where a removal asks for the leader password) and may give any seat its **own optional PIN**.
During the game the board is always visible, but a PIN-locked seat shows an **Unlock** button in place
of its rack until its owner enters the PIN — so the device passes hand to hand and only a protected
seat hides its letters; the lock returns each turn. The **leader** (the host button in the game) can,
with the leader password, **skip** the current player's turn, **remove** a player, or **end the game
early** — an early end discards the game with no winner or loser. Hints and chat are off in a
pass-and-play game. A game that finishes normally is saved to the offline lobby like any other;
deleting any pass-and-play game — running or finished — from the lobby asks for the leader password,
so no one can wipe a game the moment they have moved.
The app also **enters offline mode on its own** when it cannot reach the network. On a cold launch The app also **enters offline mode on its own** when it cannot reach the network. On a cold launch
with no connection it switches to offline for that session; when the device is online but the gateway with no connection it switches to offline for that session; when the device is online but the gateway
stays silent for a few seconds it asks first — *No connection. Switch to offline mode?*, with stays silent for a few seconds it asks first — *No connection. Switch to offline mode?*, with
+17 -2
View File
@@ -274,8 +274,8 @@ e-mail) либо ввод фразы. Активные игры форфейтя
Установленный веб-PWA со входом по подтверждённой почте может переключиться в осознанный Установленный веб-PWA со входом по подтверждённой почте может переключиться в осознанный
**офлайн-режим** (Настройки → Режим игры). В офлайне приложение не обращается к сети: шапка синеет с **офлайн-режим** (Настройки → Режим игры). В офлайне приложение не обращается к сети: шапка синеет с
меткой *Офлайн*, лобби показывает только сохранённые на устройстве игры, а сетевые поверхности меткой *Офлайн*, лобби показывает только сохранённые на устройстве игры, а сетевые поверхности
отключены или скрыты (вкладка Статистика; варианты «с другом» и «случайный соперник» в Новой игре). отключены или скрыты (вкладка Статистика; вариант «случайный соперник» в Новой игре).
Играть можно только в **vs_ai** — Новая игра создаёт локальную игру против робота, который ходит Новая игра против робота создаёт локальную **vs_ai**-игру — он ходит
целиком в браузере без бэкенда. Локальные игры хранятся на устройстве, видны только в офлайн-режиме и целиком в браузере без бэкенда. Локальные игры хранятся на устройстве, видны только в офлайн-режиме и
никогда не синхронизируются с аккаунтом. Чтобы игру можно было создать и сыграть без связи, приложение никогда не синхронизируются с аккаунтом. Чтобы игру можно было создать и сыграть без связи, приложение
заранее, пока ещё онлайн, подгружает словари включённых игроком вариантов и (после установки) заранее, пока ещё онлайн, подгружает словари включённых игроком вариантов и (после установки)
@@ -285,6 +285,21 @@ e-mail) либо ввод фразы. Активные игры форфейтя
продолжается в фоне, так что следующее переключение мгновенно). Режим привязан к устройству и продолжается в фоне, так что следующее переключение мгновенно). Режим привязан к устройству и
сохраняется между запусками. сохраняется между запусками.
В офлайне «с друзьями» в Новой игре запускает **локальную игру по очереди (hotseat)** на 24 человек
за одним устройством. Сначала задаётся **пароль ведущего** — 4-значный PIN на клавиатуре в стиле
экрана блокировки; пока он не задан, строки игроков заблокированы. Затем приложение спрашивает,
играете ли вы сами — если да, вы занимаете первое место под именем из профиля. Вы называете каждого
игрока (от 2 до 4; строки можно добавлять и удалять, удаление спрашивает пароль ведущего) и можете
дать любому месту **свой необязательный PIN**. Во время игры доска видна всегда, но место с PIN
вместо стойки показывает кнопку **«Разблокировать»**, пока владелец не введёт PIN, — так устройство
передаётся из рук в руки, и только защищённое место прячет свои буквы; на каждом ходу замок
возвращается. **Ведущий** (кнопка ведущего в игре) может по паролю ведущего **пропустить** ход
текущего игрока, **исключить** игрока или **завершить игру досрочно** — досрочное завершение стирает
партию без победителей и проигравших. Подсказки и чат в игре по очереди отключены. Нормально
завершённая партия сохраняется в офлайн-лобби как любая другая; удаление любой игры по очереди —
идущей или завершённой — из лобби спрашивает пароль ведущего, чтобы никто не стёр партию сразу после
своего хода.
Приложение также **само включает офлайн-режим**, когда не может достучаться до сети. При холодном Приложение также **само включает офлайн-режим**, когда не может достучаться до сети. При холодном
запуске без связи оно переходит в офлайн на текущую сессию; когда устройство онлайн, но шлюз молчит запуске без связи оно переходит в офлайн на текущую сессию; когда устройство онлайн, но шлюз молчит
несколько секунд, оно сперва спрашивает — *Нет связи. Включить офлайн-режим?*, с выбором **Включить** несколько секунд, оно сперва спрашивает — *Нет связи. Включить офлайн-режим?*, с выбором **Включить**