teek 0.1.2 → 0.1.3

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: 92cd230fed0f898dcf23f80a0a6058adcf308e14b4a8f7feb6985026be4692e0
4
- data.tar.gz: 698f02315ab27973a05490552fbd6aec21447987a726119d583cbc98f197c486
3
+ metadata.gz: 8fe2f5c480c472d6479ac1b92088a748e1216cb1a15472e9e98fbd3c2e4fd347
4
+ data.tar.gz: c4d2da92180136ca756955258ea59402b27b9019e9d492f0969a7c346dcc053a
5
5
  SHA512:
6
- metadata.gz: b259a6ed4a721cfe5179d53eacffdd67f9e3213af17e81eb9c8e434a8857456bf55e2d2798ccddf0099bfe001294fae2cf187111f9aafa575e84e90a1528314e
7
- data.tar.gz: 20c96ae1cb5f4340737bcd4db2539c01f47f19c7b837bd85fff18ada2d707c4719cabfe0dc14ab1bf986e9b15aee6d61d64883dba4f33f758c3896b9877014c1
6
+ metadata.gz: 6a10e7f92ca08b1385288829cf0188beb408c90cdf52aecb30a36652f2b299954318c4b3a64434ea0158950f05e105db5a289c115dab4506fa91b7ab6e70f26e
7
+ data.tar.gz: 7cd908cf513d6f92cad5c89d04cb49f7bf3fb3e9d7868af4d0d0fbb7db04e2a7a712c3e6e1e2fd107fd4df4b84140b49d720b2357cc9b9452067a8f5b7bfe2f6
data/Rakefile CHANGED
@@ -505,6 +505,7 @@ namespace :docker do
505
505
  puts "Recording #{sample} (#{codec})..."
506
506
  env = "CODEC=#{codec}"
507
507
  env += " NAME=#{name}" if name
508
+ env += " AUDIO=1" if demo['audio']
508
509
  sh "#{env} ./scripts/docker-record.sh #{sample}"
509
510
  end
510
511
 
@@ -336,6 +336,12 @@ interp_initialize(int argc, VALUE *argv, VALUE self)
336
336
  rb_raise(eTclError, "Tk_Init failed: %s", err);
337
337
  }
338
338
 
339
+ /* Hide the Tk console if it was auto-created during Tk_Init.
340
+ * On macOS/Windows, Tk may create a console window depending on
341
+ * how the process was launched. "catch" handles Linux where the
342
+ * console command does not exist. */
343
+ Tcl_Eval(tip->interp, "catch {console hide}");
344
+
339
345
  /* 7. Initialize Tk stubs - after Tk_Init */
340
346
  tk_version = Tk_InitStubs(tip->interp, TK_VERSION, 0);
341
347
  if (tk_version == NULL) {
data/lib/teek/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teek
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/teek.rb CHANGED
@@ -48,13 +48,15 @@ module Teek
48
48
 
49
49
  class App
50
50
  attr_reader :interp, :widgets, :debugger
51
+ attr_writer :_pending_exception # @api private
51
52
 
52
- def initialize(track_widgets: true, debug: false, &block)
53
+ def initialize(title: nil, track_widgets: true, debug: false, &block)
53
54
  @interp = Teek::Interp.new
54
55
  @interp.tcl_eval('package require Tk')
55
56
  hide
56
57
  @widgets = {}
57
58
  @widget_counters = Hash.new(0)
59
+ @_pending_exception = nil
58
60
  debug ||= !!ENV['TEEK_DEBUG']
59
61
  track_widgets = true if debug
60
62
  setup_widget_tracking if track_widgets
@@ -62,6 +64,7 @@ module Teek
62
64
  require_relative 'teek/debugger'
63
65
  @debugger = Teek::Debugger.new(self)
64
66
  end
67
+ set_window_title(title) if title
65
68
  instance_eval(&block) if block
66
69
  end
67
70
 
@@ -119,14 +122,24 @@ module Teek
119
122
 
120
123
  # Schedule a one-shot timer. Calls the block after +ms+ milliseconds.
121
124
  # @param ms [Integer] delay in milliseconds
125
+ # @param on_error [:raise, Proc, nil] error handling strategy:
126
+ # - +:raise+ (default) — exception propagates to Tcl background error handler.
127
+ # - +Proc+ — called with the exception; error is swallowed.
128
+ # - +nil+ — error is silently swallowed.
122
129
  # @yield block to call when the timer fires
123
130
  # @return [String] timer ID, pass to {#after_cancel} to cancel
124
131
  # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/after.htm#M5 after ms
125
- def after(ms, &block)
132
+ def after(ms, on_error: :raise, &block)
126
133
  cb_id = nil
127
134
  cb_id = @interp.register_callback(proc { |*|
128
- block.call
129
- @interp.unregister_callback(cb_id)
135
+ begin
136
+ block.call
137
+ rescue => e
138
+ raise if on_error == :raise
139
+ on_error.call(e) if on_error.is_a?(Proc)
140
+ ensure
141
+ @interp.unregister_callback(cb_id)
142
+ end
130
143
  })
131
144
  after_id = @interp.tcl_eval("after #{ms.to_i} {ruby_callback #{cb_id}}")
132
145
  after_id.instance_variable_set(:@cb_id, cb_id)
@@ -148,6 +161,33 @@ module Teek
148
161
  after_id
149
162
  end
150
163
 
164
+ # Schedule a repeating timer. Calls the block every +ms+ milliseconds
165
+ # until cancelled. The block runs on the main thread in the event loop,
166
+ # so it must be fast (don't block the UI).
167
+ #
168
+ # @param ms [Integer] interval in milliseconds
169
+ # @param on_error [:raise, Proc, nil] error handling strategy:
170
+ # - +:raise+ (default) — cancels the timer and raises the exception
171
+ # from the next call to {#update}.
172
+ # - +Proc+ — called with the exception; timer keeps running.
173
+ # - +nil+ — cancels the timer silently; error stored in {RepeatingTimer#last_error}.
174
+ # @yield block to call on each tick
175
+ # @return [RepeatingTimer] cancel handle
176
+ #
177
+ # @example Basic polling loop
178
+ # timer = app.every(50) { update_display }
179
+ # timer.cancel # stop later
180
+ #
181
+ # @example With error handler (timer keeps running)
182
+ # timer = app.every(100, on_error: ->(e) { log(e) }) { risky_work }
183
+ #
184
+ # @example Silent cancel on error
185
+ # timer = app.every(50, on_error: nil) { maybe_fails }
186
+ # timer.last_error # => check later
187
+ def every(ms, on_error: :raise, &block)
188
+ RepeatingTimer.new(self, ms, on_error: on_error, &block)
189
+ end
190
+
151
191
  # Cancel a pending {#after} or {#after_idle} timer.
152
192
  # @param after_id [String] timer ID returned by {#after} or {#after_idle}
153
193
  # @return [void]
@@ -390,6 +430,10 @@ module Teek
390
430
  # @see https://www.tcl-lang.org/man/tcl8.6/TclCmd/update.htm update
391
431
  def update
392
432
  @interp.tcl_eval('update')
433
+ if (e = @_pending_exception)
434
+ @_pending_exception = nil
435
+ raise e
436
+ end
393
437
  end
394
438
 
395
439
  # Process only pending idle callbacks (e.g. geometry redraws), then return.
@@ -415,6 +459,44 @@ module Teek
415
459
  @interp.tcl_eval("wm withdraw #{window}")
416
460
  end
417
461
 
462
+ # Enable the Tk debug console. The console starts hidden and can be
463
+ # toggled with the given keyboard shortcut (default: F12).
464
+ #
465
+ # The Tk console is a built-in interactive Tcl shell — useful for
466
+ # inspecting variables, running Tcl commands, and debugging widget
467
+ # layouts at runtime. It is available on macOS and Windows only;
468
+ # on Linux this method is a no-op (Linux has the real terminal).
469
+ #
470
+ # @param keybinding [String] Tk event to toggle the console
471
+ # (default: "<F12>")
472
+ # @return [Boolean] true if the console was created, false if
473
+ # unavailable on this platform
474
+ # @example
475
+ # app = Teek::App.new
476
+ # app.add_debug_console # F12 toggles console
477
+ # app.add_debug_console("<F11>") # custom key
478
+ # @see https://www.tcl-lang.org/man/tcl8.6/TkCmd/console.htm console
479
+ def add_debug_console(keybinding = '<F12>')
480
+ @interp.create_console
481
+ @_console_visible = false
482
+
483
+ toggle = proc do |*|
484
+ if @_console_visible
485
+ tcl_eval('console hide')
486
+ @_console_visible = false
487
+ else
488
+ tcl_eval('console show')
489
+ @_console_visible = true
490
+ end
491
+ end
492
+
493
+ command(:bind, '.', keybinding, toggle)
494
+ true
495
+ rescue TclError => e
496
+ warn "Teek: debug console not available on this platform (#{e.message})"
497
+ false
498
+ end
499
+
418
500
  # Set a window's title.
419
501
  # @param title [String] new title
420
502
  # @param window [String] Tk window path
@@ -675,9 +757,122 @@ module Teek
675
757
  "{ruby_callback #{id}}"
676
758
  when Symbol
677
759
  value.to_s
760
+ when Array
761
+ "{#{value.map { |v| tcl_value(v) }.join(' ')}}"
678
762
  else
679
763
  "{#{value}}"
680
764
  end
681
765
  end
682
766
  end
767
+
768
+ # A cancellable repeating timer that fires on the main thread.
769
+ #
770
+ # Created via {App#every}. Reschedules itself after each tick using
771
+ # Tcl's +after+ command. The block runs in the event loop, so it
772
+ # must complete quickly to avoid blocking the UI.
773
+ #
774
+ # Tracks timing drift: if a tick fires significantly late (more than
775
+ # 2x the interval), a warning is printed to stderr. This helps catch
776
+ # blocks that are too slow for the requested interval.
777
+ #
778
+ # @see App#every
779
+ class RepeatingTimer
780
+ # @return [Integer] interval in milliseconds
781
+ attr_reader :interval
782
+
783
+ # @return [Exception, nil] the last error if the timer stopped due to an
784
+ # unhandled exception, nil otherwise
785
+ attr_reader :last_error
786
+
787
+ # @return [Integer] number of ticks that fired late (> 2x interval)
788
+ attr_reader :late_ticks
789
+
790
+ # @api private
791
+ def initialize(app, ms, on_error: nil, &block)
792
+ raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
793
+
794
+ @app = app
795
+ @interval = ms
796
+ @block = block
797
+ @on_error = on_error
798
+ @cancelled = false
799
+ @after_id = nil
800
+ @last_error = nil
801
+ @late_ticks = 0
802
+ @next_expected = nil
803
+ schedule
804
+ end
805
+
806
+ # Stop the timer. Safe to call multiple times.
807
+ # @return [void]
808
+ def cancel
809
+ return if @cancelled
810
+ @cancelled = true
811
+ @app.after_cancel(@after_id) if @after_id
812
+ @after_id = nil
813
+ end
814
+
815
+ # @return [Boolean] true if the timer has been cancelled
816
+ def cancelled?
817
+ @cancelled
818
+ end
819
+
820
+ # Change the interval. Takes effect on the next tick.
821
+ # @param ms [Integer] new interval in milliseconds
822
+ def interval=(ms)
823
+ raise ArgumentError, "interval must be positive, got #{ms}" if ms <= 0
824
+ @interval = ms
825
+ end
826
+
827
+ private
828
+
829
+ def schedule
830
+ return if @cancelled
831
+ @next_expected = now_ms + @interval
832
+ @after_id = @app.after(@interval) { tick }
833
+ end
834
+
835
+ def tick
836
+ return if @cancelled
837
+ check_drift
838
+ @block.call
839
+ schedule
840
+ rescue => e
841
+ @last_error = e
842
+ case @on_error
843
+ when :raise
844
+ @cancelled = true
845
+ # Store on App so it raises from the next app.update call.
846
+ # Don't re-raise here — that would go through rb_protect → bgerror.
847
+ @app._pending_exception = e
848
+ when Proc
849
+ begin
850
+ @on_error.call(e)
851
+ rescue => handler_err
852
+ @last_error = handler_err
853
+ @cancelled = true
854
+ @app._pending_exception = handler_err
855
+ return
856
+ end
857
+ schedule
858
+ when nil
859
+ @cancelled = true
860
+ end
861
+ end
862
+
863
+ def check_drift
864
+ return unless @next_expected
865
+ actual = now_ms
866
+ drift = actual - @next_expected
867
+ if drift > @interval
868
+ @late_ticks += 1
869
+ warn "Teek::RepeatingTimer: tick #{@late_ticks} fired #{drift.round}ms late " \
870
+ "(interval=#{@interval}ms)"
871
+ end
872
+ end
873
+
874
+ def now_ms
875
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
876
+ end
877
+ end
683
878
  end
@@ -0,0 +1,554 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Gamepad Viewer — SDL2 gamepad input visualized with pure Tk widgets.
4
+ #
5
+ # Demonstrates:
6
+ # - Teek::SDL2::Gamepad for controller input (polling + events)
7
+ # - Virtual gamepad wired to keyboard for testing without hardware
8
+ # - Combobox for gamepad selection with hot-plug detection
9
+ # - Canvas overlay highlights on a controller image
10
+ # - Tk label indicators for button/axis state
11
+ # - Periodic polling via app.every (50ms game loop)
12
+ #
13
+ # Controller artwork: "Generic Gamepad Template" by Erratic (CC0)
14
+ # https://opengameart.org/content/generic-gamepad-template
15
+
16
+ require_relative '../../lib/teek'
17
+ require_relative '../../teek-sdl2/lib/teek/sdl2'
18
+
19
+ class GamepadViewer
20
+ GP = Teek::SDL2::Gamepad
21
+
22
+ # Approximate pixel coordinates for button highlights on controller.png (479x310)
23
+ # Format: [center_x, center_y, radius]
24
+ BUTTON_POS = {
25
+ a: [352, 140, 18],
26
+ b: [390, 107, 18],
27
+ x: [315, 107, 18],
28
+ y: [353, 71, 18],
29
+ dpad_up: [127, 74, 12],
30
+ dpad_down: [127, 143, 12],
31
+ dpad_left: [ 93, 111, 12],
32
+ dpad_right: [160, 110, 12],
33
+ left_shoulder: [ 97, 18, 20],
34
+ right_shoulder: [383, 21, 20],
35
+ back: [215, 93, 10],
36
+ start: [262, 95, 10],
37
+ left_stick: [165, 200, 16],
38
+ right_stick: [313, 200, 16],
39
+ guide: [242, 135, 12],
40
+ }.freeze
41
+
42
+ # Center of each analog stick for the position dot
43
+ STICK_CENTER = {
44
+ left: [165, 200],
45
+ right: [313, 200],
46
+ }.freeze
47
+
48
+ # Keyboard → virtual gamepad button mapping
49
+ KEY_MAP_BUTTONS = {
50
+ 'space' => :a,
51
+ 'shift_l' => :b,
52
+ 'shift_r' => :b,
53
+ 'z' => :x,
54
+ 'c' => :y,
55
+ 'return' => :start,
56
+ 'tab' => :back,
57
+ 'q' => :left_shoulder,
58
+ 'e' => :right_shoulder,
59
+ 'up' => :dpad_up,
60
+ 'down' => :dpad_down,
61
+ 'left' => :dpad_left,
62
+ 'right' => :dpad_right,
63
+ 'f' => :left_stick,
64
+ 'g' => :right_stick,
65
+ 'escape' => :guide,
66
+ }.freeze
67
+
68
+ # Keyboard → virtual analog stick mapping
69
+ # WASD → left stick, IJKL → right stick
70
+ KEY_MAP_AXES = {
71
+ 'w' => [:left_y, -GP::AXIS_MAX],
72
+ 's' => [:left_y, GP::AXIS_MAX],
73
+ 'a' => [:left_x, GP::AXIS_MIN],
74
+ 'd' => [:left_x, GP::AXIS_MAX],
75
+ 'i' => [:right_y, -GP::AXIS_MAX],
76
+ 'k' => [:right_y, GP::AXIS_MAX],
77
+ 'j' => [:right_x, GP::AXIS_MIN],
78
+ 'l' => [:right_x, GP::AXIS_MAX],
79
+ }.freeze
80
+
81
+ def initialize(calibrate: false)
82
+ @calibrate = calibrate
83
+ @app = Teek::App.new(title: calibrate ? 'Gamepad Viewer — CALIBRATE' : 'Gamepad Viewer')
84
+ @gamepads = [] # [[display_name, :virtual | device_index], ...]
85
+ @current_gp = nil # opened Gamepad instance
86
+ @current_mode = nil # :virtual or device index
87
+ @keys_held = {} # keysym → true (for virtual stick axes)
88
+ @var_counter = 0 # unique Tcl variable names
89
+ @prev_buttons = {} # btn → pressed (skip UI update when unchanged)
90
+ @prev_axes = {} # ax → value
91
+
92
+ GP.init_subsystem
93
+
94
+ build_ui
95
+ unless @calibrate
96
+ refresh_gamepad_list
97
+ start_poll_loop
98
+ end
99
+ @app.show
100
+ end
101
+
102
+ def run
103
+ @app.mainloop
104
+ ensure
105
+ @poll_timer&.cancel
106
+ @current_gp&.close unless @current_gp&.closed?
107
+ GP.detach_virtual
108
+ GP.shutdown_subsystem
109
+ end
110
+
111
+ private
112
+
113
+ # ── UI construction ────────────────────────────────────────────────────────
114
+
115
+ def build_ui
116
+ # Top bar: gamepad selector
117
+ top = @app.create_widget('ttk::frame')
118
+ top.pack(fill: :x, padx: 8, pady: 4)
119
+
120
+ @app.create_widget('ttk::label', parent: top, text: 'Gamepad:')
121
+ .pack(side: :left)
122
+
123
+ # Tcl variable must exist before combobox references it via -textvariable
124
+ @combo_var = next_var
125
+ set_var(@combo_var, '')
126
+ @combo = @app.create_widget('ttk::combobox', parent: top,
127
+ textvariable: @combo_var,
128
+ state: :readonly, width: 35)
129
+ @combo.pack(side: :left, padx: 4)
130
+ @combo.bind('<<ComboboxSelected>>') { on_gamepad_selected }
131
+
132
+ @app.create_widget('ttk::button', parent: top, text: 'Refresh',
133
+ command: proc { refresh_gamepad_list })
134
+ .pack(side: :left, padx: 4)
135
+
136
+ # Main area: canvas (left) + info panel (right)
137
+ main = @app.create_widget('ttk::frame')
138
+ main.pack(fill: :both, expand: true, padx: 8, pady: 4)
139
+
140
+ build_canvas(main)
141
+ build_info_panel(main)
142
+
143
+ # Status bar (left = status text, right = mouse coords on canvas)
144
+ status_frame = @app.create_widget('ttk::frame')
145
+ status_frame.pack(fill: :x, side: :bottom, padx: 8, pady: 4)
146
+
147
+ @status_var = next_var
148
+ set_var(@status_var, 'Select a gamepad to begin')
149
+ @app.create_widget('ttk::label', parent: status_frame,
150
+ textvariable: @status_var, anchor: :w)
151
+ .pack(fill: :x, side: :left, expand: true)
152
+
153
+ @mouse_var = next_var
154
+ set_var(@mouse_var, 'x: — y: —')
155
+ @app.create_widget('ttk::label', parent: status_frame,
156
+ textvariable: @mouse_var, anchor: :e,
157
+ width: 16, font: 'TkFixedFont')
158
+ .pack(side: :right, padx: [4, 0])
159
+
160
+ # Keyboard bindings for virtual mode (skip in calibrate — it uses its own)
161
+ unless @calibrate
162
+ @app.bind('all', 'KeyPress', :keysym) { |k| on_key_press(k) }
163
+ @app.bind('all', 'KeyRelease', :keysym) { |k| on_key_release(k) }
164
+ end
165
+ end
166
+
167
+ def build_canvas(parent)
168
+ @canvas = @app.create_widget('canvas', parent: parent,
169
+ width: 479, height: 310,
170
+ background: '#2b2b2b', highlightthickness: 0)
171
+ @canvas.pack(side: :left, padx: [0, 8])
172
+
173
+ # Load controller image
174
+ img_path = File.join(__dir__, 'assets', 'controller.png')
175
+ @controller_img = "gp_controller"
176
+ @app.command(:image, :create, :photo, @controller_img, file: img_path)
177
+ @canvas.command(:create, :image, 0, 0, anchor: :nw, image: @controller_img,
178
+ tag: :controller)
179
+
180
+ # Mouse coordinate tracking for tweaking button positions
181
+ @canvas.bind('Motion', :x, :y) do |x, y|
182
+ set_var(@mouse_var, "x:#{x} y:#{y}")
183
+ end
184
+
185
+ # Create highlight ovals for each button
186
+ @highlight_items = {}
187
+ @calibrate_pos = {} # live positions for calibration mode
188
+ BUTTON_POS.each do |btn, (cx, cy, r)|
189
+ color = button_highlight_color(btn)
190
+ tag = "hl_#{btn}"
191
+ @canvas.command(:create, :oval, cx - r, cy - r, cx + r, cy + r,
192
+ fill: color, outline: '', tag: tag,
193
+ state: @calibrate ? :normal : :hidden)
194
+ @highlight_items[btn] = tag
195
+ @calibrate_pos[btn] = [cx, cy, r]
196
+
197
+ if @calibrate
198
+ # Label each highlight so you know which is which
199
+ @canvas.command(:create, :text, cx, cy - r - 8,
200
+ text: btn.to_s, fill: '#ffffff',
201
+ font: 'TkSmallCaptionFont', tag: "lbl_#{btn}")
202
+ end
203
+ end
204
+
205
+ if @calibrate
206
+ setup_calibration_drag
207
+ end
208
+
209
+ # Stick position dots
210
+ @stick_dots = {}
211
+ STICK_CENTER.each do |side, (cx, cy)|
212
+ tag = "stick_#{side}"
213
+ r = 6
214
+ @canvas.command(:create, :oval, cx - r, cy - r, cx + r, cy + r,
215
+ fill: '#00ff88', outline: '#00cc66', width: 2, tag: tag)
216
+ @stick_dots[side] = tag
217
+ end
218
+ end
219
+
220
+ def build_info_panel(parent)
221
+ info = @app.create_widget('ttk::labelframe', parent: parent, text: 'State')
222
+ info.pack(side: :right, fill: :both, expand: true)
223
+
224
+ # Button indicators
225
+ btn_frame = @app.create_widget('ttk::labelframe', parent: info, text: 'Buttons')
226
+ btn_frame.pack(fill: :x, padx: 4, pady: 2)
227
+
228
+ @btn_labels = {}
229
+ GP.buttons.each_slice(4) do |row|
230
+ rf = @app.create_widget('ttk::frame', parent: btn_frame)
231
+ rf.pack(fill: :x)
232
+ row.each do |btn|
233
+ var = next_var
234
+ set_var(var, btn.to_s)
235
+ # Use plain label (not ttk) for background color support
236
+ lbl = @app.create_widget('label', parent: rf,
237
+ textvariable: var,
238
+ width: 12, relief: :groove,
239
+ anchor: :center, padx: 2, pady: 1)
240
+ lbl.pack(side: :left, padx: 1, pady: 1)
241
+ # Store default colors so we can restore them when button is released
242
+ default_bg = lbl.command(:cget, '-background')
243
+ default_fg = lbl.command(:cget, '-foreground')
244
+ @btn_labels[btn] = { var: var, label: lbl,
245
+ default_bg: default_bg, default_fg: default_fg }
246
+ end
247
+ end
248
+
249
+ # Axis readouts
250
+ axis_frame = @app.create_widget('ttk::labelframe', parent: info, text: 'Axes')
251
+ axis_frame.pack(fill: :x, padx: 4, pady: 2)
252
+
253
+ @axis_labels = {}
254
+ GP.axes.each do |ax|
255
+ rf = @app.create_widget('ttk::frame', parent: axis_frame)
256
+ rf.pack(fill: :x)
257
+ @app.create_widget('ttk::label', parent: rf, text: "#{ax}:",
258
+ width: 14, anchor: :e)
259
+ .pack(side: :left)
260
+ var = next_var
261
+ set_var(var, '0')
262
+ @app.create_widget('ttk::label', parent: rf, textvariable: var,
263
+ width: 8, anchor: :w, font: 'TkFixedFont')
264
+ .pack(side: :left, padx: 4)
265
+ @axis_labels[ax] = var
266
+ end
267
+
268
+ # Key help for virtual mode
269
+ help_frame = @app.create_widget('ttk::labelframe', parent: info,
270
+ text: 'Virtual Keys')
271
+ help_frame.pack(fill: :x, padx: 4, pady: 2)
272
+
273
+ help_text = "WASD: L-stick IJKL: R-stick\n" \
274
+ "Arrows: D-pad Space: A\n" \
275
+ "Shift: B Z: X C: Y\n" \
276
+ "Enter: Start Tab: Back\n" \
277
+ "Q/E: Shoulders F/G: Sticks"
278
+ @app.create_widget('ttk::label', parent: help_frame,
279
+ text: help_text, justify: :left)
280
+ .pack(padx: 4, pady: 2)
281
+ end
282
+
283
+ # ── Gamepad selection ──────────────────────────────────────────────────────
284
+
285
+ def refresh_gamepad_list
286
+ @gamepads = [['Virtual (Keyboard)', :virtual]]
287
+
288
+ # Probe device indices for connected gamepads
289
+ 8.times do |i|
290
+ gp = begin; GP.open(i); rescue; nil; end
291
+ next unless gp
292
+ @gamepads << [gp.name, i]
293
+ gp.close
294
+ end
295
+
296
+ values = @gamepads.map(&:first)
297
+ @combo.command(:configure, values: values)
298
+
299
+ # Select first if nothing selected
300
+ current = get_var(@combo_var)
301
+ if current.empty? || !values.include?(current)
302
+ set_var(@combo_var, values.first)
303
+ on_gamepad_selected
304
+ end
305
+ end
306
+
307
+ def on_gamepad_selected
308
+ name = get_var(@combo_var)
309
+ entry = @gamepads.find { |n, _| n == name }
310
+ return unless entry
311
+
312
+ _, mode = entry
313
+ switch_gamepad(mode)
314
+ end
315
+
316
+ def switch_gamepad(mode)
317
+ # Close current
318
+ @current_gp&.close unless @current_gp&.closed?
319
+ @current_gp = nil
320
+ GP.detach_virtual
321
+
322
+ @prev_buttons.clear
323
+ @prev_axes.clear
324
+
325
+ if mode == :virtual
326
+ idx = GP.attach_virtual
327
+ @current_gp = GP.open(idx)
328
+ @current_mode = :virtual
329
+ set_var(@status_var, "Virtual gamepad active \u2014 use keyboard")
330
+ else
331
+ begin
332
+ @current_gp = GP.open(mode)
333
+ @current_mode = mode
334
+ set_var(@status_var, "Connected: #{@current_gp.name}")
335
+ rescue => e
336
+ set_var(@status_var, "Error: #{e.message}")
337
+ @current_mode = nil
338
+ end
339
+ end
340
+ end
341
+
342
+ # ── Keyboard → Virtual gamepad ─────────────────────────────────────────────
343
+
344
+ def on_key_press(keysym)
345
+ return unless @current_mode == :virtual && @current_gp && !@current_gp.closed?
346
+
347
+ key = keysym.downcase
348
+
349
+ if (btn = KEY_MAP_BUTTONS[key])
350
+ @current_gp.set_virtual_button(btn, true)
351
+ end
352
+
353
+ if KEY_MAP_AXES[key]
354
+ @keys_held[key] = true
355
+ update_virtual_axes
356
+ end
357
+ end
358
+
359
+ def on_key_release(keysym)
360
+ return unless @current_mode == :virtual && @current_gp && !@current_gp.closed?
361
+
362
+ key = keysym.downcase
363
+
364
+ if (btn = KEY_MAP_BUTTONS[key])
365
+ @current_gp.set_virtual_button(btn, false)
366
+ end
367
+
368
+ if KEY_MAP_AXES[key]
369
+ @keys_held.delete(key)
370
+ update_virtual_axes
371
+ end
372
+ end
373
+
374
+ def update_virtual_axes
375
+ return unless @current_gp && !@current_gp.closed?
376
+
377
+ # Compute net axis values from held keys
378
+ axes = Hash.new(0)
379
+ @keys_held.each_key do |key|
380
+ ax, val = KEY_MAP_AXES[key]
381
+ axes[ax] = val
382
+ end
383
+
384
+ # Set each axis — reset to 0 if no key held for that axis
385
+ %i[left_x left_y right_x right_y].each do |ax|
386
+ @current_gp.set_virtual_axis(ax, axes[ax])
387
+ end
388
+ end
389
+
390
+ # ── Poll loop ──────────────────────────────────────────────────────────────
391
+
392
+ def start_poll_loop
393
+ @poll_timer = @app.every(50, on_error: ->(e) {
394
+ set_var(@status_var, "Poll error: #{e.message}")
395
+ }) {
396
+ GP.poll_events
397
+ if @current_gp && !@current_gp.closed?
398
+ update_buttons
399
+ update_axes
400
+ end
401
+ }
402
+ end
403
+
404
+ def update_buttons
405
+ GP.buttons.each do |btn|
406
+ pressed = @current_gp.button?(btn)
407
+ next if @prev_buttons[btn] == pressed
408
+
409
+ @prev_buttons[btn] = pressed
410
+ info = @btn_labels[btn]
411
+ next unless info
412
+
413
+ if pressed
414
+ set_var(info[:var], "[ #{btn} ]")
415
+ info[:label].command(:configure,
416
+ background: button_highlight_color(btn),
417
+ foreground: '#ffffff')
418
+ else
419
+ set_var(info[:var], btn.to_s)
420
+ info[:label].command(:configure,
421
+ background: info[:default_bg],
422
+ foreground: info[:default_fg])
423
+ end
424
+
425
+ tag = @highlight_items[btn]
426
+ @canvas.command(:itemconfigure, tag, state: pressed ? :normal : :hidden) if tag
427
+ end
428
+ end
429
+
430
+ def update_axes
431
+ GP.axes.each do |ax|
432
+ val = @current_gp.axis(ax)
433
+ next if @prev_axes[ax] == val
434
+
435
+ @prev_axes[ax] = val
436
+ set_var(@axis_labels[ax], val.to_s.rjust(6))
437
+ end
438
+
439
+ # Only move stick dots when their axes actually changed
440
+ lx = @current_gp.axis(:left_x); ly = @current_gp.axis(:left_y)
441
+ rx = @current_gp.axis(:right_x); ry = @current_gp.axis(:right_y)
442
+ move_stick_dot(:left, lx, ly) if @prev_axes[:left_x] != lx || @prev_axes[:left_y] != ly
443
+ move_stick_dot(:right, rx, ry) if @prev_axes[:right_x] != rx || @prev_axes[:right_y] != ry
444
+ end
445
+
446
+ def move_stick_dot(side, raw_x, raw_y)
447
+ cx, cy = STICK_CENTER[side]
448
+ max_offset = 15.0 # pixels of travel on screen
449
+ nx = raw_x.to_f / GP::AXIS_MAX
450
+ ny = raw_y.to_f / GP::AXIS_MAX
451
+ px = cx + (nx * max_offset)
452
+ py = cy + (ny * max_offset)
453
+ r = 6
454
+ @canvas.command(:coords, @stick_dots[side],
455
+ (px - r).round, (py - r).round,
456
+ (px + r).round, (py + r).round)
457
+ end
458
+
459
+ # ── Helpers ────────────────────────────────────────────────────────────────
460
+
461
+ def button_highlight_color(btn)
462
+ case btn
463
+ when :a then '#22cc44'
464
+ when :b then '#dd3333'
465
+ when :x then '#3388ee'
466
+ when :y then '#ee8822'
467
+ when :dpad_up, :dpad_down, :dpad_left, :dpad_right then '#44aaff'
468
+ when :left_shoulder, :right_shoulder then '#aa66dd'
469
+ when :left_stick, :right_stick then '#00ff88'
470
+ when :back, :start, :guide then '#ffaa22'
471
+ else '#cccccc'
472
+ end
473
+ end
474
+
475
+ # ── Calibration mode ────────────────────────────────────────────────────────
476
+
477
+ def setup_calibration_drag
478
+ @drag_btn = nil
479
+ @drag_offset = [0, 0]
480
+
481
+ @canvas.bind('ButtonPress-1', :x, :y) { |x, y| calibrate_press(x.to_i, y.to_i) }
482
+ @canvas.bind('B1-Motion', :x, :y) { |x, y| calibrate_drag(x.to_i, y.to_i) }
483
+ @canvas.bind('ButtonRelease-1') { @drag_btn = nil }
484
+
485
+ # Print final coordinates to console
486
+ @app.bind('all', 'KeyPress', :keysym) do |k|
487
+ print_calibrated_positions if k.downcase == 'return'
488
+ end
489
+
490
+ set_var(@status_var, 'Drag circles into position. Press Enter to print coordinates.')
491
+ end
492
+
493
+ def calibrate_press(mx, my)
494
+ # Find the closest button highlight to the click
495
+ @drag_btn = nil
496
+ best_dist = 999
497
+ @calibrate_pos.each do |btn, (cx, cy, _r)|
498
+ d = Math.sqrt((mx - cx)**2 + (my - cy)**2)
499
+ if d < best_dist
500
+ best_dist = d
501
+ @drag_btn = btn
502
+ @drag_offset = [mx - cx, my - cy]
503
+ end
504
+ end
505
+ @drag_btn = nil if best_dist > 40
506
+ end
507
+
508
+ def calibrate_drag(mx, my)
509
+ return unless @drag_btn
510
+
511
+ cx = mx - @drag_offset[0]
512
+ cy = my - @drag_offset[1]
513
+ _, _, r = @calibrate_pos[@drag_btn]
514
+ @calibrate_pos[@drag_btn] = [cx, cy, r]
515
+
516
+ tag = @highlight_items[@drag_btn]
517
+ @canvas.command(:coords, tag, cx - r, cy - r, cx + r, cy + r)
518
+ @canvas.command(:coords, "lbl_#{@drag_btn}", cx, cy - r - 8)
519
+ set_var(@mouse_var, "#{@drag_btn}: #{cx},#{cy}")
520
+ end
521
+
522
+ def print_calibrated_positions
523
+ puts "\n# Updated BUTTON_POS — paste into gamepad_viewer.rb"
524
+ puts "BUTTON_POS = {"
525
+ @calibrate_pos.each do |btn, (cx, cy, r)|
526
+ puts " %-17s [%3d, %3d, %2d]," % ["#{btn}:", cx, cy, r]
527
+ end
528
+ puts "}.freeze"
529
+
530
+ # Also print stick centers from the stick buttons
531
+ ls = @calibrate_pos[:left_stick]
532
+ rs = @calibrate_pos[:right_stick]
533
+ puts "\nSTICK_CENTER = {"
534
+ puts " left: [#{ls[0]}, #{ls[1]}],"
535
+ puts " right: [#{rs[0]}, #{rs[1]}],"
536
+ puts "}.freeze"
537
+ end
538
+
539
+ # Tcl variable helpers
540
+ def next_var
541
+ @var_counter += 1
542
+ "::gpv_#{@var_counter}"
543
+ end
544
+
545
+ def set_var(name, value)
546
+ @app.command(:set, name, value)
547
+ end
548
+
549
+ def get_var(name)
550
+ @app.command(:set, name)
551
+ end
552
+ end
553
+
554
+ GamepadViewer.new(calibrate: ARGV.include?('--calibrate')).run
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # teek-record: title=Yet Another Minesweeper, audio=1
2
3
  #
3
4
  # Minesweeper clone built with Teek.
4
5
  #
@@ -13,8 +14,14 @@
13
14
  #
14
15
  # Tile artwork: "Minesweeper Tile Set" by eugeneloza (CC0)
15
16
  # https://opengameart.org/content/minesweeper-tile-set
17
+ #
18
+ # Sound effects: generated with jsfxr (https://sfxr.me), public domain
19
+ # Music: "Vaporware" by The Cynic Project (CC0)
20
+ # https://opengameart.org/content/calm-piano-1-vaporware
21
+ # cynicmusic.com / pixelsphere.org
16
22
 
17
23
  require_relative '../../lib/teek'
24
+ require_relative '../../teek-sdl2/lib/teek/sdl2'
18
25
 
19
26
  class Minesweeper
20
27
  # The source PNGs are 216x216. Tk can shrink them with "copy -subsample N N"
@@ -28,15 +35,22 @@ class Minesweeper
28
35
  expert: { cols: 30, rows: 16, mines: 99 }
29
36
  }.freeze
30
37
 
38
+ attr_reader :app
39
+
31
40
  def initialize(app, level: :beginner)
32
41
  @app = app
33
42
  @level = level
34
43
  apply_level
35
44
  load_images
45
+ load_sounds
36
46
  build_ui
37
47
  new_game
38
48
  end
39
49
 
50
+ # Simulate press/release on a cell for demo/test automation.
51
+ def press_cell(r, c) = on_left_press(r, c)
52
+ def release_cell(r, c) = on_left_release(r, c)
53
+
40
54
  private
41
55
 
42
56
  # -- Setup ---------------------------------------------------------------
@@ -83,10 +97,21 @@ class Minesweeper
83
97
  end
84
98
  end
85
99
 
100
+ def load_sounds
101
+ dir = File.join(__dir__, 'assets')
102
+ @snd_click = Teek::SDL2::Sound.new(File.join(dir, 'click.wav'))
103
+ @snd_sweep = Teek::SDL2::Sound.new(File.join(dir, 'sweep.wav'))
104
+ @snd_flag = Teek::SDL2::Sound.new(File.join(dir, 'flag.wav'))
105
+ @snd_explosion = Teek::SDL2::Sound.new(File.join(dir, 'explosion.wav'))
106
+ @music = Teek::SDL2::Music.new(File.join(dir, 'music.mp3'))
107
+ @music.volume = 48
108
+ @music_on = true
109
+ end
110
+
86
111
  def build_ui
87
112
  # "wm" commands control the window manager -- title, resizability, etc.
88
113
  # "." is the root Tk window (every widget path starts from here).
89
- @app.command(:wm, :title, '.', 'Minesweeper')
114
+ @app.command(:wm, :title, '.', 'Yet Another Minesweeper')
90
115
  @app.command(:wm, :resizable, '.', 0, 0)
91
116
 
92
117
  build_menu
@@ -165,6 +190,13 @@ class Minesweeper
165
190
  font: 'TkFixedFont 14 bold', fg: :red, bg: :black,
166
191
  relief: :sunken, anchor: :center)
167
192
  @app.command(:pack, '.hdr.time', side: :right, padx: 5, pady: 3)
193
+
194
+ # Music toggle (right, next to timer)
195
+ @music_btn = '.hdr.music'
196
+ @app.command(:button, @music_btn, text: "\u266A", width: 2,
197
+ font: 'TkDefaultFont 10',
198
+ command: proc { |*| toggle_music })
199
+ @app.command(:pack, @music_btn, side: :right, padx: 2, pady: 3)
168
200
  end
169
201
 
170
202
  # The game grid is a single Tk canvas filled with image items.
@@ -184,15 +216,27 @@ class Minesweeper
184
216
  @app.command(:canvas, @canvas, width: cw, height: ch, highlightthickness: 0)
185
217
  @app.command(:pack, @canvas)
186
218
 
187
- # Left-click: reveal a cell
188
- lcb = @app.register_callback(proc { |*|
219
+ # Left-click: press shows sunken tile + suspense face, release reveals.
220
+ # This mimics classic Windows Minesweeper's press-and-hold behavior.
221
+ @pressed_cell = nil
222
+
223
+ press_cb = @app.register_callback(proc { |*|
189
224
  row, col = canvas_cell
190
- on_left_click(row, col) if row
225
+ on_left_press(row, col) if row
191
226
  })
192
- @app.tcl_eval("bind #{@canvas} <Button-1> " \
227
+ @app.tcl_eval("bind #{@canvas} <ButtonPress-1> " \
193
228
  "{set ::_ms_x [#{@canvas} canvasx %x]; " \
194
229
  "set ::_ms_y [#{@canvas} canvasy %y]; " \
195
- "ruby_callback #{lcb}}")
230
+ "ruby_callback #{press_cb}}")
231
+
232
+ release_cb = @app.register_callback(proc { |*|
233
+ row, col = canvas_cell
234
+ on_left_release(row, col) if row
235
+ })
236
+ @app.tcl_eval("bind #{@canvas} <ButtonRelease-1> " \
237
+ "{set ::_ms_x [#{@canvas} canvasx %x]; " \
238
+ "set ::_ms_y [#{@canvas} canvasy %y]; " \
239
+ "ruby_callback #{release_cb}}")
196
240
 
197
241
  # Right-click: toggle flag. Binding all three events covers:
198
242
  # Button-2 -- right-click on macOS
@@ -240,6 +284,7 @@ class Minesweeper
240
284
  @app.command(@face, :configure, text: ':)')
241
285
 
242
286
  draw_board
287
+ @music.play if @music_on && !@music.playing?
243
288
  end
244
289
 
245
290
  # Changing difficulty resizes the canvas and resets. The window auto-shrinks
@@ -285,7 +330,8 @@ class Minesweeper
285
330
 
286
331
  candidates = []
287
332
  @rows.times { |r| @cols.times { |c| candidates << [r, c] unless safe[[r, c]] } }
288
- candidates.shuffle!.first(@num_mines).each { |r, c| @mine[r][c] = true }
333
+ rng = ENV['SEED'] ? Random.new(ENV['SEED'].to_i) : Random.new
334
+ candidates.shuffle!(random: rng).first(@num_mines).each { |r, c| @mine[r][c] = true }
289
335
 
290
336
  # Precompute how many mines neighbor each cell
291
337
  @rows.times do |r|
@@ -298,7 +344,29 @@ class Minesweeper
298
344
 
299
345
  # -- Click handlers ------------------------------------------------------
300
346
 
301
- def on_left_click(r, c)
347
+ def on_left_press(r, c)
348
+ return if @game_over || @flagged[r][c] || @revealed[r][c]
349
+
350
+ # Show sunken/pressed tile and suspense face
351
+ @pressed_cell = [r, c]
352
+ set_cell_image(r, c, :empty)
353
+ @app.command(@face, :configure, text: ':o')
354
+ end
355
+
356
+ def on_left_release(r, c)
357
+ prev = @pressed_cell
358
+ @pressed_cell = nil
359
+
360
+ # Restore face
361
+ @app.command(@face, :configure, text: ':)') unless @game_over
362
+
363
+ # If released on a different cell than pressed, restore the pressed cell
364
+ if prev && prev != [r, c]
365
+ pr, pc = prev
366
+ set_cell_image(pr, pc, :hidden) unless @revealed[pr][pc]
367
+ return
368
+ end
369
+
302
370
  return if @game_over || @flagged[r][c] || @revealed[r][c]
303
371
 
304
372
  if @first_click
@@ -308,8 +376,10 @@ class Minesweeper
308
376
  end
309
377
 
310
378
  if @mine[r][c]
379
+ @snd_explosion.play
311
380
  game_over_lose(r, c)
312
381
  else
382
+ @cascading = false
313
383
  reveal(r, c)
314
384
  check_win
315
385
  end
@@ -318,6 +388,7 @@ class Minesweeper
318
388
  def on_right_click(r, c)
319
389
  return if @game_over || @revealed[r][c]
320
390
 
391
+ @snd_flag.play
321
392
  if @flagged[r][c]
322
393
  @flagged[r][c] = false
323
394
  @flags_placed -= 1
@@ -344,8 +415,13 @@ class Minesweeper
344
415
 
345
416
  if count == 0
346
417
  set_cell_image(r, c, :empty)
418
+ unless @cascading
419
+ @cascading = true
420
+ @snd_sweep.play
421
+ end
347
422
  neighbors(r, c).each { |nr, nc| reveal(nr, nc) }
348
423
  else
424
+ @snd_click.play unless @cascading
349
425
  set_cell_image(r, c, count)
350
426
  end
351
427
  end
@@ -375,6 +451,7 @@ class Minesweeper
375
451
  @game_over = true
376
452
  stop_timer
377
453
  @app.command(@face, :configure, text: ':(')
454
+ Teek::SDL2.fade_out_music(1500) if @music_on
378
455
 
379
456
  @rows.times do |r|
380
457
  @cols.times do |c|
@@ -405,6 +482,24 @@ class Minesweeper
405
482
  @app.after(1000) { tick_timer }
406
483
  end
407
484
 
485
+ # -- Music ---------------------------------------------------------------
486
+
487
+ def toggle_music
488
+ if @music_on
489
+ @music.pause
490
+ @music_on = false
491
+ @app.command(@music_btn, :configure, text: '--')
492
+ else
493
+ if @music.paused?
494
+ @music.resume
495
+ else
496
+ @music.play
497
+ end
498
+ @music_on = true
499
+ @app.command(@music_btn, :configure, text: "\u266A")
500
+ end
501
+ end
502
+
408
503
  # -- Helpers -------------------------------------------------------------
409
504
 
410
505
  def in_bounds?(r, c)
@@ -437,16 +532,56 @@ app = Teek::App.new(track_widgets: false)
437
532
  # The root window starts withdrawn by default in Teek -- show it.
438
533
  app.show
439
534
 
440
- Minesweeper.new(app)
535
+ game = Minesweeper.new(app)
441
536
 
442
537
  # Automated demo support (for rake docker:test and recording)
443
538
  require_relative '../../lib/teek/demo_support'
444
539
  TeekDemo.app = app
445
540
 
446
- if TeekDemo.testing?
447
- TeekDemo.after_idle do
448
- app.after(500) { TeekDemo.finish }
541
+ if TeekDemo.active?
542
+ ENV['SEED'] = '42'
543
+ game.send(:new_game) # restart with deterministic layout
544
+
545
+ if TeekDemo.recording?
546
+ app.set_window_geometry('+0+0')
547
+ app.tcl_eval('. configure -cursor none')
548
+ TeekDemo.signal_recording_ready
449
549
  end
550
+
551
+ # Capture all audio output to WAV when TEEK_RECORD_AUDIO is set.
552
+ # The WAV can be muxed with the screen recording via ffmpeg:
553
+ # ffmpeg -i screen.mp4 -i yam_audio.wav -c:v copy -c:a aac -shortest out.mp4
554
+ audio_capture_path = ENV['TEEK_RECORD_AUDIO']
555
+ audio_capture_path = nil if audio_capture_path&.empty?
556
+ Teek::SDL2.start_audio_capture(audio_capture_path) if audio_capture_path
557
+
558
+ TeekDemo.after_idle {
559
+ d = TeekDemo.method(:delay)
560
+
561
+ # Click (row=2, col=3) — safe reveal, then (row=0, col=4) — mine. Boom!
562
+ steps = [
563
+ -> { game.press_cell(2, 3) },
564
+ -> { game.release_cell(2, 3) },
565
+ nil,
566
+ -> { game.press_cell(0, 4) },
567
+ -> { game.release_cell(0, 4) },
568
+ nil, nil,
569
+ -> {
570
+ Teek::SDL2.stop_audio_capture if audio_capture_path
571
+ TeekDemo.finish
572
+ },
573
+ ]
574
+
575
+ i = 0
576
+ run_step = proc {
577
+ steps[i]&.call
578
+ i += 1
579
+ if i < steps.length
580
+ app.after(d.call(test: 50, record: 1500)) { run_step.call }
581
+ end
582
+ }
583
+ run_step.call
584
+ }
450
585
  end
451
586
 
452
587
  app.mainloop
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teek
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Cook
@@ -141,22 +141,12 @@ files:
141
141
  - lib/teek/widget.rb
142
142
  - sample/calculator.rb
143
143
  - sample/debug_demo.rb
144
+ - sample/gamepad_viewer/assets/controller.png
145
+ - sample/gamepad_viewer/gamepad_viewer.rb
144
146
  - sample/goldberg.rb
145
147
  - sample/goldberg_helpers.rb
146
- - sample/minesweeper/assets/MINESWEEPER_0.png
147
- - sample/minesweeper/assets/MINESWEEPER_1.png
148
- - sample/minesweeper/assets/MINESWEEPER_2.png
149
- - sample/minesweeper/assets/MINESWEEPER_3.png
150
- - sample/minesweeper/assets/MINESWEEPER_4.png
151
- - sample/minesweeper/assets/MINESWEEPER_5.png
152
- - sample/minesweeper/assets/MINESWEEPER_6.png
153
- - sample/minesweeper/assets/MINESWEEPER_7.png
154
- - sample/minesweeper/assets/MINESWEEPER_8.png
155
- - sample/minesweeper/assets/MINESWEEPER_F.png
156
- - sample/minesweeper/assets/MINESWEEPER_M.png
157
- - sample/minesweeper/assets/MINESWEEPER_X.png
158
- - sample/minesweeper/minesweeper.rb
159
148
  - sample/optcarrot.rb
149
+ - sample/optcarrot/thwaite.nes
160
150
  - sample/optcarrot/vendor/optcarrot.rb
161
151
  - sample/optcarrot/vendor/optcarrot/apu.rb
162
152
  - sample/optcarrot/vendor/optcarrot/config.rb
@@ -183,6 +173,24 @@ files:
183
173
  - sample/paint/sparse_pixel_buffer.rb
184
174
  - sample/sdl2_demo.rb
185
175
  - sample/threading_demo.rb
176
+ - sample/yam/assets/MINESWEEPER_0.png
177
+ - sample/yam/assets/MINESWEEPER_1.png
178
+ - sample/yam/assets/MINESWEEPER_2.png
179
+ - sample/yam/assets/MINESWEEPER_3.png
180
+ - sample/yam/assets/MINESWEEPER_4.png
181
+ - sample/yam/assets/MINESWEEPER_5.png
182
+ - sample/yam/assets/MINESWEEPER_6.png
183
+ - sample/yam/assets/MINESWEEPER_7.png
184
+ - sample/yam/assets/MINESWEEPER_8.png
185
+ - sample/yam/assets/MINESWEEPER_F.png
186
+ - sample/yam/assets/MINESWEEPER_M.png
187
+ - sample/yam/assets/MINESWEEPER_X.png
188
+ - sample/yam/assets/click.wav
189
+ - sample/yam/assets/explosion.wav
190
+ - sample/yam/assets/flag.wav
191
+ - sample/yam/assets/music.mp3
192
+ - sample/yam/assets/sweep.wav
193
+ - sample/yam/yam.rb
186
194
  - teek.gemspec
187
195
  homepage: https://github.com/jamescook/teek
188
196
  licenses: