teek 0.1.3 → 0.1.4

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -0
  3. data/Rakefile +120 -22
  4. data/ext/teek/extconf.rb +19 -1
  5. data/ext/teek/tcltkbridge.c +38 -2
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkdrop.c +66 -0
  8. data/ext/teek/tkdrop.h +26 -0
  9. data/ext/teek/tkdrop_macos.m +141 -0
  10. data/ext/teek/tkdrop_win.c +232 -0
  11. data/ext/teek/tkdrop_x11.c +337 -0
  12. data/ext/teek/tkwin.c +42 -0
  13. data/lib/teek/platform.rb +29 -0
  14. data/lib/teek/version.rb +1 -1
  15. data/lib/teek.rb +49 -3
  16. data/teek.gemspec +3 -2
  17. metadata +7 -53
  18. data/sample/calculator.rb +0 -255
  19. data/sample/debug_demo.rb +0 -43
  20. data/sample/gamepad_viewer/assets/controller.png +0 -0
  21. data/sample/gamepad_viewer/gamepad_viewer.rb +0 -554
  22. data/sample/goldberg.rb +0 -1803
  23. data/sample/goldberg_helpers.rb +0 -170
  24. data/sample/optcarrot/thwaite.nes +0 -0
  25. data/sample/optcarrot/vendor/optcarrot/apu.rb +0 -856
  26. data/sample/optcarrot/vendor/optcarrot/config.rb +0 -257
  27. data/sample/optcarrot/vendor/optcarrot/cpu.rb +0 -1162
  28. data/sample/optcarrot/vendor/optcarrot/driver.rb +0 -144
  29. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +0 -14
  30. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +0 -105
  31. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +0 -153
  32. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +0 -14
  33. data/sample/optcarrot/vendor/optcarrot/nes.rb +0 -105
  34. data/sample/optcarrot/vendor/optcarrot/opt.rb +0 -168
  35. data/sample/optcarrot/vendor/optcarrot/pad.rb +0 -92
  36. data/sample/optcarrot/vendor/optcarrot/palette.rb +0 -65
  37. data/sample/optcarrot/vendor/optcarrot/ppu.rb +0 -1468
  38. data/sample/optcarrot/vendor/optcarrot/rom.rb +0 -143
  39. data/sample/optcarrot/vendor/optcarrot.rb +0 -14
  40. data/sample/optcarrot.rb +0 -354
  41. data/sample/paint/assets/bucket.png +0 -0
  42. data/sample/paint/assets/cursor.png +0 -0
  43. data/sample/paint/assets/eraser.png +0 -0
  44. data/sample/paint/assets/pencil.png +0 -0
  45. data/sample/paint/assets/spray.png +0 -0
  46. data/sample/paint/layer.rb +0 -255
  47. data/sample/paint/layer_manager.rb +0 -179
  48. data/sample/paint/paint_demo.rb +0 -837
  49. data/sample/paint/sparse_pixel_buffer.rb +0 -202
  50. data/sample/sdl2_demo.rb +0 -318
  51. data/sample/threading_demo.rb +0 -494
  52. data/sample/yam/assets/MINESWEEPER_0.png +0 -0
  53. data/sample/yam/assets/MINESWEEPER_1.png +0 -0
  54. data/sample/yam/assets/MINESWEEPER_2.png +0 -0
  55. data/sample/yam/assets/MINESWEEPER_3.png +0 -0
  56. data/sample/yam/assets/MINESWEEPER_4.png +0 -0
  57. data/sample/yam/assets/MINESWEEPER_5.png +0 -0
  58. data/sample/yam/assets/MINESWEEPER_6.png +0 -0
  59. data/sample/yam/assets/MINESWEEPER_7.png +0 -0
  60. data/sample/yam/assets/MINESWEEPER_8.png +0 -0
  61. data/sample/yam/assets/MINESWEEPER_F.png +0 -0
  62. data/sample/yam/assets/MINESWEEPER_M.png +0 -0
  63. data/sample/yam/assets/MINESWEEPER_X.png +0 -0
  64. data/sample/yam/assets/click.wav +0 -0
  65. data/sample/yam/assets/explosion.wav +0 -0
  66. data/sample/yam/assets/flag.wav +0 -0
  67. data/sample/yam/assets/music.mp3 +0 -0
  68. data/sample/yam/assets/sweep.wav +0 -0
  69. data/sample/yam/yam.rb +0 -587
data/sample/yam/yam.rb DELETED
@@ -1,587 +0,0 @@
1
- # frozen_string_literal: true
2
- # teek-record: title=Yet Another Minesweeper, audio=1
3
- #
4
- # Minesweeper clone built with Teek.
5
- #
6
- # Demonstrates:
7
- # - Loading and resizing PNG images with Tk's photo system
8
- # - Canvas-based game grid using image items
9
- # - Click handling via canvas coordinate math (not per-item bindings)
10
- # - Tcl variables for live-updating labels (-textvariable)
11
- # - Menus with radiobuttons and keyboard accelerators
12
- # - Scheduling repeated work with app.after
13
- # - register_callback to bridge Ruby procs into Tcl event handlers
14
- #
15
- # Tile artwork: "Minesweeper Tile Set" by eugeneloza (CC0)
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
22
-
23
- require_relative '../../lib/teek'
24
- require_relative '../../teek-sdl2/lib/teek/sdl2'
25
-
26
- class Minesweeper
27
- # The source PNGs are 216x216. Tk can shrink them with "copy -subsample N N"
28
- # which keeps every Nth pixel. 216 / 6 = 36, giving us nice 36px tiles.
29
- TILE_SIZE = 36
30
- SUBSAMPLE = 6
31
-
32
- LEVELS = {
33
- beginner: { cols: 9, rows: 9, mines: 10 },
34
- intermediate: { cols: 16, rows: 16, mines: 40 },
35
- expert: { cols: 30, rows: 16, mines: 99 }
36
- }.freeze
37
-
38
- attr_reader :app
39
-
40
- def initialize(app, level: :beginner)
41
- @app = app
42
- @level = level
43
- apply_level
44
- load_images
45
- load_sounds
46
- build_ui
47
- new_game
48
- end
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
-
54
- private
55
-
56
- # -- Setup ---------------------------------------------------------------
57
-
58
- def apply_level
59
- cfg = LEVELS[@level]
60
- @cols = cfg[:cols]
61
- @rows = cfg[:rows]
62
- @num_mines = cfg[:mines]
63
- end
64
-
65
- # Tk's "image create photo" loads a PNG into a named in-memory image.
66
- # The full 216x216 images are too large for game tiles, so we create a
67
- # second (empty) photo and copy into it with -subsample to shrink.
68
- # After copying, we delete the full-size image to free memory.
69
- #
70
- # The resulting @img hash maps game state keys (:hidden, :flag, 1..8, etc.)
71
- # to Tk photo names we can assign to canvas image items later.
72
- def load_images
73
- dir = File.join(__dir__, 'assets')
74
- @img = {}
75
-
76
- # Map game state keys to the PNG filename suffixes in assets/
77
- tiles = { hidden: 'X', empty: '0', flag: 'F', mine: 'M' }
78
- (1..8).each { |n| tiles[n] = n.to_s }
79
-
80
- tiles.each do |key, suffix|
81
- full = "ms_full_#{suffix}"
82
- small = "ms_#{suffix}"
83
- path = File.join(dir, "MINESWEEPER_#{suffix}.png")
84
-
85
- # Load full-size PNG into a temporary Tk photo
86
- @app.command(:image, :create, :photo, full, file: path)
87
-
88
- # Create the smaller photo and copy with subsampling.
89
- # -subsample takes two separate int args (x y), which command() can't
90
- # express as a single kwarg, so this line stays as tcl_eval.
91
- @app.command(:image, :create, :photo, small)
92
- @app.tcl_eval("#{small} copy #{full} -subsample #{SUBSAMPLE} #{SUBSAMPLE}")
93
-
94
- # Free the full-size image -- we only need the 36x36 version
95
- @app.command(:image, :delete, full)
96
- @img[key] = small
97
- end
98
- end
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
-
111
- def build_ui
112
- # "wm" commands control the window manager -- title, resizability, etc.
113
- # "." is the root Tk window (every widget path starts from here).
114
- @app.command(:wm, :title, '.', 'Yet Another Minesweeper')
115
- @app.command(:wm, :resizable, '.', 0, 0)
116
-
117
- build_menu
118
- build_header
119
- build_canvas
120
- end
121
-
122
- # Tk menus: create a menu widget, attach it to the window with "configure
123
- # -menu", then add items. Each item that triggers Ruby code needs a
124
- # registered callback -- register_callback returns an integer ID, and
125
- # "ruby_callback <id>" in Tcl invokes the corresponding Ruby proc.
126
- def build_menu
127
- @app.command(:menu, '.menubar')
128
- @app.command('.', :configure, menu: '.menubar')
129
- @app.command(:menu, '.menubar.game', tearoff: 0)
130
- @app.command('.menubar', :add, :cascade, label: 'Game', menu: '.menubar.game')
131
-
132
- # "add command" creates a clickable menu item.
133
- # -accelerator is cosmetic (shows "F2" in the menu) -- the actual
134
- # keybinding is set separately with "bind".
135
- # With command(), procs are auto-registered as callbacks -- no need
136
- # to manually call register_callback + interpolate the ID.
137
- new_game_proc = proc { |*| new_game }
138
- @app.command('.menubar.game', :add, :command,
139
- label: 'New Game', accelerator: 'F2', command: new_game_proc)
140
- @app.command(:bind, '.', '<F2>', new_game_proc)
141
-
142
- @app.command('.menubar.game', :add, :separator)
143
-
144
- # "add radiobutton" items share a Tcl variable -- Tk automatically shows
145
- # a bullet next to the selected one. The -variable points to a global
146
- # Tcl variable (:: prefix), and -value is what gets stored when selected.
147
- @level_var = '::ms_level'
148
- @app.command(:set, @level_var, @level)
149
- LEVELS.each_key do |lvl|
150
- @app.command('.menubar.game', :add, :radiobutton,
151
- label: lvl.capitalize, variable: @level_var, value: lvl,
152
- command: proc { |*| change_level(lvl) })
153
- end
154
-
155
- @app.command('.menubar.game', :add, :separator)
156
-
157
- @app.command('.menubar.game', :add, :command,
158
- label: 'Exit', command: proc { |*| @app.command(:destroy, '.') })
159
- end
160
-
161
- # The header bar uses "pack" geometry: mine counter on the left, face
162
- # button expanding to fill the center, timer on the right.
163
- #
164
- # -textvariable connects a label to a Tcl variable. When we later do
165
- # "set ::ms_mines 7", the label updates automatically -- no manual
166
- # refresh needed. This is one of Tk's nicest features.
167
- def build_header
168
- @app.command(:frame, '.hdr', relief: :raised, bd: 2)
169
- @app.command(:pack, '.hdr', fill: :x)
170
-
171
- # Mine counter (left) -- red digits on black, like the classic LCD look
172
- @mine_var = '::ms_mines'
173
- @app.command(:set, @mine_var, @num_mines)
174
- @app.command(:label, '.hdr.mines', textvariable: @mine_var, width: 4,
175
- font: 'TkFixedFont 14 bold', fg: :red, bg: :black,
176
- relief: :sunken, anchor: :center)
177
- @app.command(:pack, '.hdr.mines', side: :left, padx: 5, pady: 3)
178
-
179
- # Face button (center) -- doubles as new-game button
180
- @face = '.hdr.face'
181
- @app.command(:button, @face, text: ':)', width: 3,
182
- font: 'TkFixedFont 12 bold',
183
- command: proc { |*| new_game })
184
- @app.command(:pack, @face, side: :left, expand: 1, padx: 5, pady: 3)
185
-
186
- # Timer (right)
187
- @time_var = '::ms_time'
188
- @app.command(:set, @time_var, 0)
189
- @app.command(:label, '.hdr.time', textvariable: @time_var, width: 4,
190
- font: 'TkFixedFont 14 bold', fg: :red, bg: :black,
191
- relief: :sunken, anchor: :center)
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)
200
- end
201
-
202
- # The game grid is a single Tk canvas filled with image items.
203
- #
204
- # Instead of binding a click handler to each of the 81+ cells (which would
205
- # mean hundreds of registered callbacks), we bind ONE handler to the whole
206
- # canvas. The trick: Tk's bind substitution "%x %y" gives us the pointer
207
- # coordinates. We stash them in Tcl variables, then in the Ruby callback
208
- # we read those variables and divide by TILE_SIZE to get row/col.
209
- #
210
- # "canvasx %x" converts window coords to canvas coords (matters if the
211
- # canvas is scrolled, though we don't scroll here).
212
- def build_canvas
213
- cw = @cols * TILE_SIZE
214
- ch = @rows * TILE_SIZE
215
- @canvas = '.c'
216
- @app.command(:canvas, @canvas, width: cw, height: ch, highlightthickness: 0)
217
- @app.command(:pack, @canvas)
218
-
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 { |*|
224
- row, col = canvas_cell
225
- on_left_press(row, col) if row
226
- })
227
- @app.tcl_eval("bind #{@canvas} <ButtonPress-1> " \
228
- "{set ::_ms_x [#{@canvas} canvasx %x]; " \
229
- "set ::_ms_y [#{@canvas} canvasy %y]; " \
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}}")
240
-
241
- # Right-click: toggle flag. Binding all three events covers:
242
- # Button-2 -- right-click on macOS
243
- # Button-3 -- right-click on Linux/Windows
244
- # Ctrl+click -- fallback for single-button trackpads
245
- rcb = @app.register_callback(proc { |*|
246
- row, col = canvas_cell
247
- on_right_click(row, col) if row
248
- })
249
- %w[Button-2 Button-3 Control-Button-1].each do |ev|
250
- @app.tcl_eval("bind #{@canvas} <#{ev}> " \
251
- "{set ::_ms_x [#{@canvas} canvasx %x]; " \
252
- "set ::_ms_y [#{@canvas} canvasy %y]; " \
253
- "ruby_callback #{rcb}}")
254
- end
255
- end
256
-
257
- # Read the stashed click coordinates and convert to grid position.
258
- # Returns [row, col] or [nil, nil] if the click was outside the grid.
259
- def canvas_cell
260
- mx = @app.command(:set, '::_ms_x').to_f
261
- my = @app.command(:set, '::_ms_y').to_f
262
- col = (mx / TILE_SIZE).to_i
263
- row = (my / TILE_SIZE).to_i
264
- in_bounds?(row, col) ? [row, col] : [nil, nil]
265
- end
266
-
267
- # -- Game state ----------------------------------------------------------
268
-
269
- def new_game
270
- stop_timer
271
- @game_over = false
272
- @first_click = true
273
- @flags_placed = 0
274
- @elapsed = 0
275
-
276
- @mine = Array.new(@rows) { Array.new(@cols, false) }
277
- @revealed = Array.new(@rows) { Array.new(@cols, false) }
278
- @flagged = Array.new(@rows) { Array.new(@cols, false) }
279
- @adjacent = Array.new(@rows) { Array.new(@cols, 0) }
280
-
281
- # Update the header displays via their Tcl variables
282
- @app.command(:set, @mine_var, @num_mines)
283
- @app.command(:set, @time_var, 0)
284
- @app.command(@face, :configure, text: ':)')
285
-
286
- draw_board
287
- @music.play if @music_on && !@music.playing?
288
- end
289
-
290
- # Changing difficulty resizes the canvas and resets. The window auto-shrinks
291
- # because we set "wm resizable . 0 0" -- Tk recomputes the geometry.
292
- def change_level(level)
293
- return if level == @level
294
-
295
- @level = level
296
- apply_level
297
-
298
- cw = @cols * TILE_SIZE
299
- ch = @rows * TILE_SIZE
300
- @app.command(@canvas, :configure, width: cw, height: ch)
301
-
302
- new_game
303
- end
304
-
305
- # Populate the canvas with hidden-cell images. "canvas create image" places
306
- # a Tk photo at (x, y) with -anchor nw (top-left corner). It returns a
307
- # numeric item ID that we store in @cell_id so we can update each cell's
308
- # image later with "itemconfigure <id> -image <photo>".
309
- def draw_board
310
- @app.command(@canvas, :delete, :all)
311
- @cell_id = Array.new(@rows) { Array.new(@cols) }
312
-
313
- @rows.times do |r|
314
- @cols.times do |c|
315
- x = c * TILE_SIZE
316
- y = r * TILE_SIZE
317
- @cell_id[r][c] = @app.command(@canvas, :create, :image, x, y,
318
- image: @img[:hidden], anchor: :nw)
319
- end
320
- end
321
- end
322
-
323
- # -- Mine placement ------------------------------------------------------
324
-
325
- # Mines are placed on the first click, not at game start. This guarantees
326
- # the player's first click is always safe (and so are its neighbors).
327
- def place_mines(safe_r, safe_c)
328
- safe = { [safe_r, safe_c] => true }
329
- neighbors(safe_r, safe_c).each { |nr, nc| safe[[nr, nc]] = true }
330
-
331
- candidates = []
332
- @rows.times { |r| @cols.times { |c| candidates << [r, c] unless safe[[r, c]] } }
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 }
335
-
336
- # Precompute how many mines neighbor each cell
337
- @rows.times do |r|
338
- @cols.times do |c|
339
- next if @mine[r][c]
340
- @adjacent[r][c] = neighbors(r, c).count { |nr, nc| @mine[nr][nc] }
341
- end
342
- end
343
- end
344
-
345
- # -- Click handlers ------------------------------------------------------
346
-
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
-
370
- return if @game_over || @flagged[r][c] || @revealed[r][c]
371
-
372
- if @first_click
373
- @first_click = false
374
- place_mines(r, c)
375
- start_timer
376
- end
377
-
378
- if @mine[r][c]
379
- @snd_explosion.play
380
- game_over_lose(r, c)
381
- else
382
- @cascading = false
383
- reveal(r, c)
384
- check_win
385
- end
386
- end
387
-
388
- def on_right_click(r, c)
389
- return if @game_over || @revealed[r][c]
390
-
391
- @snd_flag.play
392
- if @flagged[r][c]
393
- @flagged[r][c] = false
394
- @flags_placed -= 1
395
- set_cell_image(r, c, :hidden)
396
- else
397
- @flagged[r][c] = true
398
- @flags_placed += 1
399
- set_cell_image(r, c, :flag)
400
- end
401
- @app.command(:set, @mine_var, @num_mines - @flags_placed)
402
- end
403
-
404
- # -- Reveal / win / lose -------------------------------------------------
405
-
406
- # Classic minesweeper flood fill: reveal a cell, and if it has zero
407
- # adjacent mines, recursively reveal all its neighbors. This produces
408
- # the satisfying "clearing" effect when you click an open area.
409
- def reveal(r, c)
410
- return unless in_bounds?(r, c)
411
- return if @revealed[r][c] || @flagged[r][c] || @mine[r][c]
412
-
413
- @revealed[r][c] = true
414
- count = @adjacent[r][c]
415
-
416
- if count == 0
417
- set_cell_image(r, c, :empty)
418
- unless @cascading
419
- @cascading = true
420
- @snd_sweep.play
421
- end
422
- neighbors(r, c).each { |nr, nc| reveal(nr, nc) }
423
- else
424
- @snd_click.play unless @cascading
425
- set_cell_image(r, c, count)
426
- end
427
- end
428
-
429
- # Win when every non-mine cell is revealed.
430
- def check_win
431
- unrevealed = 0
432
- @rows.times { |r| @cols.times { |c| unrevealed += 1 unless @revealed[r][c] } }
433
- return unless unrevealed == @num_mines
434
-
435
- @game_over = true
436
- stop_timer
437
- @app.command(@face, :configure, text: 'B)')
438
-
439
- # Auto-flag remaining mines as a visual cue
440
- @rows.times do |r|
441
- @cols.times do |c|
442
- next unless @mine[r][c] && !@flagged[r][c]
443
- @flagged[r][c] = true
444
- set_cell_image(r, c, :flag)
445
- end
446
- end
447
- @app.command(:set, @mine_var, 0)
448
- end
449
-
450
- def game_over_lose(_hit_r, _hit_c)
451
- @game_over = true
452
- stop_timer
453
- @app.command(@face, :configure, text: ':(')
454
- Teek::SDL2.fade_out_music(1500) if @music_on
455
-
456
- @rows.times do |r|
457
- @cols.times do |c|
458
- set_cell_image(r, c, :mine) if @mine[r][c]
459
- end
460
- end
461
- end
462
-
463
- # -- Timer ---------------------------------------------------------------
464
-
465
- # app.after(ms) schedules a Ruby block to run after a delay. It's a
466
- # one-shot timer, so for repeating work we schedule the next tick at the
467
- # end of each callback. Checking @timer_running lets us stop the chain
468
- # cleanly -- the next queued tick just returns without rescheduling.
469
- def start_timer
470
- @timer_running = true
471
- @app.after(1000) { tick_timer }
472
- end
473
-
474
- def stop_timer
475
- @timer_running = false
476
- end
477
-
478
- def tick_timer
479
- return unless @timer_running
480
- @elapsed += 1
481
- @app.command(:set, @time_var, @elapsed)
482
- @app.after(1000) { tick_timer }
483
- end
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
-
503
- # -- Helpers -------------------------------------------------------------
504
-
505
- def in_bounds?(r, c)
506
- r >= 0 && r < @rows && c >= 0 && c < @cols
507
- end
508
-
509
- # Return [row, col] pairs for all valid neighbors of a cell (up to 8).
510
- def neighbors(r, c)
511
- [[-1, -1], [-1, 0], [-1, 1],
512
- [0, -1], [0, 1],
513
- [1, -1], [1, 0], [1, 1]].filter_map do |dr, dc|
514
- nr, nc = r + dr, c + dc
515
- [nr, nc] if in_bounds?(nr, nc)
516
- end
517
- end
518
-
519
- # Swap a cell's displayed image. "itemconfigure" changes properties of an
520
- # existing canvas item by its numeric ID -- here we just swap -image.
521
- def set_cell_image(r, c, key)
522
- @app.command(@canvas, :itemconfigure, @cell_id[r][c], image: @img[key])
523
- end
524
- end
525
-
526
- # -- Main ------------------------------------------------------------------
527
-
528
- # track_widgets: false because we manage canvas items ourselves and don't
529
- # need Teek's automatic widget tracking overhead.
530
- app = Teek::App.new(track_widgets: false)
531
-
532
- # The root window starts withdrawn by default in Teek -- show it.
533
- app.show
534
-
535
- game = Minesweeper.new(app)
536
-
537
- # Automated demo support (for rake docker:test and recording)
538
- require_relative '../../lib/teek/demo_support'
539
- TeekDemo.app = app
540
-
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
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
- }
585
- end
586
-
587
- app.mainloop