Refactor Sudoku field and solver implementation
This commit is contained in:
+42
-88
@@ -1,123 +1,77 @@
|
||||
package field
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// NOTES STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
import "slices"
|
||||
|
||||
type Notes struct {
|
||||
numbers []int
|
||||
}
|
||||
|
||||
func (n *Notes) Add(field *Field, cell *Cell, note int, trigger string, marks []Mark) {
|
||||
if slices.Contains(n.numbers, note) {
|
||||
return // Note already exists
|
||||
func (n *Notes) Add(f *Field, cell *Cell, note int, trigger string, marks []Mark) error {
|
||||
if n == nil || cell == nil || cell.notes != n {
|
||||
return ErrInvalidCell
|
||||
}
|
||||
field.changes = append(field.changes, Change{
|
||||
Cell: cell,
|
||||
marks: marks,
|
||||
action: ActionSetNote,
|
||||
value: note,
|
||||
from: 0,
|
||||
triggerdBy: trigger,
|
||||
})
|
||||
n.numbers = append(n.numbers, note)
|
||||
return f.AddChange(&ExternalChange{Cell: cell, Marks: marks, Action: ActionSetNote, Value: note, TriggerdBy: trigger})
|
||||
}
|
||||
|
||||
func (n *Notes) Remove(field *Field, cell *Cell, note int, trigger string, marks []Mark) {
|
||||
for i, existing := range n.numbers {
|
||||
if existing == note {
|
||||
field.changes = append(field.changes, Change{
|
||||
Cell: cell,
|
||||
marks: marks,
|
||||
action: ActionRemoveNote,
|
||||
value: 0,
|
||||
from: note,
|
||||
triggerdBy: trigger,
|
||||
})
|
||||
|
||||
n.numbers = append(n.numbers[:i], n.numbers[i+1:]...)
|
||||
return
|
||||
}
|
||||
func (n *Notes) Remove(f *Field, cell *Cell, note int, trigger string, marks []Mark) error {
|
||||
if n == nil || cell == nil || cell.notes != n {
|
||||
return ErrInvalidCell
|
||||
}
|
||||
return f.AddChange(&ExternalChange{Cell: cell, Marks: marks, Action: ActionRemoveNote, From: note, TriggerdBy: trigger})
|
||||
}
|
||||
|
||||
func (n *Notes) Get() []int {
|
||||
copySlice := make([]int, len(n.numbers))
|
||||
copy(copySlice, n.numbers)
|
||||
return copySlice
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return slices.Clone(n.numbers)
|
||||
}
|
||||
|
||||
func (n *Notes) Has(i int) bool {
|
||||
return slices.Contains(n.numbers, i)
|
||||
func (n *Notes) Has(number int) bool {
|
||||
return n != nil && slices.Contains(n.numbers, number)
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// CELL STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Cell struct {
|
||||
number int
|
||||
Notes *Notes
|
||||
Pos *Position
|
||||
notes *Notes
|
||||
pos *Position
|
||||
}
|
||||
|
||||
func NewCell(number int, pos *Position) *Cell {
|
||||
cell := Cell{
|
||||
number: number,
|
||||
Pos: pos,
|
||||
Notes: &Notes{},
|
||||
}
|
||||
return &cell
|
||||
return &Cell{number: number, notes: &Notes{}, pos: pos}
|
||||
}
|
||||
|
||||
func (c *Cell) SetNumber(field *Field, n int, trigger string, marks []Mark) {
|
||||
if c.number == n {
|
||||
return
|
||||
func (c *Cell) SetNumber(f *Field, number int, trigger string, marks []Mark) error {
|
||||
if c == nil {
|
||||
return ErrInvalidCell
|
||||
}
|
||||
|
||||
if field == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if n <= 0 || n > field.props.Rows || n > field.props.Columns {
|
||||
return
|
||||
}
|
||||
|
||||
field.changes = append(field.changes, Change{
|
||||
Cell: c,
|
||||
marks: marks,
|
||||
action: ActionSetNumber,
|
||||
value: n,
|
||||
from: c.number,
|
||||
triggerdBy: trigger,
|
||||
})
|
||||
|
||||
c.number = n
|
||||
return f.AddChange(&ExternalChange{Cell: c, Marks: marks, Action: ActionSetNumber, Value: number, From: c.number, TriggerdBy: trigger})
|
||||
}
|
||||
|
||||
func (c *Cell) RemoveNumber(field *Field, trigger string, marks []Mark) {
|
||||
if c.number == 0 {
|
||||
return
|
||||
func (c *Cell) RemoveNumber(f *Field, trigger string, marks []Mark) error {
|
||||
if c == nil {
|
||||
return ErrInvalidCell
|
||||
}
|
||||
|
||||
if field == nil {
|
||||
return
|
||||
}
|
||||
|
||||
field.changes = append(field.changes, Change{
|
||||
action: ActionSetNumber,
|
||||
Cell: c,
|
||||
value: 0,
|
||||
from: c.number,
|
||||
marks: marks,
|
||||
triggerdBy: trigger,
|
||||
})
|
||||
|
||||
c.number = 0
|
||||
return f.AddChange(&ExternalChange{Cell: c, Marks: marks, Action: ActionRemoveNumber, From: c.number, TriggerdBy: trigger})
|
||||
}
|
||||
|
||||
func (c *Cell) GetNumber() int {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
return c.number
|
||||
}
|
||||
|
||||
func (c *Cell) GetNotes() *Notes {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.notes
|
||||
}
|
||||
|
||||
func (c *Cell) GetPosition() *Position {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.pos
|
||||
}
|
||||
|
||||
+166
-40
@@ -1,13 +1,11 @@
|
||||
package field
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// MARK STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Mark struct {
|
||||
Cell *Cell
|
||||
Change *Change
|
||||
@@ -15,94 +13,222 @@ type Mark struct {
|
||||
}
|
||||
|
||||
func (m *Mark) GetColor() color.Color {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.color
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// CHANGE_ACTION TYPE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type ChangeAction int
|
||||
|
||||
const (
|
||||
ActionSetNumber = iota
|
||||
ActionSetNumber ChangeAction = iota
|
||||
ActionSetNote
|
||||
ActionRemoveNumber
|
||||
ActionRemoveNote
|
||||
)
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// CHANGE STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type ExternalChange struct {
|
||||
Cell *Cell
|
||||
Marks []Mark
|
||||
Action ChangeAction
|
||||
Value int
|
||||
From int
|
||||
TriggerdBy string //strategy or manuell
|
||||
TriggerdBy string
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// CHANGE STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Change struct {
|
||||
Cell *Cell
|
||||
marks []Mark
|
||||
action ChangeAction
|
||||
value int
|
||||
from int
|
||||
triggerdBy string //strategy or manuell
|
||||
triggerdBy string
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// GETTER //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
func (c *Change) GetMarks() []Mark {
|
||||
return c.marks
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return slices.Clone(c.marks)
|
||||
}
|
||||
|
||||
func (c *Change) GetAction() ChangeAction {
|
||||
if c == nil {
|
||||
return ActionSetNumber
|
||||
}
|
||||
return c.action
|
||||
}
|
||||
|
||||
func (c *Change) GetTo() int {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
return c.value
|
||||
}
|
||||
|
||||
func (c *Change) GetFrom() int {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
return c.from
|
||||
}
|
||||
|
||||
func (c *Change) GetTriggeredBy() string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
return c.triggerdBy
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// ??? //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
func (c *Change) do() {
|
||||
switch c.action {
|
||||
func (f *Field) AddChange(external *ExternalChange) error {
|
||||
if err := f.validateChange(external); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cell := external.Cell
|
||||
switch external.Action {
|
||||
case ActionSetNumber:
|
||||
c.Cell.number = c.value
|
||||
|
||||
if cell.number == external.Value {
|
||||
return nil
|
||||
}
|
||||
cell.number = external.Value
|
||||
f.recordChange(external)
|
||||
f.clearNotes(cell, external.TriggerdBy)
|
||||
f.removePeerNote(cell, external.Value, external.TriggerdBy)
|
||||
case ActionSetNote:
|
||||
c.Cell.Notes.numbers = append(c.Cell.Notes.numbers, c.value)
|
||||
|
||||
if cell.notes.Has(external.Value) {
|
||||
return nil
|
||||
}
|
||||
cell.notes.numbers = append(cell.notes.numbers, external.Value)
|
||||
slices.Sort(cell.notes.numbers)
|
||||
f.recordChange(external)
|
||||
case ActionRemoveNumber:
|
||||
c.Cell.number = 0
|
||||
|
||||
if cell.number == 0 {
|
||||
return nil
|
||||
}
|
||||
cell.number = 0
|
||||
f.recordChange(external)
|
||||
case ActionRemoveNote:
|
||||
for i, existing := range c.Cell.Notes.numbers {
|
||||
if existing == c.from {
|
||||
c.Cell.Notes.numbers = append(c.Cell.Notes.numbers[:i], c.Cell.Notes.numbers[i+1:]...)
|
||||
return
|
||||
for i, existing := range cell.notes.numbers {
|
||||
if existing == external.From {
|
||||
cell.notes.numbers = slices.Delete(cell.notes.numbers, i, i+1)
|
||||
f.recordChange(external)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Field) AddChanges(changes []ExternalChange) error {
|
||||
if f == nil || !f.IsValid() {
|
||||
return ErrInvalidField
|
||||
}
|
||||
state := f.snapshot()
|
||||
for i := range changes {
|
||||
if err := f.AddChange(&changes[i]); err != nil {
|
||||
f.restore(state)
|
||||
return fmt.Errorf("apply change %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Field) GetChanges() []ExternalChange {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]ExternalChange, len(f.changes))
|
||||
for i := range f.changes {
|
||||
result[i] = ExternalChange{Cell: f.changes[i].Cell, Marks: slices.Clone(f.changes[i].marks), Action: f.changes[i].action, Value: f.changes[i].value, From: f.changes[i].from, TriggerdBy: f.changes[i].triggerdBy}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (f *Field) validateChange(change *ExternalChange) error {
|
||||
if f == nil || !f.IsValid() {
|
||||
return ErrInvalidField
|
||||
}
|
||||
if change == nil || change.Cell == nil || !f.ownsCell(change.Cell) {
|
||||
return fmt.Errorf("%w: cell does not belong to field", ErrInvalidChange)
|
||||
}
|
||||
cell := change.Cell
|
||||
if cell.notes == nil || cell.pos == nil {
|
||||
return fmt.Errorf("%w: incomplete cell", ErrInvalidChange)
|
||||
}
|
||||
|
||||
switch change.Action {
|
||||
case ActionSetNumber:
|
||||
if change.Value < 1 || change.Value > f.props.Rows {
|
||||
return fmt.Errorf("%w: number %d is out of range", ErrInvalidChange, change.Value)
|
||||
}
|
||||
if change.From != cell.number {
|
||||
return fmt.Errorf("%w: expected previous value %d, got %d", ErrInvalidChange, cell.number, change.From)
|
||||
}
|
||||
if f.hasPeerNumber(cell, change.Value) {
|
||||
return fmt.Errorf("%w: number %d already exists in a peer", ErrInvalidChange, change.Value)
|
||||
}
|
||||
case ActionSetNote:
|
||||
if cell.number != 0 {
|
||||
return fmt.Errorf("%w: cannot add a note to a filled cell", ErrInvalidChange)
|
||||
}
|
||||
if change.Value < 1 || change.Value > f.props.Rows || f.hasPeerNumber(cell, change.Value) {
|
||||
return fmt.Errorf("%w: note %d is not a candidate", ErrInvalidChange, change.Value)
|
||||
}
|
||||
case ActionRemoveNumber:
|
||||
if change.From != cell.number {
|
||||
return fmt.Errorf("%w: expected previous value %d, got %d", ErrInvalidChange, cell.number, change.From)
|
||||
}
|
||||
case ActionRemoveNote:
|
||||
if change.From < 1 || change.From > f.props.Rows {
|
||||
return fmt.Errorf("%w: note %d is out of range", ErrInvalidChange, change.From)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%w: unknown action %d", ErrInvalidChange, change.Action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Field) recordChange(external *ExternalChange) {
|
||||
f.changes = append(f.changes, Change{Cell: external.Cell, marks: slices.Clone(external.Marks), action: external.Action, value: external.Value, from: external.From, triggerdBy: external.TriggerdBy})
|
||||
}
|
||||
|
||||
func (f *Field) clearNotes(cell *Cell, trigger string) {
|
||||
for _, note := range slices.Clone(cell.notes.numbers) {
|
||||
cell.notes.numbers = removeNumber(cell.notes.numbers, note)
|
||||
f.recordChange(&ExternalChange{Cell: cell, Action: ActionRemoveNote, From: note, TriggerdBy: trigger})
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Field) removePeerNote(cell *Cell, number int, trigger string) {
|
||||
seen := make(map[*Cell]struct{})
|
||||
parts, err := f.GetEachPartAtPos(cell.pos)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, part := range parts {
|
||||
part.ForEachCell(func(peer *Cell) {
|
||||
if peer == cell {
|
||||
return
|
||||
}
|
||||
if _, exists := seen[peer]; exists {
|
||||
return
|
||||
}
|
||||
seen[peer] = struct{}{}
|
||||
if peer.notes.Has(number) {
|
||||
peer.notes.numbers = removeNumber(peer.notes.numbers, number)
|
||||
f.recordChange(&ExternalChange{Cell: peer, Action: ActionRemoveNote, From: number, TriggerdBy: trigger})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func removeNumber(numbers []int, number int) []int {
|
||||
for i, existing := range numbers {
|
||||
if existing == number {
|
||||
return slices.Delete(numbers, i, i+1)
|
||||
}
|
||||
}
|
||||
return numbers
|
||||
}
|
||||
|
||||
+4
-2
@@ -3,6 +3,8 @@ package field
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
outOfBound = errors.New("invalid coordinates")
|
||||
invalidField = errors.New("can not parse Field")
|
||||
ErrOutOfBounds = errors.New("coordinates out of bounds")
|
||||
ErrInvalidField = errors.New("invalid field")
|
||||
ErrInvalidCell = errors.New("invalid cell")
|
||||
ErrInvalidChange = errors.New("invalid change")
|
||||
)
|
||||
|
||||
+356
-202
@@ -2,13 +2,11 @@ package field
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// META STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Properties struct {
|
||||
Rows int
|
||||
Columns int
|
||||
@@ -19,289 +17,445 @@ type Properties struct {
|
||||
Rating float64
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// FIELD STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Field struct {
|
||||
props *Properties
|
||||
cells [][]Cell
|
||||
changes []Change
|
||||
}
|
||||
|
||||
func New(props Properties, cells [][]Cell) *Field {
|
||||
field := Field{
|
||||
props: &props,
|
||||
cells: cells,
|
||||
}
|
||||
return &field
|
||||
type fieldSnapshot struct {
|
||||
numbers [][]int
|
||||
notes [][][]int
|
||||
changes []Change
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// GETTER //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
func (f *Field) GetRow(r int) (*Row, error) {
|
||||
if r >= f.props.Rows || r < 0 {
|
||||
return nil, outOfBound
|
||||
func New(props Properties, cells [][]Cell) (*Field, error) {
|
||||
if err := validateProperties(props); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cells) != props.Rows {
|
||||
return nil, fmt.Errorf("%w: expected %d rows, got %d", ErrInvalidField, props.Rows, len(cells))
|
||||
}
|
||||
|
||||
cellPtrs := make([]*Cell, len(f.cells[r]))
|
||||
for i := range f.cells[r] {
|
||||
cellPtrs[i] = &f.cells[r][i]
|
||||
cloned := make([][]Cell, props.Rows)
|
||||
for row := 0; row < props.Rows; row++ {
|
||||
if len(cells[row]) != props.Columns {
|
||||
return nil, fmt.Errorf("%w: row %d has %d cells, expected %d", ErrInvalidField, row, len(cells[row]), props.Columns)
|
||||
}
|
||||
cloned[row] = make([]Cell, props.Columns)
|
||||
for column := 0; column < props.Columns; column++ {
|
||||
source := &cells[row][column]
|
||||
if source.pos == nil {
|
||||
return nil, fmt.Errorf("%w: cell %d/%d has no position", ErrInvalidField, row, column)
|
||||
}
|
||||
if !positionMatches(source.pos, props, row, column) {
|
||||
return nil, fmt.Errorf("%w: cell %d/%d has inconsistent position", ErrInvalidField, row, column)
|
||||
}
|
||||
if source.number < 0 || source.number > props.Rows {
|
||||
return nil, fmt.Errorf("%w: cell %d/%d contains %d", ErrInvalidField, row, column, source.number)
|
||||
}
|
||||
if source.notes == nil {
|
||||
return nil, fmt.Errorf("%w: cell %d/%d has no notes", ErrInvalidField, row, column)
|
||||
}
|
||||
notes, err := normalizeNotes(source.notes.numbers, props.Rows, source.number)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: cell %d/%d: %w", ErrInvalidField, row, column, err)
|
||||
}
|
||||
cloned[row][column] = Cell{
|
||||
number: source.number,
|
||||
notes: &Notes{numbers: notes},
|
||||
pos: NewPosition(row, column, row/props.BlockSizeRow, column/props.BlockSizeColumn, row%props.BlockSizeRow, column%props.BlockSizeColumn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Row{
|
||||
Line: Line{
|
||||
cells: cellPtrs,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetColumn(c int) (*Column, error) {
|
||||
if c >= f.props.Columns || c < 0 {
|
||||
return nil, outOfBound
|
||||
result := &Field{props: &props, cells: cloned}
|
||||
if !result.hasValidNumbers() || !result.hasValidNotes() {
|
||||
return nil, fmt.Errorf("%w: duplicate values or invalid candidates", ErrInvalidField)
|
||||
}
|
||||
|
||||
// Performance
|
||||
result := &Column{
|
||||
Line: Line{
|
||||
cells: make([]*Cell, 0, f.props.Rows),
|
||||
},
|
||||
}
|
||||
|
||||
// Copying
|
||||
for _, row := range f.cells {
|
||||
result.cells = append(result.cells, &row[c])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetBlock(r, c int) (*Block, error) {
|
||||
if r < 0 || r >= f.props.BlockRows || c < 0 || c >= f.props.BlockColumns {
|
||||
return nil, outOfBound
|
||||
func validateProperties(props Properties) error {
|
||||
if props.Rows <= 0 || props.Columns <= 0 || props.Rows != props.Columns {
|
||||
return fmt.Errorf("%w: rows and columns must be equal and positive", ErrInvalidField)
|
||||
}
|
||||
|
||||
startRow := r * f.props.BlockSizeRow
|
||||
startCol := c * f.props.BlockSizeColumn
|
||||
|
||||
block := &Block{
|
||||
cells: make([][]*Cell, f.props.BlockSizeRow),
|
||||
if props.BlockRows <= 0 || props.BlockColumns <= 0 || props.BlockSizeRow <= 0 || props.BlockSizeColumn <= 0 {
|
||||
return fmt.Errorf("%w: block dimensions must be positive", ErrInvalidField)
|
||||
}
|
||||
if props.BlockRows*props.BlockSizeRow != props.Rows || props.BlockColumns*props.BlockSizeColumn != props.Columns {
|
||||
return fmt.Errorf("%w: block dimensions do not cover the field", ErrInvalidField)
|
||||
}
|
||||
if props.BlockSizeRow*props.BlockSizeColumn != props.Rows {
|
||||
return fmt.Errorf("%w: each block must contain %d cells", ErrInvalidField, props.Rows)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for row := range block.cells {
|
||||
// Effizientes Kopieren der Zeile
|
||||
block.cells[row] = make([]*Cell, f.props.BlockSizeColumn)
|
||||
|
||||
for column := range block.cells[row] {
|
||||
block.cells[row][column] = &f.cells[startRow+row][startCol+column]
|
||||
func normalizeNotes(notes []int, max, cellNumber int) ([]int, error) {
|
||||
if cellNumber != 0 && len(notes) != 0 {
|
||||
return nil, fmt.Errorf("filled cells cannot contain notes")
|
||||
}
|
||||
result := slices.Clone(notes)
|
||||
slices.Sort(result)
|
||||
result = slices.Compact(result)
|
||||
for _, note := range result {
|
||||
if note < 1 || note > max {
|
||||
return nil, fmt.Errorf("note %d is out of range", note)
|
||||
}
|
||||
}
|
||||
return block, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetEachPartAtPos(pos *Position) []Part {
|
||||
func positionMatches(pos *Position, props Properties, row, column int) bool {
|
||||
return pos.row == row && pos.column == column &&
|
||||
pos.blockRow == row/props.BlockSizeRow && pos.blockColumn == column/props.BlockSizeColumn &&
|
||||
pos.inBlockRow == row%props.BlockSizeRow && pos.inBlockColumn == column%props.BlockSizeColumn
|
||||
}
|
||||
|
||||
func (f *Field) GetRow(row int) (*Row, error) {
|
||||
if !f.hasStructure() || row < 0 || row >= f.props.Rows {
|
||||
return nil, ErrOutOfBounds
|
||||
}
|
||||
cells := make([]*Cell, f.props.Columns)
|
||||
for column := range f.cells[row] {
|
||||
cells[column] = &f.cells[row][column]
|
||||
}
|
||||
return &Row{Line: Line{cells: cells}}, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetColumn(column int) (*Column, error) {
|
||||
if !f.hasStructure() || column < 0 || column >= f.props.Columns {
|
||||
return nil, ErrOutOfBounds
|
||||
}
|
||||
cells := make([]*Cell, f.props.Rows)
|
||||
for row := range f.cells {
|
||||
cells[row] = &f.cells[row][column]
|
||||
}
|
||||
return &Column{Line: Line{cells: cells}}, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetBlock(row, column int) (*Block, error) {
|
||||
if !f.hasStructure() || row < 0 || row >= f.props.BlockRows || column < 0 || column >= f.props.BlockColumns {
|
||||
return nil, ErrOutOfBounds
|
||||
}
|
||||
startRow := row * f.props.BlockSizeRow
|
||||
startColumn := column * f.props.BlockSizeColumn
|
||||
cells := make([][]*Cell, f.props.BlockSizeRow)
|
||||
for blockRow := range cells {
|
||||
cells[blockRow] = make([]*Cell, f.props.BlockSizeColumn)
|
||||
for blockColumn := range cells[blockRow] {
|
||||
cells[blockRow][blockColumn] = &f.cells[startRow+blockRow][startColumn+blockColumn]
|
||||
}
|
||||
}
|
||||
return &Block{cells: cells}, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetEachPartAtPos(pos *Position) ([]Part, error) {
|
||||
if pos == nil {
|
||||
return nil, ErrOutOfBounds
|
||||
}
|
||||
row, err := f.GetRow(pos.row)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
column, err := f.GetColumn(pos.column)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := f.GetBlock(pos.blockRow, pos.blockColumn)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append([]Part{}, row, column, block)
|
||||
return []Part{row, column, block}, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetCell(r, c int) (*Cell, error) {
|
||||
if r > f.props.Rows || c > f.props.Columns {
|
||||
return nil, outOfBound
|
||||
func (f *Field) GetCell(row, column int) (*Cell, error) {
|
||||
if !f.hasStructure() || row < 0 || row >= f.props.Rows || column < 0 || column >= f.props.Columns {
|
||||
return nil, ErrOutOfBounds
|
||||
}
|
||||
return &f.cells[r][c], nil
|
||||
return &f.cells[row][column], nil
|
||||
}
|
||||
|
||||
func (f *Field) GetProperties() (Properties, error) {
|
||||
if !f.hasStructure() {
|
||||
return Properties{}, ErrInvalidField
|
||||
}
|
||||
return *f.props, nil
|
||||
}
|
||||
|
||||
func (f *Field) GetRating() float64 {
|
||||
if !f.hasStructure() {
|
||||
return 0
|
||||
}
|
||||
return f.props.Rating
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// FOREACH FUNCTIONS //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
func (f *Field) ForEachPart(fn func(part Part)) {
|
||||
f.ForEachRow(func(row *Row) {
|
||||
fn(row)
|
||||
})
|
||||
|
||||
f.ForEachColumn(func(column *Column) {
|
||||
fn(column)
|
||||
})
|
||||
|
||||
f.ForEachBlock(func(block *Block) {
|
||||
fn(block)
|
||||
})
|
||||
func (f *Field) ForEachPart(fn func(Part)) {
|
||||
if !f.hasStructure() || fn == nil {
|
||||
return
|
||||
}
|
||||
f.ForEachRow(func(row *Row) { fn(row) })
|
||||
f.ForEachColumn(func(column *Column) { fn(column) })
|
||||
f.ForEachBlock(func(block *Block) { fn(block) })
|
||||
}
|
||||
|
||||
func (f *Field) ForEachPartAtPos(pos *Position, fn func(part Part)) {
|
||||
parts := f.GetEachPartAtPos(pos)
|
||||
func (f *Field) ForEachPartAtPos(pos *Position, fn func(Part)) error {
|
||||
if fn == nil {
|
||||
return nil
|
||||
}
|
||||
parts, err := f.GetEachPartAtPos(pos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, part := range parts {
|
||||
fn(part)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Field) ForEachRow(fn func(*Row)) {
|
||||
if !f.hasStructure() || fn == nil {
|
||||
return
|
||||
}
|
||||
for row := 0; row < f.props.Rows; row++ {
|
||||
part, _ := f.GetRow(row)
|
||||
fn(part)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Field) ForEachRow(fn func(row *Row)) {
|
||||
for i := range f.props.Rows {
|
||||
row, err := f.GetRow(i)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
fn(row)
|
||||
func (f *Field) ForEachColumn(fn func(*Column)) {
|
||||
if !f.hasStructure() || fn == nil {
|
||||
return
|
||||
}
|
||||
for column := 0; column < f.props.Columns; column++ {
|
||||
part, _ := f.GetColumn(column)
|
||||
fn(part)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Field) ForEachColumn(fn func(column *Column)) {
|
||||
for i := range f.props.Columns {
|
||||
column, err := f.GetColumn(i)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
fn(column)
|
||||
func (f *Field) ForEachBlock(fn func(*Block)) {
|
||||
if !f.hasStructure() || fn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (f *Field) ForEachBlock(fn func(block *Block)) {
|
||||
for r := range f.props.BlockRows {
|
||||
for c := range f.props.BlockColumns {
|
||||
block, err := f.GetBlock(r, c)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
fn(block)
|
||||
for row := 0; row < f.props.BlockRows; row++ {
|
||||
for column := 0; column < f.props.BlockColumns; column++ {
|
||||
part, _ := f.GetBlock(row, column)
|
||||
fn(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Field) ForEachCell(fn func(cell *Cell)) {
|
||||
for _, row := range f.cells {
|
||||
for _, cell := range row {
|
||||
fn(&cell)
|
||||
func (f *Field) ForEachCell(fn func(*Cell)) {
|
||||
if !f.hasStructure() || fn == nil {
|
||||
return
|
||||
}
|
||||
for row := range f.cells {
|
||||
for column := range f.cells[row] {
|
||||
fn(&f.cells[row][column])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// MODIFIER //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
func (f *Field) AddChange(eChange *ExternalChange) {
|
||||
change := Change{
|
||||
Cell: eChange.Cell,
|
||||
marks: eChange.Marks,
|
||||
action: eChange.Action,
|
||||
value: eChange.Value,
|
||||
from: eChange.From,
|
||||
triggerdBy: eChange.TriggerdBy,
|
||||
}
|
||||
|
||||
change.do()
|
||||
|
||||
f.changes = append(f.changes, change)
|
||||
}
|
||||
|
||||
func (f *Field) SetRating(rating float64) {
|
||||
f.props.Rating = rating
|
||||
if f != nil && f.props != nil {
|
||||
f.props.Rating = rating
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// STATE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
func (f *Field) String() string {
|
||||
//TODO: auf beliebige größen anpassen
|
||||
var sb strings.Builder
|
||||
|
||||
// Oberer Rahmen
|
||||
sb.WriteString("╔═══════╤═══════╤═══════╗\n")
|
||||
|
||||
for i := range f.cells {
|
||||
sb.WriteString("║ ") // Linke Rahmenseite
|
||||
|
||||
for j, cell := range f.cells[i] {
|
||||
// Wert ausgeben oder Punkt für 0
|
||||
val := cell.number
|
||||
if val == 0 {
|
||||
sb.WriteString("·")
|
||||
} else {
|
||||
sb.WriteString(string('0' + val))
|
||||
}
|
||||
|
||||
// Trennlinien zwischen Blöcken und Zellen
|
||||
if (j+1)%3 == 0 {
|
||||
if j < 8 {
|
||||
sb.WriteString(" │ ")
|
||||
if !f.hasStructure() {
|
||||
return "<invalid field>"
|
||||
}
|
||||
var result strings.Builder
|
||||
for row := 0; row < f.props.Rows; row++ {
|
||||
if row > 0 && row%f.props.BlockSizeRow == 0 {
|
||||
result.WriteByte('\n')
|
||||
}
|
||||
for column := 0; column < f.props.Columns; column++ {
|
||||
if column > 0 {
|
||||
if column%f.props.BlockSizeColumn == 0 {
|
||||
result.WriteString(" | ")
|
||||
} else {
|
||||
sb.WriteString(" ║\n") // Rechte Rahmenseite + Zeilenumbruch
|
||||
result.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
value := f.cells[row][column].number
|
||||
if value == 0 {
|
||||
result.WriteRune('·')
|
||||
} else {
|
||||
sb.WriteString(" ")
|
||||
result.WriteString(strconv.Itoa(value))
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontale Trennlinien nach jeder 3. Zeile
|
||||
if (i+1)%3 == 0 && i < 8 {
|
||||
sb.WriteString("╟───────┼───────┼───────╢\n")
|
||||
if row+1 < f.props.Rows {
|
||||
result.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// Unterer Rahmen
|
||||
sb.WriteString("╚═══════╧═══════╧═══════╝")
|
||||
|
||||
return sb.String()
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (f *Field) StringNotesForNumber(n int) string {
|
||||
return "StringNotesForNumber is not implementet"
|
||||
func (f *Field) StringNotesForNumber(number int) string {
|
||||
if !f.hasStructure() || number < 1 || number > f.props.Rows {
|
||||
return ""
|
||||
}
|
||||
var result strings.Builder
|
||||
f.ForEachCell(func(cell *Cell) {
|
||||
if cell.notes.Has(number) {
|
||||
fmt.Fprintf(&result, "%d/%d\n", cell.pos.row, cell.pos.column)
|
||||
}
|
||||
})
|
||||
return strings.TrimSuffix(result.String(), "\n")
|
||||
}
|
||||
|
||||
func (f *Field) StringNotes() string {
|
||||
var str strings.Builder
|
||||
fmt.Fprintf(&str, "Notes:\n")
|
||||
if !f.hasStructure() {
|
||||
return "Notes:"
|
||||
}
|
||||
var result strings.Builder
|
||||
result.WriteString("Notes:\n")
|
||||
f.ForEachCell(func(cell *Cell) {
|
||||
fmt.Fprintf(&str, "Pos: %d/%d - Notes: %v\n", cell.Pos.row, cell.Pos.column, cell.Notes.numbers)
|
||||
fmt.Fprintf(&result, "Pos: %d/%d - Notes: %v\n", cell.pos.row, cell.pos.column, cell.notes.numbers)
|
||||
})
|
||||
return str.String()
|
||||
return strings.TrimSuffix(result.String(), "\n")
|
||||
}
|
||||
|
||||
func (f *Field) IsSolved() bool {
|
||||
result := true
|
||||
f.ForEachCell(func(cell *Cell) {
|
||||
if cell.number == 0 {
|
||||
result = false
|
||||
}
|
||||
})
|
||||
|
||||
/* TODO:
|
||||
if !f.isValid() {
|
||||
if !f.IsValid() {
|
||||
return false
|
||||
}
|
||||
*/
|
||||
|
||||
return result
|
||||
for row := range f.cells {
|
||||
for column := range f.cells[row] {
|
||||
if f.cells[row][column].number == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *Field) IsValid() bool {
|
||||
//In jedem Part gibt es jede Zahl max ein mal
|
||||
//Die Zahl kommt nicht in intersecting parts vor
|
||||
return f.hasStructure() && f.hasValidNumbers() && f.hasValidNotes()
|
||||
}
|
||||
|
||||
func (f *Field) hasStructure() bool {
|
||||
if f == nil || f.props == nil || validateProperties(*f.props) != nil || len(f.cells) != f.props.Rows {
|
||||
return false
|
||||
}
|
||||
for row := range f.cells {
|
||||
if len(f.cells[row]) != f.props.Columns {
|
||||
return false
|
||||
}
|
||||
for column := range f.cells[row] {
|
||||
cell := &f.cells[row][column]
|
||||
if cell.notes == nil || cell.pos == nil || !positionMatches(cell.pos, *f.props, row, column) || cell.number < 0 || cell.number > f.props.Rows {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *Field) hasValidNumbers() bool {
|
||||
if !f.hasStructure() {
|
||||
return false
|
||||
}
|
||||
valid := true
|
||||
f.ForEachPart(func(part Part) {
|
||||
seen := make(map[int]struct{}, f.props.Rows)
|
||||
part.ForEachCell(func(cell *Cell) {
|
||||
if cell.number == 0 || !valid {
|
||||
return
|
||||
}
|
||||
if _, exists := seen[cell.number]; exists {
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
seen[cell.number] = struct{}{}
|
||||
})
|
||||
})
|
||||
return valid
|
||||
}
|
||||
|
||||
func (f *Field) hasValidNotes() bool {
|
||||
if !f.hasStructure() {
|
||||
return false
|
||||
}
|
||||
valid := true
|
||||
f.ForEachCell(func(cell *Cell) {
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
if cell.number != 0 && len(cell.notes.numbers) != 0 {
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
previous := 0
|
||||
for _, note := range cell.notes.numbers {
|
||||
if note <= previous || note > f.props.Rows || f.hasPeerNumber(cell, note) {
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
previous = note
|
||||
}
|
||||
})
|
||||
return valid
|
||||
}
|
||||
|
||||
func (f *Field) ownsCell(cell *Cell) bool {
|
||||
if !f.hasStructure() || cell == nil || cell.pos == nil {
|
||||
return false
|
||||
}
|
||||
row, column := cell.pos.row, cell.pos.column
|
||||
return row >= 0 && row < f.props.Rows && column >= 0 && column < f.props.Columns && &f.cells[row][column] == cell
|
||||
}
|
||||
|
||||
func (f *Field) hasPeerNumber(cell *Cell, number int) bool {
|
||||
if !f.ownsCell(cell) {
|
||||
return false
|
||||
}
|
||||
row, column := cell.pos.row, cell.pos.column
|
||||
for index := 0; index < f.props.Columns; index++ {
|
||||
if index != column && f.cells[row][index].number == number {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for index := 0; index < f.props.Rows; index++ {
|
||||
if index != row && f.cells[index][column].number == number {
|
||||
return true
|
||||
}
|
||||
}
|
||||
startRow := cell.pos.blockRow * f.props.BlockSizeRow
|
||||
startColumn := cell.pos.blockColumn * f.props.BlockSizeColumn
|
||||
for blockRow := 0; blockRow < f.props.BlockSizeRow; blockRow++ {
|
||||
for blockColumn := 0; blockColumn < f.props.BlockSizeColumn; blockColumn++ {
|
||||
peer := &f.cells[startRow+blockRow][startColumn+blockColumn]
|
||||
if peer != cell && peer.number == number {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *Field) snapshot() fieldSnapshot {
|
||||
state := fieldSnapshot{numbers: make([][]int, len(f.cells)), notes: make([][][]int, len(f.cells)), changes: slices.Clone(f.changes)}
|
||||
for row := range f.cells {
|
||||
state.numbers[row] = make([]int, len(f.cells[row]))
|
||||
state.notes[row] = make([][]int, len(f.cells[row]))
|
||||
for column := range f.cells[row] {
|
||||
state.numbers[row][column] = f.cells[row][column].number
|
||||
state.notes[row][column] = slices.Clone(f.cells[row][column].notes.numbers)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (f *Field) restore(state fieldSnapshot) {
|
||||
for row := range f.cells {
|
||||
for column := range f.cells[row] {
|
||||
f.cells[row][column].number = state.numbers[row][column]
|
||||
f.cells[row][column].notes.numbers = slices.Clone(state.notes[row][column])
|
||||
}
|
||||
}
|
||||
f.changes = slices.Clone(state.changes)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package field
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const solvedPuzzle = "123456789456789123789123456234567891567891234891234567345678912678912345912345678"
|
||||
|
||||
func classicCells(t *testing.T, digits string) (Properties, [][]Cell) {
|
||||
t.Helper()
|
||||
if len(digits) != 81 {
|
||||
t.Fatalf("test puzzle has %d characters", len(digits))
|
||||
}
|
||||
props := Properties{Rows: 9, Columns: 9, BlockRows: 3, BlockColumns: 3, BlockSizeRow: 3, BlockSizeColumn: 3}
|
||||
cells := make([][]Cell, props.Rows)
|
||||
for row := range cells {
|
||||
cells[row] = make([]Cell, props.Columns)
|
||||
for column := range cells[row] {
|
||||
position := NewPosition(row, column, row/3, column/3, row%3, column%3)
|
||||
cells[row][column] = *NewCell(int(digits[row*9+column]-'0'), position)
|
||||
}
|
||||
}
|
||||
return props, cells
|
||||
}
|
||||
|
||||
func mustField(t *testing.T, digits string) *Field {
|
||||
t.Helper()
|
||||
props, cells := classicCells(t, digits)
|
||||
result, err := New(props, cells)
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestNewValidatesStructureAndValues(t *testing.T) {
|
||||
props, cells := classicCells(t, strings.Repeat("0", 81))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*Properties, [][]Cell)
|
||||
}{
|
||||
{name: "missing row", mutate: func(_ *Properties, cells [][]Cell) { cells[0] = cells[0][:8] }},
|
||||
{name: "wrong position", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].pos = NewPosition(1, 0, 0, 0, 0, 0) }},
|
||||
{name: "number out of range", mutate: func(_ *Properties, cells [][]Cell) { cells[0][0].number = 10 }},
|
||||
{name: "invalid blocks", mutate: func(props *Properties, _ [][]Cell) { props.BlockRows = 2 }},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
localProps := props
|
||||
localCells := make([][]Cell, len(cells))
|
||||
for row := range cells {
|
||||
localCells[row] = append([]Cell(nil), cells[row]...)
|
||||
}
|
||||
test.mutate(&localProps, localCells)
|
||||
if _, err := New(localProps, localCells); !errors.Is(err, ErrInvalidField) {
|
||||
t.Fatalf("New() error = %v, want ErrInvalidField", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
duplicate := "113456789" + solvedPuzzle[9:]
|
||||
duplicateProps, duplicateCells := classicCells(t, duplicate)
|
||||
if _, err := New(duplicateProps, duplicateCells); !errors.Is(err, ErrInvalidField) {
|
||||
t.Fatalf("New() duplicate error = %v, want ErrInvalidField", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCellBoundsAndForEachCellPointers(t *testing.T) {
|
||||
f := mustField(t, strings.Repeat("0", 81))
|
||||
for _, coordinates := range [][2]int{{-1, 0}, {0, -1}, {9, 0}, {0, 9}} {
|
||||
if _, err := f.GetCell(coordinates[0], coordinates[1]); !errors.Is(err, ErrOutOfBounds) {
|
||||
t.Errorf("GetCell(%d, %d) error = %v", coordinates[0], coordinates[1], err)
|
||||
}
|
||||
}
|
||||
|
||||
want, _ := f.GetCell(4, 5)
|
||||
var found *Cell
|
||||
f.ForEachCell(func(cell *Cell) {
|
||||
if cell.GetPosition().GetRow() == 4 && cell.GetPosition().GetColumn() == 5 {
|
||||
found = cell
|
||||
}
|
||||
})
|
||||
if found != want {
|
||||
t.Fatalf("ForEachCell() returned %p, want stored cell %p", found, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidityAndSolvedState(t *testing.T) {
|
||||
incomplete := mustField(t, strings.Repeat("0", 81))
|
||||
if !incomplete.IsValid() || incomplete.IsSolved() {
|
||||
t.Fatalf("empty field: IsValid=%v IsSolved=%v", incomplete.IsValid(), incomplete.IsSolved())
|
||||
}
|
||||
|
||||
solved := mustField(t, solvedPuzzle)
|
||||
if !solved.IsValid() || !solved.IsSolved() {
|
||||
t.Fatalf("solved field: IsValid=%v IsSolved=%v", solved.IsValid(), solved.IsSolved())
|
||||
}
|
||||
row, _ := solved.GetRow(0)
|
||||
block, _ := solved.GetBlock(0, 0)
|
||||
if !row.IsSolved() || !block.IsSolved() {
|
||||
t.Fatal("completed row and block must be solved")
|
||||
}
|
||||
|
||||
solved.cells[0][0].number = solved.cells[0][1].number
|
||||
if solved.IsValid() || solved.IsSolved() {
|
||||
t.Fatal("field with a duplicate must be invalid and unsolved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangesAreValidatedAndAtomic(t *testing.T) {
|
||||
f := mustField(t, strings.Repeat("0", 81))
|
||||
first, _ := f.GetCell(0, 0)
|
||||
second, _ := f.GetCell(0, 1)
|
||||
|
||||
err := f.AddChanges([]ExternalChange{
|
||||
{Cell: first, Action: ActionSetNumber, Value: 1, From: 0},
|
||||
{Cell: second, Action: ActionSetNumber, Value: 1, From: 0},
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidChange) {
|
||||
t.Fatalf("AddChanges() error = %v, want ErrInvalidChange", err)
|
||||
}
|
||||
if first.GetNumber() != 0 || second.GetNumber() != 0 || len(f.GetChanges()) != 0 {
|
||||
t.Fatal("failed change batch was not rolled back")
|
||||
}
|
||||
|
||||
foreign := NewCell(0, NewPosition(0, 0, 0, 0, 0, 0))
|
||||
if err := f.AddChange(&ExternalChange{Cell: foreign, Action: ActionSetNumber, Value: 1}); !errors.Is(err, ErrInvalidChange) {
|
||||
t.Fatalf("foreign cell error = %v, want ErrInvalidChange", err)
|
||||
}
|
||||
if err := (*Field)(nil).AddChanges(nil); !errors.Is(err, ErrInvalidField) {
|
||||
t.Fatalf("nil field error = %v, want ErrInvalidField", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotesStaySortedUniqueAndArePropagated(t *testing.T) {
|
||||
f := mustField(t, strings.Repeat("0", 81))
|
||||
target, _ := f.GetCell(0, 0)
|
||||
rowPeer, _ := f.GetCell(0, 1)
|
||||
columnPeer, _ := f.GetCell(1, 0)
|
||||
blockPeer, _ := f.GetCell(1, 1)
|
||||
unrelated, _ := f.GetCell(4, 4)
|
||||
|
||||
for _, note := range []int{5, 1, 5} {
|
||||
if err := target.GetNotes().Add(f, target, note, "test", nil); err != nil {
|
||||
t.Fatalf("add target note: %v", err)
|
||||
}
|
||||
}
|
||||
if got, want := target.GetNotes().Get(), []int{1, 5}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("notes = %v, want %v", got, want)
|
||||
}
|
||||
for _, cell := range []*Cell{rowPeer, columnPeer, blockPeer, unrelated} {
|
||||
if err := cell.GetNotes().Add(f, cell, 5, "test", nil); err != nil {
|
||||
t.Fatalf("add peer note: %v", err)
|
||||
}
|
||||
}
|
||||
if err := target.SetNumber(f, 5, "test", nil); err != nil {
|
||||
t.Fatalf("SetNumber() error = %v", err)
|
||||
}
|
||||
for _, peer := range []*Cell{target, rowPeer, columnPeer, blockPeer} {
|
||||
if peer.GetNotes().Has(5) {
|
||||
row, column := peer.GetPosition().GetCoords()
|
||||
t.Fatalf("note 5 was not removed at %d/%d", row, column)
|
||||
}
|
||||
}
|
||||
if !unrelated.GetNotes().Has(5) {
|
||||
t.Fatal("note was removed from an unrelated cell")
|
||||
}
|
||||
if f.IsValid() == false {
|
||||
t.Fatal("field must remain valid after candidate propagation")
|
||||
}
|
||||
filled, _ := f.GetCell(0, 2)
|
||||
if err := filled.SetNumber(f, 2, "test", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := filled.GetNotes().Add(f, filled, 3, "test", nil); !errors.Is(err, ErrInvalidChange) {
|
||||
t.Fatalf("note on filled cell error = %v, want ErrInvalidChange", err)
|
||||
}
|
||||
if err := rowPeer.GetNotes().Add(f, rowPeer, 2, "test", nil); !errors.Is(err, ErrInvalidChange) {
|
||||
t.Fatalf("invalid peer note error = %v, want ErrInvalidChange", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveNumberRecordsCorrectAction(t *testing.T) {
|
||||
f := mustField(t, strings.Repeat("0", 81))
|
||||
cell, _ := f.GetCell(0, 0)
|
||||
if err := cell.SetNumber(f, 1, "set", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cell.RemoveNumber(f, "remove", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changes := f.GetChanges()
|
||||
if changes[len(changes)-1].Action != ActionRemoveNumber {
|
||||
t.Fatalf("last action = %v, want ActionRemoveNumber", changes[len(changes)-1].Action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringRendersDigitsAndNotes(t *testing.T) {
|
||||
f := mustField(t, "900000000"+strings.Repeat("0", 72))
|
||||
cell, _ := f.GetCell(0, 1)
|
||||
if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(f.String(), "9 · ·") {
|
||||
t.Fatalf("String() = %q", f.String())
|
||||
}
|
||||
if got := f.StringNotesForNumber(1); got != "0/1" {
|
||||
t.Fatalf("StringNotesForNumber() = %q, want 0/1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGettersReturnDefensiveCopies(t *testing.T) {
|
||||
f := mustField(t, strings.Repeat("0", 81))
|
||||
props, err := f.GetProperties()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
props.Rows = 4
|
||||
stored, _ := f.GetProperties()
|
||||
if stored.Rows != 9 {
|
||||
t.Fatal("GetProperties exposed internal properties")
|
||||
}
|
||||
|
||||
cell, _ := f.GetCell(0, 0)
|
||||
if err := cell.GetNotes().Add(f, cell, 1, "test", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
notes := cell.GetNotes().Get()
|
||||
notes[0] = 9
|
||||
if !cell.GetNotes().Has(1) || cell.GetNotes().Has(9) {
|
||||
t.Fatal("Notes.Get exposed its backing slice")
|
||||
}
|
||||
|
||||
changes := f.GetChanges()
|
||||
changes[0].Value = 9
|
||||
if f.GetChanges()[0].Value != 1 {
|
||||
t.Fatal("GetChanges exposed its backing slice")
|
||||
}
|
||||
}
|
||||
+79
-67
@@ -7,83 +7,72 @@ import (
|
||||
|
||||
type Part interface {
|
||||
IsSolved() bool
|
||||
ForEachCell(fn func(cell *Cell))
|
||||
ForEachCell(func(*Cell))
|
||||
GetMissingNumbers() []int
|
||||
RemoveNote(field *Field, note int, trigger string, marks []Mark)
|
||||
RemoveNote(*Field, int, string, []Mark) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// LINE STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Line struct {
|
||||
cells []*Cell
|
||||
}
|
||||
|
||||
func (l *Line) IsSolved() bool {
|
||||
//TODO
|
||||
return false
|
||||
return partIsSolved(l)
|
||||
}
|
||||
|
||||
func (l *Line) ForEachCell(fn func(cell *Cell)) {
|
||||
func (l *Line) ForEachCell(fn func(*Cell)) {
|
||||
if l == nil || fn == nil {
|
||||
return
|
||||
}
|
||||
for _, cell := range l.cells {
|
||||
fn(cell)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Line) GetMissingNumbers() []int {
|
||||
return getMissingNumbersHelper(len(l.cells), l.ForEachCell)
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return getMissingNumbers(len(l.cells), l.ForEachCell)
|
||||
}
|
||||
|
||||
func (l *Line) RemoveNote(field *Field, note int, trigger string, marks []Mark) {
|
||||
l.ForEachCell(func(cell *Cell) {
|
||||
cell.Notes.Remove(field, cell, note, trigger, marks)
|
||||
})
|
||||
func (l *Line) RemoveNote(f *Field, note int, trigger string, marks []Mark) error {
|
||||
return removeNoteFromPart(l, f, note, trigger, marks)
|
||||
}
|
||||
|
||||
func (l *Line) String() string {
|
||||
return lineString(l, "Line")
|
||||
return partString(l, "Line")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// ROW STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Row struct {
|
||||
Line
|
||||
}
|
||||
|
||||
func (r *Row) String() string {
|
||||
return lineString(r, "Row")
|
||||
return partString(r, "Row")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// COLUMN STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Column struct {
|
||||
Line
|
||||
}
|
||||
|
||||
func (c *Column) String() string {
|
||||
return lineString(c, "Column")
|
||||
return partString(c, "Column")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// BLOCK STRUCTURE //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
type Block struct {
|
||||
cells [][]*Cell
|
||||
}
|
||||
|
||||
func (b *Block) IsSolved() bool {
|
||||
//TODO
|
||||
return false
|
||||
return partIsSolved(b)
|
||||
}
|
||||
|
||||
func (b *Block) ForEachCell(fn func(cell *Cell)) {
|
||||
func (b *Block) ForEachCell(fn func(*Cell)) {
|
||||
if b == nil || fn == nil {
|
||||
return
|
||||
}
|
||||
for _, row := range b.cells {
|
||||
for _, cell := range row {
|
||||
fn(cell)
|
||||
@@ -92,56 +81,79 @@ func (b *Block) ForEachCell(fn func(cell *Cell)) {
|
||||
}
|
||||
|
||||
func (b *Block) GetMissingNumbers() []int {
|
||||
max := len(b.cells) * len(b.cells[0])
|
||||
return getMissingNumbersHelper(max, b.ForEachCell)
|
||||
if b == nil || len(b.cells) == 0 {
|
||||
return nil
|
||||
}
|
||||
size := 0
|
||||
for _, row := range b.cells {
|
||||
size += len(row)
|
||||
}
|
||||
return getMissingNumbers(size, b.ForEachCell)
|
||||
}
|
||||
|
||||
func (b *Block) RemoveNote(field *Field, note int, trigger string, marks []Mark) {
|
||||
b.ForEachCell(func(cell *Cell) {
|
||||
cell.Notes.Remove(field, cell, note, trigger, marks)
|
||||
})
|
||||
func (b *Block) RemoveNote(f *Field, note int, trigger string, marks []Mark) error {
|
||||
return removeNoteFromPart(b, f, note, trigger, marks)
|
||||
}
|
||||
|
||||
func (b *Block) String() string {
|
||||
var str strings.Builder
|
||||
str.WriteString("Block:\t")
|
||||
lastLine := 0
|
||||
b.ForEachCell(func(cell *Cell) {
|
||||
fmt.Fprintf(&str, "%d ", cell.number)
|
||||
if lastLine == cell.Pos.inBlockRow {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
return str.String()
|
||||
return partString(b, "Block")
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
// HELPER //
|
||||
// ────────────────────────────────────────────────────────────────────────────── //
|
||||
|
||||
func lineString(part Part, name string) string {
|
||||
var str strings.Builder
|
||||
fmt.Fprintf(&str, "%s:\t", name)
|
||||
func partIsSolved(part Part) bool {
|
||||
if part == nil {
|
||||
return false
|
||||
}
|
||||
cellCount := 0
|
||||
seen := make(map[int]struct{})
|
||||
valid := true
|
||||
part.ForEachCell(func(cell *Cell) {
|
||||
fmt.Fprintf(&str, "%d ", cell.number)
|
||||
cellCount++
|
||||
if cell == nil || cell.number <= 0 {
|
||||
valid = false
|
||||
return
|
||||
}
|
||||
if _, exists := seen[cell.number]; exists {
|
||||
valid = false
|
||||
}
|
||||
seen[cell.number] = struct{}{}
|
||||
})
|
||||
return str.String()
|
||||
return valid && cellCount > 0 && len(seen) == cellCount
|
||||
}
|
||||
|
||||
func getMissingNumbersHelper(max int, iterate func(fn func(cell *Cell))) []int {
|
||||
present := make(map[int]bool, max)
|
||||
func removeNoteFromPart(part Part, f *Field, note int, trigger string, marks []Mark) error {
|
||||
var firstErr error
|
||||
part.ForEachCell(func(cell *Cell) {
|
||||
if firstErr != nil || !cell.notes.Has(note) {
|
||||
return
|
||||
}
|
||||
firstErr = cell.notes.Remove(f, cell, note, trigger, marks)
|
||||
})
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func partString(part Part, name string) string {
|
||||
if part == nil {
|
||||
return name + ":"
|
||||
}
|
||||
var result strings.Builder
|
||||
fmt.Fprintf(&result, "%s:\t", name)
|
||||
part.ForEachCell(func(cell *Cell) {
|
||||
fmt.Fprintf(&result, "%d ", cell.GetNumber())
|
||||
})
|
||||
return strings.TrimSuffix(result.String(), " ")
|
||||
}
|
||||
|
||||
func getMissingNumbers(max int, iterate func(func(*Cell))) []int {
|
||||
present := make(map[int]struct{}, max)
|
||||
iterate(func(cell *Cell) {
|
||||
if cell.number != 0 {
|
||||
present[cell.number] = true
|
||||
if cell != nil && cell.number != 0 {
|
||||
present[cell.number] = struct{}{}
|
||||
}
|
||||
})
|
||||
|
||||
var missing []int
|
||||
for i := 1; i <= max; i++ {
|
||||
if !present[i] {
|
||||
missing = append(missing, i)
|
||||
missing := make([]int, 0, max-len(present))
|
||||
for number := 1; number <= max; number++ {
|
||||
if _, exists := present[number]; !exists {
|
||||
missing = append(missing, number)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
|
||||
@@ -22,37 +22,64 @@ func NewPosition(row, column, blockRow, blockColumn, inBlockRow, inBlockColumn i
|
||||
}
|
||||
|
||||
func (p *Position) GetRow() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return p.row
|
||||
}
|
||||
|
||||
func (p *Position) GetColumn() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return p.column
|
||||
}
|
||||
|
||||
func (p *Position) GetBlockRow() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return p.blockRow
|
||||
}
|
||||
|
||||
func (p *Position) GetBlockColumn() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return p.blockColumn
|
||||
}
|
||||
|
||||
func (p *Position) GetInBlockRow() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return p.inBlockRow
|
||||
}
|
||||
|
||||
func (p *Position) GetInBlockColumn() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return p.inBlockColumn
|
||||
}
|
||||
|
||||
func (p *Position) GetCoords() (int, int) {
|
||||
if p == nil {
|
||||
return 0, 0
|
||||
}
|
||||
return p.row, p.column
|
||||
}
|
||||
|
||||
func (p *Position) GetBlockCoords() (int, int) {
|
||||
if p == nil {
|
||||
return 0, 0
|
||||
}
|
||||
return p.blockRow, p.blockColumn
|
||||
}
|
||||
|
||||
func (p *Position) GetInBlockCoords() (int, int) {
|
||||
if p == nil {
|
||||
return 0, 0
|
||||
}
|
||||
return p.inBlockRow, p.inBlockColumn
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user