road_to_rubykaigi 0.2.0 → 2026.0.1

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.road_to_rubykaigi.sample +26 -0
  3. data/CHANGELOG.md +4 -0
  4. data/Rakefile +1 -3
  5. data/examples/accelerometer.rb +131 -0
  6. data/lib/road_to_rubykaigi/ansi.rb +8 -0
  7. data/lib/road_to_rubykaigi/calibration_bar.rb +84 -0
  8. data/lib/road_to_rubykaigi/calibration_result.rb +70 -0
  9. data/lib/road_to_rubykaigi/calibration_sampler.rb +52 -0
  10. data/lib/road_to_rubykaigi/calibration_screen.rb +171 -0
  11. data/lib/road_to_rubykaigi/config.rb +164 -0
  12. data/lib/road_to_rubykaigi/event_dispatcher.rb +3 -0
  13. data/lib/road_to_rubykaigi/fireworks.rb +2 -2
  14. data/lib/road_to_rubykaigi/game.rb +2 -0
  15. data/lib/road_to_rubykaigi/game_server.rb +91 -0
  16. data/lib/road_to_rubykaigi/graphics/2026/map.txt +30 -0
  17. data/lib/road_to_rubykaigi/graphics/2026/mask.txt +30 -0
  18. data/lib/road_to_rubykaigi/graphics/map.rb +2 -1
  19. data/lib/road_to_rubykaigi/graphics/mask.rb +1 -1
  20. data/lib/road_to_rubykaigi/graphics/player.rb +78 -48
  21. data/lib/road_to_rubykaigi/graphics/player.txt +4 -0
  22. data/lib/road_to_rubykaigi/jump_detector.rb +125 -0
  23. data/lib/road_to_rubykaigi/manager/audio_manager.rb +10 -3
  24. data/lib/road_to_rubykaigi/manager/game_manager.rb +3 -2
  25. data/lib/road_to_rubykaigi/opening_screen.rb +87 -16
  26. data/lib/road_to_rubykaigi/score_board.rb +6 -1
  27. data/lib/road_to_rubykaigi/serial_reader.rb +76 -0
  28. data/lib/road_to_rubykaigi/signal_interpreter.rb +199 -0
  29. data/lib/road_to_rubykaigi/signal_window.rb +139 -0
  30. data/lib/road_to_rubykaigi/sprite/bonus.rb +83 -45
  31. data/lib/road_to_rubykaigi/sprite/deadline.rb +2 -2
  32. data/lib/road_to_rubykaigi/sprite/enemy.rb +28 -21
  33. data/lib/road_to_rubykaigi/sprite/player.rb +51 -12
  34. data/lib/road_to_rubykaigi/version.rb +1 -1
  35. data/lib/road_to_rubykaigi.rb +42 -19
  36. data/public/controller.html +54 -0
  37. data/public/controller.rb +137 -0
  38. data/public/init.iife.js +122 -0
  39. data/public/picoruby.js +2 -0
  40. data/public/picoruby.wasm +0 -0
  41. data/tmp/.keep +0 -0
  42. metadata +41 -8
  43. data/.standard.yml +0 -29
  44. /data/lib/road_to_rubykaigi/graphics/{demo-map.txt → 2025/demo-map.txt} +0 -0
  45. /data/lib/road_to_rubykaigi/graphics/{demo-mask.txt → 2025/demo-mask.txt} +0 -0
  46. /data/lib/road_to_rubykaigi/graphics/{map.txt → 2025/map.txt} +0 -0
  47. /data/lib/road_to_rubykaigi/graphics/{mask.txt → 2025/mask.txt} +0 -0
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RoadToRubykaigi
4
- VERSION = "0.2.0"
4
+ VERSION = "2026.0.1"
5
5
  end
@@ -4,11 +4,21 @@ require_relative "road_to_rubykaigi/audio/audio_engine"
4
4
  require_relative "road_to_rubykaigi/audio/oscillator"
5
5
  require_relative "road_to_rubykaigi/audio/sequencer"
6
6
  require_relative "road_to_rubykaigi/audio/wav_source"
7
+ require_relative "road_to_rubykaigi/config"
7
8
  require_relative "road_to_rubykaigi/event_dispatcher"
8
9
  require_relative "road_to_rubykaigi/fireworks"
9
10
  require_relative "road_to_rubykaigi/game"
10
11
  require_relative "road_to_rubykaigi/map"
12
+ require_relative "road_to_rubykaigi/calibration_sampler"
13
+ require_relative "road_to_rubykaigi/calibration_bar"
14
+ require_relative "road_to_rubykaigi/calibration_result"
15
+ require_relative "road_to_rubykaigi/calibration_screen"
11
16
  require_relative "road_to_rubykaigi/opening_screen"
17
+ require_relative "road_to_rubykaigi/signal_window"
18
+ require_relative "road_to_rubykaigi/jump_detector"
19
+ require_relative "road_to_rubykaigi/signal_interpreter"
20
+ require_relative "road_to_rubykaigi/game_server"
21
+ require_relative "road_to_rubykaigi/serial_reader"
12
22
  require_relative "road_to_rubykaigi/score_board"
13
23
  require_relative "road_to_rubykaigi/manager/audio_manager"
14
24
  require_relative "road_to_rubykaigi/manager/collision_manager"
@@ -32,31 +42,44 @@ require "io/console"
32
42
  module RoadToRubykaigi
33
43
  class Error < StandardError; end
34
44
  END_POSITION = Map::VIEWPORT_HEIGHT + 2
45
+ VERSIONS = [2026, 2025].freeze
35
46
 
36
- def self.start(game_mode = :normal)
37
- ANSI.cursor_off
38
- at_exit do
39
- print "\e[#{END_POSITION};1H"
40
- ANSI.cursor_on
47
+ class << self
48
+ def start(game_mode = :normal)
49
+ ANSI.cursor_off
50
+ at_exit do
51
+ print "\e[#{END_POSITION};1H"
52
+ ANSI.cursor_on
53
+ end
54
+
55
+ @game_mode = game_mode
56
+ @version = VERSIONS.first
57
+ if demo?
58
+ Game.new.run
59
+ else
60
+ self.version = OpeningScreen.new.display
61
+ Game.new.run
62
+ end
41
63
  end
42
64
 
43
- @game_mode = game_mode
44
- if demo?
45
- Game.new.run
46
- else
47
- OpeningScreen.new.display && Game.new.run
65
+ def demo?
66
+ @game_mode != :normal
48
67
  end
49
- end
50
68
 
51
- def self.demo?
52
- @game_mode != :normal
53
- end
69
+ def version
70
+ @version
71
+ end
54
72
 
55
- def self.debug
56
- @debug ||= []
57
- end
73
+ def version=(version)
74
+ @version = version
75
+ end
76
+
77
+ def debug
78
+ @debug ||= []
79
+ end
58
80
 
59
- def self.debug_add(string)
60
- debug << "\e[#{END_POSITION+debug.size};1H" + string
81
+ def debug_add(string)
82
+ debug << "\e[#{END_POSITION+debug.size};1H" + string
83
+ end
61
84
  end
62
85
  end
@@ -0,0 +1,54 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Road to RubyKaigi Controller</title>
6
+ <style>
7
+ textarea {
8
+ width: 100%;
9
+ height: 240px;
10
+ }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <h1>Road to RubyKaigi Controller</h1>
15
+
16
+ <div>
17
+ <label>Device name prefix</label><br>
18
+ <input id="device-name-prefix" value="RtR" size="30">
19
+ </div>
20
+
21
+ <div>
22
+ <label>Service UUID</label><br>
23
+ <input id="svc-uuid" value="6e400001-b5a3-f393-e0a9-e50e24dcca9e" size="48">
24
+ </div>
25
+
26
+ <div>
27
+ <label>TX UUID (browser → Pico write)</label><br>
28
+ <input id="tx-uuid" value="6e400002-b5a3-f393-e0a9-e50e24dcca9e" size="48">
29
+ </div>
30
+
31
+ <div>
32
+ <label>RX UUID (Pico → browser notify)</label><br>
33
+ <input id="rx-uuid" value="6e400003-b5a3-f393-e0a9-e50e24dcca9e" size="48">
34
+ </div>
35
+
36
+ <div>
37
+ <button id="btn-connect">Connect</button>
38
+ <button id="btn-disconnect" disabled>Disconnect</button>
39
+ </div>
40
+
41
+ <div>
42
+ <label>Status</label><br>
43
+ <div id="status">Disconnected</div>
44
+ </div>
45
+
46
+ <div>
47
+ <label>Log</label><br>
48
+ <textarea id="log" readonly></textarea>
49
+ </div>
50
+
51
+ <script src="./controller.rb" type="text/ruby"></script>
52
+ <script src="./init.iife.js"></script>
53
+ </body>
54
+ </html>
@@ -0,0 +1,137 @@
1
+ require 'js'
2
+
3
+ class Controller
4
+ LOCAL_ENDPOINT = 'http://127.0.0.1:2026/road_to_rubykaigi'
5
+ POLL_INTERVAL_MS = 1000 / 60 # Manager::GameManager::FRAME_RATE
6
+ SAMPLE_SEPARATOR = '|'
7
+
8
+ def initialize
9
+ bind_events
10
+ log('[*] Ready. Click Connect to pair with a BLE UART device.')
11
+ end
12
+
13
+ def bind_events
14
+ element('btn-connect').addEventListener('click') { |_event| connect }
15
+ element('btn-disconnect').addEventListener('click') { |_event| disconnect }
16
+ JS.global.addEventListener('beforeunload') { |_event| force_disconnect }
17
+ end
18
+
19
+ def force_disconnect
20
+ return unless @uart
21
+ @uart.device.js_device[:gatt].disconnect
22
+ rescue
23
+ # best effort: ignore any errors during page unload
24
+ end
25
+
26
+ def element(id)
27
+ @document ||= JS.document
28
+ @document.getElementById(id)
29
+ end
30
+
31
+ def log(message)
32
+ log_element = element('log')
33
+ log_element.textContent = "#{log_element.textContent}#{message}\n"
34
+ log_element.scrollTop = log_element.scrollHeight
35
+ end
36
+
37
+ def connect
38
+ prefix = element('device-name-prefix').value
39
+ svc = element('svc-uuid').value.to_s
40
+ tx = element('tx-uuid').value.to_s
41
+ rx = element('rx-uuid').value.to_s
42
+ log("[*] Connecting UART (svc=#{svc} tx=#{tx} rx=#{rx} prefix=#{prefix})...")
43
+ begin
44
+ @uart = JS::BLE::UART.new(service_uuid: svc, tx_uuid: tx, rx_uuid: rx, name_prefix: prefix)
45
+ @uart.device.js_device.addEventListener('gattserverdisconnected') { |_event| handle_gatt_disconnect }
46
+ log("[+] Connected to: #{@uart.device.name}")
47
+ set_ui(true)
48
+ start_auto_read
49
+ rescue => e
50
+ log("[-] Error: #{e.message}")
51
+ end
52
+ end
53
+
54
+ def handle_gatt_disconnect
55
+ stop_auto_read
56
+ @line_buffer = ''
57
+ @uart = nil
58
+ log('[!] Device disconnected (GATT)')
59
+ set_ui(false)
60
+ end
61
+
62
+ def disconnect
63
+ stop_auto_read
64
+
65
+ return unless @uart
66
+ @uart.close
67
+ @uart = nil
68
+ log('[*] Disconnected')
69
+ set_ui(false)
70
+ end
71
+
72
+ def set_ui(connected)
73
+ if connected
74
+ element('status').textContent = 'Connected'
75
+ element('status').className = 'connected'
76
+ element('btn-connect').setAttribute('disabled', 'true')
77
+ element('btn-disconnect').removeAttribute('disabled')
78
+ else
79
+ element('status').textContent = 'Disconnected'
80
+ element('status').className = 'disconnected'
81
+ element('btn-connect').removeAttribute('disabled')
82
+ element('btn-disconnect').setAttribute('disabled', 'true')
83
+ end
84
+ end
85
+
86
+ def start_auto_read
87
+ @auto_read = true
88
+ schedule_auto_read
89
+ log('[*] Auto receive started')
90
+ end
91
+
92
+ def schedule_auto_read
93
+ return unless @auto_read
94
+
95
+ @read_timer = JS.global.setTimeout(POLL_INTERVAL_MS) do
96
+ poll_uart
97
+ schedule_auto_read
98
+ end
99
+ end
100
+
101
+ def stop_auto_read
102
+ @auto_read = false
103
+
104
+ if @read_timer
105
+ JS.global.clearTimeout(@read_timer)
106
+ @read_timer = nil
107
+ end
108
+ end
109
+
110
+ # Drain the UART buffer completely each tick.
111
+ # Looping until empty prevents buffered samples from compounding
112
+ # into seconds of latency.
113
+ def poll_uart
114
+ @line_buffer ||= ''
115
+ loop do
116
+ data = @uart.read_nonblock(256)
117
+ break if data.nil? || data.empty?
118
+ @line_buffer << data
119
+ end
120
+
121
+ while (idx = @line_buffer.index("\n"))
122
+ line = @line_buffer.slice!(0..idx).strip
123
+ JS.global.console.log("[DATA] #{line}")
124
+ line.split(SAMPLE_SEPARATOR).each { |sample| send_data(sample) }
125
+ end
126
+ end
127
+
128
+ def send_data(line)
129
+ t = JS.global[:Date].now.to_i
130
+ query = line.gsub(',', '&')
131
+ url = "#{LOCAL_ENDPOINT}?#{query}&t=#{t}"
132
+
133
+ JS.global.fetch(url) { |response| JS.global.console.log("[HTTP] GET #{response[:status]}") }
134
+ end
135
+ end
136
+
137
+ Controller.new
@@ -0,0 +1,122 @@
1
+ (async function(global) {
2
+ async function initPicoRuby() {
3
+ // Import the factory function
4
+ const baseURL = document.currentScript?.src ? new URL('.', document.currentScript.src).href : '';
5
+ const { default: createModule } = await import(baseURL + 'picoruby.js');
6
+
7
+ async function collectRubyScripts() {
8
+ const rubyScripts = document.querySelectorAll('script[type="text/ruby"]');
9
+ const taskPromises = Array.from(rubyScripts).map(async script => {
10
+ if (script.src) {
11
+ const response = await fetch(script.src);
12
+ if (!response.ok) {
13
+ throw new Error(`Failed to load ${script.src}: ${response.statusText}`);
14
+ }
15
+ return await response.text();
16
+ }
17
+ return script.textContent.trim();
18
+ });
19
+ return Promise.all(taskPromises);
20
+ }
21
+
22
+ async function collectMrbVMCode() {
23
+ const mrbScripts = document.querySelectorAll('script[type="application/x-mrb"]');
24
+ const taskPromises = Array.from(mrbScripts).map(async script => {
25
+ const src = script.src;
26
+ if (src) {
27
+ const response = await fetch(src);
28
+ if (!response.ok) {
29
+ throw new Error(`Failed to load ${src}: ${response.statusText}`);
30
+ }
31
+ return await response.arrayBuffer();
32
+ }
33
+ return null;
34
+ });
35
+ const results = await Promise.all(taskPromises);
36
+ return results.filter(Boolean);
37
+ }
38
+
39
+ // Create and initialize the module
40
+ const Module = await createModule();
41
+
42
+ // Expose Module for debugging (used by PicoRuby DevTools extension)
43
+ window.picorubyModule = Module;
44
+
45
+ Module.picorubyRun = function() {
46
+ const MRB_TICK_UNIT = 4; // Must match the value in build_config/picoruby-wasm.rb
47
+ const BATCH_DURATION = 16; // ~1 frame (16.67ms)
48
+ const MAX_CATCHUP_TICKS = 10; // Cap to avoid freeze when tab returns from background
49
+ let lastTick = performance.now();
50
+ function run() {
51
+ const now = performance.now();
52
+ let tickCount = 0;
53
+ while (now - lastTick >= MRB_TICK_UNIT && tickCount < MAX_CATCHUP_TICKS) {
54
+ Module._mrb_tick_wasm();
55
+ lastTick += MRB_TICK_UNIT;
56
+ tickCount++;
57
+ }
58
+ if (now - lastTick >= MRB_TICK_UNIT) {
59
+ lastTick = now;
60
+ }
61
+ const sliceStart = performance.now();
62
+ while (performance.now() - sliceStart < BATCH_DURATION) {
63
+ const result = Module._mrb_run_step();
64
+ if (result < 0) {
65
+ console.error('mrb_run_step returned', result, '- scheduler continues');
66
+ }
67
+ }
68
+ setTimeout(run, 0);
69
+ }
70
+ run();
71
+ };
72
+
73
+ // Initialize WASM and start tasks
74
+ Module.ccall('picorb_init', 'number', [], []);
75
+
76
+ // Collect and create tasks from Ruby script tags
77
+ try {
78
+ const rubyTasks = await collectRubyScripts();
79
+ rubyTasks.forEach(function(task) {
80
+ Module.ccall('picorb_create_task', 'number', ['string'], [task]);
81
+ });
82
+ } catch (error) {
83
+ console.error('Error loading Ruby tasks:', error);
84
+ }
85
+
86
+ // Collect and create tasks from MRB data tags
87
+ try {
88
+ const mrbTasks = await collectMrbVMCode();
89
+ mrbTasks.forEach(function(buffer) {
90
+ const ptr = Module._malloc(buffer.byteLength);
91
+ if (ptr === 0) {
92
+ throw new Error('Failed to allocate memory in Wasm heap.');
93
+ }
94
+ try {
95
+ Module.HEAPU8.set(new Uint8Array(buffer), ptr);
96
+ const result = Module.ccall('picorb_create_task_from_mrb', 'number', ['number', 'number'], [ptr, buffer.byteLength]);
97
+ if (result !== 0) {
98
+ console.error('Failed to create task from mrb.');
99
+ }
100
+ } finally {
101
+ Module._free(ptr);
102
+ }
103
+ });
104
+ } catch (error) {
105
+ console.error('Error loading MRB tasks:', error);
106
+ }
107
+
108
+ // Also support window.userTasks if present (for backward compatibility)
109
+ if (window.userTasks) {
110
+ window.userTasks.forEach(function(task) {
111
+ Module.ccall('picorb_create_task', 'number', ['string'], [task]);
112
+ });
113
+ }
114
+
115
+ // Start PicoRuby execution
116
+ Module.picorubyRun();
117
+ }
118
+
119
+ global.initPicoRuby = initPicoRuby;
120
+
121
+ await initPicoRuby();
122
+ })(typeof window !== 'undefined' ? window : this).catch(console.error);