1package sudokurgb23import (4 "crypto/sha256"5 "encoding/binary"6 "math"7 "math/rand/v2"8 "strconv"910 "github.com/FelineStateMachine/puzzletea/difficulty"11 "github.com/FelineStateMachine/puzzletea/game"12)1314var _ game.EloSpawner = SudokuRGBMode{}1516type eloSolveStats struct {17 Nodes int18 Branches int19 MaxDepth int20}2122func (s SudokuRGBMode) SpawnElo(seed string, elo difficulty.Elo) (game.Gamer, difficulty.Report, error) {23 if err := difficulty.ValidateElo(elo); err != nil {24 return nil, difficulty.Report{}, err25 }2627 mode := sudokuRGBModeForElo(s, elo)28 var bestProvided []cell29 var bestReport difficulty.Report30 haveBest := false31 for candidate := range difficulty.CandidateCount(elo) {32 provided := GenerateProvidedCellsSeeded(mode, sudokuRGBEloRNG(sudokuRGBCandidateSeed(seed, candidate), elo))33 report := scoreSudokuRGBElo(elo, provided)34 if difficulty.BetterCandidate(report, bestReport, elo, haveBest) {35 bestProvided = provided36 bestReport = report37 haveBest = true38 }39 }40 g, err := New(mode, bestProvided)41 if err != nil {42 return nil, difficulty.Report{}, err43 }44 return g, bestReport, nil45}4647func sudokuRGBModeForElo(base SudokuRGBMode, elo difficulty.Elo) SudokuRGBMode {48 score := difficulty.Score01(elo)49 provided := 60 - int(math.Round(score*36))50 if provided < 24 {51 provided = 2452 }53 mode := base54 mode.ProvidedCount = provided55 return mode56}5758func sudokuRGBEloRNG(seed string, elo difficulty.Elo) *rand.Rand {59 sum := sha256.Sum256([]byte("sudokurgb\x00" + seed + "\x00" + strconv.Itoa(int(elo))))60 return rand.New(rand.NewPCG(61 binary.LittleEndian.Uint64(sum[0:8]),62 binary.LittleEndian.Uint64(sum[8:16]),63 ))64}6566func sudokuRGBCandidateSeed(seed string, candidate int) string {67 if candidate == 0 {68 return seed69 }70 return seed + "\x00candidate:" + strconv.Itoa(candidate)71}7273func scoreSudokuRGBElo(target difficulty.Elo, provided []cell) difficulty.Report {74 g := newGrid(provided)75 solutions, stats := countSolutionsWithEloStats(&g, 2)76 metrics := difficulty.Metrics{77 "clue_count": float64(len(provided)),78 "unknown_count": float64(gridSize*gridSize - len(provided)),79 "solution_count": float64(solutions),80 "solver_nodes": float64(stats.Nodes),81 "branches": float64(stats.Branches),82 "max_depth": float64(stats.MaxDepth),83 }8485 actual := sudokuRGBActualElo(metrics)86 confidence := difficulty.ConfidenceHigh87 if solutions != 1 {88 confidence = difficulty.ConfidenceLow89 }9091 return difficulty.Report{92 TargetElo: target,93 ActualElo: actual,94 Confidence: confidence,95 Metrics: metrics,96 }97}9899func countSolutionsWithEloStats(g *grid, limit int) (int, eloSolveStats) {100 if limit <= 0 {101 return 0, eloSolveStats{}102 }103104 state, ok := buildQuotaState(g)105 if !ok {106 return 0, eloSolveStats{}107 }108109 var stats eloSolveStats110 count := countSolutionsRecWithEloStats(g, limit, &state, 0, &stats)111 return count, stats112}113114func countSolutionsRecWithEloStats(g *grid, limit int, state *quotaState, depth int, stats *eloSolveStats) int {115 if limit <= 0 {116 return 0117 }118119 stats.Nodes++120 if depth > stats.MaxDepth {121 stats.MaxDepth = depth122 }123124 x, y, candidates, found := findMostConstrainedEmptyCell(g, state)125 if !found {126 return 1127 }128 if candidates == 0 {129 return 0130 }131132 count := 0133 candidateCount := countCandidateBits(candidates)134 if candidateCount > 1 {135 stats.Branches++136 }137 for value := 1; value <= valueCount; value++ {138 if candidates&(1<<value) == 0 {139 continue140 }141 placeValue(g, state, value, x, y)142 count += countSolutionsRecWithEloStats(g, limit-count, state, depth+1, stats)143 clearValue(g, state, value, x, y)144 if count >= limit {145 return count146 }147 }148 return count149}150151func sudokuRGBActualElo(metrics difficulty.Metrics) difficulty.Elo {152 score := 0.55*normalizeSudokuRGBMetric(metrics["unknown_count"], 21, 57) +153 0.25*normalizeSudokuRGBMetric(math.Log10(metrics["solver_nodes"]+1), 1.2, 4.0) +154 0.15*normalizeSudokuRGBMetric(metrics["branches"], 0, 120) +155 0.05*normalizeSudokuRGBMetric(metrics["max_depth"], 0, 51)156 return difficulty.ClampElo(difficulty.Elo(math.Round(score * float64(difficulty.SoftCapElo))))157}158159func normalizeSudokuRGBMetric(value, low, high float64) float64 {160 if value <= low {161 return 0162 }163 if value >= high {164 return 1165 }166 return (value - low) / (high - low)167}