235 lines
5.6 KiB
Go
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
|
|
}
|