sudoku-solver 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +19 -0
- data/README.md +3 -0
- data/Rakefile +41 -0
- data/bin/solve-sudoku +27 -0
- data/lib/sudoku.rb +88 -0
- data/lib/sudoku_item.rb +60 -0
- data/lib/sudoku_result.rb +46 -0
- data/lib/sudoku_solver.rb +139 -0
- metadata +75 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Dorian Sarnowski <dorian.sarnowski@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'rake/testtask'
|
7
|
+
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
s.name = 'sudoku-solver'
|
10
|
+
s.version = '0.1.1'
|
11
|
+
s.has_rdoc = true
|
12
|
+
s.extra_rdoc_files = ['README.md', 'LICENSE']
|
13
|
+
s.summary = 'sudoku solver library'
|
14
|
+
s.description = 'set of classes to solve sudoku'
|
15
|
+
s.author = 'Dorian Sarnowski'
|
16
|
+
s.email = 'dorian.sarnowski@gmail.com'
|
17
|
+
s.homepage = 'https://github.com/dorians/sudoku'
|
18
|
+
s.executables = ['solve-sudoku']
|
19
|
+
s.files = %w(LICENSE README.md Rakefile) + Dir.glob("{bin,lib,spec}/**/*")
|
20
|
+
s.require_path = "lib"
|
21
|
+
s.bindir = "bin"
|
22
|
+
end
|
23
|
+
|
24
|
+
Rake::GemPackageTask.new(spec) do |p|
|
25
|
+
p.gem_spec = spec
|
26
|
+
p.need_tar = true
|
27
|
+
p.need_zip = true
|
28
|
+
end
|
29
|
+
|
30
|
+
Rake::RDocTask.new do |rdoc|
|
31
|
+
files =['README.md', 'LICENSE', 'lib/**/*.rb']
|
32
|
+
rdoc.rdoc_files.add(files)
|
33
|
+
rdoc.main = "README.md" # page to start on
|
34
|
+
rdoc.title = "sudoku Docs"
|
35
|
+
rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
|
36
|
+
rdoc.options << '--line-numbers'
|
37
|
+
end
|
38
|
+
|
39
|
+
Rake::TestTask.new do |t|
|
40
|
+
t.test_files = FileList['test/**/*.rb']
|
41
|
+
end
|
data/bin/solve-sudoku
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
NAME = $0
|
5
|
+
|
6
|
+
require 'sudoku_solver'
|
7
|
+
|
8
|
+
begin
|
9
|
+
input = ARGV.first
|
10
|
+
data = open(input).read
|
11
|
+
rescue
|
12
|
+
puts "#{NAME}: missing file operand"
|
13
|
+
#puts "Try `#{NAME} --help` for more information."
|
14
|
+
else
|
15
|
+
begin
|
16
|
+
sudoku = Sudoku.new(data)
|
17
|
+
solver = SudokuSolver.new sudoku
|
18
|
+
result = solver.solve
|
19
|
+
|
20
|
+
if result.correct?
|
21
|
+
puts result
|
22
|
+
else
|
23
|
+
puts 'Oh no! Something went wrong'
|
24
|
+
end
|
25
|
+
rescue
|
26
|
+
end
|
27
|
+
end
|
data/lib/sudoku.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'sudoku_item.rb'
|
4
|
+
|
5
|
+
class Sudoku
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize data
|
10
|
+
# set up data container (9x9 array)
|
11
|
+
@data = Array.new(9) { Array.new(9) }
|
12
|
+
|
13
|
+
# convert insert data into one-dimensional array
|
14
|
+
data = data.scan(/\d| /)
|
15
|
+
|
16
|
+
if data.size == 81
|
17
|
+
# split array into 9-elements arrays
|
18
|
+
data.each_index do |i|
|
19
|
+
@data[i/9][i%9] = SudokuItem.new(self, data.at(i))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def clone
|
25
|
+
Sudoku.new self.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def at x, y
|
29
|
+
@data.at(y).at(x)
|
30
|
+
end
|
31
|
+
|
32
|
+
def set x, y, value
|
33
|
+
at(x, y).set(value) unless at(x, y).related.any? {|item| item.to_i == value }
|
34
|
+
end
|
35
|
+
|
36
|
+
def count_set
|
37
|
+
count {|item| item.set?}
|
38
|
+
end
|
39
|
+
|
40
|
+
def groups
|
41
|
+
boxes + horizontals + verticals
|
42
|
+
end
|
43
|
+
|
44
|
+
def boxes
|
45
|
+
(group_by {|item| (item.x / 3).to_s + (item.y / 3).to_s }).values
|
46
|
+
end
|
47
|
+
|
48
|
+
def horizontals
|
49
|
+
@data
|
50
|
+
end
|
51
|
+
|
52
|
+
def verticals
|
53
|
+
@data.transpose
|
54
|
+
end
|
55
|
+
|
56
|
+
def solved?
|
57
|
+
all? {|item| item.set?}
|
58
|
+
end
|
59
|
+
|
60
|
+
def each
|
61
|
+
each_with_position {|item, x, y| yield item}
|
62
|
+
end
|
63
|
+
|
64
|
+
def each_with_position
|
65
|
+
@data.each_with_index {|row, y| row.each_with_index {|item, x| yield item, x, y}}
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
# output schemas
|
70
|
+
break_line = '+---+---+---+'
|
71
|
+
data_line = "|%s%s%s|%s%s%s|%s%s%s|"
|
72
|
+
|
73
|
+
# initialize the output variable as an array
|
74
|
+
result = [break_line]
|
75
|
+
|
76
|
+
@data.each_with_index do |row, i|
|
77
|
+
# add formated data line to output variable
|
78
|
+
result.push sprintf(data_line, *row)
|
79
|
+
|
80
|
+
# add a break line to the output variable if nedded
|
81
|
+
result.push break_line if (i.succ % 3).zero?
|
82
|
+
end
|
83
|
+
|
84
|
+
# return output variable with EOL symbol between each element
|
85
|
+
result.join("\n")
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
data/lib/sudoku_item.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
class SudokuItem
|
4
|
+
|
5
|
+
def initialize sudoku, value
|
6
|
+
@sudoku = sudoku
|
7
|
+
set value
|
8
|
+
end
|
9
|
+
|
10
|
+
def x
|
11
|
+
position.first
|
12
|
+
end
|
13
|
+
|
14
|
+
def y
|
15
|
+
position.last
|
16
|
+
end
|
17
|
+
|
18
|
+
def set value
|
19
|
+
@value = value.to_i.zero? ? nil : value.to_i unless set?
|
20
|
+
end
|
21
|
+
|
22
|
+
def position
|
23
|
+
@position || @sudoku.each_with_position do |element, x, y|
|
24
|
+
return (@position = x.to_i, y.to_i) if element == self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def related
|
29
|
+
(related_in_box + related_vertical + related_horizontal).uniq
|
30
|
+
end
|
31
|
+
|
32
|
+
def related_in_box
|
33
|
+
@sudoku.find_all {|item| (same_box? item) && (item != self)}
|
34
|
+
end
|
35
|
+
|
36
|
+
def related_vertical
|
37
|
+
@sudoku.find_all {|item| (item.x == self.x) && (item != self)}
|
38
|
+
end
|
39
|
+
|
40
|
+
def related_horizontal
|
41
|
+
@sudoku.find_all {|item| (item.y == self.y) && (item != self)}
|
42
|
+
end
|
43
|
+
|
44
|
+
def same_box? item
|
45
|
+
(self.x / 3) == (item.x / 3) && (self.y / 3) == (item.y / 3)
|
46
|
+
end
|
47
|
+
|
48
|
+
def set?
|
49
|
+
not @value.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_i
|
53
|
+
@value.to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
set? ? @value.to_s : ' '
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'sudoku'
|
4
|
+
|
5
|
+
class SudokuResult
|
6
|
+
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
attr_reader :depth, :forks
|
10
|
+
|
11
|
+
def initialize depth
|
12
|
+
@results = []
|
13
|
+
@depth = depth
|
14
|
+
@forks = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def each &block
|
18
|
+
@results.each &block
|
19
|
+
end
|
20
|
+
|
21
|
+
def << sudoku
|
22
|
+
@results << sudoku
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def correct?
|
27
|
+
@results.count == 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def many_solutions?
|
31
|
+
@results.count > 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
@results.empty? ? '' : @results.first.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def merge other
|
39
|
+
other.each do |sudoku|
|
40
|
+
self.<< sudoku
|
41
|
+
end
|
42
|
+
@depth = [@depth, other.depth].max
|
43
|
+
@forks += other.forks.succ
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'sudoku'
|
4
|
+
require 'sudoku_result'
|
5
|
+
|
6
|
+
class SudokuSolver
|
7
|
+
|
8
|
+
def initialize sudoku
|
9
|
+
@sudoku = sudoku.clone
|
10
|
+
@depth = 0
|
11
|
+
|
12
|
+
initialize_capabilities
|
13
|
+
end
|
14
|
+
|
15
|
+
def clone
|
16
|
+
solver = super
|
17
|
+
|
18
|
+
solver.sudoku = @sudoku.clone
|
19
|
+
solver.capabilities = Marshal.load(Marshal.dump(@capabilities))
|
20
|
+
@depth += 1
|
21
|
+
|
22
|
+
solver
|
23
|
+
end
|
24
|
+
|
25
|
+
def solve
|
26
|
+
result = SudokuResult.new @depth
|
27
|
+
|
28
|
+
while (solve_method1 || solve_method2) do
|
29
|
+
return result unless correct?
|
30
|
+
end
|
31
|
+
|
32
|
+
if @sudoku.solved?
|
33
|
+
result << @sudoku
|
34
|
+
else
|
35
|
+
element = find_best_element_to_random_solve
|
36
|
+
|
37
|
+
@capabilities[element.y][element.x].each do |random|
|
38
|
+
solver = self.clone
|
39
|
+
solver.set(element.x, element.y, random)
|
40
|
+
result.merge solver.solve
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
attr_accessor :sudoku, :capabilities, :depth
|
50
|
+
|
51
|
+
def set x, y, value
|
52
|
+
# delegate a task
|
53
|
+
if @sudoku.set x, y, value
|
54
|
+
# item was set, so clear capabilities
|
55
|
+
@capabilities[y][x].clear
|
56
|
+
|
57
|
+
# remove set value from capabilities of related items
|
58
|
+
@sudoku.at(x, y).related.each do |item|
|
59
|
+
@capabilities[item.y][item.x] -= [value]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def correct?
|
65
|
+
@sudoku.each do |item|
|
66
|
+
return false if (not item.set?) && @capabilities[item.y][item.x].empty?
|
67
|
+
end
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def solve_method1
|
74
|
+
@capabilities.each_with_index do |row, y|
|
75
|
+
row.each_with_index do |element, x|
|
76
|
+
if element.size == 1
|
77
|
+
return set x, y, element.first
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
def solve_method2
|
85
|
+
@sudoku.groups.each do |group|
|
86
|
+
set_digits = (group.find_all {|item| item.set?}).map {|item| item.to_i}
|
87
|
+
capabilities_digits = (1..9).to_a - set_digits
|
88
|
+
|
89
|
+
capabilities_digits.each do |digit|
|
90
|
+
capabilities_items = group.find_all do |item|
|
91
|
+
@capabilities[item.y][item.x].include? digit
|
92
|
+
end
|
93
|
+
if capabilities_items.size == 1
|
94
|
+
return set capabilities_items.first.x, capabilities_items.first.y, digit
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
false
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_best_element_to_random_solve
|
102
|
+
min_element = nil
|
103
|
+
|
104
|
+
@capabilities.each_with_index do |row, y|
|
105
|
+
row.each_with_index do |capabilities, x|
|
106
|
+
element = @sudoku.at(x, y)
|
107
|
+
current_count = capabilities.count
|
108
|
+
smallest_count = min_element && @capabilities[min_element.y][min_element.x].count
|
109
|
+
|
110
|
+
if current_count > 1
|
111
|
+
if current_count == 2
|
112
|
+
return element
|
113
|
+
elsif smallest_count.nil? || current_count < smallest_count
|
114
|
+
min_element = element
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
min_element
|
121
|
+
end
|
122
|
+
|
123
|
+
def initialize_capabilities
|
124
|
+
# each item of sudoku can be any digit from 1 to 9 at the beginning
|
125
|
+
@capabilities = Array.new(9) {Array.new(9) { (1..9).to_a }}
|
126
|
+
|
127
|
+
@sudoku.each do |item|
|
128
|
+
# if item is set, then capabilities should be empty
|
129
|
+
@capabilities[item.y][item.x].clear if item.set?
|
130
|
+
|
131
|
+
# get related as array of integers
|
132
|
+
related = item.related.collect {|element| element.to_i}
|
133
|
+
|
134
|
+
# any digit from related shouldn't be one of capabilities
|
135
|
+
@capabilities[item.y][item.x] -= related
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sudoku-solver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 25
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Dorian Sarnowski
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-13 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: set of classes to solve sudoku
|
23
|
+
email: dorian.sarnowski@gmail.com
|
24
|
+
executables:
|
25
|
+
- solve-sudoku
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.md
|
30
|
+
- LICENSE
|
31
|
+
files:
|
32
|
+
- LICENSE
|
33
|
+
- README.md
|
34
|
+
- Rakefile
|
35
|
+
- bin/solve-sudoku
|
36
|
+
- lib/sudoku_result.rb
|
37
|
+
- lib/sudoku_item.rb
|
38
|
+
- lib/sudoku.rb
|
39
|
+
- lib/sudoku_solver.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: https://github.com/dorians/sudoku
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
hash: 3
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
hash: 3
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.3.7
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: sudoku solver library
|
74
|
+
test_files: []
|
75
|
+
|