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,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
require 'twisty_puzzles/algorithm'
|
5
|
+
require 'twisty_puzzles/cube'
|
6
|
+
|
7
|
+
module TwistyPuzzles
|
8
|
+
|
9
|
+
# Base class for Commutators.
|
10
|
+
class Commutator
|
11
|
+
def cancellations(other, cube_size, metric = :htm)
|
12
|
+
algorithm.cancellations(other.algorithm, cube_size, metric)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Algorithm that is used like a commutator but actually isn't one.
|
17
|
+
class FakeCommutator < Commutator
|
18
|
+
def initialize(algorithm)
|
19
|
+
raise ArgumentError unless algorithm.is_a?(Algorithm)
|
20
|
+
|
21
|
+
@algorithm = algorithm
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :algorithm
|
25
|
+
|
26
|
+
def eql?(other)
|
27
|
+
self.class.equal?(other.class) && @algorithm == other.algorithm
|
28
|
+
end
|
29
|
+
|
30
|
+
alias == eql?
|
31
|
+
|
32
|
+
def hash
|
33
|
+
@hash ||= [self.class, @algorithm].hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def inverse
|
37
|
+
FakeCommutator.new(@algorithm.inverse)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@algorithm.to_s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Pure commutator of the form A B A' B'.
|
46
|
+
class PureCommutator < Commutator
|
47
|
+
def initialize(first_part, second_part)
|
48
|
+
raise ArgumentError unless first_part.is_a?(Algorithm)
|
49
|
+
raise ArgumentError unless second_part.is_a?(Algorithm)
|
50
|
+
|
51
|
+
@first_part = first_part
|
52
|
+
@second_part = second_part
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :first_part, :second_part
|
56
|
+
|
57
|
+
def eql?(other)
|
58
|
+
self.class.equal?(other.class) && @first_part == other.first_part &&
|
59
|
+
@second_part == other.second_part
|
60
|
+
end
|
61
|
+
|
62
|
+
alias == eql?
|
63
|
+
|
64
|
+
def hash
|
65
|
+
@hash ||= [self.class, @first_part, @second_part].hash
|
66
|
+
end
|
67
|
+
|
68
|
+
def inverse
|
69
|
+
PureCommutator.new(second_part, first_part)
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
"[#{@first_part}, #{@second_part}]"
|
74
|
+
end
|
75
|
+
|
76
|
+
def algorithm
|
77
|
+
first_part + second_part + first_part.inverse + second_part.inverse
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Setup commutator of the form A B A'.
|
82
|
+
class SetupCommutator < Commutator
|
83
|
+
def initialize(setup, inner_commutator)
|
84
|
+
raise ArgumentError, 'Setup move has to be an algorithm.' unless setup.is_a?(Algorithm)
|
85
|
+
unless inner_commutator.is_a?(Commutator)
|
86
|
+
raise ArgumentError, 'Inner commutator has to be a commutator.'
|
87
|
+
end
|
88
|
+
|
89
|
+
@setup = setup
|
90
|
+
@inner_commutator = inner_commutator
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_reader :setup, :inner_commutator
|
94
|
+
|
95
|
+
def eql?(other)
|
96
|
+
self.class.equal?(other.class) && @setup == other.setup &&
|
97
|
+
@inner_commutator == other.inner_commutator
|
98
|
+
end
|
99
|
+
|
100
|
+
alias == eql?
|
101
|
+
|
102
|
+
def hash
|
103
|
+
@hash ||= [self.class, @setup, @inner_commutator].hash
|
104
|
+
end
|
105
|
+
|
106
|
+
def inverse
|
107
|
+
SetupCommutator.new(setup, @inner_commutator.inverse)
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_s
|
111
|
+
"[#{@setup} : #{@inner_commutator}]"
|
112
|
+
end
|
113
|
+
|
114
|
+
def algorithm
|
115
|
+
setup + inner_commutator.algorithm + setup.inverse
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/reversible_applyable'
|
4
|
+
|
5
|
+
module TwistyPuzzles
|
6
|
+
|
7
|
+
# Base class for a compiled algorithm for a particular puzzle.
|
8
|
+
class CompiledAlgorithm
|
9
|
+
include ReversibleApplyable
|
10
|
+
|
11
|
+
def initialize(native)
|
12
|
+
raise TypeError unless native.is_a?(self.class::NATIVE_CLASS)
|
13
|
+
|
14
|
+
@native = native
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :native
|
18
|
+
|
19
|
+
def rotate_by(rotation)
|
20
|
+
self.class.new(@native.rotate_by(rotation.axis_face.face_symbol, rotation.direction.value))
|
21
|
+
end
|
22
|
+
|
23
|
+
def mirror(normal_face)
|
24
|
+
self.class.new(@native.mirror(normal_face.face_symbol))
|
25
|
+
end
|
26
|
+
|
27
|
+
def inverse
|
28
|
+
@inverse ||=
|
29
|
+
begin
|
30
|
+
alg = self.class.new(@native.inverse)
|
31
|
+
alg.inverse = self
|
32
|
+
alg
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def +(other)
|
37
|
+
self.class.new(@native + other.native)
|
38
|
+
end
|
39
|
+
|
40
|
+
def apply_to(state)
|
41
|
+
@native.apply_to(state.native)
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
attr_writer :inverse
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/compiled_algorithm'
|
4
|
+
|
5
|
+
module TwistyPuzzles
|
6
|
+
|
7
|
+
# Wrapper of the native C implementation of a compiled algorithm for a particular cube size.
|
8
|
+
class CompiledCubeAlgorithm < CompiledAlgorithm
|
9
|
+
def self.transform_rotation(move, cube_size)
|
10
|
+
slice_moves =
|
11
|
+
0.upto(cube_size - 1).map do |i|
|
12
|
+
[:slice, move.axis_face.face_symbol, move.direction.value, i]
|
13
|
+
end
|
14
|
+
[
|
15
|
+
[:face, move.axis_face.face_symbol, move.direction.value],
|
16
|
+
[:face, move.axis_face.opposite.face_symbol, move.direction.inverse.value]
|
17
|
+
] + slice_moves
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.transform_fat_mslice_move(move, cube_size)
|
21
|
+
1.upto(cube_size - 2).map do |i|
|
22
|
+
[:slice, move.axis_face.face_symbol, move.direction.value, i]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.transform_slice_move(move)
|
27
|
+
[
|
28
|
+
[:slice, move.axis_face.face_symbol, move.direction.value, move.slice_index]
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.transform_fat_move(move)
|
33
|
+
slice_moves =
|
34
|
+
0.upto(move.width - 1).map do |i|
|
35
|
+
[:slice, move.axis_face.face_symbol, move.direction.value, i]
|
36
|
+
end
|
37
|
+
[
|
38
|
+
[:face, move.axis_face.face_symbol, move.direction.value]
|
39
|
+
] + slice_moves
|
40
|
+
end
|
41
|
+
|
42
|
+
private_class_method :transform_rotation, :transform_fat_mslice_move, :transform_slice_move,
|
43
|
+
:transform_fat_move
|
44
|
+
|
45
|
+
def self.transform_move(move, cube_size)
|
46
|
+
decided_move = move.decide_meaning(cube_size)
|
47
|
+
case decided_move
|
48
|
+
when Rotation then transform_rotation(decided_move, cube_size)
|
49
|
+
when FatMSliceMove then transform_fat_mslice_move(decided_move, cube_size)
|
50
|
+
# Note that this also covers InnerMSliceMove
|
51
|
+
when SliceMove then transform_slice_move(decided_move)
|
52
|
+
when FatMove then transform_fat_move(decided_move)
|
53
|
+
else
|
54
|
+
raise TypeError, "Invalid move type #{move.class} that becomes #{decided_move.class} "\
|
55
|
+
"for cube size #{cube_size}."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.for_moves(cube_size, moves)
|
60
|
+
transformed_moves = moves.collect_concat { |m| transform_move(m, cube_size) }
|
61
|
+
native = Native::CubeAlgorithm.new(cube_size, transformed_moves)
|
62
|
+
new(native)
|
63
|
+
end
|
64
|
+
|
65
|
+
NATIVE_CLASS = Native::CubeAlgorithm
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/compiled_algorithm'
|
4
|
+
|
5
|
+
module TwistyPuzzles
|
6
|
+
|
7
|
+
# Wrapper of the native C implementation of a compiled algorithm for a particular cube size.
|
8
|
+
class CompiledSkewbAlgorithm < CompiledAlgorithm
|
9
|
+
def self.transform_move(move)
|
10
|
+
case move
|
11
|
+
when Rotation
|
12
|
+
[:rotation, move.axis_face.face_symbol, move.direction.value]
|
13
|
+
when SkewbMove
|
14
|
+
[:move, move.axis_corner.face_symbols, move.direction.value]
|
15
|
+
else
|
16
|
+
raise TypeError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.for_moves(moves)
|
21
|
+
native = Native::SkewbAlgorithm.new(moves.map { |m| transform_move(m) })
|
22
|
+
new(native)
|
23
|
+
end
|
24
|
+
|
25
|
+
NATIVE_CLASS = Native::SkewbAlgorithm
|
26
|
+
EMPTY = for_moves([])
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'twisty_puzzles/cube'
|
4
|
+
require 'twisty_puzzles/cube_constants'
|
5
|
+
require 'twisty_puzzles/native'
|
6
|
+
|
7
|
+
module TwistyPuzzles
|
8
|
+
|
9
|
+
# Coordinate of a sticker on the cube.
|
10
|
+
class Coordinate
|
11
|
+
def self.highest_coordinate(cube_size)
|
12
|
+
cube_size - 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.invert_coordinate(index, cube_size)
|
16
|
+
highest_coordinate(cube_size) - index
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.coordinate_range(cube_size)
|
20
|
+
0.upto(highest_coordinate(cube_size))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.middle(cube_size)
|
24
|
+
raise ArgumentError if cube_size.even?
|
25
|
+
|
26
|
+
cube_size / 2
|
27
|
+
end
|
28
|
+
|
29
|
+
# Middle coordinate for uneven numbers, the one before for even numbers
|
30
|
+
def self.middle_or_before(cube_size)
|
31
|
+
cube_size - cube_size / 2 - 1
|
32
|
+
end
|
33
|
+
|
34
|
+
# Middle coordinate for uneven numbers, the one after for even numbers
|
35
|
+
def self.middle_or_after(cube_size)
|
36
|
+
cube_size / 2
|
37
|
+
end
|
38
|
+
|
39
|
+
# The last coordinate that is strictly before the middle
|
40
|
+
def self.last_before_middle(cube_size)
|
41
|
+
cube_size / 2 - 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.canonicalize(index, cube_size)
|
45
|
+
raise ArgumentError unless index.is_a?(Integer) && -cube_size <= index && index < cube_size
|
46
|
+
|
47
|
+
index >= 0 ? index : cube_size + index
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.from_face_distances(face, cube_size, face_distances)
|
51
|
+
raise ArgumentError if face_distances.length != 2
|
52
|
+
|
53
|
+
coordinates = [nil, nil]
|
54
|
+
face_distances.each do |neighbor, distance|
|
55
|
+
index = face.coordinate_index_close_to(neighbor)
|
56
|
+
coordinate =
|
57
|
+
if neighbor.close_to_smaller_indices?
|
58
|
+
distance
|
59
|
+
else
|
60
|
+
invert_coordinate(distance, cube_size)
|
61
|
+
end
|
62
|
+
raise ArgumentError if coordinates[index]
|
63
|
+
|
64
|
+
coordinates[index] = coordinate
|
65
|
+
end
|
66
|
+
raise ArgumentError if coordinates.any?(&:nil?)
|
67
|
+
|
68
|
+
from_indices(face, cube_size, *coordinates)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.match_coordinate_internal(base_coordinate, other_face_symbols)
|
72
|
+
other_face_symbols.sort!
|
73
|
+
coordinate =
|
74
|
+
base_coordinate.rotations.find do |coord|
|
75
|
+
face_symbols_closeby = coord.close_neighbor_faces.map(&:face_symbol)
|
76
|
+
face_symbols_closeby.sort == other_face_symbols
|
77
|
+
end
|
78
|
+
raise "Couldn't find a fitting coordinate on the solved face." if coordinate.nil?
|
79
|
+
|
80
|
+
coordinate
|
81
|
+
end
|
82
|
+
|
83
|
+
# The coordinate of the solved position of the main sticker of this part.
|
84
|
+
def self.solved_position(part, cube_size, incarnation_index)
|
85
|
+
raise TypeError unless part.is_a?(Part)
|
86
|
+
raise unless part.class::ELEMENTS.length == 24
|
87
|
+
raise unless incarnation_index >= 0 && incarnation_index < part.num_incarnations(cube_size)
|
88
|
+
|
89
|
+
# This is a coordinate on the same face and belonging to an equivalent part.
|
90
|
+
# But it might not be the right one.
|
91
|
+
base_coordinate = Coordinate.from_indices(
|
92
|
+
part.solved_face, cube_size, *part.base_index_on_face(cube_size, incarnation_index)
|
93
|
+
)
|
94
|
+
other_face_symbols = part.corresponding_part.face_symbols[1..-1]
|
95
|
+
match_coordinate_internal(base_coordinate, other_face_symbols)
|
96
|
+
end
|
97
|
+
|
98
|
+
# The coordinate of the solved position of all stickers of this part.
|
99
|
+
# rubocop:disable Metrics/AbcSize
|
100
|
+
def self.solved_positions(part, cube_size, incarnation_index)
|
101
|
+
solved_coordinate = solved_position(part, cube_size, incarnation_index)
|
102
|
+
other_coordinates =
|
103
|
+
part.face_symbols[1..-1].map.with_index do |f, i|
|
104
|
+
face = Face.for_face_symbol(f)
|
105
|
+
# The reverse is important for edge like parts. We are not in the same position as usual
|
106
|
+
# solved pieces would be.
|
107
|
+
# For other types of pieces, it doesn't make a difference as the base index will just be
|
108
|
+
# a rotation of the original one, but we will anyway look at all rotations later.
|
109
|
+
base_indices = part.base_index_on_other_face(face, cube_size, incarnation_index).reverse
|
110
|
+
base_coordinate = Coordinate.from_indices(face, cube_size, *base_indices)
|
111
|
+
other_face_symbols = [part.face_symbols[0]] +
|
112
|
+
part.corresponding_part.face_symbols[1...i + 1] +
|
113
|
+
part.corresponding_part.face_symbols[i + 2..-1]
|
114
|
+
match_coordinate_internal(base_coordinate, other_face_symbols)
|
115
|
+
end
|
116
|
+
[solved_coordinate] + other_coordinates
|
117
|
+
end
|
118
|
+
# rubocop:enable Metrics/AbcSize
|
119
|
+
|
120
|
+
def self.center(face, cube_size)
|
121
|
+
m = middle(cube_size)
|
122
|
+
from_indices(face, cube_size, m, m)
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.edges_outside(face, cube_size)
|
126
|
+
face.neighbors.zip(face.neighbors.rotate(1)).collect_concat do |neighbor, next_neighbor|
|
127
|
+
1.upto(cube_size - 2).map do |i|
|
128
|
+
from_face_distances(neighbor, cube_size, face => 0, next_neighbor => i)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.from_indices(face, cube_size, x_index, y_index)
|
134
|
+
raise TypeError, "Unsuitable face #{face.inspect}." unless face.is_a?(Face)
|
135
|
+
raise TypeError unless cube_size.is_a?(Integer)
|
136
|
+
raise ArgumentError unless cube_size.positive?
|
137
|
+
|
138
|
+
x = Coordinate.canonicalize(x_index, cube_size)
|
139
|
+
y = Coordinate.canonicalize(y_index, cube_size)
|
140
|
+
native = Native::CubeCoordinate.new(
|
141
|
+
cube_size,
|
142
|
+
face.face_symbol,
|
143
|
+
face.coordinate_index_base_face(0).face_symbol,
|
144
|
+
face.coordinate_index_base_face(1).face_symbol,
|
145
|
+
x,
|
146
|
+
y
|
147
|
+
)
|
148
|
+
new(native)
|
149
|
+
end
|
150
|
+
|
151
|
+
private_class_method :new
|
152
|
+
|
153
|
+
def initialize(native)
|
154
|
+
raise TypeError unless native.is_a?(Native::CubeCoordinate)
|
155
|
+
|
156
|
+
@native = native
|
157
|
+
end
|
158
|
+
|
159
|
+
attr_reader :native
|
160
|
+
|
161
|
+
def face
|
162
|
+
@face ||= Face.for_face_symbol(@native.face)
|
163
|
+
end
|
164
|
+
|
165
|
+
def cube_size
|
166
|
+
@cube_size ||= @native.cube_size
|
167
|
+
end
|
168
|
+
|
169
|
+
def coordinate(coordinate_index)
|
170
|
+
native.coordinate(face.coordinate_index_base_face(coordinate_index).face_symbol)
|
171
|
+
end
|
172
|
+
|
173
|
+
def coordinates
|
174
|
+
@coordinates ||= [x, y].freeze
|
175
|
+
end
|
176
|
+
|
177
|
+
def x
|
178
|
+
@x ||= coordinate(0)
|
179
|
+
end
|
180
|
+
|
181
|
+
def y
|
182
|
+
@y ||= coordinate(1)
|
183
|
+
end
|
184
|
+
|
185
|
+
def eql?(other)
|
186
|
+
self.class.equal?(other.class) && @native == other.native
|
187
|
+
end
|
188
|
+
|
189
|
+
alias == eql?
|
190
|
+
|
191
|
+
def hash
|
192
|
+
[self.class, @native].hash
|
193
|
+
end
|
194
|
+
|
195
|
+
def can_jump_to?(to_face)
|
196
|
+
raise ArgumentError unless to_face.is_a?(Face)
|
197
|
+
|
198
|
+
jump_coordinate_index = face.coordinate_index_close_to(to_face)
|
199
|
+
jump_coordinate = coordinates[jump_coordinate_index]
|
200
|
+
(jump_coordinate.zero? && to_face.close_to_smaller_indices?) ||
|
201
|
+
(jump_coordinate == Coordinate.highest_coordinate(cube_size) &&
|
202
|
+
!to_face.close_to_smaller_indices?)
|
203
|
+
end
|
204
|
+
|
205
|
+
def jump_to_neighbor(to_face)
|
206
|
+
raise ArgumentError unless to_face.is_a?(Face)
|
207
|
+
raise ArgumentError unless face.neighbors.include?(to_face)
|
208
|
+
raise ArgumentError unless can_jump_to?(to_face)
|
209
|
+
|
210
|
+
new_coordinates = coordinates.dup
|
211
|
+
new_coordinate_index = to_face.coordinate_index_close_to(face)
|
212
|
+
new_coordinate = make_coordinate_at_edge_to(face)
|
213
|
+
new_coordinates.insert(new_coordinate_index, new_coordinate)
|
214
|
+
Coordinate.from_indices(to_face, cube_size, *new_coordinates)
|
215
|
+
end
|
216
|
+
|
217
|
+
def jump_to_coordinates(new_coordinates)
|
218
|
+
Coordinate.from_indices(@face, @cube_size, *new_coordinates)
|
219
|
+
end
|
220
|
+
|
221
|
+
def make_coordinate_at_edge_to(face)
|
222
|
+
face.close_to_smaller_indices? ? 0 : Coordinate.highest_coordinate(cube_size)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns neighbor faces that are closer to this coordinate than their opposite face.
|
226
|
+
def close_neighbor_faces
|
227
|
+
face.neighbors.select do |neighbor|
|
228
|
+
coordinate = coordinates[face.coordinate_index_close_to(neighbor)]
|
229
|
+
if neighbor.close_to_smaller_indices?
|
230
|
+
before_middle?(coordinate)
|
231
|
+
else
|
232
|
+
after_middle?(coordinate)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def after_middle?(index)
|
238
|
+
Coordinate.canonicalize(index, cube_size) > Coordinate.middle_or_before(cube_size)
|
239
|
+
end
|
240
|
+
|
241
|
+
def before_middle?(index)
|
242
|
+
Coordinate.canonicalize(index, cube_size) <= Coordinate.last_before_middle(cube_size)
|
243
|
+
end
|
244
|
+
|
245
|
+
# On a nxn grid with integer coordinates between 0 and n - 1, iterates between the 4 points
|
246
|
+
# that point (x, y) hits if you rotate by 90 degrees.
|
247
|
+
def rotate
|
248
|
+
jump_to_coordinates([y, Coordinate.invert_coordinate(x, cube_size)])
|
249
|
+
end
|
250
|
+
|
251
|
+
# On a nxn grid with integer coordinates between 0 and n - 1, give the 4 points that point
|
252
|
+
# (x, y) hits if you do a full rotation of the face in clockwise order.
|
253
|
+
def rotations
|
254
|
+
rots = []
|
255
|
+
current = self
|
256
|
+
4.times do
|
257
|
+
rots.push(current)
|
258
|
+
current = current.rotate
|
259
|
+
end
|
260
|
+
raise unless current == self
|
261
|
+
|
262
|
+
rots
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Coordinate of a sticker on the Skewb.
|
267
|
+
class SkewbCoordinate
|
268
|
+
include Comparable
|
269
|
+
include CubeConstants
|
270
|
+
def initialize(face, coordinate, native)
|
271
|
+
raise ArgumentError, "Unsuitable face #{face.inspect}." unless face.is_a?(Face)
|
272
|
+
unless coordinate.is_a?(Integer) && coordinate >= 0 && coordinate < SKEWB_STICKERS
|
273
|
+
raise ArgumentError
|
274
|
+
end
|
275
|
+
|
276
|
+
@coordinate = coordinate
|
277
|
+
@native = native
|
278
|
+
end
|
279
|
+
|
280
|
+
attr_reader :native
|
281
|
+
|
282
|
+
private_class_method :new
|
283
|
+
|
284
|
+
def self.for_center(face)
|
285
|
+
native = Native::SkewbCoordinate.for_center(face.face_symbol)
|
286
|
+
new(face, 0, native)
|
287
|
+
end
|
288
|
+
|
289
|
+
def self.corners_on_face(face)
|
290
|
+
face.clockwise_corners.map { |c| for_corner(c) }
|
291
|
+
end
|
292
|
+
|
293
|
+
def self.for_corner(corner)
|
294
|
+
native = Native::SkewbCoordinate.for_corner(corner.face_symbols)
|
295
|
+
new(Face.for_face_symbol(corner.face_symbols.first), 1 + corner.piece_index % 4, native)
|
296
|
+
end
|
297
|
+
|
298
|
+
def hash
|
299
|
+
@hash ||= [self.class, @native].hash
|
300
|
+
end
|
301
|
+
|
302
|
+
def eql?(other)
|
303
|
+
@native.eql?(other.native)
|
304
|
+
end
|
305
|
+
|
306
|
+
alias == eql?
|
307
|
+
|
308
|
+
def <=>(other)
|
309
|
+
@native <=> other.native
|
310
|
+
end
|
311
|
+
|
312
|
+
def face
|
313
|
+
@face ||= Face.for_face_symbol(@native.face)
|
314
|
+
end
|
315
|
+
|
316
|
+
attr_reader :coordinate
|
317
|
+
end
|
318
|
+
end
|