dopairb 0.1.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/LICENSE +21 -0
- data/README.md +190 -0
- data/exe/dopairb +6 -0
- data/lib/dopairb/canvas.rb +164 -0
- data/lib/dopairb/charge_line.rb +107 -0
- data/lib/dopairb/cli.rb +90 -0
- data/lib/dopairb/color.rb +126 -0
- data/lib/dopairb/config.rb +118 -0
- data/lib/dopairb/demo.rb +61 -0
- data/lib/dopairb/director.rb +190 -0
- data/lib/dopairb/font.rb +91 -0
- data/lib/dopairb/fx.rb +154 -0
- data/lib/dopairb/game.rb +182 -0
- data/lib/dopairb/input_fx.rb +499 -0
- data/lib/dopairb/irb_adapter.rb +65 -0
- data/lib/dopairb/output_tap.rb +94 -0
- data/lib/dopairb/probe.rb +159 -0
- data/lib/dopairb/reline_adapter.rb +247 -0
- data/lib/dopairb/scene.rb +154 -0
- data/lib/dopairb/scenes/failure.rb +306 -0
- data/lib/dopairb/scenes/session.rb +146 -0
- data/lib/dopairb/scenes/success.rb +418 -0
- data/lib/dopairb/session.rb +79 -0
- data/lib/dopairb/stage.rb +129 -0
- data/lib/dopairb/term.rb +131 -0
- data/lib/dopairb/version.rb +5 -0
- data/lib/dopairb.rb +126 -0
- data/spec.md +203 -0
- metadata +111 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
module Scenes
|
|
5
|
+
# Ordinary value: a comet leaves "=>" and lands with a small burst.
|
|
6
|
+
class Hit < Scene
|
|
7
|
+
def top? = !ctx.input.nil?
|
|
8
|
+
def length = 0.42
|
|
9
|
+
|
|
10
|
+
def setup
|
|
11
|
+
@target = [[@w - 30, 34].min, 8].max
|
|
12
|
+
@parts = Fx::Particles.new(@rng)
|
|
13
|
+
pal = combo_palette
|
|
14
|
+
@parts.burst(@target, 0.5, 14 + [event.combo, 20].min, at: 0.16, speed: 26, life: 0.28, palette: pal,
|
|
15
|
+
gravity: 3, angle: 0, arc: Math::PI * 2)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def draw(c, t)
|
|
19
|
+
pal = combo_palette
|
|
20
|
+
hot = t < 0.3 ? Color.mix([255, 255, 255], Color.ramp(pal, 0.2), t / 0.3) : Color.ramp(pal, 0.4)
|
|
21
|
+
c.put(0, 0, "=>", hot, bold: true)
|
|
22
|
+
if t < 0.16
|
|
23
|
+
head = 3 + (@target - 3) * Fx.ease_in(t / 0.16)
|
|
24
|
+
9.times do |i|
|
|
25
|
+
x = head - i
|
|
26
|
+
break if x < 3
|
|
27
|
+
c.put(x, 0, i.zero? ? Term.glyph("✦", "*") : (i < 3 ? "=" : "-"), Color.ramp(pal, i / 9.0), bold: i < 2)
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
p = Fx.phase(t, 0.16, 0.42)
|
|
31
|
+
@parts.draw(c, t)
|
|
32
|
+
txt = "+#{Fx.number_with_commas(event.gain)}"
|
|
33
|
+
col = Color.mix([255, 255, 255], Color.ramp(pal, 0.3), p)
|
|
34
|
+
x = c.put(@target + 2, 0, txt, col, bg: p < 0.25 ? Color.ramp(pal, 0.6) : nil, bold: true)
|
|
35
|
+
c.put(x + 2, 0, "COMBO #{event.combo}", combo_color(t), bold: event.combo >= 5) if event.combo > 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def trail
|
|
40
|
+
paint("=> #{Term.glyph('✦', '*')} +#{Fx.number_with_commas(event.gain)}" + (event.combo > 1 ? " COMBO #{event.combo}" : ""),
|
|
41
|
+
combo_color)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# nil: a swing and a miss, then smoke.
|
|
46
|
+
class Puff < Scene
|
|
47
|
+
def top? = !ctx.input.nil?
|
|
48
|
+
def length = 0.5
|
|
49
|
+
|
|
50
|
+
def setup
|
|
51
|
+
@parts = Fx::Particles.new(@rng)
|
|
52
|
+
18.times do
|
|
53
|
+
@parts.add(12 + @rng.rand * 6, 0.9, (@rng.rand - 0.3) * 10, -1.5 - @rng.rand * 2.5,
|
|
54
|
+
at: 0.12 + @rng.rand * 0.08, life: 0.35, palette: Color::SMOKE)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def draw(c, t)
|
|
59
|
+
c.put(0, 0, "=>", [160, 160, 170], bold: true)
|
|
60
|
+
if t < 0.12
|
|
61
|
+
x = 3 + 14 * Fx.ease_out(t / 0.12)
|
|
62
|
+
c.put(x - 3, 0, "~~~", [200, 200, 210])
|
|
63
|
+
else
|
|
64
|
+
@parts.draw(c, t)
|
|
65
|
+
p = Fx.phase(t, 0.12, 0.5)
|
|
66
|
+
c.put(22, 0, "nil", Color.mix([230, 230, 230], [90, 90, 100], p))
|
|
67
|
+
c.put(27, 0, "whiff...", Color.mix([150, 150, 160], [60, 60, 70], p)) if p > 0.2
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# true / false: an ink stamp slams down.
|
|
73
|
+
class Stamp < Scene
|
|
74
|
+
def height = 3
|
|
75
|
+
def length = 0.6
|
|
76
|
+
def top? = !ctx.input.nil?
|
|
77
|
+
|
|
78
|
+
def setup
|
|
79
|
+
@yes = event.info[:type] == :true
|
|
80
|
+
@label = @yes ? "YES!" : "NO!"
|
|
81
|
+
@ink = @yes ? [30, 190, 90] : [220, 40, 60]
|
|
82
|
+
@x = 4
|
|
83
|
+
@bw = 12
|
|
84
|
+
@parts = Fx::Particles.new(@rng)
|
|
85
|
+
@parts.burst(@x + @bw / 2.0, 1.5, 26, at: 0.14, speed: 30, life: 0.35,
|
|
86
|
+
palette: [Color.mix(@ink, [255, 255, 255], 0.6), @ink, Color.scale(@ink, 0.3)], gravity: 10)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def shake(t) = t.between?(0.14, 0.24) ? (@rng.rand(3) - 1) : 0
|
|
90
|
+
def flash_at = 0.14
|
|
91
|
+
|
|
92
|
+
def draw(c, t)
|
|
93
|
+
if t < 0.14
|
|
94
|
+
p = Fx.ease_in(t / 0.14)
|
|
95
|
+
grow = ((1 - p) * 10).round
|
|
96
|
+
c.fill_bg(Color.scale(@ink, 0.25 + 0.5 * p), @x - grow, 0 - (grow > 4 ? 0 : 0), @bw + grow * 2, 3)
|
|
97
|
+
c.put(@x + (@bw - @label.size) / 2, 1, @label, [255, 255, 255], bold: true) if p > 0.6
|
|
98
|
+
else
|
|
99
|
+
p = Fx.phase(t, 0.14, 0.6)
|
|
100
|
+
@parts.draw(c, t)
|
|
101
|
+
edge = Color.scale(@ink, 0.55)
|
|
102
|
+
c.fill_bg(edge, @x, 0, @bw, 3)
|
|
103
|
+
c.fill_bg(t < 0.2 ? Color.mix(@ink, [255, 255, 255], 0.6) : @ink, @x + 1, 1, @bw - 2, 1)
|
|
104
|
+
c.put(@x + (@bw - @label.size) / 2, 1, @label, [255, 255, 255], bold: true)
|
|
105
|
+
c.put(@x + @bw + 3, 1, @yes ? "true" : "false", Color.mix([255, 255, 255], @ink, p), bold: true)
|
|
106
|
+
c.put(@x + @bw + 10, 1, sub_line, [150, 150, 160]) if p > 0.3 && event.combo > 1
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def trail = paint("[ #{@label} ]", @ink, bold: true)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Integers that deserve it: count up in giant digits, then a shockwave.
|
|
114
|
+
class Counter < Scene
|
|
115
|
+
def big? = true
|
|
116
|
+
def top? = !ctx.input.nil?
|
|
117
|
+
def length = 1.0
|
|
118
|
+
|
|
119
|
+
def setup
|
|
120
|
+
@value = event.info[:value]
|
|
121
|
+
@int = Integer === @value
|
|
122
|
+
@final = @int ? Fx.number_with_commas(@value) : format_float(@value)
|
|
123
|
+
@scale = best_scale(@final)
|
|
124
|
+
@parts = Fx::Particles.new(@rng)
|
|
125
|
+
cx = @w / 2.0
|
|
126
|
+
@parts.burst(cx, 3, 60, at: 0.55, speed: 55, life: 0.5, palette: Color::GOLD, gravity: 14)
|
|
127
|
+
@parts.burst(cx, 3, 20, at: 0.58, speed: 30, life: 0.45, palette: Color::FIRE, gravity: 8,
|
|
128
|
+
glyphs: ["*", "+", "'", "."])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def height = @scale ? 7 : 3
|
|
132
|
+
|
|
133
|
+
def shake(t) = t.between?(0.55, 0.7) ? (@rng.rand(5) - 2) : 0
|
|
134
|
+
def flash_at = 0.55
|
|
135
|
+
|
|
136
|
+
def draw(c, t)
|
|
137
|
+
p = Fx.phase(t, 0, 0.55)
|
|
138
|
+
shown = if p >= 1 then @final
|
|
139
|
+
elsif @int then Fx.number_with_commas((@value * Fx.ease_out(p)).to_i)
|
|
140
|
+
else format_float(@value * Fx.ease_out(p))
|
|
141
|
+
end
|
|
142
|
+
landed = t >= 0.55
|
|
143
|
+
shimmer = (t * 1.6) % 1.0
|
|
144
|
+
if @scale
|
|
145
|
+
x = (@w - Font.width(shown, scale: @scale)) / 2
|
|
146
|
+
fn = lambda do |_rx, ry, f|
|
|
147
|
+
base = landed ? Color.ramp(Color::GOLD, 0.15 + ry * 0.12) : Color.ramp(Color::ICE, 0.2 + ry * 0.1)
|
|
148
|
+
d = (f - shimmer).abs
|
|
149
|
+
d < 0.08 ? Color.mix([255, 255, 255], base, d / 0.08) : base
|
|
150
|
+
end
|
|
151
|
+
Fx.big_text(c, shown, x, 1, fn, scale: @scale)
|
|
152
|
+
Fx.shockwave(c, @w / 2.0, 3, t - 0.55, speed: 70, life: 0.45)
|
|
153
|
+
@parts.draw(c, t)
|
|
154
|
+
c.tint_bg([255, 220, 120], 0.35 * (1 - Fx.phase(t, 0.55, 0.7)), 0, 0, @w, 7) if landed && t < 0.7 && ctx.config.flash != :off
|
|
155
|
+
if landed
|
|
156
|
+
sub = sub_line([digits_label])
|
|
157
|
+
c.put_center(6, sub, Color.ramp(Color::GOLD, 0.2), bold: true)
|
|
158
|
+
end
|
|
159
|
+
else
|
|
160
|
+
c.put(2, 1, "=> ", [255, 220, 120], bold: true)
|
|
161
|
+
c.put(5, 1, shown, landed ? [255, 230, 120] : [140, 220, 255], bold: true)
|
|
162
|
+
@parts.draw(c, t)
|
|
163
|
+
c.put(8 + shown.size, 1, sub_line([digits_label]), [220, 200, 150]) if landed
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def digits_label
|
|
168
|
+
if @int
|
|
169
|
+
"#{@value.abs.to_s.size} DIGITS"
|
|
170
|
+
else
|
|
171
|
+
"FLOAT"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def trail = paint("#{Term.glyph('✦', '*')} #{@final} #{sub_line([digits_label])}", [255, 210, 90], bold: true)
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def format_float(f)
|
|
180
|
+
return f.to_s unless f.finite?
|
|
181
|
+
f.abs >= 1e15 ? format("%.3e", f) : format("%.3f", f).sub(/\.?0+\z/, "")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Long strings: characters pour in while the length counts up.
|
|
186
|
+
class Stream < Scene
|
|
187
|
+
def height = 2
|
|
188
|
+
def length = 0.75
|
|
189
|
+
def top? = !ctx.input.nil?
|
|
190
|
+
|
|
191
|
+
def draw(c, t)
|
|
192
|
+
len = event.info[:length]
|
|
193
|
+
p = Fx.phase(t, 0, 0.5)
|
|
194
|
+
c.put(0, 0, "STRING", [255, 140, 220], bold: true)
|
|
195
|
+
c.put(8, 0, "#{Fx.number_with_commas((len * Fx.ease_out(p)).round)} chars", [255, 255, 255], bold: p >= 1)
|
|
196
|
+
c.put(26, 0, sub_line, [150, 150, 160]) if p >= 1
|
|
197
|
+
head = event.info[:head]
|
|
198
|
+
span = [@w - 4, head.size].min
|
|
199
|
+
span.times do |i|
|
|
200
|
+
arrive = i.fdiv([span, 1].max) * 0.45
|
|
201
|
+
lt = t - arrive
|
|
202
|
+
next if lt < 0
|
|
203
|
+
x = 2 + i + ((@w - i) * (1 - Fx.ease_out(lt / 0.18))).round
|
|
204
|
+
col = lt < 0.12 ? [255, 255, 255] : Color.rainbow(i / 40.0 + t, 0.6)
|
|
205
|
+
c.put(x, 1, head[i], col)
|
|
206
|
+
end
|
|
207
|
+
c.put(2 + span, 1, "...", [120, 120, 130]) if len > span && t > 0.5
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Arrays and hashes: one block per element pops into place.
|
|
212
|
+
class Crate < Scene
|
|
213
|
+
def height = 2
|
|
214
|
+
def length = 0.7
|
|
215
|
+
def top? = !ctx.input.nil?
|
|
216
|
+
|
|
217
|
+
def draw(c, t)
|
|
218
|
+
hash = event.info[:type] == :hash
|
|
219
|
+
size = event.info[:size]
|
|
220
|
+
label = hash ? "HASH" : "ARRAY"
|
|
221
|
+
cell = hash ? 3 : 2
|
|
222
|
+
fit = [(@w - 14) / cell, 1].max
|
|
223
|
+
n = [size, fit].min
|
|
224
|
+
c.put(0, 0, label, hash ? [255, 200, 90] : [110, 220, 255], bold: true)
|
|
225
|
+
c.put(label.size + 1, 0, "x #{Fx.number_with_commas((size * Fx.ease_out(Fx.phase(t, 0, 0.45))).round)}", [255, 255, 255], bold: true)
|
|
226
|
+
c.put(label.size + 12, 0, sub_line, [150, 150, 160]) if t > 0.45
|
|
227
|
+
n.times do |i|
|
|
228
|
+
born = i.fdiv([n, 1].max) * 0.4
|
|
229
|
+
age = t - born
|
|
230
|
+
next if age < 0
|
|
231
|
+
x = 2 + i * cell
|
|
232
|
+
pop = age < 0.06
|
|
233
|
+
if hash
|
|
234
|
+
c.bg(x, 1, pop ? [255, 255, 255] : Color.hsv(30 + i * 7, 0.7, 0.95))
|
|
235
|
+
c.bg(x + 1, 1, pop ? [255, 255, 255] : Color.hsv(200 + i * 7, 0.6, 0.8))
|
|
236
|
+
else
|
|
237
|
+
c.bg(x, 1, pop ? [255, 255, 255] : Color.hsv(180 + i * 9, 0.65, 0.95))
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
c.put(2 + n * cell + 1, 1, "+#{size - n}", [200, 200, 210]) if size > n && t > 0.4
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Big word banners: FIRST HIT, COMBO 10, NEW RECORD, COMEBACK, milestones.
|
|
245
|
+
class Banner < Scene
|
|
246
|
+
def initialize(ctx, event, text:, sub: nil, palette: Color::FIRE, mega: false, rainbow: false, alt: false)
|
|
247
|
+
@alt = alt
|
|
248
|
+
@text = text
|
|
249
|
+
@sub = sub
|
|
250
|
+
@pal = palette
|
|
251
|
+
@mega = mega
|
|
252
|
+
@rainbow = rainbow
|
|
253
|
+
super(ctx, event)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def big? = true
|
|
257
|
+
def mega? = @mega
|
|
258
|
+
def top? = !ctx.input.nil? && !@alt
|
|
259
|
+
def length = @mega ? 1.9 : 1.05
|
|
260
|
+
|
|
261
|
+
def setup
|
|
262
|
+
@scale = best_scale(@text)
|
|
263
|
+
@scale = 1 if @scale == 2 && !@mega && Font.width(@text, scale: 2) > @w * 0.8
|
|
264
|
+
@land = 0.2
|
|
265
|
+
@parts = Fx::Particles.new(@rng)
|
|
266
|
+
@fire = Fx::Particles.new(@rng)
|
|
267
|
+
return unless @scale
|
|
268
|
+
tw = Font.width(@text, scale: @scale)
|
|
269
|
+
@tx = (@w - tw) / 2
|
|
270
|
+
by = text_y + Font::HEIGHT
|
|
271
|
+
10.times do |i|
|
|
272
|
+
@parts.burst(@tx + tw * i / 9.0, by, @mega ? 10 : 6, at: @land, speed: 40, life: 0.55, palette: @pal,
|
|
273
|
+
gravity: 20, angle: -Math::PI / 2, arc: Math::PI * 1.6)
|
|
274
|
+
end
|
|
275
|
+
return unless @mega
|
|
276
|
+
8.times do |i|
|
|
277
|
+
at = 0.35 + i * 0.16
|
|
278
|
+
x = 4 + @rng.rand * (@w - 8)
|
|
279
|
+
y = 1 + @rng.rand * [height - 6, 2].max
|
|
280
|
+
pal = [Color::FIRE, Color::ICE, Color::NEON, Color::TOXIC, Color::GOLD][i % 5]
|
|
281
|
+
@fire.add(x, height.to_f, 0, -(height - y) / 0.15 * 2, at: at - 0.15, life: 0.15, palette: [[255, 255, 255], [255, 200, 120]])
|
|
282
|
+
@fire.burst(x, y, 45, at: at, speed: 45, life: 0.7, palette: pal, gravity: 10, drag: 2.5)
|
|
283
|
+
end
|
|
284
|
+
40.times do
|
|
285
|
+
@fire.add(@rng.rand * @w, -1, (@rng.rand - 0.5) * 4, 2 + @rng.rand * 4, at: 0.3 + @rng.rand * 1.2, life: 1.2,
|
|
286
|
+
palette: [Color.rainbow(@rng.rand), Color.rainbow(@rng.rand)],
|
|
287
|
+
glyph: ["*", "+", "'", ",", Term.glyph("✦", "*")][@rng.rand(5)], gravity: 1.5)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def height
|
|
292
|
+
return ctx.rows if @alt
|
|
293
|
+
return 2 unless @scale
|
|
294
|
+
@mega ? [[ctx.rows - 4, 13].min, 8].max : 7
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def text_y
|
|
298
|
+
@mega ? (height - Font::HEIGHT) / 2 - 1 : 1
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def shake(t)
|
|
302
|
+
return 0 unless t >= @land
|
|
303
|
+
k = 1 - Fx.phase(t, @land, @land + (@mega ? 0.45 : 0.25))
|
|
304
|
+
k > 0 ? ((@rng.rand * 2 - 1) * (@mega ? 4 : 2) * k).round : 0
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def flash_at = @land
|
|
308
|
+
|
|
309
|
+
def draw(c, t)
|
|
310
|
+
return draw_compact(c, t) unless @scale
|
|
311
|
+
drop = t < @land ? (1 - Fx.ease_in(t / @land)) * (text_y + Font::HEIGHT + 1) : 0
|
|
312
|
+
y = text_y - drop.round
|
|
313
|
+
sweep = (t * 1.3) % 1.4 - 0.2
|
|
314
|
+
fn = lambda do |_rx, ry, f|
|
|
315
|
+
base = @rainbow ? Color.rainbow(f * 0.8 - t * 0.9) : Color.ramp(@pal, 0.1 + ry * 0.14)
|
|
316
|
+
d = (f - sweep).abs
|
|
317
|
+
t >= @land && d < 0.07 ? Color.mix([255, 255, 255], base, d / 0.07) : base
|
|
318
|
+
end
|
|
319
|
+
@fire.draw(c, t) if @mega
|
|
320
|
+
Fx.big_text(c, @text, @tx, y, fn, scale: @scale)
|
|
321
|
+
if t >= @land
|
|
322
|
+
Fx.shockwave(c, @w / 2.0, text_y + 2.5, t - @land, speed: @mega ? 90 : 60, palette: @pal, life: 0.5, rings: @mega ? 3 : 2)
|
|
323
|
+
@parts.draw(c, t)
|
|
324
|
+
if ctx.config.flash != :off && t < @land + 0.12
|
|
325
|
+
c.tint_bg(Color.ramp(@pal, 0.1), 0.5 * (1 - Fx.phase(t, @land, @land + 0.12)), 0, 0, @w, height)
|
|
326
|
+
end
|
|
327
|
+
Fx.sparkles(c, t, 7, @mega ? 30 : 10, palette: @pal) if t > @land + 0.2
|
|
328
|
+
sub = banner_sub
|
|
329
|
+
n = (sub.size * Fx.phase(t, @land + 0.1, @land + 0.45)).round
|
|
330
|
+
c.put_center(text_y + Font::HEIGHT + (@mega ? 2 : 0), sub[0, n], Color.ramp(@pal, 0.15), bold: true) if n > 0 && text_y + Font::HEIGHT < height
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def banner_sub
|
|
335
|
+
[@sub, *sub_bits.reject { |b| @text.include?(b) }].compact.join(" ")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def draw_compact(c, t)
|
|
339
|
+
txt = @text.chars.join(" ")
|
|
340
|
+
p = Fx.phase(t, 0, 0.3)
|
|
341
|
+
txt.each_char.with_index do |ch, i|
|
|
342
|
+
col = @rainbow ? Color.rainbow(i / 12.0 - t) : Color.ramp(@pal, (i / [txt.size, 1].max.to_f) * 0.5)
|
|
343
|
+
c.put(1 + i, 0, ch, col, bold: true) if i <= txt.size * p
|
|
344
|
+
end
|
|
345
|
+
c.put(1, 1, banner_sub, Color.ramp(@pal, 0.2)) if t > 0.3
|
|
346
|
+
Fx.sparkles(c, t, 3, 6, palette: @pal)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def trail
|
|
350
|
+
star = Term.glyph("✦", "*")
|
|
351
|
+
s = +"#{star} #{@text} #{star}"
|
|
352
|
+
sub = banner_sub
|
|
353
|
+
s << " " << sub unless sub.empty?
|
|
354
|
+
if @rainbow && depth != :none
|
|
355
|
+
s.each_char.with_index.map { |ch, i| "#{Color.sgr(Color.rainbow(i / 20.0), nil, true, depth)}#{ch}" }.join + "\e[0m"
|
|
356
|
+
else
|
|
357
|
+
paint(s, Color.ramp(@pal, 0.15), bold: true)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Definitions: a new ability unlocks.
|
|
363
|
+
class Unlock < Banner
|
|
364
|
+
def initialize(ctx, event)
|
|
365
|
+
what = event.info[:what]
|
|
366
|
+
text = { method: "NEW ABILITY", class: "NEW CLASS", module: "NEW MODULE" }[what] || "UNLOCKED"
|
|
367
|
+
super(ctx, event, text: text, palette: Color::NEON)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def draw(c, t)
|
|
371
|
+
super
|
|
372
|
+
return unless @scale && t >= @land + 0.15
|
|
373
|
+
name = event.info[:name].to_s
|
|
374
|
+
label = "#{event.info[:what] == :method ? 'def' : event.info[:what]} #{name}"
|
|
375
|
+
n = (label.size * Fx.phase(t, @land + 0.15, @land + 0.5)).round
|
|
376
|
+
x = (@w - label.size - 4) / 2
|
|
377
|
+
c.fill_bg([90, 30, 160], x, 6, label.size + 4, 1)
|
|
378
|
+
c.put(x + 2, 6, label[0, n], [255, 255, 255], bold: true)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def trail
|
|
382
|
+
paint("#{Term.glyph('✦', '*')} #{@text}: #{event.info[:name]} #{sub_line}", Color.ramp(Color::NEON, 0.2), bold: true)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Floods of stdout: digital rain behind a counter.
|
|
387
|
+
class Rain < Scene
|
|
388
|
+
def height = 5
|
|
389
|
+
def length = 0.9
|
|
390
|
+
def big? = true
|
|
391
|
+
|
|
392
|
+
def setup
|
|
393
|
+
@cols = Array.new(@w) { [@rng.rand * 5, 6 + @rng.rand * 14, @rng.rand(3)] }
|
|
394
|
+
@chars = ("0".."9").to_a + ("a".."z").to_a + %w[{ } ( ) < > = ; :]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def draw(c, t)
|
|
398
|
+
@cols.each_with_index do |(off, speed, kind), x|
|
|
399
|
+
next if kind == 2
|
|
400
|
+
head = (t * speed - off) % (height + 6)
|
|
401
|
+
6.times do |k|
|
|
402
|
+
y = (head - k).floor
|
|
403
|
+
next if y < 0 || y >= height
|
|
404
|
+
ch = @chars[(x * 7 + y * 13 + (t * 20).to_i) % @chars.size]
|
|
405
|
+
col = k.zero? ? [230, 255, 230] : Color.ramp(Color::TOXIC, k / 6.0 + 0.1)
|
|
406
|
+
c.put(x, y, ch, col, bold: k.zero?)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
label = " OUTPUT RAIN x#{Fx.number_with_commas(event.info[:out_lines] || 0)} LINES "
|
|
410
|
+
x = (@w - label.size) / 2
|
|
411
|
+
c.fill_bg([0, 40, 10], x, 2, label.size, 1)
|
|
412
|
+
c.put(x, 2, label, [180, 255, 180], bold: true)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def trail = paint("#{Term.glyph('✦', '*')} OUTPUT RAIN x#{event.info[:out_lines]} lines", [120, 230, 140], bold: true)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# One interactive session: owns the game state and ties the evaluation
|
|
5
|
+
# cycle to the director.
|
|
6
|
+
class Session
|
|
7
|
+
attr_reader :config, :game, :director, :fx
|
|
8
|
+
|
|
9
|
+
def initialize(config)
|
|
10
|
+
@config = config
|
|
11
|
+
@game = Game.new
|
|
12
|
+
@director = Director.new(config)
|
|
13
|
+
@fx = InputFx.new(config, @game)
|
|
14
|
+
@finished = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def active?
|
|
18
|
+
!@config.off? && Term.interactive?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def intro
|
|
22
|
+
@director.intro if active?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def finish
|
|
26
|
+
return if @finished
|
|
27
|
+
@finished = true
|
|
28
|
+
@director.outro(@game.stats) if active? && @game.evals > 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def around_eval(code, last_value)
|
|
32
|
+
input = RelineAdapter.take_input(code)
|
|
33
|
+
OutputTap.start
|
|
34
|
+
charge = @config.motion ? ChargeLine.new(@config, charge: @game.charge).start : nil
|
|
35
|
+
t0 = now
|
|
36
|
+
completed = false
|
|
37
|
+
failure = nil
|
|
38
|
+
begin
|
|
39
|
+
result = yield
|
|
40
|
+
completed = true
|
|
41
|
+
result
|
|
42
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
43
|
+
failure = e
|
|
44
|
+
raise
|
|
45
|
+
ensure
|
|
46
|
+
charge&.stop
|
|
47
|
+
lines, = OutputTap.stop
|
|
48
|
+
# a throw (e.g. irb_exit) leaves neither flag set: stay quiet
|
|
49
|
+
if completed
|
|
50
|
+
react(:success, code, last_value, input, lines, now - t0, charge)
|
|
51
|
+
elsif failure && !(SystemExit === failure)
|
|
52
|
+
react(failure, code, last_value, input, lines, now - t0, charge)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def react(outcome, code, last_value, input, lines, elapsed, charge)
|
|
60
|
+
clean = !OutputTap.output?
|
|
61
|
+
shot = clean ? input : nil
|
|
62
|
+
if outcome == :success
|
|
63
|
+
info = Probe.value(last_value.call, code).merge(out_lines: lines)
|
|
64
|
+
event = @game.success(info, code: code, out_lines: lines)
|
|
65
|
+
pre = elapsed > 0.2 || shot.nil? ? 0.0 : [@config.charge, 0.3].min
|
|
66
|
+
@director.play_event(event, input: shot, pre: pre)
|
|
67
|
+
else
|
|
68
|
+
info = Probe.exception(outcome, code)
|
|
69
|
+
event = info[:type] == :interrupt ? @game.interrupted : @game.failure(info)
|
|
70
|
+
@director.play_event(event, input: shot)
|
|
71
|
+
end
|
|
72
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
73
|
+
raise if SystemExit === e
|
|
74
|
+
Dopairb.debug(e)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# A temporary block of terminal rows starting at the cursor. `top` rows
|
|
5
|
+
# already on screen (the submitted input) can be taken over and are
|
|
6
|
+
# restored verbatim on close.
|
|
7
|
+
class Stage
|
|
8
|
+
attr_reader :height, :top, :owner
|
|
9
|
+
|
|
10
|
+
def initialize(height, top: [])
|
|
11
|
+
@height = height
|
|
12
|
+
@top = top
|
|
13
|
+
@owner = Thread.current
|
|
14
|
+
@open = false
|
|
15
|
+
@closed = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def total = @top.size + @height
|
|
19
|
+
def open? = @open && !@closed
|
|
20
|
+
|
|
21
|
+
def open
|
|
22
|
+
Term::LOCK.synchronize do
|
|
23
|
+
buf = +"\e[?25l\r"
|
|
24
|
+
buf << ("\n" * (@height - 1)) if @height > 1
|
|
25
|
+
up = @height - 1 + @top.size
|
|
26
|
+
buf << "\e[#{up}A" if up > 0
|
|
27
|
+
buf << "\r"
|
|
28
|
+
Term.out.write(buf)
|
|
29
|
+
@open = true
|
|
30
|
+
Term.overlay = self
|
|
31
|
+
end
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def draw(lines)
|
|
36
|
+
Term::LOCK.synchronize do
|
|
37
|
+
return unless open?
|
|
38
|
+
Term.out.write(frame(lines))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def close
|
|
43
|
+
Term::LOCK.synchronize do
|
|
44
|
+
return unless open?
|
|
45
|
+
restore
|
|
46
|
+
Term.overlay = nil if Term.overlay.equal?(self)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Another thread is about to print: get off the screen immediately.
|
|
51
|
+
def yield_to_output
|
|
52
|
+
return unless open?
|
|
53
|
+
restore
|
|
54
|
+
Term.overlay = nil if Term.overlay.equal?(self)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def restore
|
|
60
|
+
buf = frame(@top + Array.new(@height, ""))
|
|
61
|
+
buf << "\e[#{@top.size}B" if @top.size > 0
|
|
62
|
+
buf << "\r\e[0m\e[?25h"
|
|
63
|
+
Term.out.write(buf)
|
|
64
|
+
@closed = true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def frame(lines)
|
|
68
|
+
buf = +""
|
|
69
|
+
n = total
|
|
70
|
+
n.times do |i|
|
|
71
|
+
buf << "\r\e[2K" << (lines[i] || "")
|
|
72
|
+
buf << "\e[0m\n" if i < n - 1
|
|
73
|
+
end
|
|
74
|
+
buf << "\e[0m"
|
|
75
|
+
buf << "\e[#{n - 1}A" if n > 1
|
|
76
|
+
buf << "\r"
|
|
77
|
+
buf
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Full-screen stage on the alternate screen: scrollback is left untouched.
|
|
82
|
+
class AltStage
|
|
83
|
+
attr_reader :height, :owner
|
|
84
|
+
|
|
85
|
+
def initialize(height)
|
|
86
|
+
@height = height
|
|
87
|
+
@owner = Thread.current
|
|
88
|
+
@open = false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def top = []
|
|
92
|
+
def open? = @open
|
|
93
|
+
|
|
94
|
+
def open
|
|
95
|
+
Term::LOCK.synchronize do
|
|
96
|
+
Term.out.write("\e[?1049h\e[?25l\e[H\e[2J")
|
|
97
|
+
@open = true
|
|
98
|
+
Term.overlay = self
|
|
99
|
+
end
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def draw(lines)
|
|
104
|
+
Term::LOCK.synchronize do
|
|
105
|
+
return unless @open
|
|
106
|
+
buf = +""
|
|
107
|
+
lines.each_with_index { |l, i| buf << "\e[#{i + 1};1H\e[2K" << l << "\e[0m" }
|
|
108
|
+
Term.out.write(buf)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def close
|
|
113
|
+
Term::LOCK.synchronize { restore }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def yield_to_output
|
|
117
|
+
restore
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def restore
|
|
123
|
+
return unless @open
|
|
124
|
+
Term.out.write("\e[0m\e[?1049l\e[?25h")
|
|
125
|
+
@open = false
|
|
126
|
+
Term.overlay = nil if Term.overlay.equal?(self)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|