Files

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
}