package generator import ( "math/rand" "time" "termdoku/internal/solver" ) // randomizedFullSolution builds a complete valid Sudoku solution using randomized DFS. func randomizedFullSolution(seed string, timeout time.Duration) (Grid, error) { var rng *rand.Rand if seed == "" { rng = rand.New(rand.NewSource(time.Now().UnixNano())) } else { rng = rand.New(rand.NewSource(int64(hashStringToUint64(seed)))) } deadline := time.Now().Add(timeout) var g Grid if fillCellRandom(&g, 0, 0, rng, deadline) { return g, nil } return Grid{}, ErrTimeout } func fillCellRandom(g *Grid, row, col int, rng *rand.Rand, deadline time.Time) bool { if time.Now().After(deadline) { return false } nextRow, nextCol := row, col+1 if nextCol == 9 { nextRow++ nextCol = 0 } if row == 9 { return true } vals := []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9} rng.Shuffle(len(vals), func(i, j int) { vals[i], vals[j] = vals[j], vals[i] }) for _, v := range vals { if isSafe(*g, row, col, v) { g[row][col] = v if fillCellRandom(g, nextRow, nextCol, rng, deadline) { return true } g[row][col] = 0 } } return false } func isSafe(g Grid, row, col int, v uint8) bool { for i := 0; i < 9; i++ { if g[row][i] == v || g[i][col] == v { return false } } 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] == v { return false } } } return true } // carveCellsUnique removes cells while trying to keep a single solution. func carveCellsUnique(full Grid, targetRemoved int, seed string, timeout time.Duration) (Grid, error) { puzzle := full var rng *rand.Rand if seed == "" { rng = rand.New(rand.NewSource(time.Now().UnixNano())) } else { rng = rand.New(rand.NewSource(int64(hashStringToUint64(seed) + 0x9e3779b97f4a7c15))) } deadline := time.Now().Add(timeout) cells := make([]int, 81) for i := 0; i < 81; i++ { cells[i] = i } rng.Shuffle(len(cells), func(i, j int) { cells[i], cells[j] = cells[j], cells[i] }) removed := 0 for _, idx := range cells { if time.Now().After(deadline) { break } r := idx / 9 c := idx % 9 backup := puzzle[r][c] puzzle[r][c] = 0 // Check uniqueness using solver.CountSolutions up to 2 if solver.CountSolutions(convertToSolverGrid(puzzle), 50*time.Millisecond, 2) != 1 { puzzle[r][c] = backup continue } removed++ if removed >= targetRemoved { break } } return puzzle, nil } // carveCellsSymmetric removes cells with symmetry pattern while maintaining uniqueness. func carveCellsSymmetric(full Grid, targetRemoved int, seed string, timeout time.Duration, symmetry SymmetryType) (Grid, error) { puzzle := full var rng *rand.Rand if seed == "" { rng = rand.New(rand.NewSource(time.Now().UnixNano())) } else { rng = rand.New(rand.NewSource(int64(hashStringToUint64(seed) + 0x9e3779b97f4a7c15))) } deadline := time.Now().Add(timeout) // Generate cell pairs based on symmetry type var cellPairs [][]struct{ r, c int } switch symmetry { case SymmetryRotational180: cellPairs = generateRotational180Pairs() case SymmetryVertical: cellPairs = generateVerticalPairs() case SymmetryHorizontal: cellPairs = generateHorizontalPairs() default: // Fallback to non-symmetric return carveCellsUnique(full, targetRemoved, seed, timeout) } rng.Shuffle(len(cellPairs), func(i, j int) { cellPairs[i], cellPairs[j] = cellPairs[j], cellPairs[i] }) removed := 0 for _, pair := range cellPairs { if time.Now().After(deadline) { break } // Try removing all cells in the pair backups := make([]uint8, len(pair)) for i, pos := range pair { backups[i] = puzzle[pos.r][pos.c] puzzle[pos.r][pos.c] = 0 } // Check if still unique if solver.CountSolutions(convertToSolverGrid(puzzle), 50*time.Millisecond, 2) != 1 { // Restore if not unique for i, pos := range pair { puzzle[pos.r][pos.c] = backups[i] } continue } removed += len(pair) if removed >= targetRemoved { break } } return puzzle, nil } // generateRotational180Pairs creates cell pairs with 180° rotational symmetry. func generateRotational180Pairs() [][]struct{ r, c int } { var pairs [][]struct{ r, c int } used := make(map[int]bool) for r := 0; r < 9; r++ { for c := 0; c < 9; c++ { idx := r*9 + c if used[idx] { continue } r2 := 8 - r c2 := 8 - c idx2 := r2*9 + c2 if idx == idx2 { // Center cell (4,4) pairs = append(pairs, []struct{ r, c int }{{r, c}}) } else { pairs = append(pairs, []struct{ r, c int }{{r, c}, {r2, c2}}) used[idx2] = true } used[idx] = true } } return pairs } // generateVerticalPairs creates cell pairs with vertical symmetry. func generateVerticalPairs() [][]struct{ r, c int } { var pairs [][]struct{ r, c int } used := make(map[int]bool) for r := 0; r < 9; r++ { for c := 0; c < 9; c++ { idx := r*9 + c if used[idx] { continue } c2 := 8 - c idx2 := r*9 + c2 if c == c2 { // Center column pairs = append(pairs, []struct{ r, c int }{{r, c}}) } else { pairs = append(pairs, []struct{ r, c int }{{r, c}, {r, c2}}) used[idx2] = true } used[idx] = true } } return pairs } // generateHorizontalPairs creates cell pairs with horizontal symmetry. func generateHorizontalPairs() [][]struct{ r, c int } { var pairs [][]struct{ r, c int } used := make(map[int]bool) for r := 0; r < 9; r++ { for c := 0; c < 9; c++ { idx := r*9 + c if used[idx] { continue } r2 := 8 - r idx2 := r2*9 + c if r == r2 { // Center row pairs = append(pairs, []struct{ r, c int }{{r, c}}) } else { pairs = append(pairs, []struct{ r, c int }{{r, c}, {r2, c}}) used[idx2] = true } used[idx] = true } } return pairs } func convertToSolverGrid(g Grid) solver.Grid { var s solver.Grid for r := 0; r < 9; r++ { for c := 0; c < 9; c++ { s[r][c] = g[r][c] } } return s } // Simple FNV-1a 64-bit hash for seed strings. func hashStringToUint64(s string) uint64 { const ( offset64 = 1469598103934665603 prime64 = 1099511628211 ) h := uint64(offset64) for i := 0; i < len(s); i++ { h ^= uint64(s[i]) h *= prime64 } return h }