twisty_puzzles 0.0.1 → 0.0.6

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