diff --git a/PLAN.md b/PLAN.md index dfb8a44..b8b28de 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1281,6 +1281,25 @@ provided cert) at the contour caddy; prod VPN; rollback. scroll and the new one-finger drag-back, so it stays dropped in favour of double-tap + hover-hold zoom; **robot win% in the admin game detail** (#16) — needs the seed-derived play-to-win decision exposed across the game/robot package boundary, to be picked up when that seam is added. + - **Contour-verification follow-ups** (round 5, from live testing): **resign on the opponent's turn** + now works — the engine gained `ResignSeat(seat)` and `game.Resign` bypasses the turn check to forfeit + the actor's own seat (it no longer returned "not your turn"); **quick-match cancel** was a UI no-op + (only stopped polling) — added the full path (REST `/lobby/cancel` → gateway → client) and clear the + matchmaker's pending result on cancel, so a cancelled search is dequeued (no "already queued", no later + robot-substituted game); **lobby win/loss** ranked by score, so a 0-0 resignation read as a win — + `result.ts` now places the viewer below the winner (rank ≥ 2), matching the game detail; a **friend + request to a robot** is accepted as pending and expires like a human ignore (robots no longer set + `BlockFriendRequests`); the **nudge cooldown** error got its own `nudge_too_soon` code/message and the + chat button disables for the hour, with the note reworded to "Waiting for your move!". UI polish: + **even zoom** (interpolate the scroll toward a pre-clamped target as the real width grows/shrinks — no + lurch-and-snap) that **recentres only on the first zoom-in**; a drag **drop-target highlight**; **pinch + zoom** (two-finger, preventDefault only on two touches; a second finger aborts a drag); shuffle hop + capped at **0.3 s** and off under reduce-motion; a **borderless** make-move icon, disabled on a known- + illegal pending word; **variant display names** (english & russian_scrabble → Scrabble/Скрэббл, erudit + → Erudite/Эрудит) shown on the create-game controls and the in-game title. **L2 done:** the admin game + card now shows each robot seat's per-game play-to-win **intent** + the ~40% target and, on the robot's + turn, its deterministic **next-move ETA**. *Open edge:* a bilingual New Game (both en+ru offered) would + show two "Scrabble" buttons — left as the owner's chosen naming; disambiguate if it bites. ## Deferred TODOs (cross-stage) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 47aa02d..46045bb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -235,7 +235,10 @@ Key points: applying the end-game rack-value adjustment, or a resignation. On a **resignation the resigner keeps their accumulated score (no rack adjustment) and never wins**: the win goes to the highest score among the remaining seats, - unconditionally the other player in a two-player game. The engine exposes a + unconditionally the other player in a two-player game. A player may resign **on the + opponent's turn** (a forfeit is not a turn-scoped move): `engine.ResignSeat(seat)` + resigns that player's own seat whoever is to move, and the game domain skips the turn + check for resign (Stage 17). The engine exposes a decoded, solver-free API (`SubmitPlay`/`SubmitExchange`/`EvaluatePlay`/ `HintView`/`Hand`) so `internal/game` drives it without importing the solver. - The **game domain** (`internal/game`) owns everything the engine does not — @@ -301,9 +304,10 @@ from the game's bag `seed` (a restart-stable FNV-1a mix), so a background driver (`robot.Service.Run`, mirroring the turn-timeout sweeper) recomputes the same behaviour on every scan and after a restart — the same philosophy as journal replay. A pool of durable accounts — each a `kind='robot'` identity (§4), keyed -`robot--` and provisioned at startup with chat and friend requests -blocked — backs the human-like names; those two profile toggles are all the -friend/DM blocking requires (there is no DM surface; chat is per-game). Names are +`robot--` and provisioned at startup with **chat blocked but friend +requests open** — a request to a robot is accepted as pending and expires unanswered +(the robot never responds), mirroring a human who ignores it (Stage 17); the chat +block backs the human-like names (there is no DM surface; chat is per-game). Names are **composed per language** from a first-name pool (32 full + 32 colloquial forms) and a surname pool (gender-agreed for Russian) in one of three forms (first only / first + surname initial / first + full surname), deterministically per pool slot so @@ -331,6 +335,8 @@ English game the Latin pool. - **Observability**: robot accounts accrue ordinary statistics (§9) — the authoritative balance metric (target ≈ 40% robot wins) — and a `robot_games_finished_total` OTel counter plus a per-finish log give a live view. + The **admin game card** surfaces each robot seat's per-game play-to-win intent (from + the seed) and, on the robot's turn, its deterministic **next-move ETA** (Stage 17). ## 8. Lobby & social @@ -342,6 +348,9 @@ English game the Latin pool. robot (§7) and starts the game. On a pairing or substitution the matchmaker emits a **match-found** notification (§10), delivered over the live stream; `Poll` remains as a fallback for a client that is not currently streaming. + **Cancel** (`POST /lobby/cancel`) removes the player from the pool and drops any + pending matched result, so a cancelled quick-match is dequeued rather than left for + the reaper to robot-substitute (Stage 17). - **Friends** (Stage 8): two add paths over one `friendships` table. A **one-time code** the to-be-added player issues (a `friend_codes` row: 6-digit numeric, SHA-256-hashed, **12 h** TTL, one live code per issuer, single-use, redeem diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index 9a3936d..771ae57 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -55,9 +55,11 @@ two accounts share a game still in progress. ### Lobby & matchmaking *(Stage 4 / 15)* Bottom tab menu: **my games**, **profile**. The game types offered on **New Game** are -limited to the languages the player's sign-in service supports (English → English; -Russian → Russian + Эрудит; a bilingual service shows all three, and the web client is -unrestricted). This gates only **starting** a new game — both auto-match and a friend +limited to the languages the player's sign-in service supports (English → Scrabble; +Russian → Scrabble + Erudite; a bilingual service shows all three, and the web client is +unrestricted). Variants are shown by their **display name** — both Scrabble variants read +"Scrabble"/"Скрэббл" and Erudit reads "Erudite"/"Эрудит" (by the interface language), and +the same name titles the in-game screen. This gates only **starting** a new game — both auto-match and a friend invitation — so a player still sees and plays existing games of any language. Auto-match (always 2 players) joins a per-variant pool and is paired with the next waiting human; after 10 s with no human the robot substitutes (the robot arrives in Stage 5). Friend games (2–4) are @@ -92,7 +94,8 @@ and plays at a human pace — short thinking times for most moves, the occasiona one, and a night-time pause that tracks the player's own day. It answers a nudge within a few minutes and nudges back when the player has been away a long time. It carries a human-like, language-appropriate name (a Russian game draws mostly Russian -names) and neither chats nor accepts friend requests. +names); it does not chat, and **silently ignores friend requests** — a request to a +robot stays pending and expires, exactly like a human who never responds. ### Social: friends, block, chat, nudge *(Stage 4 / 8)* Become friends in two ways: redeem a **one-time code** the other player issues (six diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index bffeb6f..cbbd2aa 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -56,9 +56,11 @@ Mini App** авторизует по подписанным `initData` плат ### Лобби и подбор *(Stage 4 / 15)* Нижнее tab-меню: **мои игры**, **профиль**. Типы партий на экране **Новая игра** -ограничены языками, которые поддерживает сервис входа игрока (английский → English; -русский → Russian + Эрудит; двуязычный сервис показывает все три, а веб-клиент не -ограничен). Это ограничивает только **старт** новой игры — и авто-подбор, и +ограничены языками, которые поддерживает сервис входа игрока (английский → Scrabble; +русский → Scrabble + Erudite; двуязычный сервис показывает все три, а веб-клиент не +ограничен). Варианты показываются под **отображаемым именем** — оба варианта Scrabble +читаются как «Scrabble»/«Скрэббл», а Erudit — «Erudite»/«Эрудит» (по языку интерфейса), +и это же имя выносится в заголовок экрана игры. Это ограничивает только **старт** новой игры — и авто-подбор, и приглашение друга, — поэтому игрок по-прежнему видит и играет существующие игры на любом языке. Авто-подбор (всегда 2 игрока) встаёт в пул по варианту и сводится со следующим ожидающим человеком; через 10 с @@ -93,8 +95,9 @@ Mini App** авторизует по подписанным `initData` плат поддавки, и ходит с человеческим темпом — чаще короткие раздумья, изредка долгие, и ночная пауза, подстроенная под день игрока. На nudge отвечает за несколько минут и сам шлёт nudge, когда игрок надолго пропал. Носит человекоподобное имя, подходящее -языку партии (в русской партии — в основном русские имена), не общается в чате и не -принимает заявки в друзья. +языку партии (в русской партии — в основном русские имена); не общается в чате и +**молча игнорирует заявки в друзья** — заявка роботу остаётся в ожидании и истекает, +ровно как у человека, который не отвечает. ### Социальное: друзья, блок, чат, nudge *(Stage 4 / 8)* Подружиться можно двумя способами: погасить **одноразовый код**, который выпускает diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md index e480580..3ba425a 100644 --- a/docs/UI_DESIGN.md +++ b/docs/UI_DESIGN.md @@ -63,16 +63,22 @@ Login uses `Screen`. differently in Safari/Chrome). Labels are sized in `cqw` against the fixed viewport, so they stay a constant size as the cells grow (relatively smaller at higher zoom). **Double-tap** an empty/filled cell toggles zoom centred on it; double-tap a **pending** - tile recalls it. On touch, placing a tile auto-zooms in centred on the target, and - **holding a dragged tile over a cell for ~1 s** auto-zooms there (Stage 17). The custom - pinch and swipe-to-open-history gestures stay dropped — they fight both native scroll and - the one-finger drag-back gesture; history opens from the menu or a tap on the players - plaque (below). A **hint** auto-zooms centred on the hint's placement, not the top-left. + tile recalls it. **Pinch** zooms toward the pinch midpoint (a two-finger gesture; + preventDefault fires only for two touches, so one-finger scroll stays native, and a second + finger aborts an in-progress drag). The pan is **interpolated toward a pre-clamped target** + as the real width grows/shrinks, so it magnifies evenly A→B instead of lurching and snapping + back near the edges (Stage 17). It **recentres only on a zoom-in** — placing a 2nd+ tile or + hovering a dragged tile never jumps the board. On touch the first tile placement auto-zooms + in centred on the target, and **holding a dragged tile over a cell ~1 s** auto-zooms there + the first time. The swipe-to-open-history gesture stays dropped (it fought native scroll); + history opens from the menu or a tap on the players plaque (below). A **hint** auto-zooms + centred on the hint's placement, not the top-left. - **Placing & recall** (`Game.svelte`): a rack tile is placed by tap-then-tap or by - dragging it onto a cell; a pending tile is taken back by a **double-tap** or by **dragging - it back onto the rack** (unzoomed board only — when zoomed the one-finger gesture scrolls). - A single tap no longer recalls (too easy to trigger); a recalled tile returns to its - original rack slot (Stage 17). + dragging it onto a cell; while a dragged tile is carried over the board, the aimed-at empty + cell is **highlighted as a drop target** (an accent ring). A pending tile is taken back by a + **double-tap** or by **dragging it back onto the rack** (unzoomed board only — when zoomed + the one-finger gesture scrolls). A single tap no longer recalls (too easy to trigger); a + recalled tile returns to its original rack slot (Stage 17). - **Players plaque & history** (`Game.svelte`, Stage 17): the seats above the board share the width evenly; the seat whose turn it is is **raised** (a drop shadow on its sides) while the others read **sunk in** (an inset shadow). A tap anywhere on the plaque toggles @@ -105,12 +111,15 @@ Login uses `Screen`. short tap opens a small popover above the button; a ~0.7 s hold runs the primary action immediately. Used by the **Skip** and **Hint** tabs (each confirmed by an **Ok ✅** popover). - **MakeMove / Reset** (Stage 17): when ≥1 tile is pending the rack collapses its used slots - and shifts left, a direct **✅** button beside the rack commits the move (no popover), and - the 🔀 Shuffle tab is replaced by a **↩️ Reset** tab. + and shifts left, a **borderless ✅ icon button** (styled like a tab, not a filled accent + button) beside the rack commits the move — no popover, and disabled while the pending word + is known illegal; the 🔀 Shuffle tab is replaced by a **↩️ Reset** tab. - **Game tab bar**: 🔄 Draw (disabled when the bag is empty), 🥺 Skip, 🛟 Hint (with a remaining-count badge, disabled at zero); 🔀 Shuffle (no label, no confirm), which **animates** — tiles hop along a low parabola to their new slots (duration scaled by the - distance) with a short haptic shake. The under-board slot shows the **Scores: N** preview. + distance, the longest ≤ 0.3 s; off under reduce-motion) with a short haptic shake. The + under-board slot shows the **Scores: N** preview. The screen **title** is the variant's + display name (Scrabble / Скрэббл / Erudite / Эрудит), not a constant "Scrabble". ## Announcement banner (`components/AdBanner.svelte`, `lib/banner.ts`)