1package nonogram23import (4 "context"5 "reflect"6 "testing"78 "github.com/FelineStateMachine/puzzletea/difficulty"9)1011func TestSpawnEloRejectsInvalidElo(t *testing.T) {12 mode := NewMode("Elo", "test", 5, 5, 0.5)1314 for _, elo := range []difficulty.Elo{difficulty.MinElo - 1, difficulty.SoftCapElo + 1} {15 gamer, report, err := mode.SpawnElo("seed", elo)16 if err == nil {17 t.Fatalf("SpawnElo(%d) error = nil, want invalid Elo error", elo)18 }19 if gamer != nil {20 t.Fatalf("SpawnElo(%d) gamer = %#v, want nil", elo, gamer)21 }22 if !reflect.DeepEqual(report, difficulty.Report{}) {23 t.Fatalf("SpawnElo(%d) report = %#v, want zero report", elo, report)24 }25 }26}2728func TestSpawnEloContextCanceled(t *testing.T) {29 mode := NewMode("Elo", "test", 5, 5, 0.5)30 ctx, cancel := context.WithCancel(context.Background())31 cancel()3233 gamer, report, err := mode.SpawnEloContext(ctx, "seed", 1200)34 if err == nil {35 t.Fatal("SpawnEloContext returned nil error for canceled context")36 }37 if gamer != nil {38 t.Fatalf("gamer = %#v, want nil", gamer)39 }40 if !reflect.DeepEqual(report, difficulty.Report{}) {41 t.Fatalf("report = %#v, want zero report", report)42 }43}4445func TestSpawnEloDeterministicForSameSeedAndElo(t *testing.T) {46 mode := NewMode("Elo", "test", 5, 5, 0.5)47 const target = difficulty.Elo(1200)4849 gameA, reportA, err := mode.SpawnElo("same-seed", target)50 if err != nil {51 t.Fatalf("first SpawnElo returned error: %v", err)52 }53 gameB, reportB, err := mode.SpawnElo("same-seed", target)54 if err != nil {55 t.Fatalf("second SpawnElo returned error: %v", err)56 }5758 saveA, err := gameA.GetSave()59 if err != nil {60 t.Fatalf("first GetSave returned error: %v", err)61 }62 saveB, err := gameB.GetSave()63 if err != nil {64 t.Fatalf("second GetSave returned error: %v", err)65 }6667 if string(saveA) != string(saveB) {68 t.Fatalf("SpawnElo saves differ for same seed and Elo:\n%s\n%s", saveA, saveB)69 }70 if !reflect.DeepEqual(reportA, reportB) {71 t.Fatalf("SpawnElo reports differ for same seed and Elo:\n%#v\n%#v", reportA, reportB)72 }73}7475func TestSpawnEloPopulatesDifficultyReport(t *testing.T) {76 mode := NewMode("Elo", "test", 5, 5, 0.5)77 const target = difficulty.Elo(1800)7879 gamer, report, err := mode.SpawnElo("report-fields", target)80 if err != nil {81 t.Fatalf("SpawnElo returned error: %v", err)82 }83 if gamer == nil {84 t.Fatal("SpawnElo returned nil gamer")85 }86 if report.TargetElo != target {87 t.Fatalf("TargetElo = %d, want %d", report.TargetElo, target)88 }89 if err := difficulty.ValidateElo(report.ActualElo); err != nil {90 t.Fatalf("ActualElo = %d is invalid: %v", report.ActualElo, err)91 }92 if report.Confidence != difficulty.ConfidenceMedium {93 t.Fatalf("Confidence = %q, want %q", report.Confidence, difficulty.ConfidenceMedium)94 }9596 requiredMetrics := map[string]float64{97 "width": 1,98 "height": 1,99 "cells": 1,100 "density": 0,101 "target_density": 0,102 "total_runs": 1,103 "line_possibilities": 1,104 "avg_line_possibilities": 1,105 "max_line_possibilities": 1,106 }107 for key, min := range requiredMetrics {108 got, ok := report.Metrics[key]109 if !ok {110 t.Fatalf("Metrics[%q] missing from %#v", key, report.Metrics)111 }112 if got < min {113 t.Fatalf("Metrics[%q] = %v, want >= %v", key, got, min)114 }115 }116 if report.Metrics["density"] > 1 {117 t.Fatalf("density = %v, want <= 1", report.Metrics["density"])118 }119}120121func TestSpawnEloPreservesSelectedDimensions(t *testing.T) {122 mode := NewMode("Mini", "5x5 grid, ~65% filled. Quick puzzle, straightforward hints.", 5, 5, 0.65)123 _, report, err := mode.SpawnElo("fixed-size", 2600)124 if err != nil {125 t.Fatalf("SpawnElo returned error: %v", err)126 }127 if got, want := report.Metrics["width"], 5.0; got != want {128 t.Fatalf("width metric = %.0f, want %.0f", got, want)129 }130 if got, want := report.Metrics["height"], 5.0; got != want {131 t.Fatalf("height metric = %.0f, want %.0f", got, want)132 }133 if got := report.Metrics["target_density"]; got == 0.65 {134 t.Fatalf("target_density = %.2f, want Elo-tuned density", got)135 }136}