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,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