diff --git a/.gitignore b/.gitignore index 5b90e79..49d9ea9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ go.work.sum # env file .env +# Local instructions for coding agents +AGENTS.md diff --git a/README.md b/README.md index 077aebb..9a58cf2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,114 @@ # Sudoku -Sudoku-Puzzle-Löser mit Lösungsweg \ No newline at end of file +Ein in Go geschriebener Sudoku-Löser, der Rätsel mit nachvollziehbaren, +menschlichen Lösungsstrategien bearbeiten soll. Neben dem gelösten Feld soll +langfristig auch der Lösungsweg mit Kandidaten, Änderungen und Markierungen +verfügbar sein. + +> [!WARNING] +> Das Projekt befindet sich in einer frühen Entwicklungsphase. Der aktuelle +> Prüflauf ist wegen eines Vet-Fehlers noch nicht grün und die Demo kann während des +> Lösens abstürzen. Siehe [Bekannte Einschränkungen](#bekannte-einschränkungen). + +## Voraussetzungen + +- Go 1.25.10 oder neuer (siehe `go.mod`) +- keine externen Go-Abhängigkeiten + +## Ausführen + +Die Demo in `main.go` liest derzeit fest das dritte Rätsel aus +`data/sudoku-exchange-puzzle-bank/easy3.txt`, zeigt das Ausgangsfeld an und +startet anschließend den Solver: + +```sh +go run . +``` + +Der Pfad und die Rätselauswahl sind momentan noch nicht über +Kommandozeilenargumente konfigurierbar. + +## Entwicklung + +Alle Pakete bauen und testen: + +```sh +go test ./... +``` + +Code formatieren und statisch prüfen: + +```sh +gofmt -w main.go field/*.go parser/*.go logic/*.go logic/strategies/*.go sudoku/*.go +go vet ./... +``` + +Aktuell gibt es noch keine automatisierten Tests. Neue Funktionalität sollte +nach Möglichkeit mit paketnahen `*_test.go`-Tests ergänzt werden. + +## Architektur + +| Pfad | Aufgabe | +| --- | --- | +| `main.go` | Kleine Demo und Zusammenbau von Parser, Solver und Strategien | +| `field/` | Spielfeld, Zellen, Positionen, Zeilen, Spalten, Blöcke, Kandidaten und Änderungsprotokoll | +| `parser/` | Parser-Schnittstelle und Import des Sudoku-Exchange-Puzzle-Bank-Formats | +| `logic/` | Ablaufsteuerung des Solvers | +| `logic/strategies/` | Lösungsstrategien und gemeinsame Strategie-Basis | +| `sudoku/` | Fassade, die Parser, Feld und Solver zu einem Spiel verbindet | +| `data/` | Mitgelieferte Beispielrätsel samt eigener Herkunfts- und Lizenzhinweise | +| `todo.md` | Offene technische Ideen und Aufgaben | + +Der Datenfluss der Demo ist: + +```text +Puzzle-Bank-Datei -> parser.PuzzleBank -> field.Field + -> sudoku.Game -> logic.Solver + -> Strategien -> Änderungen am Feld +``` + +Eine Strategie implementiert `strategies.Strategy`. `SearchProgressableCells` +sammelt mögliche Änderungen, `ApplyAll` oder `ApplyNext` übernimmt sie in das +Feld. Wird eine Zahl gesetzt, entfernt der Solver den entsprechenden Kandidaten +aus der zugehörigen Zeile, Spalte und dem Block. + +Derzeit in `main.go` aktiv: + +- Kandidaten eintragen (`Notes`) +- letzte fehlende Zahl eines Bereichs (`LastDigit`) +- einzelner Kandidat einer Zelle (`NakedSingle`) +- nur einmal vorkommender Kandidat eines Bereichs (`HiddenSingle`) + +Weitere Strategietypen sind bereits als Gerüste angelegt, aber noch nicht +implementiert. + +## Eingabedaten + +`parser.PuzzleBank` erwartet das Format der +[Sudoku Exchange Puzzle Bank](data/sudoku-exchange-puzzle-bank/README.md): pro +Zeile einen 12-stelligen Hash, 81 Ziffern für das Rätsel und eine +Schwierigkeitsbewertung. `0` steht für ein leeres Feld. Leere Zeilen werden +übersprungen; alle eingelesenen Rätsel werden im Parser gespeichert. + +Die Datensätze unter `data/sudoku-exchange-puzzle-bank/` stehen unter der dort +beiliegenden separaten Lizenz. + +## Bekannte Einschränkungen + +- `field.Field.IsValid` und die `IsSolved`-Prüfungen einzelner Bereiche sind + noch nicht implementiert. Ein vollständig belegtes, aber ungültiges Feld kann + daher als gelöst gelten. +- `logic.Solver.GetSolutionPath` liefert noch keinen Lösungsweg. +- Mehrere fortgeschrittene Strategien sind nur als leere Typen vorhanden. +- `go test ./...` meldet aktuell in `field/field.go`, dass eine Ganzzahl direkt + in einen String konvertiert wird. +- Die Demo kann in `LastDigit.SearchProgressableCells` auf eine leere Zelle + zugreifen und dadurch abstürzen. +- Fehlertexte und Bezeichner sind derzeit teilweise deutsch, teilweise + englisch; einige öffentliche Namen enthalten noch Tippfehler. + +## Lizenz + +Der Programmcode steht unter der [GNU General Public License v3.0](LICENSE). +Für die mitgelieferten Rätseldaten gelten die Hinweise im jeweiligen +Unterverzeichnis. diff --git a/board/parser.go b/board/parser.go deleted file mode 100644 index c27ee0a..0000000 --- a/board/parser.go +++ /dev/null @@ -1,98 +0,0 @@ -package board - -import ( - "bytes" - "errors" - "fmt" - "strconv" -) - -type Parser interface { - Parse(data []byte) error - GetField(i int) *Field - GetAllFields() []Field -} - -type parserHelper struct { - fields []Field -} - -func (ph *parserHelper) GetField(i int) *Field { - if i < 0 || i >= len(ph.fields) { - return &Field{} - } - return &ph.fields[i] -} - -func (ph *parserHelper) GetAllFields() []Field { - if len(ph.fields) == 0 { - return nil - } - return ph.fields -} - -// https://github.com/grantm/sudoku-exchange-puzzle-bank/tree/master -type PuzzleBank struct { - parserHelper -} - -func (pb *PuzzleBank) Parse(data []byte) error { - - lines := bytes.Split(data, []byte("\n")) - for _, line := range lines { - if len(line) == 0 { // Überspringe leere Zeile - continue - } - - if len(line) != 99 { - return fmt.Errorf("Zeile hat die falsche länge. soll: 100, ist: %d", len(line)) - } - - //siehe Readme in github repo - sha1Hash := bytes.TrimSpace(line[0:12]) - sudokuStr := string(line[13:94]) - ratingStr := string(line[96:99]) - //fmt.Println(sudokuStr) - - var rating float64 - _, err := fmt.Sscanf(ratingStr, "%f", &rating) - if err != nil { - return errors.New("can nor parse raiting") - } - - field := Field{ - rows: 9, - columns: 9, - blockRows: 3, - blockColumns: 3, - blockSizeRow: 3, - blockSizeColumn: 3, - rating: rating, - sha1: sha1Hash, - } - - field.cells = make([][]Cell, field.rows) - for i := range field.cells { - field.cells[i] = make([]Cell, field.columns) - for j := range field.cells[i] { - number, err := strconv.Atoi(string(sudokuStr[i*9+j])) - if err != nil { - return fmt.Errorf("Kann Zeichen nicht in Zahl konvertieren") - } - field.cells[i][j].number = number - field.cells[i][j].Notes = &Notes{} - field.cells[i][j].Pos = &Position{ - row: i, - column: j, - blockRow: i / field.blockRows, - blockColumn: j / field.blockColumns, - inBlockRow: i % field.blockSizeRow, - inBlockColumn: j % field.blockSizeColumn, - } - } - } - //fmt.Println(field.String()) - pb.fields = append(pb.fields, field) - } - return nil -} diff --git a/board/state.go b/board/state.go deleted file mode 100644 index 40eaa2a..0000000 --- a/board/state.go +++ /dev/null @@ -1,83 +0,0 @@ -package board - -import ( - "fmt" - "strings" -) - -func (f *Field) String() string { - //TODO: auf beliebige größen anpassen - var sb strings.Builder - - // Oberer Rahmen - sb.WriteString("╔═══════╤═══════╤═══════╗\n") - - for i := range f.cells { - sb.WriteString("║ ") // Linke Rahmenseite - - for j, cell := range f.cells[i] { - // Wert ausgeben oder Punkt für 0 - val := cell.number - if val == 0 { - sb.WriteString("·") - } else { - sb.WriteString(string('0' + val)) - } - - // Trennlinien zwischen Blöcken und Zellen - if (j+1)%3 == 0 { - if j < 8 { - sb.WriteString(" │ ") - } else { - sb.WriteString(" ║\n") // Rechte Rahmenseite + Zeilenumbruch - } - } else { - sb.WriteString(" ") - } - } - - // Horizontale Trennlinien nach jeder 3. Zeile - if (i+1)%3 == 0 && i < 8 { - sb.WriteString("╟───────┼───────┼───────╢\n") - } - } - - // Unterer Rahmen - sb.WriteString("╚═══════╧═══════╧═══════╝") - - return sb.String() -} - -func (f *Field) StringNotesForNumber(n int) string { - return "StringNotesForNumber is not implementet" -} - -func (f *Field) StringNotes() string { - var str strings.Builder - fmt.Fprintf(&str, "Notes:\n") - f.ForEachCell(func(cell *Cell) { - fmt.Fprintf(&str, "Pos: %d/%d - Notes: %v\n", cell.Pos.row, cell.Pos.column, cell.Notes.numbers) - }) - return str.String() -} - -func (f *Field) IsSolved() bool { - result := true - f.ForEachCell(func(cell *Cell) { - if cell.number == 0 { - result = false - } - }) - - /* TODO: - if !f.isValid() { - return false - } - */ - - return result -} - -func (f *Field) IsValid() bool { - return false -} diff --git a/board/cell.go b/field/cell.go similarity index 91% rename from board/cell.go rename to field/cell.go index 42d7e4e..9dc9c00 100644 --- a/board/cell.go +++ b/field/cell.go @@ -1,4 +1,4 @@ -package board +package field // ────────────────────────────────────────────────────────────────────────────── // // NOTES STRUCTURE // @@ -11,10 +11,8 @@ type Notes struct { } func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) { - for _, existing := range n.numbers { - if existing == note { - return // Note already exists - } + if slices.Contains(n.numbers, note) { + return // Note already exists } field.changes = append(field.changes, Change{ Cell: cell, @@ -65,6 +63,15 @@ type Cell struct { 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) { if c.number == n { return @@ -74,7 +81,7 @@ func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) { return } - if n <= 0 || n > field.rows || n > field.columns { + if n <= 0 || n > field.props.Rows || n > field.props.Columns { return } diff --git a/board/changes.go b/field/changes.go similarity index 99% rename from board/changes.go rename to field/changes.go index eb1c7ed..cb486b0 100644 --- a/board/changes.go +++ b/field/changes.go @@ -1,4 +1,4 @@ -package board +package field import ( "image/color" diff --git a/board/errors.go b/field/errors.go similarity index 90% rename from board/errors.go rename to field/errors.go index 5169bf6..35f1a44 100644 --- a/board/errors.go +++ b/field/errors.go @@ -1,4 +1,4 @@ -package board +package field import "errors" diff --git a/board/field.go b/field/field.go similarity index 55% rename from board/field.go rename to field/field.go index 2174844..88c1d43 100644 --- a/board/field.go +++ b/field/field.go @@ -1,24 +1,40 @@ -package board +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 { - rows int - columns int - blockRows int - blockColumns int - blockSizeRow int - blockSizeColumn int - cells [][]Cell - changes []Change - sha1 []byte - rating float64 + props *Properties + cells [][]Cell + changes []Change +} + +func New(props Properties, cells [][]Cell) *Field { + field := Field{ + props: &props, + cells: cells, + } + return &field } // ────────────────────────────────────────────────────────────────────────────── // @@ -26,7 +42,7 @@ type Field struct { // ────────────────────────────────────────────────────────────────────────────── // func (f *Field) GetRow(r int) (*Row, error) { - if r >= f.rows || r < 0 { + if r >= f.props.Rows || r < 0 { return nil, outOfBound } @@ -43,14 +59,14 @@ func (f *Field) GetRow(r int) (*Row, error) { } func (f *Field) GetColumn(c int) (*Column, error) { - if c >= f.columns || c < 0 { + if c >= f.props.Columns || c < 0 { return nil, outOfBound } // Performance result := &Column{ Line: Line{ - cells: make([]*Cell, 0, f.rows), + cells: make([]*Cell, 0, f.props.Rows), }, } @@ -63,20 +79,20 @@ func (f *Field) GetColumn(c int) (*Column, error) { } func (f *Field) GetBlock(r, c int) (*Block, error) { - if r < 0 || r >= f.blockRows || c < 0 || c >= f.blockColumns { + if r < 0 || r >= f.props.BlockRows || c < 0 || c >= f.props.BlockColumns { return nil, outOfBound } - startRow := r * f.blockSizeRow - startCol := c * f.blockSizeColumn + startRow := r * f.props.BlockSizeRow + startCol := c * f.props.BlockSizeColumn block := &Block{ - cells: make([][]*Cell, f.blockSizeRow), + cells: make([][]*Cell, f.props.BlockSizeRow), } for row := range block.cells { // Effizientes Kopieren der Zeile - block.cells[row] = make([]*Cell, f.blockSizeColumn) + block.cells[row] = make([]*Cell, f.props.BlockSizeColumn) for column := range block.cells[row] { block.cells[row][column] = &f.cells[startRow+row][startCol+column] @@ -105,12 +121,16 @@ func (f *Field) GetEachPartAtPos(pos *Position) []Part { } func (f *Field) GetCell(r, c int) (*Cell, error) { - if r > f.rows || c > f.columns { + 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 // // ────────────────────────────────────────────────────────────────────────────── // @@ -137,7 +157,7 @@ func (f *Field) ForEachPartAtPos(pos *Position, fn func(part Part)) { } func (f *Field) ForEachRow(fn func(row *Row)) { - for i := range f.rows { + for i := range f.props.Rows { row, err := f.GetRow(i) if err != nil { fmt.Println(err.Error()) @@ -148,7 +168,7 @@ func (f *Field) ForEachRow(fn func(row *Row)) { } func (f *Field) ForEachColumn(fn func(column *Column)) { - for i := range f.columns { + for i := range f.props.Columns { column, err := f.GetColumn(i) if err != nil { fmt.Println(err.Error()) @@ -160,8 +180,8 @@ func (f *Field) ForEachColumn(fn func(column *Column)) { } func (f *Field) ForEachBlock(fn func(block *Block)) { - for r := range f.blockRows { - for c := range f.blockColumns { + 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()) @@ -198,3 +218,90 @@ func (f *Field) AddChange(eChange *ExternalChange) { 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 +} diff --git a/board/parts.go b/field/parts.go similarity index 99% rename from board/parts.go rename to field/parts.go index d068ccb..cecbc8f 100644 --- a/board/parts.go +++ b/field/parts.go @@ -1,4 +1,4 @@ -package board +package field import ( "fmt" diff --git a/board/position.go b/field/position.go similarity index 70% rename from board/position.go rename to field/position.go index 96a3157..1b41c73 100644 --- a/board/position.go +++ b/field/position.go @@ -1,4 +1,4 @@ -package board +package field type Position struct { row int @@ -9,6 +9,18 @@ type Position struct { inBlockColumn int } +func NewPosition(row, column, blockRow, blockColumn, inBlockRow, inBlockColumn int) *Position { + pos := Position{ + row: row, + column: column, + blockRow: blockRow, + blockColumn: blockColumn, + inBlockRow: inBlockRow, + inBlockColumn: inBlockColumn, + } + return &pos +} + func (p *Position) GetRow() int { return p.row } diff --git a/logic/solver.go b/logic/solver.go index 332f0ec..d87c946 100644 --- a/logic/solver.go +++ b/logic/solver.go @@ -1,7 +1,7 @@ package logic import ( - "git.kleiax.de/homepage/board" + "git.kleiax.de/homepage/field" "git.kleiax.de/homepage/logic/strategies" ) @@ -12,7 +12,7 @@ import ( type Solver struct { strategies []strategies.Strategy returnTo int - field *board.Field + field *field.Field conf struct { all bool repeat bool @@ -23,7 +23,7 @@ func (s *Solver) Add(strategy 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 { @@ -38,11 +38,11 @@ func (s *Solver) Run(i int) bool { } for _, change := range s.strategies[j].ApplyAll() { - if change.Action != board.ActionSetNumber { + if change.Action != field.ActionSetNumber { continue } - s.field.ForEachPartAtPos(change.Cell.Pos, func(part board.Part) { - part.ForEachCell(func(cell *board.Cell) { + 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) }) }) @@ -61,6 +61,6 @@ func (s *Solver) Search() { } -func (s *Solver) GetSolutionPath() []board.ExternalChange { +func (s *Solver) GetSolutionPath() []field.ExternalChange { return nil } diff --git a/logic/strategies/divers.go b/logic/strategies/divers.go index 8003a2c..2496687 100644 --- a/logic/strategies/divers.go +++ b/logic/strategies/divers.go @@ -1,7 +1,7 @@ package strategies import ( - "git.kleiax.de/homepage/board" + "git.kleiax.de/homepage/field" ) // ────────────────────────────────────────────────────────────────────────────── // @@ -13,20 +13,20 @@ type LastDigit struct { } func (ld *LastDigit) SearchProgressableCells() int { - ld.field.ForEachPart(func(part board.Part) { + ld.field.ForEachPart(func(part field.Part) { missingNumbers := part.GetMissingNumbers() if len(missingNumbers) == 1 { - var emptyCell *board.Cell - part.ForEachCell(func(cell *board.Cell) { + var emptyCell *field.Cell + part.ForEachCell(func(cell *field.Cell) { if cell.GetNumber() == 0 { emptyCell = cell } }) // 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 := board.ExternalChange{ + change := field.ExternalChange{ Cell: emptyCell, - Action: board.ActionSetNumber, + Action: field.ActionSetNumber, Value: missingNumbers[0], From: emptyCell.GetNumber(), TriggerdBy: ld.getName(), diff --git a/logic/strategies/hidden.go b/logic/strategies/hidden.go index 8b82366..1c523f4 100644 --- a/logic/strategies/hidden.go +++ b/logic/strategies/hidden.go @@ -1,5 +1,7 @@ package strategies +import "git.kleiax.de/homepage/field" + // ────────────────────────────────────────────────────────────────────────────── // // HIDDEN_SINGLE STRUCTURE // // ────────────────────────────────────────────────────────────────────────────── // @@ -8,6 +10,46 @@ type HiddenSingle struct { Base } +func (hs *HiddenSingle) SearchProgressableCells() int { + hs.field.ForEachPart(func(part field.Part) { + m := make(map[int]int) + + part.ForEachCell(func(cell *field.Cell) { + notes := cell.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 // // ────────────────────────────────────────────────────────────────────────────── // diff --git a/logic/strategies/naked.go b/logic/strategies/naked.go index fcb7beb..3aa214b 100644 --- a/logic/strategies/naked.go +++ b/logic/strategies/naked.go @@ -1,6 +1,6 @@ package strategies -import "git.kleiax.de/homepage/board" +import "git.kleiax.de/homepage/field" // ────────────────────────────────────────────────────────────────────────────── // // NAKED_SINGLE STRUCTURE // @@ -11,13 +11,13 @@ type NakedSingle struct { } func (ns *NakedSingle) SearchProgressableCells() int { - ns.field.ForEachPart(func(part board.Part) { - part.ForEachCell(func(cell *board.Cell) { + ns.field.ForEachPart(func(part field.Part) { + part.ForEachCell(func(cell *field.Cell) { candidates := cell.Notes.Get() if len(candidates) == 1 && cell.GetNumber() == 0 { - change := board.ExternalChange{ + change := field.ExternalChange{ Cell: cell, - Action: board.ActionSetNumber, + Action: field.ActionSetNumber, Value: candidates[0], From: cell.GetNumber(), TriggerdBy: ns.getName(), diff --git a/logic/strategies/notes.go b/logic/strategies/notes.go index 023bc4a..841416f 100644 --- a/logic/strategies/notes.go +++ b/logic/strategies/notes.go @@ -1,7 +1,7 @@ package strategies import ( - "git.kleiax.de/homepage/board" + "git.kleiax.de/homepage/field" ) // ────────────────────────────────────────────────────────────────────────────── // @@ -13,8 +13,8 @@ type Notes struct { } func (n *Notes) SearchProgressableCells() int { - n.field.ForEachRow(func(row *board.Row) { - row.ForEachCell(func(cell *board.Cell) { + 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()) @@ -22,9 +22,9 @@ func (n *Notes) SearchProgressableCells() int { if cell.Notes.Has(note) || cell.GetNumber() != 0 { continue } - ch := board.ExternalChange{ + ch := field.ExternalChange{ Cell: cell, - Action: board.ActionSetNote, + Action: field.ActionSetNote, Value: note, TriggerdBy: n.getName(), Marks: nil, diff --git a/logic/strategies/strategy.go b/logic/strategies/strategy.go index e590656..7e40bff 100644 --- a/logic/strategies/strategy.go +++ b/logic/strategies/strategy.go @@ -3,7 +3,7 @@ package strategies import ( "fmt" - "git.kleiax.de/homepage/board" + "git.kleiax.de/homepage/field" ) // ────────────────────────────────────────────────────────────────────────────── // @@ -11,9 +11,9 @@ import ( // ────────────────────────────────────────────────────────────────────────────── // type Strategy interface { - Init(f *board.Field) - ApplyAll() []board.ExternalChange - ApplyNext() board.ExternalChange + Init(f *field.Field) + ApplyAll() []field.ExternalChange + ApplyNext() field.ExternalChange Name() string SearchProgressableCells() int } @@ -23,7 +23,7 @@ type Strategy interface { // ────────────────────────────────────────────────────────────────────────────── // type StrategyVisualization interface { - pointOut(f *board.Field) []board.Mark + pointOut(f *field.Field) []field.Mark } // ────────────────────────────────────────────────────────────────────────────── // @@ -32,16 +32,16 @@ type StrategyVisualization interface { type Base struct { name string - field *board.Field - changes []board.ExternalChange + field *field.Field + changes []field.ExternalChange } -func (b *Base) Init(f *board.Field) { +func (b *Base) Init(f *field.Field) { b.field = f } -func (b *Base) ApplyAll() []board.ExternalChange { - changesCopy := make([]board.ExternalChange, len(b.changes)) +func (b *Base) ApplyAll() []field.ExternalChange { + changesCopy := make([]field.ExternalChange, len(b.changes)) copy(changesCopy, b.changes) for _, change := range b.changes { @@ -53,9 +53,9 @@ func (b *Base) ApplyAll() []board.ExternalChange { return changesCopy } -func (b *Base) ApplyNext() board.ExternalChange { +func (b *Base) ApplyNext() field.ExternalChange { if len(b.changes) < 1 { - return board.ExternalChange{} + return field.ExternalChange{} } changeCopy := b.changes[0] diff --git a/main.go b/main.go index 140a13d..8627f1e 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,9 @@ import ( "io" "os" - "git.kleiax.de/homepage/board" "git.kleiax.de/homepage/logic" "git.kleiax.de/homepage/logic/strategies" + "git.kleiax.de/homepage/parser" "git.kleiax.de/homepage/sudoku" ) @@ -16,7 +16,8 @@ func main() { solver.Add(&strategies.Notes{}) solver.Add(&strategies.LastDigit{}) solver.Add(&strategies.NakedSingle{}) - game, err := sudoku.New(&board.PuzzleBank{}, solver, openFile()) + solver.Add(&strategies.HiddenSingle{}) + game, err := sudoku.New(&parser.PuzzleBank{}, solver, openFile()) if err != nil { fmt.Println(err.Error()) os.Exit(1) diff --git a/parser/parser.go b/parser/parser.go new file mode 100644 index 0000000..63f824a --- /dev/null +++ b/parser/parser.go @@ -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 +} diff --git a/parser/puzzleBank.go b/parser/puzzleBank.go new file mode 100644 index 0000000..48884f3 --- /dev/null +++ b/parser/puzzleBank.go @@ -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 +} diff --git a/sudoku/sudoku.go b/sudoku/sudoku.go index be3dca1..96e7ba9 100644 --- a/sudoku/sudoku.go +++ b/sudoku/sudoku.go @@ -4,21 +4,22 @@ import ( "errors" "fmt" - "git.kleiax.de/homepage/board" + "git.kleiax.de/homepage/field" "git.kleiax.de/homepage/logic" + "git.kleiax.de/homepage/parser" ) type Game struct { - field *board.Field + field *field.Field solver logic.Solver } -func New(parser board.Parser, solver logic.Solver, data []byte) (*Game, error) { +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) } - return &Game{solver: solver, field: parser.GetField(0)}, nil + return &Game{solver: solver, field: parser.GetField(2)}, nil } func (g *Game) Solve() error { @@ -43,6 +44,6 @@ func (g *Game) isFinished() bool { return g.field.IsSolved() } -func (g *Game) GetField() board.Field { +func (g *Game) GetField() field.Field { return *g.field } diff --git a/todo.md b/todo.md index f41365b..84c3ab0 100644 --- a/todo.md +++ b/todo.md @@ -1,2 +1,11 @@ -- Parser kann eigenes Package sein nur das Interface und die Helfer funktionen im Board lassen, jeder kann einen Parser für sseine Quelle schreiben. Vielleicht ein Standardparser in Board für gängige Typen oder Hilfsfunktionen -- Feld immer wieder auf Validität checken (keine doppelten Zahlen in einem Part) damit keine Fehler bei den Strategien auftauchen können \ No newline at end of file +- 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 \ No newline at end of file