Refactor Sudoku field and solver implementation
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user