twisty_puzzles 0.0.1
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 +7 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/LICENSE +21 -0
- data/README.md +32 -0
- data/ext/twisty_puzzles/native/extconf.rb +5 -0
- data/lib/twisty_puzzles/abstract_direction.rb +54 -0
- data/lib/twisty_puzzles/abstract_move.rb +170 -0
- data/lib/twisty_puzzles/abstract_move_parser.rb +45 -0
- data/lib/twisty_puzzles/algorithm.rb +155 -0
- data/lib/twisty_puzzles/algorithm_transformation.rb +33 -0
- data/lib/twisty_puzzles/axis_face_and_direction_move.rb +78 -0
- data/lib/twisty_puzzles/cancellation_helper.rb +165 -0
- data/lib/twisty_puzzles/color_scheme.rb +174 -0
- data/lib/twisty_puzzles/commutator.rb +118 -0
- data/lib/twisty_puzzles/compiled_algorithm.rb +48 -0
- data/lib/twisty_puzzles/compiled_cube_algorithm.rb +67 -0
- data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +28 -0
- data/lib/twisty_puzzles/coordinate.rb +318 -0
- data/lib/twisty_puzzles/cube.rb +660 -0
- data/lib/twisty_puzzles/cube_constants.rb +53 -0
- data/lib/twisty_puzzles/cube_direction.rb +27 -0
- data/lib/twisty_puzzles/cube_move.rb +384 -0
- data/lib/twisty_puzzles/cube_move_parser.rb +100 -0
- data/lib/twisty_puzzles/cube_print_helper.rb +160 -0
- data/lib/twisty_puzzles/cube_state.rb +113 -0
- data/lib/twisty_puzzles/letter_scheme.rb +72 -0
- data/lib/twisty_puzzles/move_type_creator.rb +27 -0
- data/lib/twisty_puzzles/parser.rb +222 -0
- data/lib/twisty_puzzles/part_cycle_factory.rb +59 -0
- data/lib/twisty_puzzles/puzzle.rb +26 -0
- data/lib/twisty_puzzles/reversible_applyable.rb +37 -0
- data/lib/twisty_puzzles/rotation.rb +105 -0
- data/lib/twisty_puzzles/skewb_direction.rb +24 -0
- data/lib/twisty_puzzles/skewb_move.rb +59 -0
- data/lib/twisty_puzzles/skewb_move_parser.rb +73 -0
- data/lib/twisty_puzzles/skewb_notation.rb +147 -0
- data/lib/twisty_puzzles/skewb_state.rb +163 -0
- data/lib/twisty_puzzles/state_helper.rb +32 -0
- data/lib/twisty_puzzles/sticker_cycle.rb +70 -0
- data/lib/twisty_puzzles/twisty_puzzles_error.rb +6 -0
- data/lib/twisty_puzzles/utils/array_helper.rb +109 -0
- data/lib/twisty_puzzles/utils/string_helper.rb +26 -0
- data/lib/twisty_puzzles/utils.rb +7 -0
- data/lib/twisty_puzzles/version.rb +3 -0
- data/lib/twisty_puzzles.rb +5 -0
- metadata +249 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TwistyPuzzles
|
4
|
+
|
5
|
+
# Class for creating one move type from its parts.
|
6
|
+
# Helper class for parsing logic.
|
7
|
+
class MoveTypeCreator
|
8
|
+
def initialize(capture_keys, move_class)
|
9
|
+
raise TypeError unless move_class.is_a?(Class)
|
10
|
+
raise TypeError unless capture_keys.all? { |k| k.is_a?(Symbol) }
|
11
|
+
|
12
|
+
@capture_keys = capture_keys.freeze
|
13
|
+
@move_class = move_class
|
14
|
+
end
|
15
|
+
|
16
|
+
def applies_to?(parsed_parts)
|
17
|
+
parsed_parts.keys.sort == @capture_keys.sort
|
18
|
+
end
|
19
|
+
|
20
|
+
def create(parsed_parts)
|
21
|
+
raise ArgumentError unless applies_to?(parsed_parts)
|
22
|
+
|
23
|
+
fields = @capture_keys.map { |name| parsed_parts[name] }
|
24
|
+
@move_class.new(*fields)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/algorithm'
|
4
|
+
require 'twisty_puzzles/commutator'
|
5
|
+
require 'twisty_puzzles/cube_move_parser'
|
6
|
+
require 'twisty_puzzles/skewb_move_parser'
|
7
|
+
require 'twisty_puzzles/twisty_puzzles_error'
|
8
|
+
|
9
|
+
module TwistyPuzzles
|
10
|
+
# rubocop:disable Style/Documentation
|
11
|
+
|
12
|
+
# rubocop:enable Style/Documentation
|
13
|
+
class CommutatorParseError < TwistyPuzzlesError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Parser for commutators and algorithms.
|
17
|
+
class Parser
|
18
|
+
OPENING_BRACKET = '['
|
19
|
+
OPENING_PAREN = '('
|
20
|
+
CLOSING_BRACKET = ']'
|
21
|
+
CLOSING_PAREN = ')'
|
22
|
+
TIMES = '*'
|
23
|
+
|
24
|
+
def initialize(alg_string, move_parser)
|
25
|
+
@alg_string = alg_string
|
26
|
+
@scanner = StringScanner.new(alg_string)
|
27
|
+
@move_parser = move_parser
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_open_paren
|
31
|
+
complain('beginning of trigger') unless @scanner.getch == OPENING_PAREN
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_close_paren
|
35
|
+
complain('end of trigger') unless @scanner.getch == CLOSING_PAREN
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_times
|
39
|
+
complain('times symbol of multiplier') unless @scanner.getch == TIMES
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_factor
|
43
|
+
number = @scanner.scan(/\d+/)
|
44
|
+
complain('factor of multiplier') unless number
|
45
|
+
Integer(number, 10)
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_multiplier
|
49
|
+
skip_spaces
|
50
|
+
parse_times
|
51
|
+
skip_spaces
|
52
|
+
parse_factor
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_trigger
|
56
|
+
parse_open_paren
|
57
|
+
skip_spaces
|
58
|
+
moves = parse_moves_with_triggers
|
59
|
+
skip_spaces
|
60
|
+
parse_close_paren
|
61
|
+
skip_spaces
|
62
|
+
case @scanner.peek(1)
|
63
|
+
when TIMES
|
64
|
+
moves * parse_multiplier
|
65
|
+
when ('0'..'9')
|
66
|
+
moves * parse_factor
|
67
|
+
else
|
68
|
+
moves
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Parses at least one move and allows for triggers in parentheses.
|
73
|
+
def parse_moves_with_triggers
|
74
|
+
skip_spaces
|
75
|
+
if @scanner.peek(1) == OPENING_PAREN
|
76
|
+
parse_trigger + parse_moves_with_triggers
|
77
|
+
else
|
78
|
+
parse_moves
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Parses at least one move.
|
83
|
+
def parse_nonempty_moves
|
84
|
+
moves = parse_moves
|
85
|
+
complain('move') if moves.empty?
|
86
|
+
moves
|
87
|
+
end
|
88
|
+
|
89
|
+
# Parses a series of moves.
|
90
|
+
def parse_moves
|
91
|
+
moves = []
|
92
|
+
while (m = begin skip_spaces; parse_move_internal end)
|
93
|
+
moves.push(m)
|
94
|
+
end
|
95
|
+
Algorithm.new(moves)
|
96
|
+
end
|
97
|
+
|
98
|
+
def complain(parsed_object)
|
99
|
+
raise CommutatorParseError, <<~ERROR.chomp
|
100
|
+
Couldn't parse #{parsed_object} here:
|
101
|
+
#{@alg_string}
|
102
|
+
#{' ' * @scanner.pos}^"
|
103
|
+
ERROR
|
104
|
+
end
|
105
|
+
|
106
|
+
def check_eos(parsed_object)
|
107
|
+
complain("end of #{parsed_object}") unless @scanner.eos?
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_open_bracket
|
111
|
+
complain('beginning of commutator') unless @scanner.getch == OPENING_BRACKET
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_close_bracket
|
115
|
+
complain('end of commutator') unless @scanner.getch == CLOSING_BRACKET
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_commutator
|
119
|
+
skip_spaces
|
120
|
+
if @scanner.peek(1) == OPENING_BRACKET
|
121
|
+
parse_commutator_internal
|
122
|
+
else
|
123
|
+
FakeCommutator.new(parse_moves_with_triggers)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse_algorithm
|
128
|
+
skip_spaces
|
129
|
+
parse_moves_with_triggers
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_setup_commutator_inner
|
133
|
+
skip_spaces
|
134
|
+
if @scanner.peek(1) == OPENING_BRACKET
|
135
|
+
parse_pure_commutator
|
136
|
+
else
|
137
|
+
FakeCommutator.new(parse_moves_with_triggers)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def parse_pure_commutator
|
142
|
+
skip_spaces
|
143
|
+
parse_open_bracket
|
144
|
+
first_part = parse_nonempty_moves
|
145
|
+
skip_spaces
|
146
|
+
complain('middle of pure commutator') unless @scanner.getch == ','
|
147
|
+
second_part = parse_nonempty_moves
|
148
|
+
skip_spaces
|
149
|
+
parse_close_bracket
|
150
|
+
PureCommutator.new(first_part, second_part)
|
151
|
+
end
|
152
|
+
|
153
|
+
def parse_commutator_internal_after_separator(setup_or_first_part, separator)
|
154
|
+
if [':', ';'].include?(separator)
|
155
|
+
inner_commutator = parse_setup_commutator_inner
|
156
|
+
SetupCommutator.new(setup_or_first_part, inner_commutator)
|
157
|
+
elsif separator == ','
|
158
|
+
second_part = parse_nonempty_moves
|
159
|
+
PureCommutator.new(setup_or_first_part, second_part)
|
160
|
+
else
|
161
|
+
complain('end of setup or middle of pure commutator') unless @scanner.eos?
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse_commutator_internal
|
166
|
+
skip_spaces
|
167
|
+
parse_open_bracket
|
168
|
+
setup_or_first_part = parse_nonempty_moves
|
169
|
+
skip_spaces
|
170
|
+
separator = @scanner.getch
|
171
|
+
comm = parse_commutator_internal_after_separator(setup_or_first_part, separator)
|
172
|
+
skip_spaces
|
173
|
+
parse_close_bracket
|
174
|
+
skip_spaces
|
175
|
+
complain('end of commutator') unless @scanner.eos?
|
176
|
+
comm
|
177
|
+
end
|
178
|
+
|
179
|
+
def parse_move_internal
|
180
|
+
move = @scanner.scan(@move_parser.regexp)
|
181
|
+
return unless move
|
182
|
+
|
183
|
+
@move_parser.parse_move(move)
|
184
|
+
end
|
185
|
+
|
186
|
+
def skip_spaces
|
187
|
+
@scanner.skip(/\s+/)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def parse_commutator(alg_string, complete_parse = true)
|
192
|
+
parser = Parser.new(alg_string, CubeMoveParser::INSTANCE)
|
193
|
+
commutator = parser.parse_commutator
|
194
|
+
parser.check_eos('commutator') if complete_parse
|
195
|
+
commutator
|
196
|
+
end
|
197
|
+
|
198
|
+
def parse_cube_algorithm(alg_string, complete_parse = true)
|
199
|
+
parser = Parser.new(alg_string, CubeMoveParser::INSTANCE)
|
200
|
+
algorithm = parser.parse_algorithm
|
201
|
+
parser.check_eos('algorithm') if complete_parse
|
202
|
+
algorithm
|
203
|
+
end
|
204
|
+
|
205
|
+
def parse_cube_move(move_string)
|
206
|
+
CubeMoveParser::INSTANCE.parse_move(move_string)
|
207
|
+
end
|
208
|
+
|
209
|
+
alias parse_algorithm parse_cube_algorithm
|
210
|
+
alias parse_move parse_cube_move
|
211
|
+
|
212
|
+
def parse_skewb_algorithm(alg_string, notation, complete_parse = true)
|
213
|
+
parser = Parser.new(alg_string, SkewbMoveParser.new(notation))
|
214
|
+
algorithm = parser.parse_algorithm
|
215
|
+
parser.check_eos('algorithm') if complete_parse
|
216
|
+
algorithm
|
217
|
+
end
|
218
|
+
|
219
|
+
def parse_skewb_move(move_string, notation)
|
220
|
+
SkewbMoveParser.new(notation).parse_move(move_string)
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/sticker_cycle'
|
4
|
+
require 'twisty_puzzles/utils/array_helper'
|
5
|
+
|
6
|
+
module TwistyPuzzles
|
7
|
+
|
8
|
+
# Factory for sticker cycles given part cycles.
|
9
|
+
class PartCycleFactory
|
10
|
+
include Utils::ArrayHelper
|
11
|
+
|
12
|
+
def initialize(cube_size, incarnation_index)
|
13
|
+
CubeState.check_cube_size(cube_size)
|
14
|
+
unless incarnation_index.is_a?(Integer) && incarnation_index >= 0
|
15
|
+
raise ArgumentError, "Invalid incarnation index #{incarnation_index}."
|
16
|
+
end
|
17
|
+
|
18
|
+
@cube_size = cube_size
|
19
|
+
@incarnation_index = incarnation_index
|
20
|
+
@cache = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def coordinates(part)
|
24
|
+
@cache[part] ||= Coordinate.solved_positions(part, @cube_size, @incarnation_index)
|
25
|
+
end
|
26
|
+
|
27
|
+
def multi_twist(parts)
|
28
|
+
unless parts.all? { |p| p.is_a?(Corner) || p.is_a?(Edge) }
|
29
|
+
raise TypeError, 'Twists are only supported for edges and corners.'
|
30
|
+
end
|
31
|
+
|
32
|
+
cycles = parts.map { |p| StickerCycle.new(@cube_size, coordinates(p)) }
|
33
|
+
StickerCycles.new(@cube_size, cycles)
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_type_consistency(parts)
|
37
|
+
return unless parts.any? { |p| p.class != parts.first.class }
|
38
|
+
|
39
|
+
raise TypeError, "Cycles of heterogenous piece types #{parts.inspect} are not supported."
|
40
|
+
end
|
41
|
+
|
42
|
+
def construct(parts)
|
43
|
+
if parts.length < 2
|
44
|
+
raise ArgumentError, 'Cycles of length smaller than 2 are not supported.'
|
45
|
+
end
|
46
|
+
|
47
|
+
unless @incarnation_index < parts.first.num_incarnations(@cube_size)
|
48
|
+
raise ArgumentError, "Incarnation index #{@incarnation_index} for cube size " \
|
49
|
+
"#{@cube_size} is not supported for #{parts.first.inspect}."
|
50
|
+
end
|
51
|
+
|
52
|
+
check_types(parts, Part)
|
53
|
+
check_type_consistency(parts)
|
54
|
+
part_coordinates = parts.map { |p| coordinates(p) }
|
55
|
+
cycles = part_coordinates.transpose.map { |c| StickerCycle.new(@cube_size, c) }
|
56
|
+
StickerCycles.new(@cube_size, cycles)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TwistyPuzzles
|
4
|
+
|
5
|
+
# Represents one type of puzzle.
|
6
|
+
class Puzzle
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
NXN_CUBE = Puzzle.new('nxn cube')
|
12
|
+
SKEWB = Puzzle.new('skewb')
|
13
|
+
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
def eql?(other)
|
17
|
+
self.class == other.class && name == other.name
|
18
|
+
end
|
19
|
+
|
20
|
+
def hash
|
21
|
+
@hash ||= [self.class, @name].hash
|
22
|
+
end
|
23
|
+
|
24
|
+
alias == eql?
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/skewb_state'
|
4
|
+
|
5
|
+
module TwistyPuzzles
|
6
|
+
|
7
|
+
# Module that makes a class that has an `apply_to` and `reverse` be able to apply temporarily.
|
8
|
+
module ReversibleApplyable
|
9
|
+
def apply_to_dupped(puzzle_state)
|
10
|
+
dupped = puzzle_state.dup
|
11
|
+
apply_to(dupped)
|
12
|
+
dupped
|
13
|
+
end
|
14
|
+
|
15
|
+
# Applies the current algorithm/cycle/whatever to the given puzzle state and yields the
|
16
|
+
# modified version. The puzzle state will be the same as the original after this function
|
17
|
+
# returns.
|
18
|
+
# Whether the yielded puzzle state is actually the same as the passed one or a copy is an
|
19
|
+
# implementation detail.
|
20
|
+
def apply_temporarily_to(puzzle_state)
|
21
|
+
return yield(apply_to_dupped(puzzle_state)) if with_dup_is_faster?(puzzle_state)
|
22
|
+
|
23
|
+
apply_to(puzzle_state)
|
24
|
+
begin
|
25
|
+
yield(puzzle_state)
|
26
|
+
ensure
|
27
|
+
inverse.apply_to(puzzle_state)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def with_dup_is_faster?(state)
|
34
|
+
!state.is_a?(CubeState) || state.n <= 4
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/algorithm'
|
4
|
+
require 'twisty_puzzles/axis_face_and_direction_move'
|
5
|
+
require 'twisty_puzzles/cube'
|
6
|
+
require 'twisty_puzzles/cube_direction'
|
7
|
+
require 'twisty_puzzles/cube_move'
|
8
|
+
require 'twisty_puzzles/puzzle'
|
9
|
+
|
10
|
+
module TwistyPuzzles
|
11
|
+
|
12
|
+
# A rotation of a Skewb or cube.
|
13
|
+
class Rotation < AxisFaceAndDirectionMove
|
14
|
+
ALL_ROTATIONS = Face::ELEMENTS.product(CubeDirection::ALL_DIRECTIONS).map { |f, d| new(f, d) }
|
15
|
+
NON_ZERO_ROTATIONS =
|
16
|
+
Face::ELEMENTS.product(CubeDirection::NON_ZERO_DIRECTIONS).map { |f, d| new(f, d) }
|
17
|
+
LEFT = new(Face::U, CubeDirection::BACKWARD)
|
18
|
+
RIGHT = new(Face::U, CubeDirection::FORWARD)
|
19
|
+
|
20
|
+
# Translates a Skewb direction into a cube direction.
|
21
|
+
def self.translated_direction(direction)
|
22
|
+
case direction
|
23
|
+
when SkewbDirection::ZERO then CubeDirection::ZERO
|
24
|
+
when SkewbDirection::FORWARD then CubeDirection::FORWARD
|
25
|
+
when SkewbDirection::BACKWARD then CubeDirection::BACKWARD
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an algorithm consisting of two rotations that are equivalent to rotating
|
30
|
+
# the puzzle around a corner.
|
31
|
+
# Takes a Skewb direction as an argument (even for cubes) because rotating around
|
32
|
+
# is like a Skewb move given that it's modulo 3.
|
33
|
+
def self.around_corner(corner, skewb_direction)
|
34
|
+
raise TypeError unless corner.is_a?(Corner)
|
35
|
+
raise TypeError unless skewb_direction.is_a?(SkewbDirection)
|
36
|
+
|
37
|
+
direction = translated_direction(skewb_direction)
|
38
|
+
|
39
|
+
Algorithm.new([
|
40
|
+
Rotation.new(corner.faces[skewb_direction.value], direction),
|
41
|
+
Rotation.new(corner.faces[0], direction)
|
42
|
+
])
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"#{AXES[@axis_face.axis_priority]}#{canonical_direction.name}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def puzzles
|
50
|
+
[Puzzle::SKEWB, Puzzle::NXN_CUBE]
|
51
|
+
end
|
52
|
+
|
53
|
+
def slice_move?
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns an alternative representation of the same rotation
|
58
|
+
def alternative
|
59
|
+
Rotation.new(@axis_face.opposite, @direction.inverse)
|
60
|
+
end
|
61
|
+
|
62
|
+
def equivalent_internal?(other, _cube_size)
|
63
|
+
[self, alternative].include?(other)
|
64
|
+
end
|
65
|
+
|
66
|
+
def prepend_rotation(other, _cube_size)
|
67
|
+
if same_axis?(other)
|
68
|
+
direction = translated_direction(other.axis_face)
|
69
|
+
Algorithm.move(Rotation.new(other.axis_face, direction + other.direction))
|
70
|
+
elsif @direction.double_move? && other.direction.double_move?
|
71
|
+
used_axis_priorities = [@axis_face, other.axis_face].map(&:axis_priority)
|
72
|
+
# Note that there are two solutions, but any works.
|
73
|
+
remaining_face =
|
74
|
+
Face::ELEMENTS.find { |f| !used_axis_priorities.include?(f.axis_priority) }
|
75
|
+
Algorithm.move(Rotation.new(remaining_face, CubeDirection::DOUBLE))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def prepend_fat_m_slice_move(_other, _cube_size)
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def prepend_fat_move(other, cube_size)
|
84
|
+
return unless compatible_fat_move?(other)
|
85
|
+
|
86
|
+
Algorithm.move(
|
87
|
+
FatMove.new(other.axis_face.opposite, other.direction, other.inverted_width(cube_size))
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def prepend_slice_move(_other, _cube_size)
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def move_count(_cube_size, _metric = :htm)
|
96
|
+
0
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def compatible_fat_move?(other)
|
102
|
+
same_axis?(other) && translated_direction(other.axis_face) == other.direction.inverse
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|