package game import ( "testing" "github.com/google/uuid" "scrabble/backend/internal/engine" ) // scriptIntn returns a drawIntn that yields the scripted indices in order, failing // the test if the script is exhausted or an index is out of range. It lets a test // drive the first-move draw deterministically (English SetupBag order: 9 'a' at // 0..8, then 'b' …, blanks last). func scriptIntn(t *testing.T, seq ...int) drawIntn { t.Helper() i := 0 return func(n int) (int, error) { if i >= len(seq) { t.Fatalf("intn script exhausted (asked for [0,%d))", n) } v := seq[i] i++ if v < 0 || v >= n { t.Fatalf("intn script value %d out of range [0,%d)", v, n) } return v, nil } } func TestSeedFirstMoveDirectWinner(t *testing.T) { a, b := uuid.New(), uuid.New() // a draws bag[0]='a' (rank 0); b draws bag[8]='b' (rank 1) → a wins, no tie. res, err := seedFirstMove(engine.VariantEnglish, []uuid.UUID{a, b}, scriptIntn(t, 0, 8)) if err != nil { t.Fatalf("seedFirstMove: %v", err) } if res.winner != a { t.Fatalf("winner = %v, want %v", res.winner, a) } if got := res.order; len(got) != 2 || got[0] != a || got[1] != b { t.Fatalf("order = %v, want [a b]", got) } if len(res.draws) != 2 { t.Fatalf("draws = %d, want 2", len(res.draws)) } } func TestSeedFirstMoveBlankSupersedes(t *testing.T) { a, b := uuid.New(), uuid.New() // a draws 'a' (rank 0); after the draw the two blanks sit at 97,98 — b draws // bag[97], a blank, which beats every letter. res, err := seedFirstMove(engine.VariantEnglish, []uuid.UUID{a, b}, scriptIntn(t, 0, 97)) if err != nil { t.Fatalf("seedFirstMove: %v", err) } if res.winner != b { t.Fatalf("winner = %v, want %v (blank supersedes)", res.winner, b) } last := res.draws[len(res.draws)-1] if !last.Blank || last.Rank != engine.BlankRank || last.Letter != "?" { t.Fatalf("winning draw = %+v, want a blank (rank %d, '?')", last, engine.BlankRank) } } func TestSeedFirstMoveTieRedraw(t *testing.T) { a, b, c := uuid.New(), uuid.New(), uuid.New() // Round 1: a→bag[0]='a'(0), b→bag[0]='a'(0), c→bag[7]='b'(1) → a,b tie best. // Round 2 (a,b only): a→bag[0]='a'(0), b→bag[8]='b'(1) → a wins. res, err := seedFirstMove(engine.VariantEnglish, []uuid.UUID{a, b, c}, scriptIntn(t, 0, 0, 7, 0, 8)) if err != nil { t.Fatalf("seedFirstMove: %v", err) } if res.winner != a { t.Fatalf("winner = %v, want %v", res.winner, a) } if len(res.draws) != 5 { t.Fatalf("draws = %d, want 5 (3 in round 1, 2 in round 2)", len(res.draws)) } if res.draws[2].Round != 1 || res.draws[3].Round != 2 { t.Fatalf("round boundaries wrong: %+v", res.draws) } // The full table keeps every account's seating order, winner first. if got := res.order; got[0] != a || got[1] != b || got[2] != c { t.Fatalf("order = %v, want [a b c]", got) } } func TestSeedFirstMoveTooFewAccounts(t *testing.T) { if _, err := seedFirstMove(engine.VariantEnglish, []uuid.UUID{uuid.New()}, scriptIntn(t)); err == nil { t.Fatal("want error for a single account") } } func TestSeedFirstMovePerpetualTieCapped(t *testing.T) { a, b := uuid.New(), uuid.New() // Always drawing bag[0] gives both players an 'a' every round — a tie that never // resolves; the round cap must turn it into an error, not an infinite loop. alwaysZero := drawIntn(func(int) (int, error) { return 0, nil }) if _, err := seedFirstMove(engine.VariantEnglish, []uuid.UUID{a, b}, alwaysZero); err == nil { t.Fatal("want error when ties never resolve") } } func TestRotateToFirst(t *testing.T) { a, b, c, d := uuid.New(), uuid.New(), uuid.New(), uuid.New() tests := []struct { name string accounts []uuid.UUID winner uuid.UUID want []uuid.UUID }{ {"two winner second", []uuid.UUID{a, b}, b, []uuid.UUID{b, a}}, {"three winner first", []uuid.UUID{a, b, c}, a, []uuid.UUID{a, b, c}}, {"four winner third", []uuid.UUID{a, b, c, d}, c, []uuid.UUID{c, d, a, b}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := rotateToFirst(tt.accounts, tt.winner) if len(got) != len(tt.want) { t.Fatalf("len = %d, want %d", len(got), len(tt.want)) } for i := range got { if got[i] != tt.want[i] { t.Fatalf("rotate = %v, want %v", got, tt.want) } } }) } }