twisty_puzzles 0.0.1

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