Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7f9c16184 |
@@ -5,10 +5,10 @@ menschlichen Lösungsstrategien bearbeiten soll. Neben dem gelösten Feld soll
|
|||||||
langfristig auch der Lösungsweg mit Kandidaten, Änderungen und Markierungen
|
langfristig auch der Lösungsweg mit Kandidaten, Änderungen und Markierungen
|
||||||
verfügbar sein.
|
verfügbar sein.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!NOTE]
|
||||||
> Das Projekt befindet sich in einer frühen Entwicklungsphase. Der aktuelle
|
> Das Projekt befindet sich weiterhin in einer frühen Entwicklungsphase. Die
|
||||||
> Prüflauf ist wegen eines Vet-Fehlers noch nicht grün und die Demo kann während des
|
> Basis ist getestet und die Demo läuft, der menschliche Solver unterstützt aber
|
||||||
> Lösens abstürzen. Siehe [Bekannte Einschränkungen](#bekannte-einschränkungen).
|
> bisher nur vier grundlegende Strategien.
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
@@ -43,8 +43,9 @@ gofmt -w main.go field/*.go parser/*.go logic/*.go logic/strategies/*.go sudoku/
|
|||||||
go vet ./...
|
go vet ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
Aktuell gibt es noch keine automatisierten Tests. Neue Funktionalität sollte
|
Die Pakete `field`, `parser`, `logic`, `logic/strategies` und `sudoku` besitzen
|
||||||
nach Möglichkeit mit paketnahen `*_test.go`-Tests ergänzt werden.
|
Unit- und Regressionstests. Neue Funktionalität sollte weiterhin mit paketnahen,
|
||||||
|
vorzugsweise tabellengesteuerten `*_test.go`-Tests ergänzt werden.
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
@@ -70,8 +71,9 @@ Puzzle-Bank-Datei -> parser.PuzzleBank -> field.Field
|
|||||||
|
|
||||||
Eine Strategie implementiert `strategies.Strategy`. `SearchProgressableCells`
|
Eine Strategie implementiert `strategies.Strategy`. `SearchProgressableCells`
|
||||||
sammelt mögliche Änderungen, `ApplyAll` oder `ApplyNext` übernimmt sie in das
|
sammelt mögliche Änderungen, `ApplyAll` oder `ApplyNext` übernimmt sie in das
|
||||||
Feld. Wird eine Zahl gesetzt, entfernt der Solver den entsprechenden Kandidaten
|
Feld. Wird eine Zahl gesetzt, entfernt das Feldmodell den entsprechenden
|
||||||
aus der zugehörigen Zeile, Spalte und dem Block.
|
Kandidaten automatisch und ohne Duplikate aus der zugehörigen Zeile, Spalte und
|
||||||
|
dem Block.
|
||||||
|
|
||||||
Derzeit in `main.go` aktiv:
|
Derzeit in `main.go` aktiv:
|
||||||
|
|
||||||
@@ -88,25 +90,27 @@ implementiert.
|
|||||||
`parser.PuzzleBank` erwartet das Format der
|
`parser.PuzzleBank` erwartet das Format der
|
||||||
[Sudoku Exchange Puzzle Bank](data/sudoku-exchange-puzzle-bank/README.md): pro
|
[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
|
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
|
Schwierigkeitsbewertung. Hash, Zeichenzahl, Ziffern, Rating und Ausgangsbelegung
|
||||||
übersprungen; alle eingelesenen Rätsel werden im Parser gespeichert.
|
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
|
Die Datensätze unter `data/sudoku-exchange-puzzle-bank/` stehen unter der dort
|
||||||
beiliegenden separaten Lizenz.
|
beiliegenden separaten Lizenz.
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
## 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.
|
- `logic.Solver.GetSolutionPath` liefert noch keinen Lösungsweg.
|
||||||
- Mehrere fortgeschrittene Strategien sind nur als leere Typen vorhanden.
|
- Mehrere fortgeschrittene Strategien sind nur als leere Typen vorhanden.
|
||||||
- `go test ./...` meldet aktuell in `field/field.go`, dass eine Ganzzahl direkt
|
- Der Solver besitzt noch kein gemeinsames Interface für menschliche,
|
||||||
in einen String konvertiert wird.
|
Backtracking- und DLX-Implementierungen.
|
||||||
- Die Demo kann in `LastDigit.SearchProgressableCells` auf eine leere Zelle
|
- Die Demo verwendet weiterhin einen fest kodierten Dateipfad und Puzzle-Index.
|
||||||
zugreifen und dadurch abstürzen.
|
- Der alte öffentliche Bezeichner `InitStragies` bleibt vorerst als
|
||||||
- Fehlertexte und Bezeichner sind derzeit teilweise deutsch, teilweise
|
Kompatibilitätsalias bestehen.
|
||||||
englisch; einige öffentliche Namen enthalten noch Tippfehler.
|
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
|
|||||||
+22
-22
@@ -20,7 +20,9 @@ Das erste stabile Release soll:
|
|||||||
Backtracking, DLX und sehr fortgeschrittene menschliche Strategien folgen auf
|
Backtracking, DLX und sehr fortgeschrittene menschliche Strategien folgen auf
|
||||||
dieses Kern-Release.
|
dieses Kern-Release.
|
||||||
|
|
||||||
## Phase 0 – Lauffähige und getestete Basis (P0)
|
## Phase 0 – Lauffähige und getestete Basis (P0) ✅
|
||||||
|
|
||||||
|
Abgeschlossen am 10. September 2026.
|
||||||
|
|
||||||
Zuerst wird der bestehende Zustand stabilisiert. In dieser Phase werden keine
|
Zuerst wird der bestehende Zustand stabilisiert. In dieser Phase werden keine
|
||||||
neuen Lösungsstrategien ergänzt.
|
neuen Lösungsstrategien ergänzt.
|
||||||
@@ -49,7 +51,9 @@ Abgeschlossen, wenn:
|
|||||||
- `go run .` das ausgewählte Beispiel ohne Panic beendet und
|
- `go run .` das ausgewählte Beispiel ohne Panic beendet und
|
||||||
- die bekannten Fehler jeweils einen Regressionstest besitzen.
|
- die bekannten Fehler jeweils einen Regressionstest besitzen.
|
||||||
|
|
||||||
## Phase 1 – Verlässliches Feldmodell (P0)
|
## Phase 1 – Verlässliches Feldmodell (P0) ✅
|
||||||
|
|
||||||
|
Abgeschlossen am 10. September 2026.
|
||||||
|
|
||||||
- `field.Properties` beim Erzeugen prüfen: positive Dimensionen, passende
|
- `field.Properties` beim Erzeugen prüfen: positive Dimensionen, passende
|
||||||
Blockaufteilung und exakt passende Zellmatrix.
|
Blockaufteilung und exakt passende Zellmatrix.
|
||||||
@@ -65,14 +69,16 @@ Abgeschlossen, wenn:
|
|||||||
Fehler zurückgeben und das Feld unverändert lassen.
|
Fehler zurückgeben und das Feld unverändert lassen.
|
||||||
- lesenden Zugriff auf Eigenschaften und Änderungshistorie anbieten, ohne
|
- lesenden Zugriff auf Eigenschaften und Änderungshistorie anbieten, ohne
|
||||||
interne Slices veränderbar nach außen zu geben.
|
interne Slices veränderbar nach außen zu geben.
|
||||||
- allgemeine und 9x9-spezifische Darstellung trennen; `StringNotesForNumber`
|
- die fest codierte 9x9-Darstellung durch eine allgemeine Darstellung ersetzen
|
||||||
implementieren oder bis zu einem echten Bedarf aus der API entfernen.
|
und `StringNotesForNumber` implementieren.
|
||||||
|
|
||||||
Abgeschlossen, wenn gültige, ungültige, unvollständige und gelöste Felder in
|
Abgeschlossen, wenn gültige, ungültige, unvollständige und gelöste Felder in
|
||||||
Tabellentests eindeutig unterschieden werden und keine Mutation die
|
Tabellentests eindeutig unterschieden werden und keine Mutation die
|
||||||
Feld-Invarianten umgehen kann.
|
Feld-Invarianten umgehen kann.
|
||||||
|
|
||||||
## Phase 2 – Robuste Parser- und Game-API (P0)
|
## Phase 2 – Robuste Parser- und Game-API (P0) ✅
|
||||||
|
|
||||||
|
Abgeschlossen am 10. September 2026.
|
||||||
|
|
||||||
- Parserfehler vereinheitlichen und mit Zeilennummer sowie fehlerhaftem Feld
|
- Parserfehler vereinheitlichen und mit Zeilennummer sowie fehlerhaftem Feld
|
||||||
anreichern.
|
anreichern.
|
||||||
@@ -95,17 +101,12 @@ und Parser sowie `Game` keine implizite Rätselauswahl mehr enthalten.
|
|||||||
|
|
||||||
- ein Solver-Interface definieren, das später menschlichen Solver, Backtracking
|
- ein Solver-Interface definieren, das später menschlichen Solver, Backtracking
|
||||||
und DLX austauschbar macht.
|
und DLX austauschbar macht.
|
||||||
- `Run(int) bool` durch eine aussagekräftige Schritt-API ersetzen, zum Beispiel
|
- `Run(int) (bool, error)` durch eine aussagekräftige Schritt-API ersetzen, zum Beispiel
|
||||||
mit den Zuständen `Progress`, `Solved`, `Stuck`, `Invalid` und `Failed`.
|
mit den Zuständen `Progress`, `Solved`, `Stuck`, `Invalid` und `Failed`.
|
||||||
- Konfiguration aus den ungenutzten `conf`-Feldern ableiten oder diese entfernen.
|
|
||||||
- Strategie-Reihenfolge, Wiederholungspunkt und `ApplyNext`/`ApplyAll` eindeutig
|
- Strategie-Reihenfolge, Wiederholungspunkt und `ApplyNext`/`ApplyAll` eindeutig
|
||||||
definieren.
|
definieren.
|
||||||
- gefundene Änderungen vor dem Anwenden deduplizieren und auf Konflikte prüfen.
|
|
||||||
- Kandidaten nach jedem Zahlenschritt korrekt und deterministisch aktualisieren.
|
|
||||||
- Tippfehler in öffentlichen Namen (`InitStragies`, `TriggerdBy`) kontrolliert
|
- Tippfehler in öffentlichen Namen (`InitStragies`, `TriggerdBy`) kontrolliert
|
||||||
migrieren und alle Aufrufer anpassen.
|
migrieren und alle Aufrufer anpassen.
|
||||||
- Abbruch bei Stillstand, ungültigem Zustand und internem Fehler sauber durch
|
|
||||||
`sudoku.Game.Solve` reichen.
|
|
||||||
|
|
||||||
Abgeschlossen, wenn derselbe Input stets dieselben Schritte erzeugt und jeder
|
Abgeschlossen, wenn derselbe Input stets dieselben Schritte erzeugt und jeder
|
||||||
Solverlauf genau einen überprüfbaren Endzustand besitzt.
|
Solverlauf genau einen überprüfbaren Endzustand besitzt.
|
||||||
@@ -208,17 +209,16 @@ verwendet werden kann.
|
|||||||
|
|
||||||
Diese Tickets bilden die kürzeste Route zu einem stabilen Zwischenstand:
|
Diese Tickets bilden die kürzeste Route zu einem stabilen Zwischenstand:
|
||||||
|
|
||||||
1. Test-Helfer und Feldzugriffs-Tests erstellen.
|
1. ein gemeinsames Solver-Interface und aussagekräftige Schrittzustände
|
||||||
2. `ForEachCell`, `GetCell` und `Field.String` korrigieren.
|
entwerfen.
|
||||||
3. Change-Aktionen sowie Nil- und Werteprüfung absichern.
|
2. Strategie-Reihenfolge sowie `ApplyNext` und `ApplyAll` als öffentlichen
|
||||||
4. `LastDigit` deduplizieren und den bekannten Panic per Regressionstest
|
Vertrag festlegen und testen.
|
||||||
beseitigen.
|
3. `InitStragies` und `TriggerdBy` kontrolliert auf korrekt geschriebene Namen
|
||||||
5. `HiddenSingle` korrigieren und vollständig testen.
|
migrieren.
|
||||||
6. `Part.IsSolved`, `Field.IsValid` und `Field.IsSolved` implementieren.
|
4. End-to-End-Tests für mehrere einfache und mittlere Puzzle-Bank-Rätsel
|
||||||
7. Puzzle-Bank-Parser atomar und indexsicher machen.
|
ergänzen.
|
||||||
8. Solver-Schrittergebnis und Konflikterkennung einführen.
|
5. anschließend `NakedPair`, `NakedTriple`, `HiddenPair` und `HiddenTriple`
|
||||||
9. die vier Basisstrategien durch End-to-End-Rätseltests absichern.
|
implementieren.
|
||||||
10. erst danach Paar-/Tripel-Strategien oder neue Bedienfunktionen beginnen.
|
|
||||||
|
|
||||||
Nach jedem Arbeitspaket müssen `gofmt`, `go test ./...` und `go vet ./...`
|
Nach jedem Arbeitspaket müssen `gofmt`, `go test ./...` und `go vet ./...`
|
||||||
erfolgreich sein. Neue bekannte Baseline-Fehler sollen nicht angesammelt werden.
|
erfolgreich sein. Neue bekannte Baseline-Fehler sollen nicht angesammelt werden.
|
||||||
|
|||||||
+42
-88
@@ -1,123 +1,77 @@
|
|||||||
package field
|
package field
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// NOTES STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
import "slices"
|
import "slices"
|
||||||
|
|
||||||
type Notes struct {
|
type Notes struct {
|
||||||
numbers []int
|
numbers []int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) {
|
func (n *Notes) Add(f *Field, cell *Cell, note int, trigger string, marks []Mark) error {
|
||||||
if slices.Contains(n.numbers, note) {
|
if n == nil || cell == nil || cell.notes != n {
|
||||||
return // Note already exists
|
return ErrInvalidCell
|
||||||
}
|
}
|
||||||
field.changes = append(field.changes, Change{
|
return f.AddChange(&ExternalChange{Cell: cell, Marks: marks, Action: ActionSetNote, Value: note, TriggerdBy: trigger})
|
||||||
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) {
|
func (n *Notes) Remove(f *Field, cell *Cell, note int, trigger string, marks []Mark) error {
|
||||||
for i, existing := range n.numbers {
|
if n == nil || cell == nil || cell.notes != n {
|
||||||
if existing == note {
|
return ErrInvalidCell
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return f.AddChange(&ExternalChange{Cell: cell, Marks: marks, Action: ActionRemoveNote, From: note, TriggerdBy: trigger})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notes) Get() []int {
|
func (n *Notes) Get() []int {
|
||||||
copySlice := make([]int, len(n.numbers))
|
if n == nil {
|
||||||
copy(copySlice, n.numbers)
|
return nil
|
||||||
return copySlice
|
}
|
||||||
|
return slices.Clone(n.numbers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notes) Has(i int) bool {
|
func (n *Notes) Has(number int) bool {
|
||||||
return slices.Contains(n.numbers, i)
|
return n != nil && slices.Contains(n.numbers, number)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// CELL STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Cell struct {
|
type Cell struct {
|
||||||
number int
|
number int
|
||||||
Notes *Notes
|
notes *Notes
|
||||||
Pos *Position
|
pos *Position
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCell(number int, pos *Position) *Cell {
|
func NewCell(number int, pos *Position) *Cell {
|
||||||
cell := Cell{
|
return &Cell{number: number, notes: &Notes{}, pos: pos}
|
||||||
number: number,
|
|
||||||
Pos: pos,
|
|
||||||
Notes: &Notes{},
|
|
||||||
}
|
|
||||||
return &cell
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) {
|
func (c *Cell) SetNumber(f *Field, number int, trigger string, marks []Mark) error {
|
||||||
if c.number == n {
|
if c == nil {
|
||||||
return
|
return ErrInvalidCell
|
||||||
}
|
}
|
||||||
|
return f.AddChange(&ExternalChange{Cell: c, Marks: marks, Action: ActionSetNumber, Value: number, From: c.number, TriggerdBy: trigger})
|
||||||
if field == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if n <= 0 || n > field.props.Rows || n > field.props.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) {
|
func (c *Cell) RemoveNumber(f *Field, trigger string, marks []Mark) error {
|
||||||
if c.number == 0 {
|
if c == nil {
|
||||||
return
|
return ErrInvalidCell
|
||||||
}
|
}
|
||||||
|
return f.AddChange(&ExternalChange{Cell: c, Marks: marks, Action: ActionRemoveNumber, From: c.number, TriggerdBy: trigger})
|
||||||
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 {
|
func (c *Cell) GetNumber() int {
|
||||||
|
if c == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return c.number
|
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
|
||||||
|
}
|
||||||
|
|||||||
+166
-40
@@ -1,13 +1,11 @@
|
|||||||
package field
|
package field
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// MARK STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Mark struct {
|
type Mark struct {
|
||||||
Cell *Cell
|
Cell *Cell
|
||||||
Change *Change
|
Change *Change
|
||||||
@@ -15,94 +13,222 @@ type Mark struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mark) GetColor() color.Color {
|
func (m *Mark) GetColor() color.Color {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return m.color
|
return m.color
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// CHANGE_ACTION TYPE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type ChangeAction int
|
type ChangeAction int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ActionSetNumber = iota
|
ActionSetNumber ChangeAction = iota
|
||||||
ActionSetNote
|
ActionSetNote
|
||||||
ActionRemoveNumber
|
ActionRemoveNumber
|
||||||
ActionRemoveNote
|
ActionRemoveNote
|
||||||
)
|
)
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// CHANGE STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type ExternalChange struct {
|
type ExternalChange struct {
|
||||||
Cell *Cell
|
Cell *Cell
|
||||||
Marks []Mark
|
Marks []Mark
|
||||||
Action ChangeAction
|
Action ChangeAction
|
||||||
Value int
|
Value int
|
||||||
From int
|
From int
|
||||||
TriggerdBy string //strategy or manuell
|
TriggerdBy string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// CHANGE STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Change struct {
|
type Change struct {
|
||||||
Cell *Cell
|
Cell *Cell
|
||||||
marks []Mark
|
marks []Mark
|
||||||
action ChangeAction
|
action ChangeAction
|
||||||
value int
|
value int
|
||||||
from int
|
from int
|
||||||
triggerdBy string //strategy or manuell
|
triggerdBy string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// GETTER //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
func (c *Change) GetMarks() []Mark {
|
func (c *Change) GetMarks() []Mark {
|
||||||
return c.marks
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return slices.Clone(c.marks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Change) GetAction() ChangeAction {
|
func (c *Change) GetAction() ChangeAction {
|
||||||
|
if c == nil {
|
||||||
|
return ActionSetNumber
|
||||||
|
}
|
||||||
return c.action
|
return c.action
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Change) GetTo() int {
|
func (c *Change) GetTo() int {
|
||||||
|
if c == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return c.value
|
return c.value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Change) GetFrom() int {
|
func (c *Change) GetFrom() int {
|
||||||
|
if c == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return c.from
|
return c.from
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Change) GetTriggeredBy() string {
|
func (c *Change) GetTriggeredBy() string {
|
||||||
|
if c == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return c.triggerdBy
|
return c.triggerdBy
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
func (f *Field) AddChange(external *ExternalChange) error {
|
||||||
// ??? //
|
if err := f.validateChange(external); err != nil {
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
return err
|
||||||
|
}
|
||||||
func (c *Change) do() {
|
|
||||||
switch c.action {
|
|
||||||
|
|
||||||
|
cell := external.Cell
|
||||||
|
switch external.Action {
|
||||||
case ActionSetNumber:
|
case ActionSetNumber:
|
||||||
c.Cell.number = c.value
|
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:
|
case ActionSetNote:
|
||||||
c.Cell.Notes.numbers = append(c.Cell.Notes.numbers, c.value)
|
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:
|
case ActionRemoveNumber:
|
||||||
c.Cell.number = 0
|
if cell.number == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cell.number = 0
|
||||||
|
f.recordChange(external)
|
||||||
case ActionRemoveNote:
|
case ActionRemoveNote:
|
||||||
for i, existing := range c.Cell.Notes.numbers {
|
for i, existing := range cell.notes.numbers {
|
||||||
if existing == c.from {
|
if existing == external.From {
|
||||||
c.Cell.Notes.numbers = append(c.Cell.Notes.numbers[:i], c.Cell.Notes.numbers[i+1:]...)
|
cell.notes.numbers = slices.Delete(cell.notes.numbers, i, i+1)
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -3,6 +3,8 @@ package field
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
outOfBound = errors.New("invalid coordinates")
|
ErrOutOfBounds = errors.New("coordinates out of bounds")
|
||||||
invalidField = errors.New("can not parse Field")
|
ErrInvalidField = errors.New("invalid field")
|
||||||
|
ErrInvalidCell = errors.New("invalid cell")
|
||||||
|
ErrInvalidChange = errors.New("invalid change")
|
||||||
)
|
)
|
||||||
|
|||||||
+356
-202
@@ -2,13 +2,11 @@ package field
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// META STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Properties struct {
|
type Properties struct {
|
||||||
Rows int
|
Rows int
|
||||||
Columns int
|
Columns int
|
||||||
@@ -19,289 +17,445 @@ type Properties struct {
|
|||||||
Rating float64
|
Rating float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// FIELD STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Field struct {
|
type Field struct {
|
||||||
props *Properties
|
props *Properties
|
||||||
cells [][]Cell
|
cells [][]Cell
|
||||||
changes []Change
|
changes []Change
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(props Properties, cells [][]Cell) *Field {
|
type fieldSnapshot struct {
|
||||||
field := Field{
|
numbers [][]int
|
||||||
props: &props,
|
notes [][][]int
|
||||||
cells: cells,
|
changes []Change
|
||||||
}
|
|
||||||
return &field
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
func New(props Properties, cells [][]Cell) (*Field, error) {
|
||||||
// GETTER //
|
if err := validateProperties(props); err != nil {
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
return nil, err
|
||||||
|
}
|
||||||
func (f *Field) GetRow(r int) (*Row, error) {
|
if len(cells) != props.Rows {
|
||||||
if r >= f.props.Rows || r < 0 {
|
return nil, fmt.Errorf("%w: expected %d rows, got %d", ErrInvalidField, props.Rows, len(cells))
|
||||||
return nil, outOfBound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cellPtrs := make([]*Cell, len(f.cells[r]))
|
cloned := make([][]Cell, props.Rows)
|
||||||
for i := range f.cells[r] {
|
for row := 0; row < props.Rows; row++ {
|
||||||
cellPtrs[i] = &f.cells[r][i]
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Row{
|
result := &Field{props: &props, cells: cloned}
|
||||||
Line: Line{
|
if !result.hasValidNumbers() || !result.hasValidNotes() {
|
||||||
cells: cellPtrs,
|
return nil, fmt.Errorf("%w: duplicate values or invalid candidates", ErrInvalidField)
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Field) GetColumn(c int) (*Column, error) {
|
|
||||||
if c >= f.props.Columns || c < 0 {
|
|
||||||
return nil, outOfBound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Performance
|
|
||||||
result := &Column{
|
|
||||||
Line: Line{
|
|
||||||
cells: make([]*Cell, 0, f.props.Rows),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copying
|
|
||||||
for _, row := range f.cells {
|
|
||||||
result.cells = append(result.cells, &row[c])
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) GetBlock(r, c int) (*Block, error) {
|
func validateProperties(props Properties) error {
|
||||||
if r < 0 || r >= f.props.BlockRows || c < 0 || c >= f.props.BlockColumns {
|
if props.Rows <= 0 || props.Columns <= 0 || props.Rows != props.Columns {
|
||||||
return nil, outOfBound
|
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 {
|
||||||
startRow := r * f.props.BlockSizeRow
|
return fmt.Errorf("%w: block dimensions must be positive", ErrInvalidField)
|
||||||
startCol := c * f.props.BlockSizeColumn
|
|
||||||
|
|
||||||
block := &Block{
|
|
||||||
cells: make([][]*Cell, f.props.BlockSizeRow),
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
for row := range block.cells {
|
func normalizeNotes(notes []int, max, cellNumber int) ([]int, error) {
|
||||||
// Effizientes Kopieren der Zeile
|
if cellNumber != 0 && len(notes) != 0 {
|
||||||
block.cells[row] = make([]*Cell, f.props.BlockSizeColumn)
|
return nil, fmt.Errorf("filled cells cannot contain notes")
|
||||||
|
}
|
||||||
for column := range block.cells[row] {
|
result := slices.Clone(notes)
|
||||||
block.cells[row][column] = &f.cells[startRow+row][startCol+column]
|
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 block, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) GetEachPartAtPos(pos *Position) []Part {
|
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)
|
row, err := f.GetRow(pos.row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
column, err := f.GetColumn(pos.column)
|
column, err := f.GetColumn(pos.column)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
block, err := f.GetBlock(pos.blockRow, pos.blockColumn)
|
block, err := f.GetBlock(pos.blockRow, pos.blockColumn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return []Part{row, column, block}, nil
|
||||||
return append([]Part{}, row, column, block)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) GetCell(r, c int) (*Cell, error) {
|
func (f *Field) GetCell(row, column int) (*Cell, error) {
|
||||||
if r > f.props.Rows || c > f.props.Columns {
|
if !f.hasStructure() || row < 0 || row >= f.props.Rows || column < 0 || column >= f.props.Columns {
|
||||||
return nil, outOfBound
|
return nil, ErrOutOfBounds
|
||||||
}
|
}
|
||||||
return &f.cells[r][c], nil
|
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 {
|
func (f *Field) GetRating() float64 {
|
||||||
|
if !f.hasStructure() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return f.props.Rating
|
return f.props.Rating
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
func (f *Field) ForEachPart(fn func(Part)) {
|
||||||
// FOREACH FUNCTIONS //
|
if !f.hasStructure() || fn == nil {
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
return
|
||||||
|
}
|
||||||
func (f *Field) ForEachPart(fn func(part Part)) {
|
f.ForEachRow(func(row *Row) { fn(row) })
|
||||||
f.ForEachRow(func(row *Row) {
|
f.ForEachColumn(func(column *Column) { fn(column) })
|
||||||
fn(row)
|
f.ForEachBlock(func(block *Block) { fn(block) })
|
||||||
})
|
|
||||||
|
|
||||||
f.ForEachColumn(func(column *Column) {
|
|
||||||
fn(column)
|
|
||||||
})
|
|
||||||
|
|
||||||
f.ForEachBlock(func(block *Block) {
|
|
||||||
fn(block)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) ForEachPartAtPos(pos *Position, fn func(part Part)) {
|
func (f *Field) ForEachPartAtPos(pos *Position, fn func(Part)) error {
|
||||||
parts := f.GetEachPartAtPos(pos)
|
if fn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts, err := f.GetEachPartAtPos(pos)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
fn(part)
|
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) ForEachRow(fn func(row *Row)) {
|
func (f *Field) ForEachColumn(fn func(*Column)) {
|
||||||
for i := range f.props.Rows {
|
if !f.hasStructure() || fn == nil {
|
||||||
row, err := f.GetRow(i)
|
return
|
||||||
if err != nil {
|
}
|
||||||
fmt.Println(err.Error())
|
for column := 0; column < f.props.Columns; column++ {
|
||||||
return
|
part, _ := f.GetColumn(column)
|
||||||
}
|
fn(part)
|
||||||
fn(row)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) ForEachColumn(fn func(column *Column)) {
|
func (f *Field) ForEachBlock(fn func(*Block)) {
|
||||||
for i := range f.props.Columns {
|
if !f.hasStructure() || fn == nil {
|
||||||
column, err := f.GetColumn(i)
|
return
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fn(column)
|
|
||||||
}
|
}
|
||||||
|
for row := 0; row < f.props.BlockRows; row++ {
|
||||||
}
|
for column := 0; column < f.props.BlockColumns; column++ {
|
||||||
|
part, _ := f.GetBlock(row, column)
|
||||||
func (f *Field) ForEachBlock(fn func(block *Block)) {
|
fn(part)
|
||||||
for r := range f.props.BlockRows {
|
|
||||||
for c := range f.props.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)) {
|
func (f *Field) ForEachCell(fn func(*Cell)) {
|
||||||
for _, row := range f.cells {
|
if !f.hasStructure() || fn == nil {
|
||||||
for _, cell := range row {
|
return
|
||||||
fn(&cell)
|
}
|
||||||
|
for row := range f.cells {
|
||||||
|
for column := range f.cells[row] {
|
||||||
|
fn(&f.cells[row][column])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Field) SetRating(rating float64) {
|
func (f *Field) SetRating(rating float64) {
|
||||||
f.props.Rating = rating
|
if f != nil && f.props != nil {
|
||||||
|
f.props.Rating = rating
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// STATE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
func (f *Field) String() string {
|
func (f *Field) String() string {
|
||||||
//TODO: auf beliebige größen anpassen
|
if !f.hasStructure() {
|
||||||
var sb strings.Builder
|
return "<invalid field>"
|
||||||
|
}
|
||||||
// Oberer Rahmen
|
var result strings.Builder
|
||||||
sb.WriteString("╔═══════╤═══════╤═══════╗\n")
|
for row := 0; row < f.props.Rows; row++ {
|
||||||
|
if row > 0 && row%f.props.BlockSizeRow == 0 {
|
||||||
for i := range f.cells {
|
result.WriteByte('\n')
|
||||||
sb.WriteString("║ ") // Linke Rahmenseite
|
}
|
||||||
|
for column := 0; column < f.props.Columns; column++ {
|
||||||
for j, cell := range f.cells[i] {
|
if column > 0 {
|
||||||
// Wert ausgeben oder Punkt für 0
|
if column%f.props.BlockSizeColumn == 0 {
|
||||||
val := cell.number
|
result.WriteString(" | ")
|
||||||
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 {
|
} else {
|
||||||
sb.WriteString(" ║\n") // Rechte Rahmenseite + Zeilenumbruch
|
result.WriteByte(' ')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
value := f.cells[row][column].number
|
||||||
|
if value == 0 {
|
||||||
|
result.WriteRune('·')
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(" ")
|
result.WriteString(strconv.Itoa(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if row+1 < f.props.Rows {
|
||||||
// Horizontale Trennlinien nach jeder 3. Zeile
|
result.WriteByte('\n')
|
||||||
if (i+1)%3 == 0 && i < 8 {
|
|
||||||
sb.WriteString("╟───────┼───────┼───────╢\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result.String()
|
||||||
// Unterer Rahmen
|
|
||||||
sb.WriteString("╚═══════╧═══════╧═══════╝")
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) StringNotesForNumber(n int) string {
|
func (f *Field) StringNotesForNumber(number int) string {
|
||||||
return "StringNotesForNumber is not implementet"
|
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 {
|
func (f *Field) StringNotes() string {
|
||||||
var str strings.Builder
|
if !f.hasStructure() {
|
||||||
fmt.Fprintf(&str, "Notes:\n")
|
return "Notes:"
|
||||||
|
}
|
||||||
|
var result strings.Builder
|
||||||
|
result.WriteString("Notes:\n")
|
||||||
f.ForEachCell(func(cell *Cell) {
|
f.ForEachCell(func(cell *Cell) {
|
||||||
fmt.Fprintf(&str, "Pos: %d/%d - Notes: %v\n", cell.Pos.row, cell.Pos.column, cell.Notes.numbers)
|
fmt.Fprintf(&result, "Pos: %d/%d - Notes: %v\n", cell.pos.row, cell.pos.column, cell.notes.numbers)
|
||||||
})
|
})
|
||||||
return str.String()
|
return strings.TrimSuffix(result.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) IsSolved() bool {
|
func (f *Field) IsSolved() bool {
|
||||||
result := true
|
if !f.IsValid() {
|
||||||
f.ForEachCell(func(cell *Cell) {
|
|
||||||
if cell.number == 0 {
|
|
||||||
result = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/* TODO:
|
|
||||||
if !f.isValid() {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
*/
|
for row := range f.cells {
|
||||||
|
for column := range f.cells[row] {
|
||||||
return result
|
if f.cells[row][column].number == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) IsValid() bool {
|
func (f *Field) IsValid() bool {
|
||||||
//In jedem Part gibt es jede Zahl max ein mal
|
return f.hasStructure() && f.hasValidNumbers() && f.hasValidNotes()
|
||||||
//Die Zahl kommt nicht in intersecting parts vor
|
}
|
||||||
|
|
||||||
|
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
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
-67
@@ -7,83 +7,72 @@ import (
|
|||||||
|
|
||||||
type Part interface {
|
type Part interface {
|
||||||
IsSolved() bool
|
IsSolved() bool
|
||||||
ForEachCell(fn func(cell *Cell))
|
ForEachCell(func(*Cell))
|
||||||
GetMissingNumbers() []int
|
GetMissingNumbers() []int
|
||||||
RemoveNote(field *Field, note int, trigger string, marks []Mark)
|
RemoveNote(*Field, int, string, []Mark) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// LINE STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Line struct {
|
type Line struct {
|
||||||
cells []*Cell
|
cells []*Cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Line) IsSolved() bool {
|
func (l *Line) IsSolved() bool {
|
||||||
//TODO
|
return partIsSolved(l)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Line) ForEachCell(fn func(cell *Cell)) {
|
func (l *Line) ForEachCell(fn func(*Cell)) {
|
||||||
|
if l == nil || fn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, cell := range l.cells {
|
for _, cell := range l.cells {
|
||||||
fn(cell)
|
fn(cell)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Line) GetMissingNumbers() []int {
|
func (l *Line) GetMissingNumbers() []int {
|
||||||
return getMissingNumbersHelper(len(l.cells), l.ForEachCell)
|
if l == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return getMissingNumbers(len(l.cells), l.ForEachCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Line) RemoveNote(field *Field, note int, trigger string, marks []Mark) {
|
func (l *Line) RemoveNote(f *Field, note int, trigger string, marks []Mark) error {
|
||||||
l.ForEachCell(func(cell *Cell) {
|
return removeNoteFromPart(l, f, note, trigger, marks)
|
||||||
cell.Notes.Remove(field, cell, note, trigger, marks)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Line) String() string {
|
func (l *Line) String() string {
|
||||||
return lineString(l, "Line")
|
return partString(l, "Line")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// ROW STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Row struct {
|
type Row struct {
|
||||||
Line
|
Line
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Row) String() string {
|
func (r *Row) String() string {
|
||||||
return lineString(r, "Row")
|
return partString(r, "Row")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// COLUMN STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Column struct {
|
type Column struct {
|
||||||
Line
|
Line
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Column) String() string {
|
func (c *Column) String() string {
|
||||||
return lineString(c, "Column")
|
return partString(c, "Column")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
// BLOCK STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Block struct {
|
type Block struct {
|
||||||
cells [][]*Cell
|
cells [][]*Cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) IsSolved() bool {
|
func (b *Block) IsSolved() bool {
|
||||||
//TODO
|
return partIsSolved(b)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) ForEachCell(fn func(cell *Cell)) {
|
func (b *Block) ForEachCell(fn func(*Cell)) {
|
||||||
|
if b == nil || fn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, row := range b.cells {
|
for _, row := range b.cells {
|
||||||
for _, cell := range row {
|
for _, cell := range row {
|
||||||
fn(cell)
|
fn(cell)
|
||||||
@@ -92,56 +81,79 @@ func (b *Block) ForEachCell(fn func(cell *Cell)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) GetMissingNumbers() []int {
|
func (b *Block) GetMissingNumbers() []int {
|
||||||
max := len(b.cells) * len(b.cells[0])
|
if b == nil || len(b.cells) == 0 {
|
||||||
return getMissingNumbersHelper(max, b.ForEachCell)
|
return nil
|
||||||
|
}
|
||||||
|
size := 0
|
||||||
|
for _, row := range b.cells {
|
||||||
|
size += len(row)
|
||||||
|
}
|
||||||
|
return getMissingNumbers(size, b.ForEachCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) RemoveNote(field *Field, note int, trigger string, marks []Mark) {
|
func (b *Block) RemoveNote(f *Field, note int, trigger string, marks []Mark) error {
|
||||||
b.ForEachCell(func(cell *Cell) {
|
return removeNoteFromPart(b, f, note, trigger, marks)
|
||||||
cell.Notes.Remove(field, cell, note, trigger, marks)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) String() string {
|
func (b *Block) String() string {
|
||||||
var str strings.Builder
|
return partString(b, "Block")
|
||||||
str.WriteString("Block:\t")
|
|
||||||
lastLine := 0
|
|
||||||
b.ForEachCell(func(cell *Cell) {
|
|
||||||
fmt.Fprintf(&str, "%d ", cell.number)
|
|
||||||
if lastLine == cell.Pos.inBlockRow {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return str.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
func partIsSolved(part Part) bool {
|
||||||
// HELPER //
|
if part == nil {
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
return false
|
||||||
|
}
|
||||||
func lineString(part Part, name string) string {
|
cellCount := 0
|
||||||
var str strings.Builder
|
seen := make(map[int]struct{})
|
||||||
fmt.Fprintf(&str, "%s:\t", name)
|
valid := true
|
||||||
part.ForEachCell(func(cell *Cell) {
|
part.ForEachCell(func(cell *Cell) {
|
||||||
fmt.Fprintf(&str, "%d ", cell.number)
|
cellCount++
|
||||||
|
if cell == nil || cell.number <= 0 {
|
||||||
|
valid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, exists := seen[cell.number]; exists {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
seen[cell.number] = struct{}{}
|
||||||
})
|
})
|
||||||
return str.String()
|
return valid && cellCount > 0 && len(seen) == cellCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMissingNumbersHelper(max int, iterate func(fn func(cell *Cell))) []int {
|
func removeNoteFromPart(part Part, f *Field, note int, trigger string, marks []Mark) error {
|
||||||
present := make(map[int]bool, max)
|
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) {
|
iterate(func(cell *Cell) {
|
||||||
if cell.number != 0 {
|
if cell != nil && cell.number != 0 {
|
||||||
present[cell.number] = true
|
present[cell.number] = struct{}{}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
missing := make([]int, 0, max-len(present))
|
||||||
var missing []int
|
for number := 1; number <= max; number++ {
|
||||||
for i := 1; i <= max; i++ {
|
if _, exists := present[number]; !exists {
|
||||||
if !present[i] {
|
missing = append(missing, number)
|
||||||
missing = append(missing, i)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return missing
|
return missing
|
||||||
|
|||||||
@@ -22,37 +22,64 @@ func NewPosition(row, column, blockRow, blockColumn, inBlockRow, inBlockColumn i
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetRow() int {
|
func (p *Position) GetRow() int {
|
||||||
|
if p == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return p.row
|
return p.row
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetColumn() int {
|
func (p *Position) GetColumn() int {
|
||||||
|
if p == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return p.column
|
return p.column
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetBlockRow() int {
|
func (p *Position) GetBlockRow() int {
|
||||||
|
if p == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return p.blockRow
|
return p.blockRow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetBlockColumn() int {
|
func (p *Position) GetBlockColumn() int {
|
||||||
|
if p == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return p.blockColumn
|
return p.blockColumn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetInBlockRow() int {
|
func (p *Position) GetInBlockRow() int {
|
||||||
|
if p == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return p.inBlockRow
|
return p.inBlockRow
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetInBlockColumn() int {
|
func (p *Position) GetInBlockColumn() int {
|
||||||
|
if p == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return p.inBlockColumn
|
return p.inBlockColumn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetCoords() (int, int) {
|
func (p *Position) GetCoords() (int, int) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
return p.row, p.column
|
return p.row, p.column
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetBlockCoords() (int, int) {
|
func (p *Position) GetBlockCoords() (int, int) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
return p.blockRow, p.blockColumn
|
return p.blockRow, p.blockColumn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) GetInBlockCoords() (int, int) {
|
func (p *Position) GetInBlockCoords() (int, int) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
return p.inBlockRow, p.inBlockColumn
|
return p.inBlockRow, p.inBlockColumn
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-36
@@ -1,65 +1,83 @@
|
|||||||
package logic
|
package logic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"git.kleiax.de/homepage/field"
|
"git.kleiax.de/homepage/field"
|
||||||
"git.kleiax.de/homepage/logic/strategies"
|
"git.kleiax.de/homepage/logic/strategies"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
var ErrNoProgress = errors.New("no strategy can solve the puzzle")
|
||||||
// SOLVER STRUCTURE //
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
|
||||||
|
|
||||||
type Solver struct {
|
type Solver struct {
|
||||||
strategies []strategies.Strategy
|
strategies []strategies.Strategy
|
||||||
returnTo int
|
|
||||||
field *field.Field
|
field *field.Field
|
||||||
conf struct {
|
|
||||||
all bool
|
|
||||||
repeat bool
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Solver) Add(strategy strategies.Strategy) {
|
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 *field.Field) {
|
|
||||||
s.returnTo = 1
|
|
||||||
s.field = field
|
|
||||||
for _, strategy := range s.strategies {
|
|
||||||
strategy.Init(field)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Solver) Run(i int) bool {
|
func (s *Solver) InitStrategies(f *field.Field) error {
|
||||||
for j := 0; j < len(s.strategies); j++ {
|
if f == nil || !f.IsValid() {
|
||||||
if s.strategies[j].SearchProgressableCells() == 0 {
|
return field.ErrInvalidField
|
||||||
continue
|
}
|
||||||
|
s.field = f
|
||||||
|
for _, strategy := range s.strategies {
|
||||||
|
strategy.Init(f)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitStragies is kept for compatibility. New code should use InitStrategies.
|
||||||
|
func (s *Solver) InitStragies(f *field.Field) error {
|
||||||
|
return s.InitStrategies(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run applies strategies from the requested index until the field is solved or
|
||||||
|
// no strategy can make progress. It returns false without an error for a valid
|
||||||
|
// but currently unsolved field.
|
||||||
|
func (s *Solver) Run(start int) (bool, error) {
|
||||||
|
if s.field == nil || !s.field.IsValid() {
|
||||||
|
return false, field.ErrInvalidField
|
||||||
|
}
|
||||||
|
if start < 0 || start > len(s.strategies) {
|
||||||
|
return false, fmt.Errorf("strategy index %d out of range", start)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if s.field.IsSolved() {
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, change := range s.strategies[j].ApplyAll() {
|
progress := false
|
||||||
if change.Action != field.ActionSetNumber {
|
for index := start; index < len(s.strategies); index++ {
|
||||||
|
strategy := s.strategies[index]
|
||||||
|
if strategy.SearchProgressableCells() == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.field.ForEachPartAtPos(change.Cell.Pos, func(part field.Part) {
|
changes, err := strategy.ApplyAll()
|
||||||
part.ForEachCell(func(cell *field.Cell) {
|
if err != nil {
|
||||||
cell.Notes.Remove(s.field, cell, change.Value, "remove note after insert of a number", nil)
|
return false, fmt.Errorf("apply strategy %q: %w", strategy.Name(), err)
|
||||||
})
|
}
|
||||||
})
|
if len(changes) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
progress = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
j = s.returnTo - 1
|
if !progress {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
start = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.field.IsSolved() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Solver) Search() {
|
func (s *Solver) Search() {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Solver) GetSolutionPath() []field.ExternalChange {
|
func (s *Solver) GetSolutionPath() []field.ExternalChange {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package logic_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kleiax.de/homepage/field"
|
||||||
|
"git.kleiax.de/homepage/logic"
|
||||||
|
"git.kleiax.de/homepage/logic/strategies"
|
||||||
|
"git.kleiax.de/homepage/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
const solvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
|
||||||
|
|
||||||
|
func parsedField(t *testing.T, puzzle string) *field.Field {
|
||||||
|
t.Helper()
|
||||||
|
input := &parser.PuzzleString{}
|
||||||
|
if err := input.Parse([]byte(puzzle)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result, err := input.GetField(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSolverRunSolvesAndDetectsNoProgress(t *testing.T) {
|
||||||
|
t.Run("solved", func(t *testing.T) {
|
||||||
|
f := parsedField(t, "0"+solvedPuzzle[1:])
|
||||||
|
solver := &logic.Solver{}
|
||||||
|
solver.Add(&strategies.LastDigit{})
|
||||||
|
if err := solver.InitStrategies(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
solved, err := solver.Run(0)
|
||||||
|
if err != nil || !solved || !f.IsSolved() {
|
||||||
|
t.Fatalf("Run() = (%v, %v), field solved = %v", solved, err, f.IsSolved())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no progress", func(t *testing.T) {
|
||||||
|
f := parsedField(t, "0"+solvedPuzzle[1:])
|
||||||
|
solver := &logic.Solver{}
|
||||||
|
if err := solver.InitStrategies(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
solved, err := solver.Run(0)
|
||||||
|
if err != nil || solved {
|
||||||
|
t.Fatalf("Run() = (%v, %v), want (false, nil)", solved, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSolverRejectsInvalidStateAndStrategyIndex(t *testing.T) {
|
||||||
|
solver := &logic.Solver{}
|
||||||
|
if _, err := solver.Run(0); !errors.Is(err, field.ErrInvalidField) {
|
||||||
|
t.Fatalf("Run() before initialization error = %v", err)
|
||||||
|
}
|
||||||
|
if err := solver.InitStrategies(nil); !errors.Is(err, field.ErrInvalidField) {
|
||||||
|
t.Fatalf("InitStrategies(nil) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f := parsedField(t, "0"+solvedPuzzle[1:])
|
||||||
|
if err := solver.InitStrategies(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := solver.Run(1); err == nil {
|
||||||
|
t.Fatal("Run() with invalid strategy index error = nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,16 @@ func (ld *LastDigit) SearchProgressableCells() int {
|
|||||||
missingNumbers := part.GetMissingNumbers()
|
missingNumbers := part.GetMissingNumbers()
|
||||||
if len(missingNumbers) == 1 {
|
if len(missingNumbers) == 1 {
|
||||||
var emptyCell *field.Cell
|
var emptyCell *field.Cell
|
||||||
|
emptyCount := 0
|
||||||
part.ForEachCell(func(cell *field.Cell) {
|
part.ForEachCell(func(cell *field.Cell) {
|
||||||
if cell.GetNumber() == 0 {
|
if cell.GetNumber() == 0 {
|
||||||
emptyCell = cell
|
emptyCell = cell
|
||||||
|
emptyCount++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// fmt.Println(part)
|
if emptyCount != 1 {
|
||||||
// fmt.Printf("Gefundene Zelle: %d/%d - %d, missungNumber: %v, Typ: %T\n", emptyCell.Pos.GetRow(), emptyCell.Pos.GetColumn(), emptyCell.GetNumber(), missingNumbers, part)
|
return
|
||||||
|
}
|
||||||
change := field.ExternalChange{
|
change := field.ExternalChange{
|
||||||
Cell: emptyCell,
|
Cell: emptyCell,
|
||||||
Action: field.ActionSetNumber,
|
Action: field.ActionSetNumber,
|
||||||
@@ -31,10 +34,9 @@ func (ld *LastDigit) SearchProgressableCells() int {
|
|||||||
From: emptyCell.GetNumber(),
|
From: emptyCell.GetNumber(),
|
||||||
TriggerdBy: ld.getName(),
|
TriggerdBy: ld.getName(),
|
||||||
}
|
}
|
||||||
ld.changes = append(ld.changes, change)
|
ld.queue(change)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// fmt.Printf("LastDigit Changes %d\n", len(ld.changes))
|
|
||||||
return len(ld.changes)
|
return len(ld.changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +44,10 @@ func (ld *LastDigit) getName() string {
|
|||||||
return "Last Digit"
|
return "Last Digit"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ld *LastDigit) Name() string {
|
||||||
|
return ld.getName()
|
||||||
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
// ────────────────────────────────────────────────────────────────────────────── //
|
||||||
// CRP STRUCTURE //
|
// CRP STRUCTURE //
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
// ────────────────────────────────────────────────────────────────────────────── //
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ func (hs *HiddenSingle) SearchProgressableCells() int {
|
|||||||
m := make(map[int]int)
|
m := make(map[int]int)
|
||||||
|
|
||||||
part.ForEachCell(func(cell *field.Cell) {
|
part.ForEachCell(func(cell *field.Cell) {
|
||||||
notes := cell.Notes.Get()
|
notes := cell.GetNotes().Get()
|
||||||
for note := range notes {
|
for _, note := range notes {
|
||||||
m[note]++
|
m[note]++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -28,9 +28,9 @@ func (hs *HiddenSingle) SearchProgressableCells() int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for num := range hiddenSingles {
|
for _, num := range hiddenSingles {
|
||||||
part.ForEachCell(func(cell *field.Cell) {
|
part.ForEachCell(func(cell *field.Cell) {
|
||||||
if cell.Notes.Has(num) {
|
if cell.GetNumber() == 0 && cell.GetNotes().Has(num) {
|
||||||
change := field.ExternalChange{
|
change := field.ExternalChange{
|
||||||
Cell: cell,
|
Cell: cell,
|
||||||
Action: field.ActionSetNumber,
|
Action: field.ActionSetNumber,
|
||||||
@@ -38,7 +38,7 @@ func (hs *HiddenSingle) SearchProgressableCells() int {
|
|||||||
From: cell.GetNumber(),
|
From: cell.GetNumber(),
|
||||||
TriggerdBy: hs.getName(),
|
TriggerdBy: hs.getName(),
|
||||||
}
|
}
|
||||||
hs.changes = append(hs.changes, change)
|
hs.queue(change)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -50,6 +50,10 @@ func (hs *HiddenSingle) getName() string {
|
|||||||
return "Hidden Single"
|
return "Hidden Single"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hs *HiddenSingle) Name() string {
|
||||||
|
return hs.getName()
|
||||||
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
// ────────────────────────────────────────────────────────────────────────────── //
|
||||||
// HIDDEN_PAIR STRUCTURE //
|
// HIDDEN_PAIR STRUCTURE //
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
// ────────────────────────────────────────────────────────────────────────────── //
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type NakedSingle struct {
|
|||||||
func (ns *NakedSingle) SearchProgressableCells() int {
|
func (ns *NakedSingle) SearchProgressableCells() int {
|
||||||
ns.field.ForEachPart(func(part field.Part) {
|
ns.field.ForEachPart(func(part field.Part) {
|
||||||
part.ForEachCell(func(cell *field.Cell) {
|
part.ForEachCell(func(cell *field.Cell) {
|
||||||
candidates := cell.Notes.Get()
|
candidates := cell.GetNotes().Get()
|
||||||
if len(candidates) == 1 && cell.GetNumber() == 0 {
|
if len(candidates) == 1 && cell.GetNumber() == 0 {
|
||||||
change := field.ExternalChange{
|
change := field.ExternalChange{
|
||||||
Cell: cell,
|
Cell: cell,
|
||||||
@@ -22,7 +22,7 @@ func (ns *NakedSingle) SearchProgressableCells() int {
|
|||||||
From: cell.GetNumber(),
|
From: cell.GetNumber(),
|
||||||
TriggerdBy: ns.getName(),
|
TriggerdBy: ns.getName(),
|
||||||
}
|
}
|
||||||
ns.changes = append(ns.changes, change)
|
ns.queue(change)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -33,6 +33,10 @@ func (ns *NakedSingle) getName() string {
|
|||||||
return "Naked Single"
|
return "Naked Single"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ns *NakedSingle) Name() string {
|
||||||
|
return ns.getName()
|
||||||
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
// ────────────────────────────────────────────────────────────────────────────── //
|
||||||
// NAKED_DOUBLE STRUCTURE //
|
// NAKED_DOUBLE STRUCTURE //
|
||||||
// ────────────────────────────────────────────────────────────────────────────── //
|
// ────────────────────────────────────────────────────────────────────────────── //
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package strategies
|
package strategies
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
"git.kleiax.de/homepage/field"
|
"git.kleiax.de/homepage/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,11 +17,12 @@ type Notes struct {
|
|||||||
func (n *Notes) SearchProgressableCells() int {
|
func (n *Notes) SearchProgressableCells() int {
|
||||||
n.field.ForEachRow(func(row *field.Row) {
|
n.field.ForEachRow(func(row *field.Row) {
|
||||||
row.ForEachCell(func(cell *field.Cell) {
|
row.ForEachCell(func(cell *field.Cell) {
|
||||||
column, _ := n.field.GetColumn(cell.Pos.GetColumn())
|
pos := cell.GetPosition()
|
||||||
block, _ := n.field.GetBlock(cell.Pos.GetBlockRow(), cell.Pos.GetBlockColumn())
|
column, _ := n.field.GetColumn(pos.GetColumn())
|
||||||
|
block, _ := n.field.GetBlock(pos.GetBlockRow(), pos.GetBlockColumn())
|
||||||
candidates := intersection3(row.GetMissingNumbers(), column.GetMissingNumbers(), block.GetMissingNumbers())
|
candidates := intersection3(row.GetMissingNumbers(), column.GetMissingNumbers(), block.GetMissingNumbers())
|
||||||
for _, note := range candidates {
|
for _, note := range candidates {
|
||||||
if cell.Notes.Has(note) || cell.GetNumber() != 0 {
|
if cell.GetNotes().Has(note) || cell.GetNumber() != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ch := field.ExternalChange{
|
ch := field.ExternalChange{
|
||||||
@@ -30,7 +33,7 @@ func (n *Notes) SearchProgressableCells() int {
|
|||||||
Marks: nil,
|
Marks: nil,
|
||||||
From: 0,
|
From: 0,
|
||||||
}
|
}
|
||||||
n.changes = append(n.changes, ch)
|
n.queue(ch)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -41,6 +44,10 @@ func (n *Notes) getName() string {
|
|||||||
return "Make Notes"
|
return "Make Notes"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Notes) Name() string {
|
||||||
|
return n.getName()
|
||||||
|
}
|
||||||
|
|
||||||
func intersection3(a, b, c []int) []int {
|
func intersection3(a, b, c []int) []int {
|
||||||
set := make(map[int]bool)
|
set := make(map[int]bool)
|
||||||
|
|
||||||
@@ -76,6 +83,7 @@ func intersection3(a, b, c []int) []int {
|
|||||||
for v := range set {
|
for v := range set {
|
||||||
result = append(result, v)
|
result = append(result, v)
|
||||||
}
|
}
|
||||||
|
sort.Ints(result)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package strategies
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kleiax.de/homepage/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
const solvedGrid = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
|
||||||
|
|
||||||
|
func strategyField(t *testing.T, digits string) *field.Field {
|
||||||
|
t.Helper()
|
||||||
|
props := field.Properties{Rows: 9, Columns: 9, BlockRows: 3, BlockColumns: 3, BlockSizeRow: 3, BlockSizeColumn: 3}
|
||||||
|
cells := make([][]field.Cell, 9)
|
||||||
|
for row := range cells {
|
||||||
|
cells[row] = make([]field.Cell, 9)
|
||||||
|
for column := range cells[row] {
|
||||||
|
pos := field.NewPosition(row, column, row/3, column/3, row%3, column%3)
|
||||||
|
cells[row][column] = *field.NewCell(int(digits[row*9+column]-'0'), pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, err := field.New(props, cells)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastDigitDeduplicatesOverlappingParts(t *testing.T) {
|
||||||
|
f := strategyField(t, "0"+solvedGrid[1:])
|
||||||
|
strategy := &LastDigit{}
|
||||||
|
strategy.Init(f)
|
||||||
|
if got := strategy.SearchProgressableCells(); got != 1 {
|
||||||
|
t.Fatalf("SearchProgressableCells() = %d, want 1", got)
|
||||||
|
}
|
||||||
|
changes, err := strategy.ApplyAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyAll() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(changes) != 1 || !f.IsSolved() {
|
||||||
|
t.Fatalf("changes = %d, solved = %v", len(changes), f.IsSolved())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHiddenSingleUsesCandidateValuesNotSliceIndexes(t *testing.T) {
|
||||||
|
f := strategyField(t, strings.Repeat("0", 81))
|
||||||
|
first, _ := f.GetCell(0, 0)
|
||||||
|
second, _ := f.GetCell(0, 1)
|
||||||
|
third, _ := f.GetCell(0, 2)
|
||||||
|
for _, setup := range []struct {
|
||||||
|
cell *field.Cell
|
||||||
|
note int
|
||||||
|
}{{first, 9}, {second, 8}, {third, 8}} {
|
||||||
|
if err := setup.cell.GetNotes().Add(f, setup.cell, setup.note, "test", nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy := &HiddenSingle{}
|
||||||
|
strategy.Init(f)
|
||||||
|
strategy.SearchProgressableCells()
|
||||||
|
found := false
|
||||||
|
for _, change := range strategy.changes {
|
||||||
|
if change.Cell == first && change.Value == 9 {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("hidden single for candidate value 9 was not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntersection3IsSorted(t *testing.T) {
|
||||||
|
got := intersection3([]int{9, 1, 5}, []int{5, 9, 1}, []int{9, 5, 1})
|
||||||
|
want := []int{1, 5, 9}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Fatalf("intersection3() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
|
|
||||||
type Strategy interface {
|
type Strategy interface {
|
||||||
Init(f *field.Field)
|
Init(f *field.Field)
|
||||||
ApplyAll() []field.ExternalChange
|
ApplyAll() ([]field.ExternalChange, error)
|
||||||
ApplyNext() field.ExternalChange
|
ApplyNext() (field.ExternalChange, error)
|
||||||
Name() string
|
Name() string
|
||||||
SearchProgressableCells() int
|
SearchProgressableCells() int
|
||||||
}
|
}
|
||||||
@@ -40,35 +40,40 @@ func (b *Base) Init(f *field.Field) {
|
|||||||
b.field = f
|
b.field = f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) ApplyAll() []field.ExternalChange {
|
func (b *Base) ApplyAll() ([]field.ExternalChange, error) {
|
||||||
changesCopy := make([]field.ExternalChange, len(b.changes))
|
changesCopy := make([]field.ExternalChange, len(b.changes))
|
||||||
copy(changesCopy, b.changes)
|
copy(changesCopy, b.changes)
|
||||||
|
|
||||||
for _, change := range b.changes {
|
|
||||||
b.field.AddChange(&change)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.changes = b.changes[:0]
|
b.changes = b.changes[:0]
|
||||||
|
if err := b.field.AddChanges(changesCopy); err != nil {
|
||||||
return changesCopy
|
return nil, err
|
||||||
|
}
|
||||||
|
return changesCopy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) ApplyNext() field.ExternalChange {
|
func (b *Base) ApplyNext() (field.ExternalChange, error) {
|
||||||
if len(b.changes) < 1 {
|
if len(b.changes) < 1 {
|
||||||
return field.ExternalChange{}
|
return field.ExternalChange{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
changeCopy := b.changes[0]
|
changeCopy := b.changes[0]
|
||||||
|
|
||||||
b.field.AddChange(&b.changes[0])
|
|
||||||
|
|
||||||
b.changes = b.changes[1:]
|
b.changes = b.changes[1:]
|
||||||
|
if err := b.field.AddChange(&changeCopy); err != nil {
|
||||||
|
return field.ExternalChange{}, err
|
||||||
|
}
|
||||||
|
return changeCopy, nil
|
||||||
|
}
|
||||||
|
|
||||||
return changeCopy
|
func (b *Base) queue(change field.ExternalChange) {
|
||||||
|
for _, existing := range b.changes {
|
||||||
|
if existing.Cell == change.Cell && existing.Action == change.Action &&
|
||||||
|
existing.Value == change.Value && existing.From == change.From {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.changes = append(b.changes, change)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) getName() string {
|
func (b *Base) getName() string {
|
||||||
return "Unkown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) Name() string {
|
func (b *Base) Name() string {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func main() {
|
|||||||
solver.Add(&strategies.LastDigit{})
|
solver.Add(&strategies.LastDigit{})
|
||||||
solver.Add(&strategies.NakedSingle{})
|
solver.Add(&strategies.NakedSingle{})
|
||||||
solver.Add(&strategies.HiddenSingle{})
|
solver.Add(&strategies.HiddenSingle{})
|
||||||
game, err := sudoku.New(&parser.PuzzleBank{}, solver, openFile())
|
game, err := sudoku.New(&parser.PuzzleBank{}, solver, openFile(), 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+90
-14
@@ -1,30 +1,106 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import "git.kleiax.de/homepage/field"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
//TODO: const für klassik sudoku board.Props
|
"git.kleiax.de/homepage/field"
|
||||||
//TODO: funktion für [][]Cell generierung
|
)
|
||||||
|
|
||||||
|
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 {
|
type Parser interface {
|
||||||
Parse(data []byte) error
|
Parse([]byte) error
|
||||||
GetField(i int) *field.Field
|
GetField(int) (*field.Field, error)
|
||||||
GetAllFields() []field.Field
|
GetAllFields() []*field.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParserHelper struct {
|
type ParserHelper struct {
|
||||||
fields []field.Field
|
fields []*field.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ph *ParserHelper) GetField(i int) *field.Field {
|
func (ph *ParserHelper) GetField(index int) (*field.Field, error) {
|
||||||
if i < 0 || i >= len(ph.fields) {
|
if ph == nil || index < 0 || index >= len(ph.fields) {
|
||||||
return &field.Field{}
|
return nil, fmt.Errorf("%w: %d", ErrFieldIndex, index)
|
||||||
}
|
}
|
||||||
return &ph.fields[i]
|
return ph.fields[index], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ph *ParserHelper) GetAllFields() []field.Field {
|
func (ph *ParserHelper) GetAllFields() []*field.Field {
|
||||||
if len(ph.fields) == 0 {
|
if ph == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return ph.fields
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-44
@@ -2,73 +2,66 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.kleiax.de/homepage/field"
|
"git.kleiax.de/homepage/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://github.com/grantm/sudoku-exchange-puzzle-bank/tree/master
|
|
||||||
type PuzzleBank struct {
|
type PuzzleBank struct {
|
||||||
ParserHelper
|
ParserHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pb *PuzzleBank) Parse(data []byte) error {
|
func (pb *PuzzleBank) Parse(data []byte) error {
|
||||||
|
if pb == nil {
|
||||||
|
return errors.New("nil puzzle bank parser")
|
||||||
|
}
|
||||||
|
|
||||||
lines := bytes.Split(data, []byte("\n"))
|
var parsed []*field.Field
|
||||||
for _, line := range lines {
|
for index, rawLine := range bytes.Split(data, []byte("\n")) {
|
||||||
if len(line) == 0 { // Überspringe leere Zeile
|
lineNumber := index + 1
|
||||||
|
line := strings.TrimSpace(string(rawLine))
|
||||||
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(line) != 99 {
|
columns := strings.Fields(line)
|
||||||
return fmt.Errorf("Zeile hat die falsche länge. soll: 100, ist: %d", len(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
//siehe Readme in github repo
|
rating, err := strconv.ParseFloat(columns[2], 64)
|
||||||
//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 {
|
if err != nil {
|
||||||
return errors.New("can nor parse raiting")
|
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) {
|
||||||
props := field.Properties{
|
return fmt.Errorf("line %d: %w: rating must be a finite non-negative number", lineNumber, ErrInvalidInput)
|
||||||
Rows: 9,
|
|
||||||
Columns: 9,
|
|
||||||
BlockRows: 3,
|
|
||||||
BlockColumns: 3,
|
|
||||||
BlockSizeRow: 3,
|
|
||||||
BlockSizeColumn: 3,
|
|
||||||
Rating: rating,
|
|
||||||
}
|
}
|
||||||
|
parsedField, err := classicField(columns[1], rating)
|
||||||
cells := make([][]field.Cell, props.Rows)
|
if err != nil {
|
||||||
for i := range cells {
|
return fmt.Errorf("line %d: %w", lineNumber, err)
|
||||||
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))
|
parsed = append(parsed, parsedField)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(parsed) == 0 {
|
||||||
|
return ErrNoPuzzles
|
||||||
|
}
|
||||||
|
pb.fields = parsed
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-24
@@ -14,36 +14,43 @@ type Game struct {
|
|||||||
solver logic.Solver
|
solver logic.Solver
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(parser parser.Parser, solver logic.Solver, data []byte) (*Game, error) {
|
func New(input parser.Parser, solver logic.Solver, data []byte, fieldIndex int) (*Game, error) {
|
||||||
err := parser.Parse(data)
|
if input == nil {
|
||||||
if err != nil {
|
return nil, errors.New("cannot create game: nil parser")
|
||||||
return &Game{}, fmt.Errorf("can not create game: %w", err)
|
|
||||||
}
|
}
|
||||||
return &Game{solver: solver, field: parser.GetField(2)}, 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 {
|
func (g *Game) Solve() error {
|
||||||
g.solver.InitStragies(g.field)
|
if g == nil || g.field == nil {
|
||||||
for !g.isFinished() {
|
return field.ErrInvalidField
|
||||||
if !g.nextSolveStep() {
|
}
|
||||||
return errors.New("no strategy can solve the puzzle")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) nextSolveStep() bool {
|
func (g *Game) GetField() *field.Field {
|
||||||
return g.solver.Run(0)
|
if g == nil {
|
||||||
}
|
return nil
|
||||||
|
}
|
||||||
func (g *Game) prevSolveStep() {
|
return g.field
|
||||||
// far far in the future
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Game) isFinished() bool {
|
|
||||||
return g.field.IsSolved()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Game) GetField() field.Field {
|
|
||||||
return *g.field
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
- Feld immer wieder auf Validität checken (keine doppelten Zahlen in einem Part) damit keine Fehler bei den Strategien auftauchen können
|
# Offene Punkte
|
||||||
- Validator in Logic oder Field?
|
|
||||||
- kommt in field
|
Der priorisierte und aktuelle Stand befindet sich in [ROADMAP.md](ROADMAP.md).
|
||||||
- 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:
|
- gemeinsames Interface für menschlichen Solver, Backtracking und DLX
|
||||||
- backtracking Solver
|
implementieren
|
||||||
- dlx solver
|
- strukturiertes, optionales Logging an den Anwendungsgrenzen ergänzen
|
||||||
- notes (menschlicher solver)
|
- optionalen Diagnosemodus für zusätzliche Invariantenprüfungen entwickeln
|
||||||
- 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
|
- Lösungsweg, Gruppierung von Änderungen und Undo/Redo implementieren
|
||||||
- Debugging verbesser, Loglevel etc, überall sinnvolle Logs einbauen
|
- weitere menschliche Lösungsstrategien in der Reihenfolge der Roadmap ergänzen
|
||||||
- Flag oder so für sich selbst überprüfende Sachen, nur im debug modus nach jedem bisschen das Feld auf validität prüfen
|
|
||||||
|
|||||||
Reference in New Issue
Block a user