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.
- 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/grid.rb
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
module Doku
|
2
|
+
# This module is meant to be included in subclasses of {Puzzle} where the
|
3
|
+
# squares are arranged in a grid.
|
4
|
+
#
|
5
|
+
# The {Puzzle} class contains only very abstract code, dealing with
|
6
|
+
# the abstract concepts of {Puzzle.squares squares},
|
7
|
+
# {Puzzle.glyphs glyphs}, and {Puzzle.groups groups}.
|
8
|
+
# The {Puzzle} class can represent a wide variety of puzzles, including
|
9
|
+
# three-dimensional puzzles or puzzles that don't have any particular
|
10
|
+
# spatial arrangement.
|
11
|
+
# However, most of the puzzles we are interested in studying are arranged
|
12
|
+
# in a grid, and this module contains code that makes it easy to define
|
13
|
+
# and work with those puzzles.
|
14
|
+
#
|
15
|
+
# Every {Puzzle.squares square} in a {PuzzleOnGrid} puzzle is a
|
16
|
+
# {SquareOnGrid} with x and y coordinates to represent its position on the grid.
|
17
|
+
# The x coordinate is 0 for the first row, 1 for the second row, etc.
|
18
|
+
# The y coordinate is 0 for the first column, 1 for the second column, etc.
|
19
|
+
#
|
20
|
+
# See the {ClassMethods} module for the class methods that are added to each
|
21
|
+
# class that includes PuzzleOnGrid.
|
22
|
+
module PuzzleOnGrid
|
23
|
+
# These are the separators that can appear in the {ClassMethods#template template}
|
24
|
+
# string or the string argument to the {#initialize} to make it more readable.
|
25
|
+
Separators = ['-', '+', '|']
|
26
|
+
|
27
|
+
private
|
28
|
+
def self.included(klass)
|
29
|
+
klass.extend ClassMethods
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.parse_grid_string(string)
|
33
|
+
y = 0
|
34
|
+
string.lines.each_with_index do |line, line_number|
|
35
|
+
line.chomp!
|
36
|
+
next if (line.chars.to_a - Separators).empty?
|
37
|
+
|
38
|
+
x = 0
|
39
|
+
line.chars.each_with_index do |char, char_number|
|
40
|
+
next if Separators.include?(char)
|
41
|
+
|
42
|
+
yield char, char_number, line_number, x, y
|
43
|
+
|
44
|
+
x += 1
|
45
|
+
end
|
46
|
+
y += 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
public
|
50
|
+
|
51
|
+
module ClassMethods
|
52
|
+
# @return (Array) Array with the line number and character number
|
53
|
+
# of the given square in the {#template} string.
|
54
|
+
def coordinates_in_grid_string(square)
|
55
|
+
[@line_number[square.y], @char_number[square.x]]
|
56
|
+
end
|
57
|
+
|
58
|
+
# This is a multi-line string that defines all the squares in the puzzle and the
|
59
|
+
# {Separators} to use to make the puzzle more readable in {#to_grid_string}.
|
60
|
+
# A period character ('.') represents a square.
|
61
|
+
# This is defined in the class by using the {#has_template} method.
|
62
|
+
# @return (String) The template string from the class definition.
|
63
|
+
attr_reader :template
|
64
|
+
|
65
|
+
# This is an array of characters (strings of length 1) which are used in
|
66
|
+
# multi-line grid strings to represent {Puzzle.glyphs glyphs} in a square.
|
67
|
+
# The order of this array must correspond to the order of the {Puzzle.glyphs glyphs}.
|
68
|
+
# For example, for {Sudoku}, this is
|
69
|
+
# ['1', '2', '3', '4', '5', '6', '7', '8', '9'].
|
70
|
+
# This is defined in the class definition using the {#has_glyph_chars} method.
|
71
|
+
# @return (Array) Array of characters.
|
72
|
+
attr_reader :glyph_chars
|
73
|
+
|
74
|
+
# Selects all the squares in this puzzle which match
|
75
|
+
# the specified condition.
|
76
|
+
# @param conditions (Hash) See {SquareOnGrid#matches?}.
|
77
|
+
# @return (Array) Array of squares.
|
78
|
+
def squares_matching(conditions)
|
79
|
+
squares.select { |sq| sq.matches? conditions }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Selects all the squares in this puzzle that are within a certain
|
83
|
+
# square area on the grid.
|
84
|
+
# @param leftmost_x (Integer) The x coordinate of the left-most column in the square area.
|
85
|
+
# @param top_y (Integer) The y coordinate of the top-most row in the square area.
|
86
|
+
# @param size (Integer) The width and height of the square.
|
87
|
+
# @return (Array) Array of squares.
|
88
|
+
def square_group(leftmost_x, top_y, size=Math.sqrt(glyphs.size))
|
89
|
+
squares_matching :x => leftmost_x...(leftmost_x+size), :y => top_y...(top_y+size)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Selects some squares with a specific y coordinate.
|
93
|
+
# @param leftmost_x (Integer) The x coordinate of the left-most square.
|
94
|
+
# @param size (Integer) The width of the row.
|
95
|
+
# @return (Array) Array of squares.
|
96
|
+
def row(y, leftmost_x=0, size=glyphs.size)
|
97
|
+
squares_matching :x => leftmost_x...(leftmost_x+size), :y => y
|
98
|
+
end
|
99
|
+
|
100
|
+
# Selects some squares with a specific x coordinate.
|
101
|
+
# @param top_y (Integer) The y coordinate of the top-most square.
|
102
|
+
# @param size (Integer) The height of the column.
|
103
|
+
# @return (Array) Array of squares.
|
104
|
+
def column(x, top_y=0, size=glyphs.size)
|
105
|
+
squares_matching :x => x, :y => top_y...(top_y+size)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return (String) The character that represents the given {Puzzle.glyphs glyph}.
|
109
|
+
def glyph_char(glyph)
|
110
|
+
glyph_chars.at(glyphs.index(glyph) || (raise ArgumentError, "Invalid glyph #{glyph}."))
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return (Object) The {Puzzle.glyphs glyph} represented by the given character.
|
114
|
+
def glyph_parse(char)
|
115
|
+
glyphs.at(glyph_chars.index(char.upcase) || (raise ArgumentError, "Invalid character '#{char}'."))
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# This is called in the class definition to define the {#template} string,
|
121
|
+
# thus defining which squares are in the puzzle.
|
122
|
+
def has_template(string)
|
123
|
+
@template = string.freeze
|
124
|
+
define_squares_from_template
|
125
|
+
end
|
126
|
+
|
127
|
+
# This is called in the class definition to define the {#glyph_chars}.
|
128
|
+
# This function converts the characters to upper case to make it
|
129
|
+
# easier to parse strings provided by the user in a case insensitive way.
|
130
|
+
def has_glyph_chars(chars)
|
131
|
+
@glyph_chars = chars.collect &:upcase
|
132
|
+
end
|
133
|
+
|
134
|
+
def define_square_on_grid(x, y, line_num, char_num)
|
135
|
+
define_square SquareOnGrid.new x, y
|
136
|
+
|
137
|
+
@line_number ||= []
|
138
|
+
@char_number ||= []
|
139
|
+
@line_number[y] = line_num
|
140
|
+
@char_number[x] = char_num
|
141
|
+
end
|
142
|
+
|
143
|
+
# Using the {#template} provided for the puzzle, this method
|
144
|
+
# defines objects to represent each of the different squares.
|
145
|
+
def define_squares_from_template
|
146
|
+
PuzzleOnGrid.parse_grid_string(template) do |char, char_number, line_number, x, y|
|
147
|
+
if char == '.'
|
148
|
+
define_square_on_grid x, y, line_number, char_number
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Creates a new instance of the puzzle.
|
155
|
+
# @param grid_string (String) A multi-line string defining which glyphs are
|
156
|
+
# currently written in which squares. You can use {Separators}
|
157
|
+
# to make this string more readable.
|
158
|
+
# This parameter is provided to make it easy to manually type in puzzles.
|
159
|
+
# If you are not typing the puzzle in, you should probably use {#set} instead.
|
160
|
+
# A good way to type this string is to copy the class's
|
161
|
+
# {ClassMethods#template template} and replace some of the periods with
|
162
|
+
# {ClassMethods#glyph_chars glyph_chars} (e.g. replace a '.' with '3').
|
163
|
+
def initialize(grid_string=nil)
|
164
|
+
super()
|
165
|
+
parse_initial_grid_string grid_string if grid_string
|
166
|
+
end
|
167
|
+
|
168
|
+
# @return (String) A multi-line string representation of the puzzle suitable
|
169
|
+
# for displaying, based on the {ClassMethods#template template}.
|
170
|
+
def to_grid_string
|
171
|
+
lines = self.class.template.split("\n")
|
172
|
+
each do |square, glyph|
|
173
|
+
line_number, char_number = self.class.coordinates_in_grid_string square
|
174
|
+
lines[line_number][char_number] = self.class.glyph_char glyph
|
175
|
+
end
|
176
|
+
lines.join "\n"
|
177
|
+
end
|
178
|
+
|
179
|
+
# Assigns a glyph to a square.
|
180
|
+
# This will modify the state of the puzzle, overwriting the previous
|
181
|
+
# glyph assignment.
|
182
|
+
# @param x (Integer) The x coordinate of the square.
|
183
|
+
# @param y (Integer) The y coordinate of the square.
|
184
|
+
# @param glyph The {Puzzle.glyphs glyph} to assign to that square, or nil.
|
185
|
+
def set(x, y, glyph)
|
186
|
+
self[SquareOnGrid.new(x, y)] = glyph
|
187
|
+
end
|
188
|
+
|
189
|
+
# Gets the glyph assignment for a given square.
|
190
|
+
# @param x (Integer) The x coordinate of the square.
|
191
|
+
# @param x (Integer) The y coordinate of the square.
|
192
|
+
# @return (Object) The {Puzzle.glyphs glyph} assigned to that square, or nil if
|
193
|
+
# none is assigned.
|
194
|
+
def get(x, y)
|
195
|
+
self[SquareOnGrid.new(x, y)]
|
196
|
+
end
|
197
|
+
|
198
|
+
# @return (String) The same as {#to_grid_string}.
|
199
|
+
def to_s
|
200
|
+
to_grid_string
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
def parse_initial_grid_string(grid_string)
|
205
|
+
PuzzleOnGrid.parse_grid_string(grid_string) do |original_char, char_number, line_number, x, y|
|
206
|
+
square = SquareOnGrid.new(x, y)
|
207
|
+
|
208
|
+
char = original_char.upcase
|
209
|
+
|
210
|
+
if !squares.include?(square)
|
211
|
+
raise "Line #{line_number}, character #{char_number}: Invalid character. Expected space." if char != ' '
|
212
|
+
elsif char == '.'
|
213
|
+
# No glyph specified for this square.
|
214
|
+
elsif self.class.glyph_chars.include?(char)
|
215
|
+
self[square] = self.class.glyph_parse(char)
|
216
|
+
else
|
217
|
+
raise ArgumentError, "Line #{line_number}, character #{char_number}: Invalid character '#{original_char}'. Expected period (.) or glyph (#{self.class.glyph_chars.join ','})."
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Represents a square on a grid.
|
224
|
+
# Any two instances with the same x and y coordinates are considered
|
225
|
+
# to be equal, which makes it convenient to use SquareOnGrid instances
|
226
|
+
# as a key in a hash table.
|
227
|
+
# This class is used by the {PuzzleOnGrid} module to represent the
|
228
|
+
# {Puzzle.squares squares} in grid-based {Puzzle}s.
|
229
|
+
class SquareOnGrid < Struct.new(:x, :y)
|
230
|
+
# @param conditions (Hash) Should be a hash where the keys are
|
231
|
+
# :x or :y and the values are either Integers or Integer ranges.
|
232
|
+
# @return (Boolean) True if the square matches all the conditions.
|
233
|
+
def matches?(conditions)
|
234
|
+
conditions.all? { |property, values| values === send(property) }
|
235
|
+
end
|
236
|
+
|
237
|
+
def to_s
|
238
|
+
"Square(#{x}, #{y})"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'backports' unless defined?(require_relative)
|
2
|
+
require_relative 'puzzle'
|
3
|
+
require_relative 'grid'
|
4
|
+
|
5
|
+
module Doku
|
6
|
+
# This class represents Hexadoku, otherwise known as 16x16 {Sudoku}.
|
7
|
+
# This is a more complex version of Sudoku with a 16x16 grid, using
|
8
|
+
# hex digits 0 through F.
|
9
|
+
# Each instance of this class represents a particular arrangement of
|
10
|
+
# numbers written in the boxes.
|
11
|
+
class Hexadoku < Puzzle
|
12
|
+
include PuzzleOnGrid
|
13
|
+
extend PuzzleOnGrid::ClassMethods # improves generated docs
|
14
|
+
|
15
|
+
has_glyphs (0..15).to_a
|
16
|
+
has_glyph_chars glyphs.collect { |s| '%x'%[s] }
|
17
|
+
|
18
|
+
has_template <<END
|
19
|
+
....|....|....|....
|
20
|
+
....|....|....|....
|
21
|
+
....|....|....|....
|
22
|
+
....|....|....|....
|
23
|
+
----+----+----+----
|
24
|
+
....|....|....|....
|
25
|
+
....|....|....|....
|
26
|
+
....|....|....|....
|
27
|
+
....|....|....|....
|
28
|
+
----+----+----+----
|
29
|
+
....|....|....|....
|
30
|
+
....|....|....|....
|
31
|
+
....|....|....|....
|
32
|
+
....|....|....|....
|
33
|
+
----+----+----+----
|
34
|
+
....|....|....|....
|
35
|
+
....|....|....|....
|
36
|
+
....|....|....|....
|
37
|
+
....|....|....|....
|
38
|
+
END
|
39
|
+
|
40
|
+
# Define row and column groups.
|
41
|
+
0.upto(15).each do |n|
|
42
|
+
define_group row(n)
|
43
|
+
define_group column(n)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Define the 4x4 groups.
|
47
|
+
0.step(12,4).each do |x|
|
48
|
+
0.step(12,4).each do |y|
|
49
|
+
define_group square_group(x, y)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'backports' unless defined?(require_relative)
|
2
|
+
require_relative 'puzzle'
|
3
|
+
require_relative 'grid'
|
4
|
+
|
5
|
+
module Doku
|
6
|
+
# This class represents the Hexamurai, a puzzle that consists of
|
7
|
+
# five {Hexadoku} puzzles superimposed on eachother.
|
8
|
+
# The {http://www.elektor.com/magazines/2011/july-047-august/hexamurai.1852786.lynkx Hexamurai puzzle appeared in the July/August issue of Elektor magazine}.
|
9
|
+
# Each instance of this class represents a particular arrangement of
|
10
|
+
# numbers written in the boxes.
|
11
|
+
class Hexamurai < Puzzle
|
12
|
+
include PuzzleOnGrid
|
13
|
+
extend Doku::PuzzleOnGrid::ClassMethods # improves generated docs
|
14
|
+
|
15
|
+
has_glyphs (0..15).to_a
|
16
|
+
has_glyph_chars glyphs.collect { |s| "%X"%[s] }
|
17
|
+
|
18
|
+
has_template <<END
|
19
|
+
|........|........|
|
20
|
+
|........|........|
|
21
|
+
|........|........|
|
22
|
+
|........|........|
|
23
|
+
|........|........|
|
24
|
+
|........|........|
|
25
|
+
|........|........|
|
26
|
+
|........|........|
|
27
|
+
--------+--------+--------+--------
|
28
|
+
........|........|........|........
|
29
|
+
........|........|........|........
|
30
|
+
........|........|........|........
|
31
|
+
........|........|........|........
|
32
|
+
........|........|........|........
|
33
|
+
........|........|........|........
|
34
|
+
........|........|........|........
|
35
|
+
........|........|........|........
|
36
|
+
--------+--------+--------+--------
|
37
|
+
........|........|........|........
|
38
|
+
........|........|........|........
|
39
|
+
........|........|........|........
|
40
|
+
........|........|........|........
|
41
|
+
........|........|........|........
|
42
|
+
........|........|........|........
|
43
|
+
........|........|........|........
|
44
|
+
........|........|........|........
|
45
|
+
--------+--------+--------+--------
|
46
|
+
|........|........|
|
47
|
+
|........|........|
|
48
|
+
|........|........|
|
49
|
+
|........|........|
|
50
|
+
|........|........|
|
51
|
+
|........|........|
|
52
|
+
|........|........|
|
53
|
+
|........|........|
|
54
|
+
END
|
55
|
+
|
56
|
+
# A track of 32 rows that runs down the center, and
|
57
|
+
# a track of 32 columns that runs through the center.
|
58
|
+
0.upto(31) do |n|
|
59
|
+
define_group row(n, 8)
|
60
|
+
define_group column(n, 8)
|
61
|
+
end
|
62
|
+
|
63
|
+
# The columns and rows that weren't included in the
|
64
|
+
# defintitions above.
|
65
|
+
0.upto(15) do |n|
|
66
|
+
define_group row(n+8, 0) # for the left Hexadoku
|
67
|
+
define_group row(n+8, 16) # for the right Hexadoku
|
68
|
+
define_group column(n+8, 0) # for the top Hexadoku
|
69
|
+
define_group column(n+8, 16) # for the bottom Hexadoku
|
70
|
+
end
|
71
|
+
|
72
|
+
# A track of 32 4x4 groups that runs from top-to-bottom
|
73
|
+
# and a track of 32 4x4 groups that runs from left-to-right.
|
74
|
+
# These two tracks intersect, but that's ok because
|
75
|
+
# define_group checks for uniqueness of groups.
|
76
|
+
0.step(28, 4) do |n|
|
77
|
+
8.step(20, 4) do |m|
|
78
|
+
define_group square_group(n, m)
|
79
|
+
define_group square_group(m, n)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Deduced groups!
|
84
|
+
# These groups are not explicitly stated in the puzzle, but
|
85
|
+
# we can use logic to deduce that they are groups (no
|
86
|
+
# glyph can appear twice in them).
|
87
|
+
# TODO: see if these deduced groups actually speed up algorithm!
|
88
|
+
8.upto(23) do |n|
|
89
|
+
define_group row(n, 0, 8) + row(n, 24, 8)
|
90
|
+
define_group column(n, 0, 8) + column(n, 24, 8)
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
data/lib/doku/puzzle.rb
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'backports' unless defined?(require_relative)
|
3
|
+
require_relative 'solver'
|
4
|
+
|
5
|
+
# The Doku module contains everything defined by the doku gem, including
|
6
|
+
# classes for puzzles, and a general-purpose implementation of the
|
7
|
+
# {DancingLinks} algorithm for finding exact covers.
|
8
|
+
module Doku
|
9
|
+
|
10
|
+
# @abstract Use the {Sudoku}, {Hexadoku}, or {Hexamurai}
|
11
|
+
# subclasses or make a subclass to represent your own type
|
12
|
+
# of Sudoku-like puzzle.
|
13
|
+
#
|
14
|
+
# This in abstract class for creating classes that represent
|
15
|
+
# Sudoku-like puzzles.
|
16
|
+
#
|
17
|
+
# Every subclass of {Doku::Puzzle} represents a Sudoku-like puzzle consisting
|
18
|
+
# of a set of glyphs, a set of squares, and a set of groups of squares.
|
19
|
+
# For example, the {Doku::Sudoku} subclass represents the famous 9x9 puzzle,
|
20
|
+
# Sudoku.
|
21
|
+
#
|
22
|
+
# Every instance of a subclass of Puzzle represents a particular state
|
23
|
+
# of that type of puzzle, i.e. a record of which glyph is assigned
|
24
|
+
# to each square.
|
25
|
+
class Puzzle
|
26
|
+
include SolvableWithDancingLinks
|
27
|
+
|
28
|
+
# A hash that associates squares to glyphs, representing
|
29
|
+
# the arrangement of glyphs in the puzzle.
|
30
|
+
attr_reader :glyph_state
|
31
|
+
|
32
|
+
# Creates a new instance of the puzzle.
|
33
|
+
#
|
34
|
+
# @param [Hash] glyph_state The state of the puzzle, represented as a hash
|
35
|
+
# where the keys are squares and the values are nil or glyphs in the context
|
36
|
+
# of this puzzle class. For example, this represents what numbers have been
|
37
|
+
# written in the boxes of a {Sudoku} puzzle.
|
38
|
+
def initialize(glyph_state = {})
|
39
|
+
@glyph_state = {}
|
40
|
+
# We initialize glyph_state this way so that the data gets validated.
|
41
|
+
glyph_state.each { |square, glyph| self[square] = glyph }
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
|
46
|
+
# Returns an array of all the valid glyphs for this class of puzzle.
|
47
|
+
# A glyph can be any type of Ruby object, and it is meant to
|
48
|
+
# represent a symbol which can be drawn inside a square in a
|
49
|
+
# Sudoku-like puzzle.
|
50
|
+
#
|
51
|
+
# For example, the glyphs for {Sudoku} are the Ruby integers 1, 2, 3,
|
52
|
+
# 4, 5, 6, 7, 8, and 9.
|
53
|
+
#
|
54
|
+
# The glyphs, squares, and groups, are defined at the class level, in the subclasses of Doku::Puzzle.
|
55
|
+
# @return [Array] Array of objects representing glyphs.
|
56
|
+
attr_reader :glyphs
|
57
|
+
|
58
|
+
# Returns an array of all the valid squares in this class of puzzle.
|
59
|
+
# A square can be any type of Ruby object, and it is meant to
|
60
|
+
# represent a square in which glyphs are drawn in a Sudoku-like puzzle.
|
61
|
+
#
|
62
|
+
# For example, there are 81 squares defined in the {Sudoku} class,
|
63
|
+
# one for each square on the 9x9 Sudoku grid.
|
64
|
+
#
|
65
|
+
# The glyphs, squares, and groups, are defined at the class level, in the subclasses of Doku::Puzzle.
|
66
|
+
# @return [Array] Array of objects representing squares.
|
67
|
+
attr_reader :squares
|
68
|
+
|
69
|
+
# Returns an array of all the groups for this class of puzzle.
|
70
|
+
# A group should be a Set object that contains some squares.
|
71
|
+
# A group represents a constraint on solutions to the puzzle:
|
72
|
+
# every glyph must appear exactly once in every group.
|
73
|
+
#
|
74
|
+
# For example, the groups of the {Sudoku} class represent the
|
75
|
+
# nie columns, nine rows, and nine 3x3 boxes of Sudoku.
|
76
|
+
#
|
77
|
+
# The glyphs, squares, and groups, are defined at the class level, in the subclasses of Doku::Puzzle.
|
78
|
+
# @return [Array] Array of glyphs.
|
79
|
+
attr_reader :groups
|
80
|
+
end
|
81
|
+
|
82
|
+
# Shortcut for calling the {Puzzle.glyphs} class method.
|
83
|
+
# @return [Array] Array of glyphs.
|
84
|
+
def glyphs
|
85
|
+
self.class.glyphs
|
86
|
+
end
|
87
|
+
|
88
|
+
# Shortcut for calling the {Puzzle.squares} class method.
|
89
|
+
# @return [Array] Array of squares.
|
90
|
+
def squares
|
91
|
+
self.class.squares
|
92
|
+
end
|
93
|
+
|
94
|
+
# Shortcut for calling the {Puzzle.groups} class method.
|
95
|
+
# @return [Array] Array of groups.
|
96
|
+
def groups
|
97
|
+
self.class.groups
|
98
|
+
end
|
99
|
+
|
100
|
+
# Gets the glyph assigned to the given square.
|
101
|
+
# @param square Must be one of the {Puzzle.squares} for this puzzle.
|
102
|
+
# @return The glyph that is assigned to the given square
|
103
|
+
# (one of the {Puzzle.glyphs} defined for this puzzle),
|
104
|
+
# or nil if no glyph is assigned.
|
105
|
+
def [](square)
|
106
|
+
raise IndexError, "Square not found in #{self.class.name}: #{square}." if !squares.include?(square)
|
107
|
+
@glyph_state[square]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Sets the glyph assigned to the given square.
|
111
|
+
# @param square Must be one of the {Puzzle.squares} for this puzzle.
|
112
|
+
# @param glyph Must be one of the {Puzzle.glyphs} for this puzzle, or nil.
|
113
|
+
def []=(square, glyph)
|
114
|
+
raise IndexError, "Square not found in #{self.class}: #{square}." if !squares.include?(square)
|
115
|
+
raise ArgumentError, "Value must be a glyph in this puzzle or nil." if !glyph.nil? && !glyphs.include?(glyph)
|
116
|
+
|
117
|
+
# Do NOT store nils as values in the hash, because we
|
118
|
+
# don't want them to affect equality comparisons.
|
119
|
+
if glyph == nil
|
120
|
+
@glyph_state.delete square
|
121
|
+
else
|
122
|
+
@glyph_state[square] = glyph
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# This method allows you to iterate over every square that has a
|
127
|
+
# glyph assigned to it.
|
128
|
+
#
|
129
|
+
# @yield [square, glyph]
|
130
|
+
# @yieldparam square A square that has a glyph assigned to it.
|
131
|
+
# @yieldparam glyph The glyph that is assigned to the square.
|
132
|
+
def each(&block)
|
133
|
+
@glyph_state.each(&block)
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [Fixnum] Returns a hash code based on the glyph assignments.
|
137
|
+
def hash
|
138
|
+
@glyph_state.hash
|
139
|
+
end
|
140
|
+
|
141
|
+
# Two puzzles are equal if they have the same class and glyph assignments.
|
142
|
+
# @return [Boolean] True if the two puzzles are equal.
|
143
|
+
def eql?(puzzle)
|
144
|
+
self.class == puzzle.class and glyph_state == puzzle.glyph_state
|
145
|
+
end
|
146
|
+
|
147
|
+
# Same as {#eql?}.
|
148
|
+
def == (puzzle)
|
149
|
+
eql? puzzle
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns true if the puzzle's glyphs assignments are a subset
|
153
|
+
# of the given puzzle's glyph assignments and the two puzzles are
|
154
|
+
# the same class.
|
155
|
+
#
|
156
|
+
# Every puzzle is a subset of itself.
|
157
|
+
#
|
158
|
+
# For example, if you find a Sudoku puzzle and start working on it,
|
159
|
+
# you have changed the original puzzle into a new puzzle.
|
160
|
+
# The original puzzle will be a subset of the new puzzle,
|
161
|
+
# assuming you didn't erase any numbers.
|
162
|
+
#
|
163
|
+
# @return [Boolean]
|
164
|
+
def subset?(puzzle)
|
165
|
+
self.class == puzzle.class and glyph_assignment_subset?(puzzle)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns true if this puzzle is completely filled in,
|
169
|
+
# which means every square has a glyph assigned to it.
|
170
|
+
# For example, a Sudoku puzzle is considered to be filled after
|
171
|
+
# you have written a number in every box, regardless of whether
|
172
|
+
# the numbers obey the rules of Sudoku or not.
|
173
|
+
# See also {#solution?} and {#solution_for?}.
|
174
|
+
#
|
175
|
+
# @return [Boolean]
|
176
|
+
def filled?
|
177
|
+
squares.size == glyph_state.keys.size
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns true if this puzzle follows the rules.
|
181
|
+
# A puzzle is valid if no glyph appears twice in any group.
|
182
|
+
# For example, a {Sudoku} puzzle would be invalid if you
|
183
|
+
# wrote a "3" twice in the same column.
|
184
|
+
#
|
185
|
+
# @return [Boolean]
|
186
|
+
def valid?
|
187
|
+
groups.all? do |group|
|
188
|
+
glyphs = group.collect { |square| self[square] } - [nil]
|
189
|
+
glyphs.uniq.size == glyphs.size
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns true if the puzzle is {#filled?} and {#valid?}.
|
194
|
+
# @return [Boolean]
|
195
|
+
def solution?
|
196
|
+
filled? and valid?
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns true if the puzzle is valid solution for the given puzzle.
|
200
|
+
#
|
201
|
+
# @return [Boolean]
|
202
|
+
def solution_for?(puzzle)
|
203
|
+
solution? and puzzle.subset?(self)
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# This is called when the puzzle is duped or cloned.
|
209
|
+
def initialize_copy(source)
|
210
|
+
@glyph_state = @glyph_state.dup
|
211
|
+
end
|
212
|
+
|
213
|
+
# This should be called inside the definition of a Puzzle subclass
|
214
|
+
# to define what glyphs the puzzle has.
|
215
|
+
def self.has_glyphs(glyphs)
|
216
|
+
@glyphs = glyphs
|
217
|
+
end
|
218
|
+
|
219
|
+
# This should be called inside the definition of a Puzzle subclass
|
220
|
+
# to define what squares the puzzle has.
|
221
|
+
# This method defines one square at a time. See also #has_squares.
|
222
|
+
def self.define_square(square)
|
223
|
+
raise ArgumentError, "square should not be nil" if square.nil?
|
224
|
+
@squares ||= []
|
225
|
+
@squares << square
|
226
|
+
end
|
227
|
+
|
228
|
+
# This should be called inside the definition of a Puzzle subclass
|
229
|
+
# to define what squares the puzzle has.
|
230
|
+
# This method defines all the squares at once. See also #define_square.
|
231
|
+
def self.has_squares(squares)
|
232
|
+
raise ArgumentError, "list of squares should not contain nil" if squares.include? nil
|
233
|
+
@squares = squares.uniq
|
234
|
+
end
|
235
|
+
|
236
|
+
def glyph_assignment_subset?(puzzle)
|
237
|
+
glyph_state.all? do |square, glyph|
|
238
|
+
glyph == puzzle[square]
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.define_group(squares)
|
243
|
+
group = Set.new(squares)
|
244
|
+
raise ArgumentError, "Expected groups to be of size #{glyphs.size} but got one of size #{group.size}. squares = #{group.inspect}" if group.size != glyphs.size
|
245
|
+
@groups ||= []
|
246
|
+
@groups << group unless @groups.include?(group)
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
end
|