feat(ui): merge Exchange/Pass into one action; drop dead Tournaments tab
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) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m0s

Lobby: remove the inert 🏆 Tournaments tab (it only raised a 'coming soon' toast); the lobby is back to three tabs, matching docs/FUNCTIONAL.md.

Game: fold the separate 🥺 Skip (pass) tab into the 🔄 tab, now Exchange/Pass. Its dialog passes when no tile is selected (button 'Pass without exchanging') and exchanges when tiles are ('Exchange N'). The tab is no longer gated on an empty bag (pass must stay reachable in the endgame); inside the dialog tile selection is disabled while the bag is below a full rack (bagLen >= RACK_SIZE).

The merge is UI-only. A pass is NOT an exchange of zero tiles: the rules allow an exchange only with a full rack left in the bag and forbid a zero-tile swap, while a pass is always legal; GCG (Poslfit) writes a pass as a bare '-' and an exchange as '-TILES'. Pass and exchange stay distinct end-to-end (wire, engine, history/GCG); the dialog dispatches the existing gateway.pass / gateway.exchange. No backend/wire/history/GCG change.

Docs: docs/UI_DESIGN.md, docs/FUNCTIONAL.md (+_ru), PRERELEASE.md. Tests: ui/e2e/game.spec.ts.
This commit is contained in:
Ilia Denisov
2026-06-13 16:41:55 +02:00
parent 1d41cf8222
commit a4e6727c70
9 changed files with 82 additions and 41 deletions
+3 -5
View File
@@ -30,7 +30,6 @@ export const en = {
'lobby.noFinished': 'No finished games yet.',
'lobby.new': 'New',
'lobby.stats': 'Stats',
'lobby.tournaments': 'Tourn.',
'lobby.profile': 'Profile',
'lobby.settings': 'Settings',
'lobby.about': 'About',
@@ -38,7 +37,6 @@ export const en = {
'lobby.theirTurn': 'Their turn',
'lobby.hideGame': 'Remove from list',
'lobby.vs': 'vs {opponents}',
'lobby.soon': 'Coming soon',
'new.title': 'New game',
'new.subtitle': 'Auto-match with another player',
@@ -63,8 +61,7 @@ export const en = {
'game.waiting': "Waiting for {name}",
'game.makeMove': 'Make move',
'game.reset': 'Reset',
'game.draw': 'Draw',
'game.skip': 'Skip',
'game.draw': 'Exchange/Pass',
'game.shuffle': 'Shuffle',
'game.hint': 'Hint',
'game.chat': 'Chat',
@@ -75,8 +72,9 @@ export const en = {
'game.previewIllegal': 'Not a legal move',
'game.oneWordRule': 'One word per turn',
'game.chooseBlank': 'Choose a letter for the blank',
'game.exchangeTitle': 'Select tiles to exchange',
'game.exchangeTitle': 'Exchange or pass',
'game.exchangeConfirm': 'Exchange {n}',
'game.passNoExchange': 'Pass without exchanging',
'game.confirmResign': 'Resign this game?',
'game.hintShown': 'Best move: {word} for {n}',
'game.over': 'Game over',
+3 -5
View File
@@ -31,7 +31,6 @@ export const ru: Record<MessageKey, string> = {
'lobby.noFinished': 'Пока нет завершённых игр.',
'lobby.new': 'Новая',
'lobby.stats': 'Статы',
'lobby.tournaments': 'Турниры',
'lobby.profile': 'Профиль',
'lobby.settings': 'Настройки',
'lobby.about': 'О программе',
@@ -39,7 +38,6 @@ export const ru: Record<MessageKey, string> = {
'lobby.theirTurn': 'Ход соперника',
'lobby.hideGame': 'Убрать из списка',
'lobby.vs': 'против {opponents}',
'lobby.soon': 'Скоро',
'new.title': 'Новая игра',
'new.subtitle': 'Автоподбор соперника',
@@ -64,8 +62,7 @@ export const ru: Record<MessageKey, string> = {
'game.waiting': 'Ожидаем {name}',
'game.makeMove': 'Сделать ход',
'game.reset': 'Сброс',
'game.draw': 'Обмен',
'game.skip': 'Пас',
'game.draw': 'Обмен/Пас',
'game.shuffle': 'Перемешать',
'game.hint': 'Подсказка',
'game.chat': 'Чат',
@@ -76,8 +73,9 @@ export const ru: Record<MessageKey, string> = {
'game.previewIllegal': 'Недопустимый ход',
'game.oneWordRule': 'Одно слово за ход',
'game.chooseBlank': 'Выберите букву для бланка',
'game.exchangeTitle': 'Выберите фишки для обмена',
'game.exchangeTitle': 'Обмен или пас',
'game.exchangeConfirm': 'Обменять {n}',
'game.passNoExchange': 'Пас без обмена',
'game.confirmResign': 'Сдаться в этой игре?',
'game.hintShown': 'Лучший ход: {word} на {n}',
'game.over': 'Игра окончена',