twisty_puzzles 0.0.1 → 0.0.6
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 +21 -1
- data/README.md +6 -1
- data/ext/twisty_puzzles/native/cube_algorithm.c +267 -0
- data/ext/twisty_puzzles/native/cube_algorithm.h +5 -0
- data/ext/twisty_puzzles/native/cube_average.c +184 -0
- data/ext/twisty_puzzles/native/cube_average.h +5 -0
- data/ext/twisty_puzzles/native/cube_coordinate.c +207 -0
- data/ext/twisty_puzzles/native/cube_coordinate.h +34 -0
- data/ext/twisty_puzzles/native/cube_state.c +264 -0
- data/ext/twisty_puzzles/native/cube_state.h +31 -0
- data/ext/twisty_puzzles/native/extconf.rb +1 -1
- data/ext/twisty_puzzles/native/face_symbols.c +67 -0
- data/ext/twisty_puzzles/native/face_symbols.h +34 -0
- data/ext/twisty_puzzles/native/native.c +28 -0
- data/ext/twisty_puzzles/native/skewb_algorithm.c +331 -0
- data/ext/twisty_puzzles/native/skewb_algorithm.h +5 -0
- data/ext/twisty_puzzles/native/skewb_coordinate.c +237 -0
- data/ext/twisty_puzzles/native/skewb_coordinate.h +36 -0
- data/ext/twisty_puzzles/native/skewb_layer_fingerprint.c +271 -0
- data/ext/twisty_puzzles/native/skewb_layer_fingerprint.h +5 -0
- data/ext/twisty_puzzles/native/skewb_state.c +214 -0
- data/ext/twisty_puzzles/native/skewb_state.h +23 -0
- data/ext/twisty_puzzles/native/utils.c +76 -0
- data/ext/twisty_puzzles/native/utils.h +31 -0
- data/lib/twisty_puzzles.rb +38 -0
- data/lib/twisty_puzzles/abstract_direction.rb +38 -39
- data/lib/twisty_puzzles/abstract_move.rb +1 -2
- 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 +56 -56
- data/lib/twisty_puzzles/cancellation_helper.rb +124 -125
- data/lib/twisty_puzzles/color_scheme.rb +1 -1
- data/lib/twisty_puzzles/commutator.rb +82 -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 +243 -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 +285 -290
- data/lib/twisty_puzzles/cube_move_parser.rb +75 -76
- data/lib/twisty_puzzles/cube_print_helper.rb +133 -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 +76 -75
- data/lib/twisty_puzzles/skewb_direction.rb +14 -15
- data/lib/twisty_puzzles/skewb_move.rb +49 -49
- data/lib/twisty_puzzles/skewb_move_parser.rb +51 -51
- data/lib/twisty_puzzles/skewb_notation.rb +121 -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 +30 -10
@@ -150,8 +150,7 @@ module TwistyPuzzles
|
|
150
150
|
when :qtm then slice_factor * direction_factor
|
151
151
|
when :htm then slice_factor
|
152
152
|
when :stm then 1
|
153
|
-
when :qstm then direction_factor
|
154
|
-
when :sqtm then direction_factor
|
153
|
+
when :qstm, :sqtm then direction_factor
|
155
154
|
else raise ArgumentError, "Invalid move metric #{metric.inspect}."
|
156
155
|
end
|
157
156
|
end
|
@@ -1,45 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TwistyPuzzles
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
4
|
+
# Base class for move parsers.
|
5
|
+
class AbstractMoveParser
|
6
|
+
def regexp
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def parse_part_key(_name)
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
def parse_move_part(_name, _string)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
def move_type_creators
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
22
|
+
def parse_named_captures(match)
|
23
|
+
present_named_captures = match.named_captures.compact
|
24
|
+
present_named_captures.map do |name, string|
|
25
|
+
key = parse_part_key(name).to_sym
|
26
|
+
value = parse_move_part(name, string)
|
27
|
+
[key, value]
|
28
|
+
end.to_h
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
31
|
+
def parse_move(move_string)
|
32
|
+
match = move_string.match(regexp)
|
33
|
+
if !match || !match.pre_match.empty? || !match.post_match.empty?
|
34
|
+
raise ArgumentError("Invalid move #{move_string}.")
|
35
|
+
end
|
37
36
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
42
|
-
raise "No move type creator applies to #{parsed_parts}"
|
37
|
+
parsed_parts = parse_named_captures(match)
|
38
|
+
move_type_creators.each do |parser|
|
39
|
+
return parser.create(parsed_parts) if parser.applies_to?(parsed_parts)
|
43
40
|
end
|
41
|
+
raise "No move type creator applies to #{parsed_parts}"
|
44
42
|
end
|
43
|
+
end
|
45
44
|
end
|
@@ -8,148 +8,147 @@ require 'twisty_puzzles/compiled_cube_algorithm'
|
|
8
8
|
require 'twisty_puzzles/compiled_skewb_algorithm'
|
9
9
|
|
10
10
|
module TwistyPuzzles
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
include Comparable
|
16
|
-
|
17
|
-
def initialize(moves)
|
18
|
-
moves.each do |m|
|
19
|
-
raise TypeError, "#{m.inspect} is not a suitable move." unless m.is_a?(AbstractMove)
|
20
|
-
end
|
21
|
-
@moves = moves
|
22
|
-
end
|
23
|
-
|
24
|
-
EMPTY = Algorithm.new([])
|
11
|
+
# Represents a sequence of moves that can be applied to puzzle states.
|
12
|
+
class Algorithm
|
13
|
+
include ReversibleApplyable
|
14
|
+
include Comparable
|
25
15
|
|
26
|
-
|
27
|
-
|
28
|
-
|
16
|
+
def initialize(moves)
|
17
|
+
moves.each do |m|
|
18
|
+
raise TypeError, "#{m.inspect} is not a suitable move." unless m.is_a?(AbstractMove)
|
29
19
|
end
|
20
|
+
@moves = moves
|
21
|
+
end
|
30
22
|
|
31
|
-
|
23
|
+
EMPTY = Algorithm.new([])
|
32
24
|
|
33
|
-
|
34
|
-
|
35
|
-
|
25
|
+
# Creates a one move algorithm.
|
26
|
+
def self.move(move)
|
27
|
+
Algorithm.new([move])
|
28
|
+
end
|
36
29
|
|
37
|
-
|
30
|
+
attr_reader :moves
|
38
31
|
|
39
|
-
|
40
|
-
|
41
|
-
|
32
|
+
def eql?(other)
|
33
|
+
self.class.equal?(other.class) && @moves == other.moves
|
34
|
+
end
|
42
35
|
|
43
|
-
|
44
|
-
@moves.length
|
45
|
-
end
|
36
|
+
alias == eql?
|
46
37
|
|
47
|
-
|
48
|
-
|
49
|
-
|
38
|
+
def hash
|
39
|
+
@hash ||= ([self.class] + @moves).hash
|
40
|
+
end
|
50
41
|
|
51
|
-
|
52
|
-
|
53
|
-
|
42
|
+
def length
|
43
|
+
@moves.length
|
44
|
+
end
|
54
45
|
|
55
|
-
|
56
|
-
|
57
|
-
|
46
|
+
def empty?
|
47
|
+
@moves.empty?
|
48
|
+
end
|
58
49
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
compiled_for_skewb.apply_to(cube_state)
|
63
|
-
when CubeState
|
64
|
-
compiled_for_cube(cube_state.n).apply_to(cube_state)
|
65
|
-
else
|
66
|
-
raise TypeError, "Unsupported cube state class #{cube_state.class}."
|
67
|
-
end
|
68
|
-
end
|
50
|
+
def to_s
|
51
|
+
@moves.join(' ')
|
52
|
+
end
|
69
53
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
alg = self.class.new(@moves.reverse.map(&:inverse))
|
74
|
-
alg.inverse = self
|
75
|
-
alg
|
76
|
-
end
|
77
|
-
end
|
54
|
+
def inspect
|
55
|
+
"Algorithm(#{self})"
|
56
|
+
end
|
78
57
|
|
79
|
-
|
80
|
-
|
58
|
+
def apply_to(cube_state)
|
59
|
+
case cube_state
|
60
|
+
when SkewbState
|
61
|
+
compiled_for_skewb.apply_to(cube_state)
|
62
|
+
when CubeState
|
63
|
+
compiled_for_cube(cube_state.n).apply_to(cube_state)
|
64
|
+
else
|
65
|
+
raise TypeError, "Unsupported cube state class #{cube_state.class}."
|
81
66
|
end
|
67
|
+
end
|
82
68
|
|
83
|
-
|
84
|
-
|
85
|
-
|
69
|
+
def inverse
|
70
|
+
@inverse ||=
|
71
|
+
begin
|
72
|
+
alg = self.class.new(@moves.reverse.map(&:inverse))
|
73
|
+
alg.inverse = self
|
74
|
+
alg
|
75
|
+
end
|
76
|
+
end
|
86
77
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
CancellationHelper.cancel(self, cube_size)
|
91
|
-
end
|
78
|
+
def +(other)
|
79
|
+
self.class.new(@moves + other.moves)
|
80
|
+
end
|
92
81
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
CubeState.check_cube_size(cube_size)
|
97
|
-
AbstractMove.check_move_metric(metric)
|
98
|
-
cancelled = cancelled(cube_size)
|
99
|
-
other_cancelled = other.cancelled(cube_size)
|
100
|
-
together_cancelled = (self + other).cancelled(cube_size)
|
101
|
-
cancelled.move_count(cube_size, metric) +
|
102
|
-
other_cancelled.move_count(cube_size, metric) -
|
103
|
-
together_cancelled.move_count(cube_size, metric)
|
104
|
-
end
|
82
|
+
def <=>(other)
|
83
|
+
[length, @moves] <=> [other.length, other.moves]
|
84
|
+
end
|
105
85
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
return self if rotation.direction.zero?
|
86
|
+
# Returns the cancelled version of the given algorithm.
|
87
|
+
# Note that the cube size is important to know which fat moves cancel
|
88
|
+
def cancelled(cube_size)
|
89
|
+
CancellationHelper.cancel(self, cube_size)
|
90
|
+
end
|
112
91
|
|
113
|
-
|
114
|
-
|
92
|
+
# Returns the number of moves that cancel if you concat the algorithm to the right of self.
|
93
|
+
# Note that the cube size is important to know which fat moves cancel
|
94
|
+
def cancellations(other, cube_size, metric = :htm)
|
95
|
+
CubeState.check_cube_size(cube_size)
|
96
|
+
AbstractMove.check_move_metric(metric)
|
97
|
+
cancelled = cancelled(cube_size)
|
98
|
+
other_cancelled = other.cancelled(cube_size)
|
99
|
+
together_cancelled = (self + other).cancelled(cube_size)
|
100
|
+
cancelled.move_count(cube_size, metric) +
|
101
|
+
other_cancelled.move_count(cube_size, metric) -
|
102
|
+
together_cancelled.move_count(cube_size, metric)
|
103
|
+
end
|
115
104
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
105
|
+
# Rotates the algorithm, e.g. applying "y" to "R U" becomes "F U".
|
106
|
+
# Applying rotation r to alg a is equivalent to r' a r.
|
107
|
+
# Note that this is not implemented for all moves.
|
108
|
+
def rotate_by(rotation)
|
109
|
+
raise TypeError unless rotation.is_a?(Rotation)
|
110
|
+
return self if rotation.direction.zero?
|
120
111
|
|
121
|
-
|
122
|
-
|
112
|
+
self.class.new(@moves.map { |m| m.rotate_by(rotation) })
|
113
|
+
end
|
123
114
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
115
|
+
# Mirrors the algorithm and uses the given face as the normal of the mirroring.
|
116
|
+
# E.g. mirroring "R U F" with "R" as the normal face, we get "L U' F'".
|
117
|
+
def mirror(normal_face)
|
118
|
+
raise TypeError unless normal_face.is_a?(Face)
|
128
119
|
|
129
|
-
|
130
|
-
|
120
|
+
self.class.new(@moves.map { |m| m.mirror(normal_face) })
|
121
|
+
end
|
131
122
|
|
132
|
-
|
133
|
-
|
123
|
+
# Cube size is needed to decide whether 'u' is a slice move (like on bigger cubes) or a
|
124
|
+
# fat move (like on 3x3).
|
125
|
+
def move_count(cube_size, metric = :htm)
|
126
|
+
raise TypeError unless cube_size.is_a?(Integer)
|
134
127
|
|
135
|
-
|
136
|
-
|
137
|
-
raise ArgumentError if other.negative?
|
128
|
+
AbstractMove.check_move_metric(metric)
|
129
|
+
return 0 if empty?
|
138
130
|
|
139
|
-
|
140
|
-
|
131
|
+
@moves.sum { |m| m.move_count(cube_size, metric) }
|
132
|
+
end
|
141
133
|
|
142
|
-
|
143
|
-
|
144
|
-
|
134
|
+
def *(other)
|
135
|
+
raise TypeError unless other.is_a?(Integer)
|
136
|
+
raise ArgumentError if other.negative?
|
145
137
|
|
146
|
-
|
147
|
-
|
148
|
-
CompiledCubeAlgorithm.for_moves(cube_size, @moves)
|
149
|
-
end
|
138
|
+
self.class.new(@moves * other)
|
139
|
+
end
|
150
140
|
|
151
|
-
|
141
|
+
def compiled_for_skewb
|
142
|
+
@compiled_for_skewb ||= CompiledSkewbAlgorithm.for_moves(@moves)
|
143
|
+
end
|
152
144
|
|
153
|
-
|
145
|
+
def compiled_for_cube(cube_size)
|
146
|
+
(@compiled_for_cube ||= {})[cube_size] ||=
|
147
|
+
CompiledCubeAlgorithm.for_moves(cube_size, @moves)
|
154
148
|
end
|
149
|
+
|
150
|
+
protected
|
151
|
+
|
152
|
+
attr_writer :inverse
|
153
|
+
end
|
155
154
|
end
|
@@ -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,75 @@ 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
|
+
super()
|
15
|
+
@axis_face = axis_face
|
16
|
+
@direction = direction
|
17
|
+
end
|
18
18
|
|
19
|
-
|
19
|
+
attr_reader :direction, :axis_face
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
21
|
+
def translated_direction(other_axis_face)
|
22
|
+
case @axis_face
|
23
|
+
when other_axis_face then @direction
|
24
|
+
when other_axis_face.opposite then @direction.inverse
|
25
|
+
else raise ArgumentError
|
27
26
|
end
|
27
|
+
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
def same_axis?(other)
|
30
|
+
@axis_face.same_axis?(other.axis_face)
|
31
|
+
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
def identifying_fields
|
34
|
+
[@axis_face, @direction]
|
35
|
+
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
def canonical_direction
|
38
|
+
@axis_face.canonical_axis_face? ? @direction : @direction.inverse
|
39
|
+
end
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
def can_swap?(other)
|
42
|
+
super || same_axis?(other)
|
43
|
+
end
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
45
|
+
def swap_internal(other)
|
46
|
+
if same_axis?(other)
|
47
|
+
[other, self]
|
48
|
+
else
|
49
|
+
super
|
51
50
|
end
|
51
|
+
end
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end
|
53
|
+
def rotate_by(rotation)
|
54
|
+
if same_axis?(rotation)
|
55
|
+
self
|
56
|
+
else
|
57
|
+
rotation_neighbors = rotation.axis_face.neighbors
|
58
|
+
face_index = rotation_neighbors.index(@axis_face) || raise
|
59
|
+
new_axis_face =
|
60
|
+
rotation_neighbors[(face_index + rotation.direction.value) % rotation_neighbors.length]
|
61
|
+
fields = replace_once(identifying_fields, @axis_face, new_axis_face)
|
62
|
+
self.class.new(*fields)
|
64
63
|
end
|
64
|
+
end
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
66
|
+
def mirror(normal_face)
|
67
|
+
if normal_face.same_axis?(@axis_face)
|
68
|
+
fields = replace_once(
|
69
|
+
replace_once(identifying_fields, @direction, @direction.inverse),
|
70
|
+
@axis_face, @axis_face.opposite
|
71
|
+
)
|
72
|
+
self.class.new(*fields)
|
73
|
+
else
|
74
|
+
inverse
|
76
75
|
end
|
77
76
|
end
|
77
|
+
end
|
78
78
|
end
|