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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/LICENSE +21 -0
- data/README.md +32 -0
- data/ext/twisty_puzzles/native/extconf.rb +5 -0
- data/lib/twisty_puzzles/abstract_direction.rb +54 -0
- data/lib/twisty_puzzles/abstract_move.rb +170 -0
- data/lib/twisty_puzzles/abstract_move_parser.rb +45 -0
- data/lib/twisty_puzzles/algorithm.rb +155 -0
- data/lib/twisty_puzzles/algorithm_transformation.rb +33 -0
- data/lib/twisty_puzzles/axis_face_and_direction_move.rb +78 -0
- data/lib/twisty_puzzles/cancellation_helper.rb +165 -0
- data/lib/twisty_puzzles/color_scheme.rb +174 -0
- data/lib/twisty_puzzles/commutator.rb +118 -0
- data/lib/twisty_puzzles/compiled_algorithm.rb +48 -0
- data/lib/twisty_puzzles/compiled_cube_algorithm.rb +67 -0
- data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +28 -0
- data/lib/twisty_puzzles/coordinate.rb +318 -0
- data/lib/twisty_puzzles/cube.rb +660 -0
- data/lib/twisty_puzzles/cube_constants.rb +53 -0
- data/lib/twisty_puzzles/cube_direction.rb +27 -0
- data/lib/twisty_puzzles/cube_move.rb +384 -0
- data/lib/twisty_puzzles/cube_move_parser.rb +100 -0
- data/lib/twisty_puzzles/cube_print_helper.rb +160 -0
- data/lib/twisty_puzzles/cube_state.rb +113 -0
- data/lib/twisty_puzzles/letter_scheme.rb +72 -0
- data/lib/twisty_puzzles/move_type_creator.rb +27 -0
- data/lib/twisty_puzzles/parser.rb +222 -0
- data/lib/twisty_puzzles/part_cycle_factory.rb +59 -0
- data/lib/twisty_puzzles/puzzle.rb +26 -0
- data/lib/twisty_puzzles/reversible_applyable.rb +37 -0
- data/lib/twisty_puzzles/rotation.rb +105 -0
- data/lib/twisty_puzzles/skewb_direction.rb +24 -0
- data/lib/twisty_puzzles/skewb_move.rb +59 -0
- data/lib/twisty_puzzles/skewb_move_parser.rb +73 -0
- data/lib/twisty_puzzles/skewb_notation.rb +147 -0
- data/lib/twisty_puzzles/skewb_state.rb +163 -0
- data/lib/twisty_puzzles/state_helper.rb +32 -0
- data/lib/twisty_puzzles/sticker_cycle.rb +70 -0
- data/lib/twisty_puzzles/twisty_puzzles_error.rb +6 -0
- data/lib/twisty_puzzles/utils/array_helper.rb +109 -0
- data/lib/twisty_puzzles/utils/string_helper.rb +26 -0
- data/lib/twisty_puzzles/utils.rb +7 -0
- data/lib/twisty_puzzles/version.rb +3 -0
- data/lib/twisty_puzzles.rb +5 -0
- 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
|