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