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
@@ -4,30 +4,28 @@ require 'twisty_puzzles/cube_direction'
|
|
4
4
|
require 'twisty_puzzles/rotation'
|
5
5
|
|
6
6
|
module TwistyPuzzles
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
7
|
+
AlgorithmTransformation =
|
8
|
+
Struct.new(:rotation, :mirror, :mirror_normal_face) do
|
9
|
+
def transformed(algorithm)
|
10
|
+
algorithm = algorithm.mirror(mirror_normal_face) if mirror
|
11
|
+
algorithm.rotate_by(rotation)
|
12
|
+
end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
def identity?
|
15
|
+
rotation.identity? && !mirror
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
18
|
+
# Returns algorithm transformations that mirror an algorithm and rotate it around a face.
|
19
|
+
def self.around_face(face)
|
20
|
+
around_face_rotations = CubeDirection::ALL_DIRECTIONS.map { |d| Rotation.new(face, d) }
|
21
|
+
mirror_normal_face = face.neighbors.first
|
22
|
+
around_face_rotations.product([true, false]).map do |r, m|
|
23
|
+
AlgorithmTransformation.new(r, m, mirror_normal_face)
|
26
24
|
end
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
end
|
27
|
+
def self.around_face_without_identity(face)
|
28
|
+
around_face(face).reject(&:identity?)
|
31
29
|
end
|
32
|
-
|
30
|
+
end
|
33
31
|
end
|
@@ -4,75 +4,74 @@ require 'twisty_puzzles/abstract_move'
|
|
4
4
|
require 'twisty_puzzles/cube_direction'
|
5
5
|
|
6
6
|
module TwistyPuzzles
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
raise TypeError unless direction.is_a?(CubeDirection)
|
7
|
+
# Intermediate base class for all types of moves that have an axis face and a direction,
|
8
|
+
# i.e. cube moves and rotations.
|
9
|
+
class AxisFaceAndDirectionMove < AbstractMove
|
10
|
+
def initialize(axis_face, direction)
|
11
|
+
raise TypeError, "Unsuitable axis face #{axis_face}." unless axis_face.is_a?(Face)
|
12
|
+
raise TypeError unless direction.is_a?(CubeDirection)
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
@axis_face = axis_face
|
15
|
+
@direction = direction
|
16
|
+
end
|
18
17
|
|
19
|
-
|
18
|
+
attr_reader :direction, :axis_face
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
20
|
+
def translated_direction(other_axis_face)
|
21
|
+
case @axis_face
|
22
|
+
when other_axis_face then @direction
|
23
|
+
when other_axis_face.opposite then @direction.inverse
|
24
|
+
else raise ArgumentError
|
27
25
|
end
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
def same_axis?(other)
|
29
|
+
@axis_face.same_axis?(other.axis_face)
|
30
|
+
end
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
def identifying_fields
|
33
|
+
[@axis_face, @direction]
|
34
|
+
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def canonical_direction
|
37
|
+
@axis_face.canonical_axis_face? ? @direction : @direction.inverse
|
38
|
+
end
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
def can_swap?(other)
|
41
|
+
super || same_axis?(other)
|
42
|
+
end
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
44
|
+
def swap_internal(other)
|
45
|
+
if same_axis?(other)
|
46
|
+
[other, self]
|
47
|
+
else
|
48
|
+
super
|
51
49
|
end
|
50
|
+
end
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end
|
52
|
+
def rotate_by(rotation)
|
53
|
+
if same_axis?(rotation)
|
54
|
+
self
|
55
|
+
else
|
56
|
+
rotation_neighbors = rotation.axis_face.neighbors
|
57
|
+
face_index = rotation_neighbors.index(@axis_face) || raise
|
58
|
+
new_axis_face =
|
59
|
+
rotation_neighbors[(face_index + rotation.direction.value) % rotation_neighbors.length]
|
60
|
+
fields = replace_once(identifying_fields, @axis_face, new_axis_face)
|
61
|
+
self.class.new(*fields)
|
64
62
|
end
|
63
|
+
end
|
65
64
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
65
|
+
def mirror(normal_face)
|
66
|
+
if normal_face.same_axis?(@axis_face)
|
67
|
+
fields = replace_once(
|
68
|
+
replace_once(identifying_fields, @direction, @direction.inverse),
|
69
|
+
@axis_face, @axis_face.opposite
|
70
|
+
)
|
71
|
+
self.class.new(*fields)
|
72
|
+
else
|
73
|
+
inverse
|
76
74
|
end
|
77
75
|
end
|
76
|
+
end
|
78
77
|
end
|
@@ -6,160 +6,159 @@ require 'twisty_puzzles/cube_constants'
|
|
6
6
|
require 'twisty_puzzles/cube_state'
|
7
7
|
|
8
8
|
module TwistyPuzzles
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
new_moves[current_index], new_moves[obstacle_index] = current.swap(obstacle)
|
23
|
-
end
|
24
|
-
Algorithm.new(new_moves)
|
9
|
+
# Helper class to figure out information about the cancellation between two algs.
|
10
|
+
module CancellationHelper
|
11
|
+
include CubeConstants
|
12
|
+
|
13
|
+
def self.swap_to_end(algorithm, index)
|
14
|
+
new_moves = algorithm.moves.dup
|
15
|
+
index.upto(algorithm.length - 2) do |current_index|
|
16
|
+
obstacle_index = current_index + 1
|
17
|
+
current = new_moves[current_index]
|
18
|
+
obstacle = new_moves[obstacle_index]
|
19
|
+
return nil unless current.can_swap?(obstacle)
|
20
|
+
|
21
|
+
new_moves[current_index], new_moves[obstacle_index] = current.swap(obstacle)
|
25
22
|
end
|
23
|
+
Algorithm.new(new_moves)
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
variants.push(variant)
|
36
|
-
end
|
37
|
-
raise if variants.empty?
|
26
|
+
# Possible variations of the algorithm where the last move has been swapped as much as allowed
|
27
|
+
# (e.g. D U can swap).
|
28
|
+
def self.cancel_variants(algorithm)
|
29
|
+
variants = []
|
30
|
+
algorithm.moves.each_index.reverse_each do |i|
|
31
|
+
variant = swap_to_end(algorithm, i)
|
32
|
+
break unless variant
|
38
33
|
|
39
|
-
variants
|
34
|
+
variants.push(variant)
|
40
35
|
end
|
36
|
+
raise if variants.empty?
|
41
37
|
|
42
|
-
|
43
|
-
|
44
|
-
raise TypeError unless algorithm.is_a?(Algorithm)
|
38
|
+
variants
|
39
|
+
end
|
45
40
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
alg = push_with_cancellation(alg, m, cube_size)
|
50
|
-
end
|
51
|
-
alg
|
52
|
-
end
|
41
|
+
# Cancel this algorithm as much as possilbe
|
42
|
+
def self.cancel(algorithm, cube_size)
|
43
|
+
raise TypeError unless algorithm.is_a?(Algorithm)
|
53
44
|
|
54
|
-
|
55
|
-
|
45
|
+
CubeState.check_cube_size(cube_size)
|
46
|
+
alg = Algorithm::EMPTY
|
47
|
+
algorithm.moves.each do |m|
|
48
|
+
alg = push_with_cancellation(alg, m, cube_size)
|
56
49
|
end
|
50
|
+
alg
|
51
|
+
end
|
57
52
|
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
def self.combine_transformations(left, right)
|
54
|
+
left.dup.transform_values { |e| right[e] }.freeze
|
55
|
+
end
|
61
56
|
|
62
|
-
|
57
|
+
def self.apply_transformation_to!(transformation, face_state)
|
58
|
+
face_state.map! { |f| transformation[f] }
|
59
|
+
end
|
63
60
|
|
64
|
-
|
65
|
-
twice = combine_transformations(basic_transformation, basic_transformation)
|
66
|
-
thrice = combine_transformations(twice, basic_transformation)
|
67
|
-
non_zero_transformations = [basic_transformation, twice, thrice]
|
68
|
-
adjusted_non_zero_transformations =
|
69
|
-
invert ? non_zero_transformations.reverse : non_zero_transformations
|
70
|
-
[TRIVIAL_CENTER_TRANSFORMATION] + adjusted_non_zero_transformations
|
71
|
-
end
|
61
|
+
TRIVIAL_CENTER_TRANSFORMATION = { U: :U, F: :F, R: :R, L: :L, B: :B, D: :D }.freeze
|
72
62
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
R: create_directed_transformations(x_transformation, false),
|
82
|
-
L: create_directed_transformations(x_transformation, true),
|
83
|
-
B: create_directed_transformations(z_transformation, true),
|
84
|
-
D: create_directed_transformations(y_transformation, true)
|
85
|
-
}
|
86
|
-
end
|
63
|
+
def self.create_directed_transformations(basic_transformation, invert)
|
64
|
+
twice = combine_transformations(basic_transformation, basic_transformation)
|
65
|
+
thrice = combine_transformations(twice, basic_transformation)
|
66
|
+
non_zero_transformations = [basic_transformation, twice, thrice]
|
67
|
+
adjusted_non_zero_transformations =
|
68
|
+
invert ? non_zero_transformations.reverse : non_zero_transformations
|
69
|
+
[TRIVIAL_CENTER_TRANSFORMATION] + adjusted_non_zero_transformations
|
70
|
+
end
|
87
71
|
|
88
|
-
|
89
|
-
|
72
|
+
CENTER_TRANSFORMATIONS =
|
73
|
+
begin
|
74
|
+
x_transformation = { U: :B, F: :U, R: :R, L: :L, B: :D, D: :F }.freeze
|
75
|
+
y_transformation = { U: :U, F: :L, R: :F, L: :B, B: :R, D: :D }.freeze
|
76
|
+
z_transformation = { U: :R, F: :F, R: :D, L: :U, B: :B, D: :L }.freeze
|
77
|
+
{
|
78
|
+
U: create_directed_transformations(y_transformation, false),
|
79
|
+
F: create_directed_transformations(z_transformation, false),
|
80
|
+
R: create_directed_transformations(x_transformation, false),
|
81
|
+
L: create_directed_transformations(x_transformation, true),
|
82
|
+
B: create_directed_transformations(z_transformation, true),
|
83
|
+
D: create_directed_transformations(y_transformation, true)
|
84
|
+
}
|
90
85
|
end
|
91
86
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
end
|
96
|
-
end
|
87
|
+
def self.center_transformation(rotation)
|
88
|
+
CENTER_TRANSFORMATIONS[rotation.axis_face.face_symbol][rotation.direction.value]
|
89
|
+
end
|
97
90
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
Rotation::NON_ZERO_ROTATIONS.reject do |e|
|
102
|
-
e.direction.double_move? || e.same_axis?(left)
|
103
|
-
end
|
104
|
-
second_rotations.map { |right| Algorithm.new([left, right]) }
|
105
|
-
end
|
91
|
+
def self.rotated_center_state(rotations)
|
92
|
+
rotations.reduce(FACE_SYMBOLS.dup) do |center_state, rotation|
|
93
|
+
apply_transformation_to!(center_transformation(rotation), center_state)
|
106
94
|
end
|
95
|
+
end
|
107
96
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
combined_rotation_algs = self.combined_rotation_algs
|
114
|
-
rotation_algs = trivial_rotation_algs + single_rotation_algs + combined_rotation_algs
|
115
|
-
rotation_algs.map do |alg|
|
116
|
-
[rotated_center_state(alg.moves), alg]
|
117
|
-
end.to_h.freeze
|
97
|
+
def self.combined_rotation_algs
|
98
|
+
Rotation::NON_ZERO_ROTATIONS.collect_concat do |left|
|
99
|
+
second_rotations =
|
100
|
+
Rotation::NON_ZERO_ROTATIONS.reject do |e|
|
101
|
+
e.direction.double_move? || e.same_axis?(left)
|
118
102
|
end
|
103
|
+
second_rotations.map { |right| Algorithm.new([left, right]) }
|
119
104
|
end
|
105
|
+
end
|
120
106
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
107
|
+
def self.rotation_sequences
|
108
|
+
@rotation_sequences ||=
|
109
|
+
begin
|
110
|
+
trivial_rotation_algs = [Algorithm::EMPTY]
|
111
|
+
single_rotation_algs = Rotation::NON_ZERO_ROTATIONS.map { |e| Algorithm.move(e) }
|
112
|
+
combined_rotation_algs = self.combined_rotation_algs
|
113
|
+
rotation_algs = trivial_rotation_algs + single_rotation_algs + combined_rotation_algs
|
114
|
+
rotation_algs.map do |alg|
|
115
|
+
[rotated_center_state(alg.moves), alg]
|
116
|
+
end.to_h.freeze
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.cancelled_rotations(rotations)
|
121
|
+
center_state = rotated_center_state(rotations)
|
122
|
+
rotation_sequences[center_state]
|
123
|
+
end
|
125
124
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
125
|
+
def self.num_tail_rotations(algorithm)
|
126
|
+
num = 0
|
127
|
+
algorithm.moves.reverse_each do |e|
|
128
|
+
break unless e.is_a?(Rotation)
|
130
129
|
|
131
|
-
|
132
|
-
end
|
133
|
-
num
|
130
|
+
num += 1
|
134
131
|
end
|
132
|
+
num
|
133
|
+
end
|
135
134
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
end
|
135
|
+
def self.alg_plus_cancelled_move(algorithm, move, cube_size)
|
136
|
+
if move.is_a?(Rotation) && (tail_rotations = num_tail_rotations(algorithm)) >= 2
|
137
|
+
Algorithm.new(algorithm.moves[0...-tail_rotations]) +
|
138
|
+
cancelled_rotations(algorithm.moves[-tail_rotations..-1] + [move])
|
139
|
+
else
|
140
|
+
Algorithm.new(algorithm.moves[0...-1]) +
|
141
|
+
algorithm.moves[-1].join_with_cancellation(move, cube_size)
|
144
142
|
end
|
143
|
+
end
|
145
144
|
|
146
|
-
|
147
|
-
|
148
|
-
|
145
|
+
def self.push_with_cancellation(algorithm, move, cube_size)
|
146
|
+
raise TypeError unless move.is_a?(AbstractMove)
|
147
|
+
return Algorithm.move(move) if algorithm.empty?
|
149
148
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
end
|
154
|
-
cancel_variants.min_by do |alg|
|
155
|
-
# QTM is the most sensitive metric, so we use that as the highest priority for
|
156
|
-
# cancellations.
|
157
|
-
# We use HTM as a second priority to make sure something like RR still gets merged into
|
158
|
-
# R2.
|
159
|
-
# We use the length as tertiary priority to make sure rotations get cancelled even if they
|
160
|
-
# don't change the move count.
|
161
|
-
[alg.move_count(cube_size, :qtm), alg.move_count(cube_size, :htm), alg.length]
|
149
|
+
cancel_variants =
|
150
|
+
cancel_variants(algorithm).map do |alg|
|
151
|
+
alg_plus_cancelled_move(alg, move, cube_size)
|
162
152
|
end
|
153
|
+
cancel_variants.min_by do |alg|
|
154
|
+
# QTM is the most sensitive metric, so we use that as the highest priority for
|
155
|
+
# cancellations.
|
156
|
+
# We use HTM as a second priority to make sure something like RR still gets merged into
|
157
|
+
# R2.
|
158
|
+
# We use the length as tertiary priority to make sure rotations get cancelled even if they
|
159
|
+
# don't change the move count.
|
160
|
+
[alg.move_count(cube_size, :qtm), alg.move_count(cube_size, :htm), alg.length]
|
163
161
|
end
|
164
162
|
end
|
163
|
+
end
|
165
164
|
end
|