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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +6 -1
  4. data/ext/twisty_puzzles/native/cube_algorithm.c +267 -0
  5. data/ext/twisty_puzzles/native/cube_algorithm.h +5 -0
  6. data/ext/twisty_puzzles/native/cube_average.c +184 -0
  7. data/ext/twisty_puzzles/native/cube_average.h +5 -0
  8. data/ext/twisty_puzzles/native/cube_coordinate.c +207 -0
  9. data/ext/twisty_puzzles/native/cube_coordinate.h +34 -0
  10. data/ext/twisty_puzzles/native/cube_state.c +264 -0
  11. data/ext/twisty_puzzles/native/cube_state.h +31 -0
  12. data/ext/twisty_puzzles/native/extconf.rb +1 -1
  13. data/ext/twisty_puzzles/native/face_symbols.c +67 -0
  14. data/ext/twisty_puzzles/native/face_symbols.h +34 -0
  15. data/ext/twisty_puzzles/native/native.c +28 -0
  16. data/ext/twisty_puzzles/native/skewb_algorithm.c +331 -0
  17. data/ext/twisty_puzzles/native/skewb_algorithm.h +5 -0
  18. data/ext/twisty_puzzles/native/skewb_coordinate.c +237 -0
  19. data/ext/twisty_puzzles/native/skewb_coordinate.h +36 -0
  20. data/ext/twisty_puzzles/native/skewb_layer_fingerprint.c +271 -0
  21. data/ext/twisty_puzzles/native/skewb_layer_fingerprint.h +5 -0
  22. data/ext/twisty_puzzles/native/skewb_state.c +214 -0
  23. data/ext/twisty_puzzles/native/skewb_state.h +23 -0
  24. data/ext/twisty_puzzles/native/utils.c +76 -0
  25. data/ext/twisty_puzzles/native/utils.h +31 -0
  26. data/lib/twisty_puzzles.rb +38 -0
  27. data/lib/twisty_puzzles/abstract_direction.rb +38 -39
  28. data/lib/twisty_puzzles/abstract_move.rb +1 -2
  29. data/lib/twisty_puzzles/abstract_move_parser.rb +32 -33
  30. data/lib/twisty_puzzles/algorithm.rb +112 -113
  31. data/lib/twisty_puzzles/algorithm_transformation.rb +19 -21
  32. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +56 -56
  33. data/lib/twisty_puzzles/cancellation_helper.rb +124 -125
  34. data/lib/twisty_puzzles/color_scheme.rb +1 -1
  35. data/lib/twisty_puzzles/commutator.rb +82 -80
  36. data/lib/twisty_puzzles/compiled_algorithm.rb +31 -32
  37. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +49 -50
  38. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +18 -19
  39. data/lib/twisty_puzzles/coordinate.rb +243 -246
  40. data/lib/twisty_puzzles/cube.rb +494 -495
  41. data/lib/twisty_puzzles/cube_constants.rb +40 -41
  42. data/lib/twisty_puzzles/cube_direction.rb +15 -18
  43. data/lib/twisty_puzzles/cube_move.rb +285 -290
  44. data/lib/twisty_puzzles/cube_move_parser.rb +75 -76
  45. data/lib/twisty_puzzles/cube_print_helper.rb +133 -133
  46. data/lib/twisty_puzzles/cube_state.rb +80 -81
  47. data/lib/twisty_puzzles/move_type_creator.rb +17 -18
  48. data/lib/twisty_puzzles/parser.rb +176 -179
  49. data/lib/twisty_puzzles/part_cycle_factory.rb +39 -42
  50. data/lib/twisty_puzzles/puzzle.rb +16 -17
  51. data/lib/twisty_puzzles/reversible_applyable.rb +24 -25
  52. data/lib/twisty_puzzles/rotation.rb +76 -75
  53. data/lib/twisty_puzzles/skewb_direction.rb +14 -15
  54. data/lib/twisty_puzzles/skewb_move.rb +49 -49
  55. data/lib/twisty_puzzles/skewb_move_parser.rb +51 -51
  56. data/lib/twisty_puzzles/skewb_notation.rb +121 -118
  57. data/lib/twisty_puzzles/skewb_state.rb +120 -121
  58. data/lib/twisty_puzzles/state_helper.rb +20 -21
  59. data/lib/twisty_puzzles/sticker_cycle.rb +43 -44
  60. data/lib/twisty_puzzles/utils.rb +3 -0
  61. data/lib/twisty_puzzles/version.rb +3 -1
  62. 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
- # Base class for move parsers.
6
- class AbstractMoveParser
7
- def regexp
8
- raise NotImplementedError
9
- end
4
+ # Base class for move parsers.
5
+ class AbstractMoveParser
6
+ def regexp
7
+ raise NotImplementedError
8
+ end
10
9
 
11
- def parse_part_key(_name)
12
- raise NotImplementedError
13
- end
10
+ def parse_part_key(_name)
11
+ raise NotImplementedError
12
+ end
14
13
 
15
- def parse_move_part(_name, _string)
16
- raise NotImplementedError
17
- end
14
+ def parse_move_part(_name, _string)
15
+ raise NotImplementedError
16
+ end
18
17
 
19
- def move_type_creators
20
- raise NotImplementedError
21
- end
18
+ def move_type_creators
19
+ raise NotImplementedError
20
+ end
22
21
 
23
- def parse_named_captures(match)
24
- present_named_captures = match.named_captures.reject { |_n, v| v.nil? }
25
- present_named_captures.map do |name, string|
26
- key = parse_part_key(name).to_sym
27
- value = parse_move_part(name, string)
28
- [key, value]
29
- end.to_h
30
- end
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
- def parse_move(move_string)
33
- match = move_string.match(regexp)
34
- if !match || !match.pre_match.empty? || !match.post_match.empty?
35
- raise ArgumentError("Invalid move #{move_string}.")
36
- end
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
- parsed_parts = parse_named_captures(match)
39
- move_type_creators.each do |parser|
40
- return parser.create(parsed_parts) if parser.applies_to?(parsed_parts)
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
- # Represents a sequence of moves that can be applied to puzzle states.
13
- class Algorithm
14
- include ReversibleApplyable
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
- # Creates a one move algorithm.
27
- def self.move(move)
28
- Algorithm.new([move])
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
- attr_reader :moves
23
+ EMPTY = Algorithm.new([])
32
24
 
33
- def eql?(other)
34
- self.class.equal?(other.class) && @moves == other.moves
35
- end
25
+ # Creates a one move algorithm.
26
+ def self.move(move)
27
+ Algorithm.new([move])
28
+ end
36
29
 
37
- alias == eql?
30
+ attr_reader :moves
38
31
 
39
- def hash
40
- @hash ||= ([self.class] + @moves).hash
41
- end
32
+ def eql?(other)
33
+ self.class.equal?(other.class) && @moves == other.moves
34
+ end
42
35
 
43
- def length
44
- @moves.length
45
- end
36
+ alias == eql?
46
37
 
47
- def empty?
48
- @moves.empty?
49
- end
38
+ def hash
39
+ @hash ||= ([self.class] + @moves).hash
40
+ end
50
41
 
51
- def to_s
52
- @moves.join(' ')
53
- end
42
+ def length
43
+ @moves.length
44
+ end
54
45
 
55
- def inspect
56
- "Algorithm(#{self})"
57
- end
46
+ def empty?
47
+ @moves.empty?
48
+ end
58
49
 
59
- def apply_to(cube_state)
60
- case cube_state
61
- when SkewbState
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
- def inverse
71
- @inverse ||=
72
- begin
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
- def +(other)
80
- self.class.new(@moves + other.moves)
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
- def <=>(other)
84
- [length, @moves] <=> [other.length, other.moves]
85
- end
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
- # Returns the cancelled version of the given algorithm.
88
- # Note that the cube size is important to know which fat moves cancel
89
- def cancelled(cube_size)
90
- CancellationHelper.cancel(self, cube_size)
91
- end
78
+ def +(other)
79
+ self.class.new(@moves + other.moves)
80
+ end
92
81
 
93
- # Returns the number of moves that cancel if you concat the algorithm to the right of self.
94
- # Note that the cube size is important to know which fat moves cancel
95
- def cancellations(other, cube_size, metric = :htm)
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
- # Rotates the algorithm, e.g. applying "y" to "R U" becomes "F U".
107
- # Applying rotation r to alg a is equivalent to r' a r.
108
- # Note that this is not implemented for all moves.
109
- def rotate_by(rotation)
110
- raise TypeError unless rotation.is_a?(Rotation)
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
- self.class.new(@moves.map { |m| m.rotate_by(rotation) })
114
- end
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
- # Mirrors the algorithm and uses the given face as the normal of the mirroring.
117
- # E.g. mirroring "R U F" with "R" as the normal face, we get "L U' F'".
118
- def mirror(normal_face)
119
- raise TypeError unless normal_face.is_a?(Face)
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
- self.class.new(@moves.map { |m| m.mirror(normal_face) })
122
- end
112
+ self.class.new(@moves.map { |m| m.rotate_by(rotation) })
113
+ end
123
114
 
124
- # Cube size is needed to decide whether 'u' is a slice move (like on bigger cubes) or a
125
- # fat move (like on 3x3).
126
- def move_count(cube_size, metric = :htm)
127
- raise TypeError unless cube_size.is_a?(Integer)
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
- AbstractMove.check_move_metric(metric)
130
- return 0 if empty?
120
+ self.class.new(@moves.map { |m| m.mirror(normal_face) })
121
+ end
131
122
 
132
- @moves.map { |m| m.move_count(cube_size, metric) }.reduce(:+)
133
- end
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
- def *(other)
136
- raise TypeError unless other.is_a?(Integer)
137
- raise ArgumentError if other.negative?
128
+ AbstractMove.check_move_metric(metric)
129
+ return 0 if empty?
138
130
 
139
- self.class.new(@moves * other)
140
- end
131
+ @moves.sum { |m| m.move_count(cube_size, metric) }
132
+ end
141
133
 
142
- def compiled_for_skewb
143
- @compiled_for_skewb ||= CompiledSkewbAlgorithm.for_moves(@moves)
144
- end
134
+ def *(other)
135
+ raise TypeError unless other.is_a?(Integer)
136
+ raise ArgumentError if other.negative?
145
137
 
146
- def compiled_for_cube(cube_size)
147
- (@compiled_for_cube ||= {})[cube_size] ||=
148
- CompiledCubeAlgorithm.for_moves(cube_size, @moves)
149
- end
138
+ self.class.new(@moves * other)
139
+ end
150
140
 
151
- protected
141
+ def compiled_for_skewb
142
+ @compiled_for_skewb ||= CompiledSkewbAlgorithm.for_moves(@moves)
143
+ end
152
144
 
153
- attr_writer :inverse
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
- AlgorithmTransformation =
9
- Struct.new(:rotation, :mirror, :mirror_normal_face) do
10
- def transformed(algorithm)
11
- algorithm = algorithm.mirror(mirror_normal_face) if mirror
12
- algorithm.rotate_by(rotation)
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
- def identity?
16
- rotation.identity? && !mirror
17
- end
14
+ def identity?
15
+ rotation.identity? && !mirror
16
+ end
18
17
 
19
- # Returns algorithm transformations that mirror an algorithm and rotate it around a face.
20
- def self.around_face(face)
21
- around_face_rotations = CubeDirection::ALL_DIRECTIONS.map { |d| Rotation.new(face, d) }
22
- mirror_normal_face = face.neighbors.first
23
- around_face_rotations.product([true, false]).map do |r, m|
24
- AlgorithmTransformation.new(r, m, mirror_normal_face)
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
- def self.around_face_without_identity(face)
29
- around_face(face).reject(&:identity?)
30
- end
27
+ def self.around_face_without_identity(face)
28
+ around_face(face).reject(&:identity?)
31
29
  end
32
- end
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
- # Intermediate base class for all types of moves that have an axis face and a direction,
9
- # i.e. cube moves and rotations.
10
- class AxisFaceAndDirectionMove < AbstractMove
11
- def initialize(axis_face, direction)
12
- raise TypeError, "Unsuitable axis face #{axis_face}." unless axis_face.is_a?(Face)
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
- @axis_face = axis_face
16
- @direction = direction
17
- end
14
+ super()
15
+ @axis_face = axis_face
16
+ @direction = direction
17
+ end
18
18
 
19
- attr_reader :direction, :axis_face
19
+ attr_reader :direction, :axis_face
20
20
 
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
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
- def same_axis?(other)
30
- @axis_face.same_axis?(other.axis_face)
31
- end
29
+ def same_axis?(other)
30
+ @axis_face.same_axis?(other.axis_face)
31
+ end
32
32
 
33
- def identifying_fields
34
- [@axis_face, @direction]
35
- end
33
+ def identifying_fields
34
+ [@axis_face, @direction]
35
+ end
36
36
 
37
- def canonical_direction
38
- @axis_face.canonical_axis_face? ? @direction : @direction.inverse
39
- end
37
+ def canonical_direction
38
+ @axis_face.canonical_axis_face? ? @direction : @direction.inverse
39
+ end
40
40
 
41
- def can_swap?(other)
42
- super || same_axis?(other)
43
- end
41
+ def can_swap?(other)
42
+ super || same_axis?(other)
43
+ end
44
44
 
45
- def swap_internal(other)
46
- if same_axis?(other)
47
- [other, self]
48
- else
49
- super
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
- 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)
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
- 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
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