twisty_puzzles 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwistyPuzzles
4
+
5
+ # Class for creating one move type from its parts.
6
+ # Helper class for parsing logic.
7
+ class MoveTypeCreator
8
+ def initialize(capture_keys, move_class)
9
+ raise TypeError unless move_class.is_a?(Class)
10
+ raise TypeError unless capture_keys.all? { |k| k.is_a?(Symbol) }
11
+
12
+ @capture_keys = capture_keys.freeze
13
+ @move_class = move_class
14
+ end
15
+
16
+ def applies_to?(parsed_parts)
17
+ parsed_parts.keys.sort == @capture_keys.sort
18
+ end
19
+
20
+ def create(parsed_parts)
21
+ raise ArgumentError unless applies_to?(parsed_parts)
22
+
23
+ fields = @capture_keys.map { |name| parsed_parts[name] }
24
+ @move_class.new(*fields)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/algorithm'
4
+ require 'twisty_puzzles/commutator'
5
+ require 'twisty_puzzles/cube_move_parser'
6
+ require 'twisty_puzzles/skewb_move_parser'
7
+ require 'twisty_puzzles/twisty_puzzles_error'
8
+
9
+ module TwistyPuzzles
10
+ # rubocop:disable Style/Documentation
11
+
12
+ # rubocop:enable Style/Documentation
13
+ class CommutatorParseError < TwistyPuzzlesError
14
+ end
15
+
16
+ # Parser for commutators and algorithms.
17
+ class Parser
18
+ OPENING_BRACKET = '['
19
+ OPENING_PAREN = '('
20
+ CLOSING_BRACKET = ']'
21
+ CLOSING_PAREN = ')'
22
+ TIMES = '*'
23
+
24
+ def initialize(alg_string, move_parser)
25
+ @alg_string = alg_string
26
+ @scanner = StringScanner.new(alg_string)
27
+ @move_parser = move_parser
28
+ end
29
+
30
+ def parse_open_paren
31
+ complain('beginning of trigger') unless @scanner.getch == OPENING_PAREN
32
+ end
33
+
34
+ def parse_close_paren
35
+ complain('end of trigger') unless @scanner.getch == CLOSING_PAREN
36
+ end
37
+
38
+ def parse_times
39
+ complain('times symbol of multiplier') unless @scanner.getch == TIMES
40
+ end
41
+
42
+ def parse_factor
43
+ number = @scanner.scan(/\d+/)
44
+ complain('factor of multiplier') unless number
45
+ Integer(number, 10)
46
+ end
47
+
48
+ def parse_multiplier
49
+ skip_spaces
50
+ parse_times
51
+ skip_spaces
52
+ parse_factor
53
+ end
54
+
55
+ def parse_trigger
56
+ parse_open_paren
57
+ skip_spaces
58
+ moves = parse_moves_with_triggers
59
+ skip_spaces
60
+ parse_close_paren
61
+ skip_spaces
62
+ case @scanner.peek(1)
63
+ when TIMES
64
+ moves * parse_multiplier
65
+ when ('0'..'9')
66
+ moves * parse_factor
67
+ else
68
+ moves
69
+ end
70
+ end
71
+
72
+ # Parses at least one move and allows for triggers in parentheses.
73
+ def parse_moves_with_triggers
74
+ skip_spaces
75
+ if @scanner.peek(1) == OPENING_PAREN
76
+ parse_trigger + parse_moves_with_triggers
77
+ else
78
+ parse_moves
79
+ end
80
+ end
81
+
82
+ # Parses at least one move.
83
+ def parse_nonempty_moves
84
+ moves = parse_moves
85
+ complain('move') if moves.empty?
86
+ moves
87
+ end
88
+
89
+ # Parses a series of moves.
90
+ def parse_moves
91
+ moves = []
92
+ while (m = begin skip_spaces; parse_move_internal end)
93
+ moves.push(m)
94
+ end
95
+ Algorithm.new(moves)
96
+ end
97
+
98
+ def complain(parsed_object)
99
+ raise CommutatorParseError, <<~ERROR.chomp
100
+ Couldn't parse #{parsed_object} here:
101
+ #{@alg_string}
102
+ #{' ' * @scanner.pos}^"
103
+ ERROR
104
+ end
105
+
106
+ def check_eos(parsed_object)
107
+ complain("end of #{parsed_object}") unless @scanner.eos?
108
+ end
109
+
110
+ def parse_open_bracket
111
+ complain('beginning of commutator') unless @scanner.getch == OPENING_BRACKET
112
+ end
113
+
114
+ def parse_close_bracket
115
+ complain('end of commutator') unless @scanner.getch == CLOSING_BRACKET
116
+ end
117
+
118
+ def parse_commutator
119
+ skip_spaces
120
+ if @scanner.peek(1) == OPENING_BRACKET
121
+ parse_commutator_internal
122
+ else
123
+ FakeCommutator.new(parse_moves_with_triggers)
124
+ end
125
+ end
126
+
127
+ def parse_algorithm
128
+ skip_spaces
129
+ parse_moves_with_triggers
130
+ end
131
+
132
+ def parse_setup_commutator_inner
133
+ skip_spaces
134
+ if @scanner.peek(1) == OPENING_BRACKET
135
+ parse_pure_commutator
136
+ else
137
+ FakeCommutator.new(parse_moves_with_triggers)
138
+ end
139
+ end
140
+
141
+ def parse_pure_commutator
142
+ skip_spaces
143
+ parse_open_bracket
144
+ first_part = parse_nonempty_moves
145
+ skip_spaces
146
+ complain('middle of pure commutator') unless @scanner.getch == ','
147
+ second_part = parse_nonempty_moves
148
+ skip_spaces
149
+ parse_close_bracket
150
+ PureCommutator.new(first_part, second_part)
151
+ end
152
+
153
+ def parse_commutator_internal_after_separator(setup_or_first_part, separator)
154
+ if [':', ';'].include?(separator)
155
+ inner_commutator = parse_setup_commutator_inner
156
+ SetupCommutator.new(setup_or_first_part, inner_commutator)
157
+ elsif separator == ','
158
+ second_part = parse_nonempty_moves
159
+ PureCommutator.new(setup_or_first_part, second_part)
160
+ else
161
+ complain('end of setup or middle of pure commutator') unless @scanner.eos?
162
+ end
163
+ end
164
+
165
+ def parse_commutator_internal
166
+ skip_spaces
167
+ parse_open_bracket
168
+ setup_or_first_part = parse_nonempty_moves
169
+ skip_spaces
170
+ separator = @scanner.getch
171
+ comm = parse_commutator_internal_after_separator(setup_or_first_part, separator)
172
+ skip_spaces
173
+ parse_close_bracket
174
+ skip_spaces
175
+ complain('end of commutator') unless @scanner.eos?
176
+ comm
177
+ end
178
+
179
+ def parse_move_internal
180
+ move = @scanner.scan(@move_parser.regexp)
181
+ return unless move
182
+
183
+ @move_parser.parse_move(move)
184
+ end
185
+
186
+ def skip_spaces
187
+ @scanner.skip(/\s+/)
188
+ end
189
+ end
190
+
191
+ def parse_commutator(alg_string, complete_parse = true)
192
+ parser = Parser.new(alg_string, CubeMoveParser::INSTANCE)
193
+ commutator = parser.parse_commutator
194
+ parser.check_eos('commutator') if complete_parse
195
+ commutator
196
+ end
197
+
198
+ def parse_cube_algorithm(alg_string, complete_parse = true)
199
+ parser = Parser.new(alg_string, CubeMoveParser::INSTANCE)
200
+ algorithm = parser.parse_algorithm
201
+ parser.check_eos('algorithm') if complete_parse
202
+ algorithm
203
+ end
204
+
205
+ def parse_cube_move(move_string)
206
+ CubeMoveParser::INSTANCE.parse_move(move_string)
207
+ end
208
+
209
+ alias parse_algorithm parse_cube_algorithm
210
+ alias parse_move parse_cube_move
211
+
212
+ def parse_skewb_algorithm(alg_string, notation, complete_parse = true)
213
+ parser = Parser.new(alg_string, SkewbMoveParser.new(notation))
214
+ algorithm = parser.parse_algorithm
215
+ parser.check_eos('algorithm') if complete_parse
216
+ algorithm
217
+ end
218
+
219
+ def parse_skewb_move(move_string, notation)
220
+ SkewbMoveParser.new(notation).parse_move(move_string)
221
+ end
222
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/sticker_cycle'
4
+ require 'twisty_puzzles/utils/array_helper'
5
+
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 = {}
21
+ end
22
+
23
+ def coordinates(part)
24
+ @cache[part] ||= Coordinate.solved_positions(part, @cube_size, @incarnation_index)
25
+ end
26
+
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
31
+
32
+ cycles = parts.map { |p| StickerCycle.new(@cube_size, coordinates(p)) }
33
+ StickerCycles.new(@cube_size, cycles)
34
+ end
35
+
36
+ def check_type_consistency(parts)
37
+ return unless parts.any? { |p| p.class != parts.first.class }
38
+
39
+ raise TypeError, "Cycles of heterogenous piece types #{parts.inspect} are not supported."
40
+ end
41
+
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)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
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')
13
+
14
+ attr_reader :name
15
+
16
+ def eql?(other)
17
+ self.class == other.class && name == other.name
18
+ end
19
+
20
+ def hash
21
+ @hash ||= [self.class, @name].hash
22
+ end
23
+
24
+ alias == eql?
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/skewb_state'
4
+
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
14
+
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)
22
+
23
+ apply_to(puzzle_state)
24
+ begin
25
+ yield(puzzle_state)
26
+ ensure
27
+ inverse.apply_to(puzzle_state)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def with_dup_is_faster?(state)
34
+ !state.is_a?(CubeState) || state.n <= 4
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/algorithm'
4
+ require 'twisty_puzzles/axis_face_and_direction_move'
5
+ require 'twisty_puzzles/cube'
6
+ require 'twisty_puzzles/cube_direction'
7
+ require 'twisty_puzzles/cube_move'
8
+ require 'twisty_puzzles/puzzle'
9
+
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
27
+ end
28
+
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)
36
+
37
+ direction = translated_direction(skewb_direction)
38
+
39
+ Algorithm.new([
40
+ Rotation.new(corner.faces[skewb_direction.value], direction),
41
+ Rotation.new(corner.faces[0], direction)
42
+ ])
43
+ end
44
+
45
+ def to_s
46
+ "#{AXES[@axis_face.axis_priority]}#{canonical_direction.name}"
47
+ end
48
+
49
+ def puzzles
50
+ [Puzzle::SKEWB, Puzzle::NXN_CUBE]
51
+ end
52
+
53
+ def slice_move?
54
+ false
55
+ end
56
+
57
+ # Returns an alternative representation of the same rotation
58
+ def alternative
59
+ Rotation.new(@axis_face.opposite, @direction.inverse)
60
+ end
61
+
62
+ def equivalent_internal?(other, _cube_size)
63
+ [self, alternative].include?(other)
64
+ end
65
+
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
77
+ end
78
+
79
+ def prepend_fat_m_slice_move(_other, _cube_size)
80
+ nil
81
+ end
82
+
83
+ def prepend_fat_move(other, cube_size)
84
+ return unless compatible_fat_move?(other)
85
+
86
+ Algorithm.move(
87
+ FatMove.new(other.axis_face.opposite, other.direction, other.inverted_width(cube_size))
88
+ )
89
+ end
90
+
91
+ def prepend_slice_move(_other, _cube_size)
92
+ nil
93
+ end
94
+
95
+ def move_count(_cube_size, _metric = :htm)
96
+ 0
97
+ end
98
+
99
+ private
100
+
101
+ def compatible_fat_move?(other)
102
+ same_axis?(other) && translated_direction(other.axis_face) == other.direction.inverse
103
+ end
104
+ end
105
+ end