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 }