combinatorial_puzzle_solver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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