Files
termdoku/internal/generator/api.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
}