This week's weekend challenge #3 seemed like a great opportunity to learn Ruby! Unfortunately, my workload and looming vacation did not cooperate. :( The puzzle will make forced moves automatically, but I was hoping to implement rules such as Naked Pairs before resorting to brute force.
But there's plenty of code to make a review worthwhile, and I'm hoping some experienced Ruby coders will give it a shot. I have included a few test puzzles (easy and medium solve to completion), and it's easy to add more. See the bottom for a sample test run.
The real meat of the algorithm is the interaction between the Cell
and Group
instances. Board
merely instantiates everything and applies the initial puzzle state.
Update: I'm looking for feedback on Ruby style (e.g. "avoid
and
andor
as they are confusing"), correct use of idioms and the standard library, code organization, etc. This is a Ruby learning exercise, so I want to avoid picking up bad habits early. Since I haven't implemented any brute-force or complex solving yet, performance isn't an issue.
Board.rb
require 'Cell'
require 'Group'
require 'Inspector'
module Sudoku
# Builds the cells and groups and provides an API for setting up and interacting with the puzzle.
#
# Several debugging methods are provided that are being extracted to the inspector.
class Board
# Builds the board structure and initializes an empty puzzle.
#
# @param start [String] optional initial values passed to #setup
def initialize(start = nil)
@cells = Array.new(9) { |r| Array.new(9) { |c| Cell.new r, c } }
@rows = Array.new(9) { |i| Group.new :row, i }
@cols = Array.new(9) { |i| Group.new :col, i }
@boxes = Array.new(9) { |i| Group.new :box, i }
(0..8).each do |i|
connect @rows[i], row_cells(i)
connect @cols[i], col_cells(i)
connect @boxes[i], box_cells(i)
end
setup start if start
end
# Sets the initial values of the puzzle.
#
# Place each row on a separate line and nine cell values on each row.
# Whitespace is ignored around the row on each line. Each cell value
# may be a digit [1-9] or a period (.) if empty.
#
# @param start [String] optional initial values passed to #setup
#
# @example Nine queens
# .....9...
# ...9.....
# ......9..
# 9........
# .......9.
# .9.......
# ....9....
# ..9......
# ........9
def setup(puzzle)
r = 1
puzzle.split.each do |line|
c = 1
line.each_char do |n|
set(r, c, n.to_i, :start) if n =~ /[1-9]/
c += 1
end
r += 1
end
end
# Returns true if the cell can be set to the value.
#
# @param r [Fixnum] one-based row
# @param c [Fixnum] one-based column
# @param n [Fixnum] one-based cell value
# @return [Boolean]
def possible?(r, c, n)
cell(r, c).possible? n
end
# Returns true if the cell has been set to a value.
#
# @param r [Fixnum] one-based row
# @param c [Fixnum] one-based column
# @return [Boolean]
def known?(r, c)
cell(r, c).known?
end
# Sets the cell to the value for the given reason (omit when playing).
#
# @param r [Fixnum] one-based row
# @param c [Fixnum] one-based column
# @param n [Fixnum] one-based cell value
# @param reason [Symbol] one of :start, :choose, or :force
# @return [void]
#
# @raise [RuntimeError] if the cell is already set
# @raise [RuntimeError] if the value is not possible for the cell
def set(r, c, n, reason = :choose)
cell(r, c).set(n, reason)
end
# Iterates over each cell on the board from left-to-right and top-to-bottom.
#
# Used by the inspector.
#
# @yieldparam cell [Cell]
# @return [void]
def each_cell
@cells.each { |row| row.each { |cell| yield cell } }
end
def to_s
@cells.map { |row| row * "" } * "\n"
end
# Creates an inspector for this board.
#
# @return [Inspector] doesn't yet fully implement the output available in this class
def inspector
Inspector.new self
end
private
def cell(r, c)
@cells[r - 1][c - 1]
end
def row_cells(r)
Array.new(9) { |c| @cells[r][c] }
end
def col_cells(c)
Array.new(9) { |r| @cells[r][c] }
end
def box_cells(b)
box_r = b % 3 * 3
box_c = b / 3 * 3
Array.new(9) { |i| @cells[box_r + i % 3][box_c + i / 3] }
end
def connect(group, cells)
cells.each { |cell| cell.join group }
end
### Debugging
public
def inspect
"Board State\n#{to_s}"
end
def debug
debug_cells
debug_rows
debug_cols
debug_boxes
end
def debug_cells
puts "Cell Possibilities"
puts @cells.map { |row| row.map { |cell| cell.debug } * " " } * "\n"
end
def debug_rows
puts "Row Possibilities"
debug_group(@rows)
end
def debug_cols
puts "Column Possibilities"
debug_group(@cols)
end
def debug_boxes
puts "Box Possibilities"
debug_group(@boxes)
end
private
def debug_group(groups)
puts groups.map { |group| group.debug } * "\n"
end
end
end
Group.rb
require 'set'
module Sudoku
# Tracks the cells that make up a single row, column, or 3x3 box, which are still available
# to be set to which values, and the values still left to be set somewhere in the group.
class Group
# Creates an empty group of the given type.
#
# @param type [Symbol] either :row, :col, or :box
# @param index [Fixnum] zero-based index
def initialize(type, index)
@type, @index = type, index
@id = "#{type}-#{index + 1}"
@knowns = {}
@possibles = Hash[(1..9).map { |n| [n, Set.new] }]
end
attr_reader :id, :type, :index
# Makes the cell available for the given value or all values.
#
# @param cell [Cell] the cell that can be set to the value
# @param n [Fixnum or nil] omit to make the cell available for every value
# @return [void]
def available(cell, n = nil)
if n
@possibles[n] << cell
else
@possibles.each_value { |cells| cells << cell }
end
end
# Makes the cell unavailable to receive the value.
#
# If the value has only one cell possibility left, it is set to the value.
#
# @param cell [Cell] the cell that can no longer be set to the value
# @param n [Fixnum] one-based cell value
# @return [void]
def unavailable(cell, n)
# puts "%s - unavail: %s -> %d: %s" % [id, cell.id, n, debug]
cells = @possibles[n]
cells.delete cell
if cells.size == 1 and !known? n
cells.take(1).first.set n, :force
end
end
# Returns true if a cell in this group has been set to the value.
#
# @param n [Fixnum] one-based cell value
# @return [Boolean] true if set; false if still available for this group
def known?(n)
!!@knowns[n]
end
# Returns true if no cell in this group has been set to the value.
#
# @param n [Fixnum] one-based cell value
# @return [Boolean] false if none set; true if still available for this group
def possible?(n)
!@knowns[n]
end
# Returns the number of cells that can still be set to the value.
#
# @param n [Fixnum] one-based cell value
# @return [Fixnum]
def num_possible(n)
@possibles[n].size
end
# Stores that the cell has been set to the value and removes the value as a possible from other cells.
#
# @param cell [Cell] the cell that was just set
# @param n [Fixnum] one-based cell value
# @return [void]
def known(cell, n)
# puts "%s - known : %s -> %d: %s" % [id, cell.id, n, debug]
raise "#{@knowns[n]} in #{@id} already known for #{n}" if known? n
raise "#{cell} in #{@id} not possible for #{n}" unless @possibles[n].delete? cell
@knowns[n] = cell
@possibles[n].each { |possible| possible.unavailable self, n }
end
# Returns a string denoting which values are still possible and in how many cells.
#
# @return [String] first group: possible values are as-is; not possible values are dots;
# second group: number of available cells per cell value
def debug
((1..9).map { |n| possible?(n) ? n : "." } * "") + " " + ((1..9).map { |n| num_possible n } * "")
end
end
end
Cell.rb
require 'set'
module Sudoku
# Models a single cell that tracks the possible values to which it may be set
# and the current value if already set.
class Cell
# Creates an empty cell at the given row and column.
#
# @param r [Fixnum] zero-based row number
# @param c [Fixnum] zero-based column number
def initialize(r, c)
@r, @c = r, c
@id = "(#{r + 1},#{c + 1})"
@initial = @current = nil
@possibles = (1..9).to_set
@groups = []
end
# Joins the given group and makes this cell available for all numbers.
#
# @param group [Group] contains this cell
# @return [void]
def join(group)
@groups << group
group.available self
end
attr_reader :id, :r, :c, :initial, :current
# Returns true if this cell can be set to the value.
#
# @param n [Fixnum] one-based cell value
# @return [Boolean]
def possible?(n)
!known? and @possibles.include? n
end
# Iterates over each value this cell can be set to.
#
# @yieldparam n [Fixnum] one-based cell value
# @return [void]
def each_possible
@possibles.each { |n| yield n } if !known?
end
# Returns true if this cell has been set to a value.
#
# @return [Boolean]
def known?
!!@current
end
# Removes a value from this cell's set of possibilities.
#
# If this leaves the cell with but one possibility, the cell is set to it.
#
# @param n [Fixnum] one-based cell value
# @return [void]
#
# @raise [RuntimeError] if n is the only possibility left
def known(n)
if possible? n
raise "No possibilities left for cell #{@id}" if @possibles.size == 1
@possibles.delete n
set(@possibles.take(1).first, :force) if @possibles.size == 1
end
end
# Notifies this cell's groups that the value is no longer possible for this cell.
#
# @param n [Fixnum] one-based cell value
# @return [void]
#
# @todo Merge with #known.
def unavailable(group, n)
# puts " - unavail: %s -> %d: %s from %s" % [id, n, debug, group.id]
@groups.each { |possible| possible.unavailable self, n }
known n
end
# Sets this cell to the value and notifies each group.
#
# @param n [Fixnum] one-based cell value
# @param reason [Symbol] either :setup, :choose, or :force
# @return [void]
#
# @raise [RuntimeError] if this cell is already set
# @raise [RuntimeError] if the value is not possible for this cell
def set(n, reason)
puts " - %-7s: %s -> %d: %s" % [reason, id, n, debug]
@initial = n if reason == :initial
return if @current == n
raise "cannot change #{@id} from #{@current} to #{n}" if @current
raise "Cannot set #{@id} to #{n}" if !possible? n
@possibles.delete n
@current = n
@groups.each do |group|
group.known self, n
@possibles.each { |n| group.unavailable self, n }
end
@possibles.clear
end
# Returns the current value of this cell if set or a dot if not.
#
# @return [String] single digit or a dot
def to_s
(@current || ".").to_s
end
# Returns a string denoting which values are still possible.
#
# @return [String] possible values are as-is; not possible values are dots
def debug
(1..9).map { |n| possible?(n) ? n : "." } * ""
end
end
end
Test.rb
require 'Board'
module Sudoku
# Creates several test puzzles from easy to evil.
#
# @see http://www.websudoku.com/
module Test
def self.easy
self.test <<-PUZZLE
..38.4.5.
84......7
6....29..
.18.5..9.
2.6.7.4.5
.3..4.86.
..51....8
1......79
.6.5.7.2.
PUZZLE
end
def self.medium
self.test <<-PUZZLE
.5.....7.
9428.....
....6...5
48..765..
.93...78.
..598..14
7...4....
.....8193
.1.....4.
PUZZLE
end
def self.hard
self.test <<-PUZZLE
.....5.8.
.8....5.1
..5.12.69
......17.
.2.....3.
.19......
15.34.7..
8.7....1.
.6.9.....
PUZZLE
end
def self.evil # http://www.websudoku.com/?level=4&set_id=6247568390
self.test <<-PUZZLE
.4.2.....
.....13.7
..5...91.
.8..13...
..3.6.5..
...72..4.
.12...4..
8.69.....
.....2.3.
PUZZLE
end
def self.test(puzzle)
puts "Initial Puzzle"
puts puzzle
b = Board.new puzzle
b.debug
b.inspector.cells
b
end
end
end
Inspector.rb
module Sudoku
# Assists debugging the internal state of the board.
class Inspector
# Stores the board for inspection
#
# @param board [Board]
def initialize(board)
@board = board
end
# Displays the current cell values.
def current
rows = []
row = nil
@board.each_cell do |cell|
if cell.c == 0
row = ""
rows << "" if cell.r != 0 && cell.r % 3 == 0
end
row += " " if cell.c != 0 && cell.c % 3 == 0
row += cell.to_s
rows << row if cell.c == 8
end
puts "Current Puzzle"
puts
puts rows * "\n"
end
# Displays the available values for each cell.
def cells
grid = ([["... " * 9] * 3, ""].flatten * 9).map(&:clone)
@board.each_cell do |cell|
r, c = 4 * cell.r, 4 * cell.c
cell.each_possible do |n|
grid[r + (n - 1) / 3][c + (n - 1) % 3] = n.to_s
end
end
puts "Cell Possibilities"
puts
puts grid * "\n"
end
def inspect
"Inspector"
end
end
end
Sample Test Run
-> require 'Test'
-> Sudoku::Test::easy
Initial Puzzle
..38.4.5.
84......7
6....29..
.18.5..9.
2.6.7.4.5
.3..4.86.
..51....8
1......79
.6.5.7.2.
start : (1,3) -> 3: 123456789
start : (1,4) -> 8: 12.456789
start : (1,6) -> 4: 12.4567.9
start : (1,8) -> 5: 12..567.9
start : (2,1) -> 8: 12.456789
start : (2,2) -> 4: 12.4567.9
start : (2,9) -> 7: 123..67.9
start : (3,1) -> 6: 12..567.9
start : (3,6) -> 2: 123.5.7.9
start : (3,7) -> 9: 1.34...89
start : (4,2) -> 1: 123.56789
start : (4,3) -> 8: .2.456789
start : (4,5) -> 5: .234567.9
start : (4,8) -> 9: .234.67.9
start : (5,1) -> 2: .2345.7.9
start : (5,3) -> 6: ...4567.9
start : (5,5) -> 7: 1.34..789
force : (3,4) -> 7: 1.3.5.7..
force : (3,2) -> 5: ....5....
force : (3,3) -> 1: 1........
force : (3,5) -> 3: ..3......
start : (5,7) -> 4: 1.345..8.
force : (5,9) -> 5: 1.3.5..8.
start : (5,9) -> 5: .........
start : (6,2) -> 3: ..3...7.9
force : (5,2) -> 9: ........9
start : (6,5) -> 4: 12.4.6.89
force : (4,1) -> 4: ...4..7..
force : (4,7) -> 7: .23..67..
force : (6,5) -> 4: .........
start : (6,7) -> 8: 12...6.8.
force : (5,6) -> 8: 1.3....8.
force : (6,7) -> 8: .........
start : (6,8) -> 6: 12...6...
start : (7,3) -> 5: .2.45.7.9
force : (6,1) -> 5: ....5.7..
force : (6,3) -> 7: ....5.7..
force : (6,1) -> 5: .........
force : (7,3) -> 5: .........
start : (7,4) -> 1: 1234.6..9
force : (5,8) -> 1: 1.3......
force : (5,4) -> 3: 1.3......
force : (4,9) -> 3: .23......
force : (4,4) -> 2: .2...6...
force : (4,6) -> 6: ..3..6...
force : (4,4) -> 2: .........
force : (6,9) -> 2: 12.......
force : (5,8) -> 1: .........
force : (6,6) -> 1: 1.......9
force : (6,4) -> 9: 1.......9
force : (6,6) -> 1: .........
start : (7,9) -> 8: ...4.6.89
force : (7,8) -> 4: .234..7..
force : (3,9) -> 4: ...4...8.
force : (3,8) -> 8: ...4...8.
force : (3,9) -> 4: .........
force : (7,8) -> 4: .........
force : (7,9) -> 8: .........
start : (8,1) -> 1: 1.3...7.9
force : (8,1) -> 1: .........
start : (8,8) -> 7: .23...7..
force : (8,8) -> 7: .........
start : (8,9) -> 9: .....6..9
force : (8,9) -> 9: .........
start : (9,2) -> 6: .2...678.
force : (9,2) -> 6: .........
force : (8,2) -> 8: .2.....8.
force : (9,5) -> 8: .2...6.89
force : (9,5) -> 8: .........
force : (8,2) -> 8: .........
force : (1,9) -> 6: 1....6...
force : (9,9) -> 1: 1....6...
force : (9,9) -> 1: .........
start : (9,4) -> 5: ...45....
force : (2,6) -> 5: ....5...9
force : (2,4) -> 6: .....6...
force : (2,4) -> 6: .........
force : (8,6) -> 3: ..3......
force : (8,6) -> 3: .........
force : (9,4) -> 5: .........
force : (8,7) -> 5: .2..56...
force : (9,4) -> 5: .........
force : (8,7) -> 5: .........
force : (7,7) -> 6: .23..6...
force : (8,5) -> 6: .2...6...
force : (7,5) -> 2: .2...6..9
force : (8,5) -> 6: .........
force : (1,2) -> 2: .2....7..
force : (1,7) -> 1: 1........
force : (2,5) -> 1: 1.......9
force : (1,7) -> 1: .........
force : (1,7) -> 1: .........
force : (2,3) -> 9: .2......9
force : (1,5) -> 9: 1.......9
force : (2,3) -> 9: .........
force : (1,5) -> 9: .........
force : (2,5) -> 1: .........
force : (2,5) -> 1: .........
force : (1,1) -> 7: ......7..
force : (1,1) -> 7: .........
force : (7,2) -> 7: .2....7..
force : (1,2) -> 2: .........
force : (1,1) -> 7: .........
force : (9,6) -> 7: ......7.9
force : (9,6) -> 7: .........
force : (7,2) -> 7: .........
force : (7,6) -> 9: ........9
force : (7,6) -> 9: .........
force : (9,1) -> 9: ..3...7.9
force : (7,1) -> 3: ..3.....9
force : (1,1) -> 7: .........
force : (2,3) -> 9: .........
force : (9,1) -> 9: .........
force : (9,6) -> 7: .........
force : (1,2) -> 2: .........
force : (7,7) -> 6: .........
force : (7,7) -> 6: .........
force : (8,3) -> 2: .2.4.....
force : (8,3) -> 2: .........
force : (9,3) -> 4: ...4.....
force : (9,3) -> 4: .........
force : (8,4) -> 4: ...45....
force : (8,4) -> 4: .........
force : (9,3) -> 4: .........
force : (9,4) -> 5: .........
start : (9,6) -> 7: .........
start : (9,8) -> 2: .23......
force : (2,7) -> 2: .23......
force : (2,8) -> 3: .23......
force : (9,7) -> 3: .23......
force : (2,7) -> 2: .........
force : (2,8) -> 3: .........
force : (9,7) -> 3: .........
force : (9,8) -> 2: .........
force : (9,8) -> 2: .........
force : (2,7) -> 2: .........
Board State
723894156
849615237
651732984
418256793
296378415
537941862
375129648
182463579
964587321