Compare commits

...
5 Commits
36 changed files with 2403 additions and 739 deletions
+2
View File
@@ -25,3 +25,5 @@ go.work.sum
# env file
.env
# Local instructions for coding agents
AGENTS.md
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}"
}
]
}
+117 -1
View File
@@ -1,3 +1,119 @@
# 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.
> [!NOTE]
> Das Projekt befindet sich weiterhin in einer frühen Entwicklungsphase. Die
> Basis ist getestet und die Demo läuft, der menschliche Solver unterstützt aber
> bisher nur vier grundlegende Strategien.
## 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 ./...
```
Die Pakete `field`, `parser`, `logic`, `logic/strategies` und `sudoku` besitzen
Unit- und Regressionstests. Neue Funktionalität sollte weiterhin mit paketnahen,
vorzugsweise tabellengesteuerten `*_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 |
| [`ROADMAP.md`](ROADMAP.md) | Priorisierter Fahrplan bis zum stabilen Solver |
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 das Feldmodell den entsprechenden
Kandidaten automatisch und ohne Duplikate 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. Hash, Zeichenzahl, Ziffern, Rating und Ausgangsbelegung
werden geprüft. `0` steht für ein leeres Feld. LF und CRLF werden unterstützt,
leere Zeilen werden übersprungen.
Für einzelne Rätsel steht außerdem `parser.PuzzleString` zur Verfügung. Er
akzeptiert genau 81 Ziffern. Parser-Zugriffe liefern bei einem ungültigen Index
einen Fehler; bei mehreren Puzzle-Bank-Einträgen wird der Index explizit an
`sudoku.New` übergeben.
Die Datensätze unter `data/sudoku-exchange-puzzle-bank/` stehen unter der dort
beiliegenden separaten Lizenz.
## Bekannte Einschränkungen
- `logic.Solver.GetSolutionPath` liefert noch keinen Lösungsweg.
- Mehrere fortgeschrittene Strategien sind nur als leere Typen vorhanden.
- Der Solver besitzt noch kein gemeinsames Interface für menschliche,
Backtracking- und DLX-Implementierungen.
- Die Demo verwendet weiterhin einen fest kodierten Dateipfad und Puzzle-Index.
- Der alte öffentliche Bezeichner `InitStragies` bleibt vorerst als
Kompatibilitätsalias bestehen.
## 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.
+224
View File
@@ -0,0 +1,224 @@
# Entwicklungsfahrplan
Dieser Fahrplan führt das Projekt vom aktuellen Prototyp zu einem verlässlichen,
erklärbaren Sudoku-Löser. Die Reihenfolge ist absichtlich abhängigkeitsorientiert:
Neue Strategien bauen erst dann auf dem Solver auf, wenn Feldmodell, Änderungen
und Fortschrittserkennung korrekt und getestet sind.
## Zielbild
Das erste stabile Release soll:
- klassische 9x9-Sudokus sicher einlesen und validieren,
- jeden Lösungsschritt deterministisch und nachvollziehbar protokollieren,
- einfache und mittlere Rätsel mit menschlichen Strategien lösen,
- bei Stillstand einen klaren Status statt eines Panic oder Endloslaufs liefern,
- über eine kleine CLI mit Datei, Standard-Eingabe oder direkter Rätselzeichenfolge
nutzbar sein und
- durch Unit-, Integrations- und Regressionstests abgesichert sein.
Backtracking, DLX und sehr fortgeschrittene menschliche Strategien folgen auf
dieses Kern-Release.
## Phase 0 Lauffähige und getestete Basis (P0) ✅
Abgeschlossen am 10. September 2026.
Zuerst wird der bestehende Zustand stabilisiert. In dieser Phase werden keine
neuen Lösungsstrategien ergänzt.
- Test-Helfer für kleine Felder und 9x9-Rätsel anlegen.
- Regressionstests für die aktuell bekannten Abstürze und Logikfehler schreiben.
- `Field.ForEachCell` so korrigieren, dass echte Zellen statt Kopien übergeben
werden.
- `Field.GetCell` gegen negative Indizes und `row == Rows` beziehungsweise
`column == Columns` absichern.
- die Zahlenausgabe in `Field.String` ohne fehlerhafte int-zu-string-Konvertierung
implementieren.
- doppelte oder widersprüchliche Änderungen aus überlappenden Zeilen, Spalten
und Blöcken verhindern; damit auch den Nil-Panic in `LastDigit` beseitigen.
- die beiden Slice-Iterationsfehler in `HiddenSingle` korrigieren: Kandidatenwerte
statt Indizes auswerten.
- in `Cell.RemoveNumber` die korrekte Aktion `ActionRemoveNumber` protokollieren.
- Nil-Eingaben bei Feld, Zelle, Position und Änderung kontrolliert als Fehler
behandeln.
- Solver-Fortschritt so erkennen, dass ein Schritt weder fälschlich Stillstand
meldet noch endlos wiederholt wird.
Abgeschlossen, wenn:
- `go test ./...` und `go vet ./...` erfolgreich sind,
- `go run .` das ausgewählte Beispiel ohne Panic beendet und
- die bekannten Fehler jeweils einen Regressionstest besitzen.
## Phase 1 Verlässliches Feldmodell (P0) ✅
Abgeschlossen am 10. September 2026.
- `field.Properties` beim Erzeugen prüfen: positive Dimensionen, passende
Blockaufteilung und exakt passende Zellmatrix.
- Zahlenbereich und Position jeder Zelle prüfen; leere Zellen bleiben als `0`
repräsentiert.
- `Part.IsSolved` für Zeilen, Spalten und Blöcke implementieren.
- `Field.IsValid` implementieren: keine Duplikate, nur erlaubte Werte und
konsistente Dimensionen.
- `Field.IsSolved` an Vollständigkeit **und** Validität koppeln.
- Kandidaten-Invarianten festlegen: sortiert, eindeutig, im gültigen Bereich und
niemals an bereits belegten Zellen.
- Änderungen atomar anwenden und validieren. Ungültige Änderungen sollen einen
Fehler zurückgeben und das Feld unverändert lassen.
- lesenden Zugriff auf Eigenschaften und Änderungshistorie anbieten, ohne
interne Slices veränderbar nach außen zu geben.
- die fest codierte 9x9-Darstellung durch eine allgemeine Darstellung ersetzen
und `StringNotesForNumber` implementieren.
Abgeschlossen, wenn gültige, ungültige, unvollständige und gelöste Felder in
Tabellentests eindeutig unterschieden werden und keine Mutation die
Feld-Invarianten umgehen kann.
## Phase 2 Robuste Parser- und Game-API (P0) ✅
Abgeschlossen am 10. September 2026.
- Parserfehler vereinheitlichen und mit Zeilennummer sowie fehlerhaftem Feld
anreichern.
- Puzzle-Bank-Datensätze strikt prüfen: Hash, 81 Ziffern, Rating, Whitespace,
LF/CRLF und leere Zeilen.
- bei einem Parse-Fehler keine teilweise eingelesene Ergebnisliste übernehmen;
wiederholte `Parse`-Aufrufe dürfen alte Ergebnisse nicht ungewollt anhängen.
- `GetField` so ändern, dass ein ungültiger Index einen Fehler liefert statt
eines leeren, später abstürzenden Felds.
- die fest kodierte Auswahl `GetField(2)` aus `sudoku.New` entfernen. Das
ausgewählte Feld soll explizit übergeben oder vom Aufrufer gewählt werden.
- zusätzlich einen einfachen Parser für eine 81-stellige Rätselzeichenfolge
bereitstellen; er eignet sich für CLI, Tests und externe Nutzung.
- die Ausgangsbelegung bereits beim Erzeugen eines `Game` validieren.
Abgeschlossen, wenn fehlerhafte Eingaben ohne Panic verständliche Fehler liefern
und Parser sowie `Game` keine implizite Rätselauswahl mehr enthalten.
## Phase 3 Klare Solver-Architektur (P0/P1)
- ein Solver-Interface definieren, das später menschlichen Solver, Backtracking
und DLX austauschbar macht.
- `Run(int) (bool, error)` durch eine aussagekräftige Schritt-API ersetzen, zum Beispiel
mit den Zuständen `Progress`, `Solved`, `Stuck`, `Invalid` und `Failed`.
- Strategie-Reihenfolge, Wiederholungspunkt und `ApplyNext`/`ApplyAll` eindeutig
definieren.
- Tippfehler in öffentlichen Namen (`InitStragies`, `TriggerdBy`) kontrolliert
migrieren und alle Aufrufer anpassen.
Abgeschlossen, wenn derselbe Input stets dieselben Schritte erzeugt und jeder
Solverlauf genau einen überprüfbaren Endzustand besitzt.
## Phase 4 Menschlicher Basis-Solver (P1)
Die Strategien werden in aufsteigender Komplexität implementiert. Jede Strategie
benötigt positive, negative und Konflikt-/Überlappungstests sowie mindestens ein
komplettes Beispielrätsel.
1. Kandidaten erzeugen und pflegen (`Notes`)
2. `LastDigit`
3. `NakedSingle`
4. `HiddenSingle`
5. `NakedPair` und `NakedTriple`
6. `HiddenPair` und `HiddenTriple`
7. `LockedCandidateT1`, `LockedCandidateT2` und `LockedPair`
Abgeschlossen, wenn die mitgelieferten einfachen und mittleren Beispieldaten
ohne Raten lösbar sind und jeder Schritt seine auslösende Strategie nennt.
## Phase 5 Erklärbarer Lösungsweg und Undo (P1)
- Änderungen zu fachlichen Schritten gruppieren: eine Schlussfolgerung kann aus
einer gesetzten Zahl und mehreren entfernten Kandidaten bestehen.
- `GetSolutionPath` mit defensiven Kopien implementieren.
- pro Schritt Strategie, betroffene Zellen, alter/neuer Wert, Begründung und
optionale Markierungen speichern.
- `Mark` vollständig von außen lesbar und unabhängig von internen
`Change`-Zeigern gestalten.
- Undo/Redo auf Basis atomarer Schritte implementieren; `prevSolveStep` entweder
fertigstellen oder bis dahin entfernen.
- menschenlesbare Textausgabe und ein stabiles JSON-Format für Lösungswege
ergänzen.
Abgeschlossen, wenn ein gelöstes Feld vollständig zum Ausgangszustand
zurückgesetzt und durch Wiederanwenden desselben Pfads identisch rekonstruiert
werden kann.
## Phase 6 Fortgeschrittene Strategien (P2)
Erst nach stabiler Schritt- und Markierungslogik ergänzen:
1. `XWing`
2. `XYWing`, `XYZWing`, `WXYZWingBasic`
3. `Turbot2StringKite` und `TurbotSkyscraper`
4. `SimpleColoringT1`
5. `EmptyRectangle`
6. `UniqueRectangleT1`, `UniqueRectangleT4`, `UniqueRectangleT7`
7. `XChain`, `XYChain`, `XChainLoop`, `XYChainLoop`
8. `CRP`, nachdem die genaue fachliche Bedeutung und Akzeptanzkriterien
dokumentiert wurden
Für jede Technik wird zuerst ein isoliertes Testmuster festgelegt. Ein leerer
Typ ohne Verhalten gilt nicht als implementierte Strategie.
## Phase 7 Vollständige algorithmische Solver (P2)
- Backtracking-Solver mit Minimum-Remaining-Values-Heuristik implementieren.
- optional einen DLX-/Algorithm-X-Solver ergänzen.
- Eindeutigkeit einer Lösung prüfen und die Fälle keine, genau eine oder mehrere
Lösungen unterscheiden.
- algorithmische Solver als Referenz in Tests verwenden, um Ergebnisse des
menschlichen Solvers zu validieren.
- explizit konfigurieren, ob ein algorithmischer Solver nur prüft, als Fallback
dient oder direkt löst.
Abgeschlossen, wenn alle gültigen klassischen 9x9-Rätsel entweder gelöst oder
eindeutig als unlösbar/mehrdeutig klassifiziert werden.
## Phase 8 Kommandozeile und Bedienbarkeit (P2)
- Flags für Datei, Puzzle-Index, direkte Zeichenfolge und Standard-Eingabe
hinzufügen.
- Solverart und zugelassene Strategien auswählbar machen.
- Ausgabeoptionen für Ausgangsfeld, Endfeld, Einzelschritte und JSON anbieten.
- konsistente Exit-Codes für Eingabefehler, ungültige Rätsel, Stillstand und
erfolgreiche Lösung definieren.
- Bibliothekspakete frei von direkter Konsolenausgabe halten; strukturiertes,
optionales Logging nur an den Anwendungsgrenzen einführen.
- große Puzzle-Bank-Dateien streamen, wenn nur ein Datensatz benötigt wird.
Abgeschlossen, wenn das Programm ohne Quellcodeänderung mit eigenen Rätseln
verwendet werden kann.
## Phase 9 Qualität, Performance und Release (P2/P3)
- Fuzz-Tests für Parser, Koordinatenzugriffe und Änderungsanwendung ergänzen.
- Integrationssuite über repräsentative einfache, mittlere, schwere, ungültige
und mehrdeutige Rätsel aufbauen.
- `go test -race ./...` in den Standardprüfungen aufnehmen.
- Benchmarks für Kandidatenberechnung, Strategie-Suche und vollständige Solver
hinzufügen; erst danach gezielt optimieren.
- CI für Formatierung, Tests, Vet und Race Detector einrichten.
- öffentliche APIs mit GoDoc dokumentieren und Beispiele ergänzen.
- README nach Stabilisierung von den Warnungen befreien, Versionsschema und
Changelog festlegen und ein erstes Release markieren.
## Empfohlene nächste Arbeitspakete
Diese Tickets bilden die kürzeste Route zu einem stabilen Zwischenstand:
1. ein gemeinsames Solver-Interface und aussagekräftige Schrittzustände
entwerfen.
2. Strategie-Reihenfolge sowie `ApplyNext` und `ApplyAll` als öffentlichen
Vertrag festlegen und testen.
3. `InitStragies` und `TriggerdBy` kontrolliert auf korrekt geschriebene Namen
migrieren.
4. End-to-End-Tests für mehrere einfache und mittlere Puzzle-Bank-Rätsel
ergänzen.
5. anschließend `NakedPair`, `NakedTriple`, `HiddenPair` und `HiddenTriple`
implementieren.
Nach jedem Arbeitspaket müssen `gofmt`, `go test ./...` und `go vet ./...`
erfolgreich sein. Neue bekannte Baseline-Fehler sollen nicht angesammelt werden.
-110
View File
@@ -1,110 +0,0 @@
package board
// ────────────────────────────────────────────────────────────────────────────── //
// NOTES STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Notes struct {
numbers []int
}
func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) {
for _, existing := range n.numbers {
if existing == note {
return // Note already exists
}
}
field.changes = append(field.changes, Change{
Cell: cell,
marks: marks,
action: ActionSetNote,
value: note,
from: 0,
triggerdBy: trigger,
})
n.numbers = append(n.numbers, note)
}
func (n *Notes) Remove(field *Field, cell *Cell, note int, trigger string, marks []Mark) {
for i, existing := range n.numbers {
if existing == note {
field.changes = append(field.changes, Change{
Cell: cell,
marks: marks,
action: ActionRemoveNote,
value: 0,
from: note,
triggerdBy: trigger,
})
n.numbers = append(n.numbers[:i], n.numbers[i+1:]...)
return
}
}
}
func (n *Notes) Get() []int {
copySlice := make([]int, len(n.numbers))
copy(copySlice, n.numbers)
return copySlice
}
// ────────────────────────────────────────────────────────────────────────────── //
// CELL STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Cell struct {
number int
Notes *Notes
Pos *Position
}
func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) {
if c.number == n {
return
}
if field == nil {
return
}
if n <= 0 || n > field.rows || n > field.columns {
return
}
field.changes = append(field.changes, Change{
Cell: c,
marks: marks,
action: ActionSetNumber,
value: n,
from: c.number,
triggerdBy: trigger,
})
c.number = n
}
func (c *Cell) RemoveNumber(field *Field, trigger string, marks []Mark) {
if c.number == 0 {
return
}
if field == nil {
return
}
field.changes = append(field.changes, Change{
action: ActionSetNumber,
Cell: c,
value: 0,
from: c.number,
marks: marks,
triggerdBy: trigger,
})
c.number = 0
}
func (c *Cell) GetNumber() int {
return c.number
}
-106
View File
@@ -1,106 +0,0 @@
package board
import "image/color"
// ────────────────────────────────────────────────────────────────────────────── //
// MARK STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Mark struct {
Cell *Cell
Change *Change
color color.Color
}
func (m *Mark) GetColor() color.Color {
return m.color
}
// ────────────────────────────────────────────────────────────────────────────── //
// CHANGE_ACTION TYPE //
// ────────────────────────────────────────────────────────────────────────────── //
type ChangeAction int
const (
ActionSetNumber = iota
ActionSetNote
ActionRemoveNumber
ActionRemoveNote
)
// ────────────────────────────────────────────────────────────────────────────── //
// CHANGE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type ExternalChange struct {
Cell *Cell
Marks []Mark
Action ChangeAction
Value int
From int
TriggerdBy string //strategy or manuell
}
// ────────────────────────────────────────────────────────────────────────────── //
// CHANGE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Change struct {
Cell *Cell
marks []Mark
action ChangeAction
value int
from int
triggerdBy string //strategy or manuell
}
// ────────────────────────────────────────────────────────────────────────────── //
// GETTER //
// ────────────────────────────────────────────────────────────────────────────── //
func (c *Change) GetMarks() []Mark {
return c.marks
}
func (c *Change) GetAction() ChangeAction {
return c.action
}
func (c *Change) GetTo() int {
return c.value
}
func (c *Change) GetFrom() int {
return c.from
}
func (c *Change) GetTriggeredBy() string {
return c.triggerdBy
}
// ────────────────────────────────────────────────────────────────────────────── //
// ??? //
// ────────────────────────────────────────────────────────────────────────────── //
func (c *Change) do() {
switch c.action {
case ActionSetNumber:
c.Cell.number = c.value
case ActionSetNote:
c.Cell.Notes.numbers = append(c.Cell.Notes.numbers, c.value)
case ActionRemoveNumber:
c.Cell.number = 0
case ActionRemoveNote:
for i, existing := range c.Cell.Notes.numbers {
if existing == c.from {
c.Cell.Notes.numbers = append(c.Cell.Notes.numbers[:i], c.Cell.Notes.numbers[i+1:]...)
return
}
}
}
}
-8
View File
@@ -1,8 +0,0 @@
package board
import "errors"
var (
outOfBound = errors.New("invalid coordinates")
invalidField = errors.New("can not parse Field")
)
-161
View File
@@ -1,161 +0,0 @@
package board
import (
"fmt"
)
// ────────────────────────────────────────────────────────────────────────────── //
// FIELD STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Field struct {
rows int
columns int
blockRows int
blockColumns int
blockSizeRow int
blockSizeColumn int
cells [][]Cell
changes []Change
sha1 []byte
rating float64
}
// ────────────────────────────────────────────────────────────────────────────── //
// GETTER //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) GetRow(r int) (*Line, error) {
if r >= f.rows || r < 0 {
return nil, outOfBound
}
return &Line{cells: append([]Cell(nil), f.cells[r]...)}, nil
}
func (f *Field) GetColumn(c int) (*Line, error) {
if c >= f.columns || c < 0 {
return nil, outOfBound
}
// Performance
result := &Line{
cells: make([]Cell, 0, f.rows),
}
// Copying
for _, row := range f.cells {
result.cells = append(result.cells, row[c])
}
return result, nil
}
func (f *Field) GetBlock(r, c int) (*Block, error) {
if r < 0 || r >= f.blockRows || c < 0 || c >= f.blockColumns {
return nil, outOfBound
}
startRow := r * f.blockSizeRow
startCol := c * f.blockSizeColumn
block := &Block{
cells: make([][]Cell, f.blockSizeRow),
}
for row := range block.cells {
// Effizientes Kopieren der Zeile
block.cells[row] = make([]Cell, f.blockSizeColumn)
copy(block.cells[row], f.cells[startRow+row][startCol:startCol+f.blockSizeColumn])
}
return block, nil
}
func (f *Field) GetCell(r, c int) (*Cell, error) {
if r > f.rows || c > f.columns {
return nil, outOfBound
}
return &f.cells[r][c], nil
}
// ────────────────────────────────────────────────────────────────────────────── //
// FOREACH FUNCTIONS //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) ForEachPart(fn func(part Part)) {
f.ForEachRow(func(line *Line) {
fn(line)
})
f.ForEachColumn(func(line *Line) {
fn(line)
})
f.ForEachBlock(func(block *Block) {
fn(block)
})
}
func (f *Field) ForEachRow(fn func(line *Line)) {
for i := range f.rows {
line, err := f.GetRow(i)
if err != nil {
fmt.Println(err.Error())
return
}
fn(line)
}
}
func (f *Field) ForEachColumn(fn func(line *Line)) {
for i := range f.columns {
column, err := f.GetColumn(i)
if err != nil {
fmt.Println(err.Error())
return
}
fn(column)
}
}
func (f *Field) ForEachBlock(fn func(block *Block)) {
for r := range f.blockRows {
for c := range f.blockColumns {
block, err := f.GetBlock(r, c)
if err != nil {
fmt.Println(err.Error())
return
}
fn(block)
}
}
}
func (f *Field) ForEachCell(fn func(cell *Cell)) {
for _, row := range f.cells {
for _, cell := range row {
fn(&cell)
}
}
}
// ────────────────────────────────────────────────────────────────────────────── //
// MODIFIER //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) AddChange(eChange *ExternalChange) {
change := Change{
Cell: eChange.Cell,
marks: eChange.Marks,
action: eChange.Action,
value: eChange.Value,
from: eChange.From,
triggerdBy: eChange.TriggerdBy,
}
change.do()
f.changes = append(f.changes, change)
}
-97
View File
@@ -1,97 +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].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
}
-91
View File
@@ -1,91 +0,0 @@
package board
type Part interface {
IsSolved() bool
ForEachCell(fn func(cell *Cell))
GetMissingNumbers() []int
RemoveNote(field *Field, note int, trigger string, marks []Mark)
}
// ────────────────────────────────────────────────────────────────────────────── //
// LINE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Line struct {
cells []Cell
}
func (l *Line) IsSolved() bool {
//TODO
return false
}
func (l *Line) ForEachCell(fn func(cell *Cell)) {
for _, cell := range l.cells {
fn(&cell)
}
}
func (l *Line) GetMissingNumbers() []int {
return getMissingNumbersHelper(len(l.cells), l.ForEachCell)
}
func (l *Line) RemoveNote(field *Field, note int, trigger string, marks []Mark) {
l.ForEachCell(func(cell *Cell) {
cell.Notes.Remove(field, cell, note, trigger, marks)
})
}
// ────────────────────────────────────────────────────────────────────────────── //
// BLOCK STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Block struct {
cells [][]Cell
}
func (b *Block) IsSolved() bool {
//TODO
return false
}
func (b *Block) ForEachCell(fn func(cell *Cell)) {
for _, row := range b.cells {
for _, cell := range row {
fn(&cell)
}
}
}
func (b *Block) GetMissingNumbers() []int {
max := len(b.cells) * len(b.cells[0])
return getMissingNumbersHelper(max, b.ForEachCell)
}
func (b *Block) RemoveNote(field *Field, note int, trigger string, marks []Mark) {
b.ForEachCell(func(cell *Cell) {
cell.Notes.Remove(field, cell, note, trigger, marks)
})
}
// ────────────────────────────────────────────────────────────────────────────── //
// HELPER //
// ────────────────────────────────────────────────────────────────────────────── //
func getMissingNumbersHelper(max int, iterate func(fn func(cell *Cell))) []int {
present := make(map[int]bool, max)
iterate(func(cell *Cell) {
if cell.number != 0 {
present[cell.number] = true
}
})
var missing []int
for i := 1; i <= max; i++ {
if !present[i] {
missing = append(missing, i)
}
}
return missing
}
-67
View File
@@ -1,67 +0,0 @@
package board
import "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) 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
}
+1 -1
View File
@@ -1,3 +1,3 @@
0000183b305c 451723860007000800000816000000030000005000100730040086906000204840572093000409000 1.2
0000183b305c 050703060007000800000816000000030000005000100730040086906000204840572093000409000 1.2
0001d5d6314e 302401809001000300000000000040708010780502036000090000200609003900000008800070005 1.2
000212406270 000823001003000400070000052300960010000102000010038006830000040002000900600789000 1.2
+77
View File
@@ -0,0 +1,77 @@
package field
import "slices"
type Notes struct {
numbers []int
}
func (n *Notes) Add(f *Field, cell *Cell, note int, trigger string, marks []Mark) error {
if n == nil || cell == nil || cell.notes != n {
return ErrInvalidCell
}
return f.AddChange(&ExternalChange{Cell: cell, Marks: marks, Action: ActionSetNote, Value: note, TriggerdBy: trigger})
}
func (n *Notes) Remove(f *Field, cell *Cell, note int, trigger string, marks []Mark) error {
if n == nil || cell == nil || cell.notes != n {
return ErrInvalidCell
}
return f.AddChange(&ExternalChange{Cell: cell, Marks: marks, Action: ActionRemoveNote, From: note, TriggerdBy: trigger})
}
func (n *Notes) Get() []int {
if n == nil {
return nil
}
return slices.Clone(n.numbers)
}
func (n *Notes) Has(number int) bool {
return n != nil && slices.Contains(n.numbers, number)
}
type Cell struct {
number int
notes *Notes
pos *Position
}
func NewCell(number int, pos *Position) *Cell {
return &Cell{number: number, notes: &Notes{}, pos: pos}
}
func (c *Cell) SetNumber(f *Field, number int, trigger string, marks []Mark) error {
if c == nil {
return ErrInvalidCell
}
return f.AddChange(&ExternalChange{Cell: c, Marks: marks, Action: ActionSetNumber, Value: number, From: c.number, TriggerdBy: trigger})
}
func (c *Cell) RemoveNumber(f *Field, trigger string, marks []Mark) error {
if c == nil {
return ErrInvalidCell
}
return f.AddChange(&ExternalChange{Cell: c, Marks: marks, Action: ActionRemoveNumber, From: c.number, TriggerdBy: trigger})
}
func (c *Cell) GetNumber() int {
if c == nil {
return 0
}
return c.number
}
func (c *Cell) GetNotes() *Notes {
if c == nil {
return nil
}
return c.notes
}
func (c *Cell) GetPosition() *Position {
if c == nil {
return nil
}
return c.pos
}
+234
View File
@@ -0,0 +1,234 @@
package field
import (
"fmt"
"image/color"
"slices"
)
type Mark struct {
Cell *Cell
Change *Change
color color.Color
}
func (m *Mark) GetColor() color.Color {
if m == nil {
return nil
}
return m.color
}
type ChangeAction int
const (
ActionSetNumber ChangeAction = iota
ActionSetNote
ActionRemoveNumber
ActionRemoveNote
)
type ExternalChange struct {
Cell *Cell
Marks []Mark
Action ChangeAction
Value int
From int
TriggerdBy string
}
type Change struct {
Cell *Cell
marks []Mark
action ChangeAction
value int
from int
triggerdBy string
}
func (c *Change) GetMarks() []Mark {
if c == nil {
return nil
}
return slices.Clone(c.marks)
}
func (c *Change) GetAction() ChangeAction {
if c == nil {
return ActionSetNumber
}
return c.action
}
func (c *Change) GetTo() int {
if c == nil {
return 0
}
return c.value
}
func (c *Change) GetFrom() int {
if c == nil {
return 0
}
return c.from
}
func (c *Change) GetTriggeredBy() string {
if c == nil {
return ""
}
return c.triggerdBy
}
func (f *Field) AddChange(external *ExternalChange) error {
if err := f.validateChange(external); err != nil {
return err
}
cell := external.Cell
switch external.Action {
case ActionSetNumber:
if cell.number == external.Value {
return nil
}
cell.number = external.Value
f.recordChange(external)
f.clearNotes(cell, external.TriggerdBy)
f.removePeerNote(cell, external.Value, external.TriggerdBy)
case ActionSetNote:
if cell.notes.Has(external.Value) {
return nil
}
cell.notes.numbers = append(cell.notes.numbers, external.Value)
slices.Sort(cell.notes.numbers)
f.recordChange(external)
case ActionRemoveNumber:
if cell.number == 0 {
return nil
}
cell.number = 0
f.recordChange(external)
case ActionRemoveNote:
for i, existing := range cell.notes.numbers {
if existing == external.From {
cell.notes.numbers = slices.Delete(cell.notes.numbers, i, i+1)
f.recordChange(external)
break
}
}
}
return nil
}
func (f *Field) AddChanges(changes []ExternalChange) error {
if f == nil || !f.IsValid() {
return ErrInvalidField
}
state := f.snapshot()
for i := range changes {
if err := f.AddChange(&changes[i]); err != nil {
f.restore(state)
return fmt.Errorf("apply change %d: %w", i, err)
}
}
return nil
}
func (f *Field) GetChanges() []ExternalChange {
if f == nil {
return nil
}
result := make([]ExternalChange, len(f.changes))
for i := range f.changes {
result[i] = ExternalChange{Cell: f.changes[i].Cell, Marks: slices.Clone(f.changes[i].marks), Action: f.changes[i].action, Value: f.changes[i].value, From: f.changes[i].from, TriggerdBy: f.changes[i].triggerdBy}
}
return result
}
func (f *Field) validateChange(change *ExternalChange) error {
if f == nil || !f.IsValid() {
return ErrInvalidField
}
if change == nil || change.Cell == nil || !f.ownsCell(change.Cell) {
return fmt.Errorf("%w: cell does not belong to field", ErrInvalidChange)
}
cell := change.Cell
if cell.notes == nil || cell.pos == nil {
return fmt.Errorf("%w: incomplete cell", ErrInvalidChange)
}
switch change.Action {
case ActionSetNumber:
if change.Value < 1 || change.Value > f.props.Rows {
return fmt.Errorf("%w: number %d is out of range", ErrInvalidChange, change.Value)
}
if change.From != cell.number {
return fmt.Errorf("%w: expected previous value %d, got %d", ErrInvalidChange, cell.number, change.From)
}
if f.hasPeerNumber(cell, change.Value) {
return fmt.Errorf("%w: number %d already exists in a peer", ErrInvalidChange, change.Value)
}
case ActionSetNote:
if cell.number != 0 {
return fmt.Errorf("%w: cannot add a note to a filled cell", ErrInvalidChange)
}
if change.Value < 1 || change.Value > f.props.Rows || f.hasPeerNumber(cell, change.Value) {
return fmt.Errorf("%w: note %d is not a candidate", ErrInvalidChange, change.Value)
}
case ActionRemoveNumber:
if change.From != cell.number {
return fmt.Errorf("%w: expected previous value %d, got %d", ErrInvalidChange, cell.number, change.From)
}
case ActionRemoveNote:
if change.From < 1 || change.From > f.props.Rows {
return fmt.Errorf("%w: note %d is out of range", ErrInvalidChange, change.From)
}
default:
return fmt.Errorf("%w: unknown action %d", ErrInvalidChange, change.Action)
}
return nil
}
func (f *Field) recordChange(external *ExternalChange) {
f.changes = append(f.changes, Change{Cell: external.Cell, marks: slices.Clone(external.Marks), action: external.Action, value: external.Value, from: external.From, triggerdBy: external.TriggerdBy})
}
func (f *Field) clearNotes(cell *Cell, trigger string) {
for _, note := range slices.Clone(cell.notes.numbers) {
cell.notes.numbers = removeNumber(cell.notes.numbers, note)
f.recordChange(&ExternalChange{Cell: cell, Action: ActionRemoveNote, From: note, TriggerdBy: trigger})
}
}
func (f *Field) removePeerNote(cell *Cell, number int, trigger string) {
seen := make(map[*Cell]struct{})
parts, err := f.GetEachPartAtPos(cell.pos)
if err != nil {
return
}
for _, part := range parts {
part.ForEachCell(func(peer *Cell) {
if peer == cell {
return
}
if _, exists := seen[peer]; exists {
return
}
seen[peer] = struct{}{}
if peer.notes.Has(number) {
peer.notes.numbers = removeNumber(peer.notes.numbers, number)
f.recordChange(&ExternalChange{Cell: peer, Action: ActionRemoveNote, From: number, TriggerdBy: trigger})
}
})
}
}
func removeNumber(numbers []int, number int) []int {
for i, existing := range numbers {
if existing == number {
return slices.Delete(numbers, i, i+1)
}
}
return numbers
}
+10
View File
@@ -0,0 +1,10 @@
package field
import "errors"
var (
ErrOutOfBounds = errors.New("coordinates out of bounds")
ErrInvalidField = errors.New("invalid field")
ErrInvalidCell = errors.New("invalid cell")
ErrInvalidChange = errors.New("invalid change")
)
+461
View File
@@ -0,0 +1,461 @@
package field
import (
"fmt"
"slices"
"strconv"
"strings"
)
type Properties struct {
Rows int
Columns int
BlockRows int
BlockColumns int
BlockSizeRow int
BlockSizeColumn int
Rating float64
}
type Field struct {
props *Properties
cells [][]Cell
changes []Change
}
type fieldSnapshot struct {
numbers [][]int
notes [][][]int
changes []Change
}
func New(props Properties, cells [][]Cell) (*Field, error) {
if err := validateProperties(props); err != nil {
return nil, err
}
if len(cells) != props.Rows {
return nil, fmt.Errorf("%w: expected %d rows, got %d", ErrInvalidField, props.Rows, len(cells))
}
cloned := make([][]Cell, props.Rows)
for row := 0; row < props.Rows; row++ {
if len(cells[row]) != props.Columns {
return nil, fmt.Errorf("%w: row %d has %d cells, expected %d", ErrInvalidField, row, len(cells[row]), props.Columns)
}
cloned[row] = make([]Cell, props.Columns)
for column := 0; column < props.Columns; column++ {
source := &cells[row][column]
if source.pos == nil {
return nil, fmt.Errorf("%w: cell %d/%d has no position", ErrInvalidField, row, column)
}
if !positionMatches(source.pos, props, row, column) {
return nil, fmt.Errorf("%w: cell %d/%d has inconsistent position", ErrInvalidField, row, column)
}
if source.number < 0 || source.number > props.Rows {
return nil, fmt.Errorf("%w: cell %d/%d contains %d", ErrInvalidField, row, column, source.number)
}
if source.notes == nil {
return nil, fmt.Errorf("%w: cell %d/%d has no notes", ErrInvalidField, row, column)
}
notes, err := normalizeNotes(source.notes.numbers, props.Rows, source.number)
if err != nil {
return nil, fmt.Errorf("%w: cell %d/%d: %w", ErrInvalidField, row, column, err)
}
cloned[row][column] = Cell{
number: source.number,
notes: &Notes{numbers: notes},
pos: NewPosition(row, column, row/props.BlockSizeRow, column/props.BlockSizeColumn, row%props.BlockSizeRow, column%props.BlockSizeColumn),
}
}
}
result := &Field{props: &props, cells: cloned}
if !result.hasValidNumbers() || !result.hasValidNotes() {
return nil, fmt.Errorf("%w: duplicate values or invalid candidates", ErrInvalidField)
}
return result, nil
}
func validateProperties(props Properties) error {
if props.Rows <= 0 || props.Columns <= 0 || props.Rows != props.Columns {
return fmt.Errorf("%w: rows and columns must be equal and positive", ErrInvalidField)
}
if props.BlockRows <= 0 || props.BlockColumns <= 0 || props.BlockSizeRow <= 0 || props.BlockSizeColumn <= 0 {
return fmt.Errorf("%w: block dimensions must be positive", ErrInvalidField)
}
if props.BlockRows*props.BlockSizeRow != props.Rows || props.BlockColumns*props.BlockSizeColumn != props.Columns {
return fmt.Errorf("%w: block dimensions do not cover the field", ErrInvalidField)
}
if props.BlockSizeRow*props.BlockSizeColumn != props.Rows {
return fmt.Errorf("%w: each block must contain %d cells", ErrInvalidField, props.Rows)
}
return nil
}
func normalizeNotes(notes []int, max, cellNumber int) ([]int, error) {
if cellNumber != 0 && len(notes) != 0 {
return nil, fmt.Errorf("filled cells cannot contain notes")
}
result := slices.Clone(notes)
slices.Sort(result)
result = slices.Compact(result)
for _, note := range result {
if note < 1 || note > max {
return nil, fmt.Errorf("note %d is out of range", note)
}
}
return result, nil
}
func positionMatches(pos *Position, props Properties, row, column int) bool {
return pos.row == row && pos.column == column &&
pos.blockRow == row/props.BlockSizeRow && pos.blockColumn == column/props.BlockSizeColumn &&
pos.inBlockRow == row%props.BlockSizeRow && pos.inBlockColumn == column%props.BlockSizeColumn
}
func (f *Field) GetRow(row int) (*Row, error) {
if !f.hasStructure() || row < 0 || row >= f.props.Rows {
return nil, ErrOutOfBounds
}
cells := make([]*Cell, f.props.Columns)
for column := range f.cells[row] {
cells[column] = &f.cells[row][column]
}
return &Row{Line: Line{cells: cells}}, nil
}
func (f *Field) GetColumn(column int) (*Column, error) {
if !f.hasStructure() || column < 0 || column >= f.props.Columns {
return nil, ErrOutOfBounds
}
cells := make([]*Cell, f.props.Rows)
for row := range f.cells {
cells[row] = &f.cells[row][column]
}
return &Column{Line: Line{cells: cells}}, nil
}
func (f *Field) GetBlock(row, column int) (*Block, error) {
if !f.hasStructure() || row < 0 || row >= f.props.BlockRows || column < 0 || column >= f.props.BlockColumns {
return nil, ErrOutOfBounds
}
startRow := row * f.props.BlockSizeRow
startColumn := column * f.props.BlockSizeColumn
cells := make([][]*Cell, f.props.BlockSizeRow)
for blockRow := range cells {
cells[blockRow] = make([]*Cell, f.props.BlockSizeColumn)
for blockColumn := range cells[blockRow] {
cells[blockRow][blockColumn] = &f.cells[startRow+blockRow][startColumn+blockColumn]
}
}
return &Block{cells: cells}, nil
}
func (f *Field) GetEachPartAtPos(pos *Position) ([]Part, error) {
if pos == nil {
return nil, ErrOutOfBounds
}
row, err := f.GetRow(pos.row)
if err != nil {
return nil, err
}
column, err := f.GetColumn(pos.column)
if err != nil {
return nil, err
}
block, err := f.GetBlock(pos.blockRow, pos.blockColumn)
if err != nil {
return nil, err
}
return []Part{row, column, block}, nil
}
func (f *Field) GetCell(row, column int) (*Cell, error) {
if !f.hasStructure() || row < 0 || row >= f.props.Rows || column < 0 || column >= f.props.Columns {
return nil, ErrOutOfBounds
}
return &f.cells[row][column], nil
}
func (f *Field) GetProperties() (Properties, error) {
if !f.hasStructure() {
return Properties{}, ErrInvalidField
}
return *f.props, nil
}
func (f *Field) GetRating() float64 {
if !f.hasStructure() {
return 0
}
return f.props.Rating
}
func (f *Field) ForEachPart(fn func(Part)) {
if !f.hasStructure() || fn == nil {
return
}
f.ForEachRow(func(row *Row) { fn(row) })
f.ForEachColumn(func(column *Column) { fn(column) })
f.ForEachBlock(func(block *Block) { fn(block) })
}
func (f *Field) ForEachPartAtPos(pos *Position, fn func(Part)) error {
if fn == nil {
return nil
}
parts, err := f.GetEachPartAtPos(pos)
if err != nil {
return err
}
for _, part := range parts {
fn(part)
}
return nil
}
func (f *Field) ForEachRow(fn func(*Row)) {
if !f.hasStructure() || fn == nil {
return
}
for row := 0; row < f.props.Rows; row++ {
part, _ := f.GetRow(row)
fn(part)
}
}
func (f *Field) ForEachColumn(fn func(*Column)) {
if !f.hasStructure() || fn == nil {
return
}
for column := 0; column < f.props.Columns; column++ {
part, _ := f.GetColumn(column)
fn(part)
}
}
func (f *Field) ForEachBlock(fn func(*Block)) {
if !f.hasStructure() || fn == nil {
return
}
for row := 0; row < f.props.BlockRows; row++ {
for column := 0; column < f.props.BlockColumns; column++ {
part, _ := f.GetBlock(row, column)
fn(part)
}
}
}
func (f *Field) ForEachCell(fn func(*Cell)) {
if !f.hasStructure() || fn == nil {
return
}
for row := range f.cells {
for column := range f.cells[row] {
fn(&f.cells[row][column])
}
}
}
func (f *Field) SetRating(rating float64) {
if f != nil && f.props != nil {
f.props.Rating = rating
}
}
func (f *Field) String() string {
if !f.hasStructure() {
return "<invalid field>"
}
var result strings.Builder
for row := 0; row < f.props.Rows; row++ {
if row > 0 && row%f.props.BlockSizeRow == 0 {
result.WriteByte('\n')
}
for column := 0; column < f.props.Columns; column++ {
if column > 0 {
if column%f.props.BlockSizeColumn == 0 {
result.WriteString(" | ")
} else {
result.WriteByte(' ')
}
}
value := f.cells[row][column].number
if value == 0 {
result.WriteRune('·')
} else {
result.WriteString(strconv.Itoa(value))
}
}
if row+1 < f.props.Rows {
result.WriteByte('\n')
}
}
return result.String()
}
func (f *Field) StringNotesForNumber(number int) string {
if !f.hasStructure() || number < 1 || number > f.props.Rows {
return ""
}
var result strings.Builder
f.ForEachCell(func(cell *Cell) {
if cell.notes.Has(number) {
fmt.Fprintf(&result, "%d/%d\n", cell.pos.row, cell.pos.column)
}
})
return strings.TrimSuffix(result.String(), "\n")
}
func (f *Field) StringNotes() string {
if !f.hasStructure() {
return "Notes:"
}
var result strings.Builder
result.WriteString("Notes:\n")
f.ForEachCell(func(cell *Cell) {
fmt.Fprintf(&result, "Pos: %d/%d - Notes: %v\n", cell.pos.row, cell.pos.column, cell.notes.numbers)
})
return strings.TrimSuffix(result.String(), "\n")
}
func (f *Field) IsSolved() bool {
if !f.IsValid() {
return false
}
for row := range f.cells {
for column := range f.cells[row] {
if f.cells[row][column].number == 0 {
return false
}
}
}
return true
}
func (f *Field) IsValid() bool {
return f.hasStructure() && f.hasValidNumbers() && f.hasValidNotes()
}
func (f *Field) hasStructure() bool {
if f == nil || f.props == nil || validateProperties(*f.props) != nil || len(f.cells) != f.props.Rows {
return false
}
for row := range f.cells {
if len(f.cells[row]) != f.props.Columns {
return false
}
for column := range f.cells[row] {
cell := &f.cells[row][column]
if cell.notes == nil || cell.pos == nil || !positionMatches(cell.pos, *f.props, row, column) || cell.number < 0 || cell.number > f.props.Rows {
return false
}
}
}
return true
}
func (f *Field) hasValidNumbers() bool {
if !f.hasStructure() {
return false
}
valid := true
f.ForEachPart(func(part Part) {
seen := make(map[int]struct{}, f.props.Rows)
part.ForEachCell(func(cell *Cell) {
if cell.number == 0 || !valid {
return
}
if _, exists := seen[cell.number]; exists {
valid = false
return
}
seen[cell.number] = struct{}{}
})
})
return valid
}
func (f *Field) hasValidNotes() bool {
if !f.hasStructure() {
return false
}
valid := true
f.ForEachCell(func(cell *Cell) {
if !valid {
return
}
if cell.number != 0 && len(cell.notes.numbers) != 0 {
valid = false
return
}
previous := 0
for _, note := range cell.notes.numbers {
if note <= previous || note > f.props.Rows || f.hasPeerNumber(cell, note) {
valid = false
return
}
previous = note
}
})
return valid
}
func (f *Field) ownsCell(cell *Cell) bool {
if !f.hasStructure() || cell == nil || cell.pos == nil {
return false
}
row, column := cell.pos.row, cell.pos.column
return row >= 0 && row < f.props.Rows && column >= 0 && column < f.props.Columns && &f.cells[row][column] == cell
}
func (f *Field) hasPeerNumber(cell *Cell, number int) bool {
if !f.ownsCell(cell) {
return false
}
row, column := cell.pos.row, cell.pos.column
for index := 0; index < f.props.Columns; index++ {
if index != column && f.cells[row][index].number == number {
return true
}
}
for index := 0; index < f.props.Rows; index++ {
if index != row && f.cells[index][column].number == number {
return true
}
}
startRow := cell.pos.blockRow * f.props.BlockSizeRow
startColumn := cell.pos.blockColumn * f.props.BlockSizeColumn
for blockRow := 0; blockRow < f.props.BlockSizeRow; blockRow++ {
for blockColumn := 0; blockColumn < f.props.BlockSizeColumn; blockColumn++ {
peer := &f.cells[startRow+blockRow][startColumn+blockColumn]
if peer != cell && peer.number == number {
return true
}
}
}
return false
}
func (f *Field) snapshot() fieldSnapshot {
state := fieldSnapshot{numbers: make([][]int, len(f.cells)), notes: make([][][]int, len(f.cells)), changes: slices.Clone(f.changes)}
for row := range f.cells {
state.numbers[row] = make([]int, len(f.cells[row]))
state.notes[row] = make([][]int, len(f.cells[row]))
for column := range f.cells[row] {
state.numbers[row][column] = f.cells[row][column].number
state.notes[row][column] = slices.Clone(f.cells[row][column].notes.numbers)
}
}
return state
}
func (f *Field) restore(state fieldSnapshot) {
for row := range f.cells {
for column := range f.cells[row] {
f.cells[row][column].number = state.numbers[row][column]
f.cells[row][column].notes.numbers = slices.Clone(state.notes[row][column])
}
}
f.changes = slices.Clone(state.changes)
}
+244
View File
@@ -0,0 +1,244 @@
package field
import (
"errors"
"reflect"
"strings"
"testing"
)
const solvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
func classicCells(t *testing.T, digits string) (Properties, [][]Cell) {
t.Helper()
if len(digits) != 81 {
t.Fatalf("test puzzle has %d characters", len(digits))
}
props := Properties{Rows: 9, Columns: 9, BlockRows: 3, BlockColumns: 3, BlockSizeRow: 3, BlockSizeColumn: 3}
cells := make([][]Cell, props.Rows)
for row := range cells {
cells[row] = make([]Cell, props.Columns)
for column := range cells[row] {
position := NewPosition(row, column, row/3, column/3, row%3, column%3)
cells[row][column] = *NewCell(int(digits[row*9+column]-'0'), position)
}
}
return props, cells
}
func mustField(t *testing.T, digits string) *Field {
t.Helper()
props, cells := classicCells(t, digits)
result, err := New(props, cells)
if err != nil {
t.Fatalf("New() error = %v", err)
}
return result
}
func TestNewValidatesStructureAndValues(t *testing.T) {
props, cells := classicCells(t, strings.Repeat("0", 81))
tests := []struct {
name string
mutate func(*Properties, [][]Cell)
}{
{name: "missing row", mutate: func(_ *Properties, cells [][]Cell) { cells[0] = cells[0][:8] }},
{name: "wrong position", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].pos = NewPosition(1, 0, 0, 0, 0, 0) }},
{name: "number out of range", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].number = 10 }},
{name: "invalid blocks", mutate: func(props *Properties, _ [][]Cell) { props.BlockRows = 2 }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
localProps := props
localCells := make([][]Cell, len(cells))
for row := range cells {
localCells[row] = append([]Cell(nil), cells[row]...)
}
test.mutate(&localProps, localCells)
if _, err := New(localProps, localCells); !errors.Is(err, ErrInvalidField) {
t.Fatalf("New() error = %v, want ErrInvalidField", err)
}
})
}
duplicate := "113456789" + solvedPuzzle[9:]
duplicateProps, duplicateCells := classicCells(t, duplicate)
if _, err := New(duplicateProps, duplicateCells); !errors.Is(err, ErrInvalidField) {
t.Fatalf("New() duplicate error = %v, want ErrInvalidField", err)
}
}
func TestGetCellBoundsAndForEachCellPointers(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
for _, coordinates := range [][2]int{{-1, 0}, {0, -1}, {9, 0}, {0, 9}} {
if _, err := f.GetCell(coordinates[0], coordinates[1]); !errors.Is(err, ErrOutOfBounds) {
t.Errorf("GetCell(%d, %d) error = %v", coordinates[0], coordinates[1], err)
}
}
want, _ := f.GetCell(4, 5)
var found *Cell
f.ForEachCell(func(cell *Cell) {
if cell.GetPosition().GetRow() == 4 && cell.GetPosition().GetColumn() == 5 {
found = cell
}
})
if found != want {
t.Fatalf("ForEachCell() returned %p, want stored cell %p", found, want)
}
}
func TestValidityAndSolvedState(t *testing.T) {
incomplete := mustField(t, strings.Repeat("0", 81))
if !incomplete.IsValid() || incomplete.IsSolved() {
t.Fatalf("empty field: IsValid=%v IsSolved=%v", incomplete.IsValid(), incomplete.IsSolved())
}
solved := mustField(t, solvedPuzzle)
if !solved.IsValid() || !solved.IsSolved() {
t.Fatalf("solved field: IsValid=%v IsSolved=%v", solved.IsValid(), solved.IsSolved())
}
row, _ := solved.GetRow(0)
block, _ := solved.GetBlock(0, 0)
if !row.IsSolved() || !block.IsSolved() {
t.Fatal("completed row and block must be solved")
}
solved.cells[0][0].number = solved.cells[0][1].number
if solved.IsValid() || solved.IsSolved() {
t.Fatal("field with a duplicate must be invalid and unsolved")
}
}
func TestChangesAreValidatedAndAtomic(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
first, _ := f.GetCell(0, 0)
second, _ := f.GetCell(0, 1)
err := f.AddChanges([]ExternalChange{
{Cell: first, Action: ActionSetNumber, Value: 1, From: 0},
{Cell: second, Action: ActionSetNumber, Value: 1, From: 0},
})
if !errors.Is(err, ErrInvalidChange) {
t.Fatalf("AddChanges() error = %v, want ErrInvalidChange", err)
}
if first.GetNumber() != 0 || second.GetNumber() != 0 || len(f.GetChanges()) != 0 {
t.Fatal("failed change batch was not rolled back")
}
foreign := NewCell(0, NewPosition(0, 0, 0, 0, 0, 0))
if err := f.AddChange(&ExternalChange{Cell: foreign, Action: ActionSetNumber, Value: 1}); !errors.Is(err, ErrInvalidChange) {
t.Fatalf("foreign cell error = %v, want ErrInvalidChange", err)
}
if err := (*Field)(nil).AddChanges(nil); !errors.Is(err, ErrInvalidField) {
t.Fatalf("nil field error = %v, want ErrInvalidField", err)
}
}
func TestNotesStaySortedUniqueAndArePropagated(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
target, _ := f.GetCell(0, 0)
rowPeer, _ := f.GetCell(0, 1)
columnPeer, _ := f.GetCell(1, 0)
blockPeer, _ := f.GetCell(1, 1)
unrelated, _ := f.GetCell(4, 4)
for _, note := range []int{5, 1, 5} {
if err := target.GetNotes().Add(f, target, note, "test", nil); err != nil {
t.Fatalf("add target note: %v", err)
}
}
if got, want := target.GetNotes().Get(), []int{1, 5}; !reflect.DeepEqual(got, want) {
t.Fatalf("notes = %v, want %v", got, want)
}
for _, cell := range []*Cell{rowPeer, columnPeer, blockPeer, unrelated} {
if err := cell.GetNotes().Add(f, cell, 5, "test", nil); err != nil {
t.Fatalf("add peer note: %v", err)
}
}
if err := target.SetNumber(f, 5, "test", nil); err != nil {
t.Fatalf("SetNumber() error = %v", err)
}
for _, peer := range []*Cell{target, rowPeer, columnPeer, blockPeer} {
if peer.GetNotes().Has(5) {
row, column := peer.GetPosition().GetCoords()
t.Fatalf("note 5 was not removed at %d/%d", row, column)
}
}
if !unrelated.GetNotes().Has(5) {
t.Fatal("note was removed from an unrelated cell")
}
if f.IsValid() == false {
t.Fatal("field must remain valid after candidate propagation")
}
filled, _ := f.GetCell(0, 2)
if err := filled.SetNumber(f, 2, "test", nil); err != nil {
t.Fatal(err)
}
if err := filled.GetNotes().Add(f, filled, 3, "test", nil); !errors.Is(err, ErrInvalidChange) {
t.Fatalf("note on filled cell error = %v, want ErrInvalidChange", err)
}
if err := rowPeer.GetNotes().Add(f, rowPeer, 2, "test", nil); !errors.Is(err, ErrInvalidChange) {
t.Fatalf("invalid peer note error = %v, want ErrInvalidChange", err)
}
}
func TestRemoveNumberRecordsCorrectAction(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
cell, _ := f.GetCell(0, 0)
if err := cell.SetNumber(f, 1, "set", nil); err != nil {
t.Fatal(err)
}
if err := cell.RemoveNumber(f, "remove", nil); err != nil {
t.Fatal(err)
}
changes := f.GetChanges()
if changes[len(changes)-1].Action != ActionRemoveNumber {
t.Fatalf("last action = %v, want ActionRemoveNumber", changes[len(changes)-1].Action)
}
}
func TestStringRendersDigitsAndNotes(t *testing.T) {
f := mustField(t, "900000000"+strings.Repeat("0", 72))
cell, _ := f.GetCell(0, 1)
if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(f.String(), "9 · ·") {
t.Fatalf("String() = %q", f.String())
}
if got := f.StringNotesForNumber(1); got != "0/1" {
t.Fatalf("StringNotesForNumber() = %q, want 0/1", got)
}
}
func TestGettersReturnDefensiveCopies(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
props, err := f.GetProperties()
if err != nil {
t.Fatal(err)
}
props.Rows = 4
stored, _ := f.GetProperties()
if stored.Rows != 9 {
t.Fatal("GetProperties exposed internal properties")
}
cell, _ := f.GetCell(0, 0)
if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil {
t.Fatal(err)
}
notes := cell.GetNotes().Get()
notes[0] = 9
if !cell.GetNotes().Has(1) || cell.GetNotes().Has(9) {
t.Fatal("Notes.Get exposed its backing slice")
}
changes := f.GetChanges()
changes[0].Value = 9
if f.GetChanges()[0].Value != 1 {
t.Fatal("GetChanges exposed its backing slice")
}
}
+160
View File
@@ -0,0 +1,160 @@
package field
import (
"fmt"
"strings"
)
type Part interface {
IsSolved() bool
ForEachCell(func(*Cell))
GetMissingNumbers() []int
RemoveNote(*Field, int, string, []Mark) error
String() string
}
type Line struct {
cells []*Cell
}
func (l *Line) IsSolved() bool {
return partIsSolved(l)
}
func (l *Line) ForEachCell(fn func(*Cell)) {
if l == nil || fn == nil {
return
}
for _, cell := range l.cells {
fn(cell)
}
}
func (l *Line) GetMissingNumbers() []int {
if l == nil {
return nil
}
return getMissingNumbers(len(l.cells), l.ForEachCell)
}
func (l *Line) RemoveNote(f *Field, note int, trigger string, marks []Mark) error {
return removeNoteFromPart(l, f, note, trigger, marks)
}
func (l *Line) String() string {
return partString(l, "Line")
}
type Row struct {
Line
}
func (r *Row) String() string {
return partString(r, "Row")
}
type Column struct {
Line
}
func (c *Column) String() string {
return partString(c, "Column")
}
type Block struct {
cells [][]*Cell
}
func (b *Block) IsSolved() bool {
return partIsSolved(b)
}
func (b *Block) ForEachCell(fn func(*Cell)) {
if b == nil || fn == nil {
return
}
for _, row := range b.cells {
for _, cell := range row {
fn(cell)
}
}
}
func (b *Block) GetMissingNumbers() []int {
if b == nil || len(b.cells) == 0 {
return nil
}
size := 0
for _, row := range b.cells {
size += len(row)
}
return getMissingNumbers(size, b.ForEachCell)
}
func (b *Block) RemoveNote(f *Field, note int, trigger string, marks []Mark) error {
return removeNoteFromPart(b, f, note, trigger, marks)
}
func (b *Block) String() string {
return partString(b, "Block")
}
func partIsSolved(part Part) bool {
if part == nil {
return false
}
cellCount := 0
seen := make(map[int]struct{})
valid := true
part.ForEachCell(func(cell *Cell) {
cellCount++
if cell == nil || cell.number <= 0 {
valid = false
return
}
if _, exists := seen[cell.number]; exists {
valid = false
}
seen[cell.number] = struct{}{}
})
return valid && cellCount > 0 && len(seen) == cellCount
}
func removeNoteFromPart(part Part, f *Field, note int, trigger string, marks []Mark) error {
var firstErr error
part.ForEachCell(func(cell *Cell) {
if firstErr != nil || !cell.notes.Has(note) {
return
}
firstErr = cell.notes.Remove(f, cell, note, trigger, marks)
})
return firstErr
}
func partString(part Part, name string) string {
if part == nil {
return name + ":"
}
var result strings.Builder
fmt.Fprintf(&result, "%s:\t", name)
part.ForEachCell(func(cell *Cell) {
fmt.Fprintf(&result, "%d ", cell.GetNumber())
})
return strings.TrimSuffix(result.String(), " ")
}
func getMissingNumbers(max int, iterate func(func(*Cell))) []int {
present := make(map[int]struct{}, max)
iterate(func(cell *Cell) {
if cell != nil && cell.number != 0 {
present[cell.number] = struct{}{}
}
})
missing := make([]int, 0, max-len(present))
for number := 1; number <= max; number++ {
if _, exists := present[number]; !exists {
missing = append(missing, number)
}
}
return missing
}
+40 -1
View File
@@ -1,4 +1,4 @@
package board
package field
type Position struct {
row int
@@ -9,38 +9,77 @@ type Position struct {
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 {
if p == nil {
return 0
}
return p.row
}
func (p *Position) GetColumn() int {
if p == nil {
return 0
}
return p.column
}
func (p *Position) GetBlockRow() int {
if p == nil {
return 0
}
return p.blockRow
}
func (p *Position) GetBlockColumn() int {
if p == nil {
return 0
}
return p.blockColumn
}
func (p *Position) GetInBlockRow() int {
if p == nil {
return 0
}
return p.inBlockRow
}
func (p *Position) GetInBlockColumn() int {
if p == nil {
return 0
}
return p.inBlockColumn
}
func (p *Position) GetCoords() (int, int) {
if p == nil {
return 0, 0
}
return p.row, p.column
}
func (p *Position) GetBlockCoords() (int, int) {
if p == nil {
return 0, 0
}
return p.blockRow, p.blockColumn
}
func (p *Position) GetInBlockCoords() (int, int) {
if p == nil {
return 0, 0
}
return p.inBlockRow, p.inBlockColumn
}
-4
View File
@@ -1,4 +0,0 @@
package logic
type Change struct {
}
-1
View File
@@ -1 +0,0 @@
package logic
+66 -28
View File
@@ -1,46 +1,84 @@
package logic
import (
"git.kleiax.de/homepage/board"
"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
conf struct {
all bool
repeat bool
}
field *field.Field
}
func (s *Solver) Add(strategy strategies.Strategy) {
s.strategies = append(s.strategies, strategy)
if strategy != nil {
s.strategies = append(s.strategies, strategy)
}
}
func (s *Solver) InitStragies(field *board.Field) {
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(field)
strategy.Init(f)
}
}
func (s *Solver) Run(i int) bool {
//TODO: clean und Fehlerauffangen
s.strategies[i].SearchProgressableCells()
numberOfChanges := s.strategies[i].ApplyAll()
if numberOfChanges == 0 {
return false
}
return true
}
func (s *Solver) Search() {
}
func (s *Solver) GetSolutionPath() []board.ExternalChange {
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
}
progress := false
for index := start; index < len(s.strategies); index++ {
strategy := s.strategies[index]
if strategy.SearchProgressableCells() == 0 {
continue
}
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
}
if !progress {
return false, nil
}
start = 0
}
}
func (s *Solver) Search() {}
func (s *Solver) GetSolutionPath() []field.ExternalChange {
return nil
}
+71
View File
@@ -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")
}
}
+19 -9
View File
@@ -1,6 +1,8 @@
package strategies
import "git.kleiax.de/homepage/board"
import (
"git.kleiax.de/homepage/field"
)
// ────────────────────────────────────────────────────────────────────────────── //
// LAST_DIGIT STRUCTURE //
@@ -11,31 +13,39 @@ type LastDigit struct {
}
func (ld *LastDigit) SearchProgressableCells() int {
ld.field.ForEachPart(func(part board.Part) {
ld.field.ForEachPart(func(part field.Part) {
missingNumbers := part.GetMissingNumbers()
if len(missingNumbers) == 1 {
var emptyCell *board.Cell
part.ForEachCell(func(cell *board.Cell) {
var emptyCell *field.Cell
emptyCount := 0
part.ForEachCell(func(cell *field.Cell) {
if cell.GetNumber() == 0 {
emptyCell = cell
emptyCount++
}
})
change := board.ExternalChange{
if emptyCount != 1 {
return
}
change := field.ExternalChange{
Cell: emptyCell,
Action: board.ActionSetNumber,
Action: field.ActionSetNumber,
Value: missingNumbers[0],
From: emptyCell.GetNumber(),
TriggerdBy: ld.getName(),
}
ld.changes = append(ld.changes, change)
ld.queue(change)
}
})
//fmt.Printf("LastDigit Changes %d", len(ld.changes))
return len(ld.changes)
}
func (ld *LastDigit) getName() string {
return "Last Digit - Letzte Zahl"
return "Last Digit"
}
func (ld *LastDigit) Name() string {
return ld.getName()
}
// ────────────────────────────────────────────────────────────────────────────── //
+46
View File
@@ -1,5 +1,7 @@
package strategies
import "git.kleiax.de/homepage/field"
// ────────────────────────────────────────────────────────────────────────────── //
// HIDDEN_SINGLE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
@@ -8,6 +10,50 @@ type HiddenSingle struct {
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.GetNotes().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.GetNumber() == 0 && cell.GetNotes().Has(num) {
change := field.ExternalChange{
Cell: cell,
Action: field.ActionSetNumber,
Value: num,
From: cell.GetNumber(),
TriggerdBy: hs.getName(),
}
hs.queue(change)
}
})
}
})
return len(hs.changes)
}
func (hs *HiddenSingle) getName() string {
return "Hidden Single"
}
func (hs *HiddenSingle) Name() string {
return hs.getName()
}
// ────────────────────────────────────────────────────────────────────────────── //
// HIDDEN_PAIR STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
+16 -8
View File
@@ -1,6 +1,6 @@
package strategies
import "git.kleiax.de/homepage/board"
import "git.kleiax.de/homepage/field"
// ────────────────────────────────────────────────────────────────────────────── //
// NAKED_SINGLE STRUCTURE //
@@ -11,24 +11,32 @@ type NakedSingle struct {
}
func (ns *NakedSingle) SearchProgressableCells() int {
ns.field.ForEachPart(func(part board.Part) {
part.ForEachCell(func(cell *board.Cell) {
candidates := cell.Notes.Get()
if len(candidates) == 1 {
change := board.ExternalChange{
ns.field.ForEachPart(func(part field.Part) {
part.ForEachCell(func(cell *field.Cell) {
candidates := cell.GetNotes().Get()
if len(candidates) == 1 && cell.GetNumber() == 0 {
change := field.ExternalChange{
Cell: cell,
Action: board.ActionSetNumber,
Action: field.ActionSetNumber,
Value: candidates[0],
From: cell.GetNumber(),
TriggerdBy: ns.getName(),
}
ns.changes = append(ns.changes, change)
ns.queue(change)
}
})
})
return len(ns.changes)
}
func (ns *NakedSingle) getName() string {
return "Naked Single"
}
func (ns *NakedSingle) Name() string {
return ns.getName()
}
// ────────────────────────────────────────────────────────────────────────────── //
// NAKED_DOUBLE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
+89
View File
@@ -0,0 +1,89 @@
package strategies
import (
"sort"
"git.kleiax.de/homepage/field"
)
// ────────────────────────────────────────────────────────────────────────────── //
// NOTES STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Notes struct {
Base
}
func (n *Notes) SearchProgressableCells() int {
n.field.ForEachRow(func(row *field.Row) {
row.ForEachCell(func(cell *field.Cell) {
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.GetNotes().Has(note) || cell.GetNumber() != 0 {
continue
}
ch := field.ExternalChange{
Cell: cell,
Action: field.ActionSetNote,
Value: note,
TriggerdBy: n.getName(),
Marks: nil,
From: 0,
}
n.queue(ch)
}
})
})
return len(n.changes)
}
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)
for _, v := range a {
set[v] = true
}
// Nur Werte behalten, die auch in b vorkommen
inB := make(map[int]bool)
for _, v := range b {
inB[v] = true
}
for v := range set {
if !inB[v] {
delete(set, v)
}
}
// Nur Werte behalten, die auch in c vorkommen
inC := make(map[int]bool)
for _, v := range c {
inC[v] = true
}
for v := range set {
if !inC[v] {
delete(set, v)
}
}
result := make([]int, 0, len(set))
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)
}
}
}
+33 -17
View File
@@ -3,7 +3,7 @@ package strategies
import (
"fmt"
"git.kleiax.de/homepage/board"
"git.kleiax.de/homepage/field"
)
// ────────────────────────────────────────────────────────────────────────────── //
@@ -11,9 +11,9 @@ import (
// ────────────────────────────────────────────────────────────────────────────── //
type Strategy interface {
Init(f *board.Field)
ApplyAll() int
ApplyNext() bool
Init(f *field.Field)
ApplyAll() ([]field.ExternalChange, error)
ApplyNext() (field.ExternalChange, error)
Name() string
SearchProgressableCells() int
}
@@ -23,7 +23,7 @@ type Strategy interface {
// ────────────────────────────────────────────────────────────────────────────── //
type StrategyVisualization interface {
pointOut(f *board.Field) []board.Mark
pointOut(f *field.Field) []field.Mark
}
// ────────────────────────────────────────────────────────────────────────────── //
@@ -32,32 +32,48 @@ type StrategyVisualization interface {
type Base struct {
name string
field *board.Field
changes []board.ExternalChange
field *field.Field
changes []field.ExternalChange
}
func (b *Base) Init(f *board.Field) {
func (b *Base) Init(f *field.Field) {
b.field = f
}
func (b *Base) ApplyAll() int {
for _, change := range b.changes {
b.field.AddChange(&change)
func (b *Base) ApplyAll() ([]field.ExternalChange, error) {
changesCopy := make([]field.ExternalChange, len(b.changes))
copy(changesCopy, b.changes)
b.changes = b.changes[:0]
if err := b.field.AddChanges(changesCopy); err != nil {
return nil, err
}
return len(b.changes)
return changesCopy, nil
}
func (b *Base) ApplyNext() bool {
func (b *Base) ApplyNext() (field.ExternalChange, error) {
if len(b.changes) < 1 {
return false
return field.ExternalChange{}, nil
}
b.field.AddChange(&b.changes[0])
changeCopy := b.changes[0]
b.changes = b.changes[1:]
return true
if err := b.field.AddChange(&changeCopy); err != nil {
return field.ExternalChange{}, err
}
return changeCopy, nil
}
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 {
+4 -2
View File
@@ -5,17 +5,19 @@ import (
"io"
"os"
"git.kleiax.de/homepage/board"
"git.kleiax.de/homepage/logic"
"git.kleiax.de/homepage/logic/strategies"
"git.kleiax.de/homepage/parser"
"git.kleiax.de/homepage/sudoku"
)
func main() {
solver := logic.Solver{}
solver.Add(&strategies.Notes{})
solver.Add(&strategies.LastDigit{})
solver.Add(&strategies.NakedSingle{})
game, err := sudoku.New(&board.PuzzleBank{}, solver, openFile())
solver.Add(&strategies.HiddenSingle{})
game, err := sudoku.New(&parser.PuzzleBank{}, solver, openFile(), 2)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
+106
View File
@@ -0,0 +1,106 @@
package parser
import (
"errors"
"fmt"
"slices"
"strings"
"git.kleiax.de/homepage/field"
)
var (
ErrFieldIndex = errors.New("field index out of range")
ErrInvalidInput = errors.New("invalid puzzle input")
ErrNoPuzzles = errors.New("input contains no puzzles")
)
func ClassicProperties() field.Properties {
return field.Properties{
Rows: 9,
Columns: 9,
BlockRows: 3,
BlockColumns: 3,
BlockSizeRow: 3,
BlockSizeColumn: 3,
}
}
type Parser interface {
Parse([]byte) error
GetField(int) (*field.Field, error)
GetAllFields() []*field.Field
}
type ParserHelper struct {
fields []*field.Field
}
func (ph *ParserHelper) GetField(index int) (*field.Field, error) {
if ph == nil || index < 0 || index >= len(ph.fields) {
return nil, fmt.Errorf("%w: %d", ErrFieldIndex, index)
}
return ph.fields[index], nil
}
func (ph *ParserHelper) GetAllFields() []*field.Field {
if ph == nil {
return nil
}
return slices.Clone(ph.fields)
}
func classicField(digits string, rating float64) (*field.Field, error) {
props := ClassicProperties()
if len(digits) != props.Rows*props.Columns {
return nil, fmt.Errorf("%w: puzzle has %d characters, expected 81", ErrInvalidInput, len(digits))
}
props.Rating = rating
cells := make([][]field.Cell, props.Rows)
for row := 0; row < props.Rows; row++ {
cells[row] = make([]field.Cell, props.Columns)
for column := 0; column < props.Columns; column++ {
char := digits[row*props.Columns+column]
if char < '0' || char > '9' {
return nil, fmt.Errorf("%w: puzzle contains invalid character %q at position %d", ErrInvalidInput, char, row*props.Columns+column)
}
position := field.NewPosition(
row,
column,
row/props.BlockSizeRow,
column/props.BlockSizeColumn,
row%props.BlockSizeRow,
column%props.BlockSizeColumn,
)
cells[row][column] = *field.NewCell(int(char-'0'), position)
}
}
result, err := field.New(props, cells)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidInput, err)
}
return result, nil
}
// PuzzleString parses one classic Sudoku represented by exactly 81 digits.
// Whitespace around the complete string is ignored; 0 denotes an empty cell.
type PuzzleString struct {
ParserHelper
}
func (p *PuzzleString) Parse(data []byte) error {
if p == nil {
return errors.New("nil puzzle string parser")
}
digits := strings.TrimSpace(string(data))
if digits == "" {
return ErrNoPuzzles
}
parsed, err := classicField(digits, 0)
if err != nil {
return fmt.Errorf("parse puzzle string: %w", err)
}
p.fields = []*field.Field{parsed}
return nil
}
+110
View File
@@ -0,0 +1,110 @@
package parser
import (
"crypto/sha1"
"errors"
"fmt"
"strings"
"testing"
)
const validSolvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
func bankRecord(digits string, rating float64) string {
hash := fmt.Sprintf("%x", sha1.Sum([]byte(digits)))[:12]
return fmt.Sprintf("%s %s %.1f", hash, digits, rating)
}
func TestPuzzleBankParsesLFCRLFAndFinalLine(t *testing.T) {
first := "0" + validSolvedPuzzle[1:]
data := bankRecord(first, 1.2) + "\r\n\n" + bankRecord(validSolvedPuzzle, 2.5)
parser := &PuzzleBank{}
if err := parser.Parse([]byte(data)); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if got := len(parser.GetAllFields()); got != 2 {
t.Fatalf("field count = %d, want 2", got)
}
second, err := parser.GetField(1)
if err != nil {
t.Fatal(err)
}
if second.GetRating() != 2.5 || !second.IsSolved() {
t.Fatalf("second field: rating=%v solved=%v", second.GetRating(), second.IsSolved())
}
}
func TestPuzzleBankRejectsMalformedInput(t *testing.T) {
valid := bankRecord(validSolvedPuzzle, 1.2)
tests := map[string]string{
"empty": "\n\r\n",
"columns": "broken",
"hash length": "abc " + validSolvedPuzzle + " 1.2",
"hash mismatch": "ffffffffffff" + valid[12:],
"bad puzzle": bankRecord("x"+validSolvedPuzzle[1:], 1.2),
"bad rating": strings.TrimSuffix(valid, "1.2") + "nope",
}
for name, input := range tests {
t.Run(name, func(t *testing.T) {
if err := (&PuzzleBank{}).Parse([]byte(input)); err == nil || (name != "empty" && !errors.Is(err, ErrInvalidInput)) {
t.Fatalf("Parse() error = %v", err)
}
})
}
}
func TestPuzzleBankParseIsAtomicAndDoesNotAppend(t *testing.T) {
parser := &PuzzleBank{}
valid := bankRecord(validSolvedPuzzle, 1.2)
if err := parser.Parse([]byte(valid)); err != nil {
t.Fatal(err)
}
original, _ := parser.GetField(0)
if err := parser.Parse([]byte(valid + "\ninvalid")); err == nil {
t.Fatal("invalid Parse() error = nil")
}
afterFailure, _ := parser.GetField(0)
if afterFailure != original || len(parser.GetAllFields()) != 1 {
t.Fatal("failed parse changed previously parsed fields")
}
if err := parser.Parse([]byte(valid)); err != nil {
t.Fatal(err)
}
if len(parser.GetAllFields()) != 1 {
t.Fatal("repeated Parse() appended fields")
}
}
func TestParserHelperBoundsAndDefensiveSlice(t *testing.T) {
parser := &PuzzleString{}
if err := parser.Parse([]byte(validSolvedPuzzle)); err != nil {
t.Fatal(err)
}
for _, index := range []int{-1, 1} {
if _, err := parser.GetField(index); !errors.Is(err, ErrFieldIndex) {
t.Fatalf("GetField(%d) error = %v", index, err)
}
}
fields := parser.GetAllFields()
fields[0] = nil
if stored, err := parser.GetField(0); err != nil || stored == nil {
t.Fatal("GetAllFields exposed its backing slice")
}
}
func TestPuzzleStringValidatesInput(t *testing.T) {
parser := &PuzzleString{}
if err := parser.Parse([]byte(" \n" + validSolvedPuzzle + "\n")); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if fields := parser.GetAllFields(); len(fields) != 1 || !fields[0].IsSolved() {
t.Fatal("valid puzzle string was not parsed")
}
invalid := []string{"", validSolvedPuzzle[:80], "x" + validSolvedPuzzle[1:], "11" + validSolvedPuzzle[2:]}
for _, input := range invalid {
if err := (&PuzzleString{}).Parse([]byte(input)); err == nil {
t.Fatalf("Parse(%q) error = nil", input)
}
}
}
+67
View File
@@ -0,0 +1,67 @@
package parser
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"math"
"strconv"
"strings"
"git.kleiax.de/homepage/field"
)
type PuzzleBank struct {
ParserHelper
}
func (pb *PuzzleBank) Parse(data []byte) error {
if pb == nil {
return errors.New("nil puzzle bank parser")
}
var parsed []*field.Field
for index, rawLine := range bytes.Split(data, []byte("\n")) {
lineNumber := index + 1
line := strings.TrimSpace(string(rawLine))
if line == "" {
continue
}
columns := strings.Fields(line)
if len(columns) != 3 {
return fmt.Errorf("line %d: %w: expected hash, puzzle and rating", lineNumber, ErrInvalidInput)
}
if len(columns[0]) != 12 {
return fmt.Errorf("line %d: %w: hash has %d characters, expected 12", lineNumber, ErrInvalidInput, len(columns[0]))
}
if _, err := hex.DecodeString(columns[0]); err != nil {
return fmt.Errorf("line %d: %w: invalid hash: %v", lineNumber, ErrInvalidInput, err)
}
digest := sha1.Sum([]byte(columns[1]))
if columns[0] != hex.EncodeToString(digest[:])[:12] {
return fmt.Errorf("line %d: %w: hash does not match puzzle", lineNumber, ErrInvalidInput)
}
rating, err := strconv.ParseFloat(columns[2], 64)
if err != nil {
return fmt.Errorf("line %d: %w: invalid rating %q: %v", lineNumber, ErrInvalidInput, columns[2], err)
}
if rating < 0 || math.IsNaN(rating) || math.IsInf(rating, 0) {
return fmt.Errorf("line %d: %w: rating must be a finite non-negative number", lineNumber, ErrInvalidInput)
}
parsedField, err := classicField(columns[1], rating)
if err != nil {
return fmt.Errorf("line %d: %w", lineNumber, err)
}
parsed = append(parsed, parsedField)
}
if len(parsed) == 0 {
return ErrNoPuzzles
}
pb.fields = parsed
return nil
}
+34 -26
View File
@@ -4,45 +4,53 @@ import (
"errors"
"fmt"
"git.kleiax.de/homepage/board"
"git.kleiax.de/homepage/field"
"git.kleiax.de/homepage/logic"
"git.kleiax.de/homepage/parser"
)
type Game struct {
field *board.Field
field *field.Field
solver logic.Solver
}
func New(parser board.Parser, solver logic.Solver, data []byte) (*Game, error) {
err := parser.Parse(data)
if err != nil {
return &Game{}, fmt.Errorf("can not create game: %w", err)
func New(input parser.Parser, solver logic.Solver, data []byte, fieldIndex int) (*Game, error) {
if input == nil {
return nil, errors.New("cannot create game: nil parser")
}
return &Game{solver: solver, field: parser.GetField(0)}, nil
if err := input.Parse(data); err != nil {
return nil, fmt.Errorf("cannot create game: %w", err)
}
parsedField, err := input.GetField(fieldIndex)
if err != nil {
return nil, fmt.Errorf("cannot create game: %w", err)
}
if parsedField == nil || !parsedField.IsValid() {
return nil, fmt.Errorf("cannot create game: %w", field.ErrInvalidField)
}
return &Game{solver: solver, field: parsedField}, nil
}
func (g *Game) Solve() error {
g.solver.InitStragies(g.field)
for !g.isFinished() {
if !g.nextSolveStep() {
return errors.New("no strategy can solve the puzzle")
}
if g == nil || g.field == nil {
return field.ErrInvalidField
}
if err := g.solver.InitStrategies(g.field); err != nil {
return err
}
solved, err := g.solver.Run(0)
if err != nil {
return err
}
if !solved {
return logic.ErrNoProgress
}
return nil
}
func (g *Game) nextSolveStep() bool {
return g.solver.Run(0)
}
func (g *Game) prevSolveStep() {
// far far in the future
}
func (g *Game) isFinished() bool {
return g.field.IsSolved()
}
func (g *Game) GetField() board.Field {
return *g.field
func (g *Game) GetField() *field.Field {
if g == nil {
return nil
}
return g.field
}
+65
View File
@@ -0,0 +1,65 @@
package sudoku
import (
"crypto/sha1"
"errors"
"fmt"
"testing"
"git.kleiax.de/homepage/logic"
"git.kleiax.de/homepage/logic/strategies"
"git.kleiax.de/homepage/parser"
)
const solvedSudoku = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
func puzzleBankRecord(digits string, rating float64) string {
hash := fmt.Sprintf("%x", sha1.Sum([]byte(digits)))[:12]
return fmt.Sprintf("%s %s %.1f", hash, digits, rating)
}
func TestNewSelectsExplicitPuzzleIndex(t *testing.T) {
data := puzzleBankRecord("0"+solvedSudoku[1:], 1.0) + "\n" + puzzleBankRecord(solvedSudoku, 2.0)
game, err := New(&parser.PuzzleBank{}, logic.Solver{}, []byte(data), 1)
if err != nil {
t.Fatalf("New() error = %v", err)
}
if game.GetField().GetRating() != 2 || !game.GetField().IsSolved() {
t.Fatal("New() did not select puzzle index 1")
}
}
func TestNewRejectsInvalidIndexAndInput(t *testing.T) {
valid := puzzleBankRecord(solvedSudoku, 1.0)
if _, err := New(&parser.PuzzleBank{}, logic.Solver{}, []byte(valid), 1); !errors.Is(err, parser.ErrFieldIndex) {
t.Fatalf("index error = %v, want ErrFieldIndex", err)
}
if _, err := New(&parser.PuzzleString{}, logic.Solver{}, []byte("invalid"), 0); err == nil {
t.Fatal("invalid input error = nil")
}
}
func TestSolveCompletesSimplePuzzle(t *testing.T) {
solver := logic.Solver{}
solver.Add(&strategies.LastDigit{})
game, err := New(&parser.PuzzleString{}, solver, []byte("0"+solvedSudoku[1:]), 0)
if err != nil {
t.Fatal(err)
}
if err := game.Solve(); err != nil {
t.Fatalf("Solve() error = %v", err)
}
if !game.GetField().IsSolved() {
t.Fatal("field is not solved")
}
}
func TestSolveReportsNoProgress(t *testing.T) {
game, err := New(&parser.PuzzleString{}, logic.Solver{}, []byte("0"+solvedSudoku[1:]), 0)
if err != nil {
t.Fatal(err)
}
if err := game.Solve(); !errors.Is(err, logic.ErrNoProgress) {
t.Fatalf("Solve() error = %v, want ErrNoProgress", err)
}
}
+10 -1
View File
@@ -1 +1,10 @@
- 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
# Offene Punkte
Der priorisierte und aktuelle Stand befindet sich in [ROADMAP.md](ROADMAP.md).
- gemeinsames Interface für menschlichen Solver, Backtracking und DLX
implementieren
- strukturiertes, optionales Logging an den Anwendungsgrenzen ergänzen
- optionalen Diagnosemodus für zusätzliche Invariantenprüfungen entwickeln
- Lösungsweg, Gruppierung von Änderungen und Undo/Redo implementieren
- weitere menschliche Lösungsstrategien in der Reihenfolge der Roadmap ergänzen