combinatorial_puzzle_solver 0.1.0
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/.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
|