twisty_puzzles 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/LICENSE +21 -0
  5. data/README.md +32 -0
  6. data/ext/twisty_puzzles/native/extconf.rb +5 -0
  7. data/lib/twisty_puzzles/abstract_direction.rb +54 -0
  8. data/lib/twisty_puzzles/abstract_move.rb +170 -0
  9. data/lib/twisty_puzzles/abstract_move_parser.rb +45 -0
  10. data/lib/twisty_puzzles/algorithm.rb +155 -0
  11. data/lib/twisty_puzzles/algorithm_transformation.rb +33 -0
  12. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +78 -0
  13. data/lib/twisty_puzzles/cancellation_helper.rb +165 -0
  14. data/lib/twisty_puzzles/color_scheme.rb +174 -0
  15. data/lib/twisty_puzzles/commutator.rb +118 -0
  16. data/lib/twisty_puzzles/compiled_algorithm.rb +48 -0
  17. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +67 -0
  18. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +28 -0
  19. data/lib/twisty_puzzles/coordinate.rb +318 -0
  20. data/lib/twisty_puzzles/cube.rb +660 -0
  21. data/lib/twisty_puzzles/cube_constants.rb +53 -0
  22. data/lib/twisty_puzzles/cube_direction.rb +27 -0
  23. data/lib/twisty_puzzles/cube_move.rb +384 -0
  24. data/lib/twisty_puzzles/cube_move_parser.rb +100 -0
  25. data/lib/twisty_puzzles/cube_print_helper.rb +160 -0
  26. data/lib/twisty_puzzles/cube_state.rb +113 -0
  27. data/lib/twisty_puzzles/letter_scheme.rb +72 -0
  28. data/lib/twisty_puzzles/move_type_creator.rb +27 -0
  29. data/lib/twisty_puzzles/parser.rb +222 -0
  30. data/lib/twisty_puzzles/part_cycle_factory.rb +59 -0
  31. data/lib/twisty_puzzles/puzzle.rb +26 -0
  32. data/lib/twisty_puzzles/reversible_applyable.rb +37 -0
  33. data/lib/twisty_puzzles/rotation.rb +105 -0
  34. data/lib/twisty_puzzles/skewb_direction.rb +24 -0
  35. data/lib/twisty_puzzles/skewb_move.rb +59 -0
  36. data/lib/twisty_puzzles/skewb_move_parser.rb +73 -0
  37. data/lib/twisty_puzzles/skewb_notation.rb +147 -0
  38. data/lib/twisty_puzzles/skewb_state.rb +163 -0
  39. data/lib/twisty_puzzles/state_helper.rb +32 -0
  40. data/lib/twisty_puzzles/sticker_cycle.rb +70 -0
  41. data/lib/twisty_puzzles/twisty_puzzles_error.rb +6 -0
  42. data/lib/twisty_puzzles/utils/array_helper.rb +109 -0
  43. data/lib/twisty_puzzles/utils/string_helper.rb +26 -0
  44. data/lib/twisty_puzzles/utils.rb +7 -0
  45. data/lib/twisty_puzzles/version.rb +3 -0
  46. data/lib/twisty_puzzles.rb +5 -0
  47. metadata +249 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4c23d462856d3827fe5386ca3d4d3816c2ce37cf6e06eb3876a9e4b18c4aef36
4
+ data.tar.gz: 561a871d7287fde1d568a00b243dff15337c0ae785c3caa55e71a2e94a0d09a0
5
+ SHA512:
6
+ metadata.gz: b944ba73e3cf1c4cad55156237fd4c9a87888796ea5e6fbff6606c36a9d97b84a59e1c85a42318e70c71848399205dd117c8385a01d592be6f2bdbcdaeef3570
7
+ data.tar.gz: 41894ab27afc69dffef137acb506819be9d94b3b27b339ba3ff1bc75731736575f412609a7e127920d60c9c5954f808ef98deafee6ec27f23856f1e794f67fdd
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.0.1]
8
+ ### Added
9
+ Split off core twisty puzzles functionality from cube_trainer repo into a Gem.
@@ -0,0 +1,76 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project owner at bernhard.brodowsky@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project owner is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Bernhard F. Brodowsky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Twisty Puzzles
2
+ Gem for my cube_trainer rails app. Some things are better left in a separate gem with no rails, e.g. native extensions. The main purpose is to support my Rails app, but if it's useful for someone else, feel free to use it at your own risk.
3
+
4
+ ## Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'twisty_puzzles'
10
+ ```
11
+
12
+ And then execute:
13
+
14
+ $ bundle install
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install twisty_puzzles
19
+
20
+ ## Usage
21
+
22
+ TODO: Write usage instructions here
23
+
24
+ ## Development
25
+
26
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bundle exec bin/console` for an interactive prompt that will allow you to experiment.
27
+
28
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
29
+
30
+ ## Contributing
31
+
32
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Lykos/twisty_puzzles.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+
5
+ create_makefile('native')
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwistyPuzzles
4
+
5
+ # Base class for directions.
6
+ class AbstractDirection
7
+ include Comparable
8
+ POSSIBLE_DIRECTION_NAMES = [[''], ['2', '2\''], ['\'', '3']].freeze
9
+ SIMPLE_DIRECTION_NAMES = (['0'] + POSSIBLE_DIRECTION_NAMES.map(&:first)).freeze
10
+ POSSIBLE_SKEWB_DIRECTION_NAMES = [['', '2\''], ['\'', '2']].freeze
11
+ SIMPLE_SKEWB_DIRECTION_NAMES = (['0'] + POSSIBLE_SKEWB_DIRECTION_NAMES.map(&:first)).freeze
12
+
13
+ def initialize(value)
14
+ raise TypeError, "Direction value #{value} isn't an integer." unless value.is_a?(Integer)
15
+ unless value >= 0 && value < self.class::NUM_DIRECTIONS
16
+ raise ArgumentError, "Invalid direction value #{value}."
17
+ end
18
+
19
+ @value = value
20
+ end
21
+
22
+ attr_reader :value
23
+
24
+ def <=>(other)
25
+ @value <=> other.value
26
+ end
27
+
28
+ def zero?
29
+ @value.zero?
30
+ end
31
+
32
+ def non_zero?
33
+ @value.positive?
34
+ end
35
+
36
+ def inverse
37
+ self.class.new((self.class::NUM_DIRECTIONS - @value) % self.class::NUM_DIRECTIONS)
38
+ end
39
+
40
+ def +(other)
41
+ self.class.new((@value + other.value) % self.class::NUM_DIRECTIONS)
42
+ end
43
+
44
+ def eql?(other)
45
+ self.class.equal?(other.class) && @value == other.value
46
+ end
47
+
48
+ alias == eql?
49
+
50
+ def hash
51
+ @value.hash
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/algorithm'
4
+ require 'twisty_puzzles/cube'
5
+ require 'twisty_puzzles/utils/string_helper'
6
+ require 'twisty_puzzles/utils/array_helper'
7
+
8
+ module TwistyPuzzles
9
+ # Base class for moves.
10
+ class AbstractMove
11
+ include Comparable
12
+ include Utils::StringHelper
13
+ include Utils::ArrayHelper
14
+ AXES = %w[y z x].freeze
15
+ # rubocop:disable Style/StringHashKeys
16
+ SLICE_FACES = { 'E' => Face::D, 'S' => Face::F, 'M' => Face::L }.freeze
17
+ # rubocop:enable Style/StringHashKeys
18
+ SLICE_NAMES = SLICE_FACES.invert.freeze
19
+ MOVE_METRICS = %i[qtm htm stm sqtm qstm].freeze
20
+
21
+ def <=>(other)
22
+ [self.class.name] + identifying_fields <=> [other.class.name] + other.identifying_fields
23
+ end
24
+
25
+ def hash
26
+ @hash ||= ([self.class] + identifying_fields).hash
27
+ end
28
+
29
+ def eql?(other)
30
+ self.class == other.class && identifying_fields == other.identifying_fields
31
+ end
32
+
33
+ alias == eql?
34
+
35
+ def identifying_fields
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def inverse
40
+ fields = replace_once(identifying_fields, direction, direction.inverse)
41
+ self.class.new(*fields)
42
+ end
43
+
44
+ def identity?
45
+ direction.zero?
46
+ end
47
+
48
+ def self.check_move_metric(metric)
49
+ raise ArgumentError, "Invalid move metric #{metric}." unless MOVE_METRICS.include?(metric)
50
+ end
51
+
52
+ def equivalent?(other, cube_size)
53
+ decide_meaning(cube_size).equivalent_internal?(other.decide_meaning(cube_size), cube_size)
54
+ end
55
+
56
+ def equivalent_internal?(other, _cube_size)
57
+ self == other
58
+ end
59
+
60
+ def can_swap?(other)
61
+ is_a?(Rotation) || other.is_a?(Rotation)
62
+ end
63
+
64
+ # For moves A, B, returns [C, D] if they can be swapped.
65
+ def swap(other)
66
+ raise ArgumentError unless can_swap?(other)
67
+
68
+ if is_a?(Rotation)
69
+ [other.rotate_by(inverse), self]
70
+ elsif other.is_a?(Rotation)
71
+ [other, rotate_by(other)]
72
+ else
73
+ swap_internal(other)
74
+ end
75
+ end
76
+
77
+ def swap_internal(other)
78
+ raise NotImplementedError,
79
+ "Not implemented for #{self}:#{self.class} and #{other}:#{other.class}."
80
+ end
81
+
82
+ # Cube size is needed to decide whether 'u' is a slice move (like on bigger cubes) or a fat
83
+ # move (like on 3x3).
84
+ def move_count(cube_size, metric = :htm)
85
+ raise TypeError unless cube_size.is_a?(Integer)
86
+
87
+ AbstractMove.check_move_metric(metric)
88
+ return 0 if direction.zero?
89
+
90
+ slice_factor = decide_meaning(cube_size).slice_move? ? 2 : 1
91
+ direction_factor = direction.double_move? ? 2 : 1
92
+ move_count_internal(metric, slice_factor, direction_factor)
93
+ end
94
+
95
+ def slice_move?
96
+ raise NotImplementedError, "Not implemented for #{self}:#{self.class}."
97
+ end
98
+
99
+ def direction
100
+ raise NotImplementedError
101
+ end
102
+
103
+ def rotate_by(_rotation)
104
+ raise NotImplementedError
105
+ end
106
+
107
+ def mirror(_normal_face)
108
+ raise NotImplementedError
109
+ end
110
+
111
+ # The superclass for all moves that work on the same type puzzle as the given one
112
+ # (modulo cube size, i.e. 3x3 is the same as 4x4, but Skewb is different).
113
+ def puzzles
114
+ raise NotImplementedError
115
+ end
116
+
117
+ # Return an algorithm from cancelling this move with `other` and cancelling as much as
118
+ # possible.
119
+ # Note that it doesn't cancel rotations with moves even if we theoretically could do this by
120
+ # using uncanonical wide moves.
121
+ # Expects prepend_xyz methods to be present. That one can return a cancelled implementation
122
+ # or nil if nothing can be cancelled.
123
+ def join_with_cancellation(other, cube_size)
124
+ raise ArgumentError if (puzzles & other.puzzles).empty?
125
+
126
+ maybe_alg = prepend_to(other, cube_size)
127
+ if maybe_alg
128
+ Algorithm.new(maybe_alg.moves.select { |m| m.direction.non_zero? })
129
+ else
130
+ Algorithm.new([self, other].select { |m| m.direction.non_zero? })
131
+ end
132
+ end
133
+
134
+ # We handle the annoying inconsistency that u is a slice move for bigger cubes, but a fat
135
+ # move for 3x3. Furthermore, M slice moves are fat m slice moves for even cubes and normal
136
+ # m slice moves for odd cubes.
137
+ def decide_meaning(_cube_size)
138
+ self
139
+ end
140
+
141
+ # In terms of prepending, inner M slice moves are exactly like other slice moves.
142
+ def prepend_inner_m_slice_move(other, cube_size)
143
+ prepend_slice_move(other, cube_size)
144
+ end
145
+
146
+ private
147
+
148
+ def move_count_internal(metric, slice_factor, direction_factor)
149
+ case metric
150
+ when :qtm then slice_factor * direction_factor
151
+ when :htm then slice_factor
152
+ when :stm then 1
153
+ when :qstm then direction_factor
154
+ when :sqtm then direction_factor
155
+ else raise ArgumentError, "Invalid move metric #{metric.inspect}."
156
+ end
157
+ end
158
+
159
+ def prepend_to(other, cube_size)
160
+ this = decide_meaning(cube_size)
161
+ other = other.decide_meaning(cube_size)
162
+ method_symbol = "prepend_#{snake_case_class_name(this.class)}".to_sym
163
+ unless other.respond_to?(method_symbol)
164
+ raise NotImplementedError, "#{other.class}##{method_symbol} is not implemented"
165
+ end
166
+
167
+ other.method(method_symbol).call(this, cube_size)
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwistyPuzzles
4
+
5
+ # Base class for move parsers.
6
+ class AbstractMoveParser
7
+ def regexp
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def parse_part_key(_name)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def parse_move_part(_name, _string)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def move_type_creators
20
+ raise NotImplementedError
21
+ end
22
+
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
31
+
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
37
+
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}"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_move'
4
+ require 'twisty_puzzles/reversible_applyable'
5
+ require 'twisty_puzzles/cancellation_helper'
6
+ require 'twisty_puzzles/cube_state'
7
+ require 'twisty_puzzles/compiled_cube_algorithm'
8
+ require 'twisty_puzzles/compiled_skewb_algorithm'
9
+
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([])
25
+
26
+ # Creates a one move algorithm.
27
+ def self.move(move)
28
+ Algorithm.new([move])
29
+ end
30
+
31
+ attr_reader :moves
32
+
33
+ def eql?(other)
34
+ self.class.equal?(other.class) && @moves == other.moves
35
+ end
36
+
37
+ alias == eql?
38
+
39
+ def hash
40
+ @hash ||= ([self.class] + @moves).hash
41
+ end
42
+
43
+ def length
44
+ @moves.length
45
+ end
46
+
47
+ def empty?
48
+ @moves.empty?
49
+ end
50
+
51
+ def to_s
52
+ @moves.join(' ')
53
+ end
54
+
55
+ def inspect
56
+ "Algorithm(#{self})"
57
+ end
58
+
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
69
+
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
78
+
79
+ def +(other)
80
+ self.class.new(@moves + other.moves)
81
+ end
82
+
83
+ def <=>(other)
84
+ [length, @moves] <=> [other.length, other.moves]
85
+ end
86
+
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
92
+
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
105
+
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?
112
+
113
+ self.class.new(@moves.map { |m| m.rotate_by(rotation) })
114
+ end
115
+
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)
120
+
121
+ self.class.new(@moves.map { |m| m.mirror(normal_face) })
122
+ end
123
+
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)
128
+
129
+ AbstractMove.check_move_metric(metric)
130
+ return 0 if empty?
131
+
132
+ @moves.map { |m| m.move_count(cube_size, metric) }.reduce(:+)
133
+ end
134
+
135
+ def *(other)
136
+ raise TypeError unless other.is_a?(Integer)
137
+ raise ArgumentError if other.negative?
138
+
139
+ self.class.new(@moves * other)
140
+ end
141
+
142
+ def compiled_for_skewb
143
+ @compiled_for_skewb ||= CompiledSkewbAlgorithm.for_moves(@moves)
144
+ end
145
+
146
+ def compiled_for_cube(cube_size)
147
+ (@compiled_for_cube ||= {})[cube_size] ||=
148
+ CompiledCubeAlgorithm.for_moves(cube_size, @moves)
149
+ end
150
+
151
+ protected
152
+
153
+ attr_writer :inverse
154
+ end
155
+ end