diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index bc69e80..e6bd4de 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -224,9 +224,10 @@ barred from feedback (a role, not a full account block) sees the send control di ### History & statistics Finished games are archived in a dictionary-independent form and exportable to -GCG; the export is offered **only once a game is finished** (exporting a live game -would leak the move journal), and the client shares the `.gcg` file where the -platform supports it, otherwise downloads it. Statistics (durable accounts only): +GCG; the export is offered **only once a game is finished**, and never for an +honest-AI practice game (a live game's export would leak the move journal; an AI +game is throwaway). The client shares the `.gcg` file where the platform supports +it, otherwise downloads it. Statistics (durable accounts only): wins, losses, draws, max points in a game, and max points for a single move (the best play, which already includes every word it formed plus the all-tiles bonus). A game that can no longer be continued — because a rule changed and an earlier diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index 3d8beed..025ffa9 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -228,9 +228,10 @@ UTC), суточного окна отсутствия (away; сетка по 10 ### История и статистика Завершённые партии архивируются в независимом от словаря виде и экспортируются -в GCG; экспорт доступен **только после завершения партии** (экспорт идущей партии -раскрыл бы журнал ходов), и клиент делится файлом `.gcg` там, где платформа это -поддерживает, иначе скачивает его. Статистика (только у постоянных аккаунтов): +в GCG; экспорт доступен **только после завершения партии** и никогда — для +тренировочной партии с ИИ (экспорт идущей партии раскрыл бы журнал ходов, а партия +с ИИ одноразовая). Клиент делится файлом `.gcg` там, где платформа это поддерживает, +иначе скачивает его. Статистика (только у постоянных аккаунтов): победы, поражения, ничьи, макс. очков за партию и макс. очков за один ход (лучший ход, уже включающий все образованные им слова и бонус за все фишки). Партия, которую больше нельзя продолжить — из-за изменения правил более ранний ход diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md index 469bd1a..53b6976 100644 --- a/docs/UI_DESIGN.md +++ b/docs/UI_DESIGN.md @@ -300,9 +300,11 @@ enabled on the first, uncached load) and flip in place when an event refreshes t Safari. - **History / GCG**: the in-game slide-down history lays each move out in a per-seat grid (the word(s) and the move score, no running total); *Export GCG* (the 📤 in the history - header) shares or downloads the `.gcg` file and appears only once the game is finished. + header) shares or downloads the `.gcg` file and appears only once the game is finished — and + never in an honest-AI game (throwaway practice). Confirming a resign reveals the full board: + it closes the history drawer (portrait) and zooms the board out. - **Finished game**: the board keeps no last-word highlight and no zoom; the history header - offers *Export GCG* (not *Drop game*) and the comms hub hides the 🔎 *Dictionary* tab; and + offers *Export GCG* (not *Drop game*; an AI game offers neither) and the comms hub hides the 🔎 *Dictionary* tab; and the footer (rack + tab bar) is **drawn but inert** (greyed, non-interactive) rather than hidden, so the layout does not jump. Chat send / nudge are the ⬆️ / 🛎️ icons. The input row is turn-driven: on your turn the message field shows until your one allowed message is diff --git a/ui/e2e/quickmatch.spec.ts b/ui/e2e/quickmatch.spec.ts index dc66901..2a2318d 100644 --- a/ui/e2e/quickmatch.spec.ts +++ b/ui/e2e/quickmatch.spec.ts @@ -59,6 +59,29 @@ test('AI game: 🤖 opponent, no wait, chat disabled, dictionary still works', a await expect(page.locator('.check input')).toBeEnabled(); }); +// GCG export is pointless for a practice AI game, so the finished AI game hides the 📤 export +// (the comms hub stays). Start an AI game, resign it, reopen the history and check the header. +test('AI game: no GCG export offered after it ends', async ({ page }) => { + await page.goto('/'); + await page.getByRole('button', { name: /guest/i }).click(); + await page.getByRole('button', { name: /New/ }).click(); + await page.locator('.variant').first().click(); // AI is the default opponent + await page.getByRole('button', { name: /Start game/i }).click(); + await expect(page.locator('.scoreboard').getByText('🤖')).toBeVisible(); + + // Resign the practice game from the history header. + await page.locator('.scoreboard').click(); + await page.getByRole('button', { name: 'Drop game' }).click(); + await page.locator('button.danger').click(); + await expect(page.locator('.status .over')).toBeVisible(); + + // Reopen the history (resigning auto-closed it): the finished AI game offers no GCG export, + // while the comms hub stays reachable. + await page.locator('.scoreboard').click(); + await expect(page.getByRole('button', { name: 'Export GCG' })).toHaveCount(0); + await expect(page.getByRole('button', { name: 'Chat' })).toBeVisible(); +}); + // The opponent_joined push is best-effort and never replayed, so a join that lands while the live // stream is down is lost. The waiting game screen recovers it without a push: a poll while the // stream is down, and a refetch on stream reconnect. The __stream hook (lib/app.svelte.ts) drops / diff --git a/ui/src/game/Game.svelte b/ui/src/game/Game.svelte index 8bc1c20..b098ea3 100644 --- a/ui/src/game/Game.svelte +++ b/ui/src/game/Game.svelte @@ -986,7 +986,13 @@ {#if view}
{#if gameOver} - + {#if view.game.vsAi} + + + {:else} + + {/if} {:else} {/if}