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