sudokusolver 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +1 -0
- data/Changes.rdoc +1 -0
- data/LICENSE +22 -0
- data/README.rdoc +20 -0
- data/Rakefile +256 -0
- data/lib/sudokusolver.rb +197 -0
- data/test/driver.rb +10 -0
- data/test/tc_sudoku.rb +53 -0
- metadata +57 -0
data/COPYING
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
See the file called LICENSE
|
data/Changes.rdoc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2007-04-09 Version 0.1 => initial release
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2007
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
This sofware was translated from the python source code
|
2
|
+
obtained from Peter Norvig's website:
|
3
|
+
|
4
|
+
http://www.norvig.com/sudoku.html
|
5
|
+
http://www.norvig.com/sudo.py
|
6
|
+
|
7
|
+
Thank you to Peter Norvig for the original python source code,
|
8
|
+
algorithms and exceptionally clear explanations.
|
9
|
+
|
10
|
+
example:
|
11
|
+
========
|
12
|
+
|
13
|
+
require 'sudoku_solver'
|
14
|
+
|
15
|
+
# The puzzle representation is simply the 9 rows of the Sudoku grid stringed together
|
16
|
+
# from top to bottom (periods representing blank squares)
|
17
|
+
|
18
|
+
puzzle = "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......"
|
19
|
+
s = SudokuSolver.new
|
20
|
+
s.print_grid(s.search(s.parse_grid(puzzle)))
|
data/Rakefile
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
# Rakefile
|
2
|
+
require "rake/testtask"
|
3
|
+
require "rake/clean"
|
4
|
+
require "rake/rdoctask"
|
5
|
+
require "rake/gempackagetask"
|
6
|
+
|
7
|
+
#---
|
8
|
+
# The name of your project
|
9
|
+
PROJECT = "SudokuSolver"
|
10
|
+
|
11
|
+
# Your name, used in packaging.
|
12
|
+
MY_NAME = "Martin-Louis Bright"
|
13
|
+
|
14
|
+
# Your email address, used in packaging.
|
15
|
+
MY_EMAIL = "mlbright@gmail.com"
|
16
|
+
|
17
|
+
# Short summary of your project, used in packaging.
|
18
|
+
PROJECT_SUMMARY = "Commandline program and library for solving Sudoku puzzles"
|
19
|
+
|
20
|
+
# The project's package name (as opposed to its display name). Used for
|
21
|
+
# RubyForge connectivity and packaging.
|
22
|
+
UNIX_NAME = "sudokusolver"
|
23
|
+
|
24
|
+
# Your RubyForge user name.
|
25
|
+
RUBYFORGE_USER = ENV["RUBYFORGE_USER"] || "mlbright"
|
26
|
+
|
27
|
+
# Directory on RubyForge where your website's files should be uploaded.
|
28
|
+
WEBSITE_DIR = "sudokusolver"
|
29
|
+
|
30
|
+
# Output directory for the rdoc html files.
|
31
|
+
# If you don't have a custom homepage, and want to use the RDoc
|
32
|
+
# index.html as homepage, just set it to WEBSITE_DIR.
|
33
|
+
RDOC_HTML_DIR = "#{WEBSITE_DIR}/rdoc"
|
34
|
+
|
35
|
+
#---
|
36
|
+
# Variable settings for extension support.
|
37
|
+
EXT_DIR = "ext"
|
38
|
+
HAVE_EXT = File.directory?(EXT_DIR)
|
39
|
+
EXTCONF_FILES = FileList["#{EXT_DIR}/**/extconf.rb"]
|
40
|
+
EXT_SOURCES = FileList["#{EXT_DIR}/**/*.{c,h}"]
|
41
|
+
# Eventually add other files from EXT_DIR, like "MANIFEST"
|
42
|
+
EXT_DIST_FILES = EXT_SOURCES + EXTCONF_FILES
|
43
|
+
|
44
|
+
#---
|
45
|
+
REQUIRE_PATHS = ["lib"]
|
46
|
+
REQUIRE_PATHS << EXT_DIR if HAVE_EXT
|
47
|
+
$LOAD_PATH.concat(REQUIRE_PATHS)
|
48
|
+
# This library file defines the MyProject::VERSION constant.
|
49
|
+
require "#{UNIX_NAME}"
|
50
|
+
PROJECT_VERSION = eval("#{PROJECT}::VERSION") # e.g. "1.0.2"
|
51
|
+
|
52
|
+
#---
|
53
|
+
# Clobber object files and Makefiles generated by extconf.rb.
|
54
|
+
CLOBBER.include("#{EXT_DIR}/**/*.{so,dll,o}", "#{EXT_DIR}/**/Makefile")
|
55
|
+
# Clobber .config generated by setup.rb.
|
56
|
+
CLOBBER.include(".config")
|
57
|
+
|
58
|
+
#---
|
59
|
+
# Options common to RDocTask AND Gem::Specification.
|
60
|
+
# The --main argument specifies which file appears on the index.html page
|
61
|
+
GENERAL_RDOC_OPTS = {
|
62
|
+
"--title" => "#{PROJECT} API documentation",
|
63
|
+
"--main" => "README.rdoc"
|
64
|
+
}
|
65
|
+
|
66
|
+
# Additional RDoc formatted files, besides the Ruby source files.
|
67
|
+
RDOC_FILES = FileList["README.rdoc", "Changes.rdoc"]
|
68
|
+
# Remove the following line if you don't want to extract RDoc from
|
69
|
+
# the extension C sources.
|
70
|
+
RDOC_FILES.include(EXT_SOURCES)
|
71
|
+
|
72
|
+
# Ruby library code.
|
73
|
+
LIB_FILES = FileList["lib/**/*.rb"]
|
74
|
+
|
75
|
+
# Filelist with Test::Unit test cases.
|
76
|
+
TEST_FILES = FileList["test/**/tc_*.rb"]
|
77
|
+
|
78
|
+
# Executable scripts, all non-garbage files under bin/.
|
79
|
+
BIN_FILES = FileList["bin/*"]
|
80
|
+
|
81
|
+
# This filelist is used to create source packages.
|
82
|
+
# Include all Ruby and RDoc files.
|
83
|
+
DIST_FILES = FileList["**/*.rb", "**/*.rdoc"]
|
84
|
+
DIST_FILES.include("Rakefile", "COPYING", "LICENSE")
|
85
|
+
DIST_FILES.include(BIN_FILES)
|
86
|
+
DIST_FILES.include("data/**/*", "test/data/**/*")
|
87
|
+
DIST_FILES.include("#{WEBSITE_DIR}/**/*.{html,css}", "man/*.[0-9]")
|
88
|
+
# Don't package files which are autogenerated by RDocTask
|
89
|
+
DIST_FILES.exclude(/^(\.\/)?#{RDOC_HTML_DIR}(\/|$)/)
|
90
|
+
# Include extension source files.
|
91
|
+
DIST_FILES.include(EXT_DIST_FILES)
|
92
|
+
# Don't package temporary files, perhaps created by tests.
|
93
|
+
DIST_FILES.exclude("**/temp_*", "**/*.tmp")
|
94
|
+
# Don't get into recursion...
|
95
|
+
DIST_FILES.exclude(/^(\.\/)?pkg(\/|$)/)
|
96
|
+
|
97
|
+
#---
|
98
|
+
# Run the tests if rake is invoked without arguments.
|
99
|
+
task "default" => ["test"]
|
100
|
+
|
101
|
+
test_task_name = HAVE_EXT ? "run-tests" : "test"
|
102
|
+
Rake::TestTask.new(test_task_name) do |t|
|
103
|
+
t.test_files = TEST_FILES
|
104
|
+
t.libs = REQUIRE_PATHS
|
105
|
+
end
|
106
|
+
|
107
|
+
#---
|
108
|
+
# Set an environment variable with any configuration options you want to
|
109
|
+
# be passed through to "setup.rb config".
|
110
|
+
CONFIG_OPTS = ENV["CONFIG"]
|
111
|
+
if HAVE_EXT
|
112
|
+
file_create ".config" do
|
113
|
+
ruby "setup.rb config #{CONFIG_OPTS}"
|
114
|
+
end
|
115
|
+
|
116
|
+
desc "Configure and make extension. " +
|
117
|
+
"The CONFIG variable is passed to `setup.rb config'"
|
118
|
+
task "make-ext" => ".config" do
|
119
|
+
# The -q option suppresses messages from setup.rb.
|
120
|
+
ruby "setup.rb -q setup"
|
121
|
+
end
|
122
|
+
|
123
|
+
desc "Run tests after making the extension."
|
124
|
+
task "test" do
|
125
|
+
Rake::Task["make-ext"].invoke
|
126
|
+
Rake::Task["run-tests"].invoke
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
#---
|
131
|
+
# The "rdoc" task generates API documentation.
|
132
|
+
Rake::RDocTask.new("rdoc") do |t|
|
133
|
+
t.rdoc_files = RDOC_FILES + LIB_FILES
|
134
|
+
t.title = GENERAL_RDOC_OPTS["--title"]
|
135
|
+
t.main = GENERAL_RDOC_OPTS["--main"]
|
136
|
+
t.rdoc_dir = RDOC_HTML_DIR
|
137
|
+
end
|
138
|
+
|
139
|
+
#---
|
140
|
+
GEM_SPEC = Gem::Specification.new do |s|
|
141
|
+
s.name = UNIX_NAME
|
142
|
+
s.version = PROJECT_VERSION
|
143
|
+
s.summary = PROJECT_SUMMARY
|
144
|
+
s.rubyforge_project = UNIX_NAME
|
145
|
+
s.homepage = "http://#{UNIX_NAME}.rubyforge.org/"
|
146
|
+
s.author = MY_NAME
|
147
|
+
s.email = MY_EMAIL
|
148
|
+
s.files = DIST_FILES
|
149
|
+
s.test_files = TEST_FILES
|
150
|
+
s.executables = BIN_FILES.map { |fn| File.basename(fn) }
|
151
|
+
s.has_rdoc = true
|
152
|
+
s.extra_rdoc_files = RDOC_FILES
|
153
|
+
s.rdoc_options = GENERAL_RDOC_OPTS.to_a.flatten
|
154
|
+
if HAVE_EXT
|
155
|
+
s.extensions = EXTCONF_FILES
|
156
|
+
s.require_paths << EXT_DIR
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Now we can generate the package-related tasks.
|
161
|
+
Rake::GemPackageTask.new(GEM_SPEC) do |pkg|
|
162
|
+
pkg.need_zip = true
|
163
|
+
pkg.need_tar = true
|
164
|
+
end
|
165
|
+
|
166
|
+
#---
|
167
|
+
desc "Upload website to RubyForge. " +
|
168
|
+
"scp will prompt for your RubyForge password."
|
169
|
+
task "publish-website" => ["rdoc"] do
|
170
|
+
rubyforge_path = "/var/www/gforge-projects/#{UNIX_NAME}/"
|
171
|
+
sh "scp -r #{WEBSITE_DIR}/* " +
|
172
|
+
"#{RUBYFORGE_USER}@rubyforge.org:#{rubyforge_path}",
|
173
|
+
:verbose => true
|
174
|
+
end
|
175
|
+
|
176
|
+
#---
|
177
|
+
task "rubyforge-setup" do
|
178
|
+
unless File.exist?(File.join(ENV["HOME"], ".rubyforge"))
|
179
|
+
puts "rubyforge will ask you to edit its config.yml now."
|
180
|
+
puts "Please set the `username' and `password' entries"
|
181
|
+
puts "to your RubyForge username and RubyForge password!"
|
182
|
+
puts "Press ENTER to continue."
|
183
|
+
$stdin.gets
|
184
|
+
sh "rubyforge setup", :verbose => true
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
task "rubyforge-login" => ["rubyforge-setup"] do
|
189
|
+
# Note: We assume that username and password were set in
|
190
|
+
# rubyforge's config.yml.
|
191
|
+
sh "rubyforge login", :verbose => true
|
192
|
+
end
|
193
|
+
|
194
|
+
task "publish-packages" => ["package", "rubyforge-login"] do
|
195
|
+
# Upload packages under pkg/ to RubyForge
|
196
|
+
# This task makes some assumptions:
|
197
|
+
# * You have already created a package on the "Files" tab on the
|
198
|
+
# RubyForge project page. See pkg_name variable below.
|
199
|
+
# * You made entries under package_ids and group_ids for this
|
200
|
+
# project in rubyforge's config.yml. If not, eventually read
|
201
|
+
# "rubyforge --help" and then run "rubyforge setup".
|
202
|
+
pkg_name = ENV["PKG_NAME"] || UNIX_NAME
|
203
|
+
cmd = "rubyforge add_release #{UNIX_NAME} #{pkg_name} " +
|
204
|
+
"#{PROJECT_VERSION} #{UNIX_NAME}-#{PROJECT_VERSION}"
|
205
|
+
cd "pkg" do
|
206
|
+
sh(cmd + ".gem", :verbose => true)
|
207
|
+
sh(cmd + ".tgz", :verbose => true)
|
208
|
+
sh(cmd + ".zip", :verbose => true)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
#---
|
213
|
+
# The "prepare-release" task makes sure your tests run, and then generates
|
214
|
+
# files for a new release.
|
215
|
+
desc "Run tests, generate RDoc and create packages."
|
216
|
+
task "prepare-release" => ["clobber"] do
|
217
|
+
puts "Preparing release of #{PROJECT} version #{VERSION}"
|
218
|
+
Rake::Task["test"].invoke
|
219
|
+
Rake::Task["rdoc"].invoke
|
220
|
+
Rake::Task["package"].invoke
|
221
|
+
end
|
222
|
+
|
223
|
+
# The "publish" task is the overarching task for the whole project. It
|
224
|
+
# builds a release and then publishes it to RubyForge.
|
225
|
+
desc "Publish new release of #{PROJECT}"
|
226
|
+
task "publish" => ["prepare-release"] do
|
227
|
+
puts "Uploading documentation..."
|
228
|
+
Rake::Task["publish-website"].invoke
|
229
|
+
puts "Checking for rubyforge command..."
|
230
|
+
`rubyforge --help`
|
231
|
+
if $? == 0
|
232
|
+
puts "Uploading packages..."
|
233
|
+
Rake::Task["publish-packages"].invoke
|
234
|
+
puts "Release done!"
|
235
|
+
else
|
236
|
+
puts "Can't invoke rubyforge command."
|
237
|
+
puts "Either install rubyforge with 'gem install rubyforge'"
|
238
|
+
puts "and retry or upload the package files manually!"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
#---
|
243
|
+
# $ rake -T
|
244
|
+
# rake clean # Remove any temporary products.
|
245
|
+
# rake clobber # Remove any generated file.
|
246
|
+
# rake clobber_package # Remove package products
|
247
|
+
# rake clobber_rdoc # Remove rdoc products
|
248
|
+
# rake package # Build all the packages
|
249
|
+
# rake prepare-release # Run tests, generate RDoc and create packages.
|
250
|
+
# rake publish # Publish new release of MyProject
|
251
|
+
# rake publish-website # Upload website to RubyForge. scp will prompt for your RubyForge password.
|
252
|
+
# rake rdoc # Build the rdoc HTML Files
|
253
|
+
# rake repackage # Force a rebuild of the package files
|
254
|
+
# rake rerdoc # Force a rebuild of the RDOC files
|
255
|
+
# rake test # Run tests for test
|
256
|
+
#---
|
data/lib/sudokusolver.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Translated into ruby from python by Martin-Louis Bright
|
4
|
+
# Algorithm, overall structure and original python source code by Peter Norvig
|
5
|
+
# See http://norvig.com/sudoku.html
|
6
|
+
|
7
|
+
## Throughout this program:
|
8
|
+
## r is a row, e.g. 'A'
|
9
|
+
## c is a column, e.g. '3'
|
10
|
+
## s is a square, e.g. 'A3'
|
11
|
+
## d is a digit, e.g. '9'
|
12
|
+
## u is a unit, e.g. ['A1','B1','C1','D1','E1','F1','G1','H1','I1']
|
13
|
+
## g is a grid, e.g. 81 non-blank chars, e.g. starting with '.18...7...
|
14
|
+
## values is a hash of possible values, e.g. {'A1':'123489', 'A2':'8', ...}
|
15
|
+
|
16
|
+
class SudokuSolver
|
17
|
+
VERSION = "1.0"
|
18
|
+
attr_reader :rows, :cols, :squares, :unitlist, :peers, :units
|
19
|
+
|
20
|
+
def cross(a, b)
|
21
|
+
cp = Array.new # cross product
|
22
|
+
a.each do |x|
|
23
|
+
b.each do |y|
|
24
|
+
cp << x+y
|
25
|
+
end
|
26
|
+
end
|
27
|
+
return cp
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize()
|
31
|
+
@rows = ('A'..'I').to_a
|
32
|
+
@cols = ('1'..'9').to_a
|
33
|
+
@squares = cross(@rows, @cols)
|
34
|
+
@unitlist = Array.new
|
35
|
+
cols.each { |c| @unitlist.push(cross(rows, c)) }
|
36
|
+
rows.each { |r| @unitlist.push(cross(r, cols)) }
|
37
|
+
for rb in ['ABC','DEF','GHI'] do
|
38
|
+
for cb in ['123','456','789'] do
|
39
|
+
@unitlist << cross(rb.split(''),cb.split(''))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@units = Hash.new
|
44
|
+
squares.each do |s|
|
45
|
+
@units[s] = Array.new
|
46
|
+
unitlist.each do |u|
|
47
|
+
u.each do |x|
|
48
|
+
@units[s].push(u) if s == x
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
@peers = Hash.new
|
54
|
+
squares.each do |s|
|
55
|
+
@peers[s] = Array.new
|
56
|
+
units[s].each do |u|
|
57
|
+
u.each { |s2| @peers[s] << s2 if s2 != s }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_grid(g)
|
64
|
+
g = g.chomp
|
65
|
+
g = g.split('')
|
66
|
+
values = Hash.new
|
67
|
+
# Initially any square can be anything.
|
68
|
+
squares.each { |s| values[s] = "123456789" }
|
69
|
+
for s,d in squares.zip(g)
|
70
|
+
return false unless assign(values, s, d) if d =~ /\d/
|
71
|
+
end
|
72
|
+
return values
|
73
|
+
end
|
74
|
+
|
75
|
+
def assign(values, s, d)
|
76
|
+
values[s].split('').each do |d2|
|
77
|
+
unless d2 == d
|
78
|
+
return false if eliminate(values, s, d2) == false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
return values
|
82
|
+
end
|
83
|
+
|
84
|
+
def eliminate(values, s, d)
|
85
|
+
return values unless values[s].include?(d) ## Already eliminated.
|
86
|
+
|
87
|
+
values[s] = values[s].sub(d,'') ## Remove the digit from the string of possibilities
|
88
|
+
## values[s].sub!(d,'') => why doesn't sub!() work?
|
89
|
+
|
90
|
+
return false if values[s].length == 0 ## Contradiction: no more values (no more digits can be assigned)
|
91
|
+
|
92
|
+
## Remove digit from all peers
|
93
|
+
peers[s].each { |s2| return false unless eliminate(values, s2, values[s]) } if values[s].length == 1
|
94
|
+
|
95
|
+
## Assign digit if, by elimination, there is only one square left
|
96
|
+
## in the units for this square that can hold the digit
|
97
|
+
units[s].each do |u|
|
98
|
+
dplaces = Array.new
|
99
|
+
u.each { |s2| dplaces << s2 if values[s2].include?(d) }
|
100
|
+
return false if dplaces.length == 0 # bad
|
101
|
+
return false if assign(values, dplaces[0], d) == false if dplaces.length == 1
|
102
|
+
end
|
103
|
+
return values
|
104
|
+
end
|
105
|
+
|
106
|
+
def search(values)
|
107
|
+
return false if values == false
|
108
|
+
|
109
|
+
solved = true ## assumption
|
110
|
+
squares.each do |s|
|
111
|
+
unless values[s].length == 1
|
112
|
+
solved = false
|
113
|
+
break
|
114
|
+
end
|
115
|
+
end
|
116
|
+
return values if solved == true ## Solved!
|
117
|
+
|
118
|
+
min = 10
|
119
|
+
start = nil
|
120
|
+
squares.each do |s| ## Chose the undetermined square s with the fewest possibilities
|
121
|
+
l = values[s].length
|
122
|
+
if l > 1 && l < min
|
123
|
+
min = l
|
124
|
+
start = s
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
values[start].split('').each do |d|
|
129
|
+
solution = search(assign(values.clone,start,d))
|
130
|
+
return solution unless solution == false
|
131
|
+
end
|
132
|
+
return false
|
133
|
+
end
|
134
|
+
|
135
|
+
def print_grid(values)
|
136
|
+
return if values == false
|
137
|
+
max = 0
|
138
|
+
squares.each { |s| max = values[s].length if values[s].length > max }
|
139
|
+
width = 1 + max
|
140
|
+
a = Array.new
|
141
|
+
3.times do |c|
|
142
|
+
tmp = ""
|
143
|
+
(3*width).times do
|
144
|
+
tmp = tmp + '-'
|
145
|
+
end
|
146
|
+
tmp += "-" if c == 1
|
147
|
+
a.push(tmp)
|
148
|
+
end
|
149
|
+
line = "\n" + a.join('+')
|
150
|
+
|
151
|
+
tmp = ""
|
152
|
+
for r in rows
|
153
|
+
for c in cols
|
154
|
+
tmp = tmp + values[r+c].center(width)
|
155
|
+
if c == '3' or c == '6'
|
156
|
+
tmp = tmp + '| '
|
157
|
+
end
|
158
|
+
end
|
159
|
+
tmp = tmp + line if r == 'C' or r == 'F'
|
160
|
+
tmp = tmp + "\n"
|
161
|
+
end
|
162
|
+
puts tmp + "\n"
|
163
|
+
return values
|
164
|
+
end
|
165
|
+
|
166
|
+
def string_solution(values)
|
167
|
+
solution = ""
|
168
|
+
squares.each do |s|
|
169
|
+
solution += values[s]
|
170
|
+
end
|
171
|
+
return solution
|
172
|
+
end
|
173
|
+
|
174
|
+
def check_solution(solution)
|
175
|
+
values = Hash.new
|
176
|
+
for s,d in squares.zip(solution.split(''))
|
177
|
+
values[s] = d
|
178
|
+
end
|
179
|
+
|
180
|
+
unitlist.each do |u|
|
181
|
+
tmp = Hash.new
|
182
|
+
u.each do |s|
|
183
|
+
tmp[values[s]] = true
|
184
|
+
end
|
185
|
+
return false unless tmp.keys.length == 9
|
186
|
+
end
|
187
|
+
return true
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
## Algorithm by Peter Norvig @ http://www.norvig.com/sudoku.html
|
193
|
+
|
194
|
+
## More constraints:
|
195
|
+
## http://www.scanraid.com/BasicStrategies.htm
|
196
|
+
## http://www.krazydad.com/blog/2005/09/29/an-index-of-sudoku-strategies/
|
197
|
+
## http://www2.warwick.ac.uk/fac/sci/moac/currentstudents/peter_cock/python/sudoku/
|
data/test/driver.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Martin-Louis Bright on 2007-04-10.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require 'sudokusolver'
|
7
|
+
|
8
|
+
puzzle = "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......"
|
9
|
+
s = SudokuSolver.new
|
10
|
+
s.print_grid(s.search(s.parse_grid(puzzle)))
|
data/test/tc_sudoku.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Martin-Louis Bright on 2007-03-21.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'sudokusolver')
|
7
|
+
require 'test/unit'
|
8
|
+
|
9
|
+
class SudokuTest < Test::Unit::TestCase
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@path = File.join(File.dirname(__FILE__), '..')
|
13
|
+
@easy = File.read(@path + '/test/easy_puzzles.txt').split("\n")
|
14
|
+
@hard = File.read(@path + '/test/top95.txt').split("\n")
|
15
|
+
@s = SudokuSolver.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_empty_puzzle
|
19
|
+
e = "................................................................................."
|
20
|
+
sol = @s.string_solution(@s.search(@s.parse_grid(e)))
|
21
|
+
puts sol
|
22
|
+
assert(@s.check_solution(sol))
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_sanity
|
26
|
+
puts "Easy puzzle (constraint satisfaction only): "
|
27
|
+
puts
|
28
|
+
@s.print_grid(@s.search(@s.parse_grid(@easy[0])))
|
29
|
+
puts "Hard puzzle (constraint satisfaction + search): "
|
30
|
+
puts
|
31
|
+
@s.print_grid(@s.search(@s.parse_grid(@hard[0])))
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_easy
|
35
|
+
multiple(@easy)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_hard
|
39
|
+
multiple(@hard)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_bmark_hard
|
43
|
+
@hard.each do |g|
|
44
|
+
puts @s.string_solution(@s.search(@s.parse_grid(g)))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def multiple(puzzles)
|
49
|
+
puzzles.each do |g|
|
50
|
+
assert(@s.check_solution(@s.string_solution(@s.search(@s.parse_grid(g)))))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.2
|
3
|
+
specification_version: 1
|
4
|
+
name: sudokusolver
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "1.0"
|
7
|
+
date: 2007-04-15 00:00:00 -04:00
|
8
|
+
summary: Commandline program and library for solving Sudoku puzzles
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: mlbright@gmail.com
|
12
|
+
homepage: http://sudokusolver.rubyforge.org/
|
13
|
+
rubyforge_project: sudokusolver
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Martin-Louis Bright
|
31
|
+
files:
|
32
|
+
- lib/sudokusolver.rb
|
33
|
+
- test/driver.rb
|
34
|
+
- test/tc_sudoku.rb
|
35
|
+
- Changes.rdoc
|
36
|
+
- README.rdoc
|
37
|
+
- Rakefile
|
38
|
+
- COPYING
|
39
|
+
- LICENSE
|
40
|
+
test_files:
|
41
|
+
- test/tc_sudoku.rb
|
42
|
+
rdoc_options:
|
43
|
+
- --title
|
44
|
+
- SudokuSolver API documentation
|
45
|
+
- --main
|
46
|
+
- README.rdoc
|
47
|
+
extra_rdoc_files:
|
48
|
+
- README.rdoc
|
49
|
+
- Changes.rdoc
|
50
|
+
executables: []
|
51
|
+
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
dependencies: []
|
57
|
+
|