package generator import ( "slices" "termdoku/internal/solver" "time" ) // Puzzle returns a copy of the grid suitable for rendering: 0 means blank. func (g Grid) Puzzle() [9][9]uint8 { return g } // Clone creates a deep copy of the grid. func (g Grid) Clone() Grid { var clone Grid for r := range 9 { for c := range 9 { clone[r][c] = g[r][c] } } return clone } // IsValid checks if the current grid state is valid (no conflicts). func (g Grid) IsValid() bool { for r := range 9 { seen := make(map[uint8]bool) for c := range 9 { v := g[r][c] if v == 0 { continue } if seen[v] { return false } seen[v] = true } } // Check columns for c := range 9 { seen := make(map[uint8]bool) for r := range 9 { v := g[r][c] if v == 0 { continue } if seen[v] { return false } seen[v] = true } } // Check 3x3 blocks for br := range 3 { for bc := range 3 { seen := make(map[uint8]bool) for r := br * 3; r < br*3+3; r++ { for c := bc * 3; c < bc*3+3; c++ { v := g[r][c] if v == 0 { continue } if seen[v] { return false } seen[v] = true } } } } return true } // IsSolved checks if the grid is completely filled and valid. func (g Grid) IsSolved() bool { if !g.IsValid() { return false } for r := range 9 { for c := range 9 { if g[r][c] == 0 { return false } } } return true } // CountFilledCells returns the number of non-zero cells. func (g Grid) CountFilledCells() int { count := 0 for r := range 9 { for c := range 9 { if g[r][c] != 0 { count++ } } } return count } // GetCandidates returns possible values for a given cell. func (g Grid) GetCandidates(row, col int) []uint8 { if g[row][col] != 0 { return nil } used := make(map[uint8]bool) for c := range 9 { if g[row][c] != 0 { used[g[row][c]] = true } } for r := range 9 { if g[r][col] != 0 { used[g[r][col]] = true } } // Check 3x3 block r0 := (row / 3) * 3 c0 := (col / 3) * 3 for r := r0; r < r0+3; r++ { for c := c0; c < c0+3; c++ { if g[r][c] != 0 { used[g[r][c]] = true } } } var candidates []uint8 for v := uint8(1); v <= 9; v++ { if !used[v] { candidates = append(candidates, v) } } return candidates } // Hint represents a hint for the player. type Hint struct { Row int Col int Value uint8 Type HintType } // HintType categorizes different hint strategies. type HintType int const ( HintNakedSingle HintType = iota // Only one candidate for a cell HintHiddenSingle // Only cell in row/col/box for a value HintRandom // Random valid cell (fallback) ) // GenerateHint provides a hint for the puzzle based on solving techniques. func (g Grid) GenerateHint(solution Grid) *Hint { // First, try to find a naked single (cell with only one candidate) for r := range 9 { for c := range 9 { if g[r][c] == 0 { candidates := g.GetCandidates(r, c) if len(candidates) == 1 { return &Hint{ Row: r, Col: c, Value: candidates[0], Type: HintNakedSingle, } } } } } // Try to find a hidden single if hint := g.findHiddenSingle(); hint != nil { return hint } // Fallback: return a random empty cell with its solution for r := range 9 { for c := range 9 { if g[r][c] == 0 { return &Hint{ Row: r, Col: c, Value: solution[r][c], Type: HintRandom, } } } } return nil } // findHiddenSingle finds a cell where a value can only go in one place in a row/col/box. func (g Grid) findHiddenSingle() *Hint { // Check rows for r := range 9 { for v := uint8(1); v <= 9; v++ { var possibleCols []int for c := range 9 { if g[r][c] == 0 { candidates := g.GetCandidates(r, c) if slices.Contains(candidates, v) { possibleCols = append(possibleCols, c) } } } if len(possibleCols) == 1 { return &Hint{ Row: r, Col: possibleCols[0], Value: v, Type: HintHiddenSingle, } } } } // Check columns for c := range 9 { for v := uint8(1); v <= 9; v++ { var possibleRows []int for r := range 9 { if g[r][c] == 0 { candidates := g.GetCandidates(r, c) for _, cand := range candidates { if cand == v { possibleRows = append(possibleRows, r) break } } } } if len(possibleRows) == 1 { return &Hint{ Row: possibleRows[0], Col: c, Value: v, Type: HintHiddenSingle, } } } } // Check 3x3 blocks for br := range 3 { for bc := range 3 { for v := uint8(1); v <= 9; v++ { type pos struct{ r, c int } var possiblePos []pos for r := br * 3; r < br*3+3; r++ { for c := bc * 3; c < bc*3+3; c++ { if g[r][c] == 0 { candidates := g.GetCandidates(r, c) for _, cand := range candidates { if cand == v { possiblePos = append(possiblePos, pos{r, c}) break } } } } } if len(possiblePos) == 1 { return &Hint{ Row: possiblePos[0].r, Col: possiblePos[0].c, Value: v, Type: HintHiddenSingle, } } } } } return nil } // PuzzleAnalysis contains metrics about puzzle difficulty and characteristics. type PuzzleAnalysis struct { FilledCells int EmptyCells int MinCandidates int MaxCandidates int AvgCandidates float64 HasUniqueSolution bool EstimatedDifficulty Difficulty SolvingTechniques []string SymmetryType SymmetryType } // SymmetryType represents the symmetry pattern of the puzzle. type SymmetryType int const ( SymmetryNone SymmetryType = iota SymmetryRotational180 SymmetryRotational90 SymmetryVertical SymmetryHorizontal SymmetryDiagonal ) func (s SymmetryType) String() string { switch s { case SymmetryRotational180: return "180° Rotational" case SymmetryRotational90: return "90° Rotational" case SymmetryVertical: return "Vertical" case SymmetryHorizontal: return "Horizontal" case SymmetryDiagonal: return "Diagonal" default: return "None" } } // Analyze performs a comprehensive analysis of the puzzle. func (g Grid) Analyze() PuzzleAnalysis { analysis := PuzzleAnalysis{ FilledCells: g.CountFilledCells(), EmptyCells: 81 - g.CountFilledCells(), } // Analyze candidates var totalCandidates int var emptyCellCount int analysis.MinCandidates = 9 analysis.MaxCandidates = 0 for r := range 9 { for c := range 9 { if g[r][c] == 0 { emptyCellCount++ candidates := g.GetCandidates(r, c) count := len(candidates) totalCandidates += count if count < analysis.MinCandidates { analysis.MinCandidates = count } if count > analysis.MaxCandidates { analysis.MaxCandidates = count } } } } if emptyCellCount > 0 { analysis.AvgCandidates = float64(totalCandidates) / float64(emptyCellCount) } solverGrid := convertToSolverGrid(g) analysis.HasUniqueSolution = solver.CountSolutions(solverGrid, 100*time.Millisecond, 2) == 1 analysis.SolvingTechniques = g.detectSolvingTechniques() analysis.EstimatedDifficulty = g.estimateDifficulty(analysis) analysis.SymmetryType = g.detectSymmetry() return analysis } // detectSolvingTechniques identifies which solving techniques are needed. func (g Grid) detectSolvingTechniques() []string { var techniques []string hasNakedSingle := false for r := range 9 { for c := range 9 { if g[r][c] == 0 { if len(g.GetCandidates(r, c)) == 1 { hasNakedSingle = true break } } } } if hasNakedSingle { techniques = append(techniques, "Naked Singles") } if g.findHiddenSingle() != nil { techniques = append(techniques, "Hidden Singles") } if len(techniques) == 0 { techniques = append(techniques, "Advanced Techniques Required") } return techniques } // estimateDifficulty estimates puzzle difficulty based on analysis. func (g Grid) estimateDifficulty(analysis PuzzleAnalysis) Difficulty { // Use multiple factors to estimate difficulty emptyCells := analysis.EmptyCells avgCandidates := analysis.AvgCandidates minCandidates := analysis.MinCandidates // More empty cells generally means harder if emptyCells >= 58 { return Lunatic } else if emptyCells >= 52 { // Check if it requires advanced techniques if minCandidates <= 2 || avgCandidates < 3.5 { return Lunatic } return Hard } else if emptyCells >= 46 { if minCandidates <= 2 { return Hard } return Normal } else if emptyCells >= 38 { return Normal } return Easy } // detectSymmetry detects the symmetry pattern of empty cells. func (g Grid) detectSymmetry() SymmetryType { if g.hasRotational180Symmetry() { return SymmetryRotational180 } if g.hasVerticalSymmetry() { return SymmetryVertical } if g.hasHorizontalSymmetry() { return SymmetryHorizontal } return SymmetryNone } func (g Grid) hasRotational180Symmetry() bool { for r := range 9 { for c := range 9 { // Check if empty cells are symmetric if (g[r][c] == 0) != (g[8-r][8-c] == 0) { return false } } } return true } func (g Grid) hasVerticalSymmetry() bool { for r := range 9 { for c := range 9 { if (g[r][c] == 0) != (g[r][8-c] == 0) { return false } } } return true } func (g Grid) hasHorizontalSymmetry() bool { for r := range 9 { for c := range 9 { if (g[r][c] == 0) != (g[8-r][c] == 0) { return false } } } return true }