Refactor Sudoku field and solver implementation

This commit is contained in:
2026-09-16 22:52:44 +02:00
parent 8a5ae8c640
commit f7f9c16184
24 changed files with 1574 additions and 603 deletions
+42 -88
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+244
View File
@@ -0,0 +1,244 @@
package field
import (
"errors"
"reflect"
"strings"
"testing"
)
const solvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
func classicCells(t *testing.T, digits string) (Properties, [][]Cell) {
t.Helper()
if len(digits) != 81 {
t.Fatalf("test puzzle has %d characters", len(digits))
}
props := Properties{Rows: 9, Columns: 9, BlockRows: 3, BlockColumns: 3, BlockSizeRow: 3, BlockSizeColumn: 3}
cells := make([][]Cell, props.Rows)
for row := range cells {
cells[row] = make([]Cell, props.Columns)
for column := range cells[row] {
position := NewPosition(row, column, row/3, column/3, row%3, column%3)
cells[row][column] = *NewCell(int(digits[row*9+column]-'0'), position)
}
}
return props, cells
}
func mustField(t *testing.T, digits string) *Field {
t.Helper()
props, cells := classicCells(t, digits)
result, err := New(props, cells)
if err != nil {
t.Fatalf("New() error = %v", err)
}
return result
}
func TestNewValidatesStructureAndValues(t *testing.T) {
props, cells := classicCells(t, strings.Repeat("0", 81))
tests := []struct {
name string
mutate func(*Properties, [][]Cell)
}{
{name: "missing row", mutate: func(_ *Properties, cells [][]Cell) { cells[0] = cells[0][:8] }},
{name: "wrong position", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].pos = NewPosition(1, 0, 0, 0, 0, 0) }},
{name: "number out of range", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].number = 10 }},
{name: "invalid blocks", mutate: func(props *Properties, _ [][]Cell) { props.BlockRows = 2 }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
localProps := props
localCells := make([][]Cell, len(cells))
for row := range cells {
localCells[row] = append([]Cell(nil), cells[row]...)
}
test.mutate(&localProps, localCells)
if _, err := New(localProps, localCells); !errors.Is(err, ErrInvalidField) {
t.Fatalf("New() error = %v, want ErrInvalidField", err)
}
})
}
duplicate := "113456789" + solvedPuzzle[9:]
duplicateProps, duplicateCells := classicCells(t, duplicate)
if _, err := New(duplicateProps, duplicateCells); !errors.Is(err, ErrInvalidField) {
t.Fatalf("New() duplicate error = %v, want ErrInvalidField", err)
}
}
func TestGetCellBoundsAndForEachCellPointers(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
for _, coordinates := range [][2]int{{-1, 0}, {0, -1}, {9, 0}, {0, 9}} {
if _, err := f.GetCell(coordinates[0], coordinates[1]); !errors.Is(err, ErrOutOfBounds) {
t.Errorf("GetCell(%d, %d) error = %v", coordinates[0], coordinates[1], err)
}
}
want, _ := f.GetCell(4, 5)
var found *Cell
f.ForEachCell(func(cell *Cell) {
if cell.GetPosition().GetRow() == 4 && cell.GetPosition().GetColumn() == 5 {
found = cell
}
})
if found != want {
t.Fatalf("ForEachCell() returned %p, want stored cell %p", found, want)
}
}
func TestValidityAndSolvedState(t *testing.T) {
incomplete := mustField(t, strings.Repeat("0", 81))
if !incomplete.IsValid() || incomplete.IsSolved() {
t.Fatalf("empty field: IsValid=%v IsSolved=%v", incomplete.IsValid(), incomplete.IsSolved())
}
solved := mustField(t, solvedPuzzle)
if !solved.IsValid() || !solved.IsSolved() {
t.Fatalf("solved field: IsValid=%v IsSolved=%v", solved.IsValid(), solved.IsSolved())
}
row, _ := solved.GetRow(0)
block, _ := solved.GetBlock(0, 0)
if !row.IsSolved() || !block.IsSolved() {
t.Fatal("completed row and block must be solved")
}
solved.cells[0][0].number = solved.cells[0][1].number
if solved.IsValid() || solved.IsSolved() {
t.Fatal("field with a duplicate must be invalid and unsolved")
}
}
func TestChangesAreValidatedAndAtomic(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
first, _ := f.GetCell(0, 0)
second, _ := f.GetCell(0, 1)
err := f.AddChanges([]ExternalChange{
{Cell: first, Action: ActionSetNumber, Value: 1, From: 0},
{Cell: second, Action: ActionSetNumber, Value: 1, From: 0},
})
if !errors.Is(err, ErrInvalidChange) {
t.Fatalf("AddChanges() error = %v, want ErrInvalidChange", err)
}
if first.GetNumber() != 0 || second.GetNumber() != 0 || len(f.GetChanges()) != 0 {
t.Fatal("failed change batch was not rolled back")
}
foreign := NewCell(0, NewPosition(0, 0, 0, 0, 0, 0))
if err := f.AddChange(&ExternalChange{Cell: foreign, Action: ActionSetNumber, Value: 1}); !errors.Is(err, ErrInvalidChange) {
t.Fatalf("foreign cell error = %v, want ErrInvalidChange", err)
}
if err := (*Field)(nil).AddChanges(nil); !errors.Is(err, ErrInvalidField) {
t.Fatalf("nil field error = %v, want ErrInvalidField", err)
}
}
func TestNotesStaySortedUniqueAndArePropagated(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
target, _ := f.GetCell(0, 0)
rowPeer, _ := f.GetCell(0, 1)
columnPeer, _ := f.GetCell(1, 0)
blockPeer, _ := f.GetCell(1, 1)
unrelated, _ := f.GetCell(4, 4)
for _, note := range []int{5, 1, 5} {
if err := target.GetNotes().Add(f, target, note, "test", nil); err != nil {
t.Fatalf("add target note: %v", err)
}
}
if got, want := target.GetNotes().Get(), []int{1, 5}; !reflect.DeepEqual(got, want) {
t.Fatalf("notes = %v, want %v", got, want)
}
for _, cell := range []*Cell{rowPeer, columnPeer, blockPeer, unrelated} {
if err := cell.GetNotes().Add(f, cell, 5, "test", nil); err != nil {
t.Fatalf("add peer note: %v", err)
}
}
if err := target.SetNumber(f, 5, "test", nil); err != nil {
t.Fatalf("SetNumber() error = %v", err)
}
for _, peer := range []*Cell{target, rowPeer, columnPeer, blockPeer} {
if peer.GetNotes().Has(5) {
row, column := peer.GetPosition().GetCoords()
t.Fatalf("note 5 was not removed at %d/%d", row, column)
}
}
if !unrelated.GetNotes().Has(5) {
t.Fatal("note was removed from an unrelated cell")
}
if f.IsValid() == false {
t.Fatal("field must remain valid after candidate propagation")
}
filled, _ := f.GetCell(0, 2)
if err := filled.SetNumber(f, 2, "test", nil); err != nil {
t.Fatal(err)
}
if err := filled.GetNotes().Add(f, filled, 3, "test", nil); !errors.Is(err, ErrInvalidChange) {
t.Fatalf("note on filled cell error = %v, want ErrInvalidChange", err)
}
if err := rowPeer.GetNotes().Add(f, rowPeer, 2, "test", nil); !errors.Is(err, ErrInvalidChange) {
t.Fatalf("invalid peer note error = %v, want ErrInvalidChange", err)
}
}
func TestRemoveNumberRecordsCorrectAction(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
cell, _ := f.GetCell(0, 0)
if err := cell.SetNumber(f, 1, "set", nil); err != nil {
t.Fatal(err)
}
if err := cell.RemoveNumber(f, "remove", nil); err != nil {
t.Fatal(err)
}
changes := f.GetChanges()
if changes[len(changes)-1].Action != ActionRemoveNumber {
t.Fatalf("last action = %v, want ActionRemoveNumber", changes[len(changes)-1].Action)
}
}
func TestStringRendersDigitsAndNotes(t *testing.T) {
f := mustField(t, "900000000"+strings.Repeat("0", 72))
cell, _ := f.GetCell(0, 1)
if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(f.String(), "9 · ·") {
t.Fatalf("String() = %q", f.String())
}
if got := f.StringNotesForNumber(1); got != "0/1" {
t.Fatalf("StringNotesForNumber() = %q, want 0/1", got)
}
}
func TestGettersReturnDefensiveCopies(t *testing.T) {
f := mustField(t, strings.Repeat("0", 81))
props, err := f.GetProperties()
if err != nil {
t.Fatal(err)
}
props.Rows = 4
stored, _ := f.GetProperties()
if stored.Rows != 9 {
t.Fatal("GetProperties exposed internal properties")
}
cell, _ := f.GetCell(0, 0)
if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil {
t.Fatal(err)
}
notes := cell.GetNotes().Get()
notes[0] = 9
if !cell.GetNotes().Has(1) || cell.GetNotes().Has(9) {
t.Fatal("Notes.Get exposed its backing slice")
}
changes := f.GetChanges()
changes[0].Value = 9
if f.GetChanges()[0].Value != 1 {
t.Fatal("GetChanges exposed its backing slice")
}
}
+79 -67
View File
@@ -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
+27
View File
@@ -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
}