twisty_puzzles 0.0.1 → 0.0.6

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +6 -1
  4. data/ext/twisty_puzzles/native/cube_algorithm.c +267 -0
  5. data/ext/twisty_puzzles/native/cube_algorithm.h +5 -0
  6. data/ext/twisty_puzzles/native/cube_average.c +184 -0
  7. data/ext/twisty_puzzles/native/cube_average.h +5 -0
  8. data/ext/twisty_puzzles/native/cube_coordinate.c +207 -0
  9. data/ext/twisty_puzzles/native/cube_coordinate.h +34 -0
  10. data/ext/twisty_puzzles/native/cube_state.c +264 -0
  11. data/ext/twisty_puzzles/native/cube_state.h +31 -0
  12. data/ext/twisty_puzzles/native/extconf.rb +1 -1
  13. data/ext/twisty_puzzles/native/face_symbols.c +67 -0
  14. data/ext/twisty_puzzles/native/face_symbols.h +34 -0
  15. data/ext/twisty_puzzles/native/native.c +28 -0
  16. data/ext/twisty_puzzles/native/skewb_algorithm.c +331 -0
  17. data/ext/twisty_puzzles/native/skewb_algorithm.h +5 -0
  18. data/ext/twisty_puzzles/native/skewb_coordinate.c +237 -0
  19. data/ext/twisty_puzzles/native/skewb_coordinate.h +36 -0
  20. data/ext/twisty_puzzles/native/skewb_layer_fingerprint.c +271 -0
  21. data/ext/twisty_puzzles/native/skewb_layer_fingerprint.h +5 -0
  22. data/ext/twisty_puzzles/native/skewb_state.c +214 -0
  23. data/ext/twisty_puzzles/native/skewb_state.h +23 -0
  24. data/ext/twisty_puzzles/native/utils.c +76 -0
  25. data/ext/twisty_puzzles/native/utils.h +31 -0
  26. data/lib/twisty_puzzles.rb +38 -0
  27. data/lib/twisty_puzzles/abstract_direction.rb +38 -39
  28. data/lib/twisty_puzzles/abstract_move.rb +1 -2
  29. data/lib/twisty_puzzles/abstract_move_parser.rb +32 -33
  30. data/lib/twisty_puzzles/algorithm.rb +112 -113
  31. data/lib/twisty_puzzles/algorithm_transformation.rb +19 -21
  32. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +56 -56
  33. data/lib/twisty_puzzles/cancellation_helper.rb +124 -125
  34. data/lib/twisty_puzzles/color_scheme.rb +1 -1
  35. data/lib/twisty_puzzles/commutator.rb +82 -80
  36. data/lib/twisty_puzzles/compiled_algorithm.rb +31 -32
  37. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +49 -50
  38. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +18 -19
  39. data/lib/twisty_puzzles/coordinate.rb +243 -246
  40. data/lib/twisty_puzzles/cube.rb +494 -495
  41. data/lib/twisty_puzzles/cube_constants.rb +40 -41
  42. data/lib/twisty_puzzles/cube_direction.rb +15 -18
  43. data/lib/twisty_puzzles/cube_move.rb +285 -290
  44. data/lib/twisty_puzzles/cube_move_parser.rb +75 -76
  45. data/lib/twisty_puzzles/cube_print_helper.rb +133 -133
  46. data/lib/twisty_puzzles/cube_state.rb +80 -81
  47. data/lib/twisty_puzzles/move_type_creator.rb +17 -18
  48. data/lib/twisty_puzzles/parser.rb +176 -179
  49. data/lib/twisty_puzzles/part_cycle_factory.rb +39 -42
  50. data/lib/twisty_puzzles/puzzle.rb +16 -17
  51. data/lib/twisty_puzzles/reversible_applyable.rb +24 -25
  52. data/lib/twisty_puzzles/rotation.rb +76 -75
  53. data/lib/twisty_puzzles/skewb_direction.rb +14 -15
  54. data/lib/twisty_puzzles/skewb_move.rb +49 -49
  55. data/lib/twisty_puzzles/skewb_move_parser.rb +51 -51
  56. data/lib/twisty_puzzles/skewb_notation.rb +121 -118
  57. data/lib/twisty_puzzles/skewb_state.rb +120 -121
  58. data/lib/twisty_puzzles/state_helper.rb +20 -21
  59. data/lib/twisty_puzzles/sticker_cycle.rb +43 -44
  60. data/lib/twisty_puzzles/utils.rb +3 -0
  61. data/lib/twisty_puzzles/version.rb +3 -1
  62. metadata +30 -10
@@ -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}(#{@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}(#{face_symbol}, #{@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..].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