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