package generator import ( "errors" "termdoku/internal/solver" "time" ) // Difficulty represents puzzle difficulty tiers. type Difficulty int const ( Easy Difficulty = iota Normal Hard Expert Lunatic ) // String returns the string representation of the difficulty. func (d Difficulty) String() string { switch d { case Easy: return "Easy" case Normal: return "Normal" case Hard: return "Hard" case Expert: return "Expert" case Lunatic: return "Lunatic" default: return "Unknown" } } // DailySeed returns a stable seed based on UTC date (YYYY-MM-DD). func DailySeed(t time.Time) string { utc := t.UTC() return utc.Format("2006-01-02") } // Params controls generation knobs derived from difficulty. type Params struct { // number of blanks/removed cells; higher -> harder RemovedCells int // backtracking timeout to avoid worst-cases Timeout time.Duration // symmetry pattern to use (optional) Symmetry SymmetryType // whether to enforce symmetry UseSymmetry bool } // paramsFor maps Difficulty to generation parameters. func paramsFor(d Difficulty) Params { switch d { case Easy: return Params{ RemovedCells: 38, Timeout: 150 * time.Millisecond, UseSymmetry: false, } case Normal: return Params{ RemovedCells: 46, Timeout: 150 * time.Millisecond, UseSymmetry: false, } case Hard: return Params{ RemovedCells: 52, Timeout: 200 * time.Millisecond, UseSymmetry: false, } case Expert: return Params{ RemovedCells: 56, Timeout: 250 * time.Millisecond, UseSymmetry: false, } case Lunatic: return Params{ RemovedCells: 60, Timeout: 300 * time.Millisecond, UseSymmetry: false, } default: return Params{ RemovedCells: 46, Timeout: 150 * time.Millisecond, UseSymmetry: false, } } } // Grid is a 9x9 Sudoku grid. 0 represents empty. type Grid [9][9]uint8 // ErrTimeout is returned when generation exceeds the configured timeout. var ErrTimeout = errors.New("generation timed out") // Generate creates a Sudoku puzzle with the given difficulty and seed. // - If seed is empty, uses current time for randomness. // - For Daily mode, pass seed from DailySeed(date). // Returns a puzzle grid with 0 as blanks, aimed at single-solution. func Generate(d Difficulty, seed string) (Grid, error) { p := paramsFor(d) return generateWithParams(p, seed) } // GenerateDaily creates a daily puzzle based on UTC date. func GenerateDaily(date time.Time) (Grid, error) { return Generate(Normal, DailySeed(date)) } // GenerateWithSymmetry creates a puzzle with a specific symmetry pattern. func GenerateWithSymmetry(d Difficulty, seed string, symmetry SymmetryType) (Grid, error) { p := paramsFor(d) p.UseSymmetry = true p.Symmetry = symmetry return generateWithParams(p, seed) } // GenerateCustom creates a puzzle with custom parameters. func GenerateCustom(removedCells int, seed string, useSymmetry bool, symmetry SymmetryType) (Grid, error) { p := Params{ RemovedCells: removedCells, Timeout: 300 * time.Millisecond, UseSymmetry: useSymmetry, Symmetry: symmetry, } return generateWithParams(p, seed) } // PuzzleWithSolution represents a puzzle along with its solution. type PuzzleWithSolution struct { Puzzle Grid Solution Grid Analysis PuzzleAnalysis } // GenerateWithAnalysis creates a puzzle and returns it with its solution and analysis. func GenerateWithAnalysis(d Difficulty, seed string) (PuzzleWithSolution, error) { puzzle, err := Generate(d, seed) if err != nil { return PuzzleWithSolution{}, err } // Solve to get the solution solution := puzzle.Clone() solverGrid := convertToSolverGrid(solution) if !solver.Solve(&solverGrid, 500*time.Millisecond) { return PuzzleWithSolution{}, errors.New("failed to solve generated puzzle") } // Convert back to Grid for r := 0; r < 9; r++ { for c := 0; c < 9; c++ { solution[r][c] = solverGrid[r][c] } } // Analyze the puzzle analysis := puzzle.Analyze() return PuzzleWithSolution{ Puzzle: puzzle, Solution: solution, Analysis: analysis, }, nil } // RatePuzzle provides a difficulty rating from 0-100 based on puzzle characteristics. func RatePuzzle(g Grid) int { analysis := g.Analyze() // Base score from empty cells (0-40 points) emptyScore := (analysis.EmptyCells * 40) / 81 // Candidate complexity (0-30 points) candidateScore := 0 if analysis.AvgCandidates > 0 { // Lower average candidates = harder candidateScore = int((9.0 - analysis.AvgCandidates) * 3.3) if candidateScore < 0 { candidateScore = 0 } if candidateScore > 30 { candidateScore = 30 } } // Technique complexity (0-30 points) techniqueScore := 0 for _, tech := range analysis.SolvingTechniques { if tech == "Advanced Techniques Required" { techniqueScore = 30 break } else if tech == "Hidden Singles" { techniqueScore = 15 } else if tech == "Naked Singles" { techniqueScore = 5 } } totalScore := emptyScore + candidateScore + techniqueScore if totalScore > 100 { totalScore = 100 } return totalScore } // DifficultyFromRating converts a rating (0-100) to a Difficulty level. func DifficultyFromRating(rating int) Difficulty { if rating >= 85 { return Lunatic } else if rating >= 70 { return Expert } else if rating >= 50 { return Hard } else if rating >= 30 { return Normal } return Easy } // generateWithParams contains the core generation pipeline. func generateWithParams(p Params, seed string) (Grid, error) { // 1) Create a full valid solution via randomized backtracking full, err := randomizedFullSolution(seed, p.Timeout) if err != nil { return Grid{}, err } // 2) Remove cells according to difficulty while keeping uniqueness if possible var puzzle Grid if p.UseSymmetry { puzzle, err = carveCellsSymmetric(full, p.RemovedCells, seed, p.Timeout, p.Symmetry) } else { puzzle, err = carveCellsUnique(full, p.RemovedCells, seed, p.Timeout) } if err != nil { return Grid{}, err } return puzzle, nil }