twisty_puzzles 0.0.1 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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