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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/lib/twisty_puzzles.rb +37 -0
- data/lib/twisty_puzzles/abstract_direction.rb +38 -39
- data/lib/twisty_puzzles/abstract_move_parser.rb +32 -33
- data/lib/twisty_puzzles/algorithm.rb +112 -113
- data/lib/twisty_puzzles/algorithm_transformation.rb +19 -21
- data/lib/twisty_puzzles/axis_face_and_direction_move.rb +55 -56
- data/lib/twisty_puzzles/cancellation_helper.rb +124 -125
- data/lib/twisty_puzzles/commutator.rb +79 -80
- data/lib/twisty_puzzles/compiled_algorithm.rb +31 -32
- data/lib/twisty_puzzles/compiled_cube_algorithm.rb +49 -50
- data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +18 -19
- data/lib/twisty_puzzles/coordinate.rb +245 -246
- data/lib/twisty_puzzles/cube.rb +494 -495
- data/lib/twisty_puzzles/cube_constants.rb +40 -41
- data/lib/twisty_puzzles/cube_direction.rb +15 -18
- data/lib/twisty_puzzles/cube_move.rb +289 -290
- data/lib/twisty_puzzles/cube_move_parser.rb +75 -76
- data/lib/twisty_puzzles/cube_print_helper.rb +132 -133
- data/lib/twisty_puzzles/cube_state.rb +80 -81
- data/lib/twisty_puzzles/move_type_creator.rb +17 -18
- data/lib/twisty_puzzles/parser.rb +176 -179
- data/lib/twisty_puzzles/part_cycle_factory.rb +39 -42
- data/lib/twisty_puzzles/puzzle.rb +16 -17
- data/lib/twisty_puzzles/reversible_applyable.rb +24 -25
- data/lib/twisty_puzzles/rotation.rb +74 -75
- data/lib/twisty_puzzles/skewb_direction.rb +14 -15
- data/lib/twisty_puzzles/skewb_move.rb +48 -49
- data/lib/twisty_puzzles/skewb_move_parser.rb +50 -51
- data/lib/twisty_puzzles/skewb_notation.rb +115 -118
- data/lib/twisty_puzzles/skewb_state.rb +120 -121
- data/lib/twisty_puzzles/state_helper.rb +20 -21
- data/lib/twisty_puzzles/sticker_cycle.rb +43 -44
- data/lib/twisty_puzzles/utils.rb +3 -0
- data/lib/twisty_puzzles/version.rb +3 -1
- metadata +3 -3
@@ -3,51 +3,50 @@
|
|
3
3
|
require 'twisty_puzzles/utils/array_helper'
|
4
4
|
|
5
5
|
module TwistyPuzzles
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
16
|
+
def name
|
17
|
+
SIMPLE_DIRECTION_NAMES[@value]
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
37
|
-
|
31
|
+
def prepend_rotation(_other, _cube_size)
|
32
|
+
nil
|
33
|
+
end
|
38
34
|
|
39
|
-
|
40
|
-
|
41
|
-
end
|
35
|
+
def prepend_fat_m_slice_move(other, _cube_size)
|
36
|
+
return unless same_axis?(other)
|
42
37
|
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
48
|
+
def prepend_slice_move(_other, _cube_size)
|
49
|
+
nil
|
50
|
+
end
|
56
51
|
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
64
|
+
false
|
65
|
+
end
|
69
66
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
101
|
-
|
99
|
+
@width = width
|
100
|
+
end
|
102
101
|
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
106
|
+
attr_reader :width
|
108
107
|
|
109
|
-
|
110
|
-
|
111
|
-
|
108
|
+
def identifying_fields
|
109
|
+
super + [@width]
|
110
|
+
end
|
112
111
|
|
113
|
-
|
114
|
-
|
115
|
-
|
112
|
+
def to_s
|
113
|
+
"#{@width > 2 ? @width : ''}#{@axis_face.name}#{@width > 1 ? 'w' : ''}#{@direction.name}"
|
114
|
+
end
|
116
115
|
|
117
|
-
|
118
|
-
|
119
|
-
|
116
|
+
def slice_move?
|
117
|
+
false
|
118
|
+
end
|
120
119
|
|
121
|
-
|
122
|
-
|
123
|
-
|
120
|
+
def with_width(width)
|
121
|
+
FatMove.new(@axis_face, @direction, width)
|
122
|
+
end
|
124
123
|
|
125
|
-
|
126
|
-
|
127
|
-
|
124
|
+
def inverted_width(cube_size)
|
125
|
+
cube_size - @width
|
126
|
+
end
|
128
127
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
162
|
+
def prepend_slice_move(other, cube_size)
|
163
|
+
return unless same_axis?(other)
|
185
164
|
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
176
|
+
with_width(@width - 1)
|
177
|
+
else
|
178
|
+
return
|
179
|
+
end
|
180
|
+
Algorithm.move(move)
|
181
|
+
end
|
199
182
|
|
200
|
-
|
201
|
-
end
|
183
|
+
protected
|
202
184
|
|
203
|
-
|
204
|
-
|
205
|
-
|
185
|
+
def merge_with_same_fat_block(other)
|
186
|
+
Algorithm.move(FatMove.new(@axis_face, @direction + other.direction, @width))
|
187
|
+
end
|
206
188
|
|
207
|
-
|
208
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
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
|
-
|
216
|
-
|
217
|
-
end
|
199
|
+
SliceMove.new(@axis_face, @direction, @width - 1)
|
200
|
+
end
|
218
201
|
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
224
|
-
|
225
|
-
@direction == other.direction.inverse
|
226
|
-
end
|
206
|
+
FatMSliceMove.new(@axis_face, @direction)
|
207
|
+
end
|
227
208
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
234
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
248
|
-
|
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
|
-
|
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
|
-
|
253
|
-
|
254
|
-
|
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
|
-
|
257
|
-
|
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
|
-
|
261
|
-
|
262
|
-
end
|
246
|
+
@slice_index = slice_index
|
247
|
+
end
|
263
248
|
|
264
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
251
|
+
def identifying_fields
|
252
|
+
super + [@slice_index]
|
253
|
+
end
|
275
254
|
|
276
|
-
|
277
|
-
|
255
|
+
def to_s
|
256
|
+
"#{@slice_index > 1 ? @slice_index : ''}#{@axis_face.name.downcase}#{@direction.name}"
|
257
|
+
end
|
278
258
|
|
279
|
-
|
280
|
-
|
281
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
293
|
-
|
294
|
-
|
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
|
-
|
297
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
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
|
-
|
307
|
-
|
291
|
+
def prepend_rotation(_other, _cube_size)
|
292
|
+
nil
|
293
|
+
end
|
308
294
|
|
309
|
-
|
310
|
-
|
311
|
-
|
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
|
-
|
316
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
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
|
-
|
314
|
+
other = other.simplified(cube_size)
|
315
|
+
return unless this.same_slice?(other)
|
328
316
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
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
|
-
|
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
|
-
|
343
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
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
|
-
|
355
|
-
|
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
|
-
#
|
363
|
-
|
364
|
-
|
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
|
-
#
|
368
|
-
#
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
-
|
381
|
-
|
382
|
-
end
|
379
|
+
def to_s
|
380
|
+
"#{@axis_face.name.downcase}#{@direction.name}"
|
383
381
|
end
|
382
|
+
end
|
384
383
|
end
|