clone

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

feat/elo-difficulty
puzzletea / gameentry / gameentry_test.go
gameentry_test.go 4.5K
1package gameentry23import (4 "context"5 "errors"6 "math/rand/v2"7 "testing"89 "github.com/FelineStateMachine/puzzletea/difficulty"10 "github.com/FelineStateMachine/puzzletea/game"11 "github.com/FelineStateMachine/puzzletea/puzzle"12)1314type stubMode struct {15 game.BaseMode16}1718func (m stubMode) Spawn() (game.Gamer, error) {19 return nil, errors.New("not implemented")20}2122type seededStubMode struct {23 stubMode24}2526func (m seededStubMode) SpawnSeeded(rng *rand.Rand) (game.Gamer, error) {27 return nil, errors.New("not implemented")28}2930type cancellableEloStubMode struct {31 stubMode32 calledWithCanceledContext bool33}3435func (m *cancellableEloStubMode) SpawnElo(seed string, elo difficulty.Elo) (game.Gamer, difficulty.Report, error) {36 return nil, difficulty.Report{}, errors.New("non-context spawn called")37}3839func (m *cancellableEloStubMode) SpawnEloContext(ctx context.Context, seed string, elo difficulty.Elo) (game.Gamer, difficulty.Report, error) {40 if err := ctx.Err(); err != nil {41 m.calledWithCanceledContext = true42 return nil, difficulty.Report{}, err43 }44 return nil, difficulty.Report{}, nil45}4647func TestBuildModeDefsMarksSeededModes(t *testing.T) {48 modes := []game.Mode{49 stubMode{BaseMode: game.NewBaseMode("Beginner", "Easy board")},50 seededStubMode{stubMode{BaseMode: game.NewBaseMode("Expert", "Hard board")}},51 }5253 defs := BuildModeDefs(modes)54 if len(defs) != 2 {55 t.Fatalf("len(BuildModeDefs) = %d, want 2", len(defs))56 }57 if defs[0].Seeded {58 t.Fatal("defs[0].Seeded = true, want false")59 }60 if !defs[1].Seeded {61 t.Fatal("defs[1].Seeded = false, want true")62 }63}6465func TestNewEntryKeepsMetadataAndRuntimeModesAligned(t *testing.T) {66 modes := []game.Mode{67 seededStubMode{stubMode{BaseMode: game.NewBaseMode("Medium", "Balanced board")}},68 }69 definition := puzzle.NewDefinition(puzzle.DefinitionSpec{70 Name: "Sudoku",71 Modes: BuildModeDefs(modes),72 DailyModeIDs: []puzzle.ModeID{"medium"},73 })7475 entry := NewEntry(EntrySpec{76 Definition: definition,77 Help: "Rules",78 Import: func([]byte) (game.Gamer, error) { return nil, nil },79 Modes: modes,80 })8182 if got, want := entry.Definition.Name, "Sudoku"; got != want {83 t.Fatalf("Definition.Name = %q, want %q", got, want)84 }85 if got, want := len(entry.Modes), 1; got != want {86 t.Fatalf("len(Modes) = %d, want %d", got, want)87 }88 if entry.Modes[0].Seeded == nil {89 t.Fatal("Modes[0].Seeded = nil, want seeded spawner")90 }91 if entry.Print != nil {92 t.Fatal("Print = non-nil, want nil")93 }94}9596func TestVariantEloSpawnerPreservesCancellation(t *testing.T) {97 preset := difficulty.Elo(1200)98 mode := &cancellableEloStubMode{99 stubMode: stubMode{BaseMode: game.NewBaseMode("Hard", "Hard board")},100 }101 entry := NewEntry(EntrySpec{102 Definition: puzzle.NewDefinition(puzzle.DefinitionSpec{103 Name: "Test",104 Modes: []puzzle.ModeDef{puzzle.NewModeDef(puzzle.ModeSpec{105 Title: mode.Title(),106 Description: mode.Description(),107 PresetElo: &preset,108 })},109 }),110 Modes: []game.Mode{mode},111 })112 if len(entry.Variants) != 1 {113 t.Fatalf("len(Variants) = %d, want 1", len(entry.Variants))114 }115 spawner, ok := entry.Variants[0].Elo.(game.CancellableEloSpawner)116 if !ok {117 t.Fatal("variant Elo spawner does not implement game.CancellableEloSpawner")118 }119120 ctx, cancel := context.WithCancel(t.Context())121 cancel()122 _, _, err := spawner.SpawnEloContext(ctx, "seed", preset)123 if !errors.Is(err, context.Canceled) {124 t.Fatalf("SpawnEloContext error = %v, want context.Canceled", err)125 }126 if !mode.calledWithCanceledContext {127 t.Fatal("underlying mode did not receive the canceled context")128 }129}130131func TestVariantSeededSpawnerPreservesCancellation(t *testing.T) {132 preset := difficulty.Elo(1200)133 mode := &cancellableEloStubMode{134 stubMode: stubMode{BaseMode: game.NewBaseMode("Hard", "Hard board")},135 }136 entry := NewEntry(EntrySpec{137 Definition: puzzle.NewDefinition(puzzle.DefinitionSpec{138 Name: "Test",139 Modes: []puzzle.ModeDef{puzzle.NewModeDef(puzzle.ModeSpec{140 Title: mode.Title(),141 Description: mode.Description(),142 PresetElo: &preset,143 })},144 }),145 Modes: []game.Mode{mode},146 })147 spawner, ok := entry.Variants[0].Seeded.(game.CancellableSeededSpawner)148 if !ok {149 t.Fatal("variant seeded spawner does not implement game.CancellableSeededSpawner")150 }151152 ctx, cancel := context.WithCancel(t.Context())153 cancel()154 _, err := spawner.SpawnSeededContext(ctx, rand.New(rand.NewPCG(1, 2)))155 if !errors.Is(err, context.Canceled) {156 t.Fatalf("SpawnSeededContext error = %v, want context.Canceled", err)157 }158 if !mode.calledWithCanceledContext {159 t.Fatal("underlying mode did not receive the canceled context")160 }161}