299 lines
6.2 KiB
Go
299 lines
6.2 KiB
Go
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
|
|
}
|