textbringer 22 → 24

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.
@@ -0,0 +1,322 @@
1
+ module Textbringer
2
+ class DiredMode < Mode
3
+ define_keymap :DIRED_MODE_MAP
4
+ DIRED_MODE_MAP.define_key("n", :dired_next_line_command)
5
+ DIRED_MODE_MAP.define_key(" ", :dired_next_line_command)
6
+ DIRED_MODE_MAP.define_key("\C-n", :dired_next_line_command)
7
+ DIRED_MODE_MAP.define_key("p", :dired_previous_line_command)
8
+ DIRED_MODE_MAP.define_key("\C-p", :dired_previous_line_command)
9
+ DIRED_MODE_MAP.define_key("^", :dired_up_directory_command)
10
+ DIRED_MODE_MAP.define_key("\C-m", :dired_find_file_command)
11
+ DIRED_MODE_MAP.define_key("f", :dired_find_file_command)
12
+ DIRED_MODE_MAP.define_key("o", :dired_find_file_other_window_command)
13
+ DIRED_MODE_MAP.define_key("d", :dired_flag_file_deletion_command)
14
+ DIRED_MODE_MAP.define_key("u", :dired_unmark_command)
15
+ DIRED_MODE_MAP.define_key("U", :dired_unmark_all_command)
16
+ DIRED_MODE_MAP.define_key("x", :dired_do_flagged_delete_command)
17
+ DIRED_MODE_MAP.define_key("R", :dired_do_rename_command)
18
+ DIRED_MODE_MAP.define_key("C", :dired_do_copy_command)
19
+ DIRED_MODE_MAP.define_key("+", :dired_create_directory_command)
20
+ DIRED_MODE_MAP.define_key("g", :dired_revert_command)
21
+ DIRED_MODE_MAP.define_key("q", :bury_buffer)
22
+
23
+ # Deletion-flagged lines
24
+ define_syntax :dired_flagged, /^D .+$/
25
+ # Symlinks
26
+ define_syntax :dired_symlink, /^[ D] \S+\s+\d+\s+[\d-]+ [\d:]+ .+ -> .+$/
27
+ # Directories
28
+ define_syntax :dired_directory, /^[ D] d\S+\s+\d+\s+[\d-]+ [\d:]+ .+\/$/
29
+ # Executables
30
+ define_syntax :dired_executable, /^[ D] -[r-][w-]x.+$/
31
+
32
+ PERM_BITS = [
33
+ ["r", 0400], ["w", 0200], ["x", 0100],
34
+ ["r", 0040], ["w", 0020], ["x", 0010],
35
+ ["r", 0004], ["w", 0002], ["x", 0001]
36
+ ]
37
+
38
+ def self.format_permissions(stat)
39
+ type = if stat.directory? then "d"
40
+ elsif stat.symlink? then "l"
41
+ elsif stat.pipe? then "p"
42
+ elsif stat.socket? then "s"
43
+ elsif stat.chardev? then "c"
44
+ elsif stat.blockdev? then "b"
45
+ else "-"
46
+ end
47
+ type + PERM_BITS.map { |ch, mask| stat.mode & mask != 0 ? ch : "-" }.join
48
+ end
49
+
50
+ def self.generate_listing(dir)
51
+ entries = []
52
+ Dir.foreach(dir) do |name|
53
+ path = File.join(dir, name)
54
+ begin
55
+ stat = File.lstat(path)
56
+ perms = format_permissions(stat)
57
+ size = stat.size
58
+ mtime = stat.mtime.strftime("%Y-%m-%d %H:%M")
59
+ if stat.symlink?
60
+ begin
61
+ target = File.readlink(path)
62
+ rescue SystemCallError
63
+ target = "?"
64
+ end
65
+ display = "#{name} -> #{target}"
66
+ elsif stat.directory?
67
+ display = "#{name}/"
68
+ else
69
+ display = name
70
+ end
71
+ entries << {
72
+ name: name,
73
+ display: display,
74
+ perms: perms,
75
+ size: size,
76
+ mtime: mtime,
77
+ directory: stat.directory?
78
+ }
79
+ rescue SystemCallError => e
80
+ entries << {
81
+ name: name,
82
+ display: name,
83
+ perms: "??????????",
84
+ size: 0,
85
+ mtime: "????-??-?? ??:??",
86
+ directory: false,
87
+ error: e.message
88
+ }
89
+ end
90
+ end
91
+
92
+ entries.sort_by! { |e| [e[:directory] ? 0 : 1, e[:name].downcase] }
93
+
94
+ lines = [" #{dir}:\n"]
95
+ entries.each do |e|
96
+ line = " #{e[:perms]} #{e[:size].to_s.rjust(8)} #{e[:mtime]} #{e[:display]}\n"
97
+ lines << line
98
+ end
99
+ lines.join
100
+ end
101
+
102
+ def initialize(buffer)
103
+ super(buffer)
104
+ buffer.keymap = DIRED_MODE_MAP
105
+ end
106
+
107
+ define_local_command(:dired_move_to_filename,
108
+ doc: "Move point to the filename on the current line.") do
109
+ @buffer.beginning_of_line
110
+ if @buffer.looking_at?(/^[D ] \S+\s+\d+\s+[\d-]+\s+[\d:]+\s+/)
111
+ @buffer.forward_char(@buffer.match_string(0).length)
112
+ end
113
+ end
114
+
115
+ define_local_command(:dired_next_line, doc: "Move to next file line.") do
116
+ @buffer.next_line
117
+ dired_move_to_filename
118
+ end
119
+
120
+ define_local_command(:dired_previous_line, doc: "Move to previous file line.") do
121
+ first_file_line = @buffer.save_excursion {
122
+ @buffer.beginning_of_buffer
123
+ @buffer.current_line
124
+ }
125
+ if @buffer.current_line > first_file_line
126
+ @buffer.previous_line
127
+ end
128
+ dired_move_to_filename
129
+ end
130
+
131
+ define_local_command(:dired_up_directory, doc: "Go up to parent directory.") do
132
+ dir = @buffer[:dired_directory]
133
+ parent = File.dirname(dir)
134
+ dired(parent)
135
+ end
136
+
137
+ define_local_command(:dired_find_file, doc: "Visit file or directory at point.") do
138
+ name = current_file_name
139
+ return unless name
140
+ dir = @buffer[:dired_directory]
141
+ path = File.join(dir, name)
142
+ if File.directory?(path)
143
+ dired(path)
144
+ else
145
+ find_file(path)
146
+ end
147
+ end
148
+
149
+ define_local_command(:dired_find_file_other_window,
150
+ doc: "Visit file at point in other window.") do
151
+ name = current_file_name
152
+ return unless name
153
+ dir = @buffer[:dired_directory]
154
+ path = File.join(dir, name)
155
+ if Window.list.size == 1
156
+ split_window
157
+ end
158
+ other_window
159
+ if File.directory?(path)
160
+ dired(path)
161
+ else
162
+ find_file(path)
163
+ end
164
+ end
165
+
166
+ define_local_command(:dired_flag_file_deletion,
167
+ doc: "Flag file at point for deletion.") do
168
+ set_flag("D")
169
+ dired_next_line
170
+ end
171
+
172
+ define_local_command(:dired_unmark, doc: "Remove deletion flag from file at point.") do
173
+ set_flag(" ")
174
+ dired_next_line
175
+ end
176
+
177
+ define_local_command(:dired_unmark_all, doc: "Remove all deletion flags.") do
178
+ @buffer.save_excursion do
179
+ @buffer.beginning_of_buffer
180
+ while !@buffer.end_of_buffer?
181
+ set_flag(" ")
182
+ @buffer.next_line
183
+ end
184
+ end
185
+ end
186
+
187
+ define_local_command(:dired_do_flagged_delete,
188
+ doc: "Delete files flagged for deletion.") do
189
+ files = collect_flagged_files
190
+ return if files.empty?
191
+ list = files.map { |f| " #{f}" }.join("\n")
192
+ if yes_or_no?("Delete these files?")
193
+ files.each do |name|
194
+ next if name == "." || name == ".."
195
+ path = File.join(@buffer[:dired_directory], name)
196
+ begin
197
+ if File.directory?(path) && !File.symlink?(path)
198
+ FileUtils.rm_rf(path)
199
+ else
200
+ File.delete(path)
201
+ end
202
+ rescue SystemCallError => e
203
+ message("Error deleting #{name}: #{e.message}")
204
+ end
205
+ end
206
+ dired_revert
207
+ end
208
+ end
209
+
210
+ define_local_command(:dired_do_rename, doc: "Rename/move file at point.") do
211
+ name = current_file_name
212
+ return unless name
213
+ dir = @buffer[:dired_directory]
214
+ src = File.join(dir, name)
215
+ dest = read_file_name("Rename #{name} to: ", default: dir + "/")
216
+ dest = File.expand_path(dest, dir)
217
+ FileUtils.mv(src, dest)
218
+ dired_revert
219
+ end
220
+
221
+ define_local_command(:dired_do_copy, doc: "Copy file at point.") do
222
+ name = current_file_name
223
+ return unless name
224
+ dir = @buffer[:dired_directory]
225
+ src = File.join(dir, name)
226
+ dest = read_file_name("Copy #{name} to: ", default: dir + "/")
227
+ dest = File.expand_path(dest, dir)
228
+ FileUtils.cp_r(src, dest)
229
+ dired_revert
230
+ end
231
+
232
+ define_local_command(:dired_create_directory, doc: "Create a new directory.") do
233
+ dir = @buffer[:dired_directory]
234
+ name = read_from_minibuffer("Create directory: ", default: dir + "/")
235
+ name = File.expand_path(name, dir)
236
+ FileUtils.mkdir_p(name)
237
+ dired_revert
238
+ end
239
+
240
+ define_local_command(:dired_revert, doc: "Refresh directory listing.") do
241
+ dir = @buffer[:dired_directory]
242
+ saved_name = current_file_name
243
+ saved_line = @buffer.current_line
244
+ @buffer.read_only_edit do
245
+ @buffer.clear
246
+ @buffer.insert(DiredMode.generate_listing(dir))
247
+ @buffer.beginning_of_buffer
248
+ @buffer.forward_line
249
+ end
250
+ if saved_name
251
+ @buffer.beginning_of_buffer
252
+ found = false
253
+ while !@buffer.end_of_buffer?
254
+ if current_file_name == saved_name
255
+ found = true
256
+ break
257
+ end
258
+ @buffer.next_line
259
+ end
260
+ unless found
261
+ goto_line(saved_line)
262
+ @buffer.beginning_of_line
263
+ if @buffer.end_of_buffer? || current_file_name.nil?
264
+ @buffer.end_of_buffer
265
+ @buffer.previous_line while current_file_name.nil? && @buffer.current_line > 2
266
+ end
267
+ end
268
+ end
269
+ dired_move_to_filename
270
+ end
271
+
272
+ private
273
+
274
+ def current_file_name
275
+ @buffer.save_excursion do
276
+ @buffer.beginning_of_line
277
+ # Line format: " perms size date time display_name"
278
+ # or: "D perms size date time display_name"
279
+ if @buffer.looking_at?(/^[D ] (\S+)\s+\d+\s+[\d-]+\s+[\d:]+\s+(.+)$/)
280
+ perms = @buffer.match_string(1)
281
+ display = @buffer.match_string(2)
282
+ # Strip symlink target: "name -> target" -> "name" (only for symlinks)
283
+ display = display.sub(/ -> .+$/, "") if perms.start_with?("l")
284
+ # Strip trailing slash for directories: "name/" -> "name"
285
+ display = display.chomp("/")
286
+ display
287
+ end
288
+ end
289
+ end
290
+
291
+ def set_flag(char)
292
+ @buffer.read_only_edit do
293
+ @buffer.save_excursion do
294
+ @buffer.beginning_of_line
295
+ if @buffer.looking_at?(/^[D ]/)
296
+ @buffer.delete_char(1)
297
+ @buffer.insert(char)
298
+ end
299
+ end
300
+ end
301
+ end
302
+
303
+ def collect_flagged_files
304
+ files = []
305
+ @buffer.save_excursion do
306
+ @buffer.beginning_of_buffer
307
+ while !@buffer.end_of_buffer?
308
+ @buffer.beginning_of_line
309
+ if @buffer.looking_at?(/^D (\S+)\s+\d+\s+[\d-]+\s+[\d:]+\s+(.+)$/)
310
+ perms = @buffer.match_string(1)
311
+ display = @buffer.match_string(2)
312
+ display = display.sub(/ -> .+$/, "") if perms.start_with?("l")
313
+ display = display.chomp("/")
314
+ files << display
315
+ end
316
+ @buffer.next_line
317
+ end
318
+ end
319
+ files
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,46 @@
1
+ module Textbringer
2
+ class GamegridMode < Mode
3
+ @syntax_table = {}
4
+
5
+ def self.inherited(child)
6
+ super
7
+ child.instance_variable_set(:@syntax_table, {})
8
+ end
9
+
10
+ define_keymap :GAMEGRID_MODE_MAP
11
+ GAMEGRID_MODE_MAP.define_key("q", :gamegrid_quit_command)
12
+
13
+ def initialize(buffer)
14
+ super(buffer)
15
+ buffer.keymap = GAMEGRID_MODE_MAP
16
+ end
17
+
18
+
19
+ define_local_command(:gamegrid_init,
20
+ doc: "Initialize a gamegrid in the current buffer.") do |width, height, margin_left: 0|
21
+ grid = Gamegrid.new(width, height, margin_left: margin_left)
22
+ @buffer[:gamegrid] = grid
23
+ @buffer.read_only = true
24
+ @buffer[:highlight_override] = -> { grid.face_map }
25
+ grid
26
+ end
27
+
28
+ define_local_command(:gamegrid_refresh,
29
+ doc: "Refresh the gamegrid display.") do
30
+ grid = @buffer[:gamegrid]
31
+ return unless grid
32
+ @buffer.read_only_edit do
33
+ @buffer.clear
34
+ @buffer.insert(grid.render)
35
+ @buffer.beginning_of_buffer
36
+ end
37
+ end
38
+
39
+ define_local_command(:gamegrid_quit,
40
+ doc: "Quit the current game.") do
41
+ grid = @buffer[:gamegrid]
42
+ grid&.stop_timer
43
+ bury_buffer
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,316 @@
1
+ module Textbringer
2
+ class TetrisMode < GamegridMode
3
+ BOARD_WIDTH = 10
4
+ BOARD_HEIGHT = 20
5
+ BORDER_VALUE = 8 # cell value used for the 1-cell border around the board
6
+
7
+ PIECE_COLORS = {
8
+ 1 => :gamegrid_block_cyan,
9
+ 2 => :gamegrid_block_yellow,
10
+ 3 => :gamegrid_block_magenta,
11
+ 4 => :gamegrid_block_green,
12
+ 5 => :gamegrid_block_red,
13
+ 6 => :gamegrid_block_blue,
14
+ 7 => :gamegrid_block_white,
15
+ }.freeze
16
+
17
+ PIECE_NAMES = ["", "I", "O", "T", "S", "Z", "J", "L"].freeze
18
+
19
+ # Pieces[type][rotation][row][col] — 4×4 bounding box, 1-indexed types
20
+ PIECES = [
21
+ nil,
22
+ # 1: I (cyan)
23
+ [
24
+ [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
25
+ [[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]],
26
+ [[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]],
27
+ [[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]],
28
+ ],
29
+ # 2: O (yellow)
30
+ [
31
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
32
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
33
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
34
+ [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
35
+ ],
36
+ # 3: T (magenta)
37
+ [
38
+ [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
39
+ [[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
40
+ [[0,0,0,0],[1,1,1,0],[0,1,0,0],[0,0,0,0]],
41
+ [[0,1,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]],
42
+ ],
43
+ # 4: S (green)
44
+ [
45
+ [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]],
46
+ [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]],
47
+ [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]],
48
+ [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]],
49
+ ],
50
+ # 5: Z (red)
51
+ [
52
+ [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
53
+ [[0,0,1,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
54
+ [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
55
+ [[0,0,1,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
56
+ ],
57
+ # 6: J (blue)
58
+ [
59
+ [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
60
+ [[0,1,1,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]],
61
+ [[0,0,0,0],[1,1,1,0],[0,0,1,0],[0,0,0,0]],
62
+ [[0,1,0,0],[0,1,0,0],[1,1,0,0],[0,0,0,0]],
63
+ ],
64
+ # 7: L (white)
65
+ [
66
+ [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
67
+ [[0,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,0,0]],
68
+ [[0,0,0,0],[1,1,1,0],[1,0,0,0],[0,0,0,0]],
69
+ [[1,1,0,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]],
70
+ ],
71
+ ].freeze
72
+
73
+ define_keymap :TETRIS_MODE_MAP
74
+ TETRIS_MODE_MAP.define_key("q", :gamegrid_quit_command)
75
+ TETRIS_MODE_MAP.define_key("n", :tetris_new_game_command)
76
+ TETRIS_MODE_MAP.define_key(:left, :tetris_move_left_command)
77
+ TETRIS_MODE_MAP.define_key("h", :tetris_move_left_command)
78
+ TETRIS_MODE_MAP.define_key(:right, :tetris_move_right_command)
79
+ TETRIS_MODE_MAP.define_key("l", :tetris_move_right_command)
80
+ TETRIS_MODE_MAP.define_key(:down, :tetris_move_down_command)
81
+ TETRIS_MODE_MAP.define_key("j", :tetris_move_down_command)
82
+ TETRIS_MODE_MAP.define_key(:up, :tetris_rotate_command)
83
+ TETRIS_MODE_MAP.define_key("k", :tetris_rotate_command)
84
+ TETRIS_MODE_MAP.define_key(" ", :tetris_drop_command)
85
+ TETRIS_MODE_MAP.define_key("p", :tetris_pause_command)
86
+
87
+ attr_reader :score, :level, :lines_cleared,
88
+ :piece_type, :piece_rot, :piece_x, :piece_y,
89
+ :next_type, :game_over, :paused
90
+
91
+ def initialize(buffer)
92
+ super
93
+ buffer.keymap = TETRIS_MODE_MAP
94
+ @game_over = true
95
+ @paused = false
96
+ @grid = nil
97
+ end
98
+
99
+ define_local_command(:tetris_new_game, doc: "Start a new Tetris game.") do
100
+ @grid&.stop_timer
101
+ @grid = gamegrid_init(BOARD_WIDTH + 2, BOARD_HEIGHT + 2, margin_left: 2)
102
+ @grid.set_display_option(0, char: " ")
103
+ @grid.set_display_option(BORDER_VALUE, char: "[]", face: :gamegrid_border)
104
+ PIECE_COLORS.each { |v, f| @grid.set_display_option(v, char: "[]", face: f) }
105
+
106
+ @board = Array.new(BOARD_HEIGHT) { Array.new(BOARD_WIDTH, 0) }
107
+ @score = 0
108
+ @level = 1
109
+ @lines_cleared = 0
110
+ @game_over = false
111
+ @paused = false
112
+ @next_type = random_piece_type
113
+
114
+ spawn_piece
115
+ start_game_timer unless @game_over
116
+ render_board
117
+ end
118
+
119
+ define_local_command(:tetris_move_left, doc: "Move piece left.") do
120
+ return unless active?
121
+ if valid_position?(@piece_x - 1, @piece_y, @piece_type, @piece_rot)
122
+ @piece_x -= 1
123
+ render_board
124
+ end
125
+ end
126
+
127
+ define_local_command(:tetris_move_right, doc: "Move piece right.") do
128
+ return unless active?
129
+ if valid_position?(@piece_x + 1, @piece_y, @piece_type, @piece_rot)
130
+ @piece_x += 1
131
+ render_board
132
+ end
133
+ end
134
+
135
+ define_local_command(:tetris_move_down, doc: "Soft-drop current piece.") do
136
+ return unless active?
137
+ step_down
138
+ end
139
+
140
+ define_local_command(:tetris_rotate, doc: "Rotate current piece clockwise.") do
141
+ return unless active?
142
+ new_rot = (@piece_rot + 1) % 4
143
+ # Try basic rotation then simple wall-kicks (±1, ±2 columns)
144
+ [0, -1, 1, -2, 2].each do |kick|
145
+ if valid_position?(@piece_x + kick, @piece_y, @piece_type, new_rot)
146
+ @piece_x += kick
147
+ @piece_rot = new_rot
148
+ break
149
+ end
150
+ end
151
+ render_board
152
+ end
153
+
154
+ define_local_command(:tetris_drop, doc: "Hard-drop current piece.") do
155
+ return unless active?
156
+ while valid_position?(@piece_x, @piece_y + 1, @piece_type, @piece_rot)
157
+ @piece_y += 1
158
+ @score += 2
159
+ end
160
+ lock_and_continue
161
+ render_board
162
+ end
163
+
164
+ define_local_command(:tetris_pause, doc: "Toggle game pause.") do
165
+ return if @game_over || !@grid
166
+ @paused = !@paused
167
+ if @paused
168
+ @grid.stop_timer
169
+ else
170
+ start_game_timer
171
+ end
172
+ render_board
173
+ end
174
+
175
+ private
176
+
177
+ def active?
178
+ !@game_over && !@paused && @grid
179
+ end
180
+
181
+ def random_piece_type
182
+ rand(1..7)
183
+ end
184
+
185
+ def spawn_piece
186
+ @piece_type = @next_type
187
+ @next_type = random_piece_type
188
+ @piece_rot = 0
189
+ @piece_x = BOARD_WIDTH / 2 - 2
190
+ @piece_y = 0
191
+ @game_over = !valid_position?(@piece_x, @piece_y, @piece_type, @piece_rot)
192
+ end
193
+
194
+ def valid_position?(x, y, type, rot)
195
+ PIECES[type][rot].each_with_index do |row, row_i|
196
+ row.each_with_index do |cell, col_i|
197
+ next if cell == 0
198
+ bx = x + col_i
199
+ by = y + row_i
200
+ return false if bx < 0 || bx >= BOARD_WIDTH
201
+ return false if by >= BOARD_HEIGHT
202
+ next if by < 0 # piece can start partially above the board
203
+ return false if @board[by][bx] != 0
204
+ end
205
+ end
206
+ true
207
+ end
208
+
209
+ def lock_piece
210
+ PIECES[@piece_type][@piece_rot].each_with_index do |row, row_i|
211
+ row.each_with_index do |cell, col_i|
212
+ next if cell == 0
213
+ by = @piece_y + row_i
214
+ bx = @piece_x + col_i
215
+ next if by < 0 || by >= BOARD_HEIGHT || bx < 0 || bx >= BOARD_WIDTH
216
+ @board[by][bx] = @piece_type
217
+ end
218
+ end
219
+ end
220
+
221
+ def clear_lines
222
+ full = (0...BOARD_HEIGHT).select { |y| @board[y].all? { |c| c != 0 } }
223
+ return 0 if full.empty?
224
+ full.reverse_each { |y| @board.delete_at(y) }
225
+ full.size.times { @board.unshift(Array.new(BOARD_WIDTH, 0)) }
226
+ n = full.size
227
+ @score += [0, 100, 300, 500, 800][n].to_i * @level
228
+ @lines_cleared += n
229
+ @level = @lines_cleared / 10 + 1
230
+ n
231
+ end
232
+
233
+ def step_down
234
+ if valid_position?(@piece_x, @piece_y + 1, @piece_type, @piece_rot)
235
+ @piece_y += 1
236
+ else
237
+ lock_and_continue
238
+ end
239
+ render_board
240
+ end
241
+
242
+ def lock_and_continue
243
+ lock_piece
244
+ clear_lines
245
+ spawn_piece
246
+ if @game_over
247
+ @grid.stop_timer
248
+ else
249
+ start_game_timer
250
+ end
251
+ end
252
+
253
+ def start_game_timer
254
+ @grid.start_timer(timer_interval) { step_down }
255
+ end
256
+
257
+ def timer_interval
258
+ # Level 1 = 1.0 s, each level adds 0.1 s speed, floor at 0.1 s
259
+ [1.0 - (@level - 1) * 0.1, 0.1].max
260
+ end
261
+
262
+ def update_grid
263
+ # Border: top and bottom rows
264
+ (BOARD_WIDTH + 2).times do |x|
265
+ @grid.set_cell(x, 0, BORDER_VALUE)
266
+ @grid.set_cell(x, BOARD_HEIGHT + 1, BORDER_VALUE)
267
+ end
268
+ # Border: left and right columns (inner rows only)
269
+ BOARD_HEIGHT.times do |y|
270
+ @grid.set_cell(0, y + 1, BORDER_VALUE)
271
+ @grid.set_cell(BOARD_WIDTH + 1, y + 1, BORDER_VALUE)
272
+ end
273
+ # Board content, offset by (1, 1)
274
+ BOARD_HEIGHT.times do |y|
275
+ BOARD_WIDTH.times do |x|
276
+ @grid.set_cell(x + 1, y + 1, @board[y][x])
277
+ end
278
+ end
279
+ return if @game_over
280
+ # Current piece, offset by (1, 1)
281
+ PIECES[@piece_type][@piece_rot].each_with_index do |row, row_i|
282
+ row.each_with_index do |cell, col_i|
283
+ next if cell == 0
284
+ bx = @piece_x + col_i + 1
285
+ by = @piece_y + row_i + 1
286
+ next if bx < 1 || bx > BOARD_WIDTH || by < 1 || by > BOARD_HEIGHT
287
+ @grid.set_cell(bx, by, @piece_type)
288
+ end
289
+ end
290
+ end
291
+
292
+ def render_board
293
+ update_grid
294
+ @buffer.read_only_edit do
295
+ @buffer.clear
296
+ @buffer.insert(@grid.render)
297
+ @buffer.insert("\n")
298
+ @buffer.insert(status_text)
299
+ @buffer.beginning_of_buffer
300
+ end
301
+ end
302
+
303
+ def status_text
304
+ if @game_over
305
+ "GAME OVER Score: #{@score} Level: #{@level} Lines: #{@lines_cleared}" \
306
+ " [n]ew game [q]uit\n"
307
+ elsif @paused
308
+ "PAUSED Score: #{@score} Level: #{@level} Lines: #{@lines_cleared}" \
309
+ " [p] resume\n"
310
+ else
311
+ "Score: #{@score} Level: #{@level} Lines: #{@lines_cleared}" \
312
+ " Next: #{PIECE_NAMES[@next_type]}\n"
313
+ end
314
+ end
315
+ end
316
+ end
@@ -1,3 +1,3 @@
1
1
  module Textbringer
2
- VERSION = "22"
2
+ VERSION = "24"
3
3
  end