doku 1.0.0

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