Rosetta Example: Sudoku
+Solve a partially filled-in 9x9 Sudoku grid and display the result +in a human-readable format. For detailed description of this +example, see http://rosettacode.org/wiki/Sudoku_Solver
This implementation is based on http://wiki.tcl.tk/19934
package require nx
The class Sudoku
implements the basic interface to a sudoku 9x9
+board to load/dump data and to set/access cells, rows, columns and
+regions.
nx::Class create Sudoku { + + :variable board + + # Setup an array from 0..9 to ease iterations over the cells of + # lines and columns. + for {set i 0} {$i < 9} {incr i} {lappend positions $i} + :variable positions $positions + + :public method load {data} { + # + # Load a 9x9 partially solved sudoku. The unsolved cells are + # represented by a@ symbols. + # + set error "data must be a 9-element list, each element also being a\ + list of 9 numbers from 1 to 9 or blank or an @ symbol." + if {[llength $data] != 9} { + error $error + } + foreach y ${:positions} { + set row [lindex $data $y] + if {[llength $row] != 9} { + error $error + } + foreach x ${:positions} { + set cell [lindex $row $x] + if {![regexp {^[@1-9]?$} $cell]} { + error $cell-$error + } + if {$cell eq "@"} {set cell ""} + :set $x $y $cell + } + } + } + + :public method dump {-pretty-print:switch} { + # + # Output the current state of the sudoku either as list or in + # a pretty-print style. + # + set rows [lmap y ${:positions} {:getRow 0 $y}] + if {${pretty-print}} { + set result +-----+-----+-----+\n + foreach line $rows postline {0 0 1 0 0 1 0 0 1} { + append result |[lrange $line 0 2]|[lrange $line 3 5]|[lrange $line 6 8]|\n + if {$postline} { + append result +-----+-----+-----+\n + } + } + return $result + } else { + return $rows + } + } + + :method log {msg} { + #puts "log: $msg" + } + + :method set {x y value:integer,0..1} { + # + # Set cell at position x,y to the given value or empty. + # + if {$value<1 || $value>9} { + set :board($x,$y) {} + } else { + set :board($x,$y) $value + } + } + :method get {x y} { + # + # Get value of cell at position x, y. + # + return [set :board($x,$y)] + } + + :method getRow {x y} { + # + # Return a row at constant position y. + # + return [lmap x ${:positions} {:get $x $y}] + } + :method getCol {x y} { + # + # Return a column at constant position x. + # + return [lmap y ${:positions} {:get $x $y}] + } + + :method getRegion {x y} { + # + # Return a 3x3 region + # + set xR [expr {($x/3)*3}] + set yR [expr {($y/3)*3}] + set regn {} + for {set x $xR} {$x < $xR+3} {incr x} { + for {set y $yR} {$y < $yR+3} {incr y} { + lappend regn [:get $x $y] + } + } + return $regn + } +} +
The class SudokuSolver
inherits from Sudoku
, and adds the
+ability to solve a given Sudoku game. The method solve applies all
+rules for each unsolved cell until it finds a safe solution.
+nx::Class create SudokuSolver -superclass Sudoku { + + :public method validchoices {x y} { + set v [:get $x $y] + if {$v ne {}} { + return $v + } + + set row [:getRow $x $y] + set col [:getCol $x $y] + set regn [:getRegion $x $y] + set eliminate [list {*}$row {*}$col {*}$regn] + set eliminate [lsearch -all -inline -not $eliminate {}] + set eliminate [lsort -unique $eliminate] + + set choices {} + for {set c 1} {$c < 10} {incr c} { + if {$c ni $eliminate} { + lappend choices $c + } + } + if {[llength $choices]==0} { + error "No choices left for square $x,$y" + } + return $choices + } + + :method completion {} { + # + # Return the number of already solved items. + # + return [expr {81-[llength [lsearch -all -inline [join [:dump]] {}]]}] + } + + :public method solve {} { + # + # Try to solve the sudoku by applying the provided rules. + # + while {1} { + set begin [:completion] + foreach y ${:positions} { + foreach x ${:positions} { + if {[:get $x $y] eq ""} { + foreach rule [Rule info instances] { + set c [$rule solve [self] $x $y] + if {$c} { + :set $x $y $c + :log "[$rule info class] solved [self] at $x,$y for $c" + break + } + } + } + } + } + set end [:completion] + if {$end == 81} { + :log "Finished solving!" + break + } elseif {$begin == $end} { + :log "A round finished without solving any squares, giving up." + break + } + } + } +} +
The class rule provides "solve" as public interface for all rule +objects. The rule objects apply their logic to the values +passed in and return either 0 or a number to allocate to the +requested square.
nx::Class create Rule { + + :public method solve {hSudoku:object,type=::SudokuSolver x y} { + :Solve $hSudoku $x $y [$hSudoku validchoices $x $y] + } + + # Get all the allocated numbers for each square in the the row, column, and + # region containing $x,$y. If there is only one unallocated number among all + # three groups, it must be allocated at $x,$y + :create ruleOnlyChoice { + :object method Solve {hSudoku x y choices} { + if {[llength $choices] == 1} { + return $choices + } else { + return 0 + } + } + } + + # Test each column to determine if $choice is an invalid choice for all other + # columns in row $X. If it is, it must only go in square $x,$y. + :create RuleColumnChoice { + :object method Solve {hSudoku x y choices} { + foreach choice $choices { + set failed 0 + for {set x2 0} {$x2 < 9} {incr x2} { + if {$x2 != $x && $choice in [$hSudoku validchoices $x2 $y]} { + set failed 1 + break + } + } + if {!$failed} {return $choice} + } + return 0 + } + } + + # Test each row to determine if $choice is an invalid choice for all other + # rows in column $y. If it is, it must only go in square $x,$y. + :create RuleRowChoice { + :object method Solve {hSudoku x y choices} { + foreach choice $choices { + set failed 0 + for {set y2 0} {$y2 < 9} {incr y2} { + if {$y2 != $y && $choice in [$hSudoku validchoices $x $y2]} { + set failed 1 + break + } + } + if {!$failed} {return $choice} + } + return 0 + } + } + + # Test each square in the region occupied by $x,$y to determine if $choice is + # an invalid choice for all other squares in that region. If it is, it must + # only go in square $x,$y. + :create RuleRegionChoice { + :object method Solve {hSudoku x y choices} { + foreach choice $choices { + set failed 0 + set regnX [expr {($x/3)*3}] + set regnY [expr {($y/3)*3}] + for {set y2 $regnY} {$y2 < $regnY+3} {incr y2} { + for {set x2 $regnX} {$x2 < $regnX+3} {incr x2} { + if { + ($x2!=$x || $y2!=$y) + && $choice in [$hSudoku validchoices $x2 $y2] + } then { + set failed 1 + break + } + } + } + if {!$failed} {return $choice} + } + return 0 + } + } +} + +SudokuSolver create sudoku { + + :load { + {3 9 4 @ @ 2 6 7 @} + {@ @ @ 3 @ @ 4 @ @} + {5 @ @ 6 9 @ @ 2 @} + + {@ 4 5 @ @ @ 9 @ @} + {6 @ @ @ @ @ @ @ 7} + {@ @ 7 @ @ @ 5 8 @} + + {@ 1 @ @ 6 7 @ @ 8} + {@ @ 9 @ @ 8 @ @ @} + {@ 2 6 4 @ @ 7 3 5} + } + :solve + + puts [:dump -pretty-print] +}
The dump method outputs the solved Sudoku:
+-----+-----+-----+
+|3 9 4|8 5 2|6 7 1|
+|2 6 8|3 7 1|4 5 9|
+|5 7 1|6 9 4|8 2 3|
++-----+-----+-----+
+|1 4 5|7 8 3|9 6 2|
+|6 8 2|9 4 5|3 1 7|
+|9 3 7|1 2 6|5 8 4|
++-----+-----+-----+
+|4 1 3|5 6 7|2 9 8|
+|7 5 9|2 3 8|1 4 6|
+|8 2 6|4 1 9|7 3 5|
++-----+-----+-----+
+