Refactor Sudoku field and solver implementation
This commit is contained in:
+54
-36
@@ -1,65 +1,83 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kleiax.de/homepage/field"
|
||||
"git.kleiax.de/homepage/logic/strategies"
|
||||
)
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// SOLVER STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
var ErrNoProgress = errors.New("no strategy can solve the puzzle")
|
||||
|
||||
type Solver struct {
|
||||
strategies []strategies.Strategy
|
||||
returnTo int
|
||||
field *field.Field
|
||||
conf struct {
|
||||
all bool
|
||||
repeat bool
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Solver) Add(strategy strategies.Strategy) {
|
||||
s.strategies = append(s.strategies, strategy)
|
||||
}
|
||||
|
||||
func (s *Solver) InitStragies(field *field.Field) {
|
||||
s.returnTo = 1
|
||||
s.field = field
|
||||
for _, strategy := range s.strategies {
|
||||
strategy.Init(field)
|
||||
if strategy != nil {
|
||||
s.strategies = append(s.strategies, strategy)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Solver) Run(i int) bool {
|
||||
for j := 0; j < len(s.strategies); j++ {
|
||||
if s.strategies[j].SearchProgressableCells() == 0 {
|
||||
continue
|
||||
func (s *Solver) InitStrategies(f *field.Field) error {
|
||||
if f == nil || !f.IsValid() {
|
||||
return field.ErrInvalidField
|
||||
}
|
||||
s.field = f
|
||||
for _, strategy := range s.strategies {
|
||||
strategy.Init(f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitStragies is kept for compatibility. New code should use InitStrategies.
|
||||
func (s *Solver) InitStragies(f *field.Field) error {
|
||||
return s.InitStrategies(f)
|
||||
}
|
||||
|
||||
// Run applies strategies from the requested index until the field is solved or
|
||||
// no strategy can make progress. It returns false without an error for a valid
|
||||
// but currently unsolved field.
|
||||
func (s *Solver) Run(start int) (bool, error) {
|
||||
if s.field == nil || !s.field.IsValid() {
|
||||
return false, field.ErrInvalidField
|
||||
}
|
||||
if start < 0 || start > len(s.strategies) {
|
||||
return false, fmt.Errorf("strategy index %d out of range", start)
|
||||
}
|
||||
|
||||
for {
|
||||
if s.field.IsSolved() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for _, change := range s.strategies[j].ApplyAll() {
|
||||
if change.Action != field.ActionSetNumber {
|
||||
progress := false
|
||||
for index := start; index < len(s.strategies); index++ {
|
||||
strategy := s.strategies[index]
|
||||
if strategy.SearchProgressableCells() == 0 {
|
||||
continue
|
||||
}
|
||||
s.field.ForEachPartAtPos(change.Cell.Pos, func(part field.Part) {
|
||||
part.ForEachCell(func(cell *field.Cell) {
|
||||
cell.Notes.Remove(s.field, cell, change.Value, "remove note after insert of a number", nil)
|
||||
})
|
||||
})
|
||||
changes, err := strategy.ApplyAll()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("apply strategy %q: %w", strategy.Name(), err)
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
continue
|
||||
}
|
||||
progress = true
|
||||
break
|
||||
}
|
||||
|
||||
j = s.returnTo - 1
|
||||
if !progress {
|
||||
return false, nil
|
||||
}
|
||||
start = 0
|
||||
}
|
||||
|
||||
if s.field.IsSolved() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Solver) Search() {
|
||||
|
||||
}
|
||||
func (s *Solver) Search() {}
|
||||
|
||||
func (s *Solver) GetSolutionPath() []field.ExternalChange {
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package logic_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"git.kleiax.de/homepage/field"
|
||||
"git.kleiax.de/homepage/logic"
|
||||
"git.kleiax.de/homepage/logic/strategies"
|
||||
"git.kleiax.de/homepage/parser"
|
||||
)
|
||||
|
||||
const solvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
|
||||
|
||||
func parsedField(t *testing.T, puzzle string) *field.Field {
|
||||
t.Helper()
|
||||
input := &parser.PuzzleString{}
|
||||
if err := input.Parse([]byte(puzzle)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := input.GetField(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestSolverRunSolvesAndDetectsNoProgress(t *testing.T) {
|
||||
t.Run("solved", func(t *testing.T) {
|
||||
f := parsedField(t, "0"+solvedPuzzle[1:])
|
||||
solver := &logic.Solver{}
|
||||
solver.Add(&strategies.LastDigit{})
|
||||
if err := solver.InitStrategies(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
solved, err := solver.Run(0)
|
||||
if err != nil || !solved || !f.IsSolved() {
|
||||
t.Fatalf("Run() = (%v, %v), field solved = %v", solved, err, f.IsSolved())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no progress", func(t *testing.T) {
|
||||
f := parsedField(t, "0"+solvedPuzzle[1:])
|
||||
solver := &logic.Solver{}
|
||||
if err := solver.InitStrategies(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
solved, err := solver.Run(0)
|
||||
if err != nil || solved {
|
||||
t.Fatalf("Run() = (%v, %v), want (false, nil)", solved, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSolverRejectsInvalidStateAndStrategyIndex(t *testing.T) {
|
||||
solver := &logic.Solver{}
|
||||
if _, err := solver.Run(0); !errors.Is(err, field.ErrInvalidField) {
|
||||
t.Fatalf("Run() before initialization error = %v", err)
|
||||
}
|
||||
if err := solver.InitStrategies(nil); !errors.Is(err, field.ErrInvalidField) {
|
||||
t.Fatalf("InitStrategies(nil) error = %v", err)
|
||||
}
|
||||
|
||||
f := parsedField(t, "0"+solvedPuzzle[1:])
|
||||
if err := solver.InitStrategies(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := solver.Run(1); err == nil {
|
||||
t.Fatal("Run() with invalid strategy index error = nil")
|
||||
}
|
||||
}
|
||||
@@ -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 //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
@@ -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 //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
@@ -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 //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user