twisty_puzzles 0.0.1 → 0.0.2

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -1
  3. data/lib/twisty_puzzles.rb +37 -0
  4. data/lib/twisty_puzzles/abstract_direction.rb +38 -39
  5. data/lib/twisty_puzzles/abstract_move_parser.rb +32 -33
  6. data/lib/twisty_puzzles/algorithm.rb +112 -113
  7. data/lib/twisty_puzzles/algorithm_transformation.rb +19 -21
  8. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +55 -56
  9. data/lib/twisty_puzzles/cancellation_helper.rb +124 -125
  10. data/lib/twisty_puzzles/commutator.rb +79 -80
  11. data/lib/twisty_puzzles/compiled_algorithm.rb +31 -32
  12. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +49 -50
  13. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +18 -19
  14. data/lib/twisty_puzzles/coordinate.rb +245 -246
  15. data/lib/twisty_puzzles/cube.rb +494 -495
  16. data/lib/twisty_puzzles/cube_constants.rb +40 -41
  17. data/lib/twisty_puzzles/cube_direction.rb +15 -18
  18. data/lib/twisty_puzzles/cube_move.rb +289 -290
  19. data/lib/twisty_puzzles/cube_move_parser.rb +75 -76
  20. data/lib/twisty_puzzles/cube_print_helper.rb +132 -133
  21. data/lib/twisty_puzzles/cube_state.rb +80 -81
  22. data/lib/twisty_puzzles/move_type_creator.rb +17 -18
  23. data/lib/twisty_puzzles/parser.rb +176 -179
  24. data/lib/twisty_puzzles/part_cycle_factory.rb +39 -42
  25. data/lib/twisty_puzzles/puzzle.rb +16 -17
  26. data/lib/twisty_puzzles/reversible_applyable.rb +24 -25
  27. data/lib/twisty_puzzles/rotation.rb +74 -75
  28. data/lib/twisty_puzzles/skewb_direction.rb +14 -15
  29. data/lib/twisty_puzzles/skewb_move.rb +48 -49
  30. data/lib/twisty_puzzles/skewb_move_parser.rb +50 -51
  31. data/lib/twisty_puzzles/skewb_notation.rb +115 -118
  32. data/lib/twisty_puzzles/skewb_state.rb +120 -121
  33. data/lib/twisty_puzzles/state_helper.rb +20 -21
  34. data/lib/twisty_puzzles/sticker_cycle.rb +43 -44
  35. data/lib/twisty_puzzles/utils.rb +3 -0
  36. data/lib/twisty_puzzles/version.rb +3 -1
  37. metadata +3 -3
@@ -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,378 @@ 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
+ # rubocop:disable Metrics/PerceivedComplexity
143
+ # rubocop:disable Metrics/CyclomaticComplexity
144
+ def prepend_fat_move(other, cube_size)
145
+ if same_fat_block?(other)
146
+ merge_with_same_fat_block(other)
147
+ elsif opposite_fat_block?(other, cube_size)
148
+ merge_with_opposite_fat_block(other)
149
+ elsif leaves_inner_slice_move?(other)
150
+ Algorithm.move(inner_slice_move)
151
+ elsif other.leaves_inner_slice_move?(self)
152
+ Algorithm.move(other.inner_slice_move)
153
+ elsif leaves_inner_fat_mslice_move?(other, cube_size)
154
+ Algorithm.move(inner_fat_mslice_move(cube_size))
155
+ elsif other.leaves_inner_fat_mslice_move?(self, cube_size)
156
+ Algorithm.move(other.inner_fat_mslice_move(cube_size))
182
157
  end
158
+ end
159
+ # rubocop:enable Metrics/CyclomaticComplexity
160
+ # rubocop:enable Metrics/PerceivedComplexity
183
161
 
184
- protected
162
+ def prepend_slice_move(other, cube_size)
163
+ return unless same_axis?(other)
185
164
 
186
- def merge_with_same_fat_block(other)
187
- Algorithm.move(FatMove.new(@axis_face, @direction + other.direction, @width))
188
- end
165
+ translated_direction = other.translated_direction(@axis_face)
166
+ translated_slice_index = other.translated_slice_index(@axis_face, cube_size)
167
+ move =
168
+ case translated_slice_index
169
+ when @width
170
+ return unless translated_direction == @direction
189
171
 
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
172
+ with_width(@width + 1)
173
+ when @width - 1
174
+ return unless translated_direction == @direction.inverse
195
175
 
196
- # The outermost slice move inside this fat move.
197
- def inner_slice_move
198
- raise ArgumentError unless @width >= 2
176
+ with_width(@width - 1)
177
+ else
178
+ return
179
+ end
180
+ Algorithm.move(move)
181
+ end
199
182
 
200
- SliceMove.new(@axis_face, @direction, @width - 1)
201
- end
183
+ protected
202
184
 
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
185
+ def merge_with_same_fat_block(other)
186
+ Algorithm.move(FatMove.new(@axis_face, @direction + other.direction, @width))
187
+ end
206
188
 
207
- FatMSliceMove.new(@axis_face, @direction)
208
- end
189
+ def merge_with_opposite_fat_block(other)
190
+ rotation = Rotation.new(@axis_face, @direction)
191
+ move = FatMove.new(other.axis_face, other.direction + @direction, other.width)
192
+ Algorithm.new([move, rotation])
193
+ end
209
194
 
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
195
+ # The outermost slice move inside this fat move.
196
+ def inner_slice_move
197
+ raise ArgumentError unless @width >= 2
214
198
 
215
- def adjacent_mslice_move?(other)
216
- same_axis?(other) && @width == 1 && @direction == other.translated_direction(@axis_face)
217
- end
199
+ SliceMove.new(@axis_face, @direction, @width - 1)
200
+ end
218
201
 
219
- def same_fat_block?(other)
220
- @axis_face == other.axis_face && @width == other.width
221
- end
202
+ # The fat M-slice move inside this fat move.
203
+ def inner_fat_mslice_move(cube_size)
204
+ raise ArgumentError unless cube_size.even? && @width == cube_size - 1
222
205
 
223
- def leaves_inner_slice_move?(other)
224
- @axis_face == other.axis_face && @width == other.width + 1 &&
225
- @direction == other.direction.inverse
226
- end
206
+ FatMSliceMove.new(@axis_face, @direction)
207
+ end
227
208
 
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
209
+ def contained_mslice_move?(other, cube_size)
210
+ same_axis?(other) && @width == cube_size - 1 &&
211
+ @direction == other.translated_direction(@axis_face).inverse
212
+ end
232
213
 
233
- def opposite_fat_block?(other, cube_size)
234
- @axis_face == other.axis_face.opposite && @width + other.width == cube_size
235
- end
214
+ def adjacent_mslice_move?(other)
215
+ same_axis?(other) && @width == 1 && @direction == other.translated_direction(@axis_face)
236
216
  end
237
217
 
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
218
+ def same_fat_block?(other)
219
+ @axis_face == other.axis_face && @width == other.width
220
+ end
246
221
 
247
- @slice_index = slice_index
248
- end
222
+ def leaves_inner_slice_move?(other)
223
+ @axis_face == other.axis_face && @width == other.width + 1 &&
224
+ @direction == other.direction.inverse
225
+ end
249
226
 
250
- attr_reader :slice_index
227
+ def leaves_inner_fat_mslice_move?(other, cube_size)
228
+ cube_size.even? && @axis_face == other.axis_face && @width == cube_size - 1 &&
229
+ other.width == 1 && @direction == other.direction.inverse
230
+ end
251
231
 
252
- def identifying_fields
253
- super + [@slice_index]
254
- end
232
+ def opposite_fat_block?(other, cube_size)
233
+ @axis_face == other.axis_face.opposite && @width + other.width == cube_size
234
+ end
235
+ end
255
236
 
256
- def to_s
257
- "#{@slice_index > 1 ? @slice_index : ''}#{@axis_face.name.downcase}#{@direction.name}"
237
+ # A slice move of any slice, not necessary the middle one.
238
+ class SliceMove < CubeMove
239
+ def initialize(axis_face, direction, slice_index)
240
+ super(axis_face, direction)
241
+ raise TypeError unless slice_index.is_a?(Integer)
242
+ unless slice_index >= 1
243
+ raise ArgumentError, "Invalid slice index #{slice_index} for slice move."
258
244
  end
259
245
 
260
- def slice_move?
261
- true
262
- end
246
+ @slice_index = slice_index
247
+ end
263
248
 
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
249
+ attr_reader :slice_index
271
250
 
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)
251
+ def identifying_fields
252
+ super + [@slice_index]
253
+ end
275
254
 
276
- false
277
- end
255
+ def to_s
256
+ "#{@slice_index > 1 ? @slice_index : ''}#{@axis_face.name.downcase}#{@direction.name}"
257
+ end
278
258
 
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
259
+ def slice_move?
260
+ true
261
+ end
284
262
 
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
263
+ def mirror(normal_face)
264
+ if normal_face.same_axis?(@axis_face)
265
+ SliceMove.new(@axis_face.opposite, @direction.inverse, @slice_index)
266
+ else
267
+ inverse
290
268
  end
269
+ end
291
270
 
292
- def prepend_rotation(_other, _cube_size)
293
- nil
294
- end
271
+ def equivalent_internal?(other, cube_size)
272
+ return other.equivalent_internal?(self, cube_size) if other.is_a?(FatMSliceMove)
273
+ return simplified(cube_size) == other.simplified(cube_size) if other.is_a?(SliceMove)
295
274
 
296
- def prepend_fat_m_slice_move(_other, _cube_size)
297
- nil
275
+ false
276
+ end
277
+
278
+ def translated_slice_index(other_axis_face, cube_size)
279
+ if @slice_index >= cube_size - 1
280
+ raise ArgumentError,
281
+ "Slice index #{@slice_index} of #{self} is invalid for cube size #{cube_size}."
298
282
  end
299
283
 
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)
284
+ case @axis_face
285
+ when other_axis_face then @slice_index
286
+ when other_axis_face.opposite then invert_slice_index(cube_size)
287
+ else raise ArgumentError
304
288
  end
289
+ end
305
290
 
306
- def prepend_slice_move(other, cube_size)
307
- return unless same_axis?(other)
291
+ def prepend_rotation(_other, _cube_size)
292
+ nil
293
+ end
308
294
 
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
295
+ def prepend_fat_m_slice_move(_other, _cube_size)
296
+ nil
297
+ end
314
298
 
315
- other = other.simplified(cube_size)
316
- return unless this.same_slice?(other)
299
+ def prepend_fat_move(other, cube_size)
300
+ # Note that changing the order is safe because that method returns nil if no cancellation
301
+ # can be performed.
302
+ other.prepend_slice_move(self, cube_size)
303
+ end
317
304
 
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
- )
305
+ def prepend_slice_move(other, cube_size)
306
+ return unless same_axis?(other)
307
+
308
+ # Only for 4x4, we can join two adjacent slice moves into a fat m slice move.
309
+ this = simplified(cube_size)
310
+ if this.can_join_to_fat_mslice?(other, cube_size)
311
+ return Algorithm.move(FatMSliceMove.new(other.axis_face, other.direction))
325
312
  end
326
313
 
327
- protected
314
+ other = other.simplified(cube_size)
315
+ return unless this.same_slice?(other)
328
316
 
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
317
+ Algorithm.move(
318
+ SliceMove.new(
319
+ other.axis_face,
320
+ other.direction + this.translated_direction(other.axis_face),
321
+ other.slice_index
322
+ )
323
+ )
324
+ end
334
325
 
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
326
+ protected
341
327
 
342
- def invert_slice_index(cube_size)
343
- cube_size - 1 - @slice_index
328
+ def simplified(cube_size)
329
+ if @slice_index >= cube_size - 1
330
+ raise ArgumentError,
331
+ "Slice index #{@slice_index} of #{self} is invalid for cube size #{cube_size}."
344
332
  end
345
333
 
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)
334
+ if @slice_index >= (cube_size + 1) / 2
335
+ SliceMove.new(@axis_face.opposite, @direction.inverse, invert_slice_index(cube_size))
336
+ else
337
+ self
352
338
  end
339
+ end
353
340
 
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
341
+ def invert_slice_index(cube_size)
342
+ cube_size - 1 - @slice_index
360
343
  end
361
344
 
362
- # Inner M slice moves that move only one middle layer.
363
- class InnerMSliceMove < SliceMove
364
- include MSlicePrintHelper
345
+ # Note that this is only a partial implementation of what we need internally.
346
+ # It does NOT get all cases correctly because there might be equivalent versions of the
347
+ # same slice move.
348
+ def can_join_to_fat_mslice?(other, cube_size)
349
+ cube_size == 4 && @slice_index == 1 &&
350
+ mirror(@axis_face).equivalent_internal?(other, cube_size)
365
351
  end
366
352
 
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
353
+ # Note that this is only a partial implementation of what we need internally.
354
+ # It does NOT get all cases correctly because there might be equivalent versions of the
355
+ # same slice move.
356
+ def same_slice?(other)
357
+ @axis_face == other.axis_face && @slice_index == other.slice_index
358
+ end
359
+ end
360
+
361
+ # Inner M slice moves that move only one middle layer.
362
+ class InnerMSliceMove < SliceMove
363
+ include MSlicePrintHelper
364
+ end
365
+
366
+ # Not that this represents a move that is written as 'u' which is a slice move on bigger cubes
367
+ # but a fat move on 3x3...
368
+ class MaybeFatMaybeSliceMove < CubeMove
369
+ # We handle the annoying inconsistency that u is a slice move for bigger cubes, but a fat move
370
+ # for 3x3.
371
+ def decide_meaning(cube_size)
372
+ case cube_size
373
+ when 2 then raise ArgumentError
374
+ when 3 then FatMove.new(@axis_face, @direction, 2)
375
+ else SliceMove.new(@axis_face, @direction, 1)
378
376
  end
377
+ end
379
378
 
380
- def to_s
381
- "#{@axis_face.name.downcase}#{@direction.name}"
382
- end
379
+ def to_s
380
+ "#{@axis_face.name.downcase}#{@direction.name}"
383
381
  end
382
+ end
384
383
  end