feat(rules): forbid repeating a word already on the board in Erudit
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s

Russian "Эрудит" treats a word laid on the board as belonging to the
game: it cannot be laid again. Neither the solver, the backend nor the
offline JS port knew the rule, so a player (and the robot) could replay a
word freely. Official Scrabble places no such restriction, so both
Scrabble variants keep playing unrestricted.

The rule applies in two ways. A play whose main word is already on the
board is illegal, and is neither accepted nor generated. A play whose
perpendicular cross-word is already there stands — that word is
incidental to laying the main word — but scores nothing. The set of
played words is the game's own move journal, main words and cross-words
alike, compared decoded, so a word spelled with a blank is the same word.

It lives in the game layer, not the solver: only a game knows its
history, and the solver stays stateless and standard-rules. The backend
applies it at submit, at the move preview and over generated moves
(filtering and re-ranking them, so neither the robot nor the hint can
offer a play the engine would then refuse); the client port does the same
for the offline engine and for the on-device preview of an online game.

The rule is pinned per game (games.no_repeat_words, set from the variant
at creation) rather than keyed on the variant, because a game is replayed
from its journal on every open. Applied retroactively it would make an
already-played repeat illegal — closing that game as a draw — and would
rescore a play whose cross-word repeats an earlier word, shifting a live
game's totals. Games created before the rule keep playing without it. The
flag rides the wire as a trailing field because the client's preview must
score the way the server does, and offline games pin the same answer in
their own record.
This commit is contained in:
Ilia Denisov
2026-07-27 18:33:56 +02:00
parent ebd1f05da8
commit d7337d24ea
49 changed files with 802 additions and 26 deletions
+18
View File
@@ -561,6 +561,24 @@ Key points:
it on (standard). For auto-match the rule is part of the matchmaking key, so only players
who chose the same rule are paired (the rule field rides every create/enqueue request, so
matchmaking stays one uniform path).
- **No repeated words (Erudit).** A word laid on the board cannot be laid again — the "Эрудит"
rule; official Scrabble has no such restriction, so both Scrabble variants never take it. It
applies in two ways: a play whose **main word** is already on the board is illegal, and a play
whose **cross-word** is already there stands (it is incidental to laying the main word) but that
cross-word scores nothing. The set of played words is the game's own **move journal** — the main
word and every cross-word of every play, compared decoded, so a word spelled with a blank is the
same word. The rule lives in the **game layer**, not the solver: `backend/internal/engine`
(`norepeat.go`) and its client port `ui/src/lib/dict/norepeat.ts` apply it at submit, at the move
preview and over generated moves — filtering and re-ranking them, so neither the robot nor the
hint can offer a play the engine would then refuse. The solver stays stateless and
standard-rules; only a game knows its history.
It is **pinned per game** (`games.no_repeat_words`, set from the variant at creation and read
back thereafter, never re-derived) rather than keyed on the variant, because a game is replayed
from its journal on every open: applied retroactively it would make an already-played repeat
illegal — closing that game as a draw (§9.1) — and would rescore a play whose cross-word repeats
an earlier word. Games created before the rule therefore keep playing without it. The flag rides
the wire (`GameView.no_repeat_words`) because the client's on-device move preview must score the
way the server does; offline games pin the same answer in their own record.
- **End of game**: the bag is empty **and** a player empties their rack, **or**
**6 consecutive scoreless turns** (passes/exchanges), **or** a resignation, or
a missed turn. The **per-game turn timeout** is chosen at creation
+10 -1
View File
@@ -196,7 +196,16 @@ the simplified **single-word rule** — only the word laid along the player's li
real word (and it must still cross or extend letters already on the board along that line),
and any incidental perpendicular words are ignored and not scored — while on is
standard Scrabble. English games are always standard and show no such toggle. In auto-match
the choice joins the pairing key, so a player only meets opponents who picked the same rule. Friend games (24) are
the choice joins the pairing key, so a player only meets opponents who picked the same rule.
**Erudite forbids repeating a word.** A word laid on the board belongs to the game: it cannot be laid
again, and the AI never plays one either. A play whose main word is already on the board is rejected
like any illegal move; a play that only forms an already-played word **across** its main word stands —
that word is incidental to the move — but scores nothing. The rule is not a choice: every Erudite
game takes it, and both Scrabble variants (where the official rules place no such restriction) never
do, so the variant's one-line description on **New Game** reads "no repeated words". Games started
before the rule existed keep playing without it, so a game in progress never changes rules under its
players. Friend games (24) are
formed by inviting players from the friend list (an invitation, like a friend code,
is shareable as a Telegram deep link that opens it directly — on VK, which forwards no payload to the
Mini App, the link only opens the app and the recipient enters the copied code by hand): the inviter chooses the
+10 -1
View File
@@ -202,7 +202,16 @@ e-mail) либо ввод фразы. Активные игры форфейтя
этой линии), а случайные перпендикулярные слова игнорируются и не засчитываются;
включена — обычный скрэббл. Английские игры всегда по стандартным правилам и тоггл не
показывают. В авто-подборе выбор входит в ключ подбора, поэтому игрок сводится только с теми,
кто выбрал то же правило. Игры с друзьями (2–4)
кто выбрал то же правило.
**В «Эрудите» слово нельзя повторять.** Выставленное на доску слово принадлежит партии: выставить
его ещё раз нельзя, и робот такого хода тоже не делает. Ход, чьё основное слово уже стоит на доске,
отклоняется как любой недопустимый; ход, который лишь образует уже выставленное слово **поперёк**
основного, засчитывается — такое слово побочно для хода, — но очков не приносит. Правило не
выбирается: его берёт каждая партия «Эрудита», и никогда — оба варианта скрэббла (в официальных
правилах такого ограничения нет), поэтому однострочное описание варианта на экране **новой игры**
содержит «слова без повтора». Партии, начатые до появления правила, продолжают играть без него,
так что правила идущей партии не меняются у игроков на ходу. Игры с друзьями (2–4)
формируются приглашением игроков из списка друзей (приглашение, как и код друга,
можно отправить deep-link'ом в Telegram, который откроет его сразу — на VK, который не передаёт Mini App
никакой нагрузки, ссылка лишь открывает приложение, а скопированный код получатель вводит вручную): инициатор