(Feat): Initial Commit, Termdoku
This commit is contained in:
484
internal/generator/api.go
Normal file
484
internal/generator/api.go
Normal file
@@ -0,0 +1,484 @@
|
||||
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
|
||||
}
|
||||
220
internal/generator/benchmark.go
Normal file
220
internal/generator/benchmark.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BenchmarkResult contains statistics from puzzle generation benchmarking.
|
||||
type BenchmarkResult struct {
|
||||
Difficulty Difficulty
|
||||
TotalAttempts int
|
||||
SuccessfulPuzzles int
|
||||
FailedPuzzles int
|
||||
AverageTime time.Duration
|
||||
MinTime time.Duration
|
||||
MaxTime time.Duration
|
||||
AverageRating float64
|
||||
Ratings []int
|
||||
}
|
||||
|
||||
// BenchmarkGeneration tests puzzle generation performance for a given difficulty.
|
||||
func BenchmarkGeneration(d Difficulty, attempts int) BenchmarkResult {
|
||||
result := BenchmarkResult{
|
||||
Difficulty: d,
|
||||
TotalAttempts: attempts,
|
||||
MinTime: time.Hour, // Start with a large value
|
||||
Ratings: make([]int, 0, attempts),
|
||||
}
|
||||
|
||||
var totalDuration time.Duration
|
||||
|
||||
for i := 0; i < attempts; i++ {
|
||||
seed := fmt.Sprintf("benchmark-%d-%d", time.Now().UnixNano(), i)
|
||||
start := time.Now()
|
||||
|
||||
puzzle, err := Generate(d, seed)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
result.FailedPuzzles++
|
||||
continue
|
||||
}
|
||||
|
||||
result.SuccessfulPuzzles++
|
||||
totalDuration += elapsed
|
||||
|
||||
if elapsed < result.MinTime {
|
||||
result.MinTime = elapsed
|
||||
}
|
||||
if elapsed > result.MaxTime {
|
||||
result.MaxTime = elapsed
|
||||
}
|
||||
|
||||
rating := RatePuzzle(puzzle)
|
||||
result.Ratings = append(result.Ratings, rating)
|
||||
}
|
||||
|
||||
if result.SuccessfulPuzzles > 0 {
|
||||
result.AverageTime = totalDuration / time.Duration(result.SuccessfulPuzzles)
|
||||
|
||||
var totalRating int
|
||||
for _, r := range result.Ratings {
|
||||
totalRating += r
|
||||
}
|
||||
result.AverageRating = float64(totalRating) / float64(len(result.Ratings))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// String returns a formatted string representation of the benchmark result.
|
||||
func (br BenchmarkResult) String() string {
|
||||
successRate := float64(br.SuccessfulPuzzles) / float64(br.TotalAttempts) * 100
|
||||
|
||||
return fmt.Sprintf(`Benchmark Results for %s:
|
||||
Total Attempts: %d
|
||||
Successful: %d (%.1f%%)
|
||||
Failed: %d
|
||||
Average Time: %v
|
||||
Min Time: %v
|
||||
Max Time: %v
|
||||
Average Rating: %.1f/100
|
||||
`,
|
||||
br.Difficulty.String(),
|
||||
br.TotalAttempts,
|
||||
br.SuccessfulPuzzles,
|
||||
successRate,
|
||||
br.FailedPuzzles,
|
||||
br.AverageTime,
|
||||
br.MinTime,
|
||||
br.MaxTime,
|
||||
br.AverageRating,
|
||||
)
|
||||
}
|
||||
|
||||
// CompareGenerationMethods compares standard vs symmetric generation.
|
||||
func CompareGenerationMethods(d Difficulty, attempts int) (standard, symmetric BenchmarkResult) {
|
||||
// Benchmark standard generation
|
||||
standard = BenchmarkGeneration(d, attempts)
|
||||
|
||||
// Benchmark symmetric generation
|
||||
symmetric = BenchmarkResult{
|
||||
Difficulty: d,
|
||||
TotalAttempts: attempts,
|
||||
MinTime: time.Hour,
|
||||
Ratings: make([]int, 0, attempts),
|
||||
}
|
||||
|
||||
var totalDuration time.Duration
|
||||
|
||||
for i := 0; i < attempts; i++ {
|
||||
seed := fmt.Sprintf("symmetric-benchmark-%d-%d", time.Now().UnixNano(), i)
|
||||
start := time.Now()
|
||||
|
||||
puzzle, err := GenerateWithSymmetry(d, seed, SymmetryRotational180)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
symmetric.FailedPuzzles++
|
||||
continue
|
||||
}
|
||||
|
||||
symmetric.SuccessfulPuzzles++
|
||||
totalDuration += elapsed
|
||||
|
||||
if elapsed < symmetric.MinTime {
|
||||
symmetric.MinTime = elapsed
|
||||
}
|
||||
if elapsed > symmetric.MaxTime {
|
||||
symmetric.MaxTime = elapsed
|
||||
}
|
||||
|
||||
rating := RatePuzzle(puzzle)
|
||||
symmetric.Ratings = append(symmetric.Ratings, rating)
|
||||
}
|
||||
|
||||
if symmetric.SuccessfulPuzzles > 0 {
|
||||
symmetric.AverageTime = totalDuration / time.Duration(symmetric.SuccessfulPuzzles)
|
||||
|
||||
var totalRating int
|
||||
for _, r := range symmetric.Ratings {
|
||||
totalRating += r
|
||||
}
|
||||
symmetric.AverageRating = float64(totalRating) / float64(len(symmetric.Ratings))
|
||||
}
|
||||
|
||||
return standard, symmetric
|
||||
}
|
||||
|
||||
// PuzzleStatistics provides detailed statistics about a generated puzzle.
|
||||
type PuzzleStatistics struct {
|
||||
Grid Grid
|
||||
Analysis PuzzleAnalysis
|
||||
Rating int
|
||||
EstimatedSolveTime time.Duration
|
||||
ComplexityScore float64
|
||||
}
|
||||
|
||||
// GetPuzzleStatistics performs comprehensive analysis on a puzzle.
|
||||
func GetPuzzleStatistics(g Grid) PuzzleStatistics {
|
||||
analysis := g.Analyze()
|
||||
rating := RatePuzzle(g)
|
||||
|
||||
// Estimate solve time based on difficulty (rough approximation)
|
||||
var estimatedTime time.Duration
|
||||
switch analysis.EstimatedDifficulty {
|
||||
case Easy:
|
||||
estimatedTime = 3 * time.Minute
|
||||
case Normal:
|
||||
estimatedTime = 8 * time.Minute
|
||||
case Hard:
|
||||
estimatedTime = 15 * time.Minute
|
||||
case Expert:
|
||||
estimatedTime = 25 * time.Minute
|
||||
case Lunatic:
|
||||
estimatedTime = 45 * time.Minute
|
||||
}
|
||||
|
||||
// Calculate complexity score (0-1)
|
||||
complexityScore := float64(rating) / 100.0
|
||||
|
||||
return PuzzleStatistics{
|
||||
Grid: g,
|
||||
Analysis: analysis,
|
||||
Rating: rating,
|
||||
EstimatedSolveTime: estimatedTime,
|
||||
ComplexityScore: complexityScore,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a formatted string of puzzle statistics.
|
||||
func (ps PuzzleStatistics) String() string {
|
||||
return fmt.Sprintf(`Puzzle Statistics:
|
||||
Difficulty: %s
|
||||
Rating: %d/100
|
||||
Filled Cells: %d
|
||||
Empty Cells: %d
|
||||
Min Candidates: %d
|
||||
Max Candidates: %d
|
||||
Avg Candidates: %.2f
|
||||
Unique Solution: %v
|
||||
Symmetry: %s
|
||||
Techniques: %v
|
||||
Complexity Score: %.2f
|
||||
Est. Solve Time: %v
|
||||
`,
|
||||
ps.Analysis.EstimatedDifficulty.String(),
|
||||
ps.Rating,
|
||||
ps.Analysis.FilledCells,
|
||||
ps.Analysis.EmptyCells,
|
||||
ps.Analysis.MinCandidates,
|
||||
ps.Analysis.MaxCandidates,
|
||||
ps.Analysis.AvgCandidates,
|
||||
ps.Analysis.HasUniqueSolution,
|
||||
ps.Analysis.SymmetryType.String(),
|
||||
ps.Analysis.SolvingTechniques,
|
||||
ps.ComplexityScore,
|
||||
ps.EstimatedSolveTime,
|
||||
)
|
||||
}
|
||||
275
internal/generator/core.go
Normal file
275
internal/generator/core.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"termdoku/internal/solver"
|
||||
)
|
||||
|
||||
// randomizedFullSolution builds a complete valid Sudoku solution using randomized DFS.
|
||||
func randomizedFullSolution(seed string, timeout time.Duration) (Grid, error) {
|
||||
var rng *rand.Rand
|
||||
if seed == "" {
|
||||
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
} else {
|
||||
rng = rand.New(rand.NewSource(int64(hashStringToUint64(seed))))
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
var g Grid
|
||||
if fillCellRandom(&g, 0, 0, rng, deadline) {
|
||||
return g, nil
|
||||
}
|
||||
return Grid{}, ErrTimeout
|
||||
}
|
||||
|
||||
func fillCellRandom(g *Grid, row, col int, rng *rand.Rand, deadline time.Time) bool {
|
||||
if time.Now().After(deadline) {
|
||||
return false
|
||||
}
|
||||
nextRow, nextCol := row, col+1
|
||||
if nextCol == 9 {
|
||||
nextRow++
|
||||
nextCol = 0
|
||||
}
|
||||
if row == 9 {
|
||||
return true
|
||||
}
|
||||
vals := []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
rng.Shuffle(len(vals), func(i, j int) { vals[i], vals[j] = vals[j], vals[i] })
|
||||
for _, v := range vals {
|
||||
if isSafe(*g, row, col, v) {
|
||||
g[row][col] = v
|
||||
if fillCellRandom(g, nextRow, nextCol, rng, deadline) {
|
||||
return true
|
||||
}
|
||||
g[row][col] = 0
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isSafe(g Grid, row, col int, v uint8) bool {
|
||||
for i := 0; i < 9; i++ {
|
||||
if g[row][i] == v || g[i][col] == v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
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] == v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// carveCellsUnique removes cells while trying to keep a single solution.
|
||||
func carveCellsUnique(full Grid, targetRemoved int, seed string, timeout time.Duration) (Grid, error) {
|
||||
puzzle := full
|
||||
var rng *rand.Rand
|
||||
if seed == "" {
|
||||
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
} else {
|
||||
rng = rand.New(rand.NewSource(int64(hashStringToUint64(seed) + 0x9e3779b97f4a7c15)))
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
cells := make([]int, 81)
|
||||
for i := 0; i < 81; i++ {
|
||||
cells[i] = i
|
||||
}
|
||||
rng.Shuffle(len(cells), func(i, j int) { cells[i], cells[j] = cells[j], cells[i] })
|
||||
removed := 0
|
||||
for _, idx := range cells {
|
||||
if time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
r := idx / 9
|
||||
c := idx % 9
|
||||
backup := puzzle[r][c]
|
||||
puzzle[r][c] = 0
|
||||
// Check uniqueness using solver.CountSolutions up to 2
|
||||
if solver.CountSolutions(convertToSolverGrid(puzzle), 50*time.Millisecond, 2) != 1 {
|
||||
puzzle[r][c] = backup
|
||||
continue
|
||||
}
|
||||
removed++
|
||||
if removed >= targetRemoved {
|
||||
break
|
||||
}
|
||||
}
|
||||
return puzzle, nil
|
||||
}
|
||||
|
||||
// carveCellsSymmetric removes cells with symmetry pattern while maintaining uniqueness.
|
||||
func carveCellsSymmetric(full Grid, targetRemoved int, seed string, timeout time.Duration, symmetry SymmetryType) (Grid, error) {
|
||||
puzzle := full
|
||||
var rng *rand.Rand
|
||||
if seed == "" {
|
||||
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
} else {
|
||||
rng = rand.New(rand.NewSource(int64(hashStringToUint64(seed) + 0x9e3779b97f4a7c15)))
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
// Generate cell pairs based on symmetry type
|
||||
var cellPairs [][]struct{ r, c int }
|
||||
switch symmetry {
|
||||
case SymmetryRotational180:
|
||||
cellPairs = generateRotational180Pairs()
|
||||
case SymmetryVertical:
|
||||
cellPairs = generateVerticalPairs()
|
||||
case SymmetryHorizontal:
|
||||
cellPairs = generateHorizontalPairs()
|
||||
default:
|
||||
// Fallback to non-symmetric
|
||||
return carveCellsUnique(full, targetRemoved, seed, timeout)
|
||||
}
|
||||
|
||||
rng.Shuffle(len(cellPairs), func(i, j int) { cellPairs[i], cellPairs[j] = cellPairs[j], cellPairs[i] })
|
||||
|
||||
removed := 0
|
||||
for _, pair := range cellPairs {
|
||||
if time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
|
||||
// Try removing all cells in the pair
|
||||
backups := make([]uint8, len(pair))
|
||||
for i, pos := range pair {
|
||||
backups[i] = puzzle[pos.r][pos.c]
|
||||
puzzle[pos.r][pos.c] = 0
|
||||
}
|
||||
|
||||
// Check if still unique
|
||||
if solver.CountSolutions(convertToSolverGrid(puzzle), 50*time.Millisecond, 2) != 1 {
|
||||
// Restore if not unique
|
||||
for i, pos := range pair {
|
||||
puzzle[pos.r][pos.c] = backups[i]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
removed += len(pair)
|
||||
if removed >= targetRemoved {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return puzzle, nil
|
||||
}
|
||||
|
||||
// generateRotational180Pairs creates cell pairs with 180° rotational symmetry.
|
||||
func generateRotational180Pairs() [][]struct{ r, c int } {
|
||||
var pairs [][]struct{ r, c int }
|
||||
used := make(map[int]bool)
|
||||
|
||||
for r := 0; r < 9; r++ {
|
||||
for c := 0; c < 9; c++ {
|
||||
idx := r*9 + c
|
||||
if used[idx] {
|
||||
continue
|
||||
}
|
||||
|
||||
r2 := 8 - r
|
||||
c2 := 8 - c
|
||||
idx2 := r2*9 + c2
|
||||
|
||||
if idx == idx2 {
|
||||
// Center cell (4,4)
|
||||
pairs = append(pairs, []struct{ r, c int }{{r, c}})
|
||||
} else {
|
||||
pairs = append(pairs, []struct{ r, c int }{{r, c}, {r2, c2}})
|
||||
used[idx2] = true
|
||||
}
|
||||
used[idx] = true
|
||||
}
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
// generateVerticalPairs creates cell pairs with vertical symmetry.
|
||||
func generateVerticalPairs() [][]struct{ r, c int } {
|
||||
var pairs [][]struct{ r, c int }
|
||||
used := make(map[int]bool)
|
||||
|
||||
for r := 0; r < 9; r++ {
|
||||
for c := 0; c < 9; c++ {
|
||||
idx := r*9 + c
|
||||
if used[idx] {
|
||||
continue
|
||||
}
|
||||
|
||||
c2 := 8 - c
|
||||
idx2 := r*9 + c2
|
||||
|
||||
if c == c2 {
|
||||
// Center column
|
||||
pairs = append(pairs, []struct{ r, c int }{{r, c}})
|
||||
} else {
|
||||
pairs = append(pairs, []struct{ r, c int }{{r, c}, {r, c2}})
|
||||
used[idx2] = true
|
||||
}
|
||||
used[idx] = true
|
||||
}
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
// generateHorizontalPairs creates cell pairs with horizontal symmetry.
|
||||
func generateHorizontalPairs() [][]struct{ r, c int } {
|
||||
var pairs [][]struct{ r, c int }
|
||||
used := make(map[int]bool)
|
||||
|
||||
for r := 0; r < 9; r++ {
|
||||
for c := 0; c < 9; c++ {
|
||||
idx := r*9 + c
|
||||
if used[idx] {
|
||||
continue
|
||||
}
|
||||
|
||||
r2 := 8 - r
|
||||
idx2 := r2*9 + c
|
||||
|
||||
if r == r2 {
|
||||
// Center row
|
||||
pairs = append(pairs, []struct{ r, c int }{{r, c}})
|
||||
} else {
|
||||
pairs = append(pairs, []struct{ r, c int }{{r, c}, {r2, c}})
|
||||
used[idx2] = true
|
||||
}
|
||||
used[idx] = true
|
||||
}
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
func convertToSolverGrid(g Grid) solver.Grid {
|
||||
var s solver.Grid
|
||||
for r := 0; r < 9; r++ {
|
||||
for c := 0; c < 9; c++ {
|
||||
s[r][c] = g[r][c]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Simple FNV-1a 64-bit hash for seed strings.
|
||||
func hashStringToUint64(s string) uint64 {
|
||||
const (
|
||||
offset64 = 1469598103934665603
|
||||
prime64 = 1099511628211
|
||||
)
|
||||
h := uint64(offset64)
|
||||
for i := 0; i < len(s); i++ {
|
||||
h ^= uint64(s[i])
|
||||
h *= prime64
|
||||
}
|
||||
return h
|
||||
}
|
||||
248
internal/generator/generator.go
Normal file
248
internal/generator/generator.go
Normal file
@@ -0,0 +1,248 @@
|
||||
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
|
||||
}
|
||||
298
internal/generator/utils.go
Normal file
298
internal/generator/utils.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"termdoku/internal/solver"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PrintGrid prints the grid in a human-readable format.
|
||||
func PrintGrid(g Grid) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("┌───────┬───────┬───────┐\n")
|
||||
for r := range 9 {
|
||||
sb.WriteString("│ ")
|
||||
for c := range 9 {
|
||||
if g[r][c] == 0 {
|
||||
sb.WriteString(".")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%d", g[r][c]))
|
||||
}
|
||||
|
||||
if c%3 == 2 {
|
||||
sb.WriteString(" │ ")
|
||||
} else {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
if r%3 == 2 && r != 8 {
|
||||
sb.WriteString("├───────┼───────┼───────┤\n")
|
||||
}
|
||||
}
|
||||
sb.WriteString("└───────┴───────┴───────┘\n")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// GridToString converts a grid to a compact string representation.
|
||||
func GridToString(g Grid) string {
|
||||
var sb strings.Builder
|
||||
for r := range 9 {
|
||||
for c := range 9 {
|
||||
if g[r][c] == 0 {
|
||||
sb.WriteString(".")
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%d", g[r][c]))
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// StringToGrid converts a string representation back to a grid.
|
||||
// The string should be 81 characters long, with '0' or '.' for empty cells.
|
||||
func StringToGrid(s string) (Grid, error) {
|
||||
if len(s) != 81 {
|
||||
return Grid{}, fmt.Errorf("invalid string length: expected 81, got %d", len(s))
|
||||
}
|
||||
|
||||
var g Grid
|
||||
for i, ch := range s {
|
||||
r := i / 9
|
||||
c := i % 9
|
||||
|
||||
if ch == '.' || ch == '0' {
|
||||
g[r][c] = 0
|
||||
} else if ch >= '1' && ch <= '9' {
|
||||
g[r][c] = uint8(ch - '0')
|
||||
} else {
|
||||
return Grid{}, fmt.Errorf("invalid character at position %d: %c", i, ch)
|
||||
}
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// CompareGrids returns the differences between two grids.
|
||||
func CompareGrids(g1, g2 Grid) []struct {
|
||||
Row, Col int
|
||||
Val1, Val2 uint8
|
||||
} {
|
||||
var diffs []struct {
|
||||
Row, Col int
|
||||
Val1, Val2 uint8
|
||||
}
|
||||
|
||||
for r := 0; r < 9; r++ {
|
||||
for c := 0; c < 9; c++ {
|
||||
if g1[r][c] != g2[r][c] {
|
||||
diffs = append(diffs, struct {
|
||||
Row, Col int
|
||||
Val1, Val2 uint8
|
||||
}{
|
||||
Row: r,
|
||||
Col: c,
|
||||
Val1: g1[r][c],
|
||||
Val2: g2[r][c],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diffs
|
||||
}
|
||||
|
||||
// GetEmptyCells returns a list of all empty cell positions.
|
||||
func GetEmptyCells(g Grid) []struct{ Row, Col int } {
|
||||
var empty []struct{ Row, Col int }
|
||||
|
||||
for r := 0; r < 9; r++ {
|
||||
for c := 0; c < 9; c++ {
|
||||
if g[r][c] == 0 {
|
||||
empty = append(empty, struct{ Row, Col int }{r, c})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return empty
|
||||
}
|
||||
|
||||
// GetFilledCells returns a list of all filled cell positions with their values.
|
||||
func GetFilledCells(g Grid) []struct {
|
||||
Row, Col int
|
||||
Value uint8
|
||||
} {
|
||||
var filled []struct {
|
||||
Row, Col int
|
||||
Value uint8
|
||||
}
|
||||
|
||||
for r := 0; r < 9; r++ {
|
||||
for c := 0; c < 9; c++ {
|
||||
if g[r][c] != 0 {
|
||||
filled = append(filled, struct {
|
||||
Row, Col int
|
||||
Value uint8
|
||||
}{r, c, g[r][c]})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filled
|
||||
}
|
||||
|
||||
// GetRegionCells returns all cell positions in a 3x3 region.
|
||||
func GetRegionCells(blockRow, blockCol int) []struct{ Row, Col int } {
|
||||
if blockRow < 0 || blockRow > 2 || blockCol < 0 || blockCol > 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var cells []struct{ Row, Col int }
|
||||
startRow := blockRow * 3
|
||||
startCol := blockCol * 3
|
||||
|
||||
for r := startRow; r < startRow+3; r++ {
|
||||
for c := startCol; c < startCol+3; c++ {
|
||||
cells = append(cells, struct{ Row, Col int }{r, c})
|
||||
}
|
||||
}
|
||||
|
||||
return cells
|
||||
}
|
||||
|
||||
// ValidateGridStructure checks if a grid has valid structure (no duplicates).
|
||||
func ValidateGridStructure(g Grid) []string {
|
||||
var errors []string
|
||||
|
||||
// Check rows
|
||||
for r := range 9 {
|
||||
seen := make(map[uint8]bool)
|
||||
for c := 0; c < 9; c++ {
|
||||
v := g[r][c]
|
||||
if v == 0 {
|
||||
continue
|
||||
}
|
||||
if seen[v] {
|
||||
errors = append(errors, fmt.Sprintf("Duplicate %d in row %d", v, r+1))
|
||||
}
|
||||
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] {
|
||||
errors = append(errors, fmt.Sprintf("Duplicate %d in column %d", v, c+1))
|
||||
}
|
||||
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] {
|
||||
errors = append(errors, fmt.Sprintf("Duplicate %d in block (%d,%d)", v, br+1, bc+1))
|
||||
}
|
||||
seen[v] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// GetCandidateMap returns a map of all candidates for all empty cells.
|
||||
func GetCandidateMap(g Grid) map[struct{ Row, Col int }][]uint8 {
|
||||
candidateMap := make(map[struct{ Row, Col int }][]uint8)
|
||||
|
||||
for r := range 9 {
|
||||
for c := range 9 {
|
||||
if g[r][c] == 0 {
|
||||
candidates := g.GetCandidates(r, c)
|
||||
if len(candidates) > 0 {
|
||||
candidateMap[struct{ Row, Col int }{r, c}] = candidates
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidateMap
|
||||
}
|
||||
|
||||
// CalculateCompletionPercentage returns the percentage of filled cells.
|
||||
func CalculateCompletionPercentage(g Grid) float64 {
|
||||
filled := g.CountFilledCells()
|
||||
return (float64(filled) / 81.0) * 100.0
|
||||
}
|
||||
|
||||
// GetMostConstrainedCell returns the empty cell with the fewest candidates.
|
||||
// This is useful for implementing solving strategies.
|
||||
func GetMostConstrainedCell(g Grid) (row, col int, candidates []uint8, found bool) {
|
||||
minCandidates := 10
|
||||
found = false
|
||||
|
||||
for r := range 9 {
|
||||
for c := range 9 {
|
||||
if g[r][c] == 0 {
|
||||
cands := g.GetCandidates(r, c)
|
||||
if len(cands) < minCandidates {
|
||||
minCandidates = len(cands)
|
||||
row = r
|
||||
col = c
|
||||
candidates = cands
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsMinimalPuzzle checks if removing any filled cell would result in multiple solutions.
|
||||
// This is computationally expensive and should be used sparingly.
|
||||
func IsMinimalPuzzle(g Grid) bool {
|
||||
// For each filled cell, try removing it and check if puzzle still has unique solution
|
||||
for r := range 9 {
|
||||
for c := range 9 {
|
||||
if g[r][c] != 0 {
|
||||
// Try removing this cell
|
||||
backup := g[r][c]
|
||||
g[r][c] = 0
|
||||
|
||||
// Check if still unique
|
||||
solverGrid := convertToSolverGrid(g)
|
||||
solutionCount := solver.CountSolutions(solverGrid, 100*time.Millisecond, 2)
|
||||
|
||||
// Restore the cell
|
||||
g[r][c] = backup
|
||||
|
||||
// If removing this cell doesn't maintain uniqueness, it's not minimal
|
||||
if solutionCount != 1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user