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,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/utils/array_helper'
4
+
5
+ module TwistyPuzzles
6
+
7
+ # Various constants about the cube.
8
+ module CubeConstants
9
+ include Utils::ArrayHelper
10
+
11
+ # The order determines the priority of the faces.
12
+ FACE_SYMBOLS = %i[U F R L B D].freeze
13
+ OPPOSITE_FACE_SYMBOLS = [%i[U D], %i[F B], %i[R L]].freeze
14
+ raise unless FACE_SYMBOLS.sort == OPPOSITE_FACE_SYMBOLS.flatten.sort
15
+
16
+ FACE_NAMES = FACE_SYMBOLS.map(&:to_s).freeze
17
+ ALPHABET_SIZE = 24
18
+ # Stickers on each Skewb face.
19
+ SKEWB_STICKERS = 5
20
+ CHIRALITY_FACE_SYMBOLS = %i[U R F].freeze
21
+
22
+ def opposite_face_symbol(face_symbol)
23
+ candidates = OPPOSITE_FACE_SYMBOLS.select { |ss| ss.include?(face_symbol) }
24
+ raise if candidates.length > 1
25
+ raise ArgumentError, "Invalid face symbol #{face_symbol}." if candidates.empty?
26
+
27
+ only(only(candidates).reject { |s| s == face_symbol })
28
+ end
29
+
30
+ def chirality_canonical_face_symbol(face_symbol)
31
+ if CHIRALITY_FACE_SYMBOLS.include?(face_symbol)
32
+ face_symbol
33
+ else
34
+ opposite_face_symbol(face_symbol)
35
+ end
36
+ end
37
+
38
+ def valid_chirality?(face_symbols)
39
+ # To make it comparable to our CHIRALITY_FACE_SYMBOLS, we switch each face used in c
40
+ # different from the ones used in the CHIRALITY_FACE_SYMBOLS for the opposite face.
41
+ canonical_face_symbols = face_symbols.map { |f| chirality_canonical_face_symbol(f) }
42
+
43
+ # Each time we swap a face for the opposite, the chirality direction should be inverted.
44
+ no_swapped_face_symbols = canonical_face_symbols.zip(face_symbols).count { |a, b| a != b }
45
+ inverted = no_swapped_face_symbols.odd?
46
+ inverted_face_symbols = inverted ? canonical_face_symbols.reverse : canonical_face_symbols
47
+
48
+ # If the corner is not equal modulo rotation to CHIRALITY_FACE_SYMBOLS after this
49
+ # transformation, the original corner had a bad chirality.
50
+ turned_equals?(inverted_face_symbols, CHIRALITY_FACE_SYMBOLS)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_direction'
4
+
5
+ module TwistyPuzzles
6
+
7
+ # Represents the direction of a cube move or rotation.
8
+ class CubeDirection < AbstractDirection
9
+ NUM_DIRECTIONS = 4
10
+ NON_ZERO_DIRECTIONS = (1...NUM_DIRECTIONS).map { |d| new(d) }.freeze
11
+ ALL_DIRECTIONS = Array.new(NUM_DIRECTIONS) { |d| new(d) }.freeze
12
+ ZERO = new(0)
13
+ FORWARD = new(1)
14
+ DOUBLE = new(2)
15
+ BACKWARD = new(3)
16
+
17
+ def name
18
+ SIMPLE_DIRECTION_NAMES[@value]
19
+ end
20
+
21
+ def double_move?
22
+ @value == 2
23
+ end
24
+ end
25
+ end
26
+
27
+
@@ -0,0 +1,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_move'
4
+ require 'twisty_puzzles/axis_face_and_direction_move'
5
+ require 'twisty_puzzles/algorithm'
6
+ require 'twisty_puzzles/puzzle'
7
+
8
+ module TwistyPuzzles
9
+
10
+ # Helper class to print various types of M slice moves.
11
+ module MSlicePrintHelper
12
+ def to_s
13
+ use_face = AbstractMove::SLICE_NAMES.key?(@axis_face)
14
+ axis_face = use_face ? @axis_face : @axis_face.opposite
15
+ direction = use_face ? @direction : @direction.inverse
16
+ slice_name = AbstractMove::SLICE_NAMES[axis_face]
17
+ "#{slice_name}#{direction.name}"
18
+ end
19
+ end
20
+
21
+ # Base class for cube moves.
22
+ class CubeMove < AxisFaceAndDirectionMove
23
+ def puzzles
24
+ [Puzzle::NXN_CUBE]
25
+ end
26
+ end
27
+
28
+ # A fat M slice move that moves everything but the outer layers.
29
+ class FatMSliceMove < CubeMove
30
+ include MSlicePrintHelper
31
+
32
+ def prepend_rotation(_other, _cube_size)
33
+ nil
34
+ end
35
+
36
+ def prepend_fat_m_slice_move(other, _cube_size)
37
+ return unless same_axis?(other)
38
+
39
+ other_direction = other.translated_direction(@axis_face)
40
+ Algorithm.move(FatMSliceMove.new(@axis_face, @direction + other_direction))
41
+ end
42
+
43
+ def prepend_fat_move(other, cube_size)
44
+ # Note that changing the order is safe because that method returns nil if no cancellation
45
+ # can be performed.
46
+ other.prepend_fat_m_slice_move(self, cube_size)
47
+ end
48
+
49
+ def prepend_slice_move(_other, _cube_size)
50
+ nil
51
+ end
52
+
53
+ def slice_move?
54
+ true
55
+ end
56
+
57
+ def equivalent_internal?(other, cube_size)
58
+ case other
59
+ when SliceMove
60
+ return equivalent_slice_move?(other, cube_size)
61
+ when FatMSliceMove
62
+ return @axis_face == other.axis_face.opposite && @direction == other.direction.inverse
63
+ end
64
+
65
+ false
66
+ end
67
+
68
+ protected
69
+
70
+ def equivalent_slice_move?(other, cube_size)
71
+ cube_size == 3 && other.slice_index == 1 &&
72
+ (@axis_face == other.axis_face && @direction == other.direction ||
73
+ @axis_face == other.axis_face.opposite && @direction == other.direction.inverse)
74
+ end
75
+ end
76
+
77
+ # An M slice move for which we don't know yet whether it's an inner or fat M slice move.
78
+ class MaybeFatMSliceMaybeInnerMSliceMove < CubeMove
79
+ include MSlicePrintHelper
80
+
81
+ # For even layered cubes, m slice moves are meant as very fat moves where only the outer
82
+ # layers stay.
83
+ # For odd layered cubes, we only move the very middle.
84
+ def decide_meaning(cube_size)
85
+ if cube_size.even?
86
+ FatMSliceMove.new(@axis_face, @direction)
87
+ else
88
+ InnerMSliceMove.new(@axis_face, @direction, cube_size / 2)
89
+ end
90
+ end
91
+ end
92
+
93
+ # A fat move with a width. For width 1, this becomes a normal outer move.
94
+ class FatMove < CubeMove
95
+ def initialize(axis_face, direction, width = 1)
96
+ super(axis_face, direction)
97
+ raise TypeError unless width.is_a?(Integer)
98
+ raise ArgumentError, "Invalid width #{width} for fat move." unless width >= 1
99
+
100
+ @width = width
101
+ end
102
+
103
+ OUTER_MOVES = Face::ELEMENTS.product(CubeDirection::NON_ZERO_DIRECTIONS).map do |f, d|
104
+ FatMove.new(f, d)
105
+ end.freeze
106
+
107
+ attr_reader :width
108
+
109
+ def identifying_fields
110
+ super + [@width]
111
+ end
112
+
113
+ def to_s
114
+ "#{@width > 2 ? @width : ''}#{@axis_face.name}#{@width > 1 ? 'w' : ''}#{@direction.name}"
115
+ end
116
+
117
+ def slice_move?
118
+ false
119
+ end
120
+
121
+ def with_width(width)
122
+ FatMove.new(@axis_face, @direction, width)
123
+ end
124
+
125
+ def inverted_width(cube_size)
126
+ cube_size - @width
127
+ end
128
+
129
+ def prepend_rotation(other, cube_size)
130
+ # Note that changing the order is safe because that method returns nil if no cancellation
131
+ # can be performed.
132
+ other.prepend_fat_move(self, cube_size)
133
+ end
134
+
135
+ def prepend_fat_m_slice_move(other, cube_size)
136
+ if adjacent_mslice_move?(other)
137
+ Algorithm.move(FatMove.new(@axis_face, @direction, cube_size - 1))
138
+ elsif contained_mslice_move?(other, cube_size)
139
+ Algorithm.move(FatMove.new(@axis_face, @direction, 1))
140
+ end
141
+ end
142
+
143
+ # rubocop:disable Metrics/PerceivedComplexity
144
+ # rubocop:disable Metrics/CyclomaticComplexity
145
+ def prepend_fat_move(other, cube_size)
146
+ if same_fat_block?(other)
147
+ merge_with_same_fat_block(other)
148
+ elsif opposite_fat_block?(other, cube_size)
149
+ merge_with_opposite_fat_block(other)
150
+ elsif leaves_inner_slice_move?(other)
151
+ Algorithm.move(inner_slice_move)
152
+ elsif other.leaves_inner_slice_move?(self)
153
+ Algorithm.move(other.inner_slice_move)
154
+ elsif leaves_inner_fat_mslice_move?(other, cube_size)
155
+ Algorithm.move(inner_fat_mslice_move(cube_size))
156
+ elsif other.leaves_inner_fat_mslice_move?(self, cube_size)
157
+ Algorithm.move(other.inner_fat_mslice_move(cube_size))
158
+ end
159
+ end
160
+ # rubocop:enable Metrics/CyclomaticComplexity
161
+ # rubocop:enable Metrics/PerceivedComplexity
162
+
163
+ def prepend_slice_move(other, cube_size)
164
+ return unless same_axis?(other)
165
+
166
+ translated_direction = other.translated_direction(@axis_face)
167
+ translated_slice_index = other.translated_slice_index(@axis_face, cube_size)
168
+ move =
169
+ case translated_slice_index
170
+ when @width
171
+ return unless translated_direction == @direction
172
+
173
+ with_width(@width + 1)
174
+ when @width - 1
175
+ return unless translated_direction == @direction.inverse
176
+
177
+ with_width(@width - 1)
178
+ else
179
+ return
180
+ end
181
+ Algorithm.move(move)
182
+ end
183
+
184
+ protected
185
+
186
+ def merge_with_same_fat_block(other)
187
+ Algorithm.move(FatMove.new(@axis_face, @direction + other.direction, @width))
188
+ end
189
+
190
+ def merge_with_opposite_fat_block(other)
191
+ rotation = Rotation.new(@axis_face, @direction)
192
+ move = FatMove.new(other.axis_face, other.direction + @direction, other.width)
193
+ Algorithm.new([move, rotation])
194
+ end
195
+
196
+ # The outermost slice move inside this fat move.
197
+ def inner_slice_move
198
+ raise ArgumentError unless @width >= 2
199
+
200
+ SliceMove.new(@axis_face, @direction, @width - 1)
201
+ end
202
+
203
+ # The fat M-slice move inside this fat move.
204
+ def inner_fat_mslice_move(cube_size)
205
+ raise ArgumentError unless cube_size.even? && @width == cube_size - 1
206
+
207
+ FatMSliceMove.new(@axis_face, @direction)
208
+ end
209
+
210
+ def contained_mslice_move?(other, cube_size)
211
+ same_axis?(other) && @width == cube_size - 1 &&
212
+ @direction == other.translated_direction(@axis_face).inverse
213
+ end
214
+
215
+ def adjacent_mslice_move?(other)
216
+ same_axis?(other) && @width == 1 && @direction == other.translated_direction(@axis_face)
217
+ end
218
+
219
+ def same_fat_block?(other)
220
+ @axis_face == other.axis_face && @width == other.width
221
+ end
222
+
223
+ def leaves_inner_slice_move?(other)
224
+ @axis_face == other.axis_face && @width == other.width + 1 &&
225
+ @direction == other.direction.inverse
226
+ end
227
+
228
+ def leaves_inner_fat_mslice_move?(other, cube_size)
229
+ cube_size.even? && @axis_face == other.axis_face && @width == cube_size - 1 &&
230
+ other.width == 1 && @direction == other.direction.inverse
231
+ end
232
+
233
+ def opposite_fat_block?(other, cube_size)
234
+ @axis_face == other.axis_face.opposite && @width + other.width == cube_size
235
+ end
236
+ end
237
+
238
+ # A slice move of any slice, not necessary the middle one.
239
+ class SliceMove < CubeMove
240
+ def initialize(axis_face, direction, slice_index)
241
+ super(axis_face, direction)
242
+ raise TypeError unless slice_index.is_a?(Integer)
243
+ unless slice_index >= 1
244
+ raise ArgumentError, "Invalid slice index #{slice_index} for slice move."
245
+ end
246
+
247
+ @slice_index = slice_index
248
+ end
249
+
250
+ attr_reader :slice_index
251
+
252
+ def identifying_fields
253
+ super + [@slice_index]
254
+ end
255
+
256
+ def to_s
257
+ "#{@slice_index > 1 ? @slice_index : ''}#{@axis_face.name.downcase}#{@direction.name}"
258
+ end
259
+
260
+ def slice_move?
261
+ true
262
+ end
263
+
264
+ def mirror(normal_face)
265
+ if normal_face.same_axis?(@axis_face)
266
+ SliceMove.new(@axis_face.opposite, @direction.inverse, @slice_index)
267
+ else
268
+ inverse
269
+ end
270
+ end
271
+
272
+ def equivalent_internal?(other, cube_size)
273
+ return other.equivalent_internal?(self, cube_size) if other.is_a?(FatMSliceMove)
274
+ return simplified(cube_size) == other.simplified(cube_size) if other.is_a?(SliceMove)
275
+
276
+ false
277
+ end
278
+
279
+ def translated_slice_index(other_axis_face, cube_size)
280
+ if @slice_index >= cube_size - 1
281
+ raise ArgumentError,
282
+ "Slice index #{@slice_index} of #{self} is invalid for cube size #{cube_size}."
283
+ end
284
+
285
+ case @axis_face
286
+ when other_axis_face then @slice_index
287
+ when other_axis_face.opposite then invert_slice_index(cube_size)
288
+ else raise ArgumentError
289
+ end
290
+ end
291
+
292
+ def prepend_rotation(_other, _cube_size)
293
+ nil
294
+ end
295
+
296
+ def prepend_fat_m_slice_move(_other, _cube_size)
297
+ nil
298
+ end
299
+
300
+ def prepend_fat_move(other, cube_size)
301
+ # Note that changing the order is safe because that method returns nil if no cancellation
302
+ # can be performed.
303
+ other.prepend_slice_move(self, cube_size)
304
+ end
305
+
306
+ def prepend_slice_move(other, cube_size)
307
+ return unless same_axis?(other)
308
+
309
+ # Only for 4x4, we can join two adjacent slice moves into a fat m slice move.
310
+ this = simplified(cube_size)
311
+ if this.can_join_to_fat_mslice?(other, cube_size)
312
+ return Algorithm.move(FatMSliceMove.new(other.axis_face, other.direction))
313
+ end
314
+
315
+ other = other.simplified(cube_size)
316
+ return unless this.same_slice?(other)
317
+
318
+ Algorithm.move(
319
+ SliceMove.new(
320
+ other.axis_face,
321
+ other.direction + this.translated_direction(other.axis_face),
322
+ other.slice_index
323
+ )
324
+ )
325
+ end
326
+
327
+ protected
328
+
329
+ def simplified(cube_size)
330
+ if @slice_index >= cube_size - 1
331
+ raise ArgumentError,
332
+ "Slice index #{@slice_index} of #{self} is invalid for cube size #{cube_size}."
333
+ end
334
+
335
+ if @slice_index >= (cube_size + 1) / 2
336
+ SliceMove.new(@axis_face.opposite, @direction.inverse, invert_slice_index(cube_size))
337
+ else
338
+ self
339
+ end
340
+ end
341
+
342
+ def invert_slice_index(cube_size)
343
+ cube_size - 1 - @slice_index
344
+ end
345
+
346
+ # Note that this is only a partial implementation of what we need internally.
347
+ # It does NOT get all cases correctly because there might be equivalent versions of the
348
+ # same slice move.
349
+ def can_join_to_fat_mslice?(other, cube_size)
350
+ cube_size == 4 && @slice_index == 1 &&
351
+ mirror(@axis_face).equivalent_internal?(other, cube_size)
352
+ end
353
+
354
+ # Note that this is only a partial implementation of what we need internally.
355
+ # It does NOT get all cases correctly because there might be equivalent versions of the
356
+ # same slice move.
357
+ def same_slice?(other)
358
+ @axis_face == other.axis_face && @slice_index == other.slice_index
359
+ end
360
+ end
361
+
362
+ # Inner M slice moves that move only one middle layer.
363
+ class InnerMSliceMove < SliceMove
364
+ include MSlicePrintHelper
365
+ end
366
+
367
+ # Not that this represents a move that is written as 'u' which is a slice move on bigger cubes
368
+ # but a fat move on 3x3...
369
+ class MaybeFatMaybeSliceMove < CubeMove
370
+ # We handle the annoying inconsistency that u is a slice move for bigger cubes, but a fat move
371
+ # for 3x3.
372
+ def decide_meaning(cube_size)
373
+ case cube_size
374
+ when 2 then raise ArgumentError
375
+ when 3 then FatMove.new(@axis_face, @direction, 2)
376
+ else SliceMove.new(@axis_face, @direction, 1)
377
+ end
378
+ end
379
+
380
+ def to_s
381
+ "#{@axis_face.name.downcase}#{@direction.name}"
382
+ end
383
+ end
384
+ end