Refactor Sudoku solver architecture

This commit is contained in:
2026-09-10 23:11:34 +02:00
parent 7bf4a8d725
commit a5ec338277
21 changed files with 475 additions and 260 deletions
+2
View File
@@ -25,3 +25,5 @@ go.work.sum
# env file # env file
.env .env
# Local instructions for coding agents
AGENTS.md
+112 -1
View File
@@ -1,3 +1,114 @@
# Sudoku # Sudoku
Sudoku-Puzzle-Löser mit Lösungsweg Ein in Go geschriebener Sudoku-Löser, der Rätsel mit nachvollziehbaren,
menschlichen Lösungsstrategien bearbeiten soll. Neben dem gelösten Feld soll
langfristig auch der Lösungsweg mit Kandidaten, Änderungen und Markierungen
verfügbar sein.
> [!WARNING]
> Das Projekt befindet sich in einer frühen Entwicklungsphase. Der aktuelle
> Prüflauf ist wegen eines Vet-Fehlers noch nicht grün und die Demo kann während des
> Lösens abstürzen. Siehe [Bekannte Einschränkungen](#bekannte-einschränkungen).
## Voraussetzungen
- Go 1.25.10 oder neuer (siehe `go.mod`)
- keine externen Go-Abhängigkeiten
## Ausführen
Die Demo in `main.go` liest derzeit fest das dritte Rätsel aus
`data/sudoku-exchange-puzzle-bank/easy3.txt`, zeigt das Ausgangsfeld an und
startet anschließend den Solver:
```sh
go run .
```
Der Pfad und die Rätselauswahl sind momentan noch nicht über
Kommandozeilenargumente konfigurierbar.
## Entwicklung
Alle Pakete bauen und testen:
```sh
go test ./...
```
Code formatieren und statisch prüfen:
```sh
gofmt -w main.go field/*.go parser/*.go logic/*.go logic/strategies/*.go sudoku/*.go
go vet ./...
```
Aktuell gibt es noch keine automatisierten Tests. Neue Funktionalität sollte
nach Möglichkeit mit paketnahen `*_test.go`-Tests ergänzt werden.
## Architektur
| Pfad | Aufgabe |
| --- | --- |
| `main.go` | Kleine Demo und Zusammenbau von Parser, Solver und Strategien |
| `field/` | Spielfeld, Zellen, Positionen, Zeilen, Spalten, Blöcke, Kandidaten und Änderungsprotokoll |
| `parser/` | Parser-Schnittstelle und Import des Sudoku-Exchange-Puzzle-Bank-Formats |
| `logic/` | Ablaufsteuerung des Solvers |
| `logic/strategies/` | Lösungsstrategien und gemeinsame Strategie-Basis |
| `sudoku/` | Fassade, die Parser, Feld und Solver zu einem Spiel verbindet |
| `data/` | Mitgelieferte Beispielrätsel samt eigener Herkunfts- und Lizenzhinweise |
| `todo.md` | Offene technische Ideen und Aufgaben |
Der Datenfluss der Demo ist:
```text
Puzzle-Bank-Datei -> parser.PuzzleBank -> field.Field
-> sudoku.Game -> logic.Solver
-> Strategien -> Änderungen am Feld
```
Eine Strategie implementiert `strategies.Strategy`. `SearchProgressableCells`
sammelt mögliche Änderungen, `ApplyAll` oder `ApplyNext` übernimmt sie in das
Feld. Wird eine Zahl gesetzt, entfernt der Solver den entsprechenden Kandidaten
aus der zugehörigen Zeile, Spalte und dem Block.
Derzeit in `main.go` aktiv:
- Kandidaten eintragen (`Notes`)
- letzte fehlende Zahl eines Bereichs (`LastDigit`)
- einzelner Kandidat einer Zelle (`NakedSingle`)
- nur einmal vorkommender Kandidat eines Bereichs (`HiddenSingle`)
Weitere Strategietypen sind bereits als Gerüste angelegt, aber noch nicht
implementiert.
## Eingabedaten
`parser.PuzzleBank` erwartet das Format der
[Sudoku Exchange Puzzle Bank](data/sudoku-exchange-puzzle-bank/README.md): pro
Zeile einen 12-stelligen Hash, 81 Ziffern für das Rätsel und eine
Schwierigkeitsbewertung. `0` steht für ein leeres Feld. Leere Zeilen werden
übersprungen; alle eingelesenen Rätsel werden im Parser gespeichert.
Die Datensätze unter `data/sudoku-exchange-puzzle-bank/` stehen unter der dort
beiliegenden separaten Lizenz.
## Bekannte Einschränkungen
- `field.Field.IsValid` und die `IsSolved`-Prüfungen einzelner Bereiche sind
noch nicht implementiert. Ein vollständig belegtes, aber ungültiges Feld kann
daher als gelöst gelten.
- `logic.Solver.GetSolutionPath` liefert noch keinen Lösungsweg.
- Mehrere fortgeschrittene Strategien sind nur als leere Typen vorhanden.
- `go test ./...` meldet aktuell in `field/field.go`, dass eine Ganzzahl direkt
in einen String konvertiert wird.
- Die Demo kann in `LastDigit.SearchProgressableCells` auf eine leere Zelle
zugreifen und dadurch abstürzen.
- Fehlertexte und Bezeichner sind derzeit teilweise deutsch, teilweise
englisch; einige öffentliche Namen enthalten noch Tippfehler.
## Lizenz
Der Programmcode steht unter der [GNU General Public License v3.0](LICENSE).
Für die mitgelieferten Rätseldaten gelten die Hinweise im jeweiligen
Unterverzeichnis.
-98
View File
@@ -1,98 +0,0 @@
package board
import (
"bytes"
"errors"
"fmt"
"strconv"
)
type Parser interface {
Parse(data []byte) error
GetField(i int) *Field
GetAllFields() []Field
}
type parserHelper struct {
fields []Field
}
func (ph *parserHelper) GetField(i int) *Field {
if i < 0 || i >= len(ph.fields) {
return &Field{}
}
return &ph.fields[i]
}
func (ph *parserHelper) GetAllFields() []Field {
if len(ph.fields) == 0 {
return nil
}
return ph.fields
}
// https://github.com/grantm/sudoku-exchange-puzzle-bank/tree/master
type PuzzleBank struct {
parserHelper
}
func (pb *PuzzleBank) Parse(data []byte) error {
lines := bytes.Split(data, []byte("\n"))
for _, line := range lines {
if len(line) == 0 { // Überspringe leere Zeile
continue
}
if len(line) != 99 {
return fmt.Errorf("Zeile hat die falsche länge. soll: 100, ist: %d", len(line))
}
//siehe Readme in github repo
sha1Hash := bytes.TrimSpace(line[0:12])
sudokuStr := string(line[13:94])
ratingStr := string(line[96:99])
//fmt.Println(sudokuStr)
var rating float64
_, err := fmt.Sscanf(ratingStr, "%f", &rating)
if err != nil {
return errors.New("can nor parse raiting")
}
field := Field{
rows: 9,
columns: 9,
blockRows: 3,
blockColumns: 3,
blockSizeRow: 3,
blockSizeColumn: 3,
rating: rating,
sha1: sha1Hash,
}
field.cells = make([][]Cell, field.rows)
for i := range field.cells {
field.cells[i] = make([]Cell, field.columns)
for j := range field.cells[i] {
number, err := strconv.Atoi(string(sudokuStr[i*9+j]))
if err != nil {
return fmt.Errorf("Kann Zeichen nicht in Zahl konvertieren")
}
field.cells[i][j].number = number
field.cells[i][j].Notes = &Notes{}
field.cells[i][j].Pos = &Position{
row: i,
column: j,
blockRow: i / field.blockRows,
blockColumn: j / field.blockColumns,
inBlockRow: i % field.blockSizeRow,
inBlockColumn: j % field.blockSizeColumn,
}
}
}
//fmt.Println(field.String())
pb.fields = append(pb.fields, field)
}
return nil
}
-83
View File
@@ -1,83 +0,0 @@
package board
import (
"fmt"
"strings"
)
func (f *Field) String() string {
//TODO: auf beliebige größen anpassen
var sb strings.Builder
// Oberer Rahmen
sb.WriteString("╔═══════╤═══════╤═══════╗\n")
for i := range f.cells {
sb.WriteString("║ ") // Linke Rahmenseite
for j, cell := range f.cells[i] {
// Wert ausgeben oder Punkt für 0
val := cell.number
if val == 0 {
sb.WriteString("·")
} else {
sb.WriteString(string('0' + val))
}
// Trennlinien zwischen Blöcken und Zellen
if (j+1)%3 == 0 {
if j < 8 {
sb.WriteString(" │ ")
} else {
sb.WriteString(" ║\n") // Rechte Rahmenseite + Zeilenumbruch
}
} else {
sb.WriteString(" ")
}
}
// Horizontale Trennlinien nach jeder 3. Zeile
if (i+1)%3 == 0 && i < 8 {
sb.WriteString("╟───────┼───────┼───────╢\n")
}
}
// Unterer Rahmen
sb.WriteString("╚═══════╧═══════╧═══════╝")
return sb.String()
}
func (f *Field) StringNotesForNumber(n int) string {
return "StringNotesForNumber is not implementet"
}
func (f *Field) StringNotes() string {
var str strings.Builder
fmt.Fprintf(&str, "Notes:\n")
f.ForEachCell(func(cell *Cell) {
fmt.Fprintf(&str, "Pos: %d/%d - Notes: %v\n", cell.Pos.row, cell.Pos.column, cell.Notes.numbers)
})
return str.String()
}
func (f *Field) IsSolved() bool {
result := true
f.ForEachCell(func(cell *Cell) {
if cell.number == 0 {
result = false
}
})
/* TODO:
if !f.isValid() {
return false
}
*/
return result
}
func (f *Field) IsValid() bool {
return false
}
+12 -5
View File
@@ -1,4 +1,4 @@
package board package field
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// NOTES STRUCTURE // // NOTES STRUCTURE //
@@ -11,11 +11,9 @@ type Notes struct {
} }
func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) { func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) {
for _, existing := range n.numbers { if slices.Contains(n.numbers, note) {
if existing == note {
return // Note already exists return // Note already exists
} }
}
field.changes = append(field.changes, Change{ field.changes = append(field.changes, Change{
Cell: cell, Cell: cell,
marks: marks, marks: marks,
@@ -65,6 +63,15 @@ type Cell struct {
Pos *Position Pos *Position
} }
func NewCell(number int, pos *Position) *Cell {
cell := Cell{
number: number,
Pos: pos,
Notes: &Notes{},
}
return &cell
}
func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) { func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) {
if c.number == n { if c.number == n {
return return
@@ -74,7 +81,7 @@ func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) {
return return
} }
if n <= 0 || n > field.rows || n > field.columns { if n <= 0 || n > field.props.Rows || n > field.props.Columns {
return return
} }
+1 -1
View File
@@ -1,4 +1,4 @@
package board package field
import ( import (
"image/color" "image/color"
+1 -1
View File
@@ -1,4 +1,4 @@
package board package field
import "errors" import "errors"
+129 -22
View File
@@ -1,24 +1,40 @@
package board package field
import ( import (
"fmt" "fmt"
"strings"
) )
// ────────────────────────────────────────────────────────────────────────────── //
// META STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Properties struct {
Rows int
Columns int
BlockRows int
BlockColumns int
BlockSizeRow int
BlockSizeColumn int
Rating float64
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// FIELD STRUCTURE // // FIELD STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
type Field struct { type Field struct {
rows int props *Properties
columns int
blockRows int
blockColumns int
blockSizeRow int
blockSizeColumn int
cells [][]Cell cells [][]Cell
changes []Change changes []Change
sha1 []byte }
rating float64
func New(props Properties, cells [][]Cell) *Field {
field := Field{
props: &props,
cells: cells,
}
return &field
} }
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -26,7 +42,7 @@ type Field struct {
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) GetRow(r int) (*Row, error) { func (f *Field) GetRow(r int) (*Row, error) {
if r >= f.rows || r < 0 { if r >= f.props.Rows || r < 0 {
return nil, outOfBound return nil, outOfBound
} }
@@ -43,14 +59,14 @@ func (f *Field) GetRow(r int) (*Row, error) {
} }
func (f *Field) GetColumn(c int) (*Column, error) { func (f *Field) GetColumn(c int) (*Column, error) {
if c >= f.columns || c < 0 { if c >= f.props.Columns || c < 0 {
return nil, outOfBound return nil, outOfBound
} }
// Performance // Performance
result := &Column{ result := &Column{
Line: Line{ Line: Line{
cells: make([]*Cell, 0, f.rows), cells: make([]*Cell, 0, f.props.Rows),
}, },
} }
@@ -63,20 +79,20 @@ func (f *Field) GetColumn(c int) (*Column, error) {
} }
func (f *Field) GetBlock(r, c int) (*Block, error) { func (f *Field) GetBlock(r, c int) (*Block, error) {
if r < 0 || r >= f.blockRows || c < 0 || c >= f.blockColumns { if r < 0 || r >= f.props.BlockRows || c < 0 || c >= f.props.BlockColumns {
return nil, outOfBound return nil, outOfBound
} }
startRow := r * f.blockSizeRow startRow := r * f.props.BlockSizeRow
startCol := c * f.blockSizeColumn startCol := c * f.props.BlockSizeColumn
block := &Block{ block := &Block{
cells: make([][]*Cell, f.blockSizeRow), cells: make([][]*Cell, f.props.BlockSizeRow),
} }
for row := range block.cells { for row := range block.cells {
// Effizientes Kopieren der Zeile // Effizientes Kopieren der Zeile
block.cells[row] = make([]*Cell, f.blockSizeColumn) block.cells[row] = make([]*Cell, f.props.BlockSizeColumn)
for column := range block.cells[row] { for column := range block.cells[row] {
block.cells[row][column] = &f.cells[startRow+row][startCol+column] block.cells[row][column] = &f.cells[startRow+row][startCol+column]
@@ -105,12 +121,16 @@ func (f *Field) GetEachPartAtPos(pos *Position) []Part {
} }
func (f *Field) GetCell(r, c int) (*Cell, error) { func (f *Field) GetCell(r, c int) (*Cell, error) {
if r > f.rows || c > f.columns { if r > f.props.Rows || c > f.props.Columns {
return nil, outOfBound return nil, outOfBound
} }
return &f.cells[r][c], nil return &f.cells[r][c], nil
} }
func (f *Field) GetRating() float64 {
return f.props.Rating
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// FOREACH FUNCTIONS // // FOREACH FUNCTIONS //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -137,7 +157,7 @@ func (f *Field) ForEachPartAtPos(pos *Position, fn func(part Part)) {
} }
func (f *Field) ForEachRow(fn func(row *Row)) { func (f *Field) ForEachRow(fn func(row *Row)) {
for i := range f.rows { for i := range f.props.Rows {
row, err := f.GetRow(i) row, err := f.GetRow(i)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
@@ -148,7 +168,7 @@ func (f *Field) ForEachRow(fn func(row *Row)) {
} }
func (f *Field) ForEachColumn(fn func(column *Column)) { func (f *Field) ForEachColumn(fn func(column *Column)) {
for i := range f.columns { for i := range f.props.Columns {
column, err := f.GetColumn(i) column, err := f.GetColumn(i)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
@@ -160,8 +180,8 @@ func (f *Field) ForEachColumn(fn func(column *Column)) {
} }
func (f *Field) ForEachBlock(fn func(block *Block)) { func (f *Field) ForEachBlock(fn func(block *Block)) {
for r := range f.blockRows { for r := range f.props.BlockRows {
for c := range f.blockColumns { for c := range f.props.BlockColumns {
block, err := f.GetBlock(r, c) block, err := f.GetBlock(r, c)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
@@ -198,3 +218,90 @@ func (f *Field) AddChange(eChange *ExternalChange) {
f.changes = append(f.changes, change) f.changes = append(f.changes, change)
} }
func (f *Field) SetRating(rating float64) {
f.props.Rating = rating
}
// ────────────────────────────────────────────────────────────────────────────── //
// STATE //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) String() string {
//TODO: auf beliebige größen anpassen
var sb strings.Builder
// Oberer Rahmen
sb.WriteString("╔═══════╤═══════╤═══════╗\n")
for i := range f.cells {
sb.WriteString("║ ") // Linke Rahmenseite
for j, cell := range f.cells[i] {
// Wert ausgeben oder Punkt für 0
val := cell.number
if val == 0 {
sb.WriteString("·")
} else {
sb.WriteString(string('0' + val))
}
// Trennlinien zwischen Blöcken und Zellen
if (j+1)%3 == 0 {
if j < 8 {
sb.WriteString(" │ ")
} else {
sb.WriteString(" ║\n") // Rechte Rahmenseite + Zeilenumbruch
}
} else {
sb.WriteString(" ")
}
}
// Horizontale Trennlinien nach jeder 3. Zeile
if (i+1)%3 == 0 && i < 8 {
sb.WriteString("╟───────┼───────┼───────╢\n")
}
}
// Unterer Rahmen
sb.WriteString("╚═══════╧═══════╧═══════╝")
return sb.String()
}
func (f *Field) StringNotesForNumber(n int) string {
return "StringNotesForNumber is not implementet"
}
func (f *Field) StringNotes() string {
var str strings.Builder
fmt.Fprintf(&str, "Notes:\n")
f.ForEachCell(func(cell *Cell) {
fmt.Fprintf(&str, "Pos: %d/%d - Notes: %v\n", cell.Pos.row, cell.Pos.column, cell.Notes.numbers)
})
return str.String()
}
func (f *Field) IsSolved() bool {
result := true
f.ForEachCell(func(cell *Cell) {
if cell.number == 0 {
result = false
}
})
/* TODO:
if !f.isValid() {
return false
}
*/
return result
}
func (f *Field) IsValid() bool {
//In jedem Part gibt es jede Zahl max ein mal
//Die Zahl kommt nicht in intersecting parts vor
return false
}
+1 -1
View File
@@ -1,4 +1,4 @@
package board package field
import ( import (
"fmt" "fmt"
+13 -1
View File
@@ -1,4 +1,4 @@
package board package field
type Position struct { type Position struct {
row int row int
@@ -9,6 +9,18 @@ type Position struct {
inBlockColumn int inBlockColumn int
} }
func NewPosition(row, column, blockRow, blockColumn, inBlockRow, inBlockColumn int) *Position {
pos := Position{
row: row,
column: column,
blockRow: blockRow,
blockColumn: blockColumn,
inBlockRow: inBlockRow,
inBlockColumn: inBlockColumn,
}
return &pos
}
func (p *Position) GetRow() int { func (p *Position) GetRow() int {
return p.row return p.row
} }
+7 -7
View File
@@ -1,7 +1,7 @@
package logic package logic
import ( import (
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
"git.kleiax.de/homepage/logic/strategies" "git.kleiax.de/homepage/logic/strategies"
) )
@@ -12,7 +12,7 @@ import (
type Solver struct { type Solver struct {
strategies []strategies.Strategy strategies []strategies.Strategy
returnTo int returnTo int
field *board.Field field *field.Field
conf struct { conf struct {
all bool all bool
repeat bool repeat bool
@@ -23,7 +23,7 @@ func (s *Solver) Add(strategy strategies.Strategy) {
s.strategies = append(s.strategies, strategy) s.strategies = append(s.strategies, strategy)
} }
func (s *Solver) InitStragies(field *board.Field) { func (s *Solver) InitStragies(field *field.Field) {
s.returnTo = 1 s.returnTo = 1
s.field = field s.field = field
for _, strategy := range s.strategies { for _, strategy := range s.strategies {
@@ -38,11 +38,11 @@ func (s *Solver) Run(i int) bool {
} }
for _, change := range s.strategies[j].ApplyAll() { for _, change := range s.strategies[j].ApplyAll() {
if change.Action != board.ActionSetNumber { if change.Action != field.ActionSetNumber {
continue continue
} }
s.field.ForEachPartAtPos(change.Cell.Pos, func(part board.Part) { s.field.ForEachPartAtPos(change.Cell.Pos, func(part field.Part) {
part.ForEachCell(func(cell *board.Cell) { part.ForEachCell(func(cell *field.Cell) {
cell.Notes.Remove(s.field, cell, change.Value, "remove note after insert of a number", nil) cell.Notes.Remove(s.field, cell, change.Value, "remove note after insert of a number", nil)
}) })
}) })
@@ -61,6 +61,6 @@ func (s *Solver) Search() {
} }
func (s *Solver) GetSolutionPath() []board.ExternalChange { func (s *Solver) GetSolutionPath() []field.ExternalChange {
return nil return nil
} }
+6 -6
View File
@@ -1,7 +1,7 @@
package strategies package strategies
import ( import (
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
) )
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -13,20 +13,20 @@ type LastDigit struct {
} }
func (ld *LastDigit) SearchProgressableCells() int { func (ld *LastDigit) SearchProgressableCells() int {
ld.field.ForEachPart(func(part board.Part) { ld.field.ForEachPart(func(part field.Part) {
missingNumbers := part.GetMissingNumbers() missingNumbers := part.GetMissingNumbers()
if len(missingNumbers) == 1 { if len(missingNumbers) == 1 {
var emptyCell *board.Cell var emptyCell *field.Cell
part.ForEachCell(func(cell *board.Cell) { part.ForEachCell(func(cell *field.Cell) {
if cell.GetNumber() == 0 { if cell.GetNumber() == 0 {
emptyCell = cell emptyCell = cell
} }
}) })
// fmt.Println(part) // 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) // fmt.Printf("Gefundene Zelle: %d/%d - %d, missungNumber: %v, Typ: %T\n", emptyCell.Pos.GetRow(), emptyCell.Pos.GetColumn(), emptyCell.GetNumber(), missingNumbers, part)
change := board.ExternalChange{ change := field.ExternalChange{
Cell: emptyCell, Cell: emptyCell,
Action: board.ActionSetNumber, Action: field.ActionSetNumber,
Value: missingNumbers[0], Value: missingNumbers[0],
From: emptyCell.GetNumber(), From: emptyCell.GetNumber(),
TriggerdBy: ld.getName(), TriggerdBy: ld.getName(),
+42
View File
@@ -1,5 +1,7 @@
package strategies package strategies
import "git.kleiax.de/homepage/field"
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// HIDDEN_SINGLE STRUCTURE // // HIDDEN_SINGLE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -8,6 +10,46 @@ type HiddenSingle struct {
Base Base
} }
func (hs *HiddenSingle) SearchProgressableCells() int {
hs.field.ForEachPart(func(part field.Part) {
m := make(map[int]int)
part.ForEachCell(func(cell *field.Cell) {
notes := cell.Notes.Get()
for note := range notes {
m[note]++
}
})
var hiddenSingles []int
for num, count := range m {
if count == 1 {
hiddenSingles = append(hiddenSingles, num)
}
}
for num := range hiddenSingles {
part.ForEachCell(func(cell *field.Cell) {
if cell.Notes.Has(num) {
change := field.ExternalChange{
Cell: cell,
Action: field.ActionSetNumber,
Value: num,
From: cell.GetNumber(),
TriggerdBy: hs.getName(),
}
hs.changes = append(hs.changes, change)
}
})
}
})
return len(hs.changes)
}
func (hs *HiddenSingle) getName() string {
return "Hidden Single"
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// HIDDEN_PAIR STRUCTURE // // HIDDEN_PAIR STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
+5 -5
View File
@@ -1,6 +1,6 @@
package strategies package strategies
import "git.kleiax.de/homepage/board" import "git.kleiax.de/homepage/field"
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// NAKED_SINGLE STRUCTURE // // NAKED_SINGLE STRUCTURE //
@@ -11,13 +11,13 @@ type NakedSingle struct {
} }
func (ns *NakedSingle) SearchProgressableCells() int { func (ns *NakedSingle) SearchProgressableCells() int {
ns.field.ForEachPart(func(part board.Part) { ns.field.ForEachPart(func(part field.Part) {
part.ForEachCell(func(cell *board.Cell) { part.ForEachCell(func(cell *field.Cell) {
candidates := cell.Notes.Get() candidates := cell.Notes.Get()
if len(candidates) == 1 && cell.GetNumber() == 0 { if len(candidates) == 1 && cell.GetNumber() == 0 {
change := board.ExternalChange{ change := field.ExternalChange{
Cell: cell, Cell: cell,
Action: board.ActionSetNumber, Action: field.ActionSetNumber,
Value: candidates[0], Value: candidates[0],
From: cell.GetNumber(), From: cell.GetNumber(),
TriggerdBy: ns.getName(), TriggerdBy: ns.getName(),
+5 -5
View File
@@ -1,7 +1,7 @@
package strategies package strategies
import ( import (
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
) )
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -13,8 +13,8 @@ type Notes struct {
} }
func (n *Notes) SearchProgressableCells() int { func (n *Notes) SearchProgressableCells() int {
n.field.ForEachRow(func(row *board.Row) { n.field.ForEachRow(func(row *field.Row) {
row.ForEachCell(func(cell *board.Cell) { row.ForEachCell(func(cell *field.Cell) {
column, _ := n.field.GetColumn(cell.Pos.GetColumn()) column, _ := n.field.GetColumn(cell.Pos.GetColumn())
block, _ := n.field.GetBlock(cell.Pos.GetBlockRow(), cell.Pos.GetBlockColumn()) block, _ := n.field.GetBlock(cell.Pos.GetBlockRow(), cell.Pos.GetBlockColumn())
candidates := intersection3(row.GetMissingNumbers(), column.GetMissingNumbers(), block.GetMissingNumbers()) candidates := intersection3(row.GetMissingNumbers(), column.GetMissingNumbers(), block.GetMissingNumbers())
@@ -22,9 +22,9 @@ func (n *Notes) SearchProgressableCells() int {
if cell.Notes.Has(note) || cell.GetNumber() != 0 { if cell.Notes.Has(note) || cell.GetNumber() != 0 {
continue continue
} }
ch := board.ExternalChange{ ch := field.ExternalChange{
Cell: cell, Cell: cell,
Action: board.ActionSetNote, Action: field.ActionSetNote,
Value: note, Value: note,
TriggerdBy: n.getName(), TriggerdBy: n.getName(),
Marks: nil, Marks: nil,
+12 -12
View File
@@ -3,7 +3,7 @@ package strategies
import ( import (
"fmt" "fmt"
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
) )
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -11,9 +11,9 @@ import (
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
type Strategy interface { type Strategy interface {
Init(f *board.Field) Init(f *field.Field)
ApplyAll() []board.ExternalChange ApplyAll() []field.ExternalChange
ApplyNext() board.ExternalChange ApplyNext() field.ExternalChange
Name() string Name() string
SearchProgressableCells() int SearchProgressableCells() int
} }
@@ -23,7 +23,7 @@ type Strategy interface {
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
type StrategyVisualization interface { type StrategyVisualization interface {
pointOut(f *board.Field) []board.Mark pointOut(f *field.Field) []field.Mark
} }
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -32,16 +32,16 @@ type StrategyVisualization interface {
type Base struct { type Base struct {
name string name string
field *board.Field field *field.Field
changes []board.ExternalChange changes []field.ExternalChange
} }
func (b *Base) Init(f *board.Field) { func (b *Base) Init(f *field.Field) {
b.field = f b.field = f
} }
func (b *Base) ApplyAll() []board.ExternalChange { func (b *Base) ApplyAll() []field.ExternalChange {
changesCopy := make([]board.ExternalChange, len(b.changes)) changesCopy := make([]field.ExternalChange, len(b.changes))
copy(changesCopy, b.changes) copy(changesCopy, b.changes)
for _, change := range b.changes { for _, change := range b.changes {
@@ -53,9 +53,9 @@ func (b *Base) ApplyAll() []board.ExternalChange {
return changesCopy return changesCopy
} }
func (b *Base) ApplyNext() board.ExternalChange { func (b *Base) ApplyNext() field.ExternalChange {
if len(b.changes) < 1 { if len(b.changes) < 1 {
return board.ExternalChange{} return field.ExternalChange{}
} }
changeCopy := b.changes[0] changeCopy := b.changes[0]
+3 -2
View File
@@ -5,9 +5,9 @@ import (
"io" "io"
"os" "os"
"git.kleiax.de/homepage/board"
"git.kleiax.de/homepage/logic" "git.kleiax.de/homepage/logic"
"git.kleiax.de/homepage/logic/strategies" "git.kleiax.de/homepage/logic/strategies"
"git.kleiax.de/homepage/parser"
"git.kleiax.de/homepage/sudoku" "git.kleiax.de/homepage/sudoku"
) )
@@ -16,7 +16,8 @@ func main() {
solver.Add(&strategies.Notes{}) solver.Add(&strategies.Notes{})
solver.Add(&strategies.LastDigit{}) solver.Add(&strategies.LastDigit{})
solver.Add(&strategies.NakedSingle{}) solver.Add(&strategies.NakedSingle{})
game, err := sudoku.New(&board.PuzzleBank{}, solver, openFile()) solver.Add(&strategies.HiddenSingle{})
game, err := sudoku.New(&parser.PuzzleBank{}, solver, openFile())
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)
+30
View File
@@ -0,0 +1,30 @@
package parser
import "git.kleiax.de/homepage/field"
//TODO: const für klassik sudoku board.Props
//TODO: funktion für [][]Cell generierung
type Parser interface {
Parse(data []byte) error
GetField(i int) *field.Field
GetAllFields() []field.Field
}
type ParserHelper struct {
fields []field.Field
}
func (ph *ParserHelper) GetField(i int) *field.Field {
if i < 0 || i >= len(ph.fields) {
return &field.Field{}
}
return &ph.fields[i]
}
func (ph *ParserHelper) GetAllFields() []field.Field {
if len(ph.fields) == 0 {
return nil
}
return ph.fields
}
+74
View File
@@ -0,0 +1,74 @@
package parser
import (
"bytes"
"errors"
"fmt"
"strconv"
"git.kleiax.de/homepage/field"
)
// https://github.com/grantm/sudoku-exchange-puzzle-bank/tree/master
type PuzzleBank struct {
ParserHelper
}
func (pb *PuzzleBank) Parse(data []byte) error {
lines := bytes.Split(data, []byte("\n"))
for _, line := range lines {
if len(line) == 0 { // Überspringe leere Zeile
continue
}
if len(line) != 99 {
return fmt.Errorf("Zeile hat die falsche länge. soll: 100, ist: %d", len(line))
}
//siehe Readme in github repo
//sha1Hash := bytes.TrimSpace(line[0:12]) // Wird aktuell nicht gebraucht, später zum sudoku vergleichen
sudokuStr := string(line[13:94])
ratingStr := string(line[96:99])
//fmt.Println(sudokuStr)
var rating float64
_, err := fmt.Sscanf(ratingStr, "%f", &rating)
if err != nil {
return errors.New("can nor parse raiting")
}
props := field.Properties{
Rows: 9,
Columns: 9,
BlockRows: 3,
BlockColumns: 3,
BlockSizeRow: 3,
BlockSizeColumn: 3,
Rating: rating,
}
cells := make([][]field.Cell, props.Rows)
for i := range cells {
cells[i] = make([]field.Cell, props.Columns)
for j := range cells[i] {
number, err := strconv.Atoi(string(sudokuStr[i*9+j]))
if err != nil {
return fmt.Errorf("Kann Zeichen nicht in Zahl konvertieren")
}
pos := field.NewPosition(
i,
j,
i/props.BlockRows,
j/props.BlockColumns,
i%props.BlockRows,
j%props.BlockColumns)
cells[i][j] = *field.NewCell(number, pos)
}
}
pb.fields = append(pb.fields, *field.New(props, cells))
}
return nil
}
+6 -5
View File
@@ -4,21 +4,22 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
"git.kleiax.de/homepage/logic" "git.kleiax.de/homepage/logic"
"git.kleiax.de/homepage/parser"
) )
type Game struct { type Game struct {
field *board.Field field *field.Field
solver logic.Solver solver logic.Solver
} }
func New(parser board.Parser, solver logic.Solver, data []byte) (*Game, error) { func New(parser parser.Parser, solver logic.Solver, data []byte) (*Game, error) {
err := parser.Parse(data) err := parser.Parse(data)
if err != nil { if err != nil {
return &Game{}, fmt.Errorf("can not create game: %w", err) return &Game{}, fmt.Errorf("can not create game: %w", err)
} }
return &Game{solver: solver, field: parser.GetField(0)}, nil return &Game{solver: solver, field: parser.GetField(2)}, nil
} }
func (g *Game) Solve() error { func (g *Game) Solve() error {
@@ -43,6 +44,6 @@ func (g *Game) isFinished() bool {
return g.field.IsSolved() return g.field.IsSolved()
} }
func (g *Game) GetField() board.Field { func (g *Game) GetField() field.Field {
return *g.field return *g.field
} }
+10 -1
View File
@@ -1,2 +1,11 @@
- Parser kann eigenes Package sein nur das Interface und die Helfer funktionen im Board lassen, jeder kann einen Parser für sseine Quelle schreiben. Vielleicht ein Standardparser in Board für gängige Typen oder Hilfsfunktionen
- Feld immer wieder auf Validität checken (keine doppelten Zahlen in einem Part) damit keine Fehler bei den Strategien auftauchen können - Feld immer wieder auf Validität checken (keine doppelten Zahlen in einem Part) damit keine Fehler bei den Strategien auftauchen können
- Validator in Logic oder Field?
- kommt in field
- field sollte flag haben ob vor einem change geprüft wird, ob das Feld danach noch valid ist
- Für die solver ein Interface implementieren, damit die folgenden Solver benutzt werden können:
- backtracking Solver
- dlx solver
- notes (menschlicher solver)
- Was passiert wenn zum Beispiel bei hiddenSingle ein Treffer zwei Changes auslöst? Muss ein Überprüfung her, ob die betreffende Zelle schon in einem vorheringen Change bearbeitet wird? Ich denka ja
- Debugging verbesser, Loglevel etc, überall sinnvolle Logs einbauen
- Flag oder so für sich selbst überprüfende Sachen, nur im debug modus nach jedem bisschen das Feld auf validität prüfen