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,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+ require 'twisty_puzzles/algorithm'
5
+ require 'twisty_puzzles/cube'
6
+
7
+ module TwistyPuzzles
8
+
9
+ # Base class for Commutators.
10
+ class Commutator
11
+ def cancellations(other, cube_size, metric = :htm)
12
+ algorithm.cancellations(other.algorithm, cube_size, metric)
13
+ end
14
+ end
15
+
16
+ # Algorithm that is used like a commutator but actually isn't one.
17
+ class FakeCommutator < Commutator
18
+ def initialize(algorithm)
19
+ raise ArgumentError unless algorithm.is_a?(Algorithm)
20
+
21
+ @algorithm = algorithm
22
+ end
23
+
24
+ attr_reader :algorithm
25
+
26
+ def eql?(other)
27
+ self.class.equal?(other.class) && @algorithm == other.algorithm
28
+ end
29
+
30
+ alias == eql?
31
+
32
+ def hash
33
+ @hash ||= [self.class, @algorithm].hash
34
+ end
35
+
36
+ def inverse
37
+ FakeCommutator.new(@algorithm.inverse)
38
+ end
39
+
40
+ def to_s
41
+ @algorithm.to_s
42
+ end
43
+ end
44
+
45
+ # Pure commutator of the form A B A' B'.
46
+ class PureCommutator < Commutator
47
+ def initialize(first_part, second_part)
48
+ raise ArgumentError unless first_part.is_a?(Algorithm)
49
+ raise ArgumentError unless second_part.is_a?(Algorithm)
50
+
51
+ @first_part = first_part
52
+ @second_part = second_part
53
+ end
54
+
55
+ attr_reader :first_part, :second_part
56
+
57
+ def eql?(other)
58
+ self.class.equal?(other.class) && @first_part == other.first_part &&
59
+ @second_part == other.second_part
60
+ end
61
+
62
+ alias == eql?
63
+
64
+ def hash
65
+ @hash ||= [self.class, @first_part, @second_part].hash
66
+ end
67
+
68
+ def inverse
69
+ PureCommutator.new(second_part, first_part)
70
+ end
71
+
72
+ def to_s
73
+ "[#{@first_part}, #{@second_part}]"
74
+ end
75
+
76
+ def algorithm
77
+ first_part + second_part + first_part.inverse + second_part.inverse
78
+ end
79
+ end
80
+
81
+ # Setup commutator of the form A B A'.
82
+ class SetupCommutator < Commutator
83
+ def initialize(setup, inner_commutator)
84
+ raise ArgumentError, 'Setup move has to be an algorithm.' unless setup.is_a?(Algorithm)
85
+ unless inner_commutator.is_a?(Commutator)
86
+ raise ArgumentError, 'Inner commutator has to be a commutator.'
87
+ end
88
+
89
+ @setup = setup
90
+ @inner_commutator = inner_commutator
91
+ end
92
+
93
+ attr_reader :setup, :inner_commutator
94
+
95
+ def eql?(other)
96
+ self.class.equal?(other.class) && @setup == other.setup &&
97
+ @inner_commutator == other.inner_commutator
98
+ end
99
+
100
+ alias == eql?
101
+
102
+ def hash
103
+ @hash ||= [self.class, @setup, @inner_commutator].hash
104
+ end
105
+
106
+ def inverse
107
+ SetupCommutator.new(setup, @inner_commutator.inverse)
108
+ end
109
+
110
+ def to_s
111
+ "[#{@setup} : #{@inner_commutator}]"
112
+ end
113
+
114
+ def algorithm
115
+ setup + inner_commutator.algorithm + setup.inverse
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/reversible_applyable'
4
+
5
+ module TwistyPuzzles
6
+
7
+ # Base class for a compiled algorithm for a particular puzzle.
8
+ class CompiledAlgorithm
9
+ include ReversibleApplyable
10
+
11
+ def initialize(native)
12
+ raise TypeError unless native.is_a?(self.class::NATIVE_CLASS)
13
+
14
+ @native = native
15
+ end
16
+
17
+ attr_reader :native
18
+
19
+ def rotate_by(rotation)
20
+ self.class.new(@native.rotate_by(rotation.axis_face.face_symbol, rotation.direction.value))
21
+ end
22
+
23
+ def mirror(normal_face)
24
+ self.class.new(@native.mirror(normal_face.face_symbol))
25
+ end
26
+
27
+ def inverse
28
+ @inverse ||=
29
+ begin
30
+ alg = self.class.new(@native.inverse)
31
+ alg.inverse = self
32
+ alg
33
+ end
34
+ end
35
+
36
+ def +(other)
37
+ self.class.new(@native + other.native)
38
+ end
39
+
40
+ def apply_to(state)
41
+ @native.apply_to(state.native)
42
+ end
43
+
44
+ protected
45
+
46
+ attr_writer :inverse
47
+ end
48
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/compiled_algorithm'
4
+
5
+ module TwistyPuzzles
6
+
7
+ # Wrapper of the native C implementation of a compiled algorithm for a particular cube size.
8
+ class CompiledCubeAlgorithm < CompiledAlgorithm
9
+ def self.transform_rotation(move, cube_size)
10
+ slice_moves =
11
+ 0.upto(cube_size - 1).map do |i|
12
+ [:slice, move.axis_face.face_symbol, move.direction.value, i]
13
+ end
14
+ [
15
+ [:face, move.axis_face.face_symbol, move.direction.value],
16
+ [:face, move.axis_face.opposite.face_symbol, move.direction.inverse.value]
17
+ ] + slice_moves
18
+ end
19
+
20
+ def self.transform_fat_mslice_move(move, cube_size)
21
+ 1.upto(cube_size - 2).map do |i|
22
+ [:slice, move.axis_face.face_symbol, move.direction.value, i]
23
+ end
24
+ end
25
+
26
+ def self.transform_slice_move(move)
27
+ [
28
+ [:slice, move.axis_face.face_symbol, move.direction.value, move.slice_index]
29
+ ]
30
+ end
31
+
32
+ def self.transform_fat_move(move)
33
+ slice_moves =
34
+ 0.upto(move.width - 1).map do |i|
35
+ [:slice, move.axis_face.face_symbol, move.direction.value, i]
36
+ end
37
+ [
38
+ [:face, move.axis_face.face_symbol, move.direction.value]
39
+ ] + slice_moves
40
+ end
41
+
42
+ private_class_method :transform_rotation, :transform_fat_mslice_move, :transform_slice_move,
43
+ :transform_fat_move
44
+
45
+ def self.transform_move(move, cube_size)
46
+ decided_move = move.decide_meaning(cube_size)
47
+ case decided_move
48
+ when Rotation then transform_rotation(decided_move, cube_size)
49
+ when FatMSliceMove then transform_fat_mslice_move(decided_move, cube_size)
50
+ # Note that this also covers InnerMSliceMove
51
+ when SliceMove then transform_slice_move(decided_move)
52
+ when FatMove then transform_fat_move(decided_move)
53
+ else
54
+ raise TypeError, "Invalid move type #{move.class} that becomes #{decided_move.class} "\
55
+ "for cube size #{cube_size}."
56
+ end
57
+ end
58
+
59
+ def self.for_moves(cube_size, moves)
60
+ transformed_moves = moves.collect_concat { |m| transform_move(m, cube_size) }
61
+ native = Native::CubeAlgorithm.new(cube_size, transformed_moves)
62
+ new(native)
63
+ end
64
+
65
+ NATIVE_CLASS = Native::CubeAlgorithm
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/compiled_algorithm'
4
+
5
+ module TwistyPuzzles
6
+
7
+ # Wrapper of the native C implementation of a compiled algorithm for a particular cube size.
8
+ class CompiledSkewbAlgorithm < CompiledAlgorithm
9
+ def self.transform_move(move)
10
+ case move
11
+ when Rotation
12
+ [:rotation, move.axis_face.face_symbol, move.direction.value]
13
+ when SkewbMove
14
+ [:move, move.axis_corner.face_symbols, move.direction.value]
15
+ else
16
+ raise TypeError
17
+ end
18
+ end
19
+
20
+ def self.for_moves(moves)
21
+ native = Native::SkewbAlgorithm.new(moves.map { |m| transform_move(m) })
22
+ new(native)
23
+ end
24
+
25
+ NATIVE_CLASS = Native::SkewbAlgorithm
26
+ EMPTY = for_moves([])
27
+ end
28
+ end
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/cube'
4
+ require 'twisty_puzzles/cube_constants'
5
+ require 'twisty_puzzles/native'
6
+
7
+ module TwistyPuzzles
8
+
9
+ # Coordinate of a sticker on the cube.
10
+ class Coordinate
11
+ def self.highest_coordinate(cube_size)
12
+ cube_size - 1
13
+ end
14
+
15
+ def self.invert_coordinate(index, cube_size)
16
+ highest_coordinate(cube_size) - index
17
+ end
18
+
19
+ def self.coordinate_range(cube_size)
20
+ 0.upto(highest_coordinate(cube_size))
21
+ end
22
+
23
+ def self.middle(cube_size)
24
+ raise ArgumentError if cube_size.even?
25
+
26
+ cube_size / 2
27
+ end
28
+
29
+ # Middle coordinate for uneven numbers, the one before for even numbers
30
+ def self.middle_or_before(cube_size)
31
+ cube_size - cube_size / 2 - 1
32
+ end
33
+
34
+ # Middle coordinate for uneven numbers, the one after for even numbers
35
+ def self.middle_or_after(cube_size)
36
+ cube_size / 2
37
+ end
38
+
39
+ # The last coordinate that is strictly before the middle
40
+ def self.last_before_middle(cube_size)
41
+ cube_size / 2 - 1
42
+ end
43
+
44
+ def self.canonicalize(index, cube_size)
45
+ raise ArgumentError unless index.is_a?(Integer) && -cube_size <= index && index < cube_size
46
+
47
+ index >= 0 ? index : cube_size + index
48
+ end
49
+
50
+ def self.from_face_distances(face, cube_size, face_distances)
51
+ raise ArgumentError if face_distances.length != 2
52
+
53
+ coordinates = [nil, nil]
54
+ face_distances.each do |neighbor, distance|
55
+ index = face.coordinate_index_close_to(neighbor)
56
+ coordinate =
57
+ if neighbor.close_to_smaller_indices?
58
+ distance
59
+ else
60
+ invert_coordinate(distance, cube_size)
61
+ end
62
+ raise ArgumentError if coordinates[index]
63
+
64
+ coordinates[index] = coordinate
65
+ end
66
+ raise ArgumentError if coordinates.any?(&:nil?)
67
+
68
+ from_indices(face, cube_size, *coordinates)
69
+ end
70
+
71
+ def self.match_coordinate_internal(base_coordinate, other_face_symbols)
72
+ other_face_symbols.sort!
73
+ coordinate =
74
+ base_coordinate.rotations.find do |coord|
75
+ face_symbols_closeby = coord.close_neighbor_faces.map(&:face_symbol)
76
+ face_symbols_closeby.sort == other_face_symbols
77
+ end
78
+ raise "Couldn't find a fitting coordinate on the solved face." if coordinate.nil?
79
+
80
+ coordinate
81
+ end
82
+
83
+ # The coordinate of the solved position of the main sticker of this part.
84
+ def self.solved_position(part, cube_size, incarnation_index)
85
+ raise TypeError unless part.is_a?(Part)
86
+ raise unless part.class::ELEMENTS.length == 24
87
+ raise unless incarnation_index >= 0 && incarnation_index < part.num_incarnations(cube_size)
88
+
89
+ # This is a coordinate on the same face and belonging to an equivalent part.
90
+ # But it might not be the right one.
91
+ base_coordinate = Coordinate.from_indices(
92
+ part.solved_face, cube_size, *part.base_index_on_face(cube_size, incarnation_index)
93
+ )
94
+ other_face_symbols = part.corresponding_part.face_symbols[1..-1]
95
+ match_coordinate_internal(base_coordinate, other_face_symbols)
96
+ end
97
+
98
+ # The coordinate of the solved position of all stickers of this part.
99
+ # rubocop:disable Metrics/AbcSize
100
+ def self.solved_positions(part, cube_size, incarnation_index)
101
+ solved_coordinate = solved_position(part, cube_size, incarnation_index)
102
+ other_coordinates =
103
+ part.face_symbols[1..-1].map.with_index do |f, i|
104
+ face = Face.for_face_symbol(f)
105
+ # The reverse is important for edge like parts. We are not in the same position as usual
106
+ # solved pieces would be.
107
+ # For other types of pieces, it doesn't make a difference as the base index will just be
108
+ # a rotation of the original one, but we will anyway look at all rotations later.
109
+ base_indices = part.base_index_on_other_face(face, cube_size, incarnation_index).reverse
110
+ base_coordinate = Coordinate.from_indices(face, cube_size, *base_indices)
111
+ other_face_symbols = [part.face_symbols[0]] +
112
+ part.corresponding_part.face_symbols[1...i + 1] +
113
+ part.corresponding_part.face_symbols[i + 2..-1]
114
+ match_coordinate_internal(base_coordinate, other_face_symbols)
115
+ end
116
+ [solved_coordinate] + other_coordinates
117
+ end
118
+ # rubocop:enable Metrics/AbcSize
119
+
120
+ def self.center(face, cube_size)
121
+ m = middle(cube_size)
122
+ from_indices(face, cube_size, m, m)
123
+ end
124
+
125
+ def self.edges_outside(face, cube_size)
126
+ face.neighbors.zip(face.neighbors.rotate(1)).collect_concat do |neighbor, next_neighbor|
127
+ 1.upto(cube_size - 2).map do |i|
128
+ from_face_distances(neighbor, cube_size, face => 0, next_neighbor => i)
129
+ end
130
+ end
131
+ end
132
+
133
+ def self.from_indices(face, cube_size, x_index, y_index)
134
+ raise TypeError, "Unsuitable face #{face.inspect}." unless face.is_a?(Face)
135
+ raise TypeError unless cube_size.is_a?(Integer)
136
+ raise ArgumentError unless cube_size.positive?
137
+
138
+ x = Coordinate.canonicalize(x_index, cube_size)
139
+ y = Coordinate.canonicalize(y_index, cube_size)
140
+ native = Native::CubeCoordinate.new(
141
+ cube_size,
142
+ face.face_symbol,
143
+ face.coordinate_index_base_face(0).face_symbol,
144
+ face.coordinate_index_base_face(1).face_symbol,
145
+ x,
146
+ y
147
+ )
148
+ new(native)
149
+ end
150
+
151
+ private_class_method :new
152
+
153
+ def initialize(native)
154
+ raise TypeError unless native.is_a?(Native::CubeCoordinate)
155
+
156
+ @native = native
157
+ end
158
+
159
+ attr_reader :native
160
+
161
+ def face
162
+ @face ||= Face.for_face_symbol(@native.face)
163
+ end
164
+
165
+ def cube_size
166
+ @cube_size ||= @native.cube_size
167
+ end
168
+
169
+ def coordinate(coordinate_index)
170
+ native.coordinate(face.coordinate_index_base_face(coordinate_index).face_symbol)
171
+ end
172
+
173
+ def coordinates
174
+ @coordinates ||= [x, y].freeze
175
+ end
176
+
177
+ def x
178
+ @x ||= coordinate(0)
179
+ end
180
+
181
+ def y
182
+ @y ||= coordinate(1)
183
+ end
184
+
185
+ def eql?(other)
186
+ self.class.equal?(other.class) && @native == other.native
187
+ end
188
+
189
+ alias == eql?
190
+
191
+ def hash
192
+ [self.class, @native].hash
193
+ end
194
+
195
+ def can_jump_to?(to_face)
196
+ raise ArgumentError unless to_face.is_a?(Face)
197
+
198
+ jump_coordinate_index = face.coordinate_index_close_to(to_face)
199
+ jump_coordinate = coordinates[jump_coordinate_index]
200
+ (jump_coordinate.zero? && to_face.close_to_smaller_indices?) ||
201
+ (jump_coordinate == Coordinate.highest_coordinate(cube_size) &&
202
+ !to_face.close_to_smaller_indices?)
203
+ end
204
+
205
+ def jump_to_neighbor(to_face)
206
+ raise ArgumentError unless to_face.is_a?(Face)
207
+ raise ArgumentError unless face.neighbors.include?(to_face)
208
+ raise ArgumentError unless can_jump_to?(to_face)
209
+
210
+ new_coordinates = coordinates.dup
211
+ new_coordinate_index = to_face.coordinate_index_close_to(face)
212
+ new_coordinate = make_coordinate_at_edge_to(face)
213
+ new_coordinates.insert(new_coordinate_index, new_coordinate)
214
+ Coordinate.from_indices(to_face, cube_size, *new_coordinates)
215
+ end
216
+
217
+ def jump_to_coordinates(new_coordinates)
218
+ Coordinate.from_indices(@face, @cube_size, *new_coordinates)
219
+ end
220
+
221
+ def make_coordinate_at_edge_to(face)
222
+ face.close_to_smaller_indices? ? 0 : Coordinate.highest_coordinate(cube_size)
223
+ end
224
+
225
+ # Returns neighbor faces that are closer to this coordinate than their opposite face.
226
+ def close_neighbor_faces
227
+ face.neighbors.select do |neighbor|
228
+ coordinate = coordinates[face.coordinate_index_close_to(neighbor)]
229
+ if neighbor.close_to_smaller_indices?
230
+ before_middle?(coordinate)
231
+ else
232
+ after_middle?(coordinate)
233
+ end
234
+ end
235
+ end
236
+
237
+ def after_middle?(index)
238
+ Coordinate.canonicalize(index, cube_size) > Coordinate.middle_or_before(cube_size)
239
+ end
240
+
241
+ def before_middle?(index)
242
+ Coordinate.canonicalize(index, cube_size) <= Coordinate.last_before_middle(cube_size)
243
+ end
244
+
245
+ # On a nxn grid with integer coordinates between 0 and n - 1, iterates between the 4 points
246
+ # that point (x, y) hits if you rotate by 90 degrees.
247
+ def rotate
248
+ jump_to_coordinates([y, Coordinate.invert_coordinate(x, cube_size)])
249
+ end
250
+
251
+ # On a nxn grid with integer coordinates between 0 and n - 1, give the 4 points that point
252
+ # (x, y) hits if you do a full rotation of the face in clockwise order.
253
+ def rotations
254
+ rots = []
255
+ current = self
256
+ 4.times do
257
+ rots.push(current)
258
+ current = current.rotate
259
+ end
260
+ raise unless current == self
261
+
262
+ rots
263
+ end
264
+ end
265
+
266
+ # Coordinate of a sticker on the Skewb.
267
+ class SkewbCoordinate
268
+ include Comparable
269
+ include CubeConstants
270
+ def initialize(face, coordinate, native)
271
+ raise ArgumentError, "Unsuitable face #{face.inspect}." unless face.is_a?(Face)
272
+ unless coordinate.is_a?(Integer) && coordinate >= 0 && coordinate < SKEWB_STICKERS
273
+ raise ArgumentError
274
+ end
275
+
276
+ @coordinate = coordinate
277
+ @native = native
278
+ end
279
+
280
+ attr_reader :native
281
+
282
+ private_class_method :new
283
+
284
+ def self.for_center(face)
285
+ native = Native::SkewbCoordinate.for_center(face.face_symbol)
286
+ new(face, 0, native)
287
+ end
288
+
289
+ def self.corners_on_face(face)
290
+ face.clockwise_corners.map { |c| for_corner(c) }
291
+ end
292
+
293
+ def self.for_corner(corner)
294
+ native = Native::SkewbCoordinate.for_corner(corner.face_symbols)
295
+ new(Face.for_face_symbol(corner.face_symbols.first), 1 + corner.piece_index % 4, native)
296
+ end
297
+
298
+ def hash
299
+ @hash ||= [self.class, @native].hash
300
+ end
301
+
302
+ def eql?(other)
303
+ @native.eql?(other.native)
304
+ end
305
+
306
+ alias == eql?
307
+
308
+ def <=>(other)
309
+ @native <=> other.native
310
+ end
311
+
312
+ def face
313
+ @face ||= Face.for_face_symbol(@native.face)
314
+ end
315
+
316
+ attr_reader :coordinate
317
+ end
318
+ end