clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/elo-difficulty
puzzletea / resolve / resolve_test.go
resolve_test.go 5.3K
1package resolve23import (4 "strings"5 "testing"67 "github.com/FelineStateMachine/puzzletea/game"8 "github.com/FelineStateMachine/puzzletea/games/lightsout"9 "github.com/FelineStateMachine/puzzletea/puzzle"10 "github.com/FelineStateMachine/puzzletea/registry"11)1213var testEntries = registry.Entries()1415func TestNormalize(t *testing.T) {16 tests := []struct {17 name string18 input string19 want string20 }{21 {"lowercase", "Sudoku", "sudoku"},22 {"all caps", "SUDOKU", "sudoku"},23 {"hyphens to spaces", "lights-out", "lights out"},24 {"underscores to spaces", "word_search", "word search"},25 }2627 for _, tt := range tests {28 t.Run(tt.name, func(t *testing.T) {29 if got := Normalize(tt.input); got != tt.want {30 t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)31 }32 })33 }34}3536func TestModeEntry(t *testing.T) {37 sudokuEntry, ok := registry.Resolve("Sudoku")38 if !ok {39 t.Fatal("missing sudoku entry")40 }4142 t.Run("empty name returns default (first) mode", func(t *testing.T) {43 mode, err := ModeEntry(sudokuEntry, "")44 if err != nil {45 t.Fatal(err)46 }47 if mode.Spawner == nil || mode.Definition.Title != "Beginner" {48 t.Fatalf("default mode = (%v, %q), want Beginner", mode.Spawner != nil, mode.Definition.Title)49 }50 })5152 t.Run("case insensitive match", func(t *testing.T) {53 mode, err := ModeEntry(sudokuEntry, "easy")54 if err != nil {55 t.Fatal(err)56 }57 if mode.Definition.Title != "Easy" {58 t.Fatalf("title = %q, want Easy", mode.Definition.Title)59 }60 })6162 t.Run("unknown mode", func(t *testing.T) {63 _, err := ModeEntry(sudokuEntry, "impossible")64 if err == nil || !strings.Contains(err.Error(), "unknown mode") {65 t.Fatalf("error = %v, want unknown mode", err)66 }67 })6869 t.Run("empty modes list", func(t *testing.T) {70 emptyEntry := registry.Entry{71 Definition: puzzle.Definition{Name: "Empty"},72 }73 _, err := ModeEntry(emptyEntry, "any")74 if err == nil || !strings.Contains(err.Error(), "no available modes") {75 t.Fatalf("error = %v, want no available modes", err)76 }77 })78}7980func TestModeNames(t *testing.T) {81 entry, ok := registry.Resolve("Lights Out")82 if !ok {83 t.Fatal("missing lights out entry")84 }85 names := ModeNames(entry)86 expected := []string{"Easy", "Medium", "Hard", "Extreme"}87 if len(names) != len(expected) {88 t.Fatalf("len(ModeNames) = %d, want %d", len(names), len(expected))89 }90 for i, want := range expected {91 if names[i] != want {92 t.Fatalf("mode[%d] = %q, want %q", i, names[i], want)93 }94 }95}9697func TestVariantEntryResolvesVariantsAndLegacyAliases(t *testing.T) {98 entry, ok := registry.Resolve("Sudoku")99 if !ok {100 t.Fatal("missing sudoku entry")101 }102103 variant, err := VariantEntry(entry, "sudoku")104 if err != nil {105 t.Fatal(err)106 }107 if variant.Variant.Definition.Title != "Sudoku" || variant.ExplicitElo != nil {108 t.Fatalf("variant selection = %#v, want Sudoku without explicit Elo", variant)109 }110111 legacy, err := VariantEntry(entry, "easy")112 if err != nil {113 t.Fatal(err)114 }115 if legacy.Variant.Definition.Title != "Sudoku" {116 t.Fatalf("legacy variant = %q, want Sudoku", legacy.Variant.Definition.Title)117 }118 if legacy.ExplicitElo == nil || *legacy.ExplicitElo != 600 {119 t.Fatalf("legacy explicit Elo = %v, want 600", legacy.ExplicitElo)120 }121}122123func TestRNGFromString(t *testing.T) {124 rng1 := RNGFromString("hello-world")125 rng2 := RNGFromString("hello-world")126 for i := range 10 {127 if v1, v2 := rng1.Uint64(), rng2.Uint64(); v1 != v2 {128 t.Fatalf("draw %d: got %d and %d for same seed", i, v1, v2)129 }130 }131}132133func TestSeededMode(t *testing.T) {134 s1, gt1, m1, err := SeededMode("test-seed-42", testEntries)135 if err != nil {136 t.Fatal(err)137 }138 s2, gt2, m2, err := SeededMode("test-seed-42", testEntries)139 if err != nil {140 t.Fatal(err)141 }142 if s1 == nil || s2 == nil || gt1 != gt2 || m1 != m2 {143 t.Fatalf("seeded mode mismatch: (%v,%q,%q) vs (%v,%q,%q)", s1 != nil, gt1, m1, s2 != nil, gt2, m2)144 }145146 _, _, _, err = SeededMode("test", nil)147 if err == nil {148 t.Fatal("expected error for empty entries")149 }150151 emptyEntries := []registry.Entry{{152 Definition: puzzle.Definition{Name: "Empty"},153 }}154 _, _, _, err = SeededMode("test", emptyEntries)155 if err == nil {156 t.Fatal("expected error when no seeded modes available")157 }158}159160func TestSeededModeStableOnEntryChange(t *testing.T) {161 seeds := []string{"alpha", "bravo", "charlie", "delta"}162 type selection struct{ gameType, mode string }163 original := make([]selection, len(seeds))164 for i, seed := range seeds {165 _, gt, m, err := SeededMode(seed, testEntries)166 if err != nil {167 t.Fatal(err)168 }169 original[i] = selection{gt, m}170 }171172 extended := append([]registry.Entry(nil), testEntries...)173 extended = append(extended, registry.Entry{174 Definition: puzzle.Definition{175 ID: puzzle.CanonicalGameID("Synthetic"),176 Name: "Synthetic",177 Variants: []puzzle.VariantDef{{ID: puzzle.CanonicalVariantID("Synthetic"), Title: "Synthetic"}},178 },179 Variants: []registry.VariantEntry{{180 Definition: puzzle.VariantDef{ID: puzzle.CanonicalVariantID("Synthetic"), Title: "Synthetic"},181 Seeded: lightsout.Modes[0].(game.SeededSpawner),182 }},183 })184185 for i, seed := range seeds {186 _, gt, m, err := SeededMode(seed, extended)187 if err != nil {188 t.Fatal(err)189 }190 if gt == "Synthetic" {191 continue192 }193 if gt != original[i].gameType || m != original[i].mode {194 t.Fatalf("seed %q changed from (%q,%q) to (%q,%q)", seed, original[i].gameType, original[i].mode, gt, m)195 }196 }197}