diff --git a/cmd/sudoku/main.go b/cmd/sudoku/main.go new file mode 100644 index 0000000..e8c5aff --- /dev/null +++ b/cmd/sudoku/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "os" + + "github.com/gdey/sudoku" +) + +func main() { + var filename = "sample-puzzles/hard_1_puzzle.txt" + + if len(os.Args) > 1 { + filename = os.Args[1] + } + b, err := sudoku.LoadFromFile(filename) + if err != nil { + panic(err) + } + fmt.Println(b) + if err = b.Solve(); err != nil { + panic(err) + } + fmt.Println(b) +} diff --git a/sample-puzzles/extreme_1_puzzle.txt b/sample-puzzles/extreme_1_puzzle.txt new file mode 100644 index 0000000..d50283a --- /dev/null +++ b/sample-puzzles/extreme_1_puzzle.txt @@ -0,0 +1,9 @@ +_________ +_____3_85 +__1_2____ +___5_7___ +__4___1__ +_9_______ +5______73 +__2_1____ +____4___9 \ No newline at end of file diff --git a/sample-puzzles/extreme_1_puzzle_solution.txt b/sample-puzzles/extreme_1_puzzle_solution.txt new file mode 100644 index 0000000..14e03dc --- /dev/null +++ b/sample-puzzles/extreme_1_puzzle_solution.txt @@ -0,0 +1,9 @@ +987654321 +246173985 +351928746 +128537694 +634892157 +795461832 +519286473 +472319568 +863745219 diff --git a/sample-puzzles/hard_1_puzzle_solution.txt b/sample-puzzles/hard_1_puzzle_solution.txt index 452cb40..8150f9a 100644 --- a/sample-puzzles/hard_1_puzzle_solution.txt +++ b/sample-puzzles/hard_1_puzzle_solution.txt @@ -5,5 +5,5 @@ 257631498 843952617 179286345 -436579183 -528413976 \ No newline at end of file +436579182 +528413976 diff --git a/sample-puzzles/simple_1_puzzle_solution.txt b/sample-puzzles/simple_1_puzzle_solution.txt index 657e878..3c37357 100644 --- a/sample-puzzles/simple_1_puzzle_solution.txt +++ b/sample-puzzles/simple_1_puzzle_solution.txt @@ -1,9 +1,9 @@ 518367294 769452813 -342891476 +342891576 974625138 621983457 853174629 296718345 485239761 -137546982 \ No newline at end of file +137546982 diff --git a/sudoku.go b/sudoku.go new file mode 100644 index 0000000..ae2cf8a --- /dev/null +++ b/sudoku.go @@ -0,0 +1,172 @@ +package sudoku + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" +) + +type Board [9][9]uint8 + +var ErrUnsolvable = errors.New("Unsolvable") +var ErrTemplateSolts = errors.New("More values then slots in template.") +var ErrInvalidInput = errors.New("Got an invalid input file.") + +func Load(src io.Reader) (b *Board, err error) { + b = new(Board) + scanner := bufio.NewScanner(src) + row := 0 + for scanner.Scan() { + line := scanner.Text() + for col, ch := range line { + if ch != '_' && (ch < '1' || ch > '9') { + return nil, ErrInvalidInput + } + if ch == '_' { + continue + } + b[row][col] = uint8((ch - '1') + 1) + } + row++ + } + if err = scanner.Err(); err != nil { + return nil, err + } + return b, nil +} + +func LoadFromFile(filename string) (b *Board, err error) { + file, err := os.Open(filename) + if err != nil { + return b, err + } + defer file.Close() + return Load(file) +} + +func (b *Board) fillMaskForRow(mask *[10]bool, row int) { + for _, val := range b[row] { + if val != 0 { + mask[int(val)] = true + } + } +} +func (b *Board) fillMaskForCol(mask *[10]bool, col int) { + for _, row := range b { + val := row[col] + if val != 0 { + mask[int(val)] = true + } + } +} + +func (b *Board) fillMaskForQuad(mask *[10]bool, row, col int) { + // Get upper left coord for quad. + wrow := (row / 3) * 3 + wcol := (col / 3) * 3 + for r := wrow; r < wrow+3; r++ { + for c := wcol; c < wcol+3; c++ { + val := b[r][c] + if val != 0 { + mask[int(val)] = true + } + } + } +} + +func (b *Board) MaskForPos(row, col int) (mask [10]bool) { + b.fillMaskForRow(&mask, row) + b.fillMaskForCol(&mask, col) + b.fillMaskForQuad(&mask, row, col) + return +} + +func (b Board) FillTemplate(template []byte, repl rune, empty rune) error { + if empty == 0 { + empty = ' ' + } + for r := range b { + for c := range b[r] { + val := b[r][c] + replacement := byte(empty) + if val != 0 { + replacement = byte('1' + (val - 1)) + } + idx := bytes.IndexRune(template, repl) + if idx == -1 { + // More values then slots in templates? + return ErrTemplateSolts + } + template[idx] = replacement + } + } + return nil +} + +func (b Board) String() (out string) { + template := []byte(`┏━━━┯━━━┯━━━┓ +┃WWW│WWW│WWW┃ +┃WWW│WWW│WWW┃ +┃WWW│WWW│WWW┃ +┠───┼───┼───┨ +┃WWW│WWW│WWW┃ +┃WWW│WWW│WWW┃ +┃WWW│WWW│WWW┃ +┠───┼───┼───┨ +┃WWW│WWW│WWW┃ +┃WWW│WWW│WWW┃ +┃WWW│WWW│WWW┃ +┗━━━┷━━━┷━━━┛`) + + b.FillTemplate(template, 'W', ' ') + return string(template) +} + +func (b *Board) Solve() error { + // Stack of backtracking points. (value, row, col) + var backtrack = make([][3]int, 0, 1024/3) + startVal := 1 + for r := 0; r < len(b); r++ { + LoopCol: + for c := 0; c < len(b[r]); { + if b[r][c] != 0 { + c++ + startVal = 1 + continue + } + + mask := b.MaskForPos(r, c) + for offset, unaval := range mask[startVal:] { + if unaval { + continue + } + val := startVal + offset + // we filled out the position + b[r][c] = uint8(val) + // Store the next value position, and the current row and column. + backtrack = append(backtrack, [3]int{val + 1, r, c}) + c++ + startVal = 1 + continue LoopCol + } + + BackTrack: + // Need to backtrack. + if len(backtrack) == 0 { + return ErrUnsolvable + } + bkr := backtrack[len(backtrack)-1] + startVal, r, c = bkr[0], bkr[1], bkr[2] + // Let it know we need to recalculate this value. + b[r][c] = 0 + // Remove the last value from the stack. + backtrack = backtrack[0 : len(backtrack)-1] + if startVal >= len(mask) { + goto BackTrack + } + } + } + return nil +} diff --git a/sudoku_test.go b/sudoku_test.go new file mode 100644 index 0000000..73fd6ba --- /dev/null +++ b/sudoku_test.go @@ -0,0 +1,69 @@ +package sudoku + +import ( + "io/ioutil" + "path/filepath" + "testing" +) + +func TestSolver(t *testing.T) { + const ( + testdir = "sample-puzzles" + ) + // test files are in the sample_puzzles directory. + // Each file has solution along with it in the form of + // the puzzle name _solution. + // First thing we need to do is grab all the solution filenames. + files, err := ioutil.ReadDir(testdir) + if err != nil { + t.Fatal(err) + } + for _, f := range files { + fn := f.Name() + fnlen := len(fn) + if fnlen > 4 && fn[fnlen-4:] != ".txt" { + continue + } + if len(fn) > 13 && fn[fnlen-13:] == "_solution.txt" { + continue + } + b, err := LoadFromFile(filepath.Join(testdir, fn)) + if err != nil { + t.Fatal(err) + } + bs, err := LoadFromFile(filepath.Join(testdir, fn[:fnlen-4]+"_solution.txt")) + if err != nil { + t.Fatal(err) + } + t.Run(fn, func(t *testing.T) { + if err = b.Solve(); err != nil { + + t.Fatal(err) + } + for r := range bs { + for c := range bs[r] { + if bs[r][c] != b[r][c] { + t.Errorf("For (%v, %v) Got %v Expected %v:\nGot Puzzle:\n%v\nExpected Puzzle:\n%v\n", r, c, b[r][c], bs[r][c], b, bs) + } + } + } + }) + } +} + +func BenchmarkSolver(b *testing.B) { + for i := 0; i < b.N; i++ { + b := Board{ + {0, 0, 2, 0, 0, 4, 0, 0, 1}, + {0, 0, 0, 1, 2, 0, 0, 0, 4}, + {3, 0, 4, 0, 0, 0, 8, 2, 0}, + {6, 0, 0, 8, 4, 0, 0, 5, 0}, + {2, 0, 7, 0, 3, 0, 4, 0, 8}, + {0, 4, 0, 0, 5, 2, 0, 0, 7}, + {0, 7, 8, 0, 0, 0, 0, 0, 5}, + {4, 0, 0, 0, 7, 9, 0, 0, 0}, + {5, 0, 0, 4, 0, 0, 9, 0, 0}, + } + b.Solve() + } +}