Compare commits

...
4 Commits
Author SHA1 Message Date
kleiax 8a5ae8c640 Document solver roadmap in README 2026-09-10 23:21:38 +02:00
kleiax a5ec338277 Refactor Sudoku solver architecture 2026-09-10 23:11:34 +02:00
kleiax 7bf4a8d725 first successful run to solve a simple puzzle 2026-07-25 06:58:15 +02:00
kleiax 8e99087a30 fix part slices were no pinters 2026-07-21 09:25:45 +02:00
27 changed files with 1085 additions and 392 deletions
+2
View File
@@ -25,3 +25,5 @@ go.work.sum
# env file # env file
.env .env
# Local instructions for coding agents
AGENTS.md
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}"
}
]
}
+113 -1
View File
@@ -1,3 +1,115 @@
# Sudoku # Sudoku
Sudoku-Puzzle-Löser mit Lösungsweg Ein in Go geschriebener Sudoku-Löser, der Rätsel mit nachvollziehbaren,
menschlichen Lösungsstrategien bearbeiten soll. Neben dem gelösten Feld soll
langfristig auch der Lösungsweg mit Kandidaten, Änderungen und Markierungen
verfügbar sein.
> [!WARNING]
> Das Projekt befindet sich in einer frühen Entwicklungsphase. Der aktuelle
> Prüflauf ist wegen eines Vet-Fehlers noch nicht grün und die Demo kann während des
> Lösens abstürzen. Siehe [Bekannte Einschränkungen](#bekannte-einschränkungen).
## Voraussetzungen
- Go 1.25.10 oder neuer (siehe `go.mod`)
- keine externen Go-Abhängigkeiten
## Ausführen
Die Demo in `main.go` liest derzeit fest das dritte Rätsel aus
`data/sudoku-exchange-puzzle-bank/easy3.txt`, zeigt das Ausgangsfeld an und
startet anschließend den Solver:
```sh
go run .
```
Der Pfad und die Rätselauswahl sind momentan noch nicht über
Kommandozeilenargumente konfigurierbar.
## Entwicklung
Alle Pakete bauen und testen:
```sh
go test ./...
```
Code formatieren und statisch prüfen:
```sh
gofmt -w main.go field/*.go parser/*.go logic/*.go logic/strategies/*.go sudoku/*.go
go vet ./...
```
Aktuell gibt es noch keine automatisierten Tests. Neue Funktionalität sollte
nach Möglichkeit mit paketnahen `*_test.go`-Tests ergänzt werden.
## Architektur
| Pfad | Aufgabe |
| --- | --- |
| `main.go` | Kleine Demo und Zusammenbau von Parser, Solver und Strategien |
| `field/` | Spielfeld, Zellen, Positionen, Zeilen, Spalten, Blöcke, Kandidaten und Änderungsprotokoll |
| `parser/` | Parser-Schnittstelle und Import des Sudoku-Exchange-Puzzle-Bank-Formats |
| `logic/` | Ablaufsteuerung des Solvers |
| `logic/strategies/` | Lösungsstrategien und gemeinsame Strategie-Basis |
| `sudoku/` | Fassade, die Parser, Feld und Solver zu einem Spiel verbindet |
| `data/` | Mitgelieferte Beispielrätsel samt eigener Herkunfts- und Lizenzhinweise |
| `todo.md` | Offene technische Ideen und Aufgaben |
| [`ROADMAP.md`](ROADMAP.md) | Priorisierter Fahrplan bis zum stabilen Solver |
Der Datenfluss der Demo ist:
```text
Puzzle-Bank-Datei -> parser.PuzzleBank -> field.Field
-> sudoku.Game -> logic.Solver
-> Strategien -> Änderungen am Feld
```
Eine Strategie implementiert `strategies.Strategy`. `SearchProgressableCells`
sammelt mögliche Änderungen, `ApplyAll` oder `ApplyNext` übernimmt sie in das
Feld. Wird eine Zahl gesetzt, entfernt der Solver den entsprechenden Kandidaten
aus der zugehörigen Zeile, Spalte und dem Block.
Derzeit in `main.go` aktiv:
- Kandidaten eintragen (`Notes`)
- letzte fehlende Zahl eines Bereichs (`LastDigit`)
- einzelner Kandidat einer Zelle (`NakedSingle`)
- nur einmal vorkommender Kandidat eines Bereichs (`HiddenSingle`)
Weitere Strategietypen sind bereits als Gerüste angelegt, aber noch nicht
implementiert.
## Eingabedaten
`parser.PuzzleBank` erwartet das Format der
[Sudoku Exchange Puzzle Bank](data/sudoku-exchange-puzzle-bank/README.md): pro
Zeile einen 12-stelligen Hash, 81 Ziffern für das Rätsel und eine
Schwierigkeitsbewertung. `0` steht für ein leeres Feld. Leere Zeilen werden
übersprungen; alle eingelesenen Rätsel werden im Parser gespeichert.
Die Datensätze unter `data/sudoku-exchange-puzzle-bank/` stehen unter der dort
beiliegenden separaten Lizenz.
## Bekannte Einschränkungen
- `field.Field.IsValid` und die `IsSolved`-Prüfungen einzelner Bereiche sind
noch nicht implementiert. Ein vollständig belegtes, aber ungültiges Feld kann
daher als gelöst gelten.
- `logic.Solver.GetSolutionPath` liefert noch keinen Lösungsweg.
- Mehrere fortgeschrittene Strategien sind nur als leere Typen vorhanden.
- `go test ./...` meldet aktuell in `field/field.go`, dass eine Ganzzahl direkt
in einen String konvertiert wird.
- Die Demo kann in `LastDigit.SearchProgressableCells` auf eine leere Zelle
zugreifen und dadurch abstürzen.
- Fehlertexte und Bezeichner sind derzeit teilweise deutsch, teilweise
englisch; einige öffentliche Namen enthalten noch Tippfehler.
## Lizenz
Der Programmcode steht unter der [GNU General Public License v3.0](LICENSE).
Für die mitgelieferten Rätseldaten gelten die Hinweise im jeweiligen
Unterverzeichnis.
+224
View File
@@ -0,0 +1,224 @@
# Entwicklungsfahrplan
Dieser Fahrplan führt das Projekt vom aktuellen Prototyp zu einem verlässlichen,
erklärbaren Sudoku-Löser. Die Reihenfolge ist absichtlich abhängigkeitsorientiert:
Neue Strategien bauen erst dann auf dem Solver auf, wenn Feldmodell, Änderungen
und Fortschrittserkennung korrekt und getestet sind.
## Zielbild
Das erste stabile Release soll:
- klassische 9x9-Sudokus sicher einlesen und validieren,
- jeden Lösungsschritt deterministisch und nachvollziehbar protokollieren,
- einfache und mittlere Rätsel mit menschlichen Strategien lösen,
- bei Stillstand einen klaren Status statt eines Panic oder Endloslaufs liefern,
- über eine kleine CLI mit Datei, Standard-Eingabe oder direkter Rätselzeichenfolge
nutzbar sein und
- durch Unit-, Integrations- und Regressionstests abgesichert sein.
Backtracking, DLX und sehr fortgeschrittene menschliche Strategien folgen auf
dieses Kern-Release.
## Phase 0 Lauffähige und getestete Basis (P0)
Zuerst wird der bestehende Zustand stabilisiert. In dieser Phase werden keine
neuen Lösungsstrategien ergänzt.
- Test-Helfer für kleine Felder und 9x9-Rätsel anlegen.
- Regressionstests für die aktuell bekannten Abstürze und Logikfehler schreiben.
- `Field.ForEachCell` so korrigieren, dass echte Zellen statt Kopien übergeben
werden.
- `Field.GetCell` gegen negative Indizes und `row == Rows` beziehungsweise
`column == Columns` absichern.
- die Zahlenausgabe in `Field.String` ohne fehlerhafte int-zu-string-Konvertierung
implementieren.
- doppelte oder widersprüchliche Änderungen aus überlappenden Zeilen, Spalten
und Blöcken verhindern; damit auch den Nil-Panic in `LastDigit` beseitigen.
- die beiden Slice-Iterationsfehler in `HiddenSingle` korrigieren: Kandidatenwerte
statt Indizes auswerten.
- in `Cell.RemoveNumber` die korrekte Aktion `ActionRemoveNumber` protokollieren.
- Nil-Eingaben bei Feld, Zelle, Position und Änderung kontrolliert als Fehler
behandeln.
- Solver-Fortschritt so erkennen, dass ein Schritt weder fälschlich Stillstand
meldet noch endlos wiederholt wird.
Abgeschlossen, wenn:
- `go test ./...` und `go vet ./...` erfolgreich sind,
- `go run .` das ausgewählte Beispiel ohne Panic beendet und
- die bekannten Fehler jeweils einen Regressionstest besitzen.
## Phase 1 Verlässliches Feldmodell (P0)
- `field.Properties` beim Erzeugen prüfen: positive Dimensionen, passende
Blockaufteilung und exakt passende Zellmatrix.
- Zahlenbereich und Position jeder Zelle prüfen; leere Zellen bleiben als `0`
repräsentiert.
- `Part.IsSolved` für Zeilen, Spalten und Blöcke implementieren.
- `Field.IsValid` implementieren: keine Duplikate, nur erlaubte Werte und
konsistente Dimensionen.
- `Field.IsSolved` an Vollständigkeit **und** Validität koppeln.
- Kandidaten-Invarianten festlegen: sortiert, eindeutig, im gültigen Bereich und
niemals an bereits belegten Zellen.
- Änderungen atomar anwenden und validieren. Ungültige Änderungen sollen einen
Fehler zurückgeben und das Feld unverändert lassen.
- lesenden Zugriff auf Eigenschaften und Änderungshistorie anbieten, ohne
interne Slices veränderbar nach außen zu geben.
- allgemeine und 9x9-spezifische Darstellung trennen; `StringNotesForNumber`
implementieren oder bis zu einem echten Bedarf aus der API entfernen.
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)
- Parserfehler vereinheitlichen und mit Zeilennummer sowie fehlerhaftem Feld
anreichern.
- Puzzle-Bank-Datensätze strikt prüfen: Hash, 81 Ziffern, Rating, Whitespace,
LF/CRLF und leere Zeilen.
- bei einem Parse-Fehler keine teilweise eingelesene Ergebnisliste übernehmen;
wiederholte `Parse`-Aufrufe dürfen alte Ergebnisse nicht ungewollt anhängen.
- `GetField` so ändern, dass ein ungültiger Index einen Fehler liefert statt
eines leeren, später abstürzenden Felds.
- die fest kodierte Auswahl `GetField(2)` aus `sudoku.New` entfernen. Das
ausgewählte Feld soll explizit übergeben oder vom Aufrufer gewählt werden.
- zusätzlich einen einfachen Parser für eine 81-stellige Rätselzeichenfolge
bereitstellen; er eignet sich für CLI, Tests und externe Nutzung.
- die Ausgangsbelegung bereits beim Erzeugen eines `Game` validieren.
Abgeschlossen, wenn fehlerhafte Eingaben ohne Panic verständliche Fehler liefern
und Parser sowie `Game` keine implizite Rätselauswahl mehr enthalten.
## Phase 3 Klare Solver-Architektur (P0/P1)
- ein Solver-Interface definieren, das später menschlichen Solver, Backtracking
und DLX austauschbar macht.
- `Run(int) bool` 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.
## Phase 4 Menschlicher Basis-Solver (P1)
Die Strategien werden in aufsteigender Komplexität implementiert. Jede Strategie
benötigt positive, negative und Konflikt-/Überlappungstests sowie mindestens ein
komplettes Beispielrätsel.
1. Kandidaten erzeugen und pflegen (`Notes`)
2. `LastDigit`
3. `NakedSingle`
4. `HiddenSingle`
5. `NakedPair` und `NakedTriple`
6. `HiddenPair` und `HiddenTriple`
7. `LockedCandidateT1`, `LockedCandidateT2` und `LockedPair`
Abgeschlossen, wenn die mitgelieferten einfachen und mittleren Beispieldaten
ohne Raten lösbar sind und jeder Schritt seine auslösende Strategie nennt.
## Phase 5 Erklärbarer Lösungsweg und Undo (P1)
- Änderungen zu fachlichen Schritten gruppieren: eine Schlussfolgerung kann aus
einer gesetzten Zahl und mehreren entfernten Kandidaten bestehen.
- `GetSolutionPath` mit defensiven Kopien implementieren.
- pro Schritt Strategie, betroffene Zellen, alter/neuer Wert, Begründung und
optionale Markierungen speichern.
- `Mark` vollständig von außen lesbar und unabhängig von internen
`Change`-Zeigern gestalten.
- Undo/Redo auf Basis atomarer Schritte implementieren; `prevSolveStep` entweder
fertigstellen oder bis dahin entfernen.
- menschenlesbare Textausgabe und ein stabiles JSON-Format für Lösungswege
ergänzen.
Abgeschlossen, wenn ein gelöstes Feld vollständig zum Ausgangszustand
zurückgesetzt und durch Wiederanwenden desselben Pfads identisch rekonstruiert
werden kann.
## Phase 6 Fortgeschrittene Strategien (P2)
Erst nach stabiler Schritt- und Markierungslogik ergänzen:
1. `XWing`
2. `XYWing`, `XYZWing`, `WXYZWingBasic`
3. `Turbot2StringKite` und `TurbotSkyscraper`
4. `SimpleColoringT1`
5. `EmptyRectangle`
6. `UniqueRectangleT1`, `UniqueRectangleT4`, `UniqueRectangleT7`
7. `XChain`, `XYChain`, `XChainLoop`, `XYChainLoop`
8. `CRP`, nachdem die genaue fachliche Bedeutung und Akzeptanzkriterien
dokumentiert wurden
Für jede Technik wird zuerst ein isoliertes Testmuster festgelegt. Ein leerer
Typ ohne Verhalten gilt nicht als implementierte Strategie.
## Phase 7 Vollständige algorithmische Solver (P2)
- Backtracking-Solver mit Minimum-Remaining-Values-Heuristik implementieren.
- optional einen DLX-/Algorithm-X-Solver ergänzen.
- Eindeutigkeit einer Lösung prüfen und die Fälle keine, genau eine oder mehrere
Lösungen unterscheiden.
- algorithmische Solver als Referenz in Tests verwenden, um Ergebnisse des
menschlichen Solvers zu validieren.
- explizit konfigurieren, ob ein algorithmischer Solver nur prüft, als Fallback
dient oder direkt löst.
Abgeschlossen, wenn alle gültigen klassischen 9x9-Rätsel entweder gelöst oder
eindeutig als unlösbar/mehrdeutig klassifiziert werden.
## Phase 8 Kommandozeile und Bedienbarkeit (P2)
- Flags für Datei, Puzzle-Index, direkte Zeichenfolge und Standard-Eingabe
hinzufügen.
- Solverart und zugelassene Strategien auswählbar machen.
- Ausgabeoptionen für Ausgangsfeld, Endfeld, Einzelschritte und JSON anbieten.
- konsistente Exit-Codes für Eingabefehler, ungültige Rätsel, Stillstand und
erfolgreiche Lösung definieren.
- Bibliothekspakete frei von direkter Konsolenausgabe halten; strukturiertes,
optionales Logging nur an den Anwendungsgrenzen einführen.
- große Puzzle-Bank-Dateien streamen, wenn nur ein Datensatz benötigt wird.
Abgeschlossen, wenn das Programm ohne Quellcodeänderung mit eigenen Rätseln
verwendet werden kann.
## Phase 9 Qualität, Performance und Release (P2/P3)
- Fuzz-Tests für Parser, Koordinatenzugriffe und Änderungsanwendung ergänzen.
- Integrationssuite über repräsentative einfache, mittlere, schwere, ungültige
und mehrdeutige Rätsel aufbauen.
- `go test -race ./...` in den Standardprüfungen aufnehmen.
- Benchmarks für Kandidatenberechnung, Strategie-Suche und vollständige Solver
hinzufügen; erst danach gezielt optimieren.
- CI für Formatierung, Tests, Vet und Race Detector einrichten.
- öffentliche APIs mit GoDoc dokumentieren und Beispiele ergänzen.
- README nach Stabilisierung von den Warnungen befreien, Versionsschema und
Changelog festlegen und ein erstes Release markieren.
## Empfohlene nächste Arbeitspakete
Diese Tickets bilden die kürzeste Route zu einem stabilen Zwischenstand:
1. 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.
Nach jedem Arbeitspaket müssen `gofmt`, `go test ./...` und `go vet ./...`
erfolgreich sein. Neue bekannte Baseline-Fehler sollen nicht angesammelt werden.
-161
View File
@@ -1,161 +0,0 @@
package board
import (
"fmt"
)
// ────────────────────────────────────────────────────────────────────────────── //
// FIELD STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Field struct {
rows int
columns int
blockRows int
blockColumns int
blockSizeRow int
blockSizeColumn int
cells [][]Cell
changes []Change
sha1 []byte
rating float64
}
// ────────────────────────────────────────────────────────────────────────────── //
// GETTER //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) GetRow(r int) (*Line, error) {
if r >= f.rows || r < 0 {
return nil, outOfBound
}
return &Line{cells: append([]Cell(nil), f.cells[r]...)}, nil
}
func (f *Field) GetColumn(c int) (*Line, error) {
if c >= f.columns || c < 0 {
return nil, outOfBound
}
// Performance
result := &Line{
cells: make([]Cell, 0, f.rows),
}
// Copying
for _, row := range f.cells {
result.cells = append(result.cells, row[c])
}
return result, nil
}
func (f *Field) GetBlock(r, c int) (*Block, error) {
if r < 0 || r >= f.blockRows || c < 0 || c >= f.blockColumns {
return nil, outOfBound
}
startRow := r * f.blockSizeRow
startCol := c * f.blockSizeColumn
block := &Block{
cells: make([][]Cell, f.blockSizeRow),
}
for row := range block.cells {
// Effizientes Kopieren der Zeile
block.cells[row] = make([]Cell, f.blockSizeColumn)
copy(block.cells[row], f.cells[startRow+row][startCol:startCol+f.blockSizeColumn])
}
return block, nil
}
func (f *Field) GetCell(r, c int) (*Cell, error) {
if r > f.rows || c > f.columns {
return nil, outOfBound
}
return &f.cells[r][c], nil
}
// ────────────────────────────────────────────────────────────────────────────── //
// FOREACH FUNCTIONS //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) ForEachPart(fn func(part Part)) {
f.ForEachRow(func(line *Line) {
fn(line)
})
f.ForEachColumn(func(line *Line) {
fn(line)
})
f.ForEachBlock(func(block *Block) {
fn(block)
})
}
func (f *Field) ForEachRow(fn func(line *Line)) {
for i := range f.rows {
line, err := f.GetRow(i)
if err != nil {
fmt.Println(err.Error())
return
}
fn(line)
}
}
func (f *Field) ForEachColumn(fn func(line *Line)) {
for i := range f.columns {
column, err := f.GetColumn(i)
if err != nil {
fmt.Println(err.Error())
return
}
fn(column)
}
}
func (f *Field) ForEachBlock(fn func(block *Block)) {
for r := range f.blockRows {
for c := range f.blockColumns {
block, err := f.GetBlock(r, c)
if err != nil {
fmt.Println(err.Error())
return
}
fn(block)
}
}
}
func (f *Field) ForEachCell(fn func(cell *Cell)) {
for _, row := range f.cells {
for _, cell := range row {
fn(&cell)
}
}
}
// ────────────────────────────────────────────────────────────────────────────── //
// MODIFIER //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) AddChange(eChange *ExternalChange) {
change := Change{
Cell: eChange.Cell,
marks: eChange.Marks,
action: eChange.Action,
value: eChange.Value,
from: eChange.From,
triggerdBy: eChange.TriggerdBy,
}
change.do()
f.changes = append(f.changes, change)
}
-97
View File
@@ -1,97 +0,0 @@
package board
import (
"bytes"
"errors"
"fmt"
"strconv"
)
type Parser interface {
Parse(data []byte) error
GetField(i int) *Field
GetAllFields() []Field
}
type parserHelper struct {
fields []Field
}
func (ph *parserHelper) GetField(i int) *Field {
if i < 0 || i >= len(ph.fields) {
return &Field{}
}
return &ph.fields[i]
}
func (ph *parserHelper) GetAllFields() []Field {
if len(ph.fields) == 0 {
return nil
}
return ph.fields
}
// https://github.com/grantm/sudoku-exchange-puzzle-bank/tree/master
type PuzzleBank struct {
parserHelper
}
func (pb *PuzzleBank) Parse(data []byte) error {
lines := bytes.Split(data, []byte("\n"))
for _, line := range lines {
if len(line) == 0 { // Überspringe leere Zeile
continue
}
if len(line) != 99 {
return fmt.Errorf("Zeile hat die falsche länge. soll: 100, ist: %d", len(line))
}
//siehe Readme in github repo
sha1Hash := bytes.TrimSpace(line[0:12])
sudokuStr := string(line[13:94])
ratingStr := string(line[96:99])
//fmt.Println(sudokuStr)
var rating float64
_, err := fmt.Sscanf(ratingStr, "%f", &rating)
if err != nil {
return errors.New("can nor parse raiting")
}
field := Field{
rows: 9,
columns: 9,
blockRows: 3,
blockColumns: 3,
blockSizeRow: 3,
blockSizeColumn: 3,
rating: rating,
sha1: sha1Hash,
}
field.cells = make([][]Cell, field.rows)
for i := range field.cells {
field.cells[i] = make([]Cell, field.columns)
for j := range field.cells[i] {
number, err := strconv.Atoi(string(sudokuStr[i*9+j]))
if err != nil {
return fmt.Errorf("Kann Zeichen nicht in Zahl konvertieren")
}
field.cells[i][j].number = number
field.cells[i][j].Pos = &Position{
row: i,
column: j,
blockRow: i / field.blockRows,
blockColumn: j / field.blockColumns,
inBlockRow: i % field.blockSizeRow,
inBlockColumn: j % field.blockSizeColumn,
}
}
}
//fmt.Println(field.String())
pb.fields = append(pb.fields, field)
}
return nil
}
-67
View File
@@ -1,67 +0,0 @@
package board
import "strings"
func (f *Field) String() string {
//TODO: auf beliebige größen anpassen
var sb strings.Builder
// Oberer Rahmen
sb.WriteString("╔═══════╤═══════╤═══════╗\n")
for i := range f.cells {
sb.WriteString("║ ") // Linke Rahmenseite
for j, cell := range f.cells[i] {
// Wert ausgeben oder Punkt für 0
val := cell.number
if val == 0 {
sb.WriteString("·")
} else {
sb.WriteString(string('0' + val))
}
// Trennlinien zwischen Blöcken und Zellen
if (j+1)%3 == 0 {
if j < 8 {
sb.WriteString(" │ ")
} else {
sb.WriteString(" ║\n") // Rechte Rahmenseite + Zeilenumbruch
}
} else {
sb.WriteString(" ")
}
}
// Horizontale Trennlinien nach jeder 3. Zeile
if (i+1)%3 == 0 && i < 8 {
sb.WriteString("╟───────┼───────┼───────╢\n")
}
}
// Unterer Rahmen
sb.WriteString("╚═══════╧═══════╧═══════╝")
return sb.String()
}
func (f *Field) IsSolved() bool {
result := true
f.ForEachCell(func(cell *Cell) {
if cell.number == 0 {
result = false
}
})
/* TODO:
if !f.isValid() {
return false
}
*/
return result
}
func (f *Field) IsValid() bool {
return false
}
+1 -1
View File
@@ -1,3 +1,3 @@
0000183b305c 451723860007000800000816000000030000005000100730040086906000204840572093000409000 1.2 0000183b305c 050703060007000800000816000000030000005000100730040086906000204840572093000409000 1.2
0001d5d6314e 302401809001000300000000000040708010780502036000090000200609003900000008800070005 1.2 0001d5d6314e 302401809001000300000000000040708010780502036000090000200609003900000008800070005 1.2
000212406270 000823001003000400070000052300960010000102000010038006830000040002000900600789000 1.2 000212406270 000823001003000400070000052300960010000102000010038006830000040002000900600789000 1.2
+19 -6
View File
@@ -1,18 +1,18 @@
package board package field
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// NOTES STRUCTURE // // NOTES STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
import "slices"
type Notes struct { type Notes struct {
numbers []int numbers []int
} }
func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) { func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) {
for _, existing := range n.numbers { if slices.Contains(n.numbers, note) {
if existing == note { return // Note already exists
return // Note already exists
}
} }
field.changes = append(field.changes, Change{ field.changes = append(field.changes, Change{
Cell: cell, Cell: cell,
@@ -49,6 +49,10 @@ func (n *Notes) Get() []int {
return copySlice return copySlice
} }
func (n *Notes) Has(i int) bool {
return slices.Contains(n.numbers, i)
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// CELL STRUCTURE // // CELL STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -59,6 +63,15 @@ type Cell struct {
Pos *Position Pos *Position
} }
func NewCell(number int, pos *Position) *Cell {
cell := Cell{
number: number,
Pos: pos,
Notes: &Notes{},
}
return &cell
}
func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) { func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) {
if c.number == n { if c.number == n {
return return
@@ -68,7 +81,7 @@ func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) {
return return
} }
if n <= 0 || n > field.rows || n > field.columns { if n <= 0 || n > field.props.Rows || n > field.props.Columns {
return return
} }
+4 -2
View File
@@ -1,6 +1,8 @@
package board package field
import "image/color" import (
"image/color"
)
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// MARK STRUCTURE // // MARK STRUCTURE //
+1 -1
View File
@@ -1,4 +1,4 @@
package board package field
import "errors" import "errors"
+307
View File
@@ -0,0 +1,307 @@
package field
import (
"fmt"
"strings"
)
// ────────────────────────────────────────────────────────────────────────────── //
// META STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Properties struct {
Rows int
Columns int
BlockRows int
BlockColumns int
BlockSizeRow int
BlockSizeColumn int
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
}
// ────────────────────────────────────────────────────────────────────────────── //
// GETTER //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) GetRow(r int) (*Row, error) {
if r >= f.props.Rows || r < 0 {
return nil, outOfBound
}
cellPtrs := make([]*Cell, len(f.cells[r]))
for i := range f.cells[r] {
cellPtrs[i] = &f.cells[r][i]
}
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
}
// 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
}
startRow := r * f.props.BlockSizeRow
startCol := c * f.props.BlockSizeColumn
block := &Block{
cells: make([][]*Cell, f.props.BlockSizeRow),
}
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]
}
}
return block, nil
}
func (f *Field) GetEachPartAtPos(pos *Position) []Part {
row, err := f.GetRow(pos.row)
if err != nil {
return nil
}
column, err := f.GetColumn(pos.column)
if err != nil {
return nil
}
block, err := f.GetBlock(pos.blockRow, pos.blockColumn)
if err != nil {
return nil
}
return append([]Part{}, row, column, block)
}
func (f *Field) GetCell(r, c int) (*Cell, error) {
if r > f.props.Rows || c > f.props.Columns {
return nil, outOfBound
}
return &f.cells[r][c], nil
}
func (f *Field) GetRating() float64 {
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) ForEachPartAtPos(pos *Position, fn func(part Part)) {
parts := f.GetEachPartAtPos(pos)
for _, part := range parts {
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 *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 *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)
}
}
}
func (f *Field) ForEachCell(fn func(cell *Cell)) {
for _, row := range f.cells {
for _, cell := range row {
fn(&cell)
}
}
}
// ────────────────────────────────────────────────────────────────────────────── //
// MODIFIER //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) AddChange(eChange *ExternalChange) {
change := Change{
Cell: eChange.Cell,
marks: eChange.Marks,
action: eChange.Action,
value: eChange.Value,
from: eChange.From,
triggerdBy: eChange.TriggerdBy,
}
change.do()
f.changes = append(f.changes, change)
}
func (f *Field) SetRating(rating float64) {
f.props.Rating = rating
}
// ────────────────────────────────────────────────────────────────────────────── //
// STATE //
// ────────────────────────────────────────────────────────────────────────────── //
func (f *Field) String() string {
//TODO: auf beliebige größen anpassen
var sb strings.Builder
// Oberer Rahmen
sb.WriteString("╔═══════╤═══════╤═══════╗\n")
for i := range f.cells {
sb.WriteString("║ ") // Linke Rahmenseite
for j, cell := range f.cells[i] {
// Wert ausgeben oder Punkt für 0
val := cell.number
if val == 0 {
sb.WriteString("·")
} else {
sb.WriteString(string('0' + val))
}
// Trennlinien zwischen Blöcken und Zellen
if (j+1)%3 == 0 {
if j < 8 {
sb.WriteString(" │ ")
} else {
sb.WriteString(" ║\n") // Rechte Rahmenseite + Zeilenumbruch
}
} else {
sb.WriteString(" ")
}
}
// Horizontale Trennlinien nach jeder 3. Zeile
if (i+1)%3 == 0 && i < 8 {
sb.WriteString("╟───────┼───────┼───────╢\n")
}
}
// Unterer Rahmen
sb.WriteString("╚═══════╧═══════╧═══════╝")
return sb.String()
}
func (f *Field) StringNotesForNumber(n int) string {
return "StringNotesForNumber is not implementet"
}
func (f *Field) StringNotes() string {
var str strings.Builder
fmt.Fprintf(&str, "Notes:\n")
f.ForEachCell(func(cell *Cell) {
fmt.Fprintf(&str, "Pos: %d/%d - Notes: %v\n", cell.Pos.row, cell.Pos.column, cell.Notes.numbers)
})
return str.String()
}
func (f *Field) IsSolved() bool {
result := true
f.ForEachCell(func(cell *Cell) {
if cell.number == 0 {
result = false
}
})
/* TODO:
if !f.isValid() {
return false
}
*/
return result
}
func (f *Field) IsValid() bool {
//In jedem Part gibt es jede Zahl max ein mal
//Die Zahl kommt nicht in intersecting parts vor
return false
}
+63 -6
View File
@@ -1,10 +1,16 @@
package board package field
import (
"fmt"
"strings"
)
type Part interface { type Part interface {
IsSolved() bool IsSolved() bool
ForEachCell(fn func(cell *Cell)) ForEachCell(fn func(cell *Cell))
GetMissingNumbers() []int GetMissingNumbers() []int
RemoveNote(field *Field, note int, trigger string, marks []Mark) RemoveNote(field *Field, note int, trigger string, marks []Mark)
String() string
} }
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -12,7 +18,7 @@ type Part interface {
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
type Line struct { type Line struct {
cells []Cell cells []*Cell
} }
func (l *Line) IsSolved() bool { func (l *Line) IsSolved() bool {
@@ -22,7 +28,7 @@ func (l *Line) IsSolved() bool {
func (l *Line) ForEachCell(fn func(cell *Cell)) { func (l *Line) ForEachCell(fn func(cell *Cell)) {
for _, cell := range l.cells { for _, cell := range l.cells {
fn(&cell) fn(cell)
} }
} }
@@ -36,12 +42,40 @@ func (l *Line) RemoveNote(field *Field, note int, trigger string, marks []Mark)
}) })
} }
func (l *Line) String() string {
return lineString(l, "Line")
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// BLOCK STRUCTURE // // ROW STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Row struct {
Line
}
func (r *Row) String() string {
return lineString(r, "Row")
}
// ────────────────────────────────────────────────────────────────────────────── //
// COLUMN STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Column struct {
Line
}
func (c *Column) String() string {
return lineString(c, "Column")
}
// ────────────────────────────────────────────────────────────────────────────── //
// BLOCK STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
type Block struct { type Block struct {
cells [][]Cell cells [][]*Cell
} }
func (b *Block) IsSolved() bool { func (b *Block) IsSolved() bool {
@@ -52,7 +86,7 @@ func (b *Block) IsSolved() bool {
func (b *Block) ForEachCell(fn func(cell *Cell)) { func (b *Block) ForEachCell(fn func(cell *Cell)) {
for _, row := range b.cells { for _, row := range b.cells {
for _, cell := range row { for _, cell := range row {
fn(&cell) fn(cell)
} }
} }
} }
@@ -68,10 +102,33 @@ func (b *Block) RemoveNote(field *Field, note int, trigger string, marks []Mark)
}) })
} }
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()
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// HELPER // // HELPER //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
func lineString(part Part, name string) string {
var str strings.Builder
fmt.Fprintf(&str, "%s:\t", name)
part.ForEachCell(func(cell *Cell) {
fmt.Fprintf(&str, "%d ", cell.number)
})
return str.String()
}
func getMissingNumbersHelper(max int, iterate func(fn func(cell *Cell))) []int { func getMissingNumbersHelper(max int, iterate func(fn func(cell *Cell))) []int {
present := make(map[int]bool, max) present := make(map[int]bool, max)
+13 -1
View File
@@ -1,4 +1,4 @@
package board package field
type Position struct { type Position struct {
row int row int
@@ -9,6 +9,18 @@ type Position struct {
inBlockColumn int inBlockColumn int
} }
func NewPosition(row, column, blockRow, blockColumn, inBlockRow, inBlockColumn int) *Position {
pos := Position{
row: row,
column: column,
blockRow: blockRow,
blockColumn: blockColumn,
inBlockRow: inBlockRow,
inBlockColumn: inBlockColumn,
}
return &pos
}
func (p *Position) GetRow() int { func (p *Position) GetRow() int {
return p.row return p.row
} }
-4
View File
@@ -1,4 +0,0 @@
package logic
type Change struct {
}
-1
View File
@@ -1 +0,0 @@
package logic
+29 -9
View File
@@ -1,7 +1,7 @@
package logic package logic
import ( import (
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
"git.kleiax.de/homepage/logic/strategies" "git.kleiax.de/homepage/logic/strategies"
) )
@@ -11,6 +11,8 @@ import (
type Solver struct { type Solver struct {
strategies []strategies.Strategy strategies []strategies.Strategy
returnTo int
field *field.Field
conf struct { conf struct {
all bool all bool
repeat bool repeat bool
@@ -21,26 +23,44 @@ func (s *Solver) Add(strategy strategies.Strategy) {
s.strategies = append(s.strategies, strategy) s.strategies = append(s.strategies, strategy)
} }
func (s *Solver) InitStragies(field *board.Field) { func (s *Solver) InitStragies(field *field.Field) {
s.returnTo = 1
s.field = field
for _, strategy := range s.strategies { for _, strategy := range s.strategies {
strategy.Init(field) strategy.Init(field)
} }
} }
func (s *Solver) Run(i int) bool { func (s *Solver) Run(i int) bool {
//TODO: clean und Fehlerauffangen for j := 0; j < len(s.strategies); j++ {
s.strategies[i].SearchProgressableCells() if s.strategies[j].SearchProgressableCells() == 0 {
numberOfChanges := s.strategies[i].ApplyAll() continue
if numberOfChanges == 0 { }
return false
for _, change := range s.strategies[j].ApplyAll() {
if change.Action != field.ActionSetNumber {
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)
})
})
}
j = s.returnTo - 1
} }
return true
if s.field.IsSolved() {
return true
}
return false
} }
func (s *Solver) Search() { func (s *Solver) Search() {
} }
func (s *Solver) GetSolutionPath() []board.ExternalChange { func (s *Solver) GetSolutionPath() []field.ExternalChange {
return nil return nil
} }
+12 -8
View File
@@ -1,6 +1,8 @@
package strategies package strategies
import "git.kleiax.de/homepage/board" import (
"git.kleiax.de/homepage/field"
)
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// LAST_DIGIT STRUCTURE // // LAST_DIGIT STRUCTURE //
@@ -11,18 +13,20 @@ type LastDigit struct {
} }
func (ld *LastDigit) SearchProgressableCells() int { func (ld *LastDigit) SearchProgressableCells() int {
ld.field.ForEachPart(func(part board.Part) { ld.field.ForEachPart(func(part field.Part) {
missingNumbers := part.GetMissingNumbers() missingNumbers := part.GetMissingNumbers()
if len(missingNumbers) == 1 { if len(missingNumbers) == 1 {
var emptyCell *board.Cell var emptyCell *field.Cell
part.ForEachCell(func(cell *board.Cell) { part.ForEachCell(func(cell *field.Cell) {
if cell.GetNumber() == 0 { if cell.GetNumber() == 0 {
emptyCell = cell emptyCell = cell
} }
}) })
change := board.ExternalChange{ // 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)
change := field.ExternalChange{
Cell: emptyCell, Cell: emptyCell,
Action: board.ActionSetNumber, Action: field.ActionSetNumber,
Value: missingNumbers[0], Value: missingNumbers[0],
From: emptyCell.GetNumber(), From: emptyCell.GetNumber(),
TriggerdBy: ld.getName(), TriggerdBy: ld.getName(),
@@ -30,12 +34,12 @@ func (ld *LastDigit) SearchProgressableCells() int {
ld.changes = append(ld.changes, change) ld.changes = append(ld.changes, change)
} }
}) })
//fmt.Printf("LastDigit Changes %d", len(ld.changes)) // fmt.Printf("LastDigit Changes %d\n", len(ld.changes))
return len(ld.changes) return len(ld.changes)
} }
func (ld *LastDigit) getName() string { func (ld *LastDigit) getName() string {
return "Last Digit - Letzte Zahl" return "Last Digit"
} }
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
+42
View File
@@ -1,5 +1,7 @@
package strategies package strategies
import "git.kleiax.de/homepage/field"
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// HIDDEN_SINGLE STRUCTURE // // HIDDEN_SINGLE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -8,6 +10,46 @@ type HiddenSingle struct {
Base Base
} }
func (hs *HiddenSingle) SearchProgressableCells() int {
hs.field.ForEachPart(func(part field.Part) {
m := make(map[int]int)
part.ForEachCell(func(cell *field.Cell) {
notes := cell.Notes.Get()
for note := range notes {
m[note]++
}
})
var hiddenSingles []int
for num, count := range m {
if count == 1 {
hiddenSingles = append(hiddenSingles, num)
}
}
for num := range hiddenSingles {
part.ForEachCell(func(cell *field.Cell) {
if cell.Notes.Has(num) {
change := field.ExternalChange{
Cell: cell,
Action: field.ActionSetNumber,
Value: num,
From: cell.GetNumber(),
TriggerdBy: hs.getName(),
}
hs.changes = append(hs.changes, change)
}
})
}
})
return len(hs.changes)
}
func (hs *HiddenSingle) getName() string {
return "Hidden Single"
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// HIDDEN_PAIR STRUCTURE // // HIDDEN_PAIR STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
+10 -6
View File
@@ -1,6 +1,6 @@
package strategies package strategies
import "git.kleiax.de/homepage/board" import "git.kleiax.de/homepage/field"
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// NAKED_SINGLE STRUCTURE // // NAKED_SINGLE STRUCTURE //
@@ -11,13 +11,13 @@ type NakedSingle struct {
} }
func (ns *NakedSingle) SearchProgressableCells() int { func (ns *NakedSingle) SearchProgressableCells() int {
ns.field.ForEachPart(func(part board.Part) { ns.field.ForEachPart(func(part field.Part) {
part.ForEachCell(func(cell *board.Cell) { part.ForEachCell(func(cell *field.Cell) {
candidates := cell.Notes.Get() candidates := cell.Notes.Get()
if len(candidates) == 1 { if len(candidates) == 1 && cell.GetNumber() == 0 {
change := board.ExternalChange{ change := field.ExternalChange{
Cell: cell, Cell: cell,
Action: board.ActionSetNumber, Action: field.ActionSetNumber,
Value: candidates[0], Value: candidates[0],
From: cell.GetNumber(), From: cell.GetNumber(),
TriggerdBy: ns.getName(), TriggerdBy: ns.getName(),
@@ -29,6 +29,10 @@ func (ns *NakedSingle) SearchProgressableCells() int {
return len(ns.changes) return len(ns.changes)
} }
func (ns *NakedSingle) getName() string {
return "Naked Single"
}
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
// NAKED_DOUBLE STRUCTURE // // NAKED_DOUBLE STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
+81
View File
@@ -0,0 +1,81 @@
package strategies
import (
"git.kleiax.de/homepage/field"
)
// ────────────────────────────────────────────────────────────────────────────── //
// NOTES STRUCTURE //
// ────────────────────────────────────────────────────────────────────────────── //
type Notes struct {
Base
}
func (n *Notes) SearchProgressableCells() int {
n.field.ForEachRow(func(row *field.Row) {
row.ForEachCell(func(cell *field.Cell) {
column, _ := n.field.GetColumn(cell.Pos.GetColumn())
block, _ := n.field.GetBlock(cell.Pos.GetBlockRow(), cell.Pos.GetBlockColumn())
candidates := intersection3(row.GetMissingNumbers(), column.GetMissingNumbers(), block.GetMissingNumbers())
for _, note := range candidates {
if cell.Notes.Has(note) || cell.GetNumber() != 0 {
continue
}
ch := field.ExternalChange{
Cell: cell,
Action: field.ActionSetNote,
Value: note,
TriggerdBy: n.getName(),
Marks: nil,
From: 0,
}
n.changes = append(n.changes, ch)
}
})
})
return len(n.changes)
}
func (n *Notes) getName() string {
return "Make Notes"
}
func intersection3(a, b, c []int) []int {
set := make(map[int]bool)
for _, v := range a {
set[v] = true
}
// Nur Werte behalten, die auch in b vorkommen
inB := make(map[int]bool)
for _, v := range b {
inB[v] = true
}
for v := range set {
if !inB[v] {
delete(set, v)
}
}
// Nur Werte behalten, die auch in c vorkommen
inC := make(map[int]bool)
for _, v := range c {
inC[v] = true
}
for v := range set {
if !inC[v] {
delete(set, v)
}
}
result := make([]int, 0, len(set))
for v := range set {
result = append(result, v)
}
return result
}
+24 -13
View File
@@ -3,7 +3,7 @@ package strategies
import ( import (
"fmt" "fmt"
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
) )
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -11,9 +11,9 @@ import (
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
type Strategy interface { type Strategy interface {
Init(f *board.Field) Init(f *field.Field)
ApplyAll() int ApplyAll() []field.ExternalChange
ApplyNext() bool ApplyNext() field.ExternalChange
Name() string Name() string
SearchProgressableCells() int SearchProgressableCells() int
} }
@@ -23,7 +23,7 @@ type Strategy interface {
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
type StrategyVisualization interface { type StrategyVisualization interface {
pointOut(f *board.Field) []board.Mark pointOut(f *field.Field) []field.Mark
} }
// ────────────────────────────────────────────────────────────────────────────── // // ────────────────────────────────────────────────────────────────────────────── //
@@ -32,28 +32,39 @@ type StrategyVisualization interface {
type Base struct { type Base struct {
name string name string
field *board.Field field *field.Field
changes []board.ExternalChange changes []field.ExternalChange
} }
func (b *Base) Init(f *board.Field) { func (b *Base) Init(f *field.Field) {
b.field = f b.field = f
} }
func (b *Base) ApplyAll() int { func (b *Base) ApplyAll() []field.ExternalChange {
changesCopy := make([]field.ExternalChange, len(b.changes))
copy(changesCopy, b.changes)
for _, change := range b.changes { for _, change := range b.changes {
b.field.AddChange(&change) b.field.AddChange(&change)
} }
return len(b.changes)
b.changes = b.changes[:0]
return changesCopy
} }
func (b *Base) ApplyNext() bool { func (b *Base) ApplyNext() field.ExternalChange {
if len(b.changes) < 1 { if len(b.changes) < 1 {
return false return field.ExternalChange{}
} }
changeCopy := b.changes[0]
b.field.AddChange(&b.changes[0]) b.field.AddChange(&b.changes[0])
b.changes = b.changes[1:] b.changes = b.changes[1:]
return true
return changeCopy
} }
func (b *Base) getName() string { func (b *Base) getName() string {
+4 -2
View File
@@ -5,17 +5,19 @@ import (
"io" "io"
"os" "os"
"git.kleiax.de/homepage/board"
"git.kleiax.de/homepage/logic" "git.kleiax.de/homepage/logic"
"git.kleiax.de/homepage/logic/strategies" "git.kleiax.de/homepage/logic/strategies"
"git.kleiax.de/homepage/parser"
"git.kleiax.de/homepage/sudoku" "git.kleiax.de/homepage/sudoku"
) )
func main() { func main() {
solver := logic.Solver{} solver := logic.Solver{}
solver.Add(&strategies.Notes{})
solver.Add(&strategies.LastDigit{}) solver.Add(&strategies.LastDigit{})
solver.Add(&strategies.NakedSingle{}) solver.Add(&strategies.NakedSingle{})
game, err := sudoku.New(&board.PuzzleBank{}, solver, openFile()) solver.Add(&strategies.HiddenSingle{})
game, err := sudoku.New(&parser.PuzzleBank{}, solver, openFile())
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
os.Exit(1) os.Exit(1)
+30
View File
@@ -0,0 +1,30 @@
package parser
import "git.kleiax.de/homepage/field"
//TODO: const für klassik sudoku board.Props
//TODO: funktion für [][]Cell generierung
type Parser interface {
Parse(data []byte) error
GetField(i int) *field.Field
GetAllFields() []field.Field
}
type ParserHelper struct {
fields []field.Field
}
func (ph *ParserHelper) GetField(i int) *field.Field {
if i < 0 || i >= len(ph.fields) {
return &field.Field{}
}
return &ph.fields[i]
}
func (ph *ParserHelper) GetAllFields() []field.Field {
if len(ph.fields) == 0 {
return nil
}
return ph.fields
}
+74
View File
@@ -0,0 +1,74 @@
package parser
import (
"bytes"
"errors"
"fmt"
"strconv"
"git.kleiax.de/homepage/field"
)
// https://github.com/grantm/sudoku-exchange-puzzle-bank/tree/master
type PuzzleBank struct {
ParserHelper
}
func (pb *PuzzleBank) Parse(data []byte) error {
lines := bytes.Split(data, []byte("\n"))
for _, line := range lines {
if len(line) == 0 { // Überspringe leere Zeile
continue
}
if len(line) != 99 {
return fmt.Errorf("Zeile hat die falsche länge. soll: 100, ist: %d", len(line))
}
//siehe Readme in github repo
//sha1Hash := bytes.TrimSpace(line[0:12]) // Wird aktuell nicht gebraucht, später zum sudoku vergleichen
sudokuStr := string(line[13:94])
ratingStr := string(line[96:99])
//fmt.Println(sudokuStr)
var rating float64
_, err := fmt.Sscanf(ratingStr, "%f", &rating)
if err != nil {
return errors.New("can nor parse raiting")
}
props := field.Properties{
Rows: 9,
Columns: 9,
BlockRows: 3,
BlockColumns: 3,
BlockSizeRow: 3,
BlockSizeColumn: 3,
Rating: rating,
}
cells := make([][]field.Cell, props.Rows)
for i := range cells {
cells[i] = make([]field.Cell, props.Columns)
for j := range cells[i] {
number, err := strconv.Atoi(string(sudokuStr[i*9+j]))
if err != nil {
return fmt.Errorf("Kann Zeichen nicht in Zahl konvertieren")
}
pos := field.NewPosition(
i,
j,
i/props.BlockRows,
j/props.BlockColumns,
i%props.BlockRows,
j%props.BlockColumns)
cells[i][j] = *field.NewCell(number, pos)
}
}
pb.fields = append(pb.fields, *field.New(props, cells))
}
return nil
}
+6 -5
View File
@@ -4,21 +4,22 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.kleiax.de/homepage/board" "git.kleiax.de/homepage/field"
"git.kleiax.de/homepage/logic" "git.kleiax.de/homepage/logic"
"git.kleiax.de/homepage/parser"
) )
type Game struct { type Game struct {
field *board.Field field *field.Field
solver logic.Solver solver logic.Solver
} }
func New(parser board.Parser, solver logic.Solver, data []byte) (*Game, error) { func New(parser parser.Parser, solver logic.Solver, data []byte) (*Game, error) {
err := parser.Parse(data) err := parser.Parse(data)
if err != nil { if err != nil {
return &Game{}, fmt.Errorf("can not create game: %w", err) return &Game{}, fmt.Errorf("can not create game: %w", err)
} }
return &Game{solver: solver, field: parser.GetField(0)}, nil return &Game{solver: solver, field: parser.GetField(2)}, nil
} }
func (g *Game) Solve() error { func (g *Game) Solve() error {
@@ -43,6 +44,6 @@ func (g *Game) isFinished() bool {
return g.field.IsSolved() return g.field.IsSolved()
} }
func (g *Game) GetField() board.Field { func (g *Game) GetField() field.Field {
return *g.field return *g.field
} }
+11 -1
View File
@@ -1 +1,11 @@
- Parser kann eigenes Package sein nur das Interface und die Helfer funktionen im Board lassen, jeder kann einen Parser für sseine Quelle schreiben. Vielleicht ein Standardparser in Board für gängige Typen oder Hilfsfunktionen - Feld immer wieder auf Validität checken (keine doppelten Zahlen in einem Part) damit keine Fehler bei den Strategien auftauchen können
- 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