doku 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,147 @@
1
+ require 'backports' unless defined? require_relative
2
+ require_relative 'spec_helper'
3
+
4
+ class TestPuzzle < Doku::Puzzle
5
+ has_squares [1,2,3,4]
6
+ has_glyphs [true,false]
7
+
8
+ define_group [1,2]
9
+ define_group [2,3]
10
+ define_group [3,4]
11
+ define_group [4,1]
12
+ end
13
+
14
+ describe "Puzzle instance" do
15
+ before do
16
+ @puzzle = TestPuzzle.new(1 => true)
17
+ end
18
+
19
+ it "is basically a hash of square => glyph" do
20
+ @puzzle[1].should == true
21
+ @puzzle[4].should == nil
22
+
23
+ @puzzle[1] = false
24
+ @puzzle[1].should == false
25
+
26
+ @puzzle[1] = nil
27
+ @puzzle[1].should == nil
28
+ end
29
+
30
+ it "has an each method that excludes nil values" do
31
+ TestPuzzle.new(2 => nil).each { raise }
32
+ end
33
+
34
+ it "only allows squares as keys" do
35
+ lambda { @puzzle[0] }.should raise_error IndexError
36
+ lambda { @puzzle[0] = false }.should raise_error IndexError
37
+ end
38
+
39
+ it "only allows glyphs or nil as values" do
40
+ lambda { @puzzle[1] = 10 }.should raise_error ArgumentError
41
+ end
42
+
43
+ it "can not be initialized with bad data" do
44
+ lambda { TestPuzzle.new(:foo => true) }.should raise_error IndexError
45
+ lambda { TestPuzzle.new(1 => :foo) }.should raise_error ArgumentError
46
+ end
47
+
48
+ describe "comparison operators say" do
49
+ it "A == B iff the square=>glyph pairs in A and B are the same" do
50
+ @puzzle.should_not == TestPuzzle.new
51
+ TestPuzzle.new.should_not == @puzzle.should
52
+
53
+ puzzle2 = TestPuzzle.new(1 => true)
54
+ @puzzle.should == puzzle2
55
+ puzzle2.should == @puzzle
56
+
57
+ (TestPuzzle.new(1 => true) != @puzzle).should == false
58
+ end
59
+
60
+ it "A eql? B is the same as A == B" do
61
+ @puzzle.should_not eql TestPuzzle.new
62
+ TestPuzzle.new.should_not eql @puzzle.should
63
+
64
+ puzzle2 = TestPuzzle.new(1 => true)
65
+ @puzzle.should eql puzzle2
66
+ puzzle2.should eql @puzzle
67
+ end
68
+
69
+ it "A.subset?(B) iff square=>glyph pairs in A are a subset of those in B" do
70
+ @puzzle.should_not be_a_subset TestPuzzle.new
71
+ TestPuzzle.new.should be_a_subset @puzzle
72
+
73
+ @puzzle.should be_a_subset @puzzle
74
+
75
+ @puzzle.should be_a_subset TestPuzzle.new(1 => true, 3 => false)
76
+ end
77
+ end
78
+
79
+ describe "#filled?" do
80
+ it "returns true iff all squares have been assigned a glyph" do
81
+ TestPuzzle.new(1 => true, 2=> true, 3 => false, 4 => false).should be_filled
82
+ @puzzle.should_not be_filled
83
+ end
84
+ end
85
+
86
+ describe "#valid?" do
87
+ it "returns false iff a group has the same glyph in it twice" do
88
+ @puzzle.should be_valid
89
+ TestPuzzle.new(1 => true, 2=> true).should_not be_valid
90
+ end
91
+ end
92
+
93
+ describe "#solution?" do
94
+ it "returns true iff it is filled and valid" do
95
+ puzzle2 = TestPuzzle.new(1 => false, 2 => true, 3 => false, 4 => true)
96
+ puzzle2.should be_a_solution
97
+
98
+ puzzle3 = TestPuzzle.new(1 => true, 2=> true, 3 => false, 4 => false)
99
+ puzzle3.should_not be_a_solution
100
+ end
101
+ end
102
+
103
+ describe "#solution_for?(puzzle)" do
104
+ it "returns true if this puzzle is a solution and a super set of the other puzzle" do
105
+ puzzle4 = TestPuzzle.new(1 => false, 2 => true, 3 => false, 4 => true)
106
+ puzzle4.should_not be_a_solution_for @puzzle
107
+
108
+ puzzle5 = TestPuzzle.new(1 => true, 2 => false, 3 => true, 4 => false)
109
+ puzzle5.should be_a_solution_for @puzzle
110
+ end
111
+ end
112
+
113
+ shared_examples_for "copied puzzle" do
114
+ it "produces an equal puzzle" do
115
+ @copy.should == @puzzle
116
+ @copy.should_not equal @puzzle
117
+ end
118
+
119
+ it "produces a puzzle with a different glyph_state hash" do
120
+ @copy.glyph_state.should_not equal @puzzle.glyph_state
121
+ end
122
+ end
123
+
124
+ describe "dup" do
125
+ before do
126
+ @copy = @puzzle.dup
127
+ end
128
+
129
+ it_should_behave_like "copied puzzle"
130
+ end
131
+
132
+ describe "clone" do
133
+ before do
134
+ @copy = @puzzle.clone
135
+ end
136
+
137
+ it_should_behave_like "copied puzzle"
138
+ end
139
+
140
+ it "equality and hash codes are not affected by nil values" do
141
+ puzzle2 = TestPuzzle.new(1 => true, 3 => nil)
142
+ @puzzle[2] = nil
143
+ puzzle2.should == @puzzle
144
+ puzzle2.hash.should == @puzzle.hash
145
+ end
146
+
147
+ end
@@ -0,0 +1,278 @@
1
+ require 'backports' unless defined? require_relative
2
+ require_relative 'spec_helper'
3
+
4
+ describe "Puzzle#solve" do
5
+ context 'given the sudoku puzzle' do
6
+ before do
7
+ @puzzle = Doku::Sudoku.new <<END
8
+ ...|..8|...
9
+ ..7|.35|..9
10
+ 5..|4.6|8..
11
+ ---+---+---
12
+ ...|..4|2..
13
+ 4..|...|.37
14
+ 8..|...|5..
15
+ ---+---+---
16
+ .9.|.67|...
17
+ ..3|...|1.5
18
+ ...|...|..3
19
+ END
20
+
21
+ @solution = @puzzle.solve
22
+ end
23
+
24
+ it 'solves the puzzle' do
25
+ @solution.solution_for?(@puzzle).should == true
26
+ end
27
+
28
+ it 'is the correct solution' do
29
+ @solution.to_grid_string.strip.should == <<END.strip
30
+ 964|278|351
31
+ 287|135|649
32
+ 531|496|872
33
+ ---+---+---
34
+ 319|754|286
35
+ 452|681|937
36
+ 876|923|514
37
+ ---+---+---
38
+ 195|367|428
39
+ 723|849|165
40
+ 648|512|793
41
+ END
42
+ end
43
+
44
+ it "can also find the solution using Donald Knuth's recursive DLX" do
45
+ sm = @puzzle.to_link_matrix
46
+ exact_cover = sm.find_exact_cover_recursive
47
+ solution = @puzzle.exact_cover_to_solution exact_cover
48
+ solution.should == @solution
49
+ end
50
+ end
51
+
52
+ context 'given a sudoku puzzle with two solutions' do
53
+ before do
54
+ @puzzle = Doku::Sudoku.new <<END
55
+ ...|..8|...
56
+ ..7|.3.|..9
57
+ 5..|4.6|8..
58
+ ---+---+---
59
+ ...|..4|2..
60
+ 4..|...|.37
61
+ 8..|...|5..
62
+ ---+---+---
63
+ .9.|.67|...
64
+ ..3|...|1.5
65
+ ...|...|..3
66
+ END
67
+ end
68
+
69
+ it 'finds two solutions' do
70
+ solutions = @puzzle.solutions.to_a
71
+ solutions.size.should == 2
72
+
73
+ solutions.should include Doku::Sudoku.new <<END
74
+ 964|278|351
75
+ 287|135|649
76
+ 531|496|872
77
+ ---+---+---
78
+ 319|754|286
79
+ 452|681|937
80
+ 876|923|514
81
+ ---+---+---
82
+ 195|367|428
83
+ 723|849|165
84
+ 648|512|793
85
+ END
86
+
87
+ solutions.should include Doku::Sudoku.new <<END
88
+ 964|278|351
89
+ 287|531|649
90
+ 531|496|872
91
+ ---+---+---
92
+ 359|714|286
93
+ 412|685|937
94
+ 876|923|514
95
+ ---+---+---
96
+ 195|367|428
97
+ 723|849|165
98
+ 648|152|793
99
+ END
100
+
101
+ solutions[0].should be_a_solution_for @puzzle
102
+ solutions[1].should be_a_solution_for @puzzle
103
+ end
104
+
105
+ end
106
+
107
+ context 'given a sudoku puzzle with NO solutions' do
108
+ before do
109
+ @puzzle = Doku::Sudoku.new <<END
110
+ 123|...|...
111
+ 456|...|...
112
+ 78.|...|...
113
+ ---+---+---
114
+ ..1|...|...
115
+ ...|9..|...
116
+ ..2|...|...
117
+ ---+---+---
118
+ ..4|...|...
119
+ ...|9..|...
120
+ ..5|...|...
121
+ END
122
+ end
123
+
124
+ it 'finds no solutions' do
125
+ @puzzle.solve.should == nil
126
+ end
127
+
128
+ it 'can not tell instantly there is no solution, in this case' do
129
+ sm = @puzzle.to_link_matrix
130
+ sc = sm.columns.min_by(&:size)
131
+ sc.size.should > 0
132
+ end
133
+
134
+ it 'finds no solutions using the recursive algorithm' do
135
+ sm = @puzzle.to_link_matrix
136
+ sm.find_exact_cover_recursive.should == nil
137
+ end
138
+ end
139
+
140
+ context 'given a hexadoku puzzle' do
141
+ before do
142
+ # Elektor Hexadoku 2011
143
+ @puzzle = Doku::Hexadoku.new <<END
144
+ 2A.7|.C..|9D64|8...
145
+ ..3.|A..D|7...|.2F.
146
+ .1..|..0.|8...|.4AB
147
+ ....|.7.2|..BC|.0.3
148
+ ----+----+----+----
149
+ C2.8|.D3.|..4E|....
150
+ ..FA|7.2.|B.3.|1C04
151
+ ....|4..F|..1.|...E
152
+ 9..B|1...|....|23..
153
+ ----+----+----+----
154
+ ..8C|....|...0|3..D
155
+ 6...|.F..|1..A|....
156
+ D37E|.0.1|.9.8|AF..
157
+ ....|3B..|.2D.|C.80
158
+ ----+----+----+----
159
+ F.B.|51..|2.A.|....
160
+ 3CA.|...7|.E..|..6.
161
+ .E4.|...9|3..5|.D..
162
+ ...1|F3A4|..9.|5.E2
163
+ END
164
+
165
+ @solution = @puzzle.solve
166
+ end
167
+
168
+ it 'finds the correct solution' do
169
+ @solution.to_grid_string.should == <<END.strip
170
+ 2A07|BCF3|9D64|8E15
171
+ B839|A54D|70E1|62FC
172
+ 51CD|E906|8F23|74AB
173
+ 46EF|8712|A5BC|D093
174
+ ----+----+----+----
175
+ C218|6D3B|074E|9A5F
176
+ EDFA|7825|B639|1C04
177
+ 0753|4A9F|C812|B6DE
178
+ 946B|1EC0|DA5F|2378
179
+ ----+----+----+----
180
+ 158C|946A|EBF0|372D
181
+ 6B20|CFD8|137A|E549
182
+ D37E|2051|49C8|AFB6
183
+ AF94|3B7E|52D6|C180
184
+ ----+----+----+----
185
+ F9B6|51EC|24AD|0837
186
+ 3CA5|D287|FE0B|4961
187
+ 7E42|06B9|3185|FDCA
188
+ 80D1|F3A4|6C97|5BE2
189
+ END
190
+ end
191
+ end
192
+
193
+ context "given a Hexamurai puzzle" do
194
+ before do
195
+ # From Elektor 2011-07 (with extra hints so it can be solved quickly).
196
+ @puzzle = Doku::Hexamurai.new <<END
197
+ |39DEA62B|80C1745F|
198
+ |..213...|5.B4....|
199
+ |..0.....|......2.|
200
+ |.....4.5|..9A1.E.|
201
+ |..3C....|.1654.0.|
202
+ |.1..2...|3......6|
203
+ |...90...|BD4.25..|
204
+ |2.57....|ACE03.D1|
205
+ --------+--------+--------+--------
206
+ 52.0..3.|A.....B.|0.....3.|78A....6
207
+ 4.8.1...|..9.....|...E..4.|2.063...
208
+ .7......|.4F.50.9|.7.2..8E|.1..4...
209
+ ..6...0.|.....C3.|...6.7.0|5..3..21
210
+ A1D24E6B|9083C.5F|E62BAD14|.....8..
211
+ E0347.28|CFA5BD96|14830E72|.9....5A
212
+ 8B96FC50|ED.2134A|950CB6F8|.2.1.4.3
213
+ 7.FC3A9D|1B64E2.0|FA7..9C3|0..E.2..
214
+ --------+--------+--------+--------
215
+ .3.1..7.|B.5F2.E4|31......|B.6....4
216
+ C.......|4..7..0D|..6...B.|9.40.178
217
+ ..28....|.C.A..73|...49..1|..3.5.0.
218
+ 047.5...|.6.1....|...045..|1....3..
219
+ ..036.4.|.1.C.EA.|60473..9|........
220
+ 1......2|8.4D.96.|A3EF..2.|4.5.0...
221
+ ..4..51.|.7.E3...|B291..5.|....C637
222
+ 6.5...8.|...9...1|8DC5..0.|........
223
+ --------+--------+--------+--------
224
+ |...B..39|..0..2..|
225
+ |5.804..2|.6.3.E7.|
226
+ |...6....|....5.3.|
227
+ |...2.7..|C4.....0|
228
+ |..685340|.729A1..|
229
+ |7..51...|.....6.3|
230
+ |...4....|.B..0...|
231
+ |...3....|0......2|
232
+ END
233
+ end
234
+
235
+ it "can be solved correctly" do
236
+ solution = @puzzle.solve
237
+ solution.should be_solution_for @puzzle
238
+ solution.to_s.should == <<END.chomp
239
+ |39DEA62B|80C1745F|
240
+ |87213FDC|5EB4906A|
241
+ |450A79E1|63DF8B2C|
242
+ |6CBF8405|729A13ED|
243
+ |BA3CFE7D|21654809|
244
+ |014D25A8|39F7ECB6|
245
+ |F6E901C3|BD4825A7|
246
+ |28579B64|ACE03FD1|
247
+ --------+--------+--------+--------
248
+ 52109D3F|AEC648B7|0F19D235|78A4BEC6
249
+ 4E8D1BC5|73906AF2|D85EC14B|2F0637A9
250
+ 37CA82E6|D4FB5019|C7326A8E|D19B40F5
251
+ B96FA704|5218DC3E|4BA6F790|5CE38D21
252
+ A1D24E6B|9083C75F|E62BAD14|C3F57890
253
+ E0347128|CFA5BD96|14830E72|69BDFC5A
254
+ 8B96FC50|ED72134A|950CB6F8|A271E4D3
255
+ 75FC3A9D|1B64E280|FA7D59C3|048E126B
256
+ --------+--------+--------+--------
257
+ D3A1087C|B95F26E4|31D870AC|B5629FE4
258
+ CFB526A3|48E7910D|5C6A23BF|9E40D178
259
+ 9628E4B1|0CDAF573|2EB49861|FD375A0C
260
+ 047E5FD9|3621ABC8|79F045ED|1AC863B2
261
+ FD036947|21BC8EA5|60473FD9|8B2CA51E
262
+ 1AE7C3F2|854D096B|A3EF1C27|46590B8D
263
+ 2849B51A|670E3FDC|B291845A|E0DFC637
264
+ 6C5BD08E|FA397421|8DC5EB06|371A294F
265
+ --------+--------+--------+--------
266
+ |AF7BC839|E50D6214|
267
+ |5D804AF2|9613CE7B|
268
+ |94C6B01E|FA725D38|
269
+ |E312D756|C48BFA90|
270
+ |CB685340|D729A1FE|
271
+ |709512BF|48AED6C3|
272
+ |D2F4EC9A|1B360785|
273
+ |1EA36D87|0F5CB942|
274
+ END
275
+ end
276
+ end
277
+ end
278
+
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift File.join File.dirname(__FILE__), '..', 'lib'
2
+ require 'rspec'
3
+ require 'doku'
4
+
5
+ # Change this to true if a test is failing and you want more clues about why.
6
+ EXTRA_ASSERTS = false
7
+
8
+ if EXTRA_ASSERTS
9
+ # Add any patches here that help check the validity of the algorithm but are
10
+ # not necessary to have in the production code.
11
+
12
+ class Doku::DancingLinks::LinkMatrix::Column
13
+ def size=(num)
14
+ raise "bad class" unless Fixnum === num
15
+ raise "negative value #{num}" if num < 0
16
+ @size = num
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,52 @@
1
+ require 'backports' unless defined? require_relative
2
+ require_relative 'spec_helper'
3
+
4
+ describe Doku::Sudoku do
5
+ before do
6
+ @puzzle = Doku::Sudoku.new
7
+ end
8
+
9
+ it 'has 81 squares' do
10
+ Doku::Sudoku.squares.size.should == 81
11
+ end
12
+
13
+ it 'has 9 squares in the first row' do
14
+ first_row = Doku::Sudoku.squares.select { |s| s.matches?(:y => 0) }
15
+ first_row.size.should == 9
16
+ end
17
+
18
+ it 'has the right number of groups' do
19
+ Doku::Sudoku.groups.size.should == 3*9
20
+ end
21
+
22
+ it 'has methods for getting and setting glyphs by coordinates' do
23
+ # (just like any puzzle class that includes the PuzzleOnGrid module)
24
+
25
+ @puzzle.get(0, 1).should == nil
26
+ @puzzle.set(0, 1, 4)
27
+ @puzzle.set(5, 7, 9)
28
+ @puzzle.set(6, 6, 1)
29
+ @puzzle.set(6, 6, nil)
30
+ @puzzle.get(0, 1).should == 4
31
+ @puzzle.to_s.should == <<END.strip
32
+ ...|...|...
33
+ 4..|...|...
34
+ ...|...|...
35
+ ---+---+---
36
+ ...|...|...
37
+ ...|...|...
38
+ ...|...|...
39
+ ---+---+---
40
+ ...|...|...
41
+ ...|..9|...
42
+ ...|...|...
43
+ END
44
+ end
45
+
46
+ it "the get method has a good error message" do
47
+ msg = "Square not found in Doku::Sudoku: Square(19, david)."
48
+ lambda { @puzzle.get(19, 'david') }.should raise_error IndexError, msg
49
+ lambda { @puzzle.set(19, 'david', 10) }.should raise_error IndexError, msg
50
+ end
51
+ end
52
+