twisty_puzzles 0.0.1

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/LICENSE +21 -0
  5. data/README.md +32 -0
  6. data/ext/twisty_puzzles/native/extconf.rb +5 -0
  7. data/lib/twisty_puzzles/abstract_direction.rb +54 -0
  8. data/lib/twisty_puzzles/abstract_move.rb +170 -0
  9. data/lib/twisty_puzzles/abstract_move_parser.rb +45 -0
  10. data/lib/twisty_puzzles/algorithm.rb +155 -0
  11. data/lib/twisty_puzzles/algorithm_transformation.rb +33 -0
  12. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +78 -0
  13. data/lib/twisty_puzzles/cancellation_helper.rb +165 -0
  14. data/lib/twisty_puzzles/color_scheme.rb +174 -0
  15. data/lib/twisty_puzzles/commutator.rb +118 -0
  16. data/lib/twisty_puzzles/compiled_algorithm.rb +48 -0
  17. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +67 -0
  18. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +28 -0
  19. data/lib/twisty_puzzles/coordinate.rb +318 -0
  20. data/lib/twisty_puzzles/cube.rb +660 -0
  21. data/lib/twisty_puzzles/cube_constants.rb +53 -0
  22. data/lib/twisty_puzzles/cube_direction.rb +27 -0
  23. data/lib/twisty_puzzles/cube_move.rb +384 -0
  24. data/lib/twisty_puzzles/cube_move_parser.rb +100 -0
  25. data/lib/twisty_puzzles/cube_print_helper.rb +160 -0
  26. data/lib/twisty_puzzles/cube_state.rb +113 -0
  27. data/lib/twisty_puzzles/letter_scheme.rb +72 -0
  28. data/lib/twisty_puzzles/move_type_creator.rb +27 -0
  29. data/lib/twisty_puzzles/parser.rb +222 -0
  30. data/lib/twisty_puzzles/part_cycle_factory.rb +59 -0
  31. data/lib/twisty_puzzles/puzzle.rb +26 -0
  32. data/lib/twisty_puzzles/reversible_applyable.rb +37 -0
  33. data/lib/twisty_puzzles/rotation.rb +105 -0
  34. data/lib/twisty_puzzles/skewb_direction.rb +24 -0
  35. data/lib/twisty_puzzles/skewb_move.rb +59 -0
  36. data/lib/twisty_puzzles/skewb_move_parser.rb +73 -0
  37. data/lib/twisty_puzzles/skewb_notation.rb +147 -0
  38. data/lib/twisty_puzzles/skewb_state.rb +163 -0
  39. data/lib/twisty_puzzles/state_helper.rb +32 -0
  40. data/lib/twisty_puzzles/sticker_cycle.rb +70 -0
  41. data/lib/twisty_puzzles/twisty_puzzles_error.rb +6 -0
  42. data/lib/twisty_puzzles/utils/array_helper.rb +109 -0
  43. data/lib/twisty_puzzles/utils/string_helper.rb +26 -0
  44. data/lib/twisty_puzzles/utils.rb +7 -0
  45. data/lib/twisty_puzzles/version.rb +3 -0
  46. data/lib/twisty_puzzles.rb +5 -0
  47. metadata +249 -0
@@ -0,0 +1,660 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/cube_constants'
4
+ require 'twisty_puzzles/coordinate'
5
+ require 'twisty_puzzles/utils/array_helper'
6
+
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
31
+
32
+ @face_symbols = face_symbols
33
+ @piece_index = piece_index
34
+ end
35
+
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
50
+ end
51
+
52
+ def self.min_cube_size
53
+ 2
54
+ end
55
+
56
+ def self.max_cube_size
57
+ Float::INFINITY
58
+ end
59
+
60
+ def self.exists_on_even_cube_sizes?
61
+ true
62
+ end
63
+
64
+ def self.exists_on_odd_cube_sizes?
65
+ true
66
+ end
67
+
68
+ def base_index_on_face(cube_size, incarnation_index)
69
+ base_index_on_other_face(solved_face, cube_size, incarnation_index)
70
+ end
71
+
72
+ def self.for_face_symbols_internal(face_symbols)
73
+ raise unless face_symbols.length == self::FACES
74
+
75
+ find_only(self::ELEMENTS) { |e| e.face_symbols == face_symbols }
76
+ end
77
+
78
+ def self.for_face_symbols(face_symbols)
79
+ for_face_symbols_internal(face_symbols)
80
+ end
81
+
82
+ def self.for_index(index)
83
+ self::ELEMENTS[index]
84
+ end
85
+
86
+ def <=>(other)
87
+ @piece_index <=> other.piece_index
88
+ end
89
+
90
+ def eql?(other)
91
+ self.class.equal?(other.class) && @piece_index == other.piece_index
92
+ end
93
+
94
+ alias == eql?
95
+
96
+ def hash
97
+ @hash ||= [self.class, @piece_index].hash
98
+ end
99
+
100
+ def inspect
101
+ self.class.to_s + '(' + @face_symbols.map(&:to_s).join(', ') + ')'
102
+ end
103
+
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
110
+
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
115
+
116
+ rotate_by(index)
117
+ end
118
+
119
+ def rotate_face_up(face)
120
+ rotate_face_symbol_up(face.face_symbol)
121
+ end
122
+
123
+ def rotate_by(number)
124
+ self.class.for_face_symbols(@face_symbols.rotate(number))
125
+ end
126
+
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
132
+
133
+ def rotations
134
+ (0...@face_symbols.length).map { |i| rotate_by(i) }
135
+ end
136
+
137
+ def self.create_for_face_symbols(face_symbols)
138
+ new(face_symbols)
139
+ end
140
+
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
148
+
149
+ # Only overridden by moveable centers, but returns self for convenience.
150
+ def corresponding_part
151
+ self
152
+ end
153
+
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
158
+
159
+ def solved_coordinate(cube_size, incarnation_index = 0)
160
+ Coordinate.solved_position(self, cube_size, incarnation_index)
161
+ end
162
+
163
+ def faces
164
+ @faces ||= @face_symbols.map { |f| Face.for_face_symbol(f) }
165
+ end
166
+ end
167
+
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
171
+
172
+ def self.min_cube_size
173
+ 3
174
+ end
175
+
176
+ def self.exists_on_even_cube_sizes?
177
+ false
178
+ end
179
+
180
+ def self.for_face_symbol(face_symbol)
181
+ for_face_symbols([face_symbol])
182
+ end
183
+
184
+ def self.valid?(_face_symbols)
185
+ true
186
+ end
187
+
188
+ ELEMENTS = generate_parts
189
+
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
194
+
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
201
+
202
+ def opposite
203
+ Face.for_face_symbol(opposite_face_symbol(face_symbol))
204
+ end
205
+
206
+ def same_axis?(other)
207
+ axis_priority == other.axis_priority
208
+ end
209
+
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
217
+
218
+ to_priority = to_face.axis_priority
219
+ if axis_priority < to_priority
220
+ to_priority - 1
221
+ else
222
+ to_priority
223
+ end
224
+ end
225
+
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
231
+
232
+ def canonical_axis_face?
233
+ close_to_smaller_indices?
234
+ end
235
+
236
+ def name
237
+ @name ||= FACE_NAMES[ELEMENTS.index(self)]
238
+ end
239
+
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
243
+
244
+ ELEMENTS[index]
245
+ end
246
+
247
+ def face_symbol
248
+ @face_symbols[0]
249
+ end
250
+
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
263
+
264
+ def clockwise_neighbor_after(neighbor)
265
+ raise ArgumentError if same_axis?(neighbor)
266
+
267
+ @neighbors[(@neighbors.index(neighbor) + 1) % @neighbors.length]
268
+ end
269
+
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?
280
+ end
281
+ direction = rotation_direction_to(other)
282
+ Algorithm.move(Rotation.new(axis_face, direction))
283
+ end
284
+ end
285
+
286
+ FACE_SYMBOLS.map { |s| const_set(s, for_face_symbol(s)) }
287
+
288
+ def clockwise_corners
289
+ neighbors.zip(neighbors.rotate).map { |a, b| Corner.between_faces([self, a, b]) }
290
+ end
291
+
292
+ private
293
+
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
303
+ end
304
+
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
316
+ end
317
+
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
322
+
323
+ def self.min_cube_size
324
+ 4
325
+ end
326
+
327
+ def self.valid?(face_symbols)
328
+ self::CORRESPONDING_PART_CLASS.valid?(face_symbols)
329
+ end
330
+
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
341
+
342
+ def self.create_for_face_symbols(face_symbols)
343
+ new(self::CORRESPONDING_PART_CLASS.create_for_face_symbols(face_symbols))
344
+ end
345
+
346
+ def face_symbol
347
+ @face_symbols[0]
348
+ end
349
+
350
+ def eql?(other)
351
+ self.class.equal?(other.class) && face_symbol == other.face_symbol &&
352
+ @corresponding_part == other.corresponding_part
353
+ end
354
+
355
+ def initialize(corresponding_part, piece_index)
356
+ unless corresponding_part.is_a?(Part)
357
+ raise "Invalid corresponding part #{corresponding_part}."
358
+ end
359
+
360
+ super([corresponding_part.face_symbols[0]], piece_index)
361
+ @corresponding_part = corresponding_part
362
+ end
363
+
364
+ alias == eql?
365
+
366
+ attr_reader :corresponding_part
367
+
368
+ def inspect
369
+ self.class.to_s + '(' + face_symbol.to_s + ', ' + @corresponding_part.inspect + ')'
370
+ end
371
+
372
+ def rotate_by(_number)
373
+ self
374
+ end
375
+
376
+ def neighbor?(other)
377
+ face_symbol == other.face_symbol
378
+ end
379
+
380
+ def neighbors
381
+ self.class::ELEMENTS.select { |p| neighbor?(p) }
382
+ end
383
+
384
+ def self.generate_parts
385
+ self::CORRESPONDING_PART_CLASS::ELEMENTS.map { |p| new(p, p.piece_index) }
386
+ end
387
+ end
388
+
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
394
+ end
395
+
396
+ # Represents one edge or the position of one edge on the cube.
397
+ class Edge < Part
398
+ extend EdgeLike
399
+ FACES = 2
400
+
401
+ ELEMENTS = generate_parts
402
+
403
+ def self.min_cube_size
404
+ 3
405
+ end
406
+
407
+ def self.max_cube_size
408
+ 3
409
+ end
410
+
411
+ def self.exists_on_even_cube_sizes?
412
+ false
413
+ end
414
+
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
419
+
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
424
+ end
425
+
426
+ # Represents one midge or the position of one midge on the cube.
427
+ class Midge < Part
428
+ extend EdgeLike
429
+ FACES = 2
430
+
431
+ ELEMENTS = generate_parts
432
+
433
+ def self.min_cube_size
434
+ 5
435
+ end
436
+
437
+ def self.exists_on_even_cube_sizes?
438
+ false
439
+ end
440
+
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
445
+
446
+ def num_incarnations(cube_size)
447
+ cube_size >= 5 && cube_size.odd? ? 1 : 0
448
+ end
449
+ end
450
+
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
456
+
457
+ ELEMENTS = generate_parts
458
+
459
+ def self.min_cube_size
460
+ 4
461
+ end
462
+
463
+ def self.exists_on_odd_cube_sizes?
464
+ false
465
+ end
466
+
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
470
+
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
477
+
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
488
+
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
495
+
496
+ private_class_method :for_corner_face_symbols
497
+
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
507
+ end
508
+
509
+ def rotations
510
+ [self]
511
+ end
512
+
513
+ def rotate_by(_number)
514
+ self
515
+ end
516
+
517
+ def num_incarnations(cube_size)
518
+ [cube_size / 2 - 1, 0].max
519
+ end
520
+
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
528
+ end
529
+
530
+ # Represents one corner or the position of one corner on the cube.
531
+ class Corner < Part
532
+ FACES = 3
533
+
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
541
+
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
546
+
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]])
551
+ end
552
+ end
553
+
554
+ def self.valid_between_faces?(faces)
555
+ valid?(faces.map(&:face_symbol))
556
+ end
557
+
558
+ def self.between_faces(faces)
559
+ for_face_symbols(faces.map(&:face_symbol))
560
+ end
561
+
562
+ def self.valid?(face_symbols)
563
+ face_symbols.combination(2).all? { |e| Edge.valid?(e) } && valid_chirality?(face_symbols)
564
+ end
565
+
566
+ ELEMENTS = generate_parts
567
+
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
573
+
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
578
+
579
+ rotate_by(3 - index)
580
+ end
581
+
582
+ def diagonal_opposite
583
+ @diagonal_opposite ||=
584
+ Corner.for_face_symbols(face_symbols.map { |f| opposite_face_symbol(f) })
585
+ end
586
+
587
+ def rotate_other_face_up(face)
588
+ rotate_other_face_symbol_up(face.face_symbol)
589
+ end
590
+
591
+ def common_edge_with?(other)
592
+ common_faces(other) == 2
593
+ end
594
+
595
+ def common_faces(other)
596
+ raise TypeError unless other.is_a?(Corner)
597
+
598
+ (@face_symbols & other.face_symbols).length
599
+ end
600
+
601
+ def adjacent_edges
602
+ @adjacent_edges ||= @face_symbols.combination(2).map { |e| Edge.for_face_symbols(e) }
603
+ end
604
+
605
+ def adjacent_faces
606
+ @adjacent_faces ||= @face_symbols.map { |f| Face.for_face_symbol(f) }
607
+ end
608
+
609
+ def num_incarnations(cube_size)
610
+ cube_size >= 2 ? 1 : 0
611
+ end
612
+
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
617
+ end
618
+
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
623
+
624
+ def num_incarnations(cube_size)
625
+ [cube_size / 2 - 1, 0].max
626
+ end
627
+
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
632
+ end
633
+
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
638
+
639
+ def self.min_cube_size
640
+ 5
641
+ end
642
+
643
+ def self.exists_on_even_cube_sizes?
644
+ false
645
+ end
646
+
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
654
+
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]
658
+ end
659
+ end
660
+ end