twisty_puzzles 0.0.1 → 0.0.6

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