combinatorial_puzzle_solver 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/Gemfile +4 -0
- data/README.md +465 -0
- data/Rakefile +40 -0
- data/bin/setup +7 -0
- data/combinatorial_puzzle_solver.gemspec +28 -0
- data/example_puzzles/4x4 +1 -0
- data/example_puzzles/compile_examples.rb +116 -0
- data/example_puzzles/examples.yaml +243 -0
- data/example_puzzles/hard +11 -0
- data/example_puzzles/medium +12 -0
- data/example_puzzles/simple +9 -0
- data/exe/solve_sudoku +110 -0
- data/lib/combinatorial_puzzle_solver.rb +12 -0
- data/lib/combinatorial_puzzle_solver/constraint.rb +54 -0
- data/lib/combinatorial_puzzle_solver/identifier.rb +55 -0
- data/lib/combinatorial_puzzle_solver/inconsistency.rb +9 -0
- data/lib/combinatorial_puzzle_solver/possibilities.rb +89 -0
- data/lib/combinatorial_puzzle_solver/puzzle.rb +124 -0
- data/lib/combinatorial_puzzle_solver/solution_space.rb +159 -0
- data/lib/combinatorial_puzzle_solver/sudoku.rb +95 -0
- data/lib/combinatorial_puzzle_solver/version.rb +4 -0
- metadata +155 -0
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
desc "Execute RSpec with default formatter"
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
7
|
+
t.rspec_opts = "--format RSpec::Formatters::IllustratedDocumentationFormatter"
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Execute RSpec with HTML formatter"
|
11
|
+
# RSpec - HTML output
|
12
|
+
RSpec::Core::RakeTask.new(:html_spec) do |t|
|
13
|
+
t.rspec_opts = "--format RSpec::Formatters::IllustratedHtmlFormatter --out ./doc/rspec-results.html"
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Generate API documentation."
|
17
|
+
require 'yard'
|
18
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
19
|
+
t.files = ['lib/**/*.rb', '-', 'doc/rspec-results.html', 'doc/examples.md' ]
|
20
|
+
end
|
21
|
+
task :doc => [:html_spec, :examples]
|
22
|
+
|
23
|
+
desc "List the undocumented code."
|
24
|
+
YARD::Rake::YardocTask.new(:list_undoc) do |t|
|
25
|
+
t.stats_options = ['--list-undoc']
|
26
|
+
end
|
27
|
+
|
28
|
+
# Generate examples and documentation
|
29
|
+
require_relative 'example_puzzles/compile_examples'
|
30
|
+
task :examples => ['doc/examples.md']
|
31
|
+
file 'doc/examples.md' => FileList["example_puzzles/*"] do
|
32
|
+
markdown = compile_examples_to_markdown(Dir.glob('example_puzzles/*.yaml'))
|
33
|
+
File.write('doc/examples.md', markdown)
|
34
|
+
$stderr.puts "examples ok!"
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :test => [:spec, :examples]
|
39
|
+
task :default => :doc
|
40
|
+
|
data/bin/setup
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'combinatorial_puzzle_solver/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "combinatorial_puzzle_solver"
|
8
|
+
spec.version = CombinatorialPuzzleSolver::VERSION
|
9
|
+
spec.authors = ["Erik Schlyter"]
|
10
|
+
spec.email = ["erik@erisc.se"]
|
11
|
+
|
12
|
+
spec.summary = %q{A resolver of combinatorial number-placement puzzles, like Sudoku.}
|
13
|
+
spec.description = %q{A resolver of combinatorial number-placement puzzles, like Sudoku.}
|
14
|
+
spec.homepage = "https://github.com/ErikSchlyter/combinatorial_puzzle_solver"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec-illustrate", "~> 0.1.3"
|
24
|
+
|
25
|
+
spec.add_development_dependency "yard", "~> 0.8.7.6"
|
26
|
+
spec.add_development_dependency "redcarpet", "~> 3.2.2"
|
27
|
+
|
28
|
+
end
|
data/example_puzzles/4x4
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
this is a tiny puzzle 0040100000030100
|
@@ -0,0 +1,116 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'yaml'
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
# Reads documentation and test examples from YAML (examples.yaml) files and compiles
|
6
|
+
# it into a markdown doucument.
|
7
|
+
#
|
8
|
+
# @param yaml_files [Array<String>] the yaml files to parse
|
9
|
+
# @return [String] the compiled markdown document.
|
10
|
+
def compile_examples_to_markdown(yaml_files)
|
11
|
+
compile(yaml_files.collect{|yaml_file| YAML.load_file(yaml_file)})
|
12
|
+
end
|
13
|
+
|
14
|
+
# Compiles a given node of the YAML tree into markdown.
|
15
|
+
# - If the node is an Array, it will be compiled and concatenated.
|
16
|
+
# - If the node contains :section, it will be compiled recursively.
|
17
|
+
# - If the node contains :example, it is an example that will be executed.
|
18
|
+
# - If the node contains :comment, it is a paragraph.
|
19
|
+
#
|
20
|
+
# @param node [Object] the object that was parsed from the YAML document.
|
21
|
+
# @param level [Fixnum] the current subsection level.
|
22
|
+
# @return [String] the examples compiled into markdown.
|
23
|
+
def compile(node, level=1)
|
24
|
+
return node.collect{|e| compile(e,level)}.join("\n") if node.is_a?(Array)
|
25
|
+
|
26
|
+
if node.has_key?(:section) then
|
27
|
+
"#{("#"*level)}#{node[:section]}\n" <<
|
28
|
+
"#{node[:comment]}\n" <<
|
29
|
+
compile(node[:content], level+1)
|
30
|
+
|
31
|
+
elsif node.has_key?(:example)
|
32
|
+
example_to_markdown(verify(execute(node)), level)
|
33
|
+
|
34
|
+
elsif node.has_key?(:comment)
|
35
|
+
"#{node[:comment]}\n"
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param example [Hash] the example originally described in YAML.
|
41
|
+
# @param level [Fixnum] the subsection level
|
42
|
+
# @return [String] the example formatted in markdown
|
43
|
+
def example_to_markdown(example, level)
|
44
|
+
md = ""
|
45
|
+
|
46
|
+
if example[:example] != "" then
|
47
|
+
md << "#{("#"*(level))}#{example[:example]}\n\n"
|
48
|
+
end
|
49
|
+
|
50
|
+
if example[:comment] then
|
51
|
+
md << example[:comment].to_s << "\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
example[:input_files].each{|filename|
|
55
|
+
md << "Given the file `#{filename}`:\n"
|
56
|
+
md << block(IO.read(filename))
|
57
|
+
}
|
58
|
+
|
59
|
+
if example[:input] then
|
60
|
+
md << "Given the following input on stdin:\n"
|
61
|
+
md << block(example[:input])
|
62
|
+
end
|
63
|
+
|
64
|
+
md << "Invoking `#{example[:command]}` will return exit code " <<
|
65
|
+
"`#{example[:exit_code]}` and output:\n"
|
66
|
+
md << block(example[:stdout])
|
67
|
+
|
68
|
+
md
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [String] the given string formatted as a markdown code block.
|
72
|
+
def block(string)
|
73
|
+
"\n\t#{string.gsub(/\n/,"\n\t")}\n\n"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Executes an example and stores output and exit code in the hash.
|
77
|
+
#
|
78
|
+
# @param example [Hash] the example originally described in YAML.
|
79
|
+
# @return [Hash] the example
|
80
|
+
def execute(example)
|
81
|
+
argv = example[:input_files] + example[:argv]
|
82
|
+
example[:command] = "./solve_sudoku #{argv.join(' ')}"
|
83
|
+
full_command = "./exe/#{example[:command]}"
|
84
|
+
|
85
|
+
Open3.popen3(full_command) {|stdin, stdout, stderr, wait_thr|
|
86
|
+
stdin.write(example[:input]) if example[:input]
|
87
|
+
|
88
|
+
example[:exit_code] = wait_thr.value.to_i
|
89
|
+
example[:stdout] = stdout.read
|
90
|
+
example[:stderr] = stderr.read
|
91
|
+
}
|
92
|
+
example
|
93
|
+
end
|
94
|
+
|
95
|
+
# Compares exit code and expected output to assert example executes correctly.
|
96
|
+
#
|
97
|
+
# @param example [Hash] the example to verify
|
98
|
+
# @return [Hash] the verified example
|
99
|
+
def verify(example)
|
100
|
+
if example[:expected_output] then
|
101
|
+
if example[:expected_output].strip != example[:stdout].strip then
|
102
|
+
$stderr.puts "Expected:----\n#{example[:expected_output]}\n----"
|
103
|
+
$stderr.puts "Stdout:------\n#{example[:stdout]}\n----"
|
104
|
+
puts example.to_yaml
|
105
|
+
fail "expected output did not match."
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
if example[:expected_exit_code] then
|
110
|
+
if example[:expected_exit_code] != example[:exit_code] then
|
111
|
+
$stderr.puts example.to_yaml
|
112
|
+
fail "Wrong exit code"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
example
|
116
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
---
|
2
|
+
:section: Sudoku Solver
|
3
|
+
:content:
|
4
|
+
- :comment: >
|
5
|
+
The program `solve_sudoku` reads sudoku puzzles from files
|
6
|
+
(or stdin, if no filenames is given) and solves them by
|
7
|
+
constraint resolution. If constraint resolution is not enough
|
8
|
+
to solve the puzzle, it will resort to a trial and error
|
9
|
+
approach. The exit code will indicate if all parsed puzzles
|
10
|
+
was completely solved or not.
|
11
|
+
|
12
|
+
Each resolution step and the current state of the puzzle can
|
13
|
+
be written to stdout for diagnostic purposes. It is also
|
14
|
+
possible to abort after a given number of steps if a complete
|
15
|
+
resolution is not desired, which would be the case if you only
|
16
|
+
want a couple of clues.
|
17
|
+
|
18
|
+
Note that the step output and abort functionality is not
|
19
|
+
available when the puzzle is solved by trial and error.
|
20
|
+
|
21
|
+
The default behavior is resolve all given puzzles (with trial
|
22
|
+
and error, if neccessary) and indicate by exit status whether
|
23
|
+
all puzzles where completely resolved. No output is given
|
24
|
+
unless explicitly asked for.
|
25
|
+
|
26
|
+
- :example: Printing the parsed puzzles, `-i`.
|
27
|
+
:comment: >
|
28
|
+
The parser will interpret digits (0-9) as values in the puzzle
|
29
|
+
and disregard anything else. The option `-i` will output the
|
30
|
+
parsed puzzle before it solves it.
|
31
|
+
:input_files:
|
32
|
+
- example_puzzles/simple
|
33
|
+
:argv:
|
34
|
+
- -i
|
35
|
+
:expected_exit_code: 0
|
36
|
+
:expected_output: |
|
37
|
+
9 6|8 1 3|5 4
|
38
|
+
2 1| 4 5| 6 3
|
39
|
+
4 | |
|
40
|
+
-----+-----+-----
|
41
|
+
|6 2 | 9
|
42
|
+
9| |2
|
43
|
+
7 | 3 4|
|
44
|
+
-----+-----+-----
|
45
|
+
| | 9
|
46
|
+
5 9 |3 6 |1 4
|
47
|
+
2 7|4 5 9|3 6
|
48
|
+
- :example: "Parsing several puzzles at the same time"
|
49
|
+
:input_files:
|
50
|
+
- example_puzzles/simple
|
51
|
+
- example_puzzles/medium
|
52
|
+
:argv:
|
53
|
+
- -i
|
54
|
+
:expected_exit_code: 0
|
55
|
+
:expected_output: |
|
56
|
+
9 6|8 1 3|5 4
|
57
|
+
2 1| 4 5| 6 3
|
58
|
+
4 | |
|
59
|
+
-----+-----+-----
|
60
|
+
|6 2 | 9
|
61
|
+
9| |2
|
62
|
+
7 | 3 4|
|
63
|
+
-----+-----+-----
|
64
|
+
| | 9
|
65
|
+
5 9 |3 6 |1 4
|
66
|
+
2 7|4 5 9|3 6
|
67
|
+
|
68
|
+
|3 1|
|
69
|
+
9| |
|
70
|
+
8 | | 3
|
71
|
+
-----+-----+-----
|
72
|
+
| 4|9 8
|
73
|
+
7|1 2 |5
|
74
|
+
2 | 9 |1 7
|
75
|
+
-----+-----+-----
|
76
|
+
5 | 1 2|
|
77
|
+
9 | 7|
|
78
|
+
3 |4 5| 8
|
79
|
+
- :example: Parsing 4x4 puzzles, `-4`, `-4x4`.
|
80
|
+
:input_files:
|
81
|
+
- example_puzzles/4x4
|
82
|
+
:argv:
|
83
|
+
- -i
|
84
|
+
- -4
|
85
|
+
:expected_exit_code: 0
|
86
|
+
:expected_output: " |4 \n1 | \n---+---\n | 3\n 1| \n\n"
|
87
|
+
|
88
|
+
- :example: Printing the output, `-o`.
|
89
|
+
:input_files:
|
90
|
+
- example_puzzles/simple
|
91
|
+
:argv:
|
92
|
+
- -o
|
93
|
+
:expected_exit_code: 0
|
94
|
+
:expected_output: |
|
95
|
+
9 7 6|8 1 3|5 4 2
|
96
|
+
2 8 1|7 4 5|9 6 3
|
97
|
+
3 4 5|2 9 6|8 1 7
|
98
|
+
-----+-----+-----
|
99
|
+
8 5 3|6 2 1|4 7 9
|
100
|
+
4 6 9|5 7 8|2 3 1
|
101
|
+
7 1 2|9 3 4|6 5 8
|
102
|
+
-----+-----+-----
|
103
|
+
6 3 4|1 8 2|7 9 5
|
104
|
+
5 9 8|3 6 7|1 2 4
|
105
|
+
1 2 7|4 5 9|3 8 6
|
106
|
+
- :example: Printing the resolution steps, `-s`.
|
107
|
+
:input_files:
|
108
|
+
- example_puzzles/simple
|
109
|
+
:argv:
|
110
|
+
- -o -s
|
111
|
+
:expected_exit_code: 0
|
112
|
+
:expected_output: |
|
113
|
+
[1,2]=7
|
114
|
+
[8,3]=8
|
115
|
+
[9,8]=8
|
116
|
+
[1,9]=2
|
117
|
+
[2,2]=8
|
118
|
+
[9,1]=1
|
119
|
+
[7,7]=7
|
120
|
+
[3,1]=3
|
121
|
+
[7,5]=8
|
122
|
+
[7,9]=5
|
123
|
+
[2,7]=9
|
124
|
+
[8,8]=2
|
125
|
+
[3,3]=5
|
126
|
+
[5,5]=7
|
127
|
+
[2,4]=7
|
128
|
+
[3,7]=8
|
129
|
+
[8,6]=7
|
130
|
+
[6,3]=2
|
131
|
+
[3,5]=9
|
132
|
+
[4,7]=4
|
133
|
+
[6,7]=6
|
134
|
+
[3,4]=2
|
135
|
+
[4,1]=8
|
136
|
+
[4,3]=3
|
137
|
+
[3,6]=6
|
138
|
+
[7,4]=1
|
139
|
+
[4,6]=1
|
140
|
+
[7,3]=4
|
141
|
+
[7,6]=2
|
142
|
+
[5,4]=5
|
143
|
+
[4,2]=5
|
144
|
+
[5,6]=8
|
145
|
+
[7,1]=6
|
146
|
+
[6,4]=9
|
147
|
+
[4,8]=7
|
148
|
+
[6,2]=1
|
149
|
+
[5,9]=1
|
150
|
+
[7,2]=3
|
151
|
+
[5,1]=4
|
152
|
+
[3,8]=1
|
153
|
+
[6,8]=5
|
154
|
+
[6,9]=8
|
155
|
+
[5,2]=6
|
156
|
+
[5,8]=3
|
157
|
+
[3,9]=7
|
158
|
+
9 7 6|8 1 3|5 4 2
|
159
|
+
2 8 1|7 4 5|9 6 3
|
160
|
+
3 4 5|2 9 6|8 1 7
|
161
|
+
-----+-----+-----
|
162
|
+
8 5 3|6 2 1|4 7 9
|
163
|
+
4 6 9|5 7 8|2 3 1
|
164
|
+
7 1 2|9 3 4|6 5 8
|
165
|
+
-----+-----+-----
|
166
|
+
6 3 4|1 8 2|7 9 5
|
167
|
+
5 9 8|3 6 7|1 2 4
|
168
|
+
1 2 7|4 5 9|3 8 6
|
169
|
+
|
170
|
+
- :example: Printing the entire puzzle for each resolution step, `-p`.
|
171
|
+
:input_files:
|
172
|
+
- example_puzzles/4x4
|
173
|
+
:argv:
|
174
|
+
- --4x4 -p
|
175
|
+
:expected_exit_code: 0
|
176
|
+
#
|
177
|
+
# this output looks really bad since YAML doesn't support ASCII art that
|
178
|
+
# starts with spaces, which is the case of certain puzzles.
|
179
|
+
#
|
180
|
+
:expected_output: ! " |4 \n1 | 2\n---+---\n | 3\n 1| \n\n |4 \n1 | 2\n---+---\n
|
181
|
+
\ | 3\n 1|2 \n\n |4 \n1 |3 2\n---+---\n | 3\n 1|2 \n\n |4 1\n1 |3
|
182
|
+
2\n---+---\n | 3\n 1|2 \n\n |4 1\n1 |3 2\n---+---\n | 3\n 1|2 4\n\n
|
183
|
+
\ |4 1\n1 |3 2\n---+---\n |1 3\n 1|2 4\n\n |4 1\n1 4|3 2\n---+---\n |1
|
184
|
+
3\n 1|2 4\n\n |4 1\n1 4|3 2\n---+---\n |1 3\n3 1|2 4\n\n |4 1\n1 4|3 2\n---+---\n
|
185
|
+
\ 2|1 3\n3 1|2 4\n\n2 |4 1\n1 4|3 2\n---+---\n 2|1 3\n3 1|2 4\n\n2 |4 1\n1 4|3
|
186
|
+
2\n---+---\n4 2|1 3\n3 1|2 4\n\n2 3|4 1\n1 4|3 2\n---+---\n4 2|1 3\n3 1|2 4\n\n"
|
187
|
+
|
188
|
+
- :example: Use constraint resolution only, `-r`.
|
189
|
+
:comment: >
|
190
|
+
You can avoid the trial and error functionality. Note though that this might not
|
191
|
+
lead to a completely solved puzzle, which would imply a failure return code.
|
192
|
+
|
193
|
+
The input and incomplete result is demonstrated below.
|
194
|
+
|
195
|
+
:input_files:
|
196
|
+
- example_puzzles/medium
|
197
|
+
:argv:
|
198
|
+
- -i -o -r
|
199
|
+
:expected_exit_code: 256
|
200
|
+
#
|
201
|
+
# this puzzle looks really bad since YAML doesn't support ASCII art that
|
202
|
+
# starts with spaces, which is the case of certain puzzles.
|
203
|
+
#
|
204
|
+
:expected_output: ! " |3 1| \n 9| | \n 8 | | 3 \n-----+-----+-----\n
|
205
|
+
\ | 4|9 8 \n 7|1 2 |5 \n2 | 9 |1 7 \n-----+-----+-----\n 5
|
206
|
+
\ | 1 2| \n 9 | 7| \n3 |4 5| 8\n\n 2 |3 8 1| 5 9\n 3 9|7
|
207
|
+
5 6|8 2 \n 8 5|2 4 9| 3 \n-----+-----+-----\n5 1 3|6 7 4|9 8 2\n9 7|1 2 8|5
|
208
|
+
\ 3\n2 8|5 9 3|1 7 \n-----+-----+-----\n8 5 |9 1 2|3 7\n 9 2|8 3 7| 1 5\n3
|
209
|
+
7 1|4 6 5|2 9 8\n\n"
|
210
|
+
|
211
|
+
- :example: ""
|
212
|
+
:input_files:
|
213
|
+
- example_puzzles/hard
|
214
|
+
:argv:
|
215
|
+
- -i -o -r
|
216
|
+
:expected_exit_code: 256
|
217
|
+
#
|
218
|
+
# this puzzle looks really bad since YAML doesn't support ASCII art that
|
219
|
+
# starts with spaces, which is the case of certain puzzles.
|
220
|
+
#
|
221
|
+
:expected_output: ! " |2 | 6 3\n3 | 5|4 1\n 1| 3|9 8 \n-----+-----+-----\n
|
222
|
+
\ | | 9 \n |5 3 8| \n 3 | | \n-----+-----+-----\n 2
|
223
|
+
6|3 |5 \n5 3|7 | 8\n4 7 | 1| \n\n |2 1 |7 6 3\n3 7|
|
224
|
+
\ 5|4 2 1\n2 1| 7 3|9 8 5\n-----+-----+-----\n | |3 9 \n |5 3
|
225
|
+
8| \n 3 | |8 5 \n-----+-----+-----\n 2 6|3 |5 \n5 3|7 | 8\n4
|
226
|
+
7 | 5 1| 3 \n\n"
|
227
|
+
|
228
|
+
|
229
|
+
- :example: Abort after a given number of steps, `-c NUM`, `--clues NUM`.
|
230
|
+
:comment: >
|
231
|
+
If you don't want the entire puzzle solved, but just a couple of clues on
|
232
|
+
how to get forward, you can abort the resolution with `-c`, and print each
|
233
|
+
step with `-s`.
|
234
|
+
:input_files:
|
235
|
+
- example_puzzles/simple
|
236
|
+
:argv:
|
237
|
+
- "-s"
|
238
|
+
- "-c 3"
|
239
|
+
:expected_exit_code: 0
|
240
|
+
:expected_output: |
|
241
|
+
[1,2]=7
|
242
|
+
[8,3]=8
|
243
|
+
[9,8]=8
|