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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83be2be298c392a6739e1fdef9293579796f7d80dd3a5b3aaccc65cf34f60d4c
4
- data.tar.gz: 8afedc6a382a671a5e3dc13720f1934599757cadd6aacb28b88b69cd1986b0ac
3
+ metadata.gz: cbc57c42e0e25caed6e783bf761e9edda9822c1d853fa4d55c144ce6704a875e
4
+ data.tar.gz: a3d81e85abe05052cbbf99907162215b4d27796fd0ed14827d83190c0252672e
5
5
  SHA512:
6
- metadata.gz: 7aed8c09308468b4e3ed6e992cba5d4245d349c66bf88abfa37a4d6a214a1f457dd4610db944f49e5d53c1e024ebfc4bed7bf82fa81ef319d65e158547139633
7
- data.tar.gz: 2c6609aeb7f8939fe95c0278592bf45cc67f68eb52d715e3527837074ecea19dda912505d630715e2a5ef760d630b33a3b778f07a39faba81aef5def8fbe1119
6
+ metadata.gz: 9522cb64cb142280f6ed6070e0e92884206fb0a7eccd1d289202c64ed9cb28a2cbb14d291a686362ce5797654910dec25f91ab2383193e82782cbd9bcceb7943
7
+ data.tar.gz: 6cae99bcfcdcf7419a2e943fda49cbb97f8603a7e7a09d17f23bf5beb165b8ab85e6cea0f99c6d20f0daa6304dcb13341c5ef9f2781e635642003a4e6dd4f11f
data/README.md CHANGED
@@ -3,6 +3,10 @@
3
3
  An interactive Ruby shell that answers every keystroke, evaluation, result and exception with loud terminal effects.
4
4
  It is built as an IRB extension. The design spec is [spec.md](spec.md) (in Japanese).
5
5
 
6
+ https://github.com/user-attachments/assets/b45e4a97-ffde-4fb2-be73-ef69d5c8af42
7
+
8
+ A 70-second session with sound (`dopairb`, the defaults); also in [docs/dopairb-demo.mp4](docs/dopairb-demo.mp4). The scripts that recorded it are in [docs/recording](docs/recording).
9
+
6
10
  ```
7
11
  dopairb(main):001> (1..100).sum COMBO 00 SCORE 33 [⣿⣿⣀⣀⣀⣀⣀⣀] x1.5
8
12
  ⠂⠌⠟⠃ <- sparks, afterglow and HUD on every keystroke
@@ -29,22 +33,29 @@ To try it without installing, run `ruby exe/dopairb` in the checkout.
29
33
  ## Usage
30
34
 
31
35
  ```console
32
- $ dopairb # start it (IRB options are passed through)
36
+ $ dopairb # start it, at full blast with sound (IRB options are passed through)
33
37
  $ dopairb --calm # quieter: low intensity, no flash, shorter effects
34
- $ dopairb --max # everything at maximum; the biggest moments go full screen
35
38
  $ dopairb --party # max + full-screen flash + sound effects
36
- $ dopairb --sound # synthesized sound effects (--sound=bell for the terminal bell)
39
+ $ dopairb --intensity=normal --no-sound # the tamer pre-0.4.0 defaults
37
40
  $ dopairb --no-flash # suppress flashing, available from the very first start
38
41
  $ DOPAIRB="intensity=low,flash=off" dopairb
39
42
  ```
40
43
 
41
- To enable it in your regular `irb`, add this to `~/.irbrc`:
44
+ To turn your regular `irb` into dopairb, add this to `~/.irbrc`:
42
45
 
43
46
  ```ruby
44
- require "dopairb"
45
- Dopairb.enable # also e.g. Dopairb.enable(flash: :off, intensity: :low)
47
+ begin
48
+ require "dopairb"
49
+ Dopairb.enable # also e.g. Dopairb.enable(sound: :off, intensity: :normal)
50
+ rescue LoadError
51
+ # a Ruby without dopairb keeps plain irb
52
+ end
46
53
  ```
47
54
 
55
+ The title (and the loading screen on a cold start) plays right before the first prompt, as with the `dopairb` command.
56
+ Sessions opened by `binding.irb` get the effects too; `Dopairb.enable(binding_irb: false)` keeps them plain.
57
+ Settings for a single run (`dopairb` options and `$DOPAIRB`) win over the ones given to `Dopairb.enable` in `.irbrc`.
58
+
48
59
  Inside a session, the `dopa` command changes settings:
49
60
 
50
61
  ```
@@ -53,6 +64,8 @@ dopa off|low|normal|max
53
64
  dopa calm / dopa party
54
65
  dopa demo play every effect once (does not change your score)
55
66
  dopa loading show the loading screen
67
+ dopa gallery the bonus art you have collected (`dopa gallery starry_night` shows one)
68
+ dopa gallery tour a full-screen slideshow of all 100; uncollected ones appear as silhouettes
56
69
  dopa flash=off duration=0.5 ...
57
70
  ```
58
71
 
@@ -60,10 +73,10 @@ dopa flash=off duration=0.5 ...
60
73
 
61
74
  | Setting | Values | Default | Meaning |
62
75
  | --- | --- | --- | --- |
63
- | `intensity` | off / low / normal / max | normal | Overall amount. `off` behaves exactly like plain IRB |
76
+ | `intensity` | off / low / normal / max | max | Overall amount. `off` behaves exactly like plain IRB |
64
77
  | `motion` | on / off | on | `off` disables animation (badges only) |
65
78
  | `flash` | off / soft / full | soft | `soft` flashes only the effect area, `full` inverts the whole screen |
66
- | `sound` | off / bell / sfx | off | `sfx` plays synthesized sound effects, `bell` rings the terminal bell on big moments |
79
+ | `sound` | off / bell / sfx | sfx | `sfx` plays synthesized sound effects, `bell` rings the terminal bell on big moments |
67
80
  | `hud` | on / off | on | COMBO / SCORE / CHARGE on the right of the prompt line |
68
81
  | `duration` | 0.1–5 | 1.0 | Length multiplier for every effect |
69
82
  | `color` | auto / truecolor / 256 / 16 / none | auto | `none` when `NO_COLOR` is set |
@@ -71,6 +84,7 @@ dopa flash=off duration=0.5 ...
71
84
  | `charge` | 0–0.3 s | 0.12 | Wind-up before a fast result lands; 0 disables it |
72
85
  | `intro` | on / off | on | Title animation at startup |
73
86
  | `keys` | on / off | on | Per-keystroke effects |
87
+ | `binding_irb` | on / off | on | Effects in sessions opened by `binding.irb` too |
74
88
 
75
89
  ## Effects
76
90
 
@@ -80,6 +94,7 @@ When several fire at once, only the biggest plays. The smaller ones are folded i
80
94
  | Moment | Effect |
81
95
  | --- | --- |
82
96
  | Typing | A spark trail right of the cursor, a brief glow on the typed character, embers falling below, a x1.0–x3.0 multiplier |
97
+ | Typing streak of 8, 16, 32 … 1024 keys | Powers of two, in big half-block letters under the input: `8-BIT RUSH!`, `16-BIT COMBO!`, `32-BIT BLAZE!!`, `64-BIT OVERDRIVE!!`, `128-BIT HYPER MODE!!`, `0x100 OVERFLOW!!!`, `2^9 GODSPEED!!!`, `1KiB TYPING LEGEND!!!`, with a bonus of 4 points per key |
83
98
  | Resuming after a pause | Reignites with `CHARGE!` |
84
99
  | Backspace / Delete | The deleted character shatters and falls. The next keystroke shows `RECOVERY` |
85
100
  | Cursor movement | A light streak in the direction of travel |
@@ -104,12 +119,29 @@ When several fire at once, only the biggest plays. The smaller ones are folded i
104
119
  | Other exceptions | A red flash and a glitching `FAILED` |
105
120
  | Ctrl-C | The stored charge scatters, `INTERRUPTED` |
106
121
  | First eval / COMBO 5, 10, 25… / eval count 10, 50, 100… | `FIRST HIT!` / `COMBO 10!` / `100 EVALS` |
122
+ | Lucky success (about 1 in 10, up to 1 in 5 with a full CHARGE) | `CRITICAL!!`: a slash, a red flash and x2 / x4 / x8 points |
123
+ | Very lucky success (1 in 67) | `JACKPOT!!`: three slot reels stop on 7-7-7, coins rain, x16 points |
124
+ | COMBO 10 and beyond | `FEVER TIME!`: every point counts double until the next error; the HUD glows in rainbow `FEVER!` |
125
+ | Beating your all-time best combo | `BEST COMBO!` |
126
+ | Level up, jackpot, or a lucky drop (see [Career](#career)) | `LEVEL UP!` / `JACKPOT PRIZE` / `ART DROP!`, then a famous painting is unveiled full screen as bonus art |
107
127
  | Numeric record (more than doubled) | `NEW RECORD` |
108
128
  | Success right after an error | `FIXED!` (after one error) / `COMEBACK!` (after a streak), both huge |
109
129
  | Exit | A full-screen finale: the title drops, stats slam in one by one, the total score counts up under a drum roll, a rank is stamped (S/A/B/C with a cheerful title), then fireworks. The result is left behind as a plain table |
110
130
 
111
131
  Error effects name the kind of error. They never make fun of the failure.
112
132
 
133
+ ## Career
134
+
135
+ Between sessions dopairb keeps a small profile: lifetime XP (the sum of your scores), level, best score, best combo, a day streak and the bonus art you have collected.
136
+ Levels need powers of two: LV 2 at 512 XP, LV 3 at 1,024, LV 4 at 2,048, and so on.
137
+ Each level-up and each jackpot unveils a masterpiece full screen, and now and then (about 1 success in 50) one simply drops: `ART DROP!`. There are 100 public-domain works to collect, from the Lascaux horses and Hokusai's *Great Wave* through Vermeer, Turner and van Gogh to Klimt, Kandinsky and Mondrian, all painted procedurally in half blocks (no image files ship with the gem). New ones come first; `dopa gallery` lists yours. (Spoilers: all 100 are in [docs/gallery.png](docs/gallery.png).)
138
+ The startup title shows your level and day streak, and the finale shows `NEW BEST!` and an XP bar.
139
+
140
+ The profile is `~/.local/state/dopairb/profile.json` (`$XDG_STATE_HOME` is honored). Set `DOPAIRB_PROFILE=path` to use another file, or `DOPAIRB_PROFILE=off` to keep nothing; levels are then not shown.
141
+ Non-interactive runs never touch it.
142
+
143
+ `DOPAIRB_SEED=n` makes the luck (critical hits, jackpots, particles) repeatable, which helps when recording a demo.
144
+
113
145
  ## Sound
114
146
 
115
147
  With `sound=sfx`, every effect has a sound: key clicks, `NICE!` chimes, a comet ping, a stamp thump, a count-up that ends in a boom, fanfares and explosions for the big moments, a buzz or crack for errors.
@@ -172,7 +204,7 @@ The rest is terminal-independent logic:
172
204
  - Scores live for the session only and are not saved.
173
205
  - Effects use Unicode. Any glyph that is not one column wide is swapped for ASCII or braille at runtime.
174
206
  - Start it with the `dopairb` command, or call `Dopairb.enable` in `.irbrc`.
175
- - IRB sessions entered via `binding.irb` get the same Context hook. The result screen appears only when the outermost session ends.
207
+ - IRB sessions entered via `binding.irb` get the same Context hook (unless `binding_irb=off`; detected with `IRB::Context#from_binding?`). The result screen appears only when the outermost session ends.
176
208
 
177
209
  ## Performance
178
210
 
data/lib/dopairb/cli.rb CHANGED
@@ -15,13 +15,13 @@ module Dopairb
15
15
  o.separator ""
16
16
  o.separator "IRB with fireworks. Every other option is handed to IRB."
17
17
  o.separator ""
18
- o.on("--intensity=LEVEL", Config::INTENSITIES.map(&:to_s), "off / low / normal (default) / max") { |v| settings << [:intensity, v] }
18
+ o.on("--intensity=LEVEL", Config::INTENSITIES.map(&:to_s), "off / low / normal / max (default)") { |v| settings << [:intensity, v] }
19
19
  o.on("--calm", "low intensity, no flash, shorter effects") { settings.concat([[:intensity, :low], [:flash, :off], [:duration, 0.7]]) }
20
- o.on("--max", "everything at maximum") { settings << [:intensity, :max] }
20
+ o.on("--max", "everything at maximum (the default)") { settings << [:intensity, :max] }
21
21
  o.on("--party", "max + full-screen flash + sound effects") { settings.concat([[:intensity, :max], [:flash, :full], [:sound, :sfx]]) }
22
22
  o.on("--[no-]flash[=MODE]", "flash: off / soft (default) / full") { |v| settings << [:flash, v.nil? ? :soft : (v == false ? :off : v)] }
23
23
  o.on("--[no-]motion", "animations (default on)") { |v| settings << [:motion, v] }
24
- o.on("--[no-]sound[=MODE]", "sound: off (default) / bell / sfx") { |v| settings << [:sound, v.nil? ? :sfx : (v == false ? :off : v)] }
24
+ o.on("--[no-]sound[=MODE]", "sound: off / bell / sfx (default)") { |v| settings << [:sound, v.nil? ? :sfx : (v == false ? :off : v)] }
25
25
  o.on("--[no-]hud", "COMBO / SCORE next to the prompt (default on)") { |v| settings << [:hud, v] }
26
26
  o.on("--[no-]keys", "per-keystroke effects (default on)") { |v| settings << [:keys, v] }
27
27
  o.on("--[no-]intro", "title animation (default on)") { |v| settings << [:intro, v] }
@@ -61,9 +61,7 @@ module Dopairb
61
61
  ours, irb_args = split_args(argv.dup)
62
62
  settings = []
63
63
  parser(settings).parse!(ours)
64
- settings.each do |k, v|
65
- k == :string ? Dopairb.config.apply_string(v) : Dopairb.config.set(k, v)
66
- end
64
+ settings.each { |k, v| Dopairb.override(k, v) }
67
65
  run_irb(irb_args)
68
66
  rescue OptionParser::ParseError, ArgumentError => e
69
67
  warn "dopairb: #{e.message}"
@@ -21,15 +21,16 @@ module Dopairb
21
21
  charge: "seconds (max 0.3) of wind-up before a fast result lands; 0 disables",
22
22
  intro: "on / off -- title animation at startup",
23
23
  keys: "on / off -- per-keystroke effects while typing",
24
+ binding_irb: "on / off -- effects in sessions opened by binding.irb too",
24
25
  }.freeze
25
26
 
26
- attr_reader :intensity, :motion, :flash, :sound, :hud, :duration, :color, :trail, :charge, :intro, :keys
27
+ attr_reader :intensity, :motion, :flash, :sound, :hud, :duration, :color, :trail, :charge, :intro, :keys, :binding_irb
27
28
 
28
29
  def initialize
29
- @intensity = :normal
30
+ @intensity = :max
30
31
  @motion = true
31
32
  @flash = :soft
32
- @sound = :off
33
+ @sound = :sfx
33
34
  @hud = true
34
35
  @duration = 1.0
35
36
  @color = :auto
@@ -37,6 +38,7 @@ module Dopairb
37
38
  @charge = 0.12
38
39
  @intro = true
39
40
  @keys = true
41
+ @binding_irb = true
40
42
  end
41
43
 
42
44
  def level
@@ -60,7 +62,7 @@ module Dopairb
60
62
  when :sound
61
63
  v = bool_or_nil(value)
62
64
  @sound = v.nil? ? enum(value, SOUNDS, name) : (v ? :sfx : :off)
63
- when :motion, :hud, :intro, :keys
65
+ when :motion, :hud, :intro, :keys, :binding_irb
64
66
  v = bool_or_nil(value)
65
67
  raise ArgumentError, "#{name} expects on/off, got #{value.inspect}" if v.nil?
66
68
  instance_variable_set(:"@#{name}", v)
data/lib/dopairb/demo.rb CHANGED
@@ -7,8 +7,9 @@ module Dopairb
7
7
 
8
8
  module_function
9
9
 
10
- def ev(kind, info, flags: [], gain: 120, combo: 3, streak: 0)
11
- E.new(kind: kind, tier: nil, info: info, flags: flags, gain: gain, combo: combo, streak: streak, score: 12_480)
10
+ def ev(kind, info, flags: [], gain: 120, combo: 3, streak: 0, mult: 1, level: 5)
11
+ E.new(kind: kind, tier: nil, info: info, flags: flags, gain: gain, combo: combo, streak: streak, score: 12_480,
12
+ mult: mult, level: level)
12
13
  end
13
14
 
14
15
  def cases
@@ -33,6 +34,12 @@ module Dopairb
33
34
  ["1 + 1", ev(:success, { type: :integer, value: 2 }, flags: [:comeback], combo: 1, streak: 5, gain: 600)],
34
35
  ["loop {}", ev(:interrupt, { charge: 60 })],
35
36
  ["2**128", ev(:success, { type: :integer, value: 2**128 }, flags: [:new_record, :record_jump], gain: 420)],
37
+ ["[3, 1, 2].sort", ev(:success, { type: :array, size: 3 }, flags: [:critical], gain: 960, mult: 8)],
38
+ ["12.fdiv(5)", ev(:success, { type: :float, value: 2.4 }, flags: [:fever], combo: 12, gain: 1100, mult: 2)],
39
+ ["21.0 * 2", ev(:success, { type: :float, value: 42.0 }, flags: [:fever, :combo_best], combo: 13, gain: 1200, mult: 2)],
40
+ ["rand(777)", ev(:success, { type: :integer, value: 777 }, flags: [:jackpot], gain: 6_400, mult: 16)],
41
+ ["'level' * 2", ev(:success, { type: :string, length: 10, head: "levellevel", art: :great_wave, art_new: true, gallery: [1, 5] },
42
+ flags: [:level_up], gain: 200, level: 6)],
36
43
  ]
37
44
  end
38
45
 
@@ -6,7 +6,7 @@ module Dopairb
6
6
  class Director
7
7
  FPS = 40.0
8
8
 
9
- attr_reader :config
9
+ attr_reader :config, :rng
10
10
 
11
11
  def initialize(config, rng: Random.new)
12
12
  @config = config
@@ -50,20 +50,35 @@ module Dopairb
50
50
  Scenes::Banner.new(ctx, event, text: text, palette: palette, mega: true, rainbow: rainbow, sub: sub)
51
51
  end
52
52
  end
53
- if event.flag?(:comeback)
53
+ full = lambda { self.ctx(rows: ctx.rows, cols: ctx.w + 1).tap { |c| c.depth = ctx.depth } }
54
+ if event.flag?(:jackpot)
55
+ Scenes::Jackpot.new(ctx, event)
56
+ elsif event.flag?(:level_up) || (event.flag?(:art_drop) && info[:art])
57
+ if @config.motion && @config.level >= 2 && info[:art] && Scenes::Masterpiece.fits?(ctx.rows, ctx.w + 1)
58
+ Scenes::Masterpiece.new(full.(), event)
59
+ elsif event.flag?(:level_up)
60
+ mega.("LEVEL UP!", Color::NEON, rainbow: true, sub: "LV #{event.level}")
61
+ else
62
+ Scenes::Banner.new(ctx, event, text: "ART DROP!", palette: Color::GOLD, sub: Gallery.find(info[:art])&.title)
63
+ end
64
+ elsif event.flag?(:comeback)
54
65
  mega.(event.streak >= 2 ? "COMEBACK!" : "FIXED!", Color::GOLD, rainbow: event.streak >= 2)
55
66
  elsif event.flag?(:eval_milestone)
56
67
  mega.("#{Fx.number_with_commas(event_evals(event))} EVALS", Color::NEON, rainbow: true)
57
68
  elsif event.flag?(:record_jump)
58
69
  mega.("NEW RECORD", Color::GOLD, rainbow: true, sub: number_text(info))
59
70
  elsif event.flag?(:combo_mega)
60
- mega.("COMBO #{event.combo}!", Color::FIRE, rainbow: event.combo >= 50)
71
+ mega.("COMBO #{event.combo}!", Color::FIRE, rainbow: event.combo >= 50, sub: event.flag?(:fever_start) ? "FEVER TIME! ALL POINTS x2" : nil)
72
+ elsif event.flag?(:combo_best)
73
+ Scenes::Banner.new(ctx, event, text: "BEST COMBO!", palette: Color::FIRE, sub: "ALL-TIME BEST #{event.combo}")
61
74
  elsif info[:type] == :definition
62
75
  Scenes::Unlock.new(ctx, event)
63
76
  elsif event.flag?(:combo_milestone) && event.combo >= 5
64
77
  Scenes::Banner.new(ctx, event, text: "COMBO #{event.combo}", palette: Color::FIRE)
65
78
  elsif event.flag?(:first_hit)
66
79
  Scenes::Banner.new(ctx, event, text: "FIRST HIT!", palette: Color::ICE)
80
+ elsif event.flag?(:critical) && !counter?(event)
81
+ Scenes::Critical.new(ctx, event)
67
82
  elsif event.flag?(:output_rain)
68
83
  Scenes::Rain.new(ctx, event)
69
84
  else
@@ -71,6 +86,11 @@ module Dopairb
71
86
  end
72
87
  end
73
88
 
89
+ def counter?(event)
90
+ v = event.info[:value]
91
+ %i[integer float].include?(event.info[:type]) && (v.abs >= 10_000 || event.flag?(:new_record))
92
+ end
93
+
74
94
  def value_scene(event, ctx)
75
95
  info = event.info
76
96
  case info[:type]
@@ -92,9 +112,18 @@ module Dopairb
92
112
  c.input = nil if input && input.size + 12 > c.rows
93
113
  scene = scene_for(event, c)
94
114
  play(scene)
115
+ encore(event, c) if Scenes::Jackpot === scene
95
116
  scene
96
117
  end
97
118
 
119
+ # A jackpot pays out a masterpiece too: too good to fold away.
120
+ def encore(event, c)
121
+ return unless event.info[:art] && @config.motion && @config.level >= 2
122
+ return if Term.input_pending? || !Scenes::Masterpiece.fits?(c.rows, c.w + 1)
123
+ full = ctx(rows: c.rows, cols: c.w + 1).tap { |x| x.depth = c.depth }
124
+ play(Scenes::Masterpiece.new(full, event))
125
+ end
126
+
98
127
  # Cute loading screen until ready.call is true (e.g. the intro sound exists).
99
128
  def loading(&ready)
100
129
  return if @config.off?
@@ -105,9 +134,9 @@ module Dopairb
105
134
  raise if SystemExit === e || (SignalException === e && !(Interrupt === e))
106
135
  end
107
136
 
108
- def intro
137
+ def intro(career = nil)
109
138
  return if @config.off? || !@config.intro
110
- play(Scenes::Intro.new(ctx), force_trail: true)
139
+ play(Scenes::Intro.new(ctx, career), force_trail: true)
111
140
  end
112
141
 
113
142
  def outro(stats)
@@ -132,6 +161,7 @@ module Dopairb
132
161
  rescue Exception => e # rubocop:disable Lint/RescueException
133
162
  raise if SystemExit === e || (SignalException === e && !(Interrupt === e))
134
163
  # Never let an effect break the REPL; Ctrl-C during an effect just skips it.
164
+ Dopairb.debug(e) unless Interrupt === e
135
165
  end
136
166
 
137
167
  private
data/lib/dopairb/fx.rb CHANGED
@@ -111,6 +111,29 @@ module Dopairb
111
111
  width * scale
112
112
  end
113
113
 
114
+ # The big font squeezed into three rows with half blocks (▀ ▄ █).
115
+ # Returns the width drawn, or nil when the terminal lacks the glyphs.
116
+ def half_text(canvas, text, x, y, color_fn)
117
+ up = Term.glyph("▀", nil)
118
+ down = Term.glyph("▄", nil)
119
+ full = Term.glyph("█", nil)
120
+ return nil unless up && down && full
121
+ bm = Font.bitmap(text)
122
+ width = bm[0].size
123
+ 3.times do |r|
124
+ top = bm[r * 2]
125
+ bot = bm[r * 2 + 1] || []
126
+ width.times do |cx|
127
+ a = top[cx]
128
+ b = bot[cx]
129
+ next unless a || b
130
+ ch = a && b ? full : (a ? up : down)
131
+ canvas.put(x + cx, y + r, ch, color_fn.(cx, r, cx.fdiv([width - 1, 1].max)))
132
+ end
133
+ end
134
+ width
135
+ end
136
+
114
137
  def text_fits?(text, w, scale = 1)
115
138
  Font.width(text, scale: scale) <= w
116
139
  end