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:
@@ -27,17 +27,18 @@ type invitationInviteeDTO struct {
|
||||
|
||||
// invitationDTO is a friend-game invitation with its settings and invitees.
|
||||
type invitationDTO struct {
|
||||
ID string `json:"id"`
|
||||
Inviter accountRefDTO `json:"inviter"`
|
||||
Invitees []invitationInviteeDTO `json:"invitees"`
|
||||
Variant string `json:"variant"`
|
||||
TurnTimeoutSecs int `json:"turn_timeout_secs"`
|
||||
HintsAllowed bool `json:"hints_allowed"`
|
||||
HintsPerPlayer int `json:"hints_per_player"`
|
||||
DropoutTiles string `json:"dropout_tiles"`
|
||||
Status string `json:"status"`
|
||||
GameID string `json:"game_id,omitempty"`
|
||||
ExpiresAtUnix int64 `json:"expires_at_unix"`
|
||||
ID string `json:"id"`
|
||||
Inviter accountRefDTO `json:"inviter"`
|
||||
Invitees []invitationInviteeDTO `json:"invitees"`
|
||||
Variant string `json:"variant"`
|
||||
TurnTimeoutSecs int `json:"turn_timeout_secs"`
|
||||
HintsAllowed bool `json:"hints_allowed"`
|
||||
HintsPerPlayer int `json:"hints_per_player"`
|
||||
DropoutTiles string `json:"dropout_tiles"`
|
||||
MultipleWordsPerTurn bool `json:"multiple_words_per_turn"`
|
||||
Status string `json:"status"`
|
||||
GameID string `json:"game_id,omitempty"`
|
||||
ExpiresAtUnix int64 `json:"expires_at_unix"`
|
||||
}
|
||||
|
||||
// invitationListDTO is the caller's open invitations.
|
||||
@@ -47,27 +48,29 @@ type invitationListDTO struct {
|
||||
|
||||
// createInvitationRequest proposes a friend game to the named invitees.
|
||||
type createInvitationRequest struct {
|
||||
InviteeIDs []string `json:"invitee_ids"`
|
||||
Variant string `json:"variant"`
|
||||
TurnTimeoutSecs int `json:"turn_timeout_secs"`
|
||||
HintsAllowed bool `json:"hints_allowed"`
|
||||
HintsPerPlayer int `json:"hints_per_player"`
|
||||
DropoutTiles string `json:"dropout_tiles"`
|
||||
InviteeIDs []string `json:"invitee_ids"`
|
||||
Variant string `json:"variant"`
|
||||
TurnTimeoutSecs int `json:"turn_timeout_secs"`
|
||||
HintsAllowed bool `json:"hints_allowed"`
|
||||
HintsPerPlayer int `json:"hints_per_player"`
|
||||
DropoutTiles string `json:"dropout_tiles"`
|
||||
MultipleWordsPerTurn bool `json:"multiple_words_per_turn"`
|
||||
}
|
||||
|
||||
// invitationDTOFrom projects a lobby invitation, resolving names through memo.
|
||||
func (s *Server) invitationDTOFrom(ctx context.Context, inv lobby.Invitation, memo map[string]string) invitationDTO {
|
||||
dto := invitationDTO{
|
||||
ID: inv.ID.String(),
|
||||
Inviter: s.namedRef(ctx, inv.InviterID, memo),
|
||||
Invitees: make([]invitationInviteeDTO, 0, len(inv.Invitees)),
|
||||
Variant: inv.Settings.Variant.String(),
|
||||
TurnTimeoutSecs: int(inv.Settings.TurnTimeout.Seconds()),
|
||||
HintsAllowed: inv.Settings.HintsAllowed,
|
||||
HintsPerPlayer: inv.Settings.HintsPerPlayer,
|
||||
DropoutTiles: inv.Settings.DropoutTiles.String(),
|
||||
Status: inv.Status,
|
||||
ExpiresAtUnix: inv.ExpiresAt.Unix(),
|
||||
ID: inv.ID.String(),
|
||||
Inviter: s.namedRef(ctx, inv.InviterID, memo),
|
||||
Invitees: make([]invitationInviteeDTO, 0, len(inv.Invitees)),
|
||||
Variant: inv.Settings.Variant.String(),
|
||||
TurnTimeoutSecs: int(inv.Settings.TurnTimeout.Seconds()),
|
||||
HintsAllowed: inv.Settings.HintsAllowed,
|
||||
HintsPerPlayer: inv.Settings.HintsPerPlayer,
|
||||
DropoutTiles: inv.Settings.DropoutTiles.String(),
|
||||
MultipleWordsPerTurn: inv.Settings.MultipleWordsPerTurn,
|
||||
Status: inv.Status,
|
||||
ExpiresAtUnix: inv.ExpiresAt.Unix(),
|
||||
}
|
||||
if inv.GameID != nil {
|
||||
dto.GameID = inv.GameID.String()
|
||||
@@ -102,9 +105,10 @@ func (s *Server) handleCreateInvitation(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
settings := lobby.InvitationSettings{
|
||||
Variant: variant,
|
||||
HintsAllowed: req.HintsAllowed,
|
||||
HintsPerPlayer: req.HintsPerPlayer,
|
||||
Variant: variant,
|
||||
HintsAllowed: req.HintsAllowed,
|
||||
HintsPerPlayer: req.HintsPerPlayer,
|
||||
MultipleWordsPerTurn: req.MultipleWordsPerTurn,
|
||||
}
|
||||
if req.TurnTimeoutSecs > 0 {
|
||||
settings.TurnTimeout = time.Duration(req.TurnTimeoutSecs) * time.Second
|
||||
|
||||
@@ -133,9 +133,10 @@ func (s *Server) handleGameState(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, dto)
|
||||
}
|
||||
|
||||
// enqueueRequest joins the per-variant auto-match pool.
|
||||
// enqueueRequest joins the per-variant auto-match pool under a per-turn word rule.
|
||||
type enqueueRequest struct {
|
||||
Variant string `json:"variant"`
|
||||
Variant string `json:"variant"`
|
||||
MultipleWordsPerTurn bool `json:"multiple_words_per_turn"`
|
||||
}
|
||||
|
||||
// handleEnqueue joins the auto-match pool for a variant.
|
||||
@@ -155,7 +156,7 @@ func (s *Server) handleEnqueue(c *gin.Context) {
|
||||
abortBadRequest(c, "unknown variant")
|
||||
return
|
||||
}
|
||||
res, err := s.matchmaker.Enqueue(c.Request.Context(), uid, variant)
|
||||
res, err := s.matchmaker.Enqueue(c.Request.Context(), uid, variant, req.MultipleWordsPerTurn)
|
||||
if err != nil {
|
||||
s.abortErr(c, err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user