twisty_puzzles 0.0.1

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/LICENSE +21 -0
  5. data/README.md +32 -0
  6. data/ext/twisty_puzzles/native/extconf.rb +5 -0
  7. data/lib/twisty_puzzles/abstract_direction.rb +54 -0
  8. data/lib/twisty_puzzles/abstract_move.rb +170 -0
  9. data/lib/twisty_puzzles/abstract_move_parser.rb +45 -0
  10. data/lib/twisty_puzzles/algorithm.rb +155 -0
  11. data/lib/twisty_puzzles/algorithm_transformation.rb +33 -0
  12. data/lib/twisty_puzzles/axis_face_and_direction_move.rb +78 -0
  13. data/lib/twisty_puzzles/cancellation_helper.rb +165 -0
  14. data/lib/twisty_puzzles/color_scheme.rb +174 -0
  15. data/lib/twisty_puzzles/commutator.rb +118 -0
  16. data/lib/twisty_puzzles/compiled_algorithm.rb +48 -0
  17. data/lib/twisty_puzzles/compiled_cube_algorithm.rb +67 -0
  18. data/lib/twisty_puzzles/compiled_skewb_algorithm.rb +28 -0
  19. data/lib/twisty_puzzles/coordinate.rb +318 -0
  20. data/lib/twisty_puzzles/cube.rb +660 -0
  21. data/lib/twisty_puzzles/cube_constants.rb +53 -0
  22. data/lib/twisty_puzzles/cube_direction.rb +27 -0
  23. data/lib/twisty_puzzles/cube_move.rb +384 -0
  24. data/lib/twisty_puzzles/cube_move_parser.rb +100 -0
  25. data/lib/twisty_puzzles/cube_print_helper.rb +160 -0
  26. data/lib/twisty_puzzles/cube_state.rb +113 -0
  27. data/lib/twisty_puzzles/letter_scheme.rb +72 -0
  28. data/lib/twisty_puzzles/move_type_creator.rb +27 -0
  29. data/lib/twisty_puzzles/parser.rb +222 -0
  30. data/lib/twisty_puzzles/part_cycle_factory.rb +59 -0
  31. data/lib/twisty_puzzles/puzzle.rb +26 -0
  32. data/lib/twisty_puzzles/reversible_applyable.rb +37 -0
  33. data/lib/twisty_puzzles/rotation.rb +105 -0
  34. data/lib/twisty_puzzles/skewb_direction.rb +24 -0
  35. data/lib/twisty_puzzles/skewb_move.rb +59 -0
  36. data/lib/twisty_puzzles/skewb_move_parser.rb +73 -0
  37. data/lib/twisty_puzzles/skewb_notation.rb +147 -0
  38. data/lib/twisty_puzzles/skewb_state.rb +163 -0
  39. data/lib/twisty_puzzles/state_helper.rb +32 -0
  40. data/lib/twisty_puzzles/sticker_cycle.rb +70 -0
  41. data/lib/twisty_puzzles/twisty_puzzles_error.rb +6 -0
  42. data/lib/twisty_puzzles/utils/array_helper.rb +109 -0
  43. data/lib/twisty_puzzles/utils/string_helper.rb +26 -0
  44. data/lib/twisty_puzzles/utils.rb +7 -0
  45. data/lib/twisty_puzzles/version.rb +3 -0
  46. data/lib/twisty_puzzles.rb +5 -0
  47. metadata +249 -0
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/cube_direction'
4
+ require 'twisty_puzzles/rotation'
5
+
6
+ module TwistyPuzzles
7
+
8
+ AlgorithmTransformation =
9
+ Struct.new(:rotation, :mirror, :mirror_normal_face) do
10
+ def transformed(algorithm)
11
+ algorithm = algorithm.mirror(mirror_normal_face) if mirror
12
+ algorithm.rotate_by(rotation)
13
+ end
14
+
15
+ def identity?
16
+ rotation.identity? && !mirror
17
+ end
18
+
19
+ # Returns algorithm transformations that mirror an algorithm and rotate it around a face.
20
+ def self.around_face(face)
21
+ around_face_rotations = CubeDirection::ALL_DIRECTIONS.map { |d| Rotation.new(face, d) }
22
+ mirror_normal_face = face.neighbors.first
23
+ around_face_rotations.product([true, false]).map do |r, m|
24
+ AlgorithmTransformation.new(r, m, mirror_normal_face)
25
+ end
26
+ end
27
+
28
+ def self.around_face_without_identity(face)
29
+ around_face(face).reject(&:identity?)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_move'
4
+ require 'twisty_puzzles/cube_direction'
5
+
6
+ module TwistyPuzzles
7
+
8
+ # Intermediate base class for all types of moves that have an axis face and a direction,
9
+ # i.e. cube moves and rotations.
10
+ class AxisFaceAndDirectionMove < AbstractMove
11
+ def initialize(axis_face, direction)
12
+ raise TypeError, "Unsuitable axis face #{axis_face}." unless axis_face.is_a?(Face)
13
+ raise TypeError unless direction.is_a?(CubeDirection)
14
+
15
+ @axis_face = axis_face
16
+ @direction = direction
17
+ end
18
+
19
+ attr_reader :direction, :axis_face
20
+
21
+ def translated_direction(other_axis_face)
22
+ case @axis_face
23
+ when other_axis_face then @direction
24
+ when other_axis_face.opposite then @direction.inverse
25
+ else raise ArgumentError
26
+ end
27
+ end
28
+
29
+ def same_axis?(other)
30
+ @axis_face.same_axis?(other.axis_face)
31
+ end
32
+
33
+ def identifying_fields
34
+ [@axis_face, @direction]
35
+ end
36
+
37
+ def canonical_direction
38
+ @axis_face.canonical_axis_face? ? @direction : @direction.inverse
39
+ end
40
+
41
+ def can_swap?(other)
42
+ super || same_axis?(other)
43
+ end
44
+
45
+ def swap_internal(other)
46
+ if same_axis?(other)
47
+ [other, self]
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ def rotate_by(rotation)
54
+ if same_axis?(rotation)
55
+ self
56
+ else
57
+ rotation_neighbors = rotation.axis_face.neighbors
58
+ face_index = rotation_neighbors.index(@axis_face) || raise
59
+ new_axis_face =
60
+ rotation_neighbors[(face_index + rotation.direction.value) % rotation_neighbors.length]
61
+ fields = replace_once(identifying_fields, @axis_face, new_axis_face)
62
+ self.class.new(*fields)
63
+ end
64
+ end
65
+
66
+ def mirror(normal_face)
67
+ if normal_face.same_axis?(@axis_face)
68
+ fields = replace_once(
69
+ replace_once(identifying_fields, @direction, @direction.inverse),
70
+ @axis_face, @axis_face.opposite
71
+ )
72
+ self.class.new(*fields)
73
+ else
74
+ inverse
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_move'
4
+ require 'twisty_puzzles/algorithm'
5
+ require 'twisty_puzzles/cube_constants'
6
+ require 'twisty_puzzles/cube_state'
7
+
8
+ module TwistyPuzzles
9
+
10
+ # Helper class to figure out information about the cancellation between two algs.
11
+ module CancellationHelper
12
+ include CubeConstants
13
+
14
+ def self.swap_to_end(algorithm, index)
15
+ new_moves = algorithm.moves.dup
16
+ index.upto(algorithm.length - 2) do |current_index|
17
+ obstacle_index = current_index + 1
18
+ current = new_moves[current_index]
19
+ obstacle = new_moves[obstacle_index]
20
+ return nil unless current.can_swap?(obstacle)
21
+
22
+ new_moves[current_index], new_moves[obstacle_index] = current.swap(obstacle)
23
+ end
24
+ Algorithm.new(new_moves)
25
+ end
26
+
27
+ # Possible variations of the algorithm where the last move has been swapped as much as allowed
28
+ # (e.g. D U can swap).
29
+ def self.cancel_variants(algorithm)
30
+ variants = []
31
+ algorithm.moves.each_index.reverse_each do |i|
32
+ variant = swap_to_end(algorithm, i)
33
+ break unless variant
34
+
35
+ variants.push(variant)
36
+ end
37
+ raise if variants.empty?
38
+
39
+ variants
40
+ end
41
+
42
+ # Cancel this algorithm as much as possilbe
43
+ def self.cancel(algorithm, cube_size)
44
+ raise TypeError unless algorithm.is_a?(Algorithm)
45
+
46
+ CubeState.check_cube_size(cube_size)
47
+ alg = Algorithm::EMPTY
48
+ algorithm.moves.each do |m|
49
+ alg = push_with_cancellation(alg, m, cube_size)
50
+ end
51
+ alg
52
+ end
53
+
54
+ def self.combine_transformations(left, right)
55
+ left.dup.transform_values { |e| right[e] }.freeze
56
+ end
57
+
58
+ def self.apply_transformation_to!(transformation, face_state)
59
+ face_state.map! { |f| transformation[f] }
60
+ end
61
+
62
+ TRIVIAL_CENTER_TRANSFORMATION = { U: :U, F: :F, R: :R, L: :L, B: :B, D: :D }.freeze
63
+
64
+ def self.create_directed_transformations(basic_transformation, invert)
65
+ twice = combine_transformations(basic_transformation, basic_transformation)
66
+ thrice = combine_transformations(twice, basic_transformation)
67
+ non_zero_transformations = [basic_transformation, twice, thrice]
68
+ adjusted_non_zero_transformations =
69
+ invert ? non_zero_transformations.reverse : non_zero_transformations
70
+ [TRIVIAL_CENTER_TRANSFORMATION] + adjusted_non_zero_transformations
71
+ end
72
+
73
+ CENTER_TRANSFORMATIONS =
74
+ begin
75
+ x_transformation = { U: :B, F: :U, R: :R, L: :L, B: :D, D: :F }.freeze
76
+ y_transformation = { U: :U, F: :L, R: :F, L: :B, B: :R, D: :D }.freeze
77
+ z_transformation = { U: :R, F: :F, R: :D, L: :U, B: :B, D: :L }.freeze
78
+ {
79
+ U: create_directed_transformations(y_transformation, false),
80
+ F: create_directed_transformations(z_transformation, false),
81
+ R: create_directed_transformations(x_transformation, false),
82
+ L: create_directed_transformations(x_transformation, true),
83
+ B: create_directed_transformations(z_transformation, true),
84
+ D: create_directed_transformations(y_transformation, true)
85
+ }
86
+ end
87
+
88
+ def self.center_transformation(rotation)
89
+ CENTER_TRANSFORMATIONS[rotation.axis_face.face_symbol][rotation.direction.value]
90
+ end
91
+
92
+ def self.rotated_center_state(rotations)
93
+ rotations.reduce(FACE_SYMBOLS.dup) do |center_state, rotation|
94
+ apply_transformation_to!(center_transformation(rotation), center_state)
95
+ end
96
+ end
97
+
98
+ def self.combined_rotation_algs
99
+ Rotation::NON_ZERO_ROTATIONS.collect_concat do |left|
100
+ second_rotations =
101
+ Rotation::NON_ZERO_ROTATIONS.reject do |e|
102
+ e.direction.double_move? || e.same_axis?(left)
103
+ end
104
+ second_rotations.map { |right| Algorithm.new([left, right]) }
105
+ end
106
+ end
107
+
108
+ def self.rotation_sequences
109
+ @rotation_sequences ||=
110
+ begin
111
+ trivial_rotation_algs = [Algorithm::EMPTY]
112
+ single_rotation_algs = Rotation::NON_ZERO_ROTATIONS.map { |e| Algorithm.move(e) }
113
+ combined_rotation_algs = self.combined_rotation_algs
114
+ rotation_algs = trivial_rotation_algs + single_rotation_algs + combined_rotation_algs
115
+ rotation_algs.map do |alg|
116
+ [rotated_center_state(alg.moves), alg]
117
+ end.to_h.freeze
118
+ end
119
+ end
120
+
121
+ def self.cancelled_rotations(rotations)
122
+ center_state = rotated_center_state(rotations)
123
+ rotation_sequences[center_state]
124
+ end
125
+
126
+ def self.num_tail_rotations(algorithm)
127
+ num = 0
128
+ algorithm.moves.reverse_each do |e|
129
+ break unless e.is_a?(Rotation)
130
+
131
+ num += 1
132
+ end
133
+ num
134
+ end
135
+
136
+ def self.alg_plus_cancelled_move(algorithm, move, cube_size)
137
+ if move.is_a?(Rotation) && (tail_rotations = num_tail_rotations(algorithm)) >= 2
138
+ Algorithm.new(algorithm.moves[0...-tail_rotations]) +
139
+ cancelled_rotations(algorithm.moves[-tail_rotations..-1] + [move])
140
+ else
141
+ Algorithm.new(algorithm.moves[0...-1]) +
142
+ algorithm.moves[-1].join_with_cancellation(move, cube_size)
143
+ end
144
+ end
145
+
146
+ def self.push_with_cancellation(algorithm, move, cube_size)
147
+ raise TypeError unless move.is_a?(AbstractMove)
148
+ return Algorithm.move(move) if algorithm.empty?
149
+
150
+ cancel_variants =
151
+ cancel_variants(algorithm).map do |alg|
152
+ alg_plus_cancelled_move(alg, move, cube_size)
153
+ end
154
+ cancel_variants.min_by do |alg|
155
+ # QTM is the most sensitive metric, so we use that as the highest priority for
156
+ # cancellations.
157
+ # We use HTM as a second priority to make sure something like RR still gets merged into
158
+ # R2.
159
+ # We use the length as tertiary priority to make sure rotations get cancelled even if they
160
+ # don't change the move count.
161
+ [alg.move_count(cube_size, :qtm), alg.move_count(cube_size, :htm), alg.length]
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/cube'
4
+ require 'twisty_puzzles/cube_constants'
5
+ require 'twisty_puzzles/cube_state'
6
+ require 'twisty_puzzles/skewb_state'
7
+ require 'twisty_puzzles/utils/array_helper'
8
+
9
+ module TwistyPuzzles
10
+ # A color scheme that assigns a color to each face.
11
+ class ColorScheme
12
+ include CubeConstants
13
+ include Utils::ArrayHelper
14
+
15
+ RESERVED_COLORS = %i[transparent unknown oriented].freeze
16
+
17
+ def initialize(face_symbols_to_colors)
18
+ check_face_symbols_to_colors(face_symbols_to_colors)
19
+
20
+ num_uniq_colors = face_symbols_to_colors.values.uniq.length
21
+ unless num_uniq_colors == FACE_SYMBOLS.length
22
+ raise ArgumentError, "Got #{num_uniq_colors} unique colors " \
23
+ "#{face_symbols_to_colors.values.uniq}, " \
24
+ "but needed #{FACE_SYMBOLS.length}."
25
+ end
26
+
27
+ @face_symbols_to_colors = face_symbols_to_colors
28
+ @colors_to_face_symbols = face_symbols_to_colors.invert
29
+ end
30
+
31
+ def color(face_symbol)
32
+ @face_symbols_to_colors[face_symbol]
33
+ end
34
+
35
+ def opposite_color(color)
36
+ color(opposite_face_symbol(face_symbol(color)))
37
+ end
38
+
39
+ def part_for_colors(part_type, colors)
40
+ raise ArgumentError unless part_type.is_a?(Class)
41
+
42
+ part_type.for_face_symbols(colors.map { |c| face_symbol(c) })
43
+ end
44
+
45
+ def face_symbol(color)
46
+ @colors_to_face_symbols[color]
47
+ end
48
+
49
+ def colors
50
+ @face_symbols_to_colors.values
51
+ end
52
+
53
+ def turned(top_color, front_color)
54
+ raise ArgumentError if top_color == front_color
55
+ raise ArgumentError if opposite_color(top_color) == front_color
56
+ raise ArgumentError unless colors.include?(top_color)
57
+ raise ArgumentError unless colors.include?(front_color)
58
+
59
+ # Note: The reason that this is so complicated is that we want it to still work if the
60
+ # chirality corner gets exchanged.
61
+
62
+ # Do the obvious and handle opposites of the top and front color so we have no
63
+ # assumptions that the chirality corner contains U and F.
64
+ turned_face_symbols_to_colors =
65
+ obvious_turned_face_symbols_to_colors(top_color, front_color)
66
+
67
+ # Now find the corner that gets mapped to the chirality corner. We know
68
+ # two of its colors and the position of the missing color.
69
+ chirality_corner_source, unknown_index =
70
+ chirality_corner_source_and_unknown_index(turned_face_symbols_to_colors)
71
+
72
+ add_missing_mappings(turned_face_symbols_to_colors, chirality_corner_source, unknown_index)
73
+
74
+ ColorScheme.new(turned_face_symbols_to_colors)
75
+ end
76
+
77
+ def solved_cube_state(cube_size)
78
+ stickers =
79
+ ordered_colors.map do |c|
80
+ Array.new(cube_size) { Array.new(cube_size) { c } }
81
+ end
82
+ CubeState.from_stickers(cube_size, stickers)
83
+ end
84
+
85
+ # Colors in the order of the face symbols.
86
+ def ordered_colors
87
+ FACE_SYMBOLS.map { |s| color(s) }
88
+ end
89
+
90
+ def solved_skewb_state
91
+ SkewbState.for_solved_colors(@face_symbols_to_colors.dup)
92
+ end
93
+
94
+ private
95
+
96
+ def chirality_corner_source_and_unknown_index(obvious_turned_face_symbols_to_colors)
97
+ corner_matcher =
98
+ CornerMatcher.new(CHIRALITY_FACE_SYMBOLS.map do |s|
99
+ # This will return nil for exactly one face that we don't know yet.
100
+ @colors_to_face_symbols[obvious_turned_face_symbols_to_colors[s]]
101
+ end)
102
+
103
+ # There should be exactly one corner that gets mapped to the chirality corner.
104
+ chirality_corner_source =
105
+ find_only(Corner::ELEMENTS) do |corner|
106
+ corner_matcher.matches?(corner)
107
+ end
108
+ [chirality_corner_source, corner_matcher.wildcard_index]
109
+ end
110
+
111
+ # Corner matcher that finds a corner that has one arbitrary
112
+ # face symbol and two given face symbol.
113
+ class CornerMatcher
114
+ def initialize(face_symbol_matchers)
115
+ unless face_symbol_matchers.count(&:nil?) == 1
116
+ raise ArgumentError, 'Exactly one nil allowed in face symbol matchers.'
117
+ end
118
+
119
+ @face_symbol_matchers = face_symbol_matchers
120
+ end
121
+
122
+ def matches?(corner)
123
+ corner.face_symbols.zip(@face_symbol_matchers).all? do |face_symbol, face_symbol_matcher|
124
+ face_symbol_matcher.nil? || face_symbol == face_symbol_matcher
125
+ end
126
+ end
127
+
128
+ def wildcard_index
129
+ @face_symbol_matchers.index(nil)
130
+ end
131
+ end
132
+
133
+ def check_face_symbols_to_colors(face_symbols_to_colors)
134
+ raise ArgumentError unless face_symbols_to_colors.keys.sort == FACE_SYMBOLS.sort
135
+
136
+ face_symbols_to_colors.each_value do |c|
137
+ raise TypeError unless c.is_a?(Symbol)
138
+
139
+ if RESERVED_COLORS.include?(c)
140
+ raise ArgumentError,
141
+ "Color #{c} cannot be part of the color scheme because it is a reserved color."
142
+ end
143
+ end
144
+ raise ArgumentError unless face_symbols_to_colors.values.all? { |c| c.is_a?(Symbol) }
145
+ end
146
+
147
+ def add_missing_mappings(turned_face_symbols_to_colors, chirality_corner_source, unknown_index)
148
+ missing_face_symbol = CHIRALITY_FACE_SYMBOLS[unknown_index]
149
+ missing_face_symbol_source =
150
+ chirality_corner_source.face_symbols[unknown_index]
151
+ turned_face_symbols_to_colors[missing_face_symbol] = color(missing_face_symbol_source)
152
+ turned_face_symbols_to_colors[opposite_face_symbol(missing_face_symbol)] =
153
+ color(opposite_face_symbol(missing_face_symbol_source))
154
+ end
155
+
156
+ def obvious_turned_face_symbols_to_colors(top_color, front_color)
157
+ result = { U: top_color, F: front_color }
158
+ opposites = result.map do |face_symbol, color|
159
+ [opposite_face_symbol(face_symbol), opposite_color(color)]
160
+ end.to_h
161
+ result.merge!(opposites)
162
+ end
163
+
164
+ WCA = new(
165
+ U: :white,
166
+ F: :green,
167
+ R: :red,
168
+ L: :orange,
169
+ B: :blue,
170
+ D: :yellow
171
+ ).freeze
172
+ BERNHARD = WCA.turned(:yellow, :red).freeze
173
+ end
174
+ end