485 lines
9.3 KiB
Go
485 lines
9.3 KiB
Go
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
|
|
}
|