doku 1.0.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.
@@ -0,0 +1,103 @@
1
+ require 'backports' unless defined?(require_relative) and defined?(Enumerator)
2
+ require_relative 'dancing_links'
3
+
4
+ module Doku
5
+ # This module is included into the {Puzzle} class to provide methods
6
+ # for solving the puzzles using the {DancingLinks} algorithm.
7
+ module SolvableWithDancingLinks
8
+ # @return (Puzzle)
9
+ # Returns the first solution found by the Dancing Links algorithm,
10
+ # or nil if there is no solution.
11
+ def solve
12
+ each_solution { |s| return s }
13
+ return nil
14
+ end
15
+
16
+ # An enumerator for all the solutions to the puzzle.
17
+ # @return (Enumerable)
18
+ def solutions
19
+ Enumerator.new do |y|
20
+ each_solution do |solution|
21
+ y << solution
22
+ end
23
+ end
24
+ end
25
+
26
+ # This method lets you iterate over each solution.
27
+ # Each solution is a puzzle object of the same class
28
+ # such that solution.solution_for?(puzzle) is true.
29
+ #
30
+ # @yield [solution]
31
+ def each_solution
32
+ to_link_matrix.each_exact_cover do |exact_cover|
33
+ yield exact_cover_to_solution exact_cover
34
+ end
35
+ end
36
+
37
+ # Returns a {DancingLinks::LinkMatrix} that represents this puzzle.
38
+ # Every row is a {SquareAndGlyph} object representing a choice to
39
+ # assign a certain glyph to a certain square.
40
+ # Every column is a {GroupAndGlyph} object representing the
41
+ # requirements that needs to be satisfied (every group must have
42
+ # exactly one of each glyph assigned to a square in the group).
43
+ # @return (DancingLinks::LinkMatrix)
44
+ def to_link_matrix
45
+ # Create the link matrix. This is a generic matrix
46
+ # that does not take in to account square.given_glyph.
47
+ sm = DancingLinks::LinkMatrix.from_sets sets_for_exact_cover_problem
48
+
49
+ # Take into account square.given_glyph by covering certain
50
+ # rows (removing the row and all columns it touches).
51
+ each do |square, glyph|
52
+ sm.remove_row SquareAndGlyph.new(square,glyph)
53
+ end
54
+
55
+ sm
56
+ end
57
+
58
+ # Converts an exact cover (an array of {SquareAndGlyph} objects) to a
59
+ # solution of the puzzle.
60
+ # @return (Puzzle)
61
+ def exact_cover_to_solution(exact_cover)
62
+ solution = dup
63
+ exact_cover.each do |sg|
64
+ solution[sg.square] = sg.glyph
65
+ end
66
+
67
+ solution
68
+ end
69
+
70
+ private
71
+
72
+ def sets_for_exact_cover_problem
73
+ sets = {}
74
+ squares.each do |square|
75
+ groups_with_square = groups.select { |g| g.include? square }
76
+
77
+ glyphs.each do |glyph|
78
+ sets[SquareAndGlyph.new(square, glyph)] = [square] +
79
+ groups_with_square.collect do |group|
80
+ GroupAndGlyph.new group, glyph
81
+ end
82
+ end
83
+ end
84
+
85
+ sets
86
+ end
87
+
88
+ # This is a simple class that just represents the choice of a puzzle's
89
+ # square and a puzzle's glyph. These are identified with rows in
90
+ # the {DancingLinks::LinkMatrix} when solving puzzles, and there they
91
+ # represent the choice to assign a particula glyph to a particular square.
92
+ class SquareAndGlyph < Struct.new(:square, :glyph)
93
+ end
94
+
95
+ # This is a simple class that just represents the choice of a puzzle's
96
+ # group of squares and a glyph. These are identifies with columns in
97
+ # the {DancingLinks::LinkMatrix} when solving puzzles, and they
98
+ # represent a requirement that must be satisfied; every group must
99
+ # has one of every glyph assigned to a square in it.
100
+ class GroupAndGlyph < Struct.new(:group, :glyph)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,42 @@
1
+ require 'backports' unless defined?(require_relative)
2
+ require_relative 'puzzle'
3
+ require_relative 'grid'
4
+
5
+ module Doku
6
+ # This class represents {http://en.wikipedia.org/wiki/Sudoku Sudoku}.
7
+ # Each instance of this class represents a particular arrangement of
8
+ # numbers written in the boxes.
9
+ class Sudoku < Puzzle
10
+ include PuzzleOnGrid
11
+ extend PuzzleOnGrid::ClassMethods # improves generated docs
12
+
13
+ has_glyphs (1..9).to_a
14
+ has_glyph_chars glyphs.collect &:to_s
15
+
16
+ has_template <<END
17
+ ...|...|...
18
+ ...|...|...
19
+ ...|...|...
20
+ ---+---+---
21
+ ...|...|...
22
+ ...|...|...
23
+ ...|...|...
24
+ ---+---+---
25
+ ...|...|...
26
+ ...|...|...
27
+ ...|...|...
28
+ END
29
+
30
+ 0.upto(8) do |n|
31
+ define_group row(n)
32
+ define_group column(n)
33
+ end
34
+
35
+ 0.step(6,3).each do |x|
36
+ 0.step(6,3).each do |y|
37
+ define_group square_group(x, y)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,212 @@
1
+ require 'backports' unless defined? require_relative
2
+ require_relative 'spec_helper'
3
+
4
+ describe Doku::DancingLinks::LinkMatrix do
5
+ context "when created from scratch" do
6
+ before do
7
+ @m = Doku::DancingLinks::LinkMatrix.new
8
+ end
9
+
10
+ it "has no columns (i.e. it is empty)" do
11
+ @m.columns.to_a.size.should == 0
12
+ @m.should be_empty
13
+ end
14
+ end
15
+
16
+ describe ".from_sets" do
17
+ it "can create a matrix from a set of sets" do
18
+ @m = Doku::DancingLinks::LinkMatrix.from_sets(
19
+ Set.new([ Set.new([1, 2]), Set.new([2, 3]) ]) )
20
+ end
21
+
22
+ it "filters out duplicate column ids" do
23
+ @m = Doku::DancingLinks::LinkMatrix.from_sets [ [1,2,2] ]
24
+ @m.row([1,2,2]).nodes.to_a.size.should == 2
25
+ end
26
+
27
+ end
28
+
29
+ describe "find_exact_cover" do
30
+ it "can find one exact cover" do
31
+ m = Doku::DancingLinks::LinkMatrix.from_sets [[1,2], [2,3], [3,4]]
32
+ m.find_exact_cover.sort.should == [[1,2], [3,4]]
33
+ end
34
+
35
+ it "returns nil if there are no exact covers" do
36
+ m = Doku::DancingLinks::LinkMatrix.from_sets [[1,2], [2,3]]
37
+ m.find_exact_cover.should == nil
38
+ end
39
+
40
+ it "it finds the trivial exact cover for the trivial matrix" do
41
+ Doku::DancingLinks::LinkMatrix.new.find_exact_cover.should == []
42
+ end
43
+ end
44
+
45
+ describe "exact_covers" do
46
+ it "returns an Enumerable" do
47
+ Doku::DancingLinks::LinkMatrix.new.exact_covers.should be_a Enumerable
48
+ end
49
+
50
+ it "can find all exact covers" do
51
+ m = Doku::DancingLinks::LinkMatrix.from_sets [[1,2], [2,3], [3,4], [4,1]]
52
+ m.exact_covers.collect{|ec| ec.sort}.sort.should ==
53
+ [ [ [1,2], [3,4] ],
54
+ [ [2,3], [4,1] ] ]
55
+ end
56
+
57
+ it "it finds the trivial exact cover for the trivial matrix" do
58
+ Doku::DancingLinks::LinkMatrix.new.exact_covers.to_a.should == [[]]
59
+ end
60
+ end
61
+
62
+ describe "each_exact_cover" do
63
+ it "does not yield if there are no exact covers" do
64
+ m = Doku::DancingLinks::LinkMatrix.from_sets [[1,2], [2,3]]
65
+ m.each_exact_cover { |ec| true.should == false }
66
+ end
67
+
68
+ it "finds the trivial exact cover for the trivial matrix" do
69
+ already_yielded = false
70
+ Doku::DancingLinks::LinkMatrix.new.each_exact_cover do |ec|
71
+ ec.should == []
72
+ already_yielded.should == false
73
+ already_yielded = true
74
+ end
75
+ end
76
+ end
77
+
78
+ describe "find_exact_cover_recursive" do
79
+ it "find the trivial cover for the trivial matrix" do
80
+ Doku::DancingLinks::LinkMatrix.new.find_exact_cover_recursive.should == []
81
+ end
82
+
83
+ it "works even if final(k) < max(k)" do
84
+ # This makes sure we call collect on o[0...k] instead of on o.
85
+ m = Doku::DancingLinks::LinkMatrix.from_sets [
86
+ [1,2, ],
87
+ [ 2,3, ],
88
+ [ 3,4, ],
89
+ [ 4,5],
90
+ [1,2,3,4,5] ]
91
+ m.find_exact_cover_recursive.sort.should == [[1, 2, 3, 4, 5]]
92
+ end
93
+ end
94
+
95
+ shared_examples_for "figure 3 from Knuth" do
96
+ it "has 7 columns" do
97
+ @m.columns.to_a.size.should == 7
98
+ end
99
+
100
+ it "has the expected columns" do
101
+ @m.columns.collect(&:id).should == @universe
102
+ end
103
+
104
+ it "has the expected structure" do
105
+ # This test is not exhaustive.
106
+ columns = @m.columns.to_a
107
+ columns[0].down.should_not == columns[0]
108
+ columns[0].up.should_not == columns[0]
109
+ columns[0].nodes.to_a.size.should == 2
110
+ columns[0].up.up.should == columns[0].down
111
+ columns[0].up.should == columns[0].down.down
112
+
113
+ columns[0].down.right.up.should == columns[3]
114
+ columns[3].down.left.up.should == columns[0]
115
+ columns[0].down.down.right.up.up.should == columns[3]
116
+ columns[0].down.down.right.right.right.down.down.should == columns[3]
117
+ columns[2].up.right.down.should == columns[5]
118
+
119
+ columns[6].down.down.down.left.up.left.down.left.down.down.should == columns[1]
120
+ end
121
+
122
+ it "every row has a reference to the column" do
123
+ @m.columns.each do |column|
124
+ column.nodes.each do |node|
125
+ node.column.should == column
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ context "given figure 3 from Knuth" do
132
+ before do
133
+ @universe = [1,2,3,4,5,6,7]
134
+ @subsets = [[ 3, 5,6 ],
135
+ [1, 4, 7],
136
+ [ 2,3, 6 ],
137
+ [1, 4 ],
138
+ [ 2, 7],
139
+ Set.new([ 4,5, 7]),
140
+ ]
141
+ @m = Doku::DancingLinks::LinkMatrix.from_sets @subsets, @universe
142
+ end
143
+
144
+ it_should_behave_like "figure 3 from Knuth"
145
+
146
+ it "can find an exact cover" do
147
+ result = @m.find_exact_cover
148
+ result.collect(&:sort).sort.should == [[1, 4], [2, 7], [3, 5, 6]]
149
+ end
150
+
151
+ # TODO: test this using a matrix that has multiple exact covers
152
+ it "can find all exact covers" do
153
+ @m.exact_covers.to_a.sort.should == [[[1, 4], [3, 5, 6], [2,7]]]
154
+ end
155
+
156
+ context "after running each_exact_cover" do
157
+ before do
158
+ # If we let each_exact_cover run all the way through, it restores
159
+ # the matrix to its original state.
160
+ @m.each_exact_cover { }
161
+ end
162
+
163
+ it_should_behave_like "figure 3 from Knuth"
164
+ end
165
+
166
+ context "with one row covered" do
167
+ before do
168
+ @m.column(@universe[3]).cover
169
+ end
170
+
171
+ it "has only 6 columns" do
172
+ @m.columns.to_a.size.should == 6
173
+ end
174
+
175
+ # @m will now look like (minus means a covered element)
176
+ # 0 0 1 - 1 1 0
177
+ # - - - - - - -
178
+ # 0 1 1 - 0 1 0
179
+ # - - - - - - -
180
+ # 0 1 0 - 0 0 1
181
+ # - - - - - - -
182
+ it "has the expected column sizes" do
183
+ @universe.collect { |e| @m.column(e).size }.should == [0, 2, 2, 3, 1, 2, 1]
184
+ @m.columns.collect { |c| c.size }.should == [0, 2, 2, 1, 2, 1]
185
+ end
186
+
187
+ it "has the expected structure" do
188
+ columns = @m.columns.to_a
189
+
190
+ # Column 0 is empty.
191
+ columns[0].down.should == columns[0]
192
+ columns[0].up.should == columns[0]
193
+ columns[0].nodes.to_a.should be_empty
194
+
195
+ columns[1].down.right.up.up.should == columns[2]
196
+ columns[2].down.right.up.should == columns[3]
197
+ columns[3].up.right.down.down.should == columns[4]
198
+ columns[5].down.right.down.should == columns[1]
199
+
200
+ columns[5].up.left.up.right.up.right.right.down.down.should == columns[4]
201
+ end
202
+
203
+ context "and then uncovered" do
204
+ before do
205
+ @m.column(@universe[3]).uncover
206
+ end
207
+
208
+ it_should_behave_like "figure 3 from Knuth"
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,71 @@
1
+ require 'backports' unless defined? require_relative
2
+ require_relative 'spec_helper'
3
+
4
+ describe Doku::Hexadoku do
5
+ it 'has the upper-case glyph characters' do
6
+ Doku::Hexadoku.glyph_chars.should == %w{0 1 2 3 4 5 6 7 8 9 A B C D E F}
7
+ end
8
+
9
+ it 'rejects invalid glyphs in glyph_char' do
10
+ lambda { Doku::Hexadoku.glyph_char(19) }.should raise_error ArgumentError, "Invalid glyph 19."
11
+ end
12
+
13
+ it 'rejects invalid characters in glyph_parse' do
14
+ lambda { Doku::Hexadoku.glyph_parse('H') }.should raise_error ArgumentError, "Invalid character 'H'."
15
+ end
16
+
17
+ it 'accepts both cases in glyph_parse' do
18
+ Doku::Hexadoku.glyph_parse('d').should == 0xD
19
+ Doku::Hexadoku.glyph_parse('D').should == 0xD
20
+ end
21
+
22
+ it 'rejects invalid characters in the constructor' do
23
+ grid_string = <<END
24
+ ABCD|....|....|....
25
+ ....|abcd|....|....
26
+ X...|....|....|....
27
+ ....|....|....|....
28
+ ----+----+----+----
29
+ ....|....|....|....
30
+ ....|....|....|....
31
+ ....|....|....|....
32
+ ....|....|....|....
33
+ ----+----+----+----
34
+ ....|....|....|....
35
+ ....|....|....|....
36
+ ....|....|....|....
37
+ ....|....|....|....
38
+ ----+----+----+----
39
+ ....|....|....|....
40
+ ....|....|....|....
41
+ ....|....|....|....
42
+ ....|....|....|....
43
+ END
44
+ lambda { Doku::Hexadoku.new(grid_string) }.should raise_error ArgumentError, "Line 2, character 0: Invalid character 'X'. Expected period (.) or glyph (0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F)."
45
+ end
46
+
47
+ it 'accepts lower-case or upper-case in the consturctor' do
48
+ grid_string = <<END.strip
49
+ ABCD|....|....|....
50
+ ....|abcd|....|....
51
+ ....|....|....|....
52
+ ....|....|....|....
53
+ ----+----+----+----
54
+ ....|....|....|....
55
+ ....|....|....|....
56
+ ....|....|....|....
57
+ ....|....|....|....
58
+ ----+----+----+----
59
+ ....|....|....|....
60
+ ....|....|....|....
61
+ ....|....|....|....
62
+ ....|....|....|....
63
+ ----+----+----+----
64
+ ....|....|....|....
65
+ ....|....|....|....
66
+ ....|....|....|....
67
+ ....|....|....|....
68
+ END
69
+ Doku::Hexadoku.new(grid_string).to_s.should == grid_string.upcase
70
+ end
71
+ end
@@ -0,0 +1,38 @@
1
+ require 'backports' unless defined? require_relative
2
+ require_relative 'spec_helper'
3
+
4
+ describe Doku::Hexamurai do
5
+ it 'has 768 squares' do
6
+ Doku::Hexamurai.squares.size.should == 768
7
+ end
8
+
9
+ it 'has 16 squares in the first row' do
10
+ first_row = Doku::Hexamurai.squares_matching :y => 0
11
+ first_row.size.should == 16
12
+ end
13
+
14
+ it 'has 16 squares in the first column of the top hexadoku' do
15
+ column = Doku::Hexamurai.squares_matching :x => 8, :y => (0..15)
16
+ column.size.should == 16
17
+ end
18
+
19
+ it 'has the right number of groups' do
20
+ # A hexadoku has 3*16 groups (16 columns, 16 rows, 16 boxes)
21
+ # There are 5 hexadokus.
22
+ # The reckoning above counted the 16 rows and 16 columns of the
23
+ # center hexadoku twice (-32), and counted the 16 boxes of the
24
+ # center hexaodoku thrice (-32), so subtract 64.
25
+ # There are 2*16 inferred groups (16 columns, 16 rows).
26
+ Doku::Hexamurai.groups.size.should == (5*3*16 - 64 + 2*16)
27
+ end
28
+
29
+ it 'has valid line and char numbers' do
30
+ lines = Doku::Hexamurai.template.split("\n")
31
+ Doku::Hexamurai.squares.each do |square|
32
+ line_number, char_number = Doku::Hexamurai.coordinates_in_grid_string(square)
33
+ line = lines[line_number]
34
+ line.should_not be_nil
35
+ line.size.should > char_number
36
+ end
37
+ end
38
+ end