twisty_puzzles 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/LICENSE +21 -0
  5. data/README.md +32 -0
  6. data/ext/twisty_puzzles/native/extconf.rb +5 -0
  7. data/lib/twisty_puzzles/abstract_direction.rb +54 -0
  8. data/lib/twisty_puzzles/abstract_move.rb +170 -0
  9. data/lib/twisty_puzzles/abstract_move_parser.rb +45 -0
  10. data/lib/twisty_puzzles/algorithm.rb +155 -0
  11. data/lib/twisty_puzzles/algorithm_transformation.rb +33 -0
  12. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +78 -0
  13. data/lib/twisty_puzzles/cancellation_helper.rb +165 -0
  14. data/lib/twisty_puzzles/color_scheme.rb +174 -0
  15. data/lib/twisty_puzzles/commutator.rb +118 -0
  16. data/lib/twisty_puzzles/compiled_algorithm.rb +48 -0
  17. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +67 -0
  18. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +28 -0
  19. data/lib/twisty_puzzles/coordinate.rb +318 -0
  20. data/lib/twisty_puzzles/cube.rb +660 -0
  21. data/lib/twisty_puzzles/cube_constants.rb +53 -0
  22. data/lib/twisty_puzzles/cube_direction.rb +27 -0
  23. data/lib/twisty_puzzles/cube_move.rb +384 -0
  24. data/lib/twisty_puzzles/cube_move_parser.rb +100 -0
  25. data/lib/twisty_puzzles/cube_print_helper.rb +160 -0
  26. data/lib/twisty_puzzles/cube_state.rb +113 -0
  27. data/lib/twisty_puzzles/letter_scheme.rb +72 -0
  28. data/lib/twisty_puzzles/move_type_creator.rb +27 -0
  29. data/lib/twisty_puzzles/parser.rb +222 -0
  30. data/lib/twisty_puzzles/part_cycle_factory.rb +59 -0
  31. data/lib/twisty_puzzles/puzzle.rb +26 -0
  32. data/lib/twisty_puzzles/reversible_applyable.rb +37 -0
  33. data/lib/twisty_puzzles/rotation.rb +105 -0
  34. data/lib/twisty_puzzles/skewb_direction.rb +24 -0
  35. data/lib/twisty_puzzles/skewb_move.rb +59 -0
  36. data/lib/twisty_puzzles/skewb_move_parser.rb +73 -0
  37. data/lib/twisty_puzzles/skewb_notation.rb +147 -0
  38. data/lib/twisty_puzzles/skewb_state.rb +163 -0
  39. data/lib/twisty_puzzles/state_helper.rb +32 -0
  40. data/lib/twisty_puzzles/sticker_cycle.rb +70 -0
  41. data/lib/twisty_puzzles/twisty_puzzles_error.rb +6 -0
  42. data/lib/twisty_puzzles/utils/array_helper.rb +109 -0
  43. data/lib/twisty_puzzles/utils/string_helper.rb +26 -0
  44. data/lib/twisty_puzzles/utils.rb +7 -0
  45. data/lib/twisty_puzzles/version.rb +3 -0
  46. data/lib/twisty_puzzles.rb +5 -0
  47. metadata +249 -0
@@ -0,0 +1,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