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/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
|