635f2fd9fc
- #3 nudge-on-own-turn: distinct result code nudge_own_turn + i18n (was reused 'not_your_turn') - #2 sanitize connector registration name to the editable format; Player/Игрок-XXXXX fallback - #5 variant-aware robot name pools (composed full/colloquial first + surname forms; ru gets <=20% latin) - #4 move-number-aware robot move timing (early 1-5min -> late 10-90min, skew k=4) - #7 emit move event to the actor too (multi-device sync); opponent_moved stays in-app only - #1 live game_move_duration{variant,phase} histogram + admin console per-user min/avg/max columns and an inline-SVG move-time-by-move-number chart (offline from the journal) - ProvisionRobot bypasses editor name validation (system names like 'Peter J.')
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("english 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("russian Pick: %v", err)
|
||
}
|
||
switch got {
|
||
case enID:
|
||
en++
|
||
case ruID:
|
||
ru++
|
||
}
|
||
}
|
||
if ru <= en {
|
||
t.Errorf("russian 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 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("russian 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("english 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)
|
||
}
|
||
}
|