(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
|
||||
}
|
||||
Reference in New Issue
Block a user