twisty_puzzles 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -1
  3. data/lib/twisty_puzzles.rb +37 -0
  4. data/lib/twisty_puzzles/abstract_direction.rb +38 -39
  5. data/lib/twisty_puzzles/abstract_move_parser.rb +32 -33
  6. data/lib/twisty_puzzles/algorithm.rb +112 -113
  7. data/lib/twisty_puzzles/algorithm_transformation.rb +19 -21
  8. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +55 -56
  9. data/lib/twisty_puzzles/cancellation_helper.rb +124 -125
  10. data/lib/twisty_puzzles/commutator.rb +79 -80
  11. data/lib/twisty_puzzles/compiled_algorithm.rb +31 -32
  12. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +49 -50
  13. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +18 -19
  14. data/lib/twisty_puzzles/coordinate.rb +245 -246
  15. data/lib/twisty_puzzles/cube.rb +494 -495
  16. data/lib/twisty_puzzles/cube_constants.rb +40 -41
  17. data/lib/twisty_puzzles/cube_direction.rb +15 -18
  18. data/lib/twisty_puzzles/cube_move.rb +289 -290
  19. data/lib/twisty_puzzles/cube_move_parser.rb +75 -76
  20. data/lib/twisty_puzzles/cube_print_helper.rb +132 -133
  21. data/lib/twisty_puzzles/cube_state.rb +80 -81
  22. data/lib/twisty_puzzles/move_type_creator.rb +17 -18
  23. data/lib/twisty_puzzles/parser.rb +176 -179
  24. data/lib/twisty_puzzles/part_cycle_factory.rb +39 -42
  25. data/lib/twisty_puzzles/puzzle.rb +16 -17
  26. data/lib/twisty_puzzles/reversible_applyable.rb +24 -25
  27. data/lib/twisty_puzzles/rotation.rb +74 -75
  28. data/lib/twisty_puzzles/skewb_direction.rb +14 -15
  29. data/lib/twisty_puzzles/skewb_move.rb +48 -49
  30. data/lib/twisty_puzzles/skewb_move_parser.rb +50 -51
  31. data/lib/twisty_puzzles/skewb_notation.rb +115 -118
  32. data/lib/twisty_puzzles/skewb_state.rb +120 -121
  33. data/lib/twisty_puzzles/state_helper.rb +20 -21
  34. data/lib/twisty_puzzles/sticker_cycle.rb +43 -44
  35. data/lib/twisty_puzzles/utils.rb +3 -0
  36. data/lib/twisty_puzzles/version.rb +3 -1
  37. metadata +3 -3
@@ -5,656 +5,655 @@ require 'twisty_puzzles/coordinate'
5
5
  require 'twisty_puzzles/utils/array_helper'
6
6
 
7
7
  module TwistyPuzzles
8
-
9
- # Base class of cube parts. Represents one part or the position of one part on the cube.
10
- class Part
11
- include Utils::ArrayHelper
12
- extend Utils::ArrayHelper
13
- include CubeConstants
14
- extend CubeConstants
15
- include Comparable
16
-
17
- def initialize(face_symbols, piece_index)
18
- clazz = self.class
19
- if face_symbols.any? { |c| c.class != Symbol || !FACE_SYMBOLS.include?(c) }
20
- raise ArgumentError, "Faces symbols contain invalid item: #{face_symbols.inspect}"
21
- end
22
-
23
- if face_symbols.length != clazz::FACES
24
- raise ArgumentError, "Invalid number of face symbols #{face_symbols.length} for " \
25
- "#{clazz}. Must be #{clazz::FACES}. Got face symbols: " \
26
- "#{face_symbols.inspect}"
27
- end
28
- if face_symbols.uniq != face_symbols
29
- raise ArgumentError, "Non-unique face symbols #{face_symbols} for #{clazz}."
30
- end
8
+ # Base class of cube parts. Represents one part or the position of one part on the cube.
9
+ class Part
10
+ include Utils::ArrayHelper
11
+ extend Utils::ArrayHelper
12
+ include CubeConstants
13
+ extend CubeConstants
14
+ include Comparable
31
15
 
32
- @face_symbols = face_symbols
33
- @piece_index = piece_index
16
+ def initialize(face_symbols, piece_index)
17
+ clazz = self.class
18
+ if face_symbols.any? { |c| c.class != Symbol || !FACE_SYMBOLS.include?(c) }
19
+ raise ArgumentError, "Faces symbols contain invalid item: #{face_symbols.inspect}"
34
20
  end
35
21
 
36
- attr_reader :piece_index, :face_symbols
37
-
38
- def self.generate_parts
39
- valid_face_symbol_combinations =
40
- FACE_SYMBOLS.permutation(self::FACES).select do |p|
41
- valid?(p)
42
- end
43
- parts = valid_face_symbol_combinations.map.with_index { |p, i| new(p, i) }
44
- unless parts.length <= ALPHABET_SIZE
45
- raise "Generated #{parts.length} parts for #{self}, but the alphabet size is only " \
46
- "#{ALPHABET_SIZE}."
47
- end
48
-
49
- parts.freeze
22
+ if face_symbols.length != clazz::FACES
23
+ raise ArgumentError, "Invalid number of face symbols #{face_symbols.length} for " \
24
+ "#{clazz}. Must be #{clazz::FACES}. Got face symbols: " \
25
+ "#{face_symbols.inspect}"
50
26
  end
51
-
52
- def self.min_cube_size
53
- 2
27
+ if face_symbols.uniq != face_symbols
28
+ raise ArgumentError, "Non-unique face symbols #{face_symbols} for #{clazz}."
54
29
  end
55
30
 
56
- def self.max_cube_size
57
- Float::INFINITY
58
- end
31
+ @face_symbols = face_symbols
32
+ @piece_index = piece_index
33
+ end
59
34
 
60
- def self.exists_on_even_cube_sizes?
61
- true
62
- end
35
+ attr_reader :piece_index, :face_symbols
63
36
 
64
- def self.exists_on_odd_cube_sizes?
65
- true
37
+ def self.generate_parts
38
+ valid_face_symbol_combinations =
39
+ FACE_SYMBOLS.permutation(self::FACES).select do |p|
40
+ valid?(p)
41
+ end
42
+ parts = valid_face_symbol_combinations.map.with_index { |p, i| new(p, i) }
43
+ unless parts.length <= ALPHABET_SIZE
44
+ raise "Generated #{parts.length} parts for #{self}, but the alphabet size is only " \
45
+ "#{ALPHABET_SIZE}."
66
46
  end
67
47
 
68
- def base_index_on_face(cube_size, incarnation_index)
69
- base_index_on_other_face(solved_face, cube_size, incarnation_index)
70
- end
48
+ parts.freeze
49
+ end
71
50
 
72
- def self.for_face_symbols_internal(face_symbols)
73
- raise unless face_symbols.length == self::FACES
51
+ def self.min_cube_size
52
+ 2
53
+ end
74
54
 
75
- find_only(self::ELEMENTS) { |e| e.face_symbols == face_symbols }
76
- end
55
+ def self.max_cube_size
56
+ Float::INFINITY
57
+ end
77
58
 
78
- def self.for_face_symbols(face_symbols)
79
- for_face_symbols_internal(face_symbols)
80
- end
59
+ def self.exists_on_even_cube_sizes?
60
+ true
61
+ end
81
62
 
82
- def self.for_index(index)
83
- self::ELEMENTS[index]
84
- end
63
+ def self.exists_on_odd_cube_sizes?
64
+ true
65
+ end
85
66
 
86
- def <=>(other)
87
- @piece_index <=> other.piece_index
88
- end
67
+ def base_index_on_face(cube_size, incarnation_index)
68
+ base_index_on_other_face(solved_face, cube_size, incarnation_index)
69
+ end
89
70
 
90
- def eql?(other)
91
- self.class.equal?(other.class) && @piece_index == other.piece_index
92
- end
71
+ def self.for_face_symbols_internal(face_symbols)
72
+ raise unless face_symbols.length == self::FACES
93
73
 
94
- alias == eql?
74
+ find_only(self::ELEMENTS) { |e| e.face_symbols == face_symbols }
75
+ end
95
76
 
96
- def hash
97
- @hash ||= [self.class, @piece_index].hash
98
- end
77
+ def self.for_face_symbols(face_symbols)
78
+ for_face_symbols_internal(face_symbols)
79
+ end
99
80
 
100
- def inspect
101
- self.class.to_s + '(' + @face_symbols.map(&:to_s).join(', ') + ')'
102
- end
81
+ def self.for_index(index)
82
+ self::ELEMENTS[index]
83
+ end
103
84
 
104
- def to_s
105
- corresponding_part.face_symbols.collect.with_index do |c, i|
106
- face_name = FACE_NAMES[FACE_SYMBOLS.index(c)]
107
- i < self.class::FACES ? face_name : face_name.downcase
108
- end.join
109
- end
85
+ def <=>(other)
86
+ @piece_index <=> other.piece_index
87
+ end
110
88
 
111
- # Rotate a piece such that the given face symbol is the first face symbol.
112
- def rotate_face_symbol_up(face_symbol)
113
- index = @face_symbols.index(face_symbol)
114
- raise "Part #{self} doesn't have face symbol #{c}." unless index
89
+ def eql?(other)
90
+ self.class.equal?(other.class) && @piece_index == other.piece_index
91
+ end
115
92
 
116
- rotate_by(index)
117
- end
93
+ alias == eql?
118
94
 
119
- def rotate_face_up(face)
120
- rotate_face_symbol_up(face.face_symbol)
121
- end
95
+ def hash
96
+ @hash ||= [self.class, @piece_index].hash
97
+ end
122
98
 
123
- def rotate_by(number)
124
- self.class.for_face_symbols(@face_symbols.rotate(number))
125
- end
99
+ def inspect
100
+ self.class.to_s + '(' + @face_symbols.map(&:to_s).join(', ') + ')'
101
+ end
126
102
 
127
- # Returns true if the pieces are equal modulo rotation.
128
- def turned_equals?(other)
129
- @face_symbols.include?(other.face_symbols.first) &&
130
- rotate_face_symbol_up(other.face_symbols.first) == other
131
- end
103
+ def to_s
104
+ corresponding_part.face_symbols.collect.with_index do |c, i|
105
+ face_name = FACE_NAMES[FACE_SYMBOLS.index(c)]
106
+ i < self.class::FACES ? face_name : face_name.downcase
107
+ end.join
108
+ end
132
109
 
133
- def rotations
134
- (0...@face_symbols.length).map { |i| rotate_by(i) }
135
- end
110
+ # Rotate a piece such that the given face symbol is the first face symbol.
111
+ def rotate_face_symbol_up(face_symbol)
112
+ index = @face_symbols.index(face_symbol)
113
+ raise "Part #{self} doesn't have face symbol #{c}." unless index
136
114
 
137
- def self.create_for_face_symbols(face_symbols)
138
- new(face_symbols)
139
- end
115
+ rotate_by(index)
116
+ end
140
117
 
141
- def self.parse(piece_description)
142
- face_symbols =
143
- piece_description.upcase.strip.split('').map do |e|
144
- FACE_SYMBOLS[FACE_NAMES.index(e)]
145
- end
146
- for_face_symbols(face_symbols)
147
- end
118
+ def rotate_face_up(face)
119
+ rotate_face_symbol_up(face.face_symbol)
120
+ end
148
121
 
149
- # Only overridden by moveable centers, but returns self for convenience.
150
- def corresponding_part
151
- self
152
- end
122
+ def rotate_by(number)
123
+ self.class.for_face_symbols(@face_symbols.rotate(number))
124
+ end
153
125
 
154
- # The primary face that this piece is in in the solved state.
155
- def solved_face
156
- @solved_face ||= Face.for_face_symbol(@face_symbols.first)
157
- end
126
+ # Returns true if the pieces are equal modulo rotation.
127
+ def turned_equals?(other)
128
+ @face_symbols.include?(other.face_symbols.first) &&
129
+ rotate_face_symbol_up(other.face_symbols.first) == other
130
+ end
158
131
 
159
- def solved_coordinate(cube_size, incarnation_index = 0)
160
- Coordinate.solved_position(self, cube_size, incarnation_index)
161
- end
132
+ def rotations
133
+ (0...@face_symbols.length).map { |i| rotate_by(i) }
134
+ end
162
135
 
163
- def faces
164
- @faces ||= @face_symbols.map { |f| Face.for_face_symbol(f) }
165
- end
136
+ def self.create_for_face_symbols(face_symbols)
137
+ new(face_symbols)
166
138
  end
167
139
 
168
- # This is an unmoveable center piece, it's mostly used as a helper class for other pieces.
169
- class Face < Part
170
- FACES = 1
140
+ def self.parse(piece_description)
141
+ face_symbols =
142
+ piece_description.upcase.strip.split('').map do |e|
143
+ FACE_SYMBOLS[FACE_NAMES.index(e)]
144
+ end
145
+ for_face_symbols(face_symbols)
146
+ end
171
147
 
172
- def self.min_cube_size
173
- 3
174
- end
148
+ # Only overridden by moveable centers, but returns self for convenience.
149
+ def corresponding_part
150
+ self
151
+ end
175
152
 
176
- def self.exists_on_even_cube_sizes?
177
- false
178
- end
153
+ # The primary face that this piece is in in the solved state.
154
+ def solved_face
155
+ @solved_face ||= Face.for_face_symbol(@face_symbols.first)
156
+ end
179
157
 
180
- def self.for_face_symbol(face_symbol)
181
- for_face_symbols([face_symbol])
182
- end
158
+ def solved_coordinate(cube_size, incarnation_index = 0)
159
+ Coordinate.solved_position(self, cube_size, incarnation_index)
160
+ end
183
161
 
184
- def self.valid?(_face_symbols)
185
- true
186
- end
162
+ def faces
163
+ @faces ||= @face_symbols.map { |f| Face.for_face_symbol(f) }
164
+ end
165
+ end
187
166
 
188
- ELEMENTS = generate_parts
167
+ # This is an unmoveable center piece, it's mostly used as a helper class for other pieces.
168
+ class Face < Part
169
+ FACES = 1
189
170
 
190
- # Whether closeness to this face results in smaller indices for the stickers of other faces.
191
- def close_to_smaller_indices?
192
- @piece_index < 3
193
- end
171
+ def self.min_cube_size
172
+ 3
173
+ end
194
174
 
195
- def coordinate_index_base_face(coordinate_index)
196
- (@coordinate_index_base_face ||= {})[coordinate_index] ||=
197
- find_only(neighbors) do |n|
198
- n.close_to_smaller_indices? && coordinate_index_close_to(n) == coordinate_index
199
- end
200
- end
175
+ def self.exists_on_even_cube_sizes?
176
+ false
177
+ end
201
178
 
202
- def opposite
203
- Face.for_face_symbol(opposite_face_symbol(face_symbol))
204
- end
179
+ def self.for_face_symbol(face_symbol)
180
+ for_face_symbols([face_symbol])
181
+ end
205
182
 
206
- def same_axis?(other)
207
- axis_priority == other.axis_priority
208
- end
183
+ def self.valid?(_face_symbols)
184
+ true
185
+ end
209
186
 
210
- # Returns the index of the coordinate that is used to determine how close a sticker on
211
- # `on_face` is to `to_face`.
212
- def coordinate_index_close_to(to_face)
213
- if same_axis?(to_face)
214
- raise ArgumentError, "Cannot get the coordinate index close to #{to_face.inspect} " \
215
- "on #{inspect} because they are not neighbors."
216
- end
187
+ ELEMENTS = generate_parts
217
188
 
218
- to_priority = to_face.axis_priority
219
- if axis_priority < to_priority
220
- to_priority - 1
221
- else
222
- to_priority
189
+ # Whether closeness to this face results in smaller indices for the stickers of other faces.
190
+ def close_to_smaller_indices?
191
+ @piece_index < 3
192
+ end
193
+
194
+ def coordinate_index_base_face(coordinate_index)
195
+ (@coordinate_index_base_face ||= {})[coordinate_index] ||=
196
+ find_only(neighbors) do |n|
197
+ n.close_to_smaller_indices? && coordinate_index_close_to(n) == coordinate_index
223
198
  end
224
- end
199
+ end
225
200
 
226
- # Priority of the closeness to this face.
227
- # This is used to index the stickers on other faces.
228
- def axis_priority
229
- @axis_priority ||= [@piece_index, CubeConstants::FACE_SYMBOLS.length - 1 - @piece_index].min
230
- end
201
+ def opposite
202
+ Face.for_face_symbol(opposite_face_symbol(face_symbol))
203
+ end
231
204
 
232
- def canonical_axis_face?
233
- close_to_smaller_indices?
205
+ def same_axis?(other)
206
+ axis_priority == other.axis_priority
207
+ end
208
+
209
+ # Returns the index of the coordinate that is used to determine how close a sticker on
210
+ # `on_face` is to `to_face`.
211
+ def coordinate_index_close_to(to_face)
212
+ if same_axis?(to_face)
213
+ raise ArgumentError, "Cannot get the coordinate index close to #{to_face.inspect} " \
214
+ "on #{inspect} because they are not neighbors."
234
215
  end
235
216
 
236
- def name
237
- @name ||= FACE_NAMES[ELEMENTS.index(self)]
217
+ to_priority = to_face.axis_priority
218
+ if axis_priority < to_priority
219
+ to_priority - 1
220
+ else
221
+ to_priority
238
222
  end
223
+ end
239
224
 
240
- def self.by_name(name)
241
- index = FACE_NAMES.index(name.upcase)
242
- raise "#{name} is not a valid #{self.class.name}." unless index
225
+ # Priority of the closeness to this face.
226
+ # This is used to index the stickers on other faces.
227
+ def axis_priority
228
+ @axis_priority ||= [@piece_index, CubeConstants::FACE_SYMBOLS.length - 1 - @piece_index].min
229
+ end
243
230
 
244
- ELEMENTS[index]
245
- end
231
+ def canonical_axis_face?
232
+ close_to_smaller_indices?
233
+ end
246
234
 
247
- def face_symbol
248
- @face_symbols[0]
249
- end
235
+ def name
236
+ @name ||= FACE_NAMES[ELEMENTS.index(self)]
237
+ end
250
238
 
251
- # Neighbor faces in clockwise order.
252
- def neighbors
253
- @neighbors ||=
254
- begin
255
- partial_neighbors =
256
- self.class::ELEMENTS.select do |e|
257
- !same_axis?(e) && e.canonical_axis_face?
258
- end
259
- ordered_partial_neighbors = sort_partial_neighbors(partial_neighbors)
260
- ordered_partial_neighbors + ordered_partial_neighbors.map(&:opposite)
261
- end
262
- end
239
+ def self.by_name(name)
240
+ index = FACE_NAMES.index(name.upcase)
241
+ raise "#{name} is not a valid #{self.class.name}." unless index
263
242
 
264
- def clockwise_neighbor_after(neighbor)
265
- raise ArgumentError if same_axis?(neighbor)
243
+ ELEMENTS[index]
244
+ end
266
245
 
267
- @neighbors[(@neighbors.index(neighbor) + 1) % @neighbors.length]
268
- end
246
+ def face_symbol
247
+ @face_symbols[0]
248
+ end
269
249
 
270
- # Returns the algorithm that performs a rotation after which the current face will
271
- # lie where the given other face currently is.
272
- def rotation_to(other)
273
- if other == self
274
- Algorithm::EMPTY
275
- else
276
- # There can be multiple solutions.
277
- axis_face =
278
- self.class::ELEMENTS.find do |e|
279
- !same_axis?(e) && !other.same_axis?(e) && e.canonical_axis_face?
250
+ # Neighbor faces in clockwise order.
251
+ def neighbors
252
+ @neighbors ||=
253
+ begin
254
+ partial_neighbors =
255
+ self.class::ELEMENTS.select do |e|
256
+ !same_axis?(e) && e.canonical_axis_face?
280
257
  end
281
- direction = rotation_direction_to(other)
282
- Algorithm.move(Rotation.new(axis_face, direction))
258
+ ordered_partial_neighbors = sort_partial_neighbors(partial_neighbors)
259
+ ordered_partial_neighbors + ordered_partial_neighbors.map(&:opposite)
283
260
  end
284
- end
285
-
286
- FACE_SYMBOLS.map { |s| const_set(s, for_face_symbol(s)) }
261
+ end
287
262
 
288
- def clockwise_corners
289
- neighbors.zip(neighbors.rotate).map { |a, b| Corner.between_faces([self, a, b]) }
290
- end
263
+ def clockwise_neighbor_after(neighbor)
264
+ raise ArgumentError if same_axis?(neighbor)
291
265
 
292
- private
266
+ @neighbors[(@neighbors.index(neighbor) + 1) % @neighbors.length]
267
+ end
293
268
 
294
- def sort_partial_neighbors(partial_neighbors)
295
- if Corner.valid_between_faces?([self] + partial_neighbors)
296
- partial_neighbors
297
- elsif Corner.valid_between_faces?([self] + partial_neighbors.reverse)
298
- partial_neighbors.reverse
299
- else
300
- raise "Couldn't find a proper order for the neighbor faces " \
301
- "#{partial_neighbors.inspect} of #{inspect}."
302
- end
269
+ # Returns the algorithm that performs a rotation after which the current face will
270
+ # lie where the given other face currently is.
271
+ def rotation_to(other)
272
+ if other == self
273
+ Algorithm::EMPTY
274
+ else
275
+ # There can be multiple solutions.
276
+ axis_face =
277
+ self.class::ELEMENTS.find do |e|
278
+ !same_axis?(e) && !other.same_axis?(e) && e.canonical_axis_face?
279
+ end
280
+ direction = rotation_direction_to(other)
281
+ Algorithm.move(Rotation.new(axis_face, direction))
303
282
  end
283
+ end
304
284
 
305
- def rotation_direction_to(other)
306
- if other == opposite
307
- CubeDirection::DOUBLE
308
- elsif close_to_smaller_indices? ^
309
- other.close_to_smaller_indices? ^
310
- (axis_priority > other.axis_priority)
311
- CubeDirection::FORWARD
312
- else
313
- CubeDirection::BACKWARD
314
- end
315
- end
285
+ FACE_SYMBOLS.map { |s| const_set(s, for_face_symbol(s)) }
286
+
287
+ def clockwise_corners
288
+ neighbors.zip(neighbors.rotate).map { |a, b| Corner.between_faces([self, a, b]) }
316
289
  end
317
290
 
318
- # Base class of moveable centers. Represents one moveable center or the position of one moveable
319
- # center on the cube.
320
- class MoveableCenter < Part
321
- FACES = 1
291
+ private
322
292
 
323
- def self.min_cube_size
324
- 4
293
+ def sort_partial_neighbors(partial_neighbors)
294
+ if Corner.valid_between_faces?([self] + partial_neighbors)
295
+ partial_neighbors
296
+ elsif Corner.valid_between_faces?([self] + partial_neighbors.reverse)
297
+ partial_neighbors.reverse
298
+ else
299
+ raise "Couldn't find a proper order for the neighbor faces " \
300
+ "#{partial_neighbors.inspect} of #{inspect}."
325
301
  end
302
+ end
326
303
 
327
- def self.valid?(face_symbols)
328
- self::CORRESPONDING_PART_CLASS.valid?(face_symbols)
304
+ def rotation_direction_to(other)
305
+ if other == opposite
306
+ CubeDirection::DOUBLE
307
+ elsif close_to_smaller_indices? ^
308
+ other.close_to_smaller_indices? ^
309
+ (axis_priority > other.axis_priority)
310
+ CubeDirection::FORWARD
311
+ else
312
+ CubeDirection::BACKWARD
329
313
  end
314
+ end
315
+ end
330
316
 
331
- def self.for_face_symbols(face_symbols)
332
- unless face_symbols.length == self::CORRESPONDING_PART_CLASS::FACES
333
- raise ArgumentError, "Need #{self::CORRESPONDING_PART_CLASS::FACES} face_symbols for a " \
334
- "#{self.class}, have #{face_symbols.inspect}."
335
- end
336
-
337
- corresponding_part = self::CORRESPONDING_PART_CLASS.for_face_symbols(face_symbols)
338
- nil unless corresponding_part
339
- find_only(self::ELEMENTS) { |e| e.corresponding_part == corresponding_part }
340
- end
317
+ # Base class of moveable centers. Represents one moveable center or the position of one moveable
318
+ # center on the cube.
319
+ class MoveableCenter < Part
320
+ FACES = 1
341
321
 
342
- def self.create_for_face_symbols(face_symbols)
343
- new(self::CORRESPONDING_PART_CLASS.create_for_face_symbols(face_symbols))
344
- end
322
+ def self.min_cube_size
323
+ 4
324
+ end
345
325
 
346
- def face_symbol
347
- @face_symbols[0]
348
- end
326
+ def self.valid?(face_symbols)
327
+ self::CORRESPONDING_PART_CLASS.valid?(face_symbols)
328
+ end
349
329
 
350
- def eql?(other)
351
- self.class.equal?(other.class) && face_symbol == other.face_symbol &&
352
- @corresponding_part == other.corresponding_part
330
+ def self.for_face_symbols(face_symbols)
331
+ unless face_symbols.length == self::CORRESPONDING_PART_CLASS::FACES
332
+ raise ArgumentError, "Need #{self::CORRESPONDING_PART_CLASS::FACES} face_symbols for a " \
333
+ "#{self.class}, have #{face_symbols.inspect}."
353
334
  end
354
335
 
355
- def initialize(corresponding_part, piece_index)
356
- unless corresponding_part.is_a?(Part)
357
- raise "Invalid corresponding part #{corresponding_part}."
358
- end
336
+ corresponding_part = self::CORRESPONDING_PART_CLASS.for_face_symbols(face_symbols)
337
+ nil unless corresponding_part
338
+ find_only(self::ELEMENTS) { |e| e.corresponding_part == corresponding_part }
339
+ end
359
340
 
360
- super([corresponding_part.face_symbols[0]], piece_index)
361
- @corresponding_part = corresponding_part
362
- end
341
+ def self.create_for_face_symbols(face_symbols)
342
+ new(self::CORRESPONDING_PART_CLASS.create_for_face_symbols(face_symbols))
343
+ end
363
344
 
364
- alias == eql?
345
+ def face_symbol
346
+ @face_symbols[0]
347
+ end
365
348
 
366
- attr_reader :corresponding_part
349
+ def eql?(other)
350
+ self.class.equal?(other.class) && face_symbol == other.face_symbol &&
351
+ @corresponding_part == other.corresponding_part
352
+ end
367
353
 
368
- def inspect
369
- self.class.to_s + '(' + face_symbol.to_s + ', ' + @corresponding_part.inspect + ')'
354
+ def initialize(corresponding_part, piece_index)
355
+ unless corresponding_part.is_a?(Part)
356
+ raise "Invalid corresponding part #{corresponding_part}."
370
357
  end
371
358
 
372
- def rotate_by(_number)
373
- self
374
- end
359
+ super([corresponding_part.face_symbols[0]], piece_index)
360
+ @corresponding_part = corresponding_part
361
+ end
375
362
 
376
- def neighbor?(other)
377
- face_symbol == other.face_symbol
378
- end
363
+ alias == eql?
379
364
 
380
- def neighbors
381
- self.class::ELEMENTS.select { |p| neighbor?(p) }
382
- end
365
+ attr_reader :corresponding_part
383
366
 
384
- def self.generate_parts
385
- self::CORRESPONDING_PART_CLASS::ELEMENTS.map { |p| new(p, p.piece_index) }
386
- end
367
+ def inspect
368
+ self.class.to_s + '(' + face_symbol.to_s + ', ' + @corresponding_part.inspect + ')'
387
369
  end
388
370
 
389
- # Module for methods that are common to all edge-like part classes.
390
- module EdgeLike
391
- def valid?(face_symbols)
392
- CubeConstants::OPPOSITE_FACE_SYMBOLS.none? { |ss| ss.sort == face_symbols.sort }
393
- end
371
+ def rotate_by(_number)
372
+ self
394
373
  end
395
374
 
396
- # Represents one edge or the position of one edge on the cube.
397
- class Edge < Part
398
- extend EdgeLike
399
- FACES = 2
375
+ def neighbor?(other)
376
+ face_symbol == other.face_symbol
377
+ end
400
378
 
401
- ELEMENTS = generate_parts
379
+ def neighbors
380
+ self.class::ELEMENTS.select { |p| neighbor?(p) }
381
+ end
402
382
 
403
- def self.min_cube_size
404
- 3
405
- end
383
+ def self.generate_parts
384
+ self::CORRESPONDING_PART_CLASS::ELEMENTS.map { |p| new(p, p.piece_index) }
385
+ end
386
+ end
406
387
 
407
- def self.max_cube_size
408
- 3
409
- end
388
+ # Module for methods that are common to all edge-like part classes.
389
+ module EdgeLike
390
+ def valid?(face_symbols)
391
+ CubeConstants::OPPOSITE_FACE_SYMBOLS.none? { |ss| ss.sort == face_symbols.sort }
392
+ end
393
+ end
410
394
 
411
- def self.exists_on_even_cube_sizes?
412
- false
413
- end
395
+ # Represents one edge or the position of one edge on the cube.
396
+ class Edge < Part
397
+ extend EdgeLike
398
+ FACES = 2
414
399
 
415
- # Edges on uneven bigger cubes are midges, so edges only exist for 3x3.
416
- def num_incarnations(cube_size)
417
- cube_size == 3 ? 1 : 0
418
- end
400
+ ELEMENTS = generate_parts
419
401
 
420
- # One index of such a piece on a on a NxN face.
421
- def base_index_on_other_face(_face, _cube_size, _incarnation_index)
422
- [0, 1]
423
- end
402
+ def self.min_cube_size
403
+ 3
424
404
  end
425
405
 
426
- # Represents one midge or the position of one midge on the cube.
427
- class Midge < Part
428
- extend EdgeLike
429
- FACES = 2
406
+ def self.max_cube_size
407
+ 3
408
+ end
430
409
 
431
- ELEMENTS = generate_parts
410
+ def self.exists_on_even_cube_sizes?
411
+ false
412
+ end
432
413
 
433
- def self.min_cube_size
434
- 5
435
- end
414
+ # Edges on uneven bigger cubes are midges, so edges only exist for 3x3.
415
+ def num_incarnations(cube_size)
416
+ cube_size == 3 ? 1 : 0
417
+ end
436
418
 
437
- def self.exists_on_even_cube_sizes?
438
- false
439
- end
419
+ # One index of such a piece on a on a NxN face.
420
+ def base_index_on_other_face(_face, _cube_size, _incarnation_index)
421
+ [0, 1]
422
+ end
423
+ end
440
424
 
441
- # One index of such a piece on a on a NxN face.
442
- def base_index_on_other_face(_face, cube_size, _incarnation_index)
443
- [0, Coordinate.middle(cube_size)]
444
- end
425
+ # Represents one midge or the position of one midge on the cube.
426
+ class Midge < Part
427
+ extend EdgeLike
428
+ FACES = 2
445
429
 
446
- def num_incarnations(cube_size)
447
- cube_size >= 5 && cube_size.odd? ? 1 : 0
448
- end
449
- end
430
+ ELEMENTS = generate_parts
450
431
 
451
- # Represents one wing or the position of one wing on the cube.
452
- class Wing < Part
453
- extend EdgeLike
454
- WING_BASE_INDEX_INVERTED_FACE_SYMBOLS = %i[U R B].freeze
455
- FACES = 2
432
+ def self.min_cube_size
433
+ 5
434
+ end
456
435
 
457
- ELEMENTS = generate_parts
436
+ def self.exists_on_even_cube_sizes?
437
+ false
438
+ end
458
439
 
459
- def self.min_cube_size
460
- 4
461
- end
440
+ # One index of such a piece on a on a NxN face.
441
+ def base_index_on_other_face(_face, cube_size, _incarnation_index)
442
+ [0, Coordinate.middle(cube_size)]
443
+ end
462
444
 
463
- def self.exists_on_odd_cube_sizes?
464
- false
465
- end
445
+ def num_incarnations(cube_size)
446
+ cube_size >= 5 && cube_size.odd? ? 1 : 0
447
+ end
448
+ end
466
449
 
467
- def self.for_face_symbols(face_symbols)
468
- # One additional face symbol is usually mentioned for wings.
469
- raise unless face_symbols.length == FACES || face_symbols.length == FACES + 1
450
+ # Represents one wing or the position of one wing on the cube.
451
+ class Wing < Part
452
+ extend EdgeLike
453
+ WING_BASE_INDEX_INVERTED_FACE_SYMBOLS = %i[U R B].freeze
454
+ FACES = 2
470
455
 
471
- if face_symbols.length == 3
472
- for_corner_face_symbols(face_symbols)
473
- else
474
- for_face_symbols_internal(face_symbols)
475
- end
476
- end
456
+ ELEMENTS = generate_parts
477
457
 
478
- def self.for_corner_face_symbols(face_symbols)
479
- valid = Corner.valid?(face_symbols)
480
- reordered_face_symbols = face_symbols.dup
481
- reordered_face_symbols[0], reordered_face_symbols[1] =
482
- reordered_face_symbols[1], reordered_face_symbols[0]
483
- reordered_valid = Corner.valid?(reordered_face_symbols)
484
- if valid == reordered_valid
485
- raise "Couldn't determine chirality for #{face_symbols.inspect} which " \
486
- 'is needed to parse a wing.'
487
- end
458
+ def self.min_cube_size
459
+ 4
460
+ end
488
461
 
489
- if valid
490
- for_face_symbols(face_symbols[0..1])
491
- else
492
- for_face_symbols_internal(reordered_face_symbols[0..1])
493
- end
494
- end
462
+ def self.exists_on_odd_cube_sizes?
463
+ false
464
+ end
495
465
 
496
- private_class_method :for_corner_face_symbols
466
+ def self.for_face_symbols(face_symbols)
467
+ # One additional face symbol is usually mentioned for wings.
468
+ raise unless face_symbols.length == FACES || face_symbols.length == FACES + 1
497
469
 
498
- def corresponding_part
499
- @corresponding_part ||=
500
- begin
501
- face_symbol =
502
- find_only(FACE_SYMBOLS) do |c|
503
- !@face_symbols.include?(c) && Corner.valid?(@face_symbols + [c])
504
- end
505
- Corner.for_face_symbols(@face_symbols + [face_symbol])
506
- end
470
+ if face_symbols.length == 3
471
+ for_corner_face_symbols(face_symbols)
472
+ else
473
+ for_face_symbols_internal(face_symbols)
507
474
  end
475
+ end
508
476
 
509
- def rotations
510
- [self]
477
+ def self.for_corner_face_symbols(face_symbols)
478
+ valid = Corner.valid?(face_symbols)
479
+ reordered_face_symbols = face_symbols.dup
480
+ reordered_face_symbols[0], reordered_face_symbols[1] =
481
+ reordered_face_symbols[1], reordered_face_symbols[0]
482
+ reordered_valid = Corner.valid?(reordered_face_symbols)
483
+ if valid == reordered_valid
484
+ raise "Couldn't determine chirality for #{face_symbols.inspect} which " \
485
+ 'is needed to parse a wing.'
511
486
  end
512
487
 
513
- def rotate_by(_number)
514
- self
488
+ if valid
489
+ for_face_symbols(face_symbols[0..1])
490
+ else
491
+ for_face_symbols_internal(reordered_face_symbols[0..1])
515
492
  end
493
+ end
516
494
 
517
- def num_incarnations(cube_size)
518
- [cube_size / 2 - 1, 0].max
519
- end
495
+ private_class_method :for_corner_face_symbols
520
496
 
521
- # One index of such a piece on a on a NxN face.
522
- def base_index_on_other_face(face, _cube_size, incarnation_index)
523
- # TODO: Make this more elegant than hardcoding
524
- inverse = WING_BASE_INDEX_INVERTED_FACE_SYMBOLS.include?(face.face_symbol)
525
- coordinates = [0, 1 + incarnation_index]
526
- inverse ? coordinates.reverse : coordinates
527
- end
497
+ def corresponding_part
498
+ @corresponding_part ||=
499
+ begin
500
+ face_symbol =
501
+ find_only(FACE_SYMBOLS) do |c|
502
+ !@face_symbols.include?(c) && Corner.valid?(@face_symbols + [c])
503
+ end
504
+ Corner.for_face_symbols(@face_symbols + [face_symbol])
505
+ end
528
506
  end
529
507
 
530
- # Represents one corner or the position of one corner on the cube.
531
- class Corner < Part
532
- FACES = 3
508
+ def rotations
509
+ [self]
510
+ end
533
511
 
534
- def self.create_for_face_symbols(face_symbols)
535
- piece_candidates =
536
- face_symbols[1..-1].permutation.map do |cs|
537
- new([face_symbols[0]] + cs)
538
- end
539
- find_only(piece_candidates, &:valid?)
540
- end
512
+ def rotate_by(_number)
513
+ self
514
+ end
541
515
 
542
- def self.for_face_symbols(face_symbols)
543
- unless face_symbols.length == FACES
544
- raise "Invalid number of face_symbols to create a corner: #{face_symbols.inspect}"
545
- end
516
+ def num_incarnations(cube_size)
517
+ [cube_size / 2 - 1, 0].max
518
+ end
519
+
520
+ # One index of such a piece on a on a NxN face.
521
+ def base_index_on_other_face(face, _cube_size, incarnation_index)
522
+ # TODO: Make this more elegant than hardcoding
523
+ inverse = WING_BASE_INDEX_INVERTED_FACE_SYMBOLS.include?(face.face_symbol)
524
+ coordinates = [0, 1 + incarnation_index]
525
+ inverse ? coordinates.reverse : coordinates
526
+ end
527
+ end
528
+
529
+ # Represents one corner or the position of one corner on the cube.
530
+ class Corner < Part
531
+ FACES = 3
546
532
 
547
- if valid?(face_symbols)
548
- for_face_symbols_internal(face_symbols)
549
- else
550
- for_face_symbols_internal([face_symbols[0], face_symbols[2], face_symbols[1]])
533
+ def self.create_for_face_symbols(face_symbols)
534
+ piece_candidates =
535
+ face_symbols[1..-1].permutation.map do |cs|
536
+ new([face_symbols[0]] + cs)
551
537
  end
552
- end
538
+ find_only(piece_candidates, &:valid?)
539
+ end
553
540
 
554
- def self.valid_between_faces?(faces)
555
- valid?(faces.map(&:face_symbol))
541
+ def self.for_face_symbols(face_symbols)
542
+ unless face_symbols.length == FACES
543
+ raise "Invalid number of face_symbols to create a corner: #{face_symbols.inspect}"
556
544
  end
557
545
 
558
- def self.between_faces(faces)
559
- for_face_symbols(faces.map(&:face_symbol))
546
+ if valid?(face_symbols)
547
+ for_face_symbols_internal(face_symbols)
548
+ else
549
+ for_face_symbols_internal([face_symbols[0], face_symbols[2], face_symbols[1]])
560
550
  end
551
+ end
561
552
 
562
- def self.valid?(face_symbols)
563
- face_symbols.combination(2).all? { |e| Edge.valid?(e) } && valid_chirality?(face_symbols)
564
- end
553
+ def self.valid_between_faces?(faces)
554
+ valid?(faces.map(&:face_symbol))
555
+ end
565
556
 
566
- ELEMENTS = generate_parts
557
+ def self.between_faces(faces)
558
+ for_face_symbols(faces.map(&:face_symbol))
559
+ end
567
560
 
568
- # Rotate such that neither the current face symbol nor the given face symbol are at the
569
- # position of the letter.
570
- def rotate_other_face_symbol_up(face_symbol)
571
- index = @face_symbols.index(face_symbol)
572
- raise ArgumentError, "Part #{self} doesn't have face symbol #{face_symbol}." unless index
561
+ def self.valid?(face_symbols)
562
+ face_symbols.combination(2).all? { |e| Edge.valid?(e) } && valid_chirality?(face_symbols)
563
+ end
573
564
 
574
- if index.zero?
575
- raise ArgumentError, "Part #{self} already has face symbol #{face_symbol} up, so " \
576
- "`rotate_other_face_symbol_up(#{face_symbol}) is invalid."
577
- end
565
+ ELEMENTS = generate_parts
578
566
 
579
- rotate_by(3 - index)
580
- end
567
+ # Rotate such that neither the current face symbol nor the given face symbol are at the
568
+ # position of the letter.
569
+ def rotate_other_face_symbol_up(face_symbol)
570
+ index = @face_symbols.index(face_symbol)
571
+ raise ArgumentError, "Part #{self} doesn't have face symbol #{face_symbol}." unless index
581
572
 
582
- def diagonal_opposite
583
- @diagonal_opposite ||=
584
- Corner.for_face_symbols(face_symbols.map { |f| opposite_face_symbol(f) })
573
+ if index.zero?
574
+ raise ArgumentError, "Part #{self} already has face symbol #{face_symbol} up, so " \
575
+ "`rotate_other_face_symbol_up(#{face_symbol}) is invalid."
585
576
  end
586
577
 
587
- def rotate_other_face_up(face)
588
- rotate_other_face_symbol_up(face.face_symbol)
589
- end
578
+ rotate_by(3 - index)
579
+ end
590
580
 
591
- def common_edge_with?(other)
592
- common_faces(other) == 2
593
- end
581
+ def diagonal_opposite
582
+ @diagonal_opposite ||=
583
+ Corner.for_face_symbols(face_symbols.map { |f| opposite_face_symbol(f) })
584
+ end
594
585
 
595
- def common_faces(other)
596
- raise TypeError unless other.is_a?(Corner)
586
+ def rotate_other_face_up(face)
587
+ rotate_other_face_symbol_up(face.face_symbol)
588
+ end
597
589
 
598
- (@face_symbols & other.face_symbols).length
599
- end
590
+ def common_edge_with?(other)
591
+ common_faces(other) == 2
592
+ end
600
593
 
601
- def adjacent_edges
602
- @adjacent_edges ||= @face_symbols.combination(2).map { |e| Edge.for_face_symbols(e) }
603
- end
594
+ def common_faces(other)
595
+ raise TypeError unless other.is_a?(Corner)
604
596
 
605
- def adjacent_faces
606
- @adjacent_faces ||= @face_symbols.map { |f| Face.for_face_symbol(f) }
607
- end
597
+ (@face_symbols & other.face_symbols).length
598
+ end
608
599
 
609
- def num_incarnations(cube_size)
610
- cube_size >= 2 ? 1 : 0
611
- end
600
+ def adjacent_edges
601
+ @adjacent_edges ||= @face_symbols.combination(2).map { |e| Edge.for_face_symbols(e) }
602
+ end
612
603
 
613
- # One index of such a piece on a on a NxN face.
614
- def base_index_on_other_face(_face, _cube_size, _incarnation_index)
615
- [0, 0]
616
- end
604
+ def adjacent_faces
605
+ @adjacent_faces ||= @face_symbols.map { |f| Face.for_face_symbol(f) }
617
606
  end
618
607
 
619
- # Represents one X center or the position of one X center on the cube.
620
- class XCenter < MoveableCenter
621
- CORRESPONDING_PART_CLASS = Corner
622
- ELEMENTS = generate_parts
608
+ def num_incarnations(cube_size)
609
+ cube_size >= 2 ? 1 : 0
610
+ end
623
611
 
624
- def num_incarnations(cube_size)
625
- [cube_size / 2 - 1, 0].max
626
- end
612
+ # One index of such a piece on a on a NxN face.
613
+ def base_index_on_other_face(_face, _cube_size, _incarnation_index)
614
+ [0, 0]
615
+ end
616
+ end
627
617
 
628
- # One index of such a piece on a on a NxN face.
629
- def base_index_on_other_face(_face, _cube_size, incarnation_index)
630
- [1 + incarnation_index, 1 + incarnation_index]
631
- end
618
+ # Represents one X center or the position of one X center on the cube.
619
+ class XCenter < MoveableCenter
620
+ CORRESPONDING_PART_CLASS = Corner
621
+ ELEMENTS = generate_parts
622
+
623
+ def num_incarnations(cube_size)
624
+ [cube_size / 2 - 1, 0].max
632
625
  end
633
626
 
634
- # Represents one T center or the position of one T center on the cube.
635
- class TCenter < MoveableCenter
636
- CORRESPONDING_PART_CLASS = Edge
637
- ELEMENTS = generate_parts
627
+ # One index of such a piece on a on a NxN face.
628
+ def base_index_on_other_face(_face, _cube_size, incarnation_index)
629
+ [1 + incarnation_index, 1 + incarnation_index]
630
+ end
631
+ end
638
632
 
639
- def self.min_cube_size
640
- 5
641
- end
633
+ # Represents one T center or the position of one T center on the cube.
634
+ class TCenter < MoveableCenter
635
+ CORRESPONDING_PART_CLASS = Edge
636
+ ELEMENTS = generate_parts
642
637
 
643
- def self.exists_on_even_cube_sizes?
644
- false
645
- end
638
+ def self.min_cube_size
639
+ 5
640
+ end
646
641
 
647
- def num_incarnations(cube_size)
648
- if cube_size.even?
649
- 0
650
- else
651
- [cube_size / 2 - 1, 0].max
652
- end
653
- end
642
+ def self.exists_on_even_cube_sizes?
643
+ false
644
+ end
654
645
 
655
- # One index of such a piece on a on a NxN face.
656
- def base_index_on_other_face(_face, cube_size, incarnation_index)
657
- [1 + incarnation_index, cube_size / 2]
646
+ def num_incarnations(cube_size)
647
+ if cube_size.even?
648
+ 0
649
+ else
650
+ [cube_size / 2 - 1, 0].max
658
651
  end
659
652
  end
653
+
654
+ # One index of such a piece on a on a NxN face.
655
+ def base_index_on_other_face(_face, cube_size, incarnation_index)
656
+ [1 + incarnation_index, cube_size / 2]
657
+ end
658
+ end
660
659
  end