diff --git a/ui/e2e/game.spec.ts b/ui/e2e/game.spec.ts index 0c96883..f6e77e4 100644 --- a/ui/e2e/game.spec.ts +++ b/ui/e2e/game.spec.ts @@ -29,6 +29,14 @@ test('placing a tile and confirming via ✅ commits the move', async ({ page }) await expect(page.locator('.make')).toBeHidden(); }); +test('new game: variant buttons show a rules summary and the move-limit', async ({ page }) => { + await page.goto('/'); + await page.getByRole('button', { name: /guest/i }).click(); + await page.getByRole('button', { name: /New/ }).click(); // lobby tab bar -> auto-match + await expect(page.locator('.vrules').first()).toBeVisible(); // per-variant rules summary + await expect(page.locator('.movelimit')).toBeVisible(); // turn-time under the buttons +}); + test('a pending tile recalls on double-tap, not on a single tap', async ({ page }) => { await openGame(page); await page.locator('.rack .tile').first().click(); diff --git a/ui/public/flag-ussr.svg b/ui/public/flag-ussr.svg new file mode 100644 index 0000000..90b07b7 --- /dev/null +++ b/ui/public/flag-ussr.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index 03189ce..3a17859 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -45,6 +45,10 @@ export const en = { 'new.erudit': 'Erudite', 'new.find': 'Find a game', 'new.searching': 'Looking for an opponent…', + 'new.rulesEnglish': '100 tiles · bingo +50', + 'new.rulesRussian': '104 tiles · ё is a letter · bingo +50', + 'new.rulesErudit': '131 tiles · ё = е · no centre ×2 · bonus +15', + 'new.moveLimit': 'Move time: {n} h 00 min', 'game.bag': '{n} in the bag', 'game.bagEmpty': 'Bag is empty', diff --git a/ui/src/lib/i18n/ru.ts b/ui/src/lib/i18n/ru.ts index c7cec61..b8a2614 100644 --- a/ui/src/lib/i18n/ru.ts +++ b/ui/src/lib/i18n/ru.ts @@ -46,6 +46,10 @@ export const ru: Record = { 'new.erudit': 'Эрудит', 'new.find': 'Найти игру', 'new.searching': 'Ищем соперника…', + 'new.rulesEnglish': '100 фишек · бинго +50', + 'new.rulesRussian': '104 фишки · ё — отдельная буква · бинго +50', + 'new.rulesErudit': '131 фишка · ё = е · центр не удваивает · бонус +15', + 'new.moveLimit': 'Время на ход: {n} ч. 00 мин.', 'game.bag': '{n} в мешке', 'game.bagEmpty': 'Мешок пуст', diff --git a/ui/src/lib/variants.ts b/ui/src/lib/variants.ts index a622c2b..62e3144 100644 --- a/ui/src/lib/variants.ts +++ b/ui/src/lib/variants.ts @@ -28,6 +28,22 @@ export function variantNameKey(v: Variant): MessageKey { return ALL_VARIANTS.find((o) => o.id === v)?.label ?? 'new.english'; } +// VARIANT_RULES is the i18n key for each variant's one-line rules summary on the New Game +// buttons (bag size, the ё rule, bonus differences), sourced from the engine rulesets. +export const VARIANT_RULES: Record = { + english: 'new.rulesEnglish', + russian_scrabble: 'new.rulesRussian', + erudit: 'new.rulesErudit', +}; + +// VARIANT_FLAG is the flag shown on a variant button: an emoji for the Scrabble variants; +// Erudit uses the bundled USSR flag SVG (public/flag-ussr.svg), so its entry is empty. +export const VARIANT_FLAG: Record = { + english: '🇺🇸', + russian_scrabble: '🇷🇺', + erudit: '', +}; + // VARIANT_LANGUAGE maps each variant to its game language. en -> English; // ru -> Russian + Эрудит. export const VARIANT_LANGUAGE: Record = { english: 'en', russian_scrabble: 'ru', erudit: 'ru' }; diff --git a/ui/src/screens/NewGame.svelte b/ui/src/screens/NewGame.svelte index 0b64511..caa27ee 100644 --- a/ui/src/screens/NewGame.svelte +++ b/ui/src/screens/NewGame.svelte @@ -6,7 +6,10 @@ import { navigate } from '../lib/router.svelte'; import { t, type MessageKey } from '../lib/i18n/index.svelte'; import type { AccountRef, Variant } from '../lib/model'; - import { availableVariants } from '../lib/variants'; + import { availableVariants, VARIANT_FLAG, VARIANT_RULES } from '../lib/variants'; + + // The auto-match move clock (mirrors backend game.DefaultTurnTimeout = 24h). + const AUTO_MATCH_HOURS = 24; // The offered variants are gated by the languages the sign-in service supports // (Stage 15); the auto-match list and the friend-invite picker both use this. @@ -141,9 +144,20 @@

{t('new.subtitle')}

{#each variants as v (v.id)} - + {/each}
+

{t('new.moveLimit', { n: AUTO_MATCH_HOURS })}

{:else if friends.length === 0}

{t('new.noFriends')}

{:else} @@ -207,15 +221,48 @@ flex-direction: column; gap: 10px; } + /* A plaque per variant (like the lobby game cards): the name with its flag on the right, + and a one-line rules summary below. */ .variant { - padding: 16px; + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 14px; border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: var(--radius); + text-align: left; + user-select: none; + } + .vmain { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .vname { font-size: 1.05rem; font-weight: 600; - user-select: none; + } + .vflag { + font-size: 1.3rem; + line-height: 1; + } + .vflag-img { + width: 1.6rem; + height: auto; + border-radius: 2px; + } + .vrules { + font-size: 0.8rem; + color: var(--text-muted); + } + .movelimit { + margin: 0; + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; } .seg { display: flex;