Refactor Sudoku field and solver implementation

This commit is contained in:
2026-09-16 22:52:44 +02:00
parent 8a5ae8c640
commit f7f9c16184
24 changed files with 1574 additions and 603 deletions
+10 -4
View File
@@ -17,13 +17,16 @@ func (ld *LastDigit) SearchProgressableCells() int {
missingNumbers := part.GetMissingNumbers()
if len(missingNumbers) == 1 {
var emptyCell *field.Cell
emptyCount := 0
part.ForEachCell(func(cell *field.Cell) {
if cell.GetNumber() == 0 {
emptyCell = cell
emptyCount++
}
})
// fmt.Println(part)
// fmt.Printf("Gefundene Zelle: %d/%d - %d, missungNumber: %v, Typ: %T\n", emptyCell.Pos.GetRow(), emptyCell.Pos.GetColumn(), emptyCell.GetNumber(), missingNumbers, part)
if emptyCount != 1 {
return
}
change := field.ExternalChange{
Cell: emptyCell,
Action: field.ActionSetNumber,
@@ -31,10 +34,9 @@ func (ld *LastDigit) SearchProgressableCells() int {
From: emptyCell.GetNumber(),
TriggerdBy: ld.getName(),
}
ld.changes = append(ld.changes, change)
ld.queue(change)
}
})
// fmt.Printf("LastDigit Changes %d\n", len(ld.changes))
return len(ld.changes)
}
@@ -42,6 +44,10 @@ func (ld *LastDigit) getName() string {
return "Last Digit"
}
func (ld *LastDigit) Name() string {
return ld.getName()
}
// ────────────────────────────────────────────────────────────────────────────── //
// CRP STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
+9 -5
View File
@@ -15,8 +15,8 @@ func (hs *HiddenSingle) SearchProgressableCells() int {
m := make(map[int]int)
part.ForEachCell(func(cell *field.Cell) {
notes := cell.Notes.Get()
for note := range notes {
notes := cell.GetNotes().Get()
for _, note := range notes {
m[note]++
}
})
@@ -28,9 +28,9 @@ func (hs *HiddenSingle) SearchProgressableCells() int {
}
}
for num := range hiddenSingles {
for _, num := range hiddenSingles {
part.ForEachCell(func(cell *field.Cell) {
if cell.Notes.Has(num) {
if cell.GetNumber() == 0 && cell.GetNotes().Has(num) {
change := field.ExternalChange{
Cell: cell,
Action: field.ActionSetNumber,
@@ -38,7 +38,7 @@ func (hs *HiddenSingle) SearchProgressableCells() int {
From: cell.GetNumber(),
TriggerdBy: hs.getName(),
}
hs.changes = append(hs.changes, change)
hs.queue(change)
}
})
}
@@ -50,6 +50,10 @@ func (hs *HiddenSingle) getName() string {
return "Hidden Single"
}
func (hs *HiddenSingle) Name() string {
return hs.getName()
}
// ────────────────────────────────────────────────────────────────────────────── //
// HIDDEN_PAIR STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
+6 -2
View File
@@ -13,7 +13,7 @@ type NakedSingle struct {
func (ns *NakedSingle) SearchProgressableCells() int {
ns.field.ForEachPart(func(part field.Part) {
part.ForEachCell(func(cell *field.Cell) {
candidates := cell.Notes.Get()
candidates := cell.GetNotes().Get()
if len(candidates) == 1 && cell.GetNumber() == 0 {
change := field.ExternalChange{
Cell: cell,
@@ -22,7 +22,7 @@ func (ns *NakedSingle) SearchProgressableCells() int {
From: cell.GetNumber(),
TriggerdBy: ns.getName(),
}
ns.changes = append(ns.changes, change)
ns.queue(change)
}
})
})
@@ -33,6 +33,10 @@ func (ns *NakedSingle) getName() string {
return "Naked Single"
}
func (ns *NakedSingle) Name() string {
return ns.getName()
}
// ────────────────────────────────────────────────────────────────────────────── //
// NAKED_DOUBLE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
+12 -4
View File
@@ -1,6 +1,8 @@
package strategies
import (
"sort"
"git.kleiax.de/homepage/field"
)
@@ -15,11 +17,12 @@ type Notes struct {
func (n *Notes) SearchProgressableCells() int {
n.field.ForEachRow(func(row *field.Row) {
row.ForEachCell(func(cell *field.Cell) {
column, _ := n.field.GetColumn(cell.Pos.GetColumn())
block, _ := n.field.GetBlock(cell.Pos.GetBlockRow(), cell.Pos.GetBlockColumn())
pos := cell.GetPosition()
column, _ := n.field.GetColumn(pos.GetColumn())
block, _ := n.field.GetBlock(pos.GetBlockRow(), pos.GetBlockColumn())
candidates := intersection3(row.GetMissingNumbers(), column.GetMissingNumbers(), block.GetMissingNumbers())
for _, note := range candidates {
if cell.Notes.Has(note) || cell.GetNumber() != 0 {
if cell.GetNotes().Has(note) || cell.GetNumber() != 0 {
continue
}
ch := field.ExternalChange{
@@ -30,7 +33,7 @@ func (n *Notes) SearchProgressableCells() int {
Marks: nil,
From: 0,
}
n.changes = append(n.changes, ch)
n.queue(ch)
}
})
})
@@ -41,6 +44,10 @@ func (n *Notes) getName() string {
return "Make Notes"
}
func (n *Notes) Name() string {
return n.getName()
}
func intersection3(a, b, c []int) []int {
set := make(map[int]bool)
@@ -76,6 +83,7 @@ func intersection3(a, b, c []int) []int {
for v := range set {
result = append(result, v)
}
sort.Ints(result)
return result
}
+82
View File
@@ -0,0 +1,82 @@
package strategies
import (
"strings"
"testing"
"git.kleiax.de/homepage/field"
)
const solvedGrid = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
func strategyField(t *testing.T, digits string) *field.Field {
t.Helper()
props := field.Properties{Rows: 9, Columns: 9, BlockRows: 3, BlockColumns: 3, BlockSizeRow: 3, BlockSizeColumn: 3}
cells := make([][]field.Cell, 9)
for row := range cells {
cells[row] = make([]field.Cell, 9)
for column := range cells[row] {
pos := field.NewPosition(row, column, row/3, column/3, row%3, column%3)
cells[row][column] = *field.NewCell(int(digits[row*9+column]-'0'), pos)
}
}
result, err := field.New(props, cells)
if err != nil {
t.Fatal(err)
}
return result
}
func TestLastDigitDeduplicatesOverlappingParts(t *testing.T) {
f := strategyField(t, "0"+solvedGrid[1:])
strategy := &LastDigit{}
strategy.Init(f)
if got := strategy.SearchProgressableCells(); got != 1 {
t.Fatalf("SearchProgressableCells() = %d, want 1", got)
}
changes, err := strategy.ApplyAll()
if err != nil {
t.Fatalf("ApplyAll() error = %v", err)
}
if len(changes) != 1 || !f.IsSolved() {
t.Fatalf("changes = %d, solved = %v", len(changes), f.IsSolved())
}
}
func TestHiddenSingleUsesCandidateValuesNotSliceIndexes(t *testing.T) {
f := strategyField(t, strings.Repeat("0", 81))
first, _ := f.GetCell(0, 0)
second, _ := f.GetCell(0, 1)
third, _ := f.GetCell(0, 2)
for _, setup := range []struct {
cell *field.Cell
note int
}{{first, 9}, {second, 8}, {third, 8}} {
if err := setup.cell.GetNotes().Add(f, setup.cell, setup.note, "test", nil); err != nil {
t.Fatal(err)
}
}
strategy := &HiddenSingle{}
strategy.Init(f)
strategy.SearchProgressableCells()
found := false
for _, change := range strategy.changes {
if change.Cell == first && change.Value == 9 {
found = true
}
}
if !found {
t.Fatal("hidden single for candidate value 9 was not found")
}
}
func TestIntersection3IsSorted(t *testing.T) {
got := intersection3([]int{9, 1, 5}, []int{5, 9, 1}, []int{9, 5, 1})
want := []int{1, 5, 9}
for i := range want {
if got[i] != want[i] {
t.Fatalf("intersection3() = %v, want %v", got, want)
}
}
}
+23 -18
View File
@@ -12,8 +12,8 @@ import (
type Strategy interface {
Init(f *field.Field)
ApplyAll() []field.ExternalChange
ApplyNext() field.ExternalChange
ApplyAll() ([]field.ExternalChange, error)
ApplyNext() (field.ExternalChange, error)
Name() string
SearchProgressableCells() int
}
@@ -40,35 +40,40 @@ func (b *Base) Init(f *field.Field) {
b.field = f
}
func (b *Base) ApplyAll() []field.ExternalChange {
func (b *Base) ApplyAll() ([]field.ExternalChange, error) {
changesCopy := make([]field.ExternalChange, len(b.changes))
copy(changesCopy, b.changes)
for _, change := range b.changes {
b.field.AddChange(&change)
}
b.changes = b.changes[:0]
return changesCopy
if err := b.field.AddChanges(changesCopy); err != nil {
return nil, err
}
return changesCopy, nil
}
func (b *Base) ApplyNext() field.ExternalChange {
func (b *Base) ApplyNext() (field.ExternalChange, error) {
if len(b.changes) < 1 {
return field.ExternalChange{}
return field.ExternalChange{}, nil
}
changeCopy := b.changes[0]
b.field.AddChange(&b.changes[0])
b.changes = b.changes[1:]
if err := b.field.AddChange(&changeCopy); err != nil {
return field.ExternalChange{}, err
}
return changeCopy, nil
}
return changeCopy
func (b *Base) queue(change field.ExternalChange) {
for _, existing := range b.changes {
if existing.Cell == change.Cell && existing.Action == change.Action &&
existing.Value == change.Value && existing.From == change.From {
return
}
}
b.changes = append(b.changes, change)
}
func (b *Base) getName() string {
return "Unkown"
return "Unknown"
}
func (b *Base) Name() string {