feat: "multiple words per turn" rule for Russian games
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m10s
Add a per-game rule chosen on New Game for Russian variants (default off = the
single-word rule; on = standard Scrabble). Off, only the main word along the play
direction is validated and scored; perpendicular cross-words are ignored,
including in robot move generation. The rule rides every create and enqueue
request and joins the matchmaking key, so games and auto-match stay one uniform
path; "Russian-only" is a UI affordance (English always sends standard and shows
no toggle).
- Engine: consume scrabble-solver v1.1.0's PlayOptions{IgnoreCrossWords}, threaded
through engine.Options.MultipleWordsPerTurn -> playOpts() into validate, score
and generate.
- Backend: thread the flag through game CreateParams/Game + store (games column),
lobby InvitationSettings + invitation row, and the matchmaker queue key (variant
+ rule); persisted, so a rebuilt-from-journal game keeps it. Baseline migration
gains multiple_words_per_turn (DB not versioned); jet regenerated.
- Edge: multiple_words_per_turn added to the EnqueueRequest / CreateInvitationRequest
FlatBuffers tables (Go + TS regenerated) and threaded through the gateway.
- UI: a "Multiple words per turn" toggle on New Game, shown for Russian variants
only (auto-match and friend invite), default off; English silently sends standard.
- Tests: backend engine/matchmaker; UI unit (gating) + Playwright e2e (solver
corner-case + GCG fixtures ship in v1.1.0). Docs + PRERELEASE tracker updated.
This commit is contained in:
@@ -13,16 +13,17 @@ import (
|
||||
)
|
||||
|
||||
type GameInvitations struct {
|
||||
InvitationID uuid.UUID `sql:"primary_key"`
|
||||
InviterID uuid.UUID
|
||||
Variant string
|
||||
TurnTimeoutSecs int32
|
||||
HintsAllowed bool
|
||||
HintsPerPlayer int16
|
||||
DropoutTiles string
|
||||
Status string
|
||||
GameID *uuid.UUID
|
||||
ExpiresAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
InvitationID uuid.UUID `sql:"primary_key"`
|
||||
InviterID uuid.UUID
|
||||
Variant string
|
||||
TurnTimeoutSecs int32
|
||||
HintsAllowed bool
|
||||
HintsPerPlayer int16
|
||||
DropoutTiles string
|
||||
MultipleWordsPerTurn bool
|
||||
Status string
|
||||
GameID *uuid.UUID
|
||||
ExpiresAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
@@ -13,21 +13,22 @@ import (
|
||||
)
|
||||
|
||||
type Games struct {
|
||||
GameID uuid.UUID `sql:"primary_key"`
|
||||
Variant string
|
||||
DictVersion string
|
||||
Seed int64
|
||||
Status string
|
||||
Players int16
|
||||
ToMove int16
|
||||
TurnStartedAt time.Time
|
||||
TurnTimeoutSecs int32
|
||||
HintsAllowed bool
|
||||
HintsPerPlayer int16
|
||||
MoveCount int32
|
||||
EndReason *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
DropoutTiles string
|
||||
GameID uuid.UUID `sql:"primary_key"`
|
||||
Variant string
|
||||
DictVersion string
|
||||
Seed int64
|
||||
Status string
|
||||
Players int16
|
||||
ToMove int16
|
||||
TurnStartedAt time.Time
|
||||
TurnTimeoutSecs int32
|
||||
HintsAllowed bool
|
||||
HintsPerPlayer int16
|
||||
MoveCount int32
|
||||
EndReason *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
DropoutTiles string
|
||||
MultipleWordsPerTurn bool
|
||||
}
|
||||
|
||||
@@ -17,18 +17,19 @@ type gameInvitationsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
InvitationID postgres.ColumnString
|
||||
InviterID postgres.ColumnString
|
||||
Variant postgres.ColumnString
|
||||
TurnTimeoutSecs postgres.ColumnInteger
|
||||
HintsAllowed postgres.ColumnBool
|
||||
HintsPerPlayer postgres.ColumnInteger
|
||||
DropoutTiles postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
GameID postgres.ColumnString
|
||||
ExpiresAt postgres.ColumnTimestampz
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
InvitationID postgres.ColumnString
|
||||
InviterID postgres.ColumnString
|
||||
Variant postgres.ColumnString
|
||||
TurnTimeoutSecs postgres.ColumnInteger
|
||||
HintsAllowed postgres.ColumnBool
|
||||
HintsPerPlayer postgres.ColumnInteger
|
||||
DropoutTiles postgres.ColumnString
|
||||
MultipleWordsPerTurn postgres.ColumnBool
|
||||
Status postgres.ColumnString
|
||||
GameID postgres.ColumnString
|
||||
ExpiresAt postgres.ColumnTimestampz
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@@ -70,39 +71,41 @@ func newGameInvitationsTable(schemaName, tableName, alias string) *GameInvitatio
|
||||
|
||||
func newGameInvitationsTableImpl(schemaName, tableName, alias string) gameInvitationsTable {
|
||||
var (
|
||||
InvitationIDColumn = postgres.StringColumn("invitation_id")
|
||||
InviterIDColumn = postgres.StringColumn("inviter_id")
|
||||
VariantColumn = postgres.StringColumn("variant")
|
||||
TurnTimeoutSecsColumn = postgres.IntegerColumn("turn_timeout_secs")
|
||||
HintsAllowedColumn = postgres.BoolColumn("hints_allowed")
|
||||
HintsPerPlayerColumn = postgres.IntegerColumn("hints_per_player")
|
||||
DropoutTilesColumn = postgres.StringColumn("dropout_tiles")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
ExpiresAtColumn = postgres.TimestampzColumn("expires_at")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
allColumns = postgres.ColumnList{InvitationIDColumn, InviterIDColumn, VariantColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, StatusColumn, GameIDColumn, ExpiresAtColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{InviterIDColumn, VariantColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, StatusColumn, GameIDColumn, ExpiresAtColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, StatusColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
InvitationIDColumn = postgres.StringColumn("invitation_id")
|
||||
InviterIDColumn = postgres.StringColumn("inviter_id")
|
||||
VariantColumn = postgres.StringColumn("variant")
|
||||
TurnTimeoutSecsColumn = postgres.IntegerColumn("turn_timeout_secs")
|
||||
HintsAllowedColumn = postgres.BoolColumn("hints_allowed")
|
||||
HintsPerPlayerColumn = postgres.IntegerColumn("hints_per_player")
|
||||
DropoutTilesColumn = postgres.StringColumn("dropout_tiles")
|
||||
MultipleWordsPerTurnColumn = postgres.BoolColumn("multiple_words_per_turn")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
ExpiresAtColumn = postgres.TimestampzColumn("expires_at")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
allColumns = postgres.ColumnList{InvitationIDColumn, InviterIDColumn, VariantColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, StatusColumn, GameIDColumn, ExpiresAtColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{InviterIDColumn, VariantColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, StatusColumn, GameIDColumn, ExpiresAtColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn, StatusColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
)
|
||||
|
||||
return gameInvitationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
InvitationID: InvitationIDColumn,
|
||||
InviterID: InviterIDColumn,
|
||||
Variant: VariantColumn,
|
||||
TurnTimeoutSecs: TurnTimeoutSecsColumn,
|
||||
HintsAllowed: HintsAllowedColumn,
|
||||
HintsPerPlayer: HintsPerPlayerColumn,
|
||||
DropoutTiles: DropoutTilesColumn,
|
||||
Status: StatusColumn,
|
||||
GameID: GameIDColumn,
|
||||
ExpiresAt: ExpiresAtColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
InvitationID: InvitationIDColumn,
|
||||
InviterID: InviterIDColumn,
|
||||
Variant: VariantColumn,
|
||||
TurnTimeoutSecs: TurnTimeoutSecsColumn,
|
||||
HintsAllowed: HintsAllowedColumn,
|
||||
HintsPerPlayer: HintsPerPlayerColumn,
|
||||
DropoutTiles: DropoutTilesColumn,
|
||||
MultipleWordsPerTurn: MultipleWordsPerTurnColumn,
|
||||
Status: StatusColumn,
|
||||
GameID: GameIDColumn,
|
||||
ExpiresAt: ExpiresAtColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
||||
@@ -17,23 +17,24 @@ type gamesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
GameID postgres.ColumnString
|
||||
Variant postgres.ColumnString
|
||||
DictVersion postgres.ColumnString
|
||||
Seed postgres.ColumnInteger
|
||||
Status postgres.ColumnString
|
||||
Players postgres.ColumnInteger
|
||||
ToMove postgres.ColumnInteger
|
||||
TurnStartedAt postgres.ColumnTimestampz
|
||||
TurnTimeoutSecs postgres.ColumnInteger
|
||||
HintsAllowed postgres.ColumnBool
|
||||
HintsPerPlayer postgres.ColumnInteger
|
||||
MoveCount postgres.ColumnInteger
|
||||
EndReason postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
FinishedAt postgres.ColumnTimestampz
|
||||
DropoutTiles postgres.ColumnString
|
||||
GameID postgres.ColumnString
|
||||
Variant postgres.ColumnString
|
||||
DictVersion postgres.ColumnString
|
||||
Seed postgres.ColumnInteger
|
||||
Status postgres.ColumnString
|
||||
Players postgres.ColumnInteger
|
||||
ToMove postgres.ColumnInteger
|
||||
TurnStartedAt postgres.ColumnTimestampz
|
||||
TurnTimeoutSecs postgres.ColumnInteger
|
||||
HintsAllowed postgres.ColumnBool
|
||||
HintsPerPlayer postgres.ColumnInteger
|
||||
MoveCount postgres.ColumnInteger
|
||||
EndReason postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
FinishedAt postgres.ColumnTimestampz
|
||||
DropoutTiles postgres.ColumnString
|
||||
MultipleWordsPerTurn postgres.ColumnBool
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@@ -75,49 +76,51 @@ func newGamesTable(schemaName, tableName, alias string) *GamesTable {
|
||||
|
||||
func newGamesTableImpl(schemaName, tableName, alias string) gamesTable {
|
||||
var (
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
VariantColumn = postgres.StringColumn("variant")
|
||||
DictVersionColumn = postgres.StringColumn("dict_version")
|
||||
SeedColumn = postgres.IntegerColumn("seed")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
PlayersColumn = postgres.IntegerColumn("players")
|
||||
ToMoveColumn = postgres.IntegerColumn("to_move")
|
||||
TurnStartedAtColumn = postgres.TimestampzColumn("turn_started_at")
|
||||
TurnTimeoutSecsColumn = postgres.IntegerColumn("turn_timeout_secs")
|
||||
HintsAllowedColumn = postgres.BoolColumn("hints_allowed")
|
||||
HintsPerPlayerColumn = postgres.IntegerColumn("hints_per_player")
|
||||
MoveCountColumn = postgres.IntegerColumn("move_count")
|
||||
EndReasonColumn = postgres.StringColumn("end_reason")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
FinishedAtColumn = postgres.TimestampzColumn("finished_at")
|
||||
DropoutTilesColumn = postgres.StringColumn("dropout_tiles")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, DropoutTilesColumn}
|
||||
mutableColumns = postgres.ColumnList{VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, DropoutTilesColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, ToMoveColumn, TurnStartedAtColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, CreatedAtColumn, UpdatedAtColumn, DropoutTilesColumn}
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
VariantColumn = postgres.StringColumn("variant")
|
||||
DictVersionColumn = postgres.StringColumn("dict_version")
|
||||
SeedColumn = postgres.IntegerColumn("seed")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
PlayersColumn = postgres.IntegerColumn("players")
|
||||
ToMoveColumn = postgres.IntegerColumn("to_move")
|
||||
TurnStartedAtColumn = postgres.TimestampzColumn("turn_started_at")
|
||||
TurnTimeoutSecsColumn = postgres.IntegerColumn("turn_timeout_secs")
|
||||
HintsAllowedColumn = postgres.BoolColumn("hints_allowed")
|
||||
HintsPerPlayerColumn = postgres.IntegerColumn("hints_per_player")
|
||||
MoveCountColumn = postgres.IntegerColumn("move_count")
|
||||
EndReasonColumn = postgres.StringColumn("end_reason")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
FinishedAtColumn = postgres.TimestampzColumn("finished_at")
|
||||
DropoutTilesColumn = postgres.StringColumn("dropout_tiles")
|
||||
MultipleWordsPerTurnColumn = postgres.BoolColumn("multiple_words_per_turn")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn}
|
||||
mutableColumns = postgres.ColumnList{VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, ToMoveColumn, TurnStartedAtColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, CreatedAtColumn, UpdatedAtColumn, DropoutTilesColumn, MultipleWordsPerTurnColumn}
|
||||
)
|
||||
|
||||
return gamesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
GameID: GameIDColumn,
|
||||
Variant: VariantColumn,
|
||||
DictVersion: DictVersionColumn,
|
||||
Seed: SeedColumn,
|
||||
Status: StatusColumn,
|
||||
Players: PlayersColumn,
|
||||
ToMove: ToMoveColumn,
|
||||
TurnStartedAt: TurnStartedAtColumn,
|
||||
TurnTimeoutSecs: TurnTimeoutSecsColumn,
|
||||
HintsAllowed: HintsAllowedColumn,
|
||||
HintsPerPlayer: HintsPerPlayerColumn,
|
||||
MoveCount: MoveCountColumn,
|
||||
EndReason: EndReasonColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
FinishedAt: FinishedAtColumn,
|
||||
DropoutTiles: DropoutTilesColumn,
|
||||
GameID: GameIDColumn,
|
||||
Variant: VariantColumn,
|
||||
DictVersion: DictVersionColumn,
|
||||
Seed: SeedColumn,
|
||||
Status: StatusColumn,
|
||||
Players: PlayersColumn,
|
||||
ToMove: ToMoveColumn,
|
||||
TurnStartedAt: TurnStartedAtColumn,
|
||||
TurnTimeoutSecs: TurnTimeoutSecsColumn,
|
||||
HintsAllowed: HintsAllowedColumn,
|
||||
HintsPerPlayer: HintsPerPlayerColumn,
|
||||
MoveCount: MoveCountColumn,
|
||||
EndReason: EndReasonColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
FinishedAt: FinishedAtColumn,
|
||||
DropoutTiles: DropoutTilesColumn,
|
||||
MultipleWordsPerTurn: MultipleWordsPerTurnColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
||||
@@ -97,6 +97,7 @@ CREATE TABLE games (
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
finished_at timestamptz,
|
||||
dropout_tiles text NOT NULL DEFAULT 'remove',
|
||||
multiple_words_per_turn boolean NOT NULL DEFAULT true,
|
||||
CONSTRAINT games_variant_chk CHECK (variant IN ('scrabble_en', 'scrabble_ru', 'erudit_ru')),
|
||||
CONSTRAINT games_status_chk CHECK (status IN ('active', 'finished')),
|
||||
CONSTRAINT games_players_chk CHECK (players BETWEEN 2 AND 4),
|
||||
@@ -260,6 +261,7 @@ CREATE TABLE game_invitations (
|
||||
hints_allowed boolean NOT NULL DEFAULT true,
|
||||
hints_per_player smallint NOT NULL DEFAULT 1,
|
||||
dropout_tiles text NOT NULL DEFAULT 'remove',
|
||||
multiple_words_per_turn boolean NOT NULL DEFAULT true,
|
||||
status text NOT NULL DEFAULT 'pending',
|
||||
game_id uuid REFERENCES games (game_id) ON DELETE SET NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
|
||||
Reference in New Issue
Block a user