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/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,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ 000|200|063
2
+ 300|005|401
3
+ 001|003|980
4
+ ---+---+---
5
+ 000|000|090
6
+ 000|538|000
7
+ 030|000|000
8
+ ---+---+---
9
+ 026|300|500
10
+ 503|700|008
11
+ 470|001|000