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