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
@@ -4,56 +4,53 @@ require 'twisty_puzzles/sticker_cycle'
|
|
4
4
|
require 'twisty_puzzles/utils/array_helper'
|
5
5
|
|
6
6
|
module TwistyPuzzles
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
raise ArgumentError, "Invalid incarnation index #{incarnation_index}."
|
16
|
-
end
|
17
|
-
|
18
|
-
@cube_size = cube_size
|
19
|
-
@incarnation_index = incarnation_index
|
20
|
-
@cache = {}
|
7
|
+
# Factory for sticker cycles given part cycles.
|
8
|
+
class PartCycleFactory
|
9
|
+
include Utils::ArrayHelper
|
10
|
+
|
11
|
+
def initialize(cube_size, incarnation_index)
|
12
|
+
CubeState.check_cube_size(cube_size)
|
13
|
+
unless incarnation_index.is_a?(Integer) && incarnation_index >= 0
|
14
|
+
raise ArgumentError, "Invalid incarnation index #{incarnation_index}."
|
21
15
|
end
|
22
16
|
|
23
|
-
|
24
|
-
|
25
|
-
|
17
|
+
@cube_size = cube_size
|
18
|
+
@incarnation_index = incarnation_index
|
19
|
+
@cache = {}
|
20
|
+
end
|
26
21
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
22
|
+
def coordinates(part)
|
23
|
+
@cache[part] ||= Coordinate.solved_positions(part, @cube_size, @incarnation_index)
|
24
|
+
end
|
31
25
|
|
32
|
-
|
33
|
-
|
26
|
+
def multi_twist(parts)
|
27
|
+
unless parts.all? { |p| p.is_a?(Corner) || p.is_a?(Edge) }
|
28
|
+
raise TypeError, 'Twists are only supported for edges and corners.'
|
34
29
|
end
|
35
30
|
|
36
|
-
|
37
|
-
|
31
|
+
cycles = parts.map { |p| StickerCycle.new(@cube_size, coordinates(p)) }
|
32
|
+
StickerCycles.new(@cube_size, cycles)
|
33
|
+
end
|
38
34
|
|
39
|
-
|
40
|
-
|
35
|
+
def check_type_consistency(parts)
|
36
|
+
return unless parts.any? { |p| p.class != parts.first.class }
|
41
37
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
check_types(parts, Part)
|
53
|
-
check_type_consistency(parts)
|
54
|
-
part_coordinates = parts.map { |p| coordinates(p) }
|
55
|
-
cycles = part_coordinates.transpose.map { |c| StickerCycle.new(@cube_size, c) }
|
56
|
-
StickerCycles.new(@cube_size, cycles)
|
38
|
+
raise TypeError, "Cycles of heterogenous piece types #{parts.inspect} are not supported."
|
39
|
+
end
|
40
|
+
|
41
|
+
def construct(parts)
|
42
|
+
raise ArgumentError, 'Cycles of length smaller than 2 are not supported.' if parts.length < 2
|
43
|
+
|
44
|
+
unless @incarnation_index < parts.first.num_incarnations(@cube_size)
|
45
|
+
raise ArgumentError, "Incarnation index #{@incarnation_index} for cube size " \
|
46
|
+
"#{@cube_size} is not supported for #{parts.first.inspect}."
|
57
47
|
end
|
48
|
+
|
49
|
+
check_types(parts, Part)
|
50
|
+
check_type_consistency(parts)
|
51
|
+
part_coordinates = parts.map { |p| coordinates(p) }
|
52
|
+
cycles = part_coordinates.transpose.map { |c| StickerCycle.new(@cube_size, c) }
|
53
|
+
StickerCycles.new(@cube_size, cycles)
|
58
54
|
end
|
55
|
+
end
|
59
56
|
end
|
@@ -1,26 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TwistyPuzzles
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
|
11
|
-
NXN_CUBE = Puzzle.new('nxn cube')
|
12
|
-
SKEWB = Puzzle.new('skewb')
|
4
|
+
# Represents one type of puzzle.
|
5
|
+
class Puzzle
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
13
9
|
|
14
|
-
|
10
|
+
NXN_CUBE = Puzzle.new('nxn cube')
|
11
|
+
SKEWB = Puzzle.new('skewb')
|
15
12
|
|
16
|
-
|
17
|
-
self.class == other.class && name == other.name
|
18
|
-
end
|
13
|
+
attr_reader :name
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
def eql?(other)
|
16
|
+
self.class == other.class && name == other.name
|
17
|
+
end
|
23
18
|
|
24
|
-
|
19
|
+
def hash
|
20
|
+
@hash ||= [self.class, @name].hash
|
25
21
|
end
|
22
|
+
|
23
|
+
alias == eql?
|
24
|
+
end
|
26
25
|
end
|
@@ -3,35 +3,34 @@
|
|
3
3
|
require 'twisty_puzzles/skewb_state'
|
4
4
|
|
5
5
|
module TwistyPuzzles
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
6
|
+
# Module that makes a class that has an `apply_to` and `reverse` be able to apply temporarily.
|
7
|
+
module ReversibleApplyable
|
8
|
+
def apply_to_dupped(puzzle_state)
|
9
|
+
dupped = puzzle_state.dup
|
10
|
+
apply_to(dupped)
|
11
|
+
dupped
|
12
|
+
end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
14
|
+
# Applies the current algorithm/cycle/whatever to the given puzzle state and yields the
|
15
|
+
# modified version. The puzzle state will be the same as the original after this function
|
16
|
+
# returns.
|
17
|
+
# Whether the yielded puzzle state is actually the same as the passed one or a copy is an
|
18
|
+
# implementation detail.
|
19
|
+
def apply_temporarily_to(puzzle_state)
|
20
|
+
return yield(apply_to_dupped(puzzle_state)) if with_dup_is_faster?(puzzle_state)
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
22
|
+
apply_to(puzzle_state)
|
23
|
+
begin
|
24
|
+
yield(puzzle_state)
|
25
|
+
ensure
|
26
|
+
inverse.apply_to(puzzle_state)
|
29
27
|
end
|
28
|
+
end
|
30
29
|
|
31
|
-
|
30
|
+
private
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
end
|
32
|
+
def with_dup_is_faster?(state)
|
33
|
+
!state.is_a?(CubeState) || state.n <= 4
|
36
34
|
end
|
35
|
+
end
|
37
36
|
end
|
@@ -8,98 +8,97 @@ require 'twisty_puzzles/cube_move'
|
|
8
8
|
require 'twisty_puzzles/puzzle'
|
9
9
|
|
10
10
|
module TwistyPuzzles
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
when SkewbDirection::BACKWARD then CubeDirection::BACKWARD
|
26
|
-
end
|
11
|
+
# A rotation of a Skewb or cube.
|
12
|
+
class Rotation < AxisFaceAndDirectionMove
|
13
|
+
ALL_ROTATIONS = Face::ELEMENTS.product(CubeDirection::ALL_DIRECTIONS).map { |f, d| new(f, d) }
|
14
|
+
NON_ZERO_ROTATIONS =
|
15
|
+
Face::ELEMENTS.product(CubeDirection::NON_ZERO_DIRECTIONS).map { |f, d| new(f, d) }
|
16
|
+
LEFT = new(Face::U, CubeDirection::BACKWARD)
|
17
|
+
RIGHT = new(Face::U, CubeDirection::FORWARD)
|
18
|
+
|
19
|
+
# Translates a Skewb direction into a cube direction.
|
20
|
+
def self.translated_direction(direction)
|
21
|
+
case direction
|
22
|
+
when SkewbDirection::ZERO then CubeDirection::ZERO
|
23
|
+
when SkewbDirection::FORWARD then CubeDirection::FORWARD
|
24
|
+
when SkewbDirection::BACKWARD then CubeDirection::BACKWARD
|
27
25
|
end
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
28
|
+
# Returns an algorithm consisting of two rotations that are equivalent to rotating
|
29
|
+
# the puzzle around a corner.
|
30
|
+
# Takes a Skewb direction as an argument (even for cubes) because rotating around
|
31
|
+
# is like a Skewb move given that it's modulo 3.
|
32
|
+
def self.around_corner(corner, skewb_direction)
|
33
|
+
raise TypeError unless corner.is_a?(Corner)
|
34
|
+
raise TypeError unless skewb_direction.is_a?(SkewbDirection)
|
36
35
|
|
37
|
-
|
36
|
+
direction = translated_direction(skewb_direction)
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
38
|
+
Algorithm.new([
|
39
|
+
Rotation.new(corner.faces[skewb_direction.value], direction),
|
40
|
+
Rotation.new(corner.faces[0], direction)
|
41
|
+
])
|
42
|
+
end
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
def to_s
|
45
|
+
"#{AXES[@axis_face.axis_priority]}#{canonical_direction.name}"
|
46
|
+
end
|
48
47
|
|
49
|
-
|
50
|
-
|
51
|
-
|
48
|
+
def puzzles
|
49
|
+
[Puzzle::SKEWB, Puzzle::NXN_CUBE]
|
50
|
+
end
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
def slice_move?
|
53
|
+
false
|
54
|
+
end
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
56
|
+
# Returns an alternative representation of the same rotation
|
57
|
+
def alternative
|
58
|
+
Rotation.new(@axis_face.opposite, @direction.inverse)
|
59
|
+
end
|
61
60
|
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
def equivalent_internal?(other, _cube_size)
|
62
|
+
[self, alternative].include?(other)
|
63
|
+
end
|
65
64
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
65
|
+
def prepend_rotation(other, _cube_size)
|
66
|
+
if same_axis?(other)
|
67
|
+
direction = translated_direction(other.axis_face)
|
68
|
+
Algorithm.move(Rotation.new(other.axis_face, direction + other.direction))
|
69
|
+
elsif @direction.double_move? && other.direction.double_move?
|
70
|
+
used_axis_priorities = [@axis_face, other.axis_face].map(&:axis_priority)
|
71
|
+
# Note that there are two solutions, but any works.
|
72
|
+
remaining_face =
|
73
|
+
Face::ELEMENTS.find { |f| !used_axis_priorities.include?(f.axis_priority) }
|
74
|
+
Algorithm.move(Rotation.new(remaining_face, CubeDirection::DOUBLE))
|
77
75
|
end
|
76
|
+
end
|
78
77
|
|
79
|
-
|
80
|
-
|
81
|
-
|
78
|
+
def prepend_fat_m_slice_move(_other, _cube_size)
|
79
|
+
nil
|
80
|
+
end
|
82
81
|
|
83
|
-
|
84
|
-
|
82
|
+
def prepend_fat_move(other, cube_size)
|
83
|
+
return unless compatible_fat_move?(other)
|
85
84
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
85
|
+
Algorithm.move(
|
86
|
+
FatMove.new(other.axis_face.opposite, other.direction, other.inverted_width(cube_size))
|
87
|
+
)
|
88
|
+
end
|
90
89
|
|
91
|
-
|
92
|
-
|
93
|
-
|
90
|
+
def prepend_slice_move(_other, _cube_size)
|
91
|
+
nil
|
92
|
+
end
|
94
93
|
|
95
|
-
|
96
|
-
|
97
|
-
|
94
|
+
def move_count(_cube_size, _metric = :htm)
|
95
|
+
0
|
96
|
+
end
|
98
97
|
|
99
|
-
|
98
|
+
private
|
100
99
|
|
101
|
-
|
102
|
-
|
103
|
-
end
|
100
|
+
def compatible_fat_move?(other)
|
101
|
+
same_axis?(other) && translated_direction(other.axis_face) == other.direction.inverse
|
104
102
|
end
|
103
|
+
end
|
105
104
|
end
|
@@ -3,22 +3,21 @@
|
|
3
3
|
require 'twisty_puzzles/abstract_direction'
|
4
4
|
|
5
5
|
module TwistyPuzzles
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
BACKWARD = new(2)
|
6
|
+
# Represents the direction of a Skewb move except a rotation.
|
7
|
+
class SkewbDirection < AbstractDirection
|
8
|
+
NUM_DIRECTIONS = 3
|
9
|
+
NON_ZERO_DIRECTIONS = (1...NUM_DIRECTIONS).map { |d| new(d) }.freeze
|
10
|
+
ALL_DIRECTIONS = Array.new(NUM_DIRECTIONS) { |d| new(d) }.freeze
|
11
|
+
ZERO = new(0)
|
12
|
+
FORWARD = new(1)
|
13
|
+
BACKWARD = new(2)
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
def name
|
16
|
+
SIMPLE_SKEWB_DIRECTION_NAMES[@value]
|
17
|
+
end
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
19
|
+
def double_move?
|
20
|
+
false
|
23
21
|
end
|
22
|
+
end
|
24
23
|
end
|
@@ -6,54 +6,53 @@ require 'twisty_puzzles/skewb_direction'
|
|
6
6
|
require 'twisty_puzzles/puzzle'
|
7
7
|
|
8
8
|
module TwistyPuzzles
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@direction = direction
|
18
|
-
end
|
19
|
-
|
20
|
-
def puzzles
|
21
|
-
[Puzzle::SKEWB]
|
22
|
-
end
|
23
|
-
|
24
|
-
attr_reader :axis_corner, :direction
|
25
|
-
|
26
|
-
def to_s
|
27
|
-
"#{@axis_corner}#{@direction.name}"
|
28
|
-
end
|
29
|
-
|
30
|
-
def slice_move?
|
31
|
-
false
|
32
|
-
end
|
33
|
-
|
34
|
-
def identifying_fields
|
35
|
-
[@axis_corner, @direction]
|
36
|
-
end
|
37
|
-
|
38
|
-
def rotate_by(rotation)
|
39
|
-
nice_face =
|
40
|
-
find_only(@axis_corner.adjacent_faces) do |f|
|
41
|
-
f.same_axis?(rotation.axis_face)
|
42
|
-
end
|
43
|
-
nice_direction = rotation.translated_direction(nice_face)
|
44
|
-
nice_face_corners = nice_face.clockwise_corners
|
45
|
-
on_nice_face_index = nice_face_corners.index { |c| c.turned_equals?(@axis_corner) }
|
46
|
-
new_corner =
|
47
|
-
nice_face_corners[(on_nice_face_index + nice_direction.value) % nice_face_corners.length]
|
48
|
-
self.class.new(new_corner, @direction)
|
49
|
-
end
|
50
|
-
|
51
|
-
def mirror(normal_face)
|
52
|
-
faces = @axis_corner.adjacent_faces
|
53
|
-
replaced_face = find_only(faces) { |f| f.same_axis?(normal_face) }
|
54
|
-
new_corner =
|
55
|
-
Corner.between_faces(replace_once(faces, replaced_face, replaced_face.opposite))
|
56
|
-
self.class.new(new_corner, @direction.inverse)
|
57
|
-
end
|
9
|
+
# Base class for skewb moves.
|
10
|
+
class SkewbMove < AbstractMove
|
11
|
+
def initialize(axis_corner, direction)
|
12
|
+
raise TypeError unless axis_corner.is_a?(Corner)
|
13
|
+
raise TypeError unless direction.is_a?(SkewbDirection)
|
14
|
+
|
15
|
+
@axis_corner = axis_corner.rotate_face_up(axis_corner.faces.min_by(&:piece_index))
|
16
|
+
@direction = direction
|
58
17
|
end
|
18
|
+
|
19
|
+
def puzzles
|
20
|
+
[Puzzle::SKEWB]
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :axis_corner, :direction
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"#{@axis_corner}#{@direction.name}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def slice_move?
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def identifying_fields
|
34
|
+
[@axis_corner, @direction]
|
35
|
+
end
|
36
|
+
|
37
|
+
def rotate_by(rotation)
|
38
|
+
nice_face =
|
39
|
+
find_only(@axis_corner.adjacent_faces) do |f|
|
40
|
+
f.same_axis?(rotation.axis_face)
|
41
|
+
end
|
42
|
+
nice_direction = rotation.translated_direction(nice_face)
|
43
|
+
nice_face_corners = nice_face.clockwise_corners
|
44
|
+
on_nice_face_index = nice_face_corners.index { |c| c.turned_equals?(@axis_corner) }
|
45
|
+
new_corner =
|
46
|
+
nice_face_corners[(on_nice_face_index + nice_direction.value) % nice_face_corners.length]
|
47
|
+
self.class.new(new_corner, @direction)
|
48
|
+
end
|
49
|
+
|
50
|
+
def mirror(normal_face)
|
51
|
+
faces = @axis_corner.adjacent_faces
|
52
|
+
replaced_face = find_only(faces) { |f| f.same_axis?(normal_face) }
|
53
|
+
new_corner =
|
54
|
+
Corner.between_faces(replace_once(faces, replaced_face, replaced_face.opposite))
|
55
|
+
self.class.new(new_corner, @direction.inverse)
|
56
|
+
end
|
57
|
+
end
|
59
58
|
end
|