twisty_puzzles 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/LICENSE +21 -0
- data/README.md +32 -0
- data/ext/twisty_puzzles/native/extconf.rb +5 -0
- data/lib/twisty_puzzles/abstract_direction.rb +54 -0
- data/lib/twisty_puzzles/abstract_move.rb +170 -0
- data/lib/twisty_puzzles/abstract_move_parser.rb +45 -0
- data/lib/twisty_puzzles/algorithm.rb +155 -0
- data/lib/twisty_puzzles/algorithm_transformation.rb +33 -0
- data/lib/twisty_puzzles/axis_face_and_direction_move.rb +78 -0
- data/lib/twisty_puzzles/cancellation_helper.rb +165 -0
- data/lib/twisty_puzzles/color_scheme.rb +174 -0
- data/lib/twisty_puzzles/commutator.rb +118 -0
- data/lib/twisty_puzzles/compiled_algorithm.rb +48 -0
- data/lib/twisty_puzzles/compiled_cube_algorithm.rb +67 -0
- data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +28 -0
- data/lib/twisty_puzzles/coordinate.rb +318 -0
- data/lib/twisty_puzzles/cube.rb +660 -0
- data/lib/twisty_puzzles/cube_constants.rb +53 -0
- data/lib/twisty_puzzles/cube_direction.rb +27 -0
- data/lib/twisty_puzzles/cube_move.rb +384 -0
- data/lib/twisty_puzzles/cube_move_parser.rb +100 -0
- data/lib/twisty_puzzles/cube_print_helper.rb +160 -0
- data/lib/twisty_puzzles/cube_state.rb +113 -0
- data/lib/twisty_puzzles/letter_scheme.rb +72 -0
- data/lib/twisty_puzzles/move_type_creator.rb +27 -0
- data/lib/twisty_puzzles/parser.rb +222 -0
- data/lib/twisty_puzzles/part_cycle_factory.rb +59 -0
- data/lib/twisty_puzzles/puzzle.rb +26 -0
- data/lib/twisty_puzzles/reversible_applyable.rb +37 -0
- data/lib/twisty_puzzles/rotation.rb +105 -0
- data/lib/twisty_puzzles/skewb_direction.rb +24 -0
- data/lib/twisty_puzzles/skewb_move.rb +59 -0
- data/lib/twisty_puzzles/skewb_move_parser.rb +73 -0
- data/lib/twisty_puzzles/skewb_notation.rb +147 -0
- data/lib/twisty_puzzles/skewb_state.rb +163 -0
- data/lib/twisty_puzzles/state_helper.rb +32 -0
- data/lib/twisty_puzzles/sticker_cycle.rb +70 -0
- data/lib/twisty_puzzles/twisty_puzzles_error.rb +6 -0
- data/lib/twisty_puzzles/utils/array_helper.rb +109 -0
- data/lib/twisty_puzzles/utils/string_helper.rb +26 -0
- data/lib/twisty_puzzles/utils.rb +7 -0
- data/lib/twisty_puzzles/version.rb +3 -0
- data/lib/twisty_puzzles.rb +5 -0
- 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.
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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,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
|