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