(Feat): Initial Commit, Termdoku
This commit is contained in:
298
internal/generator/utils.go
Normal file
298
internal/generator/utils.go
Normal file
@@ -0,0 +1,298 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user