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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_direction'
4
+
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)
15
+
16
+ def name
17
+ SIMPLE_SKEWB_DIRECTION_NAMES[@value]
18
+ end
19
+
20
+ def double_move?
21
+ false
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_move'
4
+ require 'twisty_puzzles/cube'
5
+ require 'twisty_puzzles/skewb_direction'
6
+ require 'twisty_puzzles/puzzle'
7
+
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
58
+ end
59
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_direction'
4
+ require 'twisty_puzzles/abstract_move_parser'
5
+ require 'twisty_puzzles/move_type_creator'
6
+ require 'twisty_puzzles/rotation'
7
+ require 'twisty_puzzles/skewb_move'
8
+ require 'twisty_puzzles/skewb_notation'
9
+
10
+ module TwistyPuzzles
11
+
12
+ # Parser for Skewb moves.
13
+ class SkewbMoveParser < AbstractMoveParser
14
+ MOVE_TYPE_CREATORS = [
15
+ MoveTypeCreator.new(%i[axis_face cube_direction], Rotation),
16
+ MoveTypeCreator.new(%i[axis_corner skewb_direction], SkewbMove)
17
+ ].freeze
18
+
19
+ def initialize(notation)
20
+ raise TypeError unless notation.is_a?(SkewbNotation)
21
+
22
+ @notation = notation
23
+ end
24
+
25
+ FIXED_CORNER_INSTANCE = SkewbMoveParser.new(SkewbNotation.fixed_corner)
26
+ SARAH_INSTANCE = SkewbMoveParser.new(SkewbNotation.sarah)
27
+ RUBIKS_INSTANCE = SkewbMoveParser.new(SkewbNotation.rubiks)
28
+
29
+ def regexp
30
+ @regexp ||=
31
+ begin
32
+ skewb_direction_names =
33
+ AbstractDirection::POSSIBLE_SKEWB_DIRECTION_NAMES.flatten
34
+ move_part = "(?:(?<skewb_move>[#{@notation.move_strings.join}])" \
35
+ "(?<skewb_direction>[#{skewb_direction_names.join}]?))"
36
+ rotation_direction_names =
37
+ AbstractDirection::POSSIBLE_DIRECTION_NAMES.flatten
38
+ rotation_direction_names.sort_by! { |e| -e.length }
39
+ rotation_part = "(?:(?<axis_name>[#{AbstractMove::AXES.join}])" \
40
+ "(?<cube_direction>#{rotation_direction_names.join('|')}))"
41
+ Regexp.new("#{move_part}|#{rotation_part}")
42
+ end
43
+ end
44
+
45
+ def move_type_creators
46
+ MOVE_TYPE_CREATORS
47
+ end
48
+
49
+ def parse_skewb_direction(direction_string)
50
+ if AbstractDirection::POSSIBLE_DIRECTION_NAMES[0].include?(direction_string)
51
+ SkewbDirection::FORWARD
52
+ elsif AbstractDirection::POSSIBLE_DIRECTION_NAMES[-1].include?(direction_string)
53
+ SkewbDirection::BACKWARD
54
+ else
55
+ raise ArgumentError
56
+ end
57
+ end
58
+
59
+ def parse_part_key(name)
60
+ name.sub('name', 'face').sub('skewb_move', 'axis_corner')
61
+ end
62
+
63
+ def parse_move_part(name, value)
64
+ case name
65
+ when 'axis_name' then CubeMoveParser::INSTANCE.parse_axis_face(value)
66
+ when 'cube_direction' then CubeMoveParser::INSTANCE.parse_direction(value)
67
+ when 'skewb_move' then @notation.corner(value)
68
+ when 'skewb_direction' then parse_skewb_direction(value)
69
+ else raise
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/cancellation_helper'
4
+ require 'twisty_puzzles/cube'
5
+ require 'twisty_puzzles/cube_direction'
6
+ require 'twisty_puzzles/skewb_direction'
7
+ require 'twisty_puzzles/skewb_move'
8
+
9
+ module TwistyPuzzles
10
+
11
+ # Class that represents one notation for Skewb moves, e.g. Sarahs notation or fixed
12
+ # corner notation.
13
+ class SkewbNotation
14
+ def initialize(name, move_corner_pairs)
15
+ raise TypeError unless name.is_a?(String)
16
+
17
+ check_move_corner_pairs(move_corner_pairs)
18
+ @name = name
19
+ @move_to_corner = move_corner_pairs.to_h.freeze
20
+ @corner_to_move = move_corner_pairs.collect_concat do |m, c|
21
+ c.rotations.map { |e| [e, m] }
22
+ end.to_h.freeze
23
+ @move_strings = move_corner_pairs.map(&:first).freeze
24
+ @non_zero_moves =
25
+ move_corner_pairs.map(&:last).product(SkewbDirection::NON_ZERO_DIRECTIONS).map do |c, d|
26
+ SkewbMove.new(c, d)
27
+ end.freeze
28
+ freeze
29
+ end
30
+
31
+ def check_move_corner_pairs(move_corner_pairs)
32
+ move_corner_pairs.each do |m|
33
+ raise ArgumentError unless m.length == 2
34
+ raise TypeError unless m[0].is_a?(String)
35
+ raise TypeError unless m[1].is_a?(Corner)
36
+ end
37
+ if move_corner_pairs.map(&:first).uniq.length != move_corner_pairs.length
38
+ raise ArgumentError
39
+ end
40
+
41
+ check_corner_coverage(move_corner_pairs.map(&:last))
42
+ end
43
+
44
+ def check_corner_coverage(corners)
45
+ corner_closure = corners + corners.map(&:diagonal_opposite)
46
+ Corner::ELEMENTS.each do |corner|
47
+ unless corner_closure.any? { |c| c.turned_equals?(corner) }
48
+ raise ArgumentError,
49
+ "Turns around corner #{corner} cannot be represented in notation #{name}."
50
+ end
51
+ end
52
+ end
53
+
54
+ attr_reader :name, :move_strings, :non_zero_moves
55
+ private_class_method :new
56
+
57
+ def self.fixed_corner
58
+ @fixed_corner ||= new(
59
+ 'fixed corner', [
60
+ ['U', Corner.for_face_symbols(%i[U L B])],
61
+ ['R', Corner.for_face_symbols(%i[D R B])],
62
+ ['L', Corner.for_face_symbols(%i[D F L])],
63
+ ['B', Corner.for_face_symbols(%i[D B L])]
64
+ ]
65
+ )
66
+ end
67
+
68
+ def self.sarah
69
+ @sarah ||= new(
70
+ 'sarah', [
71
+ ['F', Corner.for_face_symbols(%i[U R F])],
72
+ ['R', Corner.for_face_symbols(%i[U B R])],
73
+ ['B', Corner.for_face_symbols(%i[U L B])],
74
+ ['L', Corner.for_face_symbols(%i[U F L])]
75
+ ]
76
+ )
77
+ end
78
+
79
+ def self.rubiks
80
+ @rubiks ||= new(
81
+ 'rubiks', [
82
+ ['F', Corner.for_face_symbols(%i[U R F])],
83
+ ['R', Corner.for_face_symbols(%i[U B R])],
84
+ ['B', Corner.for_face_symbols(%i[U L B])],
85
+ ['L', Corner.for_face_symbols(%i[U F L])],
86
+ ['f', Corner.for_face_symbols(%i[D F R])],
87
+ ['r', Corner.for_face_symbols(%i[D R B])],
88
+ ['b', Corner.for_face_symbols(%i[D B L])],
89
+ ['l', Corner.for_face_symbols(%i[D L F])]
90
+ ]
91
+ )
92
+ end
93
+
94
+ def to_s
95
+ @name
96
+ end
97
+
98
+ def corner(move)
99
+ @move_to_corner[move] || (raise ArgumentError)
100
+ end
101
+
102
+ def algorithm_to_string(algorithm)
103
+ reversed_rotations = []
104
+ num_tail_rotations = CancellationHelper.num_tail_rotations(algorithm)
105
+ alg_string = algorithm.moves[0...algorithm.length - num_tail_rotations].map do |m|
106
+ move_to_string(m, reversed_rotations)
107
+ end.join(' ')
108
+ new_tail_rotations = reversed_rotations.reverse! +
109
+ algorithm.moves[algorithm.length - num_tail_rotations..-1]
110
+ cancelled_rotations = Algorithm.new(new_tail_rotations).cancelled(3)
111
+ cancelled_rotations.empty? ? alg_string : "#{alg_string} #{cancelled_rotations}"
112
+ end
113
+
114
+ private
115
+
116
+ def move_to_string(move, reversed_rotations)
117
+ reversed_rotations.each { |r| move = move.rotate_by(r.inverse) }
118
+ case move
119
+ when SkewbMove then skewb_move_to_string(move, reversed_rotations)
120
+ when Rotation then move.to_s
121
+ else raise ArgumentError, "Couldn't transform #{move} to #{@name} Skewb notation."
122
+ end
123
+ end
124
+
125
+ def skewb_move_to_string(move, reversed_rotations)
126
+ move_string, rotate = move_to_string_internal(move)
127
+ if rotate
128
+ reversed_additional_rotations =
129
+ Rotation.around_corner(move.axis_corner, move.direction).moves.reverse
130
+ reversed_rotations.concat(reversed_additional_rotations)
131
+ end
132
+ "#{move_string}#{move.direction.name}"
133
+ end
134
+
135
+ # Returns the move string of the given move and true if a rotation has to be done to correct
136
+ # for the fact that we actually used the opposite corner.
137
+ def move_to_string_internal(move)
138
+ if (move_string = @corner_to_move[move.axis_corner])
139
+ [move_string, false]
140
+ elsif (move_string = @corner_to_move[move.axis_corner.diagonal_opposite])
141
+ [move_string, !move.direction.zero?]
142
+ else
143
+ raise
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/coordinate'
4
+ require 'twisty_puzzles/cube'
5
+ require 'twisty_puzzles/cube_print_helper'
6
+ require 'twisty_puzzles/state_helper'
7
+ require 'twisty_puzzles/cube_constants'
8
+
9
+ module TwistyPuzzles
10
+
11
+ # Represents the state (i.e. the sticker positions) of a Skewb.
12
+ class SkewbState
13
+ include CubePrintHelper
14
+ include StateHelper
15
+ include CubeConstants
16
+ # Pairs of coordinate pairs that should match in case of solved layers.
17
+ MATCHING_CORNERS =
18
+ begin
19
+ matching_corners = []
20
+ Corner::ELEMENTS.each do |c1|
21
+ Corner::ELEMENTS.each do |c2|
22
+ # Take corner pairs that have a common edge.
23
+ next unless c1.common_edge_with?(c2)
24
+
25
+ check_parts = []
26
+ c1.rotations.each do |c1_rot|
27
+ next unless c2.face_symbols.include?(c1_rot.face_symbols.first)
28
+
29
+ c2_rot = c2.rotate_face_symbol_up(c1_rot.face_symbols.first)
30
+ check_parts.push([
31
+ SkewbCoordinate.for_corner(c1_rot),
32
+ SkewbCoordinate.for_corner(c2_rot)
33
+ ])
34
+ end
35
+ matching_corners.push(check_parts)
36
+ end
37
+ end
38
+ matching_corners.uniq
39
+ end
40
+ # Pairs of stickers that can be used to check whether the "outside" of a layer on the given
41
+ # face is a proper layer.
42
+ LAYER_CHECK_NEIGHBORS =
43
+ begin
44
+ layer_check_neighbors = {}
45
+ MATCHING_CORNERS.each do |a, b|
46
+ [[a.first.face, b], [b.first.face, a]].each do |face, coordinates|
47
+ # We take the first one we encounter, but it doesn't matter, we could take any.
48
+ layer_check_neighbors[face] ||= coordinates
49
+ end
50
+ end
51
+ layer_check_neighbors
52
+ end
53
+
54
+ def initialize(native)
55
+ raise TypeError unless native.is_a?(Native::SkewbState)
56
+
57
+ @native = native
58
+ end
59
+
60
+ attr_reader :native
61
+
62
+ def self.for_solved_colors(solved_colors)
63
+ native = Native::SkewbState.new(solved_colors)
64
+ new(native)
65
+ end
66
+
67
+ def eql?(other)
68
+ self.class.equal?(other.class) && @native == other.native
69
+ end
70
+
71
+ alias == eql?
72
+
73
+ def hash
74
+ @hash ||= [self.class, @native].hash
75
+ end
76
+
77
+ # TODO: Get rid of this backwards compatibility artifact
78
+ def sticker_array(face)
79
+ raise TypeError unless face.is_a?(Face)
80
+
81
+ center_sticker = self[SkewbCoordinate.for_center(face)]
82
+ corner_stickers =
83
+ face.clockwise_corners.sort.map do |c|
84
+ self[SkewbCoordinate.for_corner(c)]
85
+ end
86
+ [center_sticker] + corner_stickers
87
+ end
88
+
89
+ def dup
90
+ SkewbState.new(@native.dup)
91
+ end
92
+
93
+ def to_s
94
+ skewb_string(self, :nocolor)
95
+ end
96
+
97
+ def colored_to_s
98
+ skewb_string(self, :color)
99
+ end
100
+
101
+ def apply_move(move)
102
+ move.apply_to(self)
103
+ end
104
+
105
+ def apply_algorithm(alg)
106
+ alg.apply_to(self)
107
+ end
108
+
109
+ def apply_rotation(rot)
110
+ rot.apply_to_skewb(self)
111
+ end
112
+
113
+ def [](coordinate)
114
+ @native[coordinate.native]
115
+ end
116
+
117
+ def []=(coordinate, color)
118
+ @native[coordinate.native] = color
119
+ sticker_array(coordinate.face)[coordinate.coordinate] = color
120
+ end
121
+
122
+ def any_layer_solved?
123
+ !solved_layers.empty?
124
+ end
125
+
126
+ # Returns the color of all solved layers. Empty if there is none.
127
+ def solved_layers
128
+ solved_faces = Face::ELEMENTS.select { |f| layer_at_face_solved?(f) }
129
+ solved_faces.map { |f| self[SkewbCoordinate.for_center(f)] }
130
+ end
131
+
132
+ def layer_solved?(color)
133
+ Face::ELEMENTS.any? do |f|
134
+ self[SkewbCoordinate.for_center(f)] == color && layer_at_face_solved?(f)
135
+ end
136
+ end
137
+
138
+ def center_face(color)
139
+ Face::ELEMENTS.find { |f| self[SkewbCoordinate.for_center(f)] == color }
140
+ end
141
+
142
+ def layer_check_neighbors(face)
143
+ LAYER_CHECK_NEIGHBORS[face]
144
+ end
145
+
146
+ # Note that this does NOT say that the layer corresponding to the given face is solved.
147
+ # The face argument is used as the position where a solved face is present.
148
+ def layer_at_face_solved?(face)
149
+ return false unless native.face_solved?(face.face_symbol)
150
+
151
+ layer_check_neighbors(face).map { |c| self[c] }.uniq.length == 1
152
+ end
153
+
154
+ def rotate_face(face, direction)
155
+ neighbors = face.neighbors
156
+ inverse_order_face = face.coordinate_index_close_to(neighbors[0]) <
157
+ face.coordinate_index_close_to(neighbors[1])
158
+ direction = direction.inverse if inverse_order_face
159
+ cycle = SkewbCoordinate.corners_on_face(face)
160
+ apply_4sticker_cycle(cycle, direction)
161
+ end
162
+ end
163
+ end