Files
termdoku/internal/generator/core.go

276 lines
6.1 KiB
Go

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
}