twisty_puzzles 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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