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