textbringer 23 → 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/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/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 +6 -0
- data/lib/textbringer.rb +7 -0
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7426dda13275f37439bdcc360a55971b34cc4b62916eeaee734b127ab9a8a88
|
|
4
|
+
data.tar.gz: fcc6391ac0697659a8b85005766186502731e8473928a5e5165d7c0a97d533f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be219b93c87935b98780c5d41430dd3d0760b839e20977dc843fa88aa8253614fb0e68dc7f90e37c08de6a23722daec928294f9702114908707011c33c766dc6
|
|
7
|
+
data.tar.gz: 6f61d4ed9ce65308bf150f24c886e05a1743af8bd38913c80f750db77c64a90b543ae95f60774b9d8ae52953cfa91e5f36e62665e0830b9aab6f6d97925ab289
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textbringer
|
|
4
|
+
module Commands
|
|
5
|
+
define_command(:dired, doc: "Open a directory browser.") do
|
|
6
|
+
|dir = read_file_name("Dired: ",
|
|
7
|
+
default: (Buffer.current.file_name ?
|
|
8
|
+
File.dirname(Buffer.current.file_name) : Dir.pwd) + "/")|
|
|
9
|
+
dir = File.expand_path(dir)
|
|
10
|
+
raise EditorError, "#{dir} is not a directory" unless File.directory?(dir)
|
|
11
|
+
buf_name = "*Dired: #{dir}*"
|
|
12
|
+
buffer = Buffer.find_or_new(buf_name, undo_limit: 0, read_only: true)
|
|
13
|
+
buffer[:dired_directory] = dir
|
|
14
|
+
buffer.apply_mode(DiredMode) unless buffer.mode.is_a?(DiredMode)
|
|
15
|
+
if buffer.bytesize == 0
|
|
16
|
+
buffer.read_only_edit do
|
|
17
|
+
buffer.insert(DiredMode.generate_listing(dir))
|
|
18
|
+
buffer.beginning_of_buffer
|
|
19
|
+
buffer.forward_line
|
|
20
|
+
until buffer.end_of_buffer?
|
|
21
|
+
buffer.beginning_of_line
|
|
22
|
+
break unless buffer.looking_at?(/^[D ] \S+\s+\d+\s+[\d-]+\s+[\d:]+\s+\.\.?\/$/)
|
|
23
|
+
buffer.forward_line
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
switch_to_buffer(buffer)
|
|
28
|
+
dired_move_to_filename_command
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,6 +5,10 @@ module Textbringer
|
|
|
5
5
|
module Commands
|
|
6
6
|
define_command(:find_file, doc: "Open or create a file.") do
|
|
7
7
|
|file_name = read_file_name("Find file: ", default: (Buffer.current.file_name ? File.dirname(Buffer.current.file_name) : Dir.pwd) + "/")|
|
|
8
|
+
if File.directory?(file_name)
|
|
9
|
+
dired(file_name)
|
|
10
|
+
next
|
|
11
|
+
end
|
|
8
12
|
config = EditorConfig.load_file(file_name)
|
|
9
13
|
buffer = Buffer.find_file(file_name)
|
|
10
14
|
if buffer.new_file?
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
module Commands
|
|
3
|
+
define_command(:gamegrid_show_scores,
|
|
4
|
+
doc: "Display high scores for a game.") do
|
|
5
|
+
|game_name = read_from_minibuffer("Game name: ")|
|
|
6
|
+
scores = Gamegrid.load_scores(game_name)
|
|
7
|
+
buffer = Buffer.find_or_new("*Scores*", undo_limit: 0)
|
|
8
|
+
buffer.read_only_edit do
|
|
9
|
+
buffer.clear
|
|
10
|
+
buffer.insert("High Scores for #{game_name}\n")
|
|
11
|
+
buffer.insert("=" * 40 + "\n\n")
|
|
12
|
+
if scores.empty?
|
|
13
|
+
buffer.insert("No scores recorded.\n")
|
|
14
|
+
else
|
|
15
|
+
scores.each_with_index do |entry, i|
|
|
16
|
+
buffer.insert(
|
|
17
|
+
"#{i + 1}. #{entry[:score]} #{entry[:player]} #{entry[:time]}\n"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
switch_to_buffer(buffer)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
module Commands
|
|
3
|
+
define_command(:tetris, doc: "Play Tetris.") do
|
|
4
|
+
buffer = Buffer.find_or_new("*Tetris*", undo_limit: 0)
|
|
5
|
+
buffer.apply_mode(TetrisMode) unless buffer.mode.is_a?(TetrisMode)
|
|
6
|
+
switch_to_buffer(buffer)
|
|
7
|
+
buffer.mode.tetris_new_game
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
# Foreground color faces for gamegrid
|
|
3
|
+
Face.define :gamegrid_red, foreground: "red"
|
|
4
|
+
Face.define :gamegrid_green, foreground: "green"
|
|
5
|
+
Face.define :gamegrid_blue, foreground: "blue"
|
|
6
|
+
Face.define :gamegrid_yellow, foreground: "yellow"
|
|
7
|
+
Face.define :gamegrid_cyan, foreground: "cyan"
|
|
8
|
+
Face.define :gamegrid_magenta, foreground: "magenta"
|
|
9
|
+
Face.define :gamegrid_white, foreground: "white"
|
|
10
|
+
|
|
11
|
+
# Block faces (solid background) for Tetris-style solid blocks
|
|
12
|
+
Face.define :gamegrid_block_red, background: "red", foreground: "red"
|
|
13
|
+
Face.define :gamegrid_block_green, background: "green", foreground: "green"
|
|
14
|
+
Face.define :gamegrid_block_blue, background: "blue", foreground: "blue"
|
|
15
|
+
Face.define :gamegrid_block_yellow, background: "yellow", foreground: "yellow"
|
|
16
|
+
Face.define :gamegrid_block_cyan, background: "cyan", foreground: "cyan"
|
|
17
|
+
Face.define :gamegrid_block_magenta, background: "magenta", foreground: "magenta"
|
|
18
|
+
Face.define :gamegrid_block_white, background: "white", foreground: "white"
|
|
19
|
+
|
|
20
|
+
# Utility faces
|
|
21
|
+
Face.define :gamegrid_border, foreground: "white", bold: true
|
|
22
|
+
Face.define :gamegrid_score, foreground: "yellow", bold: true
|
|
23
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "time"
|
|
3
|
+
|
|
4
|
+
module Textbringer
|
|
5
|
+
class Gamegrid
|
|
6
|
+
attr_reader :width, :height
|
|
7
|
+
attr_accessor :score
|
|
8
|
+
|
|
9
|
+
def initialize(width, height, margin_left: 0)
|
|
10
|
+
@width = width
|
|
11
|
+
@height = height
|
|
12
|
+
@margin_left = margin_left
|
|
13
|
+
@grid = Array.new(height) { Array.new(width, 0) }
|
|
14
|
+
@faces = Array.new(height) { Array.new(width, nil) }
|
|
15
|
+
@display_options = {}
|
|
16
|
+
@score = 0
|
|
17
|
+
@timer_thread = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Cell API
|
|
21
|
+
|
|
22
|
+
def set_cell(x, y, value)
|
|
23
|
+
check_bounds(x, y)
|
|
24
|
+
@grid[y][x] = value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_cell(x, y)
|
|
28
|
+
check_bounds(x, y)
|
|
29
|
+
@grid[y][x]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def set_face(x, y, face_name)
|
|
33
|
+
check_bounds(x, y)
|
|
34
|
+
@faces[y][x] = face_name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_face(x, y)
|
|
38
|
+
check_bounds(x, y)
|
|
39
|
+
@faces[y][x]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_display_option(value, char:, face: nil)
|
|
43
|
+
@display_options[value] = { char: char, face: face }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fill(value)
|
|
47
|
+
@height.times do |y|
|
|
48
|
+
@width.times do |x|
|
|
49
|
+
@grid[y][x] = value
|
|
50
|
+
@faces[y][x] = nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Rendering
|
|
56
|
+
|
|
57
|
+
def render
|
|
58
|
+
margin = " " * @margin_left
|
|
59
|
+
@height.times.map { |y|
|
|
60
|
+
margin + @width.times.map { |x|
|
|
61
|
+
cell_char(@grid[y][x])
|
|
62
|
+
}.join
|
|
63
|
+
}.join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def face_map
|
|
67
|
+
highlight_on = {}
|
|
68
|
+
highlight_off = {}
|
|
69
|
+
offset = 0
|
|
70
|
+
@height.times do |y|
|
|
71
|
+
offset += @margin_left
|
|
72
|
+
@width.times do |x|
|
|
73
|
+
value = @grid[y][x]
|
|
74
|
+
# Priority: explicit set_face > display_option face > nil
|
|
75
|
+
face_name = @faces[y][x]
|
|
76
|
+
if face_name.nil?
|
|
77
|
+
opt = @display_options[value]
|
|
78
|
+
face_name = opt[:face] if opt
|
|
79
|
+
end
|
|
80
|
+
if face_name
|
|
81
|
+
face = Face[face_name]
|
|
82
|
+
if face
|
|
83
|
+
highlight_on[offset] = face
|
|
84
|
+
char_len = cell_char(value).bytesize
|
|
85
|
+
highlight_off[offset + char_len] = true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
offset += cell_char(value).bytesize
|
|
89
|
+
end
|
|
90
|
+
offset += 1 # newline
|
|
91
|
+
end
|
|
92
|
+
[highlight_on, highlight_off]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Timer
|
|
96
|
+
|
|
97
|
+
def start_timer(interval, &callback)
|
|
98
|
+
stop_timer
|
|
99
|
+
@timer_thread = Thread.new do
|
|
100
|
+
loop do
|
|
101
|
+
sleep(interval)
|
|
102
|
+
Controller.current.next_tick(&callback)
|
|
103
|
+
rescue ThreadError
|
|
104
|
+
break
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def stop_timer
|
|
110
|
+
if @timer_thread
|
|
111
|
+
@timer_thread.kill
|
|
112
|
+
@timer_thread = nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def timer_active?
|
|
117
|
+
!@timer_thread.nil? && @timer_thread.alive?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Score persistence
|
|
121
|
+
|
|
122
|
+
def self.score_file_path(game_name)
|
|
123
|
+
safe_name = File.basename(game_name).gsub(/[^A-Za-z0-9_\-]/, "_")
|
|
124
|
+
File.expand_path("~/.textbringer/scores/#{safe_name}.scores")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.add_score(game_name, score, player_name: "anonymous")
|
|
128
|
+
path = score_file_path(game_name)
|
|
129
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
130
|
+
File.open(path, "a") do |f|
|
|
131
|
+
f.puts("#{score}\t#{player_name}\t#{Time.now.iso8601}")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.load_scores(game_name, limit: 10)
|
|
136
|
+
path = score_file_path(game_name)
|
|
137
|
+
return [] unless File.exist?(path)
|
|
138
|
+
lines = File.readlines(path, chomp: true)
|
|
139
|
+
lines.map { |line|
|
|
140
|
+
parts = line.split("\t")
|
|
141
|
+
{ score: parts[0].to_i, player: parts[1], time: parts[2] }
|
|
142
|
+
}.sort_by { |h| -h[:score] }.first(limit)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def check_bounds(x, y)
|
|
148
|
+
if x < 0 || x >= @width || y < 0 || y >= @height
|
|
149
|
+
raise ArgumentError, "coordinates (#{x}, #{y}) out of bounds"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def cell_char(value)
|
|
154
|
+
opt = @display_options[value]
|
|
155
|
+
if opt
|
|
156
|
+
opt[:char]
|
|
157
|
+
elsif value.is_a?(String)
|
|
158
|
+
value
|
|
159
|
+
else
|
|
160
|
+
" "
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/textbringer/keymap.rb
CHANGED
|
@@ -177,6 +177,7 @@ module Textbringer
|
|
|
177
177
|
GLOBAL_MAP.define_key("\C-z", :suspend_textbringer)
|
|
178
178
|
GLOBAL_MAP.define_key("\C-x\C-f", :find_file)
|
|
179
179
|
GLOBAL_MAP.define_key("\C-x\C-r", :find_file_read_only)
|
|
180
|
+
GLOBAL_MAP.define_key("\C-xd", :dired)
|
|
180
181
|
GLOBAL_MAP.define_key("\C-x\C-v", :find_alternate_file)
|
|
181
182
|
GLOBAL_MAP.define_key("\C-xb", :switch_to_buffer)
|
|
182
183
|
GLOBAL_MAP.define_key("\C-x\C-b", :list_buffers)
|
|
@@ -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
data/lib/textbringer/window.rb
CHANGED
|
@@ -141,6 +141,8 @@ module Textbringer
|
|
|
141
141
|
require_relative "faces/basic"
|
|
142
142
|
require_relative "faces/programming"
|
|
143
143
|
require_relative "faces/completion"
|
|
144
|
+
require_relative "faces/dired"
|
|
145
|
+
require_relative "faces/gamegrid"
|
|
144
146
|
end
|
|
145
147
|
|
|
146
148
|
def self.start
|
|
@@ -402,6 +404,10 @@ module Textbringer
|
|
|
402
404
|
def highlight
|
|
403
405
|
@highlight_on = {}
|
|
404
406
|
@highlight_off = {}
|
|
407
|
+
if (override = @buffer[:highlight_override])
|
|
408
|
+
@highlight_on, @highlight_off = override.call
|
|
409
|
+
return
|
|
410
|
+
end
|
|
405
411
|
return if !@@has_colors || !CONFIG[:syntax_highlight] || @buffer.binary?
|
|
406
412
|
syntax_table = @buffer.mode.syntax_table || DEFAULT_SYNTAX_TABLE
|
|
407
413
|
if @buffer.bytesize < CONFIG[:highlight_buffer_size_limit]
|
data/lib/textbringer.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "textbringer/face"
|
|
|
12
12
|
require_relative "textbringer/completion_popup"
|
|
13
13
|
require_relative "textbringer/lsp/client"
|
|
14
14
|
require_relative "textbringer/lsp/server_registry"
|
|
15
|
+
require_relative "textbringer/gamegrid"
|
|
15
16
|
require_relative "textbringer/commands"
|
|
16
17
|
require_relative "textbringer/commands/buffers"
|
|
17
18
|
require_relative "textbringer/commands/windows"
|
|
@@ -33,6 +34,9 @@ require_relative "textbringer/commands/ucs_normalize"
|
|
|
33
34
|
require_relative "textbringer/commands/help"
|
|
34
35
|
require_relative "textbringer/commands/completion"
|
|
35
36
|
require_relative "textbringer/commands/lsp"
|
|
37
|
+
require_relative "textbringer/commands/dired"
|
|
38
|
+
require_relative "textbringer/commands/gamegrid"
|
|
39
|
+
require_relative "textbringer/commands/tetris"
|
|
36
40
|
require_relative "textbringer/mode"
|
|
37
41
|
require_relative "textbringer/modes/fundamental_mode"
|
|
38
42
|
require_relative "textbringer/modes/programming_mode"
|
|
@@ -42,6 +46,9 @@ require_relative "textbringer/modes/backtrace_mode"
|
|
|
42
46
|
require_relative "textbringer/modes/completion_list_mode"
|
|
43
47
|
require_relative "textbringer/modes/buffer_list_mode"
|
|
44
48
|
require_relative "textbringer/modes/help_mode"
|
|
49
|
+
require_relative "textbringer/modes/dired_mode"
|
|
50
|
+
require_relative "textbringer/modes/gamegrid_mode"
|
|
51
|
+
require_relative "textbringer/modes/tetris_mode"
|
|
45
52
|
require_relative "textbringer/minor_mode"
|
|
46
53
|
require_relative "textbringer/global_minor_mode"
|
|
47
54
|
require_relative "textbringer/modes/overwrite_mode"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textbringer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '
|
|
4
|
+
version: '24'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shugo Maeda
|
|
@@ -363,8 +363,10 @@ files:
|
|
|
363
363
|
- lib/textbringer/commands/completion.rb
|
|
364
364
|
- lib/textbringer/commands/ctags.rb
|
|
365
365
|
- lib/textbringer/commands/dabbrev.rb
|
|
366
|
+
- lib/textbringer/commands/dired.rb
|
|
366
367
|
- lib/textbringer/commands/files.rb
|
|
367
368
|
- lib/textbringer/commands/fill.rb
|
|
369
|
+
- lib/textbringer/commands/gamegrid.rb
|
|
368
370
|
- lib/textbringer/commands/help.rb
|
|
369
371
|
- lib/textbringer/commands/input_method.rb
|
|
370
372
|
- lib/textbringer/commands/isearch.rb
|
|
@@ -376,6 +378,7 @@ files:
|
|
|
376
378
|
- lib/textbringer/commands/register.rb
|
|
377
379
|
- lib/textbringer/commands/replace.rb
|
|
378
380
|
- lib/textbringer/commands/server.rb
|
|
381
|
+
- lib/textbringer/commands/tetris.rb
|
|
379
382
|
- lib/textbringer/commands/ucs_normalize.rb
|
|
380
383
|
- lib/textbringer/commands/windows.rb
|
|
381
384
|
- lib/textbringer/completion_popup.rb
|
|
@@ -386,8 +389,11 @@ files:
|
|
|
386
389
|
- lib/textbringer/face.rb
|
|
387
390
|
- lib/textbringer/faces/basic.rb
|
|
388
391
|
- lib/textbringer/faces/completion.rb
|
|
392
|
+
- lib/textbringer/faces/dired.rb
|
|
393
|
+
- lib/textbringer/faces/gamegrid.rb
|
|
389
394
|
- lib/textbringer/faces/programming.rb
|
|
390
395
|
- lib/textbringer/floating_window.rb
|
|
396
|
+
- lib/textbringer/gamegrid.rb
|
|
391
397
|
- lib/textbringer/global_minor_mode.rb
|
|
392
398
|
- lib/textbringer/input_method.rb
|
|
393
399
|
- lib/textbringer/input_methods/hangul_input_method.rb
|
|
@@ -404,11 +410,14 @@ files:
|
|
|
404
410
|
- lib/textbringer/modes/buffer_list_mode.rb
|
|
405
411
|
- lib/textbringer/modes/c_mode.rb
|
|
406
412
|
- lib/textbringer/modes/completion_list_mode.rb
|
|
413
|
+
- lib/textbringer/modes/dired_mode.rb
|
|
407
414
|
- lib/textbringer/modes/fundamental_mode.rb
|
|
415
|
+
- lib/textbringer/modes/gamegrid_mode.rb
|
|
408
416
|
- lib/textbringer/modes/help_mode.rb
|
|
409
417
|
- lib/textbringer/modes/overwrite_mode.rb
|
|
410
418
|
- lib/textbringer/modes/programming_mode.rb
|
|
411
419
|
- lib/textbringer/modes/ruby_mode.rb
|
|
420
|
+
- lib/textbringer/modes/tetris_mode.rb
|
|
412
421
|
- lib/textbringer/modes/transient_mark_mode.rb
|
|
413
422
|
- lib/textbringer/plugin.rb
|
|
414
423
|
- lib/textbringer/ring.rb
|
|
@@ -488,7 +497,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
488
497
|
- !ruby/object:Gem::Version
|
|
489
498
|
version: '0'
|
|
490
499
|
requirements: []
|
|
491
|
-
rubygems_version: 4.0.
|
|
500
|
+
rubygems_version: 4.0.6
|
|
492
501
|
specification_version: 4
|
|
493
502
|
summary: An Emacs-like text editor
|
|
494
503
|
test_files: []
|