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.
- checksums.yaml +4 -4
- data/CLAUDE.md +0 -6
- data/lib/textbringer/buffer.rb +2 -0
- data/lib/textbringer/commands/dired.rb +31 -0
- data/lib/textbringer/commands/files.rb +4 -0
- data/lib/textbringer/commands/gamegrid.rb +25 -0
- data/lib/textbringer/commands/tetris.rb +10 -0
- data/lib/textbringer/faces/dired.rb +6 -0
- data/lib/textbringer/faces/gamegrid.rb +23 -0
- data/lib/textbringer/gamegrid.rb +164 -0
- data/lib/textbringer/input_method.rb +6 -0
- data/lib/textbringer/input_methods/skk_input_method.rb +105 -20
- data/lib/textbringer/keymap.rb +1 -0
- data/lib/textbringer/modes/dired_mode.rb +322 -0
- data/lib/textbringer/modes/gamegrid_mode.rb +46 -0
- data/lib/textbringer/modes/tetris_mode.rb +316 -0
- data/lib/textbringer/version.rb +1 -1
- data/lib/textbringer/window.rb +7 -0
- data/lib/textbringer.rb +7 -0
- metadata +11 -2
|
@@ -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
|
data/lib/textbringer/version.rb
CHANGED