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.
@@ -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
- @game = Game.new
13
- @director = Director.new(config)
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
- @director.outro(@game.stats) if active? && @game.evals > 0
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 (p = which("paplay")) && (ENV["PULSE_SERVER"] || !wsl? || File.exist?("/mnt/wslg/PulseServer"))
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 == :fast
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] ||= candidates.find { |c| c.ascii_only? || (utf8? && c.each_char.all? { |ch| char_width(ch) == 1 }) } || candidates.last
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dopairb
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
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
- c.apply_string(ENV["DOPAIRB"]) if ENV["DOPAIRB"]
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` | 端末ベル等を使う場合の明示的な有効化。初期値 `off` |
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.2.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