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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 989d528a58cb32ca49f7502249b5e5eca71a1e46726f1a79ffa64e18381d5a86
|
4
|
+
data.tar.gz: 2c1bfff00d66c4b85f15592ee50fa6883e45149ae8456c7a24cc1cba60841285
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 870c957e8ba192090401e9e6092779e5f19f45fcd995d60ab1c2e52fed62b7cd25397ce06f18fb2eae6f4ac3df88e76975390f0b73ebc45a8fe2ecdc2cae41fd
|
7
|
+
data.tar.gz: 6140ebc4150d89e0577461b66b18158dec51c11d3527d265f028d24cd2b1e44f2299b05ae46a3264bbc040dcf0b34bda83be7ec15092884234539130a402d3ef
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [0.0.2]
|
8
|
+
### Changed
|
9
|
+
- Now a simple `require 'twisty_puzzles'` is enough, users don't need to require files separately.
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
- Syntax error in file that wasn't included in tests previously.
|
13
|
+
- Rubocop fixes across the codebase.
|
14
|
+
|
7
15
|
## [0.0.1]
|
8
16
|
### Added
|
9
|
-
Split off core twisty puzzles functionality from cube_trainer repo into a Gem.
|
17
|
+
- Split off core twisty puzzles functionality from cube_trainer repo into a Gem.
|
data/lib/twisty_puzzles.rb
CHANGED
@@ -1,5 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'twisty_puzzles/abstract_direction'
|
4
|
+
require 'twisty_puzzles/abstract_move'
|
5
|
+
require 'twisty_puzzles/abstract_move_parser'
|
6
|
+
require 'twisty_puzzles/algorithm'
|
7
|
+
require 'twisty_puzzles/algorithm_transformation'
|
8
|
+
require 'twisty_puzzles/axis_face_and_direction_move'
|
9
|
+
require 'twisty_puzzles/cancellation_helper'
|
10
|
+
require 'twisty_puzzles/color_scheme'
|
11
|
+
require 'twisty_puzzles/commutator'
|
12
|
+
require 'twisty_puzzles/compiled_algorithm'
|
13
|
+
require 'twisty_puzzles/compiled_cube_algorithm'
|
14
|
+
require 'twisty_puzzles/compiled_skewb_algorithm'
|
15
|
+
require 'twisty_puzzles/coordinate'
|
16
|
+
require 'twisty_puzzles/cube'
|
17
|
+
require 'twisty_puzzles/cube_constants'
|
18
|
+
require 'twisty_puzzles/cube_direction'
|
19
|
+
require 'twisty_puzzles/cube_move'
|
20
|
+
require 'twisty_puzzles/cube_move_parser'
|
21
|
+
require 'twisty_puzzles/cube_print_helper'
|
22
|
+
require 'twisty_puzzles/cube_state'
|
23
|
+
require 'twisty_puzzles/letter_scheme'
|
24
|
+
require 'twisty_puzzles/move_type_creator'
|
25
|
+
require 'twisty_puzzles/parser'
|
26
|
+
require 'twisty_puzzles/part_cycle_factory'
|
27
|
+
require 'twisty_puzzles/puzzle'
|
28
|
+
require 'twisty_puzzles/reversible_applyable'
|
29
|
+
require 'twisty_puzzles/rotation'
|
30
|
+
require 'twisty_puzzles/skewb_direction'
|
31
|
+
require 'twisty_puzzles/skewb_move'
|
32
|
+
require 'twisty_puzzles/skewb_move_parser'
|
33
|
+
require 'twisty_puzzles/skewb_notation'
|
34
|
+
require 'twisty_puzzles/skewb_state'
|
35
|
+
require 'twisty_puzzles/state_helper'
|
36
|
+
require 'twisty_puzzles/sticker_cycle'
|
37
|
+
require 'twisty_puzzles/twisty_puzzles_error'
|
38
|
+
require 'twisty_puzzles/version'
|
39
|
+
|
3
40
|
# Libraries for the twisty puzzles.
|
4
41
|
module TwistyPuzzles
|
5
42
|
end
|
@@ -1,54 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TwistyPuzzles
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
raise ArgumentError, "Invalid direction value #{value}."
|
17
|
-
end
|
18
|
-
|
19
|
-
@value = value
|
4
|
+
# Base class for directions.
|
5
|
+
class AbstractDirection
|
6
|
+
include Comparable
|
7
|
+
POSSIBLE_DIRECTION_NAMES = [[''], ['2', '2\''], ['\'', '3']].freeze
|
8
|
+
SIMPLE_DIRECTION_NAMES = (['0'] + POSSIBLE_DIRECTION_NAMES.map(&:first)).freeze
|
9
|
+
POSSIBLE_SKEWB_DIRECTION_NAMES = [['', '2\''], ['\'', '2']].freeze
|
10
|
+
SIMPLE_SKEWB_DIRECTION_NAMES = (['0'] + POSSIBLE_SKEWB_DIRECTION_NAMES.map(&:first)).freeze
|
11
|
+
|
12
|
+
def initialize(value)
|
13
|
+
raise TypeError, "Direction value #{value} isn't an integer." unless value.is_a?(Integer)
|
14
|
+
unless value >= 0 && value < self.class::NUM_DIRECTIONS
|
15
|
+
raise ArgumentError, "Invalid direction value #{value}."
|
20
16
|
end
|
21
17
|
|
22
|
-
|
18
|
+
@value = value
|
19
|
+
end
|
23
20
|
|
24
|
-
|
25
|
-
@value <=> other.value
|
26
|
-
end
|
21
|
+
attr_reader :value
|
27
22
|
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
def <=>(other)
|
24
|
+
@value <=> other.value
|
25
|
+
end
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
def zero?
|
28
|
+
@value.zero?
|
29
|
+
end
|
35
30
|
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
def non_zero?
|
32
|
+
@value.positive?
|
33
|
+
end
|
39
34
|
|
40
|
-
|
41
|
-
|
42
|
-
|
35
|
+
def inverse
|
36
|
+
self.class.new((self.class::NUM_DIRECTIONS - @value) % self.class::NUM_DIRECTIONS)
|
37
|
+
end
|
43
38
|
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
def +(other)
|
40
|
+
self.class.new((@value + other.value) % self.class::NUM_DIRECTIONS)
|
41
|
+
end
|
42
|
+
|
43
|
+
def eql?(other)
|
44
|
+
self.class.equal?(other.class) && @value == other.value
|
45
|
+
end
|
47
46
|
|
48
|
-
|
47
|
+
alias == eql?
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
end
|
49
|
+
def hash
|
50
|
+
@value.hash
|
53
51
|
end
|
52
|
+
end
|
54
53
|
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.reject { |_n, v| v.nil? }
|
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.map { |m| m.move_count(cube_size, metric) }.reduce(:+)
|
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
|