92f48a3b12
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 44s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m9s
A single tile that only extended a word perpendicular to the client-declared direction was rejected: the UI always sent dir=H for one-tile plays (the dirOverride/Controls toggle was orphaned in the Stage 7 game rework), so placing "А" above "БАК" to form "АБАК" failed the solver's main-word-length check even though the word is in the dictionary. Make the backend infer a play's orientation from the placed tiles and the board (internal/engine.resolveDirection): two or more tiles by the line they share, a lone tile by the axis it abuts (longer word wins, horizontal on a tie). Direction becomes an output, not an input: drop dir from the SubmitPlay/Eval wire requests and add it to EvalResult. Journal replay keeps trusting the stored "H"/"V" (SubmitPlayDir) so a rebuilt game matches the one committed. UI: stop computing/sending direction; the preview now shows the words a move forms with its total score (game.previewWords); the make-move control is disabled until the play is confirmed legal; the "your turn" label hides while tiles are pending. Delete the orphaned Controls.svelte. Regenerate the FlatBuffers bindings (Go + TS) and update the gateway transcode and the loadtest edge client to the new contract. Bake the decision into ARCHITECTURE.md (§5/§9.1), FUNCTIONAL.md (+ _ru) and the backend README.
80 lines
3.0 KiB
Go
80 lines
3.0 KiB
Go
//go:build integration
|
|
|
|
package inttest
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"scrabble/backend/internal/game"
|
|
)
|
|
|
|
// TestDraftPersistAndConflictReset covers draft persistence: a round-trip of the
|
|
// rack order + board tiles, the actor's own draft cleared on their move, and an opponent's
|
|
// board draft reset when a committed play overlaps one of its cells (the rack order kept).
|
|
func TestDraftPersistAndConflictReset(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc, gameID, seats, hint := newDraftGame(t)
|
|
|
|
// Round-trip seat 0's rack order + a board draft.
|
|
d0 := game.Draft{RackOrder: "QANIWE?", BoardTiles: []game.DraftTile{{Row: 1, Col: 1, Letter: "Q"}}}
|
|
if err := svc.SaveDraft(ctx, gameID, seats[0], d0); err != nil {
|
|
t.Fatalf("save draft 0: %v", err)
|
|
}
|
|
if got, err := svc.GetDraft(ctx, gameID, seats[0]); err != nil ||
|
|
got.RackOrder != "QANIWE?" || len(got.BoardTiles) != 1 || got.BoardTiles[0].Letter != "Q" {
|
|
t.Fatalf("get draft 0 = %+v (err %v)", got, err)
|
|
}
|
|
|
|
// Seat 1 drafts a board tile on a cell the opening play will commit.
|
|
overlap := hint.Tiles[0]
|
|
if err := svc.SaveDraft(ctx, gameID, seats[1], game.Draft{
|
|
RackOrder: "ABCDEFG",
|
|
BoardTiles: []game.DraftTile{{Row: overlap.Row, Col: overlap.Col, Letter: "X"}},
|
|
}); err != nil {
|
|
t.Fatalf("save draft 1: %v", err)
|
|
}
|
|
|
|
if _, err := svc.SubmitPlay(ctx, gameID, seats[0], hint.Tiles); err != nil {
|
|
t.Fatalf("seat0 play: %v", err)
|
|
}
|
|
|
|
// Seat 0's own draft is cleared by their move.
|
|
if d, _ := svc.GetDraft(ctx, gameID, seats[0]); d.RackOrder != "" || len(d.BoardTiles) != 0 {
|
|
t.Errorf("actor draft not cleared: %+v", d)
|
|
}
|
|
// Seat 1's board draft overlapped the play and is reset; the rack order is kept.
|
|
if d, _ := svc.GetDraft(ctx, gameID, seats[1]); len(d.BoardTiles) != 0 || d.RackOrder != "ABCDEFG" {
|
|
t.Errorf("conflicting draft not reset (or rack order lost): %+v", d)
|
|
}
|
|
}
|
|
|
|
// TestDraftSurvivesNonConflictingMove checks an opponent's board draft is kept when a
|
|
// committed play does not touch any of its cells.
|
|
func TestDraftSurvivesNonConflictingMove(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc, gameID, seats, hint := newDraftGame(t)
|
|
|
|
// Seat 1 drafts a far corner tile the central opening play cannot reach.
|
|
if err := svc.SaveDraft(ctx, gameID, seats[1], game.Draft{
|
|
BoardTiles: []game.DraftTile{{Row: 0, Col: 0, Letter: "Z"}},
|
|
}); err != nil {
|
|
t.Fatalf("save draft 1: %v", err)
|
|
}
|
|
if _, err := svc.SubmitPlay(ctx, gameID, seats[0], hint.Tiles); err != nil {
|
|
t.Fatalf("seat0 play: %v", err)
|
|
}
|
|
if d, _ := svc.GetDraft(ctx, gameID, seats[1]); len(d.BoardTiles) != 1 || d.BoardTiles[0].Letter != "Z" {
|
|
t.Errorf("non-conflicting draft should survive: %+v", d)
|
|
}
|
|
}
|
|
|
|
// TestSaveDraftRejectsOutsider checks only a seated player may save a draft.
|
|
func TestSaveDraftRejectsOutsider(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc, gameID, _, _ := newDraftGame(t)
|
|
if err := svc.SaveDraft(ctx, gameID, provisionAccount(t), game.Draft{RackOrder: "X"}); err == nil {
|
|
t.Fatal("outsider SaveDraft should fail")
|
|
}
|
|
}
|