twisty_puzzles 0.0.1 → 0.0.2
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 +4 -4
- data/CHANGELOG.md +9 -1
- data/lib/twisty_puzzles.rb +37 -0
- data/lib/twisty_puzzles/abstract_direction.rb +38 -39
- data/lib/twisty_puzzles/abstract_move_parser.rb +32 -33
- data/lib/twisty_puzzles/algorithm.rb +112 -113
- data/lib/twisty_puzzles/algorithm_transformation.rb +19 -21
- data/lib/twisty_puzzles/axis_face_and_direction_move.rb +55 -56
- data/lib/twisty_puzzles/cancellation_helper.rb +124 -125
- data/lib/twisty_puzzles/commutator.rb +79 -80
- data/lib/twisty_puzzles/compiled_algorithm.rb +31 -32
- data/lib/twisty_puzzles/compiled_cube_algorithm.rb +49 -50
- data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +18 -19
- data/lib/twisty_puzzles/coordinate.rb +245 -246
- data/lib/twisty_puzzles/cube.rb +494 -495
- data/lib/twisty_puzzles/cube_constants.rb +40 -41
- data/lib/twisty_puzzles/cube_direction.rb +15 -18
- data/lib/twisty_puzzles/cube_move.rb +289 -290
- data/lib/twisty_puzzles/cube_move_parser.rb +75 -76
- data/lib/twisty_puzzles/cube_print_helper.rb +132 -133
- data/lib/twisty_puzzles/cube_state.rb +80 -81
- data/lib/twisty_puzzles/move_type_creator.rb +17 -18
- data/lib/twisty_puzzles/parser.rb +176 -179
- data/lib/twisty_puzzles/part_cycle_factory.rb +39 -42
- data/lib/twisty_puzzles/puzzle.rb +16 -17
- data/lib/twisty_puzzles/reversible_applyable.rb +24 -25
- data/lib/twisty_puzzles/rotation.rb +74 -75
- data/lib/twisty_puzzles/skewb_direction.rb +14 -15
- data/lib/twisty_puzzles/skewb_move.rb +48 -49
- data/lib/twisty_puzzles/skewb_move_parser.rb +50 -51
- data/lib/twisty_puzzles/skewb_notation.rb +115 -118
- data/lib/twisty_puzzles/skewb_state.rb +120 -121
- data/lib/twisty_puzzles/state_helper.rb +20 -21
- data/lib/twisty_puzzles/sticker_cycle.rb +43 -44
- data/lib/twisty_puzzles/utils.rb +3 -0
- data/lib/twisty_puzzles/version.rb +3 -1
- metadata +3 -3
data/lib/twisty_puzzles/cube.rb
CHANGED
@@ -5,656 +5,655 @@ require 'twisty_puzzles/coordinate'
|
|
5
5
|
require 'twisty_puzzles/utils/array_helper'
|
6
6
|
|
7
7
|
module TwistyPuzzles
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
31
|
+
@face_symbols = face_symbols
|
32
|
+
@piece_index = piece_index
|
33
|
+
end
|
59
34
|
|
60
|
-
|
61
|
-
true
|
62
|
-
end
|
35
|
+
attr_reader :piece_index, :face_symbols
|
63
36
|
|
64
|
-
|
65
|
-
|
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
|
-
|
69
|
-
|
70
|
-
end
|
48
|
+
parts.freeze
|
49
|
+
end
|
71
50
|
|
72
|
-
|
73
|
-
|
51
|
+
def self.min_cube_size
|
52
|
+
2
|
53
|
+
end
|
74
54
|
|
75
|
-
|
76
|
-
|
55
|
+
def self.max_cube_size
|
56
|
+
Float::INFINITY
|
57
|
+
end
|
77
58
|
|
78
|
-
|
79
|
-
|
80
|
-
|
59
|
+
def self.exists_on_even_cube_sizes?
|
60
|
+
true
|
61
|
+
end
|
81
62
|
|
82
|
-
|
83
|
-
|
84
|
-
|
63
|
+
def self.exists_on_odd_cube_sizes?
|
64
|
+
true
|
65
|
+
end
|
85
66
|
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
end
|
71
|
+
def self.for_face_symbols_internal(face_symbols)
|
72
|
+
raise unless face_symbols.length == self::FACES
|
93
73
|
|
94
|
-
|
74
|
+
find_only(self::ELEMENTS) { |e| e.face_symbols == face_symbols }
|
75
|
+
end
|
95
76
|
|
96
|
-
|
97
|
-
|
98
|
-
|
77
|
+
def self.for_face_symbols(face_symbols)
|
78
|
+
for_face_symbols_internal(face_symbols)
|
79
|
+
end
|
99
80
|
|
100
|
-
|
101
|
-
|
102
|
-
|
81
|
+
def self.for_index(index)
|
82
|
+
self::ELEMENTS[index]
|
83
|
+
end
|
103
84
|
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
117
|
-
end
|
93
|
+
alias == eql?
|
118
94
|
|
119
|
-
|
120
|
-
|
121
|
-
|
95
|
+
def hash
|
96
|
+
@hash ||= [self.class, @piece_index].hash
|
97
|
+
end
|
122
98
|
|
123
|
-
|
124
|
-
|
125
|
-
|
99
|
+
def inspect
|
100
|
+
self.class.to_s + '(' + @face_symbols.map(&:to_s).join(', ') + ')'
|
101
|
+
end
|
126
102
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
139
|
-
end
|
115
|
+
rotate_by(index)
|
116
|
+
end
|
140
117
|
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
end
|
122
|
+
def rotate_by(number)
|
123
|
+
self.class.for_face_symbols(@face_symbols.rotate(number))
|
124
|
+
end
|
153
125
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
132
|
+
def rotations
|
133
|
+
(0...@face_symbols.length).map { |i| rotate_by(i) }
|
134
|
+
end
|
162
135
|
|
163
|
-
|
164
|
-
|
165
|
-
end
|
136
|
+
def self.create_for_face_symbols(face_symbols)
|
137
|
+
new(face_symbols)
|
166
138
|
end
|
167
139
|
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
148
|
+
# Only overridden by moveable centers, but returns self for convenience.
|
149
|
+
def corresponding_part
|
150
|
+
self
|
151
|
+
end
|
175
152
|
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
158
|
+
def solved_coordinate(cube_size, incarnation_index = 0)
|
159
|
+
Coordinate.solved_position(self, cube_size, incarnation_index)
|
160
|
+
end
|
183
161
|
|
184
|
-
|
185
|
-
|
186
|
-
|
162
|
+
def faces
|
163
|
+
@faces ||= @face_symbols.map { |f| Face.for_face_symbol(f) }
|
164
|
+
end
|
165
|
+
end
|
187
166
|
|
188
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
end
|
171
|
+
def self.min_cube_size
|
172
|
+
3
|
173
|
+
end
|
194
174
|
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
179
|
+
def self.for_face_symbol(face_symbol)
|
180
|
+
for_face_symbols([face_symbol])
|
181
|
+
end
|
205
182
|
|
206
|
-
|
207
|
-
|
208
|
-
|
183
|
+
def self.valid?(_face_symbols)
|
184
|
+
true
|
185
|
+
end
|
209
186
|
|
210
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
199
|
+
end
|
225
200
|
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
233
|
-
|
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
|
-
|
237
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
245
|
-
|
231
|
+
def canonical_axis_face?
|
232
|
+
close_to_smaller_indices?
|
233
|
+
end
|
246
234
|
|
247
|
-
|
248
|
-
|
249
|
-
|
235
|
+
def name
|
236
|
+
@name ||= FACE_NAMES[ELEMENTS.index(self)]
|
237
|
+
end
|
250
238
|
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
-
|
265
|
-
|
243
|
+
ELEMENTS[index]
|
244
|
+
end
|
266
245
|
|
267
|
-
|
268
|
-
|
246
|
+
def face_symbol
|
247
|
+
@face_symbols[0]
|
248
|
+
end
|
269
249
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
-
|
282
|
-
|
258
|
+
ordered_partial_neighbors = sort_partial_neighbors(partial_neighbors)
|
259
|
+
ordered_partial_neighbors + ordered_partial_neighbors.map(&:opposite)
|
283
260
|
end
|
284
|
-
|
285
|
-
|
286
|
-
FACE_SYMBOLS.map { |s| const_set(s, for_face_symbol(s)) }
|
261
|
+
end
|
287
262
|
|
288
|
-
|
289
|
-
|
290
|
-
end
|
263
|
+
def clockwise_neighbor_after(neighbor)
|
264
|
+
raise ArgumentError if same_axis?(neighbor)
|
291
265
|
|
292
|
-
|
266
|
+
@neighbors[(@neighbors.index(neighbor) + 1) % @neighbors.length]
|
267
|
+
end
|
293
268
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
-
|
319
|
-
# center on the cube.
|
320
|
-
class MoveableCenter < Part
|
321
|
-
FACES = 1
|
291
|
+
private
|
322
292
|
|
323
|
-
|
324
|
-
|
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
|
-
|
328
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
-
|
343
|
-
|
344
|
-
|
322
|
+
def self.min_cube_size
|
323
|
+
4
|
324
|
+
end
|
345
325
|
|
346
|
-
|
347
|
-
|
348
|
-
|
326
|
+
def self.valid?(face_symbols)
|
327
|
+
self::CORRESPONDING_PART_CLASS.valid?(face_symbols)
|
328
|
+
end
|
349
329
|
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
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
|
-
|
361
|
-
|
362
|
-
|
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
|
-
|
345
|
+
def face_symbol
|
346
|
+
@face_symbols[0]
|
347
|
+
end
|
365
348
|
|
366
|
-
|
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
|
-
|
369
|
-
|
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
|
-
|
373
|
-
|
374
|
-
|
359
|
+
super([corresponding_part.face_symbols[0]], piece_index)
|
360
|
+
@corresponding_part = corresponding_part
|
361
|
+
end
|
375
362
|
|
376
|
-
|
377
|
-
face_symbol == other.face_symbol
|
378
|
-
end
|
363
|
+
alias == eql?
|
379
364
|
|
380
|
-
|
381
|
-
self.class::ELEMENTS.select { |p| neighbor?(p) }
|
382
|
-
end
|
365
|
+
attr_reader :corresponding_part
|
383
366
|
|
384
|
-
|
385
|
-
|
386
|
-
end
|
367
|
+
def inspect
|
368
|
+
self.class.to_s + '(' + face_symbol.to_s + ', ' + @corresponding_part.inspect + ')'
|
387
369
|
end
|
388
370
|
|
389
|
-
|
390
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
FACES = 2
|
375
|
+
def neighbor?(other)
|
376
|
+
face_symbol == other.face_symbol
|
377
|
+
end
|
400
378
|
|
401
|
-
|
379
|
+
def neighbors
|
380
|
+
self.class::ELEMENTS.select { |p| neighbor?(p) }
|
381
|
+
end
|
402
382
|
|
403
|
-
|
404
|
-
|
405
|
-
|
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
|
-
|
408
|
-
|
409
|
-
|
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
|
-
|
412
|
-
|
413
|
-
|
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
|
-
|
416
|
-
def num_incarnations(cube_size)
|
417
|
-
cube_size == 3 ? 1 : 0
|
418
|
-
end
|
400
|
+
ELEMENTS = generate_parts
|
419
401
|
|
420
|
-
|
421
|
-
|
422
|
-
[0, 1]
|
423
|
-
end
|
402
|
+
def self.min_cube_size
|
403
|
+
3
|
424
404
|
end
|
425
405
|
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
FACES = 2
|
406
|
+
def self.max_cube_size
|
407
|
+
3
|
408
|
+
end
|
430
409
|
|
431
|
-
|
410
|
+
def self.exists_on_even_cube_sizes?
|
411
|
+
false
|
412
|
+
end
|
432
413
|
|
433
|
-
|
434
|
-
|
435
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
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
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
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
|
-
|
447
|
-
cube_size >= 5 && cube_size.odd? ? 1 : 0
|
448
|
-
end
|
449
|
-
end
|
430
|
+
ELEMENTS = generate_parts
|
450
431
|
|
451
|
-
|
452
|
-
|
453
|
-
|
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
|
-
|
436
|
+
def self.exists_on_even_cube_sizes?
|
437
|
+
false
|
438
|
+
end
|
458
439
|
|
459
|
-
|
460
|
-
|
461
|
-
|
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
|
-
|
464
|
-
|
465
|
-
|
445
|
+
def num_incarnations(cube_size)
|
446
|
+
cube_size >= 5 && cube_size.odd? ? 1 : 0
|
447
|
+
end
|
448
|
+
end
|
466
449
|
|
467
|
-
|
468
|
-
|
469
|
-
|
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
|
-
|
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
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
490
|
-
|
491
|
-
|
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
|
-
|
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
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
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
|
-
|
510
|
-
|
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
|
-
|
514
|
-
|
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
|
-
|
518
|
-
[cube_size / 2 - 1, 0].max
|
519
|
-
end
|
495
|
+
private_class_method :for_corner_face_symbols
|
520
496
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
-
|
531
|
-
|
532
|
-
|
508
|
+
def rotations
|
509
|
+
[self]
|
510
|
+
end
|
533
511
|
|
534
|
-
|
535
|
-
|
536
|
-
|
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
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
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
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
533
|
+
def self.create_for_face_symbols(face_symbols)
|
534
|
+
piece_candidates =
|
535
|
+
face_symbols[1..-1].permutation.map do |cs|
|
536
|
+
new([face_symbols[0]] + cs)
|
551
537
|
end
|
552
|
-
|
538
|
+
find_only(piece_candidates, &:valid?)
|
539
|
+
end
|
553
540
|
|
554
|
-
|
555
|
-
|
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
|
-
|
559
|
-
|
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
|
-
|
563
|
-
|
564
|
-
|
553
|
+
def self.valid_between_faces?(faces)
|
554
|
+
valid?(faces.map(&:face_symbol))
|
555
|
+
end
|
565
556
|
|
566
|
-
|
557
|
+
def self.between_faces(faces)
|
558
|
+
for_face_symbols(faces.map(&:face_symbol))
|
559
|
+
end
|
567
560
|
|
568
|
-
|
569
|
-
|
570
|
-
|
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
|
-
|
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
|
-
|
580
|
-
|
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
|
-
|
583
|
-
|
584
|
-
|
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
|
-
|
588
|
-
|
589
|
-
end
|
578
|
+
rotate_by(3 - index)
|
579
|
+
end
|
590
580
|
|
591
|
-
|
592
|
-
|
593
|
-
|
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
|
-
|
596
|
-
|
586
|
+
def rotate_other_face_up(face)
|
587
|
+
rotate_other_face_symbol_up(face.face_symbol)
|
588
|
+
end
|
597
589
|
|
598
|
-
|
599
|
-
|
590
|
+
def common_edge_with?(other)
|
591
|
+
common_faces(other) == 2
|
592
|
+
end
|
600
593
|
|
601
|
-
|
602
|
-
|
603
|
-
end
|
594
|
+
def common_faces(other)
|
595
|
+
raise TypeError unless other.is_a?(Corner)
|
604
596
|
|
605
|
-
|
606
|
-
|
607
|
-
end
|
597
|
+
(@face_symbols & other.face_symbols).length
|
598
|
+
end
|
608
599
|
|
609
|
-
|
610
|
-
|
611
|
-
|
600
|
+
def adjacent_edges
|
601
|
+
@adjacent_edges ||= @face_symbols.combination(2).map { |e| Edge.for_face_symbols(e) }
|
602
|
+
end
|
612
603
|
|
613
|
-
|
614
|
-
|
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
|
-
|
620
|
-
|
621
|
-
|
622
|
-
ELEMENTS = generate_parts
|
608
|
+
def num_incarnations(cube_size)
|
609
|
+
cube_size >= 2 ? 1 : 0
|
610
|
+
end
|
623
611
|
|
624
|
-
|
625
|
-
|
626
|
-
|
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
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
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
|
-
#
|
635
|
-
|
636
|
-
|
637
|
-
|
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
|
-
|
640
|
-
|
641
|
-
|
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
|
-
|
644
|
-
|
645
|
-
|
638
|
+
def self.min_cube_size
|
639
|
+
5
|
640
|
+
end
|
646
641
|
|
647
|
-
|
648
|
-
|
649
|
-
|
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
|
-
|
656
|
-
|
657
|
-
|
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
|