dopairb 0.2.0 → 0.4.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 +4 -4
- data/README.md +41 -9
- data/lib/dopairb/cli.rb +4 -6
- data/lib/dopairb/config.rb +6 -4
- data/lib/dopairb/demo.rb +9 -2
- data/lib/dopairb/director.rb +35 -5
- data/lib/dopairb/fx.rb +23 -0
- data/lib/dopairb/gallery/impressionism.rb +818 -0
- data/lib/dopairb/gallery/japan.rb +693 -0
- data/lib/dopairb/gallery/modern_ancient.rb +774 -0
- data/lib/dopairb/gallery/renaissance.rb +829 -0
- data/lib/dopairb/gallery/romantic.rb +703 -0
- data/lib/dopairb/gallery.rb +349 -0
- data/lib/dopairb/game.rb +84 -7
- data/lib/dopairb/input_fx.rb +63 -9
- data/lib/dopairb/irb_adapter.rb +8 -0
- data/lib/dopairb/profile.rb +106 -0
- data/lib/dopairb/reline_adapter.rb +1 -0
- data/lib/dopairb/scene.rb +5 -0
- data/lib/dopairb/scenes/bonus.rb +279 -0
- data/lib/dopairb/scenes/failure.rb +1 -0
- data/lib/dopairb/scenes/finale.rb +42 -2
- data/lib/dopairb/scenes/session.rb +21 -3
- data/lib/dopairb/scenes/success.rb +11 -4
- data/lib/dopairb/session.rb +50 -6
- data/lib/dopairb/sound.rb +57 -2
- data/lib/dopairb/term.rb +3 -1
- data/lib/dopairb/tour.rb +89 -0
- data/lib/dopairb/version.rb +1 -1
- data/lib/dopairb.rb +57 -1
- data/spec.md +3 -3
- metadata +10 -1
data/lib/dopairb/session.rb
CHANGED
|
@@ -4,35 +4,66 @@ module Dopairb
|
|
|
4
4
|
# One interactive session: owns the game state and ties the evaluation
|
|
5
5
|
# cycle to the director.
|
|
6
6
|
class Session
|
|
7
|
-
attr_reader :config, :game, :director, :fx
|
|
7
|
+
attr_reader :config, :game, :director, :fx, :collected
|
|
8
8
|
attr_accessor :cold_start
|
|
9
9
|
|
|
10
10
|
def initialize(config)
|
|
11
11
|
@config = config
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# DOPAIRB_SEED makes the luck (crits, jackpots, sparks) repeatable, e.g. for recordings.
|
|
13
|
+
seed = ENV["DOPAIRB_SEED"]
|
|
14
|
+
@game = seed ? Game.new(rng: Random.new(seed.to_i)) : Game.new
|
|
15
|
+
@director = seed ? Director.new(config, rng: Random.new(seed.to_i + 1)) : Director.new(config)
|
|
14
16
|
@fx = InputFx.new(config, @game)
|
|
15
17
|
@finished = false
|
|
18
|
+
@introduced = false
|
|
19
|
+
@profile = nil
|
|
20
|
+
@collected = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Loaded on first use, and only for interactive sessions.
|
|
24
|
+
def profile
|
|
25
|
+
@profile ||= begin
|
|
26
|
+
prof = active? ? Profile.load : Profile.new
|
|
27
|
+
@start = { xp: prof.xp, level: prof.level, best_score: prof.best_score, sessions: prof.sessions }
|
|
28
|
+
@game.career(xp: prof.xp, best_combo: prof.best_combo.positive? ? prof.best_combo : nil) if prof.persistent?
|
|
29
|
+
prof
|
|
30
|
+
end
|
|
16
31
|
end
|
|
17
32
|
|
|
18
33
|
def active?
|
|
19
|
-
!@config.off? && Term.interactive?
|
|
34
|
+
!@config.off? && Term.interactive? && (@config.binding_irb || !IrbAdapter.from_binding?)
|
|
20
35
|
end
|
|
21
36
|
|
|
37
|
+
# Once per process: from the dopairb command before IRB starts, or for
|
|
38
|
+
# `Dopairb.enable` in .irbrc right before the first prompt.
|
|
22
39
|
def intro
|
|
40
|
+
return if @introduced
|
|
41
|
+
@introduced = true
|
|
23
42
|
return unless active?
|
|
43
|
+
prof = profile
|
|
44
|
+
grew = prof.visit
|
|
45
|
+
prof.save if grew
|
|
24
46
|
Sound.warm_up(@config.duration)
|
|
25
47
|
cold = @cold_start || (Sound.available? && !Sound.ready?(:intro, @config.duration))
|
|
26
48
|
if @config.intro && @config.motion && cold
|
|
27
49
|
@director.loading { Sound.ready?(:intro, @config.duration) }
|
|
28
50
|
end
|
|
29
|
-
@director.intro
|
|
51
|
+
@director.intro(prof.persistent? ? { level: prof.level, streak: prof.streak, streak_up: grew && prof.streak >= 2 } : nil)
|
|
30
52
|
end
|
|
31
53
|
|
|
32
54
|
def finish
|
|
33
55
|
return if @finished
|
|
34
56
|
@finished = true
|
|
35
|
-
|
|
57
|
+
return unless active? && @game.evals > 0
|
|
58
|
+
prof = profile
|
|
59
|
+
stats = @game.stats
|
|
60
|
+
if prof.persistent?
|
|
61
|
+
stats = stats.merge(xp_before: @start[:xp], level_before: @start[:level], level: @game.level, xp: @game.xp,
|
|
62
|
+
best_before: @start[:best_score], first_session: @start[:sessions].zero?)
|
|
63
|
+
end
|
|
64
|
+
prof.visit
|
|
65
|
+
@profile = prof.finish(stats, collected: @collected)
|
|
66
|
+
@director.outro(stats.merge(gallery: @profile.gallery.size, gallery_total: Gallery::PIECES.size))
|
|
36
67
|
end
|
|
37
68
|
|
|
38
69
|
def around_eval(code, last_value)
|
|
@@ -68,7 +99,9 @@ module Dopairb
|
|
|
68
99
|
shot = clean ? input : nil
|
|
69
100
|
if outcome == :success
|
|
70
101
|
info = Probe.value(last_value.call, code).merge(out_lines: lines)
|
|
102
|
+
profile
|
|
71
103
|
event = @game.success(info, code: code, out_lines: lines)
|
|
104
|
+
award_art(event) if %i[level_up jackpot art_drop].any? { |f| event.flag?(f) } && @game.leveling?
|
|
72
105
|
pre = elapsed > 0.2 || shot.nil? ? 0.0 : [@config.charge, 0.3].min
|
|
73
106
|
@director.play_event(event, input: shot, pre: pre)
|
|
74
107
|
else
|
|
@@ -81,6 +114,17 @@ module Dopairb
|
|
|
81
114
|
Dopairb.debug(e)
|
|
82
115
|
end
|
|
83
116
|
|
|
117
|
+
# Level-ups, jackpots and lucky drops pay out a masterpiece, a new one while any are missing.
|
|
118
|
+
def award_art(event)
|
|
119
|
+
owned = profile.gallery + @collected
|
|
120
|
+
piece = Gallery.pick(@director.rng, owned)
|
|
121
|
+
fresh = !owned.include?(piece.id)
|
|
122
|
+
@collected << piece.id if fresh
|
|
123
|
+
event.info[:art] = piece.id
|
|
124
|
+
event.info[:art_new] = fresh
|
|
125
|
+
event.info[:gallery] = [(owned + [piece.id]).uniq.size, Gallery::PIECES.size]
|
|
126
|
+
end
|
|
127
|
+
|
|
84
128
|
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
85
129
|
end
|
|
86
130
|
end
|
data/lib/dopairb/sound.rb
CHANGED
|
@@ -181,6 +181,51 @@ module Dopairb
|
|
|
181
181
|
[Synth.seq(sweep, chord), 0.3]
|
|
182
182
|
},
|
|
183
183
|
result: -> { [Synth.seq(Synth.arp([784, 988, 1175, 1568], 0.08, wave: :triangle, vol: 0.2, decay: 0.06), Synth.tone(2093, 0.4, wave: :triangle, vol: 0.18, decay: 0.2)), 0] },
|
|
184
|
+
bits: lambda {
|
|
185
|
+
up = Synth.arp([523, 659, 784, 1047, 1319, 1568, 2093], 0.035, vol: 0.13, decay: 0.03)
|
|
186
|
+
[Synth.seq(up, Synth.tone(2093, 0.18, vol: 0.1, vibrato: 0.02, decay: 0.08)), 0]
|
|
187
|
+
},
|
|
188
|
+
crit: lambda {
|
|
189
|
+
shing = Synth.tone(2400, 0.1, wave: :saw, vol: 0.12, slide_to: 5200, decay: 0.05)
|
|
190
|
+
thud = Synth.mix(Synth.noise(0.12, vol: 0.55, decay: 0.03), Synth.tone(140, 0.25, wave: :sine, vol: 0.7, slide_to: 60, decay: 0.08))
|
|
191
|
+
sting = Synth.mix(*[880, 1109, 1319].map { |f| Synth.tone(f, 0.3, wave: :square, vol: 0.06, decay: 0.12) })
|
|
192
|
+
[Synth.mix(shing, Synth.at(0.1, thud), Synth.at(0.12, sting)), 0.1]
|
|
193
|
+
},
|
|
194
|
+
jackpot: lambda {
|
|
195
|
+
r = Random.new(9)
|
|
196
|
+
buf = []
|
|
197
|
+
t = 0.0
|
|
198
|
+
while t < 0.95
|
|
199
|
+
Synth.add(buf, t, Synth.tone(700 + r.rand(500), 0.02, vol: 0.1, decay: 0.008))
|
|
200
|
+
t += 0.045
|
|
201
|
+
end
|
|
202
|
+
[0.45, 0.7, 0.95].each_with_index do |at, i|
|
|
203
|
+
Synth.add(buf, at, Synth.mix(Synth.noise(0.05, vol: 0.4, decay: 0.015, rng: r), Synth.tone(160 + i * 40, 0.15, wave: :sine, vol: 0.5, slide_to: 80, decay: 0.05)))
|
|
204
|
+
Synth.add(buf, at, Synth.tone([784, 988, 1175][i], 0.12, wave: :triangle, vol: 0.18, decay: 0.06))
|
|
205
|
+
end
|
|
206
|
+
Synth.add(buf, 0.95, Synth.boom(0.8, vol: 0.6, low: 75))
|
|
207
|
+
Synth.add(buf, 1.0, Synth.arp([1047, 1319, 1568, 2093, 1568, 2093, 2637], 0.07, vol: 0.12, decay: 0.05))
|
|
208
|
+
40.times { Synth.add(buf, 1.0 + r.rand * 1.2, Synth.tone(2600 + r.rand(1800), 0.06, wave: :sine, vol: 0.08, decay: 0.02)) }
|
|
209
|
+
[buf, 0.95]
|
|
210
|
+
},
|
|
211
|
+
levelup: lambda {
|
|
212
|
+
buf = []
|
|
213
|
+
Synth.add(buf, 0, Synth.tone(150, 0.22, wave: :saw, vol: 0.12, slide_to: 900))
|
|
214
|
+
Synth.add(buf, 0.22, Synth.boom(0.7, vol: 0.6))
|
|
215
|
+
fan = Synth.arp([523, 659, 784, 1047, 784, 1047, 1319, 1568], 0.06, vol: 0.13, decay: 0.05)
|
|
216
|
+
Synth.add(buf, 0.24, fan)
|
|
217
|
+
[1047, 1319, 1568, 2093].each { |f| Synth.add(buf, 0.72, Synth.tone(f, 0.5, vol: 0.06, vibrato: 0.015, decay: 0.3)) }
|
|
218
|
+
# the unveiling: a gong and a shimmering chord
|
|
219
|
+
Synth.add(buf, 1.0, Synth.tone(98, 1.6, wave: :sine, vol: 0.45, decay: 0.7, vibrato: 0.004))
|
|
220
|
+
Synth.add(buf, 1.0, Synth.tone(196, 1.2, wave: :triangle, vol: 0.12, decay: 0.5))
|
|
221
|
+
Synth.add(buf, 1.45, Synth.noise(0.5, vol: 0.25, decay: 0.15, smooth: 0.5))
|
|
222
|
+
[523, 659, 784, 1047, 1319].each_with_index do |f, i|
|
|
223
|
+
Synth.add(buf, 1.45 + i * 0.05, Synth.tone(f, 1.4, vol: 0.05, vibrato: 0.01, decay: 0.6))
|
|
224
|
+
end
|
|
225
|
+
r = Random.new(4)
|
|
226
|
+
16.times { Synth.add(buf, 1.6 + r.rand * 1.4, Synth.tone(3000 + r.rand(2000), 0.05, wave: :sine, vol: 0.06, decay: 0.015)) }
|
|
227
|
+
[buf, 0.22]
|
|
228
|
+
},
|
|
184
229
|
finale: ->(k = 1.0) { Sound.finale(k) },
|
|
185
230
|
})
|
|
186
231
|
|
|
@@ -260,7 +305,9 @@ module Dopairb
|
|
|
260
305
|
def backend
|
|
261
306
|
return @backend if defined?(@backend)
|
|
262
307
|
@backend =
|
|
263
|
-
if (
|
|
308
|
+
if (log = ENV["DOPAIRB_SOUND_LOG"]) && !log.empty?
|
|
309
|
+
[:log, [log]] # recordings: write "<wall clock of impact> <wav>" lines instead of playing
|
|
310
|
+
elsif (p = which("paplay")) && (ENV["PULSE_SERVER"] || !wsl? || File.exist?("/mnt/wslg/PulseServer"))
|
|
264
311
|
[:fast, [p]]
|
|
265
312
|
elsif (p = which("afplay"))
|
|
266
313
|
[:fast, [p]]
|
|
@@ -278,7 +325,7 @@ module Dopairb
|
|
|
278
325
|
def available? = !backend.nil?
|
|
279
326
|
|
|
280
327
|
# Per-keystroke sounds only make sense with a low-latency player.
|
|
281
|
-
def fast? = backend&.first
|
|
328
|
+
def fast? = %i[fast log].include?(backend&.first)
|
|
282
329
|
|
|
283
330
|
@last = {}
|
|
284
331
|
@children = []
|
|
@@ -294,6 +341,7 @@ module Dopairb
|
|
|
294
341
|
return false if @children.size >= 4
|
|
295
342
|
@last[name] = now
|
|
296
343
|
started = now
|
|
344
|
+
return log_play(argv[0], name, delay, stretch) if kind == :log
|
|
297
345
|
Thread.new do
|
|
298
346
|
Thread.current.report_on_exception = false
|
|
299
347
|
path, impact = file(name, stretch)
|
|
@@ -317,6 +365,13 @@ module Dopairb
|
|
|
317
365
|
false
|
|
318
366
|
end
|
|
319
367
|
|
|
368
|
+
def log_play(log, name, delay, stretch)
|
|
369
|
+
at = Process.clock_gettime(Process::CLOCK_REALTIME) + delay
|
|
370
|
+
path, impact = file(name, stretch)
|
|
371
|
+
File.open(log, "a") { |f| f.puts format("%.4f %s", at - impact, path) }
|
|
372
|
+
true
|
|
373
|
+
end
|
|
374
|
+
|
|
320
375
|
def ready?(name, stretch = 1.0) = !cached(key_for(name, stretch)).nil?
|
|
321
376
|
|
|
322
377
|
# Render every missing sound in the background, the startup sound first.
|
data/lib/dopairb/term.rb
CHANGED
|
@@ -85,9 +85,11 @@ module Dopairb
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# First candidate that renders one column wide on this terminal.
|
|
88
|
+
# A nil candidate means "none of the above": glyph("▀", nil) may be nil.
|
|
88
89
|
def glyph(*candidates)
|
|
89
90
|
@glyphs ||= {}
|
|
90
|
-
@glyphs[candidates]
|
|
91
|
+
return @glyphs[candidates] if @glyphs.key?(candidates)
|
|
92
|
+
@glyphs[candidates] = candidates.find { |c| c.nil? || c.ascii_only? || (utf8? && c.each_char.all? { |ch| char_width(ch) == 1 }) } || candidates.last
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def reset_glyphs
|
data/lib/dopairb/tour.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dopairb
|
|
4
|
+
# `dopa gallery tour`: a full-screen slideshow of the whole collection.
|
|
5
|
+
# Pieces you own are shown in color; the rest are silhouettes to hunt for.
|
|
6
|
+
# → / space: next, ←: back, q: leave. Slides advance on their own.
|
|
7
|
+
module Tour
|
|
8
|
+
SLIDE = 4.0
|
|
9
|
+
REVEAL = 0.35
|
|
10
|
+
FPS = 30.0
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def run(config, owned, start: 0)
|
|
15
|
+
pieces = Gallery::PIECES
|
|
16
|
+
depth = Term.depth(config)
|
|
17
|
+
i = start % pieces.size
|
|
18
|
+
stage = AltStage.new(Term.rows)
|
|
19
|
+
Term.raw do
|
|
20
|
+
stage.open
|
|
21
|
+
begin
|
|
22
|
+
loop do
|
|
23
|
+
step = show(stage, pieces, i, owned, depth, config)
|
|
24
|
+
break if step == :quit
|
|
25
|
+
i = (i + step) % pieces.size
|
|
26
|
+
end
|
|
27
|
+
ensure
|
|
28
|
+
stage.close
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
rescue Interrupt
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Animate one slide; => how many slides to move (+1 on timeout) or :quit.
|
|
36
|
+
def show(stage, pieces, i, owned, depth, config)
|
|
37
|
+
t0 = now
|
|
38
|
+
loop do
|
|
39
|
+
t = now - t0
|
|
40
|
+
stage.draw(frame(pieces[i], i, pieces.size, owned, depth, [t / REVEAL, 1.0].min, t))
|
|
41
|
+
step = read_key(t < REVEAL ? 1 / FPS : [SLIDE * config.duration - t, 0.05].max)
|
|
42
|
+
return step if step
|
|
43
|
+
return 1 if t >= SLIDE * config.duration
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def frame(piece, i, total, owned, depth, reveal, t)
|
|
48
|
+
rows, cols = Term.size
|
|
49
|
+
w = cols - 1
|
|
50
|
+
c = Canvas.new(w, rows)
|
|
51
|
+
have = owned.include?(piece.id)
|
|
52
|
+
star = Term.glyph("✦", "*")
|
|
53
|
+
head = "#{star} DOPA IRB GALLERY #{star} #{i + 1} / #{total} collected #{owned.size}"
|
|
54
|
+
c.put_center(0, head, Color.rainbow(t * 0.3), bold: true)
|
|
55
|
+
aw, ah = Gallery.fit(piece, w - 6, rows - 6)
|
|
56
|
+
ax = (w - aw) / 2
|
|
57
|
+
ay = 2
|
|
58
|
+
frame_col = have ? Color.mix([255, 240, 170], [190, 130, 30], (Math.sin(t * 3) + 1) / 2) : [70, 70, 90]
|
|
59
|
+
c.fill_bg(frame_col, ax - 2, ay - 1, aw + 4, ah / 2 + 2)
|
|
60
|
+
c.fill_bg([20, 20, 28], ax - 1, ay, aw + 2, ah / 2)
|
|
61
|
+
Gallery.draw(c, piece, ax, ay, aw, ah, reveal: reveal, depth: depth) do |top, bot, _x, _r|
|
|
62
|
+
have ? [top, bot] : [shadow(top), shadow(bot)]
|
|
63
|
+
end
|
|
64
|
+
cap = have ? "\"#{piece.title}\" #{piece.artist}, #{piece.year}" : "? ? ? (by #{piece.artist})"
|
|
65
|
+
c.put_center(ay + ah / 2 + 1, cap, have ? [240, 230, 210] : [150, 150, 175], bold: have)
|
|
66
|
+
c.put_center(rows - 1, "->/space next <- back q quit", [120, 120, 140])
|
|
67
|
+
c.render(depth)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Uncollected pieces keep their shapes but lose their colors.
|
|
71
|
+
def shadow(color)
|
|
72
|
+
Color.mix([12, 12, 22], [80, 80, 110], Color.luminance(color) / 255.0)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Keys can arrive several at once (a held arrow key): all of them count.
|
|
76
|
+
# => :quit, a step (+n / -n), or nil when nothing was typed.
|
|
77
|
+
def read_key(timeout)
|
|
78
|
+
return nil unless Term.input.wait_readable(timeout)
|
|
79
|
+
s = Term.input.read_nonblock(256, exception: false)
|
|
80
|
+
return nil unless String === s
|
|
81
|
+
keys = s.scan(/\e[\[O][\d;]*[A-Za-z~]|\e|./m)
|
|
82
|
+
return :quit if keys.any? { |k| ["q", "Q", "\e", "\x03"].include?(k) }
|
|
83
|
+
step = keys.sum { |k| ["\e[D", "\eOD", "h", "p", "b"].include?(k) ? -1 : 1 }
|
|
84
|
+
step.zero? ? nil : step
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/dopairb/version.rb
CHANGED
data/lib/dopairb.rb
CHANGED
|
@@ -19,11 +19,14 @@ require_relative "dopairb/fx"
|
|
|
19
19
|
require_relative "dopairb/stage"
|
|
20
20
|
require_relative "dopairb/game"
|
|
21
21
|
require_relative "dopairb/probe"
|
|
22
|
+
require_relative "dopairb/profile"
|
|
23
|
+
require_relative "dopairb/gallery"
|
|
22
24
|
require_relative "dopairb/scene"
|
|
23
25
|
require_relative "dopairb/scenes/success"
|
|
24
26
|
require_relative "dopairb/scenes/failure"
|
|
25
27
|
require_relative "dopairb/scenes/session"
|
|
26
28
|
require_relative "dopairb/scenes/finale"
|
|
29
|
+
require_relative "dopairb/scenes/bonus"
|
|
27
30
|
require_relative "dopairb/director"
|
|
28
31
|
require_relative "dopairb/output_tap"
|
|
29
32
|
require_relative "dopairb/charge_line"
|
|
@@ -33,6 +36,7 @@ require_relative "dopairb/reline_adapter"
|
|
|
33
36
|
require_relative "dopairb/irb_adapter"
|
|
34
37
|
require_relative "dopairb/session"
|
|
35
38
|
require_relative "dopairb/demo"
|
|
39
|
+
require_relative "dopairb/tour"
|
|
36
40
|
|
|
37
41
|
# dopairb: IRB where every keystroke, result and exception gets a show.
|
|
38
42
|
#
|
|
@@ -44,7 +48,10 @@ module Dopairb
|
|
|
44
48
|
def config
|
|
45
49
|
@config ||= begin
|
|
46
50
|
c = Config.new
|
|
47
|
-
|
|
51
|
+
if ENV["DOPAIRB"]
|
|
52
|
+
c.apply_string(ENV["DOPAIRB"])
|
|
53
|
+
overrides << [:string, ENV["DOPAIRB"]]
|
|
54
|
+
end
|
|
48
55
|
c
|
|
49
56
|
rescue ArgumentError => e
|
|
50
57
|
warn "dopairb: ignoring DOPAIRB=#{ENV['DOPAIRB'].inspect}: #{e.message}"
|
|
@@ -52,10 +59,20 @@ module Dopairb
|
|
|
52
59
|
end
|
|
53
60
|
end
|
|
54
61
|
|
|
62
|
+
# Settings for this run only ($DOPAIRB, dopairb options). They win over
|
|
63
|
+
# Dopairb.enable(...) in .irbrc, which IRB loads after they are applied.
|
|
64
|
+
def overrides = (@overrides ||= [])
|
|
65
|
+
|
|
66
|
+
def override(name, value)
|
|
67
|
+
name == :string ? config.apply_string(value) : config.set(name, value)
|
|
68
|
+
overrides << [name, value]
|
|
69
|
+
end
|
|
70
|
+
|
|
55
71
|
# Hook into IRB. Safe to call more than once; options are config settings.
|
|
56
72
|
def enable(intro: nil, **settings)
|
|
57
73
|
settings.each { |k, v| config.set(k, v) }
|
|
58
74
|
config.set(:intro, intro) unless intro.nil?
|
|
75
|
+
overrides.each { |k, v| k == :string ? config.apply_string(v) : config.set(k, v) }
|
|
59
76
|
@session ||= Session.new(config)
|
|
60
77
|
if IrbAdapter.install
|
|
61
78
|
RelineAdapter.install(@session.fx)
|
|
@@ -98,11 +115,40 @@ module Dopairb
|
|
|
98
115
|
dopa party max intensity, full-screen flash, sound effects
|
|
99
116
|
dopa demo play every effect once
|
|
100
117
|
dopa loading show the loading screen
|
|
118
|
+
dopa gallery the bonus art you have collected (dopa gallery NAME shows one)
|
|
119
|
+
dopa gallery tour a slideshow of all 100 (uncollected ones as silhouettes)
|
|
101
120
|
dopa KEY=VALUE ... change one setting, e.g. `dopa flash=off duration=0.5`
|
|
102
121
|
|
|
103
122
|
settings:
|
|
104
123
|
TXT
|
|
105
124
|
|
|
125
|
+
def gallery(name)
|
|
126
|
+
owned = ((@session&.profile&.gallery || []) + (@session&.collected || [])).uniq
|
|
127
|
+
if name == "tour"
|
|
128
|
+
return puts("dopairb: the tour needs a terminal") unless Term.interactive?
|
|
129
|
+
return Tour.run(config, owned)
|
|
130
|
+
end
|
|
131
|
+
if name.nil?
|
|
132
|
+
puts "dopairb gallery: #{owned.size}/#{Gallery::PIECES.size} (level-ups, jackpots and lucky ART DROPs bring more)"
|
|
133
|
+
Gallery::PIECES.each do |p|
|
|
134
|
+
puts format(" %-32s %s -- %s, %s", p.id, p.title, p.artist, p.year) if owned.include?(p.id)
|
|
135
|
+
end
|
|
136
|
+
rest = Gallery::PIECES.size - owned.size
|
|
137
|
+
puts " ... #{rest} more to discover" if rest > 0
|
|
138
|
+
puts " `dopa gallery NAME` shows one; `dopa gallery tour` walks through all #{Gallery::PIECES.size}"
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
piece = Gallery.find(name.to_sym) if Gallery.ids.include?(name.to_sym)
|
|
142
|
+
return puts("dopairb: no such piece #{name.inspect}") unless piece
|
|
143
|
+
return puts("dopairb: not unlocked yet -- keep playing!") unless owned.include?(piece.id)
|
|
144
|
+
cols = [Term.cols - 1, 100].min
|
|
145
|
+
w, h = Gallery.fit(piece, cols, [Term.rows - 4, 30].min)
|
|
146
|
+
c = Canvas.new(w, h / 2)
|
|
147
|
+
Gallery.draw(c, piece, 0, 0, w, h, depth: Term.depth(config))
|
|
148
|
+
puts c.render(Term.depth(config))
|
|
149
|
+
puts "\"#{piece.title}\" #{piece.artist}, #{piece.year}"
|
|
150
|
+
end
|
|
151
|
+
|
|
106
152
|
def command(arg)
|
|
107
153
|
words = arg.split
|
|
108
154
|
if words.empty? || words == ["status"]
|
|
@@ -111,10 +157,20 @@ module Dopairb
|
|
|
111
157
|
puts config.describe
|
|
112
158
|
puts " sound player: #{Sound.backend ? Sound.backend[1].first : '(none; bell only)'} cache: #{Sound.dir}"
|
|
113
159
|
puts " session: #{s.evals} evals, #{s.successes} hits, #{s.failures} errors, max combo #{s.max_combo}, score #{Fx.number_with_commas(s.score)}" if s
|
|
160
|
+
prof = @session&.profile
|
|
161
|
+
if prof&.persistent?
|
|
162
|
+
lv = s.leveling? ? s.level : prof.level
|
|
163
|
+
xp = s.leveling? ? s.xp : prof.xp
|
|
164
|
+
puts " career: LV #{lv} (#{Fx.number_with_commas(xp)} / #{Fx.number_with_commas(Game.xp_for(lv + 1))} XP), " \
|
|
165
|
+
"best score #{Fx.number_with_commas(prof.best_score)}, best combo #{prof.best_combo}, day streak #{prof.streak}, " \
|
|
166
|
+
"gallery #{prof.gallery.size}/#{Gallery::PIECES.size} (#{prof.path})"
|
|
167
|
+
end
|
|
114
168
|
elsif words == ["help"]
|
|
115
169
|
puts HELP + config.describe
|
|
116
170
|
elsif words == ["demo"]
|
|
117
171
|
Demo.run(self)
|
|
172
|
+
elsif words[0] == "gallery"
|
|
173
|
+
gallery(words[1])
|
|
118
174
|
elsif words == ["loading"]
|
|
119
175
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
120
176
|
@session&.director&.loading { Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 > 2.5 } if active?
|
data/spec.md
CHANGED
|
@@ -127,10 +127,10 @@
|
|
|
127
127
|
|
|
128
128
|
| 設定 | 内容 |
|
|
129
129
|
| --- | --- |
|
|
130
|
-
| `intensity` | `off` / `low` / `normal` / `max`。初期値 `normal` |
|
|
130
|
+
| `intensity` | `off` / `low` / `normal` / `max`。初期値 `max`(v0.4.0 で `normal` から変更) |
|
|
131
131
|
| `motion` | アニメーションの有無と長さ |
|
|
132
132
|
| `flash` | 全画面フラッシュの有無。初期値は控えめ |
|
|
133
|
-
| `sound` |
|
|
133
|
+
| `sound` | `off` / `bell` / `sfx`。初期値 `sfx`(v0.4.0 で `off` から変更。プレイヤーがなければベル) |
|
|
134
134
|
| `hud` | COMBO・CHARGE・SCORE の表示 |
|
|
135
135
|
| `duration` | 各演出時間の倍率 |
|
|
136
136
|
| `color` | 色数と配色。モノクロでも意味が分かる表示 |
|
|
@@ -196,7 +196,7 @@ stateDiagram-v2
|
|
|
196
196
|
|
|
197
197
|
- 常時 HUD を画面上部に固定するか、プロンプト周辺の一時表示にするか。
|
|
198
198
|
- `puts` などの評価途中の出力をどの範囲まで捕捉・装飾するか。
|
|
199
|
-
-
|
|
199
|
+
- ~~スコアをセッション内だけにするか、記録を保存するか。~~ → 生涯 XP・レベル・ベスト記録・連続日数・ボーナス絵を `~/.local/state/dopairb/profile.json` に保存する(`DOPAIRB_PROFILE=off` で無効)。
|
|
200
200
|
- 演出素材を Unicode 文字中心にするか、端末によって別の表現を選ぶか。
|
|
201
201
|
- gem の起動コマンドと、通常の `irb` や `.irbrc` から有効化する方法。
|
|
202
202
|
- IRB/Reline の対象バージョンと、毎キー描画に必要な拡張点。
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dopairb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Koichi Sasada
|
|
@@ -72,13 +72,21 @@ files:
|
|
|
72
72
|
- lib/dopairb/director.rb
|
|
73
73
|
- lib/dopairb/font.rb
|
|
74
74
|
- lib/dopairb/fx.rb
|
|
75
|
+
- lib/dopairb/gallery.rb
|
|
76
|
+
- lib/dopairb/gallery/impressionism.rb
|
|
77
|
+
- lib/dopairb/gallery/japan.rb
|
|
78
|
+
- lib/dopairb/gallery/modern_ancient.rb
|
|
79
|
+
- lib/dopairb/gallery/renaissance.rb
|
|
80
|
+
- lib/dopairb/gallery/romantic.rb
|
|
75
81
|
- lib/dopairb/game.rb
|
|
76
82
|
- lib/dopairb/input_fx.rb
|
|
77
83
|
- lib/dopairb/irb_adapter.rb
|
|
78
84
|
- lib/dopairb/output_tap.rb
|
|
79
85
|
- lib/dopairb/probe.rb
|
|
86
|
+
- lib/dopairb/profile.rb
|
|
80
87
|
- lib/dopairb/reline_adapter.rb
|
|
81
88
|
- lib/dopairb/scene.rb
|
|
89
|
+
- lib/dopairb/scenes/bonus.rb
|
|
82
90
|
- lib/dopairb/scenes/failure.rb
|
|
83
91
|
- lib/dopairb/scenes/finale.rb
|
|
84
92
|
- lib/dopairb/scenes/session.rb
|
|
@@ -87,6 +95,7 @@ files:
|
|
|
87
95
|
- lib/dopairb/sound.rb
|
|
88
96
|
- lib/dopairb/stage.rb
|
|
89
97
|
- lib/dopairb/term.rb
|
|
98
|
+
- lib/dopairb/tour.rb
|
|
90
99
|
- lib/dopairb/version.rb
|
|
91
100
|
- spec.md
|
|
92
101
|
homepage: https://github.com/ko1/dopairb
|