doku 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +32 -0
- data/README.rdoc +15 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/doku.rb +4 -0
- data/lib/doku/dancing_links.rb +561 -0
- data/lib/doku/grid.rb +241 -0
- data/lib/doku/hexadoku.rb +56 -0
- data/lib/doku/hexamurai.rb +95 -0
- data/lib/doku/puzzle.rb +250 -0
- data/lib/doku/solver.rb +103 -0
- data/lib/doku/sudoku.rb +42 -0
- data/spec/dancing_links_spec.rb +212 -0
- data/spec/hexadoku_spec.rb +71 -0
- data/spec/hexamurai_spec.rb +38 -0
- data/spec/puzzle_spec.rb +147 -0
- data/spec/solution_spec.rb +278 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/sudoku_spec.rb +52 -0
- data/spec/watch.rb +9 -0
- metadata +198 -0
data/lib/doku/solver.rb
ADDED
@@ -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
|
data/lib/doku/sudoku.rb
ADDED
@@ -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
|