26aa154547
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
Squash the 12 goose migrations into one 00001_baseline.sql (there is no prod data; verified schema-identical to the chain via a pg_dump diff + the green integration suite) and rename the game-variant labels english/russian_scrabble/erudit -> scrabble_en/scrabble_ru/erudit_ru across the backend, the FlatBuffers wire values and the UI. dawg filenames and the Go enum identifiers are unchanged; the i18n display keys are kept. Adds PRERELEASE.md (the R1-R7 pre-release tracker), linked from CLAUDE.md. Contour DB wipe and the scrabble-dictionary tidy are follow-ups.
120 lines
4.1 KiB
Go
120 lines
4.1 KiB
Go
package robot
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
|
||
"github.com/google/uuid"
|
||
|
||
"scrabble/backend/internal/engine"
|
||
)
|
||
|
||
// TestComposeName covers the three rendering forms, including a Cyrillic initial.
|
||
func TestComposeName(t *testing.T) {
|
||
cases := []struct {
|
||
first, surname string
|
||
form int
|
||
want string
|
||
}{
|
||
{"Anna", "Carter", nameFormFirstOnly, "Anna"},
|
||
{"Anna", "Carter", nameFormInitial, "Anna C."},
|
||
{"Anna", "Carter", nameFormFull, "Anna Carter"},
|
||
{"Маша", "Суханова", nameFormInitial, "Маша С."},
|
||
{"Маша", "Суханова", nameFormFull, "Маша Суханова"},
|
||
}
|
||
for _, c := range cases {
|
||
if got := composeName(c.first, c.surname, c.form); got != c.want {
|
||
t.Errorf("composeName(%q,%q,%d) = %q, want %q", c.first, c.surname, c.form, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestNamePoolsPaired checks the full and colloquial first-name pools line up by
|
||
// index (so a slot's gender and person are consistent) and the surname forms differ.
|
||
func TestNamePoolsPaired(t *testing.T) {
|
||
if len(firstNamesFullEN) != robotPoolSize || len(firstNamesShortEN) != robotPoolSize {
|
||
t.Fatalf("EN first-name pools must each hold %d names", robotPoolSize)
|
||
}
|
||
if len(firstNamesFullRU) != robotPoolSize || len(firstNamesShortRU) != robotPoolSize {
|
||
t.Fatalf("RU first-name pools must each hold %d names", robotPoolSize)
|
||
}
|
||
for i := range firstNamesFullRU {
|
||
if firstNamesFullRU[i].female != firstNamesShortRU[i].female {
|
||
t.Errorf("RU pair %d disagrees on gender: %q vs %q", i, firstNamesFullRU[i].name, firstNamesShortRU[i].name)
|
||
}
|
||
}
|
||
for _, sp := range surnamesRU {
|
||
if sp.m == sp.f {
|
||
t.Errorf("RU surname forms should differ: %q", sp.m)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestRobotDisplayNames checks the generated pools are the right size, non-empty and
|
||
// deterministic — durable robot accounts must keep a stable name across restarts.
|
||
func TestRobotDisplayNames(t *testing.T) {
|
||
en1, en2 := robotDisplayNamesEN(), robotDisplayNamesEN()
|
||
ru1, ru2 := robotDisplayNamesRU(), robotDisplayNamesRU()
|
||
if len(en1) != robotPoolSize || len(ru1) != robotPoolSize {
|
||
t.Fatalf("pool sizes en=%d ru=%d, want %d", len(en1), len(ru1), robotPoolSize)
|
||
}
|
||
for i := range en1 {
|
||
if en1[i] != en2[i] || ru1[i] != ru2[i] {
|
||
t.Fatalf("pool not deterministic at %d: en %q/%q ru %q/%q", i, en1[i], en2[i], ru1[i], ru2[i])
|
||
}
|
||
if en1[i] == "" || ru1[i] == "" {
|
||
t.Fatalf("empty composed name at index %d", i)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestPickVariantRouting checks English games draw the Latin pool and Russian games
|
||
// draw mostly Russian names with a Latin minority.
|
||
func TestPickVariantRouting(t *testing.T) {
|
||
enID, ruID := uuid.New(), uuid.New()
|
||
s := &Service{poolEN: []uuid.UUID{enID}, poolRU: []uuid.UUID{ruID}}
|
||
for i := 0; i < 200; i++ {
|
||
if got, err := s.Pick(engine.VariantEnglish); err != nil || got != enID {
|
||
t.Fatalf("scrabble_en Pick = (%v, %v), want (%v, nil)", got, err, enID)
|
||
}
|
||
}
|
||
var en, ru int
|
||
for i := 0; i < 4000; i++ {
|
||
got, err := s.Pick(engine.VariantRussianScrabble)
|
||
if err != nil {
|
||
t.Fatalf("scrabble_ru Pick: %v", err)
|
||
}
|
||
switch got {
|
||
case enID:
|
||
en++
|
||
case ruID:
|
||
ru++
|
||
}
|
||
}
|
||
if ru <= en {
|
||
t.Errorf("scrabble_ru names should dominate a Russian game: ru=%d en=%d", ru, en)
|
||
}
|
||
if en == 0 {
|
||
t.Errorf("some Latin names should appear in Russian games (got 0 of 4000)")
|
||
}
|
||
// Эрудит routes like Russian Scrabble.
|
||
if _, err := s.Pick(engine.VariantErudit); err != nil {
|
||
t.Errorf("erudit_ru Pick: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestPickFallback checks an empty side falls back to the other pool and an empty pool
|
||
// errors.
|
||
func TestPickFallback(t *testing.T) {
|
||
id := uuid.New()
|
||
if got, err := (&Service{poolEN: []uuid.UUID{id}}).Pick(engine.VariantRussianScrabble); err != nil || got != id {
|
||
t.Errorf("scrabble_ru fallback to EN = (%v, %v), want (%v, nil)", got, err, id)
|
||
}
|
||
if got, err := (&Service{poolRU: []uuid.UUID{id}}).Pick(engine.VariantEnglish); err != nil || got != id {
|
||
t.Errorf("scrabble_en fallback to RU = (%v, %v), want (%v, nil)", got, err, id)
|
||
}
|
||
if _, err := (&Service{}).Pick(engine.VariantEnglish); !errors.Is(err, ErrNoRobotAvailable) {
|
||
t.Errorf("empty pool err = %v, want ErrNoRobotAvailable", err)
|
||
}
|
||
}
|