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,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