doku 1.0.0

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