gemtris 1.0.0
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.
- checksums.yaml +7 -0
- data/bin/gemtris +5 -0
- data/lib/gemtris/cell.rb +53 -0
- data/lib/gemtris/grid.rb +174 -0
- data/lib/gemtris/player.rb +64 -0
- data/lib/gemtris/shapes.rb +174 -0
- data/lib/gemtris/utils.rb +45 -0
- data/lib/gemtris.rb +310 -0
- data/yml/gemtris.yml +61 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3ddbaa519dfd59a7069c33f8bf7f41ddf8f0e55e0f299bbc69ce14c359d0cd37
|
|
4
|
+
data.tar.gz: ffcb28edc3c99de861aa2d84429bbd35308a0dc34b76be4cd598f2cf040c477e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0e9a7db66141fab4057e0e17df027ef21a4ed54d65e79d8aaf813a35ab87f7c6bdee3c6c93add1158a44c576fbfacdeec96bd19300dc829d16873f59874b99b8
|
|
7
|
+
data.tar.gz: 1ce2aa2f38e817cbfb2ea557347e904975a938949a98a0cc5d1c2c99cfea28b094b5458f0b3c203a0182eab6d8c869c9d93f123f3129ef741e0d66428d65cc74
|
data/bin/gemtris
ADDED
data/lib/gemtris/cell.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# Encapsulates the logic of a single square on the coordinate grid.
|
|
4
|
+
class Cell
|
|
5
|
+
CELL_VISUAL_SIZE = 3
|
|
6
|
+
EMPTY_CELL_VISUAL = '-'.freeze
|
|
7
|
+
FILLED_CELL_VISUAL = '[ ]'.freeze
|
|
8
|
+
CENTER_CELL_VISUAL = '[+]'.freeze
|
|
9
|
+
SHADOW_CELL_VISUAL = '( )'.freeze
|
|
10
|
+
SOLID_CELL_VISUAL = '[=]'.freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :solid
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
clear
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_s
|
|
19
|
+
visual.center CELL_VISUAL_SIZE
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def clear
|
|
23
|
+
self.solid = false
|
|
24
|
+
self.visual = EMPTY_CELL_VISUAL
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def shape_fill
|
|
28
|
+
self.visual = FILLED_CELL_VISUAL
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shape_center_fill
|
|
32
|
+
self.visual = CENTER_CELL_VISUAL
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def shade
|
|
36
|
+
self.visual = SHADOW_CELL_VISUAL
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def solidify
|
|
40
|
+
self.solid = true
|
|
41
|
+
self.visual = SOLID_CELL_VISUAL
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def solid?
|
|
45
|
+
@solid
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_writer :solid
|
|
51
|
+
|
|
52
|
+
attr_accessor :visual
|
|
53
|
+
end
|
data/lib/gemtris/grid.rb
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
require_relative 'cell'
|
|
4
|
+
|
|
5
|
+
# Simulates the coordinate grid.
|
|
6
|
+
class Grid
|
|
7
|
+
GRID_BORDER_VERT = '||'.freeze
|
|
8
|
+
GRID_BORDER_HORIZ = '='.freeze
|
|
9
|
+
GRID_DEFAULT_WIDTH = 10
|
|
10
|
+
GRID_DEFAULT_HEIGHT = 20
|
|
11
|
+
|
|
12
|
+
attr_reader :height, :width, :board_visual_width
|
|
13
|
+
|
|
14
|
+
def initialize(width = GRID_DEFAULT_WIDTH, height = GRID_DEFAULT_HEIGHT)
|
|
15
|
+
@board_visual_width = (width * Cell::CELL_VISUAL_SIZE) +
|
|
16
|
+
(GRID_BORDER_VERT.size * 2)
|
|
17
|
+
@width = width
|
|
18
|
+
@height = height
|
|
19
|
+
@coordinate = init_coordinate
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
vertical_visual = grid_vertical_visual
|
|
24
|
+
"#{vertical_visual}\n#{grid_playable_visual}\n#{vertical_visual}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def draw_shape(shape)
|
|
28
|
+
draw_shadow(shape)
|
|
29
|
+
|
|
30
|
+
shape.filled_coords.each do |coord|
|
|
31
|
+
cell = find_cell(coord)
|
|
32
|
+
cell&.shape_fill
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
draw_shape_center(shape)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def draw_shadow(shape)
|
|
39
|
+
projected_landing_coords(shape).each do |shadow_coord|
|
|
40
|
+
cell = find_cell(shadow_coord)
|
|
41
|
+
cell&.shade
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def draw_shape_center(shape)
|
|
46
|
+
center_coordinates = shape.absolute_center_coord
|
|
47
|
+
return unless center_coordinates
|
|
48
|
+
|
|
49
|
+
cell = find_cell(center_coordinates)
|
|
50
|
+
cell&.shape_center_fill
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def undraw_shape(shape)
|
|
54
|
+
clear_coords = shape.filled_coords | projected_landing_coords(shape)
|
|
55
|
+
clear_coords.each do |coord|
|
|
56
|
+
cell = find_cell(coord)
|
|
57
|
+
cell&.clear
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def solidify_shape(shape)
|
|
62
|
+
shape.filled_coords.each do |coord|
|
|
63
|
+
cell = find_cell(coord)
|
|
64
|
+
cell&.solidify
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def row_full?(row_y_value)
|
|
69
|
+
row = coordinate[row_y_value]
|
|
70
|
+
row.values.all?(&:solid?)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def clear_row!(row_y_value)
|
|
74
|
+
row = coordinate[row_y_value]
|
|
75
|
+
row.each_value(&:clear)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def deep_clear_row!(row_y_value)
|
|
79
|
+
coordinate[row_y_value] = init_row_columns
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def move_row_from_to(row_number, move_to)
|
|
83
|
+
coordinate[move_to] = coordinate[row_number]
|
|
84
|
+
coordinate[row_number] = init_row_columns
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def shape_move_x?(shape, direction_sign = 1)
|
|
88
|
+
shape.filled_coords.none? do |adjacent_coord|
|
|
89
|
+
adjacent_coord[:x] += 1 * direction_sign
|
|
90
|
+
coord_out_bounds?(adjacent_coord) || !!find_cell(adjacent_coord)&.solid?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def shape_invalid_position?(shape)
|
|
95
|
+
shape_out_bounds?(shape) || shape_touching_solid?(shape)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def shape_touching_solid?(shape)
|
|
99
|
+
shape.filled_coords.any? { |coord| coord_touching_solid?(coord) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def shape_out_bounds?(shape)
|
|
103
|
+
shape.filled_coords.any? { |coord| coord_out_bounds?(coord) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def coord_touching_solid?(coord)
|
|
107
|
+
return true if coord_touching_bottom?(coord)
|
|
108
|
+
return true if find_cell(coord)&.solid?
|
|
109
|
+
|
|
110
|
+
coord_under = coord.dup
|
|
111
|
+
coord_under[:y] += 1
|
|
112
|
+
|
|
113
|
+
cell = find_cell(coord_under)
|
|
114
|
+
!!cell&.solid?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def coord_touching_bottom?(coord)
|
|
118
|
+
coord[:y] == height
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def coord_out_bounds?(coord)
|
|
122
|
+
coord[:y] <= 0 || coord[:y] > height ||
|
|
123
|
+
coord[:x] <= 0 || coord[:x] > width
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def [](row_y_value)
|
|
127
|
+
coordinate[row_y_value]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def []=(row_y_value, new_row)
|
|
131
|
+
coordinate[row_y_value] = new_row
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def projected_landing_coords(shape)
|
|
137
|
+
original_position = shape.position.dup
|
|
138
|
+
shape.position[:y] += 1 until shape_invalid_position?(shape)
|
|
139
|
+
|
|
140
|
+
landing_coords = shape.filled_coords
|
|
141
|
+
shape.position = original_position
|
|
142
|
+
landing_coords
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def find_cell(coord)
|
|
146
|
+
return nil if coord_out_bounds?(coord)
|
|
147
|
+
|
|
148
|
+
coordinate[coord[:y]][coord[:x]]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def grid_vertical_visual
|
|
152
|
+
GRID_BORDER_HORIZ * board_visual_width
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def grid_playable_visual
|
|
156
|
+
coordinate.values.map do |row|
|
|
157
|
+
"#{GRID_BORDER_VERT}#{row.values.join}#{GRID_BORDER_VERT}"
|
|
158
|
+
end.join "\n"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def init_coordinate
|
|
162
|
+
coordinate = {}
|
|
163
|
+
(1..height).each do |y|
|
|
164
|
+
coordinate[y] = init_row_columns
|
|
165
|
+
end
|
|
166
|
+
coordinate
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def init_row_columns
|
|
170
|
+
(1..width).to_h { |x_pos| [x_pos, Cell.new] }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
attr_reader :coordinate
|
|
174
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
require_relative 'utils'
|
|
4
|
+
|
|
5
|
+
# Used to encapsulate player logic.
|
|
6
|
+
class Player
|
|
7
|
+
INVALID_ACTION = 'That is not a valid action!'.freeze
|
|
8
|
+
|
|
9
|
+
FALL_ACTION = :fall
|
|
10
|
+
MOVE_LEFT_ACTION = :move_left
|
|
11
|
+
MOVE_RIGHT_ACTION = :move_right
|
|
12
|
+
ROTATE_CLOCKWISE_ACTION = :rotate_clockwise
|
|
13
|
+
ROTATE_COUNTER_ACTION = :rotate_counter_clockwise
|
|
14
|
+
FREEFALL_ACTION = :freefall
|
|
15
|
+
|
|
16
|
+
PLAY_AGAIN_CHOICE = 'yes'.freeze
|
|
17
|
+
|
|
18
|
+
INPUTS_FOR_ACTIONS = {
|
|
19
|
+
['<', ','] => MOVE_LEFT_ACTION,
|
|
20
|
+
['>', '.'] => MOVE_RIGHT_ACTION,
|
|
21
|
+
['d'] => ROTATE_COUNTER_ACTION,
|
|
22
|
+
['a'] => ROTATE_CLOCKWISE_ACTION,
|
|
23
|
+
['s'] => FREEFALL_ACTION
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
ACTION_PROMPT = format(
|
|
27
|
+
GemtrisText[:tetromino_control_prompt],
|
|
28
|
+
*INPUTS_FOR_ACTIONS.keys.flatten
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def prompt_tetromino_action
|
|
32
|
+
puts ACTION_PROMPT
|
|
33
|
+
CLI.divide_screen
|
|
34
|
+
|
|
35
|
+
loop do
|
|
36
|
+
action = find_action(CLI.user_input)
|
|
37
|
+
return action if action
|
|
38
|
+
|
|
39
|
+
CLI.error(INVALID_ACTION)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def play_again?
|
|
44
|
+
answer = CLI.prompt GemtrisText[:play_again]
|
|
45
|
+
answered_yes?(answer)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def answered_yes?(answer)
|
|
51
|
+
PLAY_AGAIN_CHOICE.start_with?(answer) ||
|
|
52
|
+
answer.start_with?(PLAY_AGAIN_CHOICE)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def find_action(player_input)
|
|
56
|
+
return FALL_ACTION if player_input.empty?
|
|
57
|
+
|
|
58
|
+
INPUTS_FOR_ACTIONS.each_key do |input_list|
|
|
59
|
+
return INPUTS_FOR_ACTIONS[input_list] if input_list.include? player_input
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# I don't remember if grid is needed.
|
|
4
|
+
# This file might only need cell.rb
|
|
5
|
+
require_relative 'grid'
|
|
6
|
+
|
|
7
|
+
# An interface for general shape-related functionality.
|
|
8
|
+
module GemtrisShapes
|
|
9
|
+
ROTATE_CENTER = :rotate_center
|
|
10
|
+
LIMB_DATA = :limb_data
|
|
11
|
+
|
|
12
|
+
FILLED_CELL_PATTERN = 'N'.freeze
|
|
13
|
+
EMPTY_CELL_PATTERN = ' '.freeze
|
|
14
|
+
|
|
15
|
+
# Golomb names taken from
|
|
16
|
+
# "https://quuxplusone.github.io/blog/2025/04/19/tetromino-names/"
|
|
17
|
+
#
|
|
18
|
+
# :rotate_center is the coordinates of the
|
|
19
|
+
# limb that the shape rotates around. if this is not provided
|
|
20
|
+
# it is assumed that the shape cannot rotate
|
|
21
|
+
|
|
22
|
+
SHAPES =
|
|
23
|
+
[
|
|
24
|
+
# Square tetromino
|
|
25
|
+
{ LIMB_DATA => "NN\nNN" },
|
|
26
|
+
|
|
27
|
+
# T-tetromino
|
|
28
|
+
{ LIMB_DATA => " N\nNNN", ROTATE_CENTER => { y: 1, x: 1 } },
|
|
29
|
+
|
|
30
|
+
# Straight tetromino
|
|
31
|
+
{ LIMB_DATA => 'NNNN', ROTATE_CENTER => { y: 0, x: 0 } },
|
|
32
|
+
|
|
33
|
+
# Skrew tetromino green
|
|
34
|
+
{ LIMB_DATA => " NN\nNN", ROTATE_CENTER => { y: 1, x: 1 } },
|
|
35
|
+
|
|
36
|
+
# Skrew tetromino red
|
|
37
|
+
{ LIMB_DATA => "NN\n NN", ROTATE_CENTER => { y: 1, x: 1 } },
|
|
38
|
+
|
|
39
|
+
# L-tetromino blue
|
|
40
|
+
{ LIMB_DATA => "N\nNNN", ROTATE_CENTER => { y: 1, x: 0 } },
|
|
41
|
+
|
|
42
|
+
# L-tetromino orange
|
|
43
|
+
{ LIMB_DATA => " N\nNNN", ROTATE_CENTER => { y: 1, x: 2 } }
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
def self.all_shapes
|
|
47
|
+
SHAPES.map { |shape_data| Shape.new(shape_data) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.random_shape
|
|
51
|
+
Shape.new(SHAPES.sample)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# This is used to simulate physical shapes in the game in conjunction with the Grid class.
|
|
56
|
+
class Shape
|
|
57
|
+
FILLED_CELL_PATTERN = GemtrisShapes::FILLED_CELL_PATTERN
|
|
58
|
+
EMPTY_CELL_PATTERN = GemtrisShapes::EMPTY_CELL_PATTERN
|
|
59
|
+
EMPTY_SPACE_VISUAL = ' ' * Cell::FILLED_CELL_VISUAL.size
|
|
60
|
+
|
|
61
|
+
CLOCKWISE_SIGN = 1
|
|
62
|
+
COUNTER_CLOCKWISE_SIGN = -1
|
|
63
|
+
|
|
64
|
+
VISUAL_REGEX = /#{FILLED_CELL_PATTERN}|#{EMPTY_CELL_PATTERN}/
|
|
65
|
+
VISUAL_HASH = {
|
|
66
|
+
FILLED_CELL_PATTERN => Cell::FILLED_CELL_VISUAL,
|
|
67
|
+
EMPTY_CELL_PATTERN => EMPTY_SPACE_VISUAL
|
|
68
|
+
}.freeze
|
|
69
|
+
|
|
70
|
+
DEFAULT_POSITION = { y: 1, x: 1 }.freeze
|
|
71
|
+
|
|
72
|
+
attr_reader :visual, :limbs, :center_limb
|
|
73
|
+
attr_accessor :position
|
|
74
|
+
|
|
75
|
+
def initialize(shape_data)
|
|
76
|
+
limb_data = shape_data[GemtrisShapes::LIMB_DATA]
|
|
77
|
+
|
|
78
|
+
@visual = create_visual(limb_data)
|
|
79
|
+
@limbs = translate_string_to_limbs(limb_data)
|
|
80
|
+
@position = DEFAULT_POSITION.dup
|
|
81
|
+
@center_limb = find_center_limb shape_data[GemtrisShapes::ROTATE_CENTER]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def filled_coords
|
|
85
|
+
limbs.map { |limb| limb_absolute_coord(limb) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def filled_x_coords
|
|
89
|
+
filled_coords.each_with_object(Set.new) { |coord, set| set << coord[:x] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def rotate(rotation_direction = CLOCKWISE_SIGN)
|
|
93
|
+
return unless can_rotate?
|
|
94
|
+
|
|
95
|
+
offset_position_by_rotation(rotation_direction)
|
|
96
|
+
limbs.each { |limb| rotate_limb!(limb, rotation_direction) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def absolute_center_coord
|
|
100
|
+
return nil unless can_rotate?
|
|
101
|
+
|
|
102
|
+
limb_absolute_coord(center_limb)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
alias to_s visual
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def can_rotate?
|
|
110
|
+
!!center_limb
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def offset_position_by_rotation(rotation_direction = CLOCKWISE_SIGN)
|
|
114
|
+
rotated_center = limb_rotated(center_limb, rotation_direction)
|
|
115
|
+
|
|
116
|
+
position[:x] -= rotated_center[:x] - center_limb[:x]
|
|
117
|
+
position[:y] -= rotated_center[:y] - center_limb[:y]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def rotate_limb!(limb, rotation_direction = CLOCKWISE_SIGN)
|
|
121
|
+
rotated_coord = limb_rotated(limb, rotation_direction)
|
|
122
|
+
limb.merge!(rotated_coord)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def limb_rotated(limb, rotation_direction = CLOCKWISE_SIGN)
|
|
126
|
+
{ y: limb[:x] * -rotation_direction, x: limb[:y] * rotation_direction }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def limb_absolute_coord(limb)
|
|
130
|
+
{
|
|
131
|
+
y: position[:y] + limb[:y],
|
|
132
|
+
x: position[:x] + limb[:x]
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def create_visual(shape_string)
|
|
137
|
+
shape_string.gsub(VISUAL_REGEX, VISUAL_HASH)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def find_center_limb(rotation_limb)
|
|
141
|
+
limbs.find { |limb| limb == rotation_limb }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def translate_string_to_limbs(shape_string)
|
|
145
|
+
data = []
|
|
146
|
+
shape_string.split(/$/).each_with_index do |row, y_coord|
|
|
147
|
+
clean_row = row.delete "\n"
|
|
148
|
+
data.concat row_shape_x_coords(y_coord, clean_row)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
data
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# rubocop:disable Metrics/MethodLength
|
|
155
|
+
def row_shape_x_coords(y_coord, row_shape_string)
|
|
156
|
+
current_coord = 0
|
|
157
|
+
last_matched_index = 0
|
|
158
|
+
row_coords = []
|
|
159
|
+
(0...row_shape_string.size).each do |substr_index|
|
|
160
|
+
pattern = row_shape_string[last_matched_index..substr_index]
|
|
161
|
+
next unless [EMPTY_CELL_PATTERN, FILLED_CELL_PATTERN].include? pattern
|
|
162
|
+
|
|
163
|
+
row_coords << { y: y_coord, x: current_coord } if cell_pattern?(pattern)
|
|
164
|
+
current_coord += 1
|
|
165
|
+
last_matched_index = substr_index.next
|
|
166
|
+
end
|
|
167
|
+
row_coords
|
|
168
|
+
end
|
|
169
|
+
# rubocop:enable Metrics/MethodLength
|
|
170
|
+
|
|
171
|
+
def cell_pattern?(pattern)
|
|
172
|
+
pattern == FILLED_CELL_PATTERN
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
require 'psych'
|
|
4
|
+
|
|
5
|
+
# Comand-Line Utilities
|
|
6
|
+
module CLI
|
|
7
|
+
DEFAULT_PROMPT = 'Please enter text:'.freeze
|
|
8
|
+
DEFAULT_ERROR = 'Unknown error!'.freeze
|
|
9
|
+
SCREEN_DIVIDER = "\n------------------------\n\n".freeze
|
|
10
|
+
|
|
11
|
+
def self.divide_screen
|
|
12
|
+
print SCREEN_DIVIDER
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.clear_screen
|
|
16
|
+
system('clear') || system('cls') || divide_screen
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.user_input
|
|
20
|
+
print '> '
|
|
21
|
+
gets.chomp.strip.downcase
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.prompt(message = DEFAULT_PROMPT)
|
|
25
|
+
puts message
|
|
26
|
+
divide_screen
|
|
27
|
+
user_input
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.error(message = DEFAULT_ERROR)
|
|
31
|
+
divide_screen
|
|
32
|
+
puts "[ERROR]: #{message}"
|
|
33
|
+
divide_screen
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Provides an interface for developers to access the related YAML texts.
|
|
38
|
+
module GemtrisText
|
|
39
|
+
# assuming yml/gemtris and we're in lib/gemtris/
|
|
40
|
+
TEXTS = Psych.load_file("#{__dir__}/../../yml/gemtris.yml") # Psych.load(IO.read("#{__dir__}/../../yml/gemtris.yml"))
|
|
41
|
+
|
|
42
|
+
def self.[](key)
|
|
43
|
+
TEXTS[key.to_s]
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/gemtris.rb
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# Load all dependencies.
|
|
4
|
+
Dir["#{__dir__}/gemtris/*.rb"].each { |path| require path }
|
|
5
|
+
|
|
6
|
+
# The orchestator of gameplay.
|
|
7
|
+
class GemtrisGame
|
|
8
|
+
TITLE_PLATE = '] GEMTRIS ['.freeze
|
|
9
|
+
TITLE_DECOR = '-'.freeze
|
|
10
|
+
|
|
11
|
+
TUTORIAL_ANSWER = 'tutorial'.freeze
|
|
12
|
+
START_ANSWER = 'start'.freeze
|
|
13
|
+
INVALID_CHOICE = 'Invalid answer!'.freeze
|
|
14
|
+
|
|
15
|
+
ACTION_TIME_STEP = :step
|
|
16
|
+
ACTION_DONT_STEP = :dont_step
|
|
17
|
+
ACTION_KILL_TETROMINO = :kill
|
|
18
|
+
|
|
19
|
+
FAIL_TETROMINO_SPAWN = :failed_spawn
|
|
20
|
+
SUCESSFUL_TETROMINO_SPAWN = :spawned_tetromino
|
|
21
|
+
|
|
22
|
+
TIME_AFTER_ACTION = 0.1
|
|
23
|
+
CELL_FALL_TIME = 0.025
|
|
24
|
+
TETROMINO_FREEFALL_WAIT = 0.025
|
|
25
|
+
TETROMINO_DEATH_WAIT = 0.3
|
|
26
|
+
|
|
27
|
+
LEFT_DIRECTION = -1
|
|
28
|
+
RIGHT_DIRECTION = 1
|
|
29
|
+
|
|
30
|
+
AFTER_TUTORIAL_TEXT = GemtrisText[:after_tutorial] % START_ANSWER
|
|
31
|
+
|
|
32
|
+
LOSE_TEXT = GemtrisText[:lose]
|
|
33
|
+
|
|
34
|
+
TUTORIAL_TEXT = format(
|
|
35
|
+
GemtrisText[:tutorial],
|
|
36
|
+
Cell::EMPTY_CELL_VISUAL,
|
|
37
|
+
Cell::FILLED_CELL_VISUAL,
|
|
38
|
+
Cell::CENTER_CELL_VISUAL,
|
|
39
|
+
Cell::SHADOW_CELL_VISUAL,
|
|
40
|
+
Cell::SOLID_CELL_VISUAL
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
WELCOME_TEXT = format(
|
|
44
|
+
GemtrisText[:welcome],
|
|
45
|
+
TUTORIAL_ANSWER,
|
|
46
|
+
START_ANSWER
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def play
|
|
50
|
+
intro_sequence
|
|
51
|
+
game_loop
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :grid, :player
|
|
57
|
+
attr_accessor :shape, :desired_spawn_x
|
|
58
|
+
|
|
59
|
+
def reset_game
|
|
60
|
+
@grid = Grid.new
|
|
61
|
+
@shape = nil
|
|
62
|
+
@player = Player.new
|
|
63
|
+
@desired_spawn_x = grid.width / 2
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def game_loop
|
|
67
|
+
loop do
|
|
68
|
+
game_sequence
|
|
69
|
+
break unless player.play_again?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def intro_sequence
|
|
74
|
+
CLI.clear_screen
|
|
75
|
+
start_or_tutorial
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def start_or_tutorial
|
|
79
|
+
puts WELCOME_TEXT
|
|
80
|
+
CLI.divide_screen
|
|
81
|
+
|
|
82
|
+
loop do
|
|
83
|
+
answer = CLI.user_input
|
|
84
|
+
|
|
85
|
+
tutorial_answer_loop if answer == TUTORIAL_ANSWER
|
|
86
|
+
break if [TUTORIAL_ANSWER, START_ANSWER].include? answer
|
|
87
|
+
|
|
88
|
+
CLI.error INVALID_CHOICE
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def tutorial_answer_loop
|
|
93
|
+
CLI.clear_screen
|
|
94
|
+
puts TUTORIAL_TEXT
|
|
95
|
+
CLI.divide_screen
|
|
96
|
+
loop do
|
|
97
|
+
answer = CLI.prompt(AFTER_TUTORIAL_TEXT)
|
|
98
|
+
break if answer == START_ANSWER
|
|
99
|
+
|
|
100
|
+
CLI.error INVALID_CHOICE
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def game_sequence
|
|
105
|
+
CLI.clear_screen
|
|
106
|
+
reset_game
|
|
107
|
+
gemtris_match_loop
|
|
108
|
+
display_match_loss
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def gemtris_match_loop
|
|
112
|
+
loop do
|
|
113
|
+
spawn_result = spawn_fitting_tetromino
|
|
114
|
+
break if spawn_result == FAIL_TETROMINO_SPAWN
|
|
115
|
+
|
|
116
|
+
tetromino_control_loop
|
|
117
|
+
tetromino_die
|
|
118
|
+
clear_full_rows
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
tetromino_die
|
|
122
|
+
display_grid
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def spawn_fitting_tetromino
|
|
126
|
+
randomized_shapes = GemtrisShapes.all_shapes.shuffle!
|
|
127
|
+
|
|
128
|
+
randomized_shapes.each do |random_shape|
|
|
129
|
+
self.shape = random_shape
|
|
130
|
+
spawn_result = set_shape_spawn
|
|
131
|
+
|
|
132
|
+
return spawn_result if spawn_result == SUCESSFUL_TETROMINO_SPAWN
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
FAIL_TETROMINO_SPAWN
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def set_shape_spawn
|
|
139
|
+
desired_spawns.each do |x_pos|
|
|
140
|
+
shape.position[:x] = x_pos
|
|
141
|
+
|
|
142
|
+
return SUCESSFUL_TETROMINO_SPAWN unless grid.shape_invalid_position?(shape)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
shape.position[:x] = desired_spawns.first
|
|
146
|
+
FAIL_TETROMINO_SPAWN
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def desired_spawns
|
|
150
|
+
sort_distance_to_desired = proc { |x_pos| (desired_spawn_x - x_pos).abs }
|
|
151
|
+
(1..grid.width).to_a.shuffle.sort_by(&sort_distance_to_desired)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def tetromino_control_loop
|
|
155
|
+
loop do
|
|
156
|
+
draw_shape_and_display_grid
|
|
157
|
+
|
|
158
|
+
sleep TIME_AFTER_ACTION
|
|
159
|
+
action_result = player_action_loop
|
|
160
|
+
|
|
161
|
+
break if action_result == ACTION_KILL_TETROMINO
|
|
162
|
+
|
|
163
|
+
tetromino_fall_step
|
|
164
|
+
|
|
165
|
+
break if tetromino_stop_control?
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def tetromino_stop_control?
|
|
170
|
+
grid.shape_touching_solid? shape
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def tetromino_die
|
|
174
|
+
grid.solidify_shape shape
|
|
175
|
+
display_grid
|
|
176
|
+
self.desired_spawn_x = shape.position[:x]
|
|
177
|
+
sleep TETROMINO_DEATH_WAIT
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def tetromino_fall_step
|
|
181
|
+
draw_shape_and_display_grid
|
|
182
|
+
sleep TIME_AFTER_ACTION
|
|
183
|
+
tetromino_descend
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def player_action_loop
|
|
187
|
+
loop do
|
|
188
|
+
player_action = player.prompt_tetromino_action
|
|
189
|
+
action_result = perform_player_action(player_action)
|
|
190
|
+
|
|
191
|
+
break action_result unless action_result == ACTION_DONT_STEP
|
|
192
|
+
|
|
193
|
+
draw_shape_and_display_grid
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def perform_player_action(player_action)
|
|
198
|
+
case player_action
|
|
199
|
+
when Player::FALL_ACTION then ACTION_TIME_STEP
|
|
200
|
+
when Player::MOVE_LEFT_ACTION then tetromino_move_left
|
|
201
|
+
when Player::MOVE_RIGHT_ACTION then tetromino_move_right
|
|
202
|
+
when Player::FREEFALL_ACTION then tetromino_freefall
|
|
203
|
+
when Player::ROTATE_CLOCKWISE_ACTION
|
|
204
|
+
rotate_tetromino Shape::CLOCKWISE_SIGN
|
|
205
|
+
when Player::ROTATE_COUNTER_ACTION
|
|
206
|
+
rotate_tetromino Shape::COUNTER_CLOCKWISE_SIGN
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def tetromino_move_left
|
|
211
|
+
tetromino_move_x LEFT_DIRECTION
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def tetromino_move_right
|
|
215
|
+
tetromino_move_x RIGHT_DIRECTION
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def tetromino_move_x(direction_sign = RIGHT_DIRECTION)
|
|
219
|
+
return ACTION_DONT_STEP unless grid.shape_move_x?(shape, direction_sign)
|
|
220
|
+
|
|
221
|
+
shape.position[:x] += 1 * direction_sign
|
|
222
|
+
ACTION_KILL_TETROMINO if tetromino_stop_control?
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def tetromino_descend
|
|
226
|
+
shape.position[:y] += 1
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def rotate_tetromino(rotation_sign = Shape::CLOCKWISE_SIGN)
|
|
230
|
+
shape.rotate(rotation_sign)
|
|
231
|
+
shape.rotate(-rotation_sign) if grid.shape_invalid_position?(shape)
|
|
232
|
+
|
|
233
|
+
ACTION_DONT_STEP
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def tetromino_freefall
|
|
237
|
+
until tetromino_stop_control?
|
|
238
|
+
tetromino_descend
|
|
239
|
+
draw_shape_and_display_grid
|
|
240
|
+
sleep TETROMINO_FREEFALL_WAIT
|
|
241
|
+
end
|
|
242
|
+
ACTION_KILL_TETROMINO
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def clear_full_rows
|
|
246
|
+
lowest_empty_row = nil
|
|
247
|
+
(1..grid.height).reverse_each do |row_number|
|
|
248
|
+
if grid.row_full?(row_number)
|
|
249
|
+
clear_row!(row_number)
|
|
250
|
+
lowest_empty_row ||= row_number
|
|
251
|
+
elsif lowest_empty_row
|
|
252
|
+
row_fall_to!(row_number, lowest_empty_row)
|
|
253
|
+
lowest_empty_row -= 1
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def row_fall_to!(from_row, to_row)
|
|
259
|
+
row = grid[from_row]
|
|
260
|
+
return if no_solid_cells_in_row?(row)
|
|
261
|
+
|
|
262
|
+
empty_row = grid[to_row]
|
|
263
|
+
|
|
264
|
+
(1..grid.width).each do |x_pos|
|
|
265
|
+
next unless row[x_pos].solid?
|
|
266
|
+
|
|
267
|
+
empty_row[x_pos] = row[x_pos]
|
|
268
|
+
row[x_pos] = Cell.new
|
|
269
|
+
display_grid
|
|
270
|
+
sleep CELL_FALL_TIME
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def clear_row!(row_y)
|
|
275
|
+
(1..grid.width).each do |x_pos|
|
|
276
|
+
cell = grid[row_y][x_pos]
|
|
277
|
+
next unless cell.solid?
|
|
278
|
+
|
|
279
|
+
cell.clear
|
|
280
|
+
display_grid
|
|
281
|
+
sleep CELL_FALL_TIME
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def no_solid_cells_in_row?(row)
|
|
286
|
+
row.values.none?(&:solid?)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def display_match_loss
|
|
290
|
+
puts LOSE_TEXT
|
|
291
|
+
CLI.divide_screen
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def display_grid
|
|
295
|
+
grid_visual_size = grid.board_visual_width
|
|
296
|
+
centered_title = TITLE_PLATE.center(grid_visual_size, TITLE_DECOR)
|
|
297
|
+
|
|
298
|
+
CLI.clear_screen
|
|
299
|
+
puts "#{centered_title}\n\n"
|
|
300
|
+
puts grid
|
|
301
|
+
puts "\n#{TITLE_DECOR * grid_visual_size}"
|
|
302
|
+
puts
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def draw_shape_and_display_grid
|
|
306
|
+
grid.draw_shape(shape)
|
|
307
|
+
display_grid
|
|
308
|
+
grid.undraw_shape(shape)
|
|
309
|
+
end
|
|
310
|
+
end
|
data/yml/gemtris.yml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
welcome: |
|
|
2
|
+
WELCOME to [TETRIS]!
|
|
3
|
+
|
|
4
|
+
If you'd like a rundown on how to play please type, (%s).
|
|
5
|
+
Otherwise, type (%s)!
|
|
6
|
+
|
|
7
|
+
lose: |
|
|
8
|
+
OH NO!
|
|
9
|
+
|
|
10
|
+
You LOST! That was a great game!
|
|
11
|
+
|
|
12
|
+
tutorial: |
|
|
13
|
+
BASICS:
|
|
14
|
+
|
|
15
|
+
Tetris is a game played on a grid of squares or "cells".
|
|
16
|
+
|
|
17
|
+
Your goal is play for as long as you can, guiding the block shapes ("tetrominos"),
|
|
18
|
+
as they descend from the top of the grid.
|
|
19
|
+
|
|
20
|
+
A tetromino will solidify when it touches the bottom of the grid or another solid cell.
|
|
21
|
+
When this occurs, a new tetromino will appear for you to control.
|
|
22
|
+
|
|
23
|
+
CONTROLS:
|
|
24
|
+
|
|
25
|
+
Tetrominos can be moved left or right, and rotated in 90 degree increments.
|
|
26
|
+
|
|
27
|
+
You will have time to think between actions. The tetromino only falls one cell
|
|
28
|
+
after an action is performed.
|
|
29
|
+
|
|
30
|
+
Rotating does not cause the tetromino to fall. This is a safe action.
|
|
31
|
+
|
|
32
|
+
A tetromino "shadow" will appear that shows you where the tetromino is going to fall.
|
|
33
|
+
|
|
34
|
+
LOSE/WIN:
|
|
35
|
+
|
|
36
|
+
You lose when a tetromino has no more space to fall and touches the top of the grid.
|
|
37
|
+
|
|
38
|
+
Connecting an entire row of solid cells will cause the row is disintegrate. The solid rows
|
|
39
|
+
above will collapse to fill the space. This is your key to success!
|
|
40
|
+
|
|
41
|
+
An empty cell looks like this: %s
|
|
42
|
+
A tetromino limb looks like this: %s
|
|
43
|
+
The center of a tetromino looks like this: %s
|
|
44
|
+
A tetromino shadow looks like this: %s
|
|
45
|
+
A solid cell looks like this: %s
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
after_tutorial: |
|
|
49
|
+
To start, please type (%s)!
|
|
50
|
+
|
|
51
|
+
tetromino_control_prompt: |
|
|
52
|
+
To Move Left: %s or %s
|
|
53
|
+
To Move Right: %s or %s
|
|
54
|
+
To Rotate Clockwise: %s
|
|
55
|
+
To Rotate Counter-Clockwise: %s
|
|
56
|
+
To Freefall: %s
|
|
57
|
+
|
|
58
|
+
Or input nothing to fall once.
|
|
59
|
+
|
|
60
|
+
play_again: |
|
|
61
|
+
Would you like to play another game? (yes or .)
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gemtris
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TacoMilitary
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
description: Using the keys on your keyboard, manipulate a falling shape within a
|
|
42
|
+
grid of cells in turn-based gameplay. Try to play for as long as possible without
|
|
43
|
+
touching the top of the grid.
|
|
44
|
+
email: tacobuddy18@outlook.com
|
|
45
|
+
executables:
|
|
46
|
+
- gemtris
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- bin/gemtris
|
|
51
|
+
- lib/gemtris.rb
|
|
52
|
+
- lib/gemtris/cell.rb
|
|
53
|
+
- lib/gemtris/grid.rb
|
|
54
|
+
- lib/gemtris/player.rb
|
|
55
|
+
- lib/gemtris/shapes.rb
|
|
56
|
+
- lib/gemtris/utils.rb
|
|
57
|
+
- yml/gemtris.yml
|
|
58
|
+
homepage: https://github.com/TacoMilitary/gemtris
|
|
59
|
+
licenses: []
|
|
60
|
+
metadata: {}
|
|
61
|
+
post_install_message:
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 3.2.0
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.4.1
|
|
77
|
+
signing_key:
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: This is a simple command-line Ruby recreation of the game Tetris!
|
|
80
|
+
test_files: []
|