Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Here is my solution to the problem. #2

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions cmd/sudoku/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
9 changes: 9 additions & 0 deletions sample-puzzles/extreme_1_puzzle.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
_________
_____3_85
__1_2____
___5_7___
__4___1__
_9_______
5______73
__2_1____
____4___9
9 changes: 9 additions & 0 deletions sample-puzzles/extreme_1_puzzle_solution.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
987654321
246173985
351928746
128537694
634892157
795461832
519286473
472319568
863745219
4 changes: 2 additions & 2 deletions sample-puzzles/hard_1_puzzle_solution.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
257631498
843952617
179286345
436579183
528413976
436579182
528413976
4 changes: 2 additions & 2 deletions sample-puzzles/simple_1_puzzle_solution.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
518367294
769452813
342891476
342891576
974625138
621983457
853174629
296718345
485239761
137546982
137546982
172 changes: 172 additions & 0 deletions sudoku.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions sudoku_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}