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
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:
@@ -34,4 +34,5 @@ type Games struct {
|
||||
MultipleWordsPerTurn bool
|
||||
VsAi bool
|
||||
GameKind int16
|
||||
NoRepeatWords bool
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ type gamesTable struct {
|
||||
MultipleWordsPerTurn postgres.ColumnBool
|
||||
VsAi postgres.ColumnBool
|
||||
GameKind postgres.ColumnInteger
|
||||
NoRepeatWords postgres.ColumnBool
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@@ -100,9 +101,10 @@ func newGamesTableImpl(schemaName, tableName, alias string) gamesTable {
|
||||
MultipleWordsPerTurnColumn = postgres.BoolColumn("multiple_words_per_turn")
|
||||
VsAiColumn = postgres.BoolColumn("vs_ai")
|
||||
GameKindColumn = postgres.IntegerColumn("game_kind")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, OpenDeadlineAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, VsAiColumn, GameKindColumn}
|
||||
mutableColumns = postgres.ColumnList{VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, OpenDeadlineAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, VsAiColumn, GameKindColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, ToMoveColumn, TurnStartedAtColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, CreatedAtColumn, UpdatedAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, VsAiColumn, GameKindColumn}
|
||||
NoRepeatWordsColumn = postgres.BoolColumn("no_repeat_words")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, OpenDeadlineAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, VsAiColumn, GameKindColumn, NoRepeatWordsColumn}
|
||||
mutableColumns = postgres.ColumnList{VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, OpenDeadlineAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, VsAiColumn, GameKindColumn, NoRepeatWordsColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, ToMoveColumn, TurnStartedAtColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, CreatedAtColumn, UpdatedAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, VsAiColumn, GameKindColumn, NoRepeatWordsColumn}
|
||||
)
|
||||
|
||||
return gamesTable{
|
||||
@@ -130,6 +132,7 @@ func newGamesTableImpl(schemaName, tableName, alias string) gamesTable {
|
||||
MultipleWordsPerTurn: MultipleWordsPerTurnColumn,
|
||||
VsAi: VsAiColumn,
|
||||
GameKind: GameKindColumn,
|
||||
NoRepeatWords: NoRepeatWordsColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- The "Эрудит" no-repeat-words rule: a word laid on the board cannot be laid again. It is pinned
|
||||
-- per game rather than derived from the variant, because a game is replayed from its journal on
|
||||
-- every open — a rule applied retroactively would make an already-played repeat illegal (voiding
|
||||
-- the game) and would rescore a play whose cross-word repeats an earlier word. Every existing game
|
||||
-- therefore keeps the unrestricted rule (DEFAULT false, no backfill) and only new erudit_ru games
|
||||
-- are created with it on. Additive column — applies forward via goose with no data rewrite (no
|
||||
-- contour wipe); an image rollback ignores the column.
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE backend.games ADD COLUMN no_repeat_words boolean NOT NULL DEFAULT false;
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE backend.games DROP COLUMN no_repeat_words;
|
||||
Reference in New Issue
Block a user