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,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/abstract_direction'
4
+ require 'twisty_puzzles/abstract_move_parser'
5
+ require 'twisty_puzzles/cube'
6
+ require 'twisty_puzzles/cube_constants'
7
+ require 'twisty_puzzles/cube_move'
8
+ require 'twisty_puzzles/move_type_creator'
9
+ require 'twisty_puzzles/rotation'
10
+ require 'twisty_puzzles/skewb_move'
11
+
12
+ module TwistyPuzzles
13
+
14
+ # Parser for cube moves.
15
+ class CubeMoveParser < AbstractMoveParser
16
+ REGEXP =
17
+ begin
18
+ axes_part = "(?<axis_name>[#{AbstractMove::AXES.join}])"
19
+ face_names = CubeConstants::FACE_NAMES.join
20
+ fat_move_part =
21
+ "(?<width>\\d*)(?<fat_face_name>[#{face_names}])w"
22
+ normal_move_part = "(?<face_name>[#{face_names}])"
23
+ downcased_face_names = face_names.downcase
24
+ maybe_fat_maybe_slice_move_part =
25
+ "(?<maybe_fat_face_maybe_slice_name>[#{downcased_face_names}])"
26
+ slice_move_part =
27
+ "(?<slice_index>\\d+)(?<slice_name>[#{downcased_face_names}])"
28
+ mslice_move_part =
29
+ "(?<mslice_name>[#{AbstractMove::SLICE_FACES.keys.join}])"
30
+ move_part = "(?:#{axes_part}|" \
31
+ "#{fat_move_part}|" \
32
+ "#{normal_move_part}|" \
33
+ "#{maybe_fat_maybe_slice_move_part}|" \
34
+ "#{slice_move_part}|#{mslice_move_part})"
35
+ direction_names =
36
+ AbstractDirection::POSSIBLE_DIRECTION_NAMES.flatten
37
+ direction_names.sort_by! { |e| -e.length }
38
+ direction_part = "(?<direction>#{direction_names.join('|')})"
39
+ Regexp.new("#{move_part}#{direction_part}")
40
+ end
41
+
42
+ MOVE_TYPE_CREATORS = [
43
+ MoveTypeCreator.new(%i[axis_face direction], Rotation),
44
+ MoveTypeCreator.new(%i[fat_face direction width], FatMove),
45
+ MoveTypeCreator.new(%i[face direction], FatMove),
46
+ MoveTypeCreator.new(%i[maybe_fat_face_maybe_slice_face direction], MaybeFatMaybeSliceMove),
47
+ MoveTypeCreator.new(%i[slice_face direction slice_index], SliceMove),
48
+ MoveTypeCreator.new(%i[mslice_face direction], MaybeFatMSliceMaybeInnerMSliceMove)
49
+ ].freeze
50
+
51
+ INSTANCE = CubeMoveParser.new
52
+ def regexp
53
+ REGEXP
54
+ end
55
+
56
+ def move_type_creators
57
+ MOVE_TYPE_CREATORS
58
+ end
59
+
60
+ def parse_part_key(name)
61
+ name.sub('_name', '_face').sub('face_face', 'face')
62
+ end
63
+
64
+ def parse_direction(direction_string)
65
+ value = AbstractDirection::POSSIBLE_DIRECTION_NAMES.index do |ds|
66
+ ds.include?(direction_string)
67
+ end + 1
68
+ CubeDirection.new(value)
69
+ end
70
+
71
+ def parse_axis_face(axis_face_string)
72
+ Face::ELEMENTS[AbstractMove::AXES.index(axis_face_string)]
73
+ end
74
+
75
+ def parse_mslice_face(mslice_name)
76
+ AbstractMove::SLICE_FACES[mslice_name]
77
+ end
78
+
79
+ def parse_width(width_string)
80
+ width_string.empty? ? 2 : Integer(width_string, 10)
81
+ end
82
+
83
+ # rubocop:disable Metrics/CyclomaticComplexity
84
+ def parse_move_part(name, value)
85
+ case name
86
+ when 'axis_name' then parse_axis_face(value)
87
+ when 'width' then parse_width(value)
88
+ when 'slice_index' then Integer(value, 10)
89
+ when 'fat_face_name', 'face_name' then Face.by_name(value)
90
+ when 'maybe_fat_face_maybe_slice_name', 'slice_name'
91
+ Face.by_name(value.upcase)
92
+ when 'mslice_name'
93
+ parse_mslice_face(value)
94
+ when 'direction' then parse_direction(value)
95
+ else raise
96
+ end
97
+ end
98
+ # rubocop:enable Metrics/CyclomaticComplexity
99
+ end
100
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+ require 'twisty_puzzles/utils/array_helper'
5
+
6
+ module TwistyPuzzles
7
+
8
+ # Module to print and display cube and Skewb states.
9
+ module CubePrintHelper
10
+ include Utils::ArrayHelper
11
+
12
+ def color_symbol(color)
13
+ case color
14
+ when :orange then :light_red
15
+ when :unknown then :light_black
16
+ else color
17
+ end
18
+ end
19
+
20
+ COLOR_MODES = %i[color nocolor].freeze
21
+ ColorInfo = Struct.new(:reverse_lines_mode, :reverse_columns_mode, :skewb_corner_permutation)
22
+ FACE_SYMBOL_INFOS = {
23
+ U: ColorInfo.new(:reverse, :reverse, [2, 3, 0, 1]),
24
+ L: ColorInfo.new(:keep, :reverse, [2, 0, 3, 1]),
25
+ F: ColorInfo.new(:keep, :reverse, [2, 0, 3, 1]),
26
+ R: ColorInfo.new(:keep, :keep, [1, 0, 3, 2]),
27
+ B: ColorInfo.new(:keep, :keep, [1, 0, 3, 2]),
28
+ D: ColorInfo.new(:keep, :reverse, [2, 0, 3, 1])
29
+ }.freeze
30
+
31
+ def color_character(color, color_mode)
32
+ unless COLOR_MODES.include?(color_mode)
33
+ raise ArgumentError, "Invalid color mode #{color_mode}"
34
+ end
35
+
36
+ char = color.to_s[0].upcase
37
+ if color_mode == :color
38
+ char.colorize(background: color_symbol(color))
39
+ else
40
+ char
41
+ end
42
+ end
43
+
44
+ def maybe_reverse(reverse_mode, stuff)
45
+ if reverse_mode == :reverse
46
+ stuff.reverse
47
+ elsif reverse_mode == :keep
48
+ stuff
49
+ else
50
+ raise
51
+ end
52
+ end
53
+
54
+ def face_lines(cube_state, face_symbol, row_multiplicity = 1, column_multiplicity = 1)
55
+ face = Face.for_face_symbol(face_symbol)
56
+ face_symbol_info = FACE_SYMBOL_INFOS[face_symbol]
57
+ stickers = cube_state.sticker_array(face)
58
+ lines =
59
+ stickers.collect_concat do |sticker_line|
60
+ line = sticker_line.map { |c| yield(c) * column_multiplicity }
61
+ [maybe_reverse(face_symbol_info.reverse_columns_mode, line).join] * row_multiplicity
62
+ end
63
+ maybe_reverse(face_symbol_info.reverse_lines_mode, lines)
64
+ end
65
+
66
+ def simple_face_lines(cube_state, face_symbol, color_mode)
67
+ face_lines(cube_state, face_symbol) { |c| color_character(c, color_mode) }
68
+ end
69
+
70
+ SKEWB_FACE_SIZE = 5
71
+
72
+ def skewb_ascii_art_line(first_color, middle_color, last_color, num_first_color)
73
+ raise if num_first_color > SKEWB_FACE_SIZE / 2
74
+
75
+ first_color * num_first_color +
76
+ middle_color * (SKEWB_FACE_SIZE - 2 * num_first_color) +
77
+ last_color * num_first_color
78
+ end
79
+
80
+ def skewb_ascii_art(center_color, corner_colors)
81
+ raise unless corner_colors.length == 4
82
+
83
+ first_part =
84
+ (1..SKEWB_FACE_SIZE / 2).to_a.reverse.map do |i|
85
+ skewb_ascii_art_line(corner_colors[0], center_color, corner_colors[1], i)
86
+ end
87
+ middle_part = SKEWB_FACE_SIZE.odd? ? [center_color * SKEWB_FACE_SIZE] : []
88
+ last_part =
89
+ (1..SKEWB_FACE_SIZE / 2).map do |i|
90
+ skewb_ascii_art_line(corner_colors[2], center_color, corner_colors[3], i)
91
+ end
92
+ first_part + middle_part + last_part
93
+ end
94
+
95
+ # Prints a Skewb face like this:
96
+ # rrgww
97
+ # rgggw
98
+ # ggggg
99
+ # ogggb
100
+ # oogbb
101
+ def skewb_face_lines(cube_state, face_symbol, color_mode)
102
+ face = Face.for_face_symbol(face_symbol)
103
+ face_symbol_info = FACE_SYMBOL_INFOS[face_symbol]
104
+ stickers = cube_state.sticker_array(face)
105
+ center_color = color_character(stickers[0], color_mode)
106
+ corner_colors = stickers[1..-1].map { |c| color_character(c, color_mode) }
107
+ permuted_corner_colors =
108
+ apply_permutation(corner_colors, face_symbol_info.skewb_corner_permutation)
109
+ raise unless corner_colors.length == 4
110
+
111
+ skewb_ascii_art(center_color, permuted_corner_colors)
112
+ end
113
+
114
+ def ll_string(cube_state, color_mode)
115
+ top_face = face_lines(cube_state, :U, 2, 3) { |c| color_character(c, color_mode) }
116
+ front_face = face_lines(cube_state, :F, 1, 3) { |c| color_character(c, color_mode) }
117
+ right_face = face_lines(cube_state, :R, 1, 3) { |c| color_character(c, color_mode) }
118
+ pll_line = front_face.first + right_face.first
119
+ (top_face + [pll_line] * 3).join("\n")
120
+ end
121
+
122
+ def cube_string(cube_state, color_mode)
123
+ top_face = simple_face_lines(cube_state, :U, color_mode)
124
+ left_face = simple_face_lines(cube_state, :L, color_mode)
125
+ front_face = simple_face_lines(cube_state, :F, color_mode)
126
+ right_face = simple_face_lines(cube_state, :R, color_mode)
127
+ back_face = simple_face_lines(cube_state, :B, color_mode)
128
+ bottom_face = simple_face_lines(cube_state, :D, color_mode)
129
+ middle_belt = zip_concat_lines(left_face, front_face, right_face, back_face)
130
+ lines = pad_lines(top_face, cube_state.n) + middle_belt +
131
+ pad_lines(bottom_face, cube_state.n)
132
+ lines.join("\n")
133
+ end
134
+
135
+ def skewb_string(skewb_state, color_mode)
136
+ top_face = skewb_face_lines(skewb_state, :U, color_mode)
137
+ left_face = skewb_face_lines(skewb_state, :L, color_mode)
138
+ front_face = skewb_face_lines(skewb_state, :F, color_mode)
139
+ right_face = skewb_face_lines(skewb_state, :R, color_mode)
140
+ back_face = skewb_face_lines(skewb_state, :B, color_mode)
141
+ bottom_face = skewb_face_lines(skewb_state, :D, color_mode)
142
+ middle_belt = zip_concat_lines(left_face, front_face, right_face, back_face)
143
+ lines = pad_lines(top_face, SKEWB_FACE_SIZE) + middle_belt +
144
+ pad_lines(bottom_face, SKEWB_FACE_SIZE)
145
+ lines.join("\n")
146
+ end
147
+
148
+ def empty_name
149
+ ' '
150
+ end
151
+
152
+ def pad_lines(lines, padding)
153
+ lines.map { |line| empty_name * padding + line }
154
+ end
155
+
156
+ def zip_concat_lines(*args)
157
+ args.transpose.map(&:join)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/cube'
4
+ require 'twisty_puzzles/cube_constants'
5
+ require 'twisty_puzzles/coordinate'
6
+ require 'twisty_puzzles/cube_print_helper'
7
+ require 'twisty_puzzles/state_helper'
8
+ require 'twisty_puzzles/utils/array_helper'
9
+ require 'twisty_puzzles/native'
10
+
11
+ module TwistyPuzzles
12
+
13
+ # Represents the state (i.e. the sticker positions) of a cube.
14
+ class CubeState
15
+ include Utils::ArrayHelper
16
+ include CubePrintHelper
17
+ include StateHelper
18
+ include CubeConstants
19
+
20
+ def self.check_cube_size(cube_size)
21
+ raise TypeError unless cube_size.is_a?(Integer)
22
+ raise ArgumentError, 'Cubes of size smaller than 2 are not supported.' if cube_size < 2
23
+ end
24
+
25
+ def self.from_stickers(cube_size, stickers)
26
+ CubeState.check_cube_size(cube_size)
27
+ unless stickers.length == FACE_SYMBOLS.length
28
+ raise ArgumentError, "Cubes must have #{FACE_SYMBOLS.length} sides."
29
+ end
30
+
31
+ unless stickers.all? { |p| p.length == cube_size && p.all? { |q| q.length == cube_size } }
32
+ raise ArgumentError,
33
+ "All sides of a #{cube_size}x#{cube_size} must be #{cube_size}x#{cube_size}."
34
+ end
35
+
36
+ stickers_hash = create_stickers_hash(stickers)
37
+ new(Native::CubeState.new(cube_size, stickers_hash))
38
+ end
39
+
40
+ def self.create_stickers_hash(stickers)
41
+ FACE_SYMBOLS.zip(stickers).map do |face_symbol, face_stickers|
42
+ face = Face.for_face_symbol(face_symbol)
43
+ face_hash = {
44
+ stickers: face_stickers,
45
+ # Note that in the ruby code, x and y are exchanged s.t. one can say bla[x][y],
46
+ # but the C code does the more logical thing,
47
+ # so we have to swap coordinates here.
48
+ x_base_face_symbol: face.coordinate_index_base_face(1).face_symbol,
49
+ y_base_face_symbol: face.coordinate_index_base_face(0).face_symbol
50
+ }
51
+ [face_symbol, face_hash]
52
+ end.to_h
53
+ end
54
+
55
+ def initialize(native)
56
+ raise TypeError unless native.is_a?(Native::CubeState)
57
+
58
+ @native = native
59
+ end
60
+
61
+ def dup
62
+ CubeState.new(@native.dup)
63
+ end
64
+
65
+ attr_reader :native
66
+
67
+ def n
68
+ @native.cube_size
69
+ end
70
+
71
+ def stickers; end
72
+
73
+ def eql?(other)
74
+ self.class.equal?(other.class) && @native == other.native
75
+ end
76
+
77
+ alias == eql?
78
+
79
+ def hash
80
+ @hash ||= [self.class, @native].hash
81
+ end
82
+
83
+ # TODO: Get rid of this backwards compatibility artifact
84
+ def sticker_array(face)
85
+ raise TypeError unless face.is_a?(Face)
86
+
87
+ @native.sticker_array(
88
+ face.face_symbol,
89
+ # Note that in the ruby code, x and y are exchanged s.t. one can say bla[x][y],
90
+ # but the C code does the more logical thing,
91
+ # so we have to swap coordinates here.
92
+ face.coordinate_index_base_face(1).face_symbol,
93
+ face.coordinate_index_base_face(0).face_symbol
94
+ )
95
+ end
96
+
97
+ def to_s
98
+ cube_string(self, :nocolor)
99
+ end
100
+
101
+ def colored_to_s
102
+ cube_string(self, :color)
103
+ end
104
+
105
+ def [](coordinate)
106
+ @native[coordinate.native]
107
+ end
108
+
109
+ def []=(coordinate, color)
110
+ @native[coordinate.native] = color
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twisty_puzzles/cube'
4
+
5
+ module TwistyPuzzles
6
+ # Letter scheme that maps stickers to letters.
7
+ class LetterScheme
8
+ def initialize
9
+ alphabet.each do |letter|
10
+ raise "Uncanonical letter #{letter} in alphabet." if letter != canonicalize_letter(letter)
11
+ end
12
+ end
13
+
14
+ def letter(piece)
15
+ alphabet[piece.piece_index]
16
+ end
17
+
18
+ def for_letter(part_type, desired_letter)
19
+ canonicalized_letter = canonicalize_letter(desired_letter)
20
+ part_type::ELEMENTS.find { |e| letter(e) == canonicalized_letter }
21
+ end
22
+
23
+ def valid_letter?(letter)
24
+ alphabet.include?(canonicalize_letter(letter))
25
+ end
26
+
27
+ def alphabet
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def canonicalize_letter(_letter)
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def parse_part(part_type, part_string)
36
+ if valid_letter?(part_string)
37
+ for_letter(part_type, part_string)
38
+ else
39
+ part_type.parse(part_string)
40
+ end
41
+ end
42
+
43
+ alias parse_buffer parse_part
44
+ end
45
+
46
+ # Letter scheme used by Bernhard Brodowsky.
47
+ class BernhardLetterScheme < LetterScheme
48
+ PART_TYPE_BUFFERS = {
49
+ Corner => Corner.for_face_symbols(%i[U L B]),
50
+ Edge => Edge.for_face_symbols(%i[U F]),
51
+ Wing => Wing.for_face_symbols(%i[F U]),
52
+ XCenter => XCenter.for_face_symbols(%i[U R F]),
53
+ TCenter => TCenter.for_face_symbols(%i[U F])
54
+ }.freeze
55
+ def alphabet
56
+ @alphabet ||= 'a'.upto('x').to_a
57
+ end
58
+
59
+ def canonicalize_letter(letter)
60
+ letter.downcase
61
+ end
62
+
63
+ # Letters that we shoot to by default.
64
+ def shoot_letters(_part_type)
65
+ %w[a b d l h t p]
66
+ end
67
+
68
+ def default_buffer(part_type)
69
+ PART_TYPE_BUFFERS[part_type]
70
+ end
71
+ end
72
+ end