Files
Sudoku/field/changes.go
T

235 lines
5.6 KiB
Go

package field
import (
"fmt"
"image/color"
"slices"
)
type Mark struct {
Cell *Cell
Change *Change
color color.Color
}
func (m *Mark) GetColor() color.Color {
if m == nil {
return nil
}
return m.color
}
type ChangeAction int
const (
ActionSetNumber ChangeAction = iota
ActionSetNote
ActionRemoveNumber
ActionRemoveNote
)
type ExternalChange struct {
Cell *Cell
Marks []Mark
Action ChangeAction
Value int
From int
TriggerdBy string
}
type Change struct {
Cell *Cell
marks []Mark
action ChangeAction
value int
from int
triggerdBy string
}
func (c *Change) GetMarks() []Mark {
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 (f *Field) AddChange(external *ExternalChange) error {
if err := f.validateChange(external); err != nil {
return err
}
cell := external.Cell
switch external.Action {
case ActionSetNumber:
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:
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:
if cell.number == 0 {
return nil
}
cell.number = 0
f.recordChange(external)
case ActionRemoveNote:
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
}