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,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
+