teek 0.1.1 → 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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -0
  3. data/Rakefile +162 -5
  4. data/ext/teek/extconf.rb +1 -1
  5. data/ext/teek/tcltkbridge.c +9 -0
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkeventsource.c +195 -0
  8. data/ext/teek/tkphoto.c +169 -5
  9. data/ext/teek/tkwin.c +84 -0
  10. data/lib/teek/background_ractor4x.rb +32 -4
  11. data/lib/teek/photo.rb +232 -0
  12. data/lib/teek/version.rb +1 -1
  13. data/lib/teek.rb +202 -5
  14. data/sample/gamepad_viewer/assets/controller.png +0 -0
  15. data/sample/gamepad_viewer/gamepad_viewer.rb +554 -0
  16. data/sample/optcarrot/thwaite.nes +0 -0
  17. data/sample/optcarrot/vendor/optcarrot/apu.rb +856 -0
  18. data/sample/optcarrot/vendor/optcarrot/config.rb +257 -0
  19. data/sample/optcarrot/vendor/optcarrot/cpu.rb +1162 -0
  20. data/sample/optcarrot/vendor/optcarrot/driver.rb +144 -0
  21. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +14 -0
  22. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +105 -0
  23. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +153 -0
  24. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +14 -0
  25. data/sample/optcarrot/vendor/optcarrot/nes.rb +105 -0
  26. data/sample/optcarrot/vendor/optcarrot/opt.rb +168 -0
  27. data/sample/optcarrot/vendor/optcarrot/pad.rb +92 -0
  28. data/sample/optcarrot/vendor/optcarrot/palette.rb +65 -0
  29. data/sample/optcarrot/vendor/optcarrot/ppu.rb +1468 -0
  30. data/sample/optcarrot/vendor/optcarrot/rom.rb +143 -0
  31. data/sample/optcarrot/vendor/optcarrot.rb +14 -0
  32. data/sample/optcarrot.rb +354 -0
  33. data/sample/paint/assets/bucket.png +0 -0
  34. data/sample/paint/assets/cursor.png +0 -0
  35. data/sample/paint/assets/eraser.png +0 -0
  36. data/sample/paint/assets/pencil.png +0 -0
  37. data/sample/paint/assets/spray.png +0 -0
  38. data/sample/paint/layer.rb +255 -0
  39. data/sample/paint/layer_manager.rb +179 -0
  40. data/sample/paint/paint_demo.rb +837 -0
  41. data/sample/paint/sparse_pixel_buffer.rb +202 -0
  42. data/sample/sdl2_demo.rb +318 -0
  43. data/sample/yam/assets/click.wav +0 -0
  44. data/sample/yam/assets/explosion.wav +0 -0
  45. data/sample/yam/assets/flag.wav +0 -0
  46. data/sample/yam/assets/music.mp3 +0 -0
  47. data/sample/yam/assets/sweep.wav +0 -0
  48. data/sample/{minesweeper/minesweeper.rb → yam/yam.rb} +147 -12
  49. metadata +50 -14
  50. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_0.png +0 -0
  51. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_1.png +0 -0
  52. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_2.png +0 -0
  53. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_3.png +0 -0
  54. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_4.png +0 -0
  55. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_5.png +0 -0
  56. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_6.png +0 -0
  57. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_7.png +0 -0
  58. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_8.png +0 -0
  59. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_F.png +0 -0
  60. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_M.png +0 -0
  61. /data/sample/{minesweeper → yam}/assets/MINESWEEPER_X.png +0 -0
@@ -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.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Cook
@@ -124,6 +124,7 @@ files:
124
124
  - ext/teek/tcl9compat.h
125
125
  - ext/teek/tcltkbridge.c
126
126
  - ext/teek/tcltkbridge.h
127
+ - ext/teek/tkeventsource.c
127
128
  - ext/teek/tkfont.c
128
129
  - ext/teek/tkphoto.c
129
130
  - ext/teek/tkwin.c
@@ -134,27 +135,62 @@ files:
134
135
  - lib/teek/debugger.rb
135
136
  - lib/teek/demo_support.rb
136
137
  - lib/teek/method_coverage_service.rb
138
+ - lib/teek/photo.rb
137
139
  - lib/teek/ractor_support.rb
138
140
  - lib/teek/version.rb
139
141
  - lib/teek/widget.rb
140
142
  - sample/calculator.rb
141
143
  - sample/debug_demo.rb
144
+ - sample/gamepad_viewer/assets/controller.png
145
+ - sample/gamepad_viewer/gamepad_viewer.rb
142
146
  - sample/goldberg.rb
143
147
  - sample/goldberg_helpers.rb
144
- - sample/minesweeper/assets/MINESWEEPER_0.png
145
- - sample/minesweeper/assets/MINESWEEPER_1.png
146
- - sample/minesweeper/assets/MINESWEEPER_2.png
147
- - sample/minesweeper/assets/MINESWEEPER_3.png
148
- - sample/minesweeper/assets/MINESWEEPER_4.png
149
- - sample/minesweeper/assets/MINESWEEPER_5.png
150
- - sample/minesweeper/assets/MINESWEEPER_6.png
151
- - sample/minesweeper/assets/MINESWEEPER_7.png
152
- - sample/minesweeper/assets/MINESWEEPER_8.png
153
- - sample/minesweeper/assets/MINESWEEPER_F.png
154
- - sample/minesweeper/assets/MINESWEEPER_M.png
155
- - sample/minesweeper/assets/MINESWEEPER_X.png
156
- - sample/minesweeper/minesweeper.rb
148
+ - sample/optcarrot.rb
149
+ - sample/optcarrot/thwaite.nes
150
+ - sample/optcarrot/vendor/optcarrot.rb
151
+ - sample/optcarrot/vendor/optcarrot/apu.rb
152
+ - sample/optcarrot/vendor/optcarrot/config.rb
153
+ - sample/optcarrot/vendor/optcarrot/cpu.rb
154
+ - sample/optcarrot/vendor/optcarrot/driver.rb
155
+ - sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb
156
+ - sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb
157
+ - sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb
158
+ - sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb
159
+ - sample/optcarrot/vendor/optcarrot/nes.rb
160
+ - sample/optcarrot/vendor/optcarrot/opt.rb
161
+ - sample/optcarrot/vendor/optcarrot/pad.rb
162
+ - sample/optcarrot/vendor/optcarrot/palette.rb
163
+ - sample/optcarrot/vendor/optcarrot/ppu.rb
164
+ - sample/optcarrot/vendor/optcarrot/rom.rb
165
+ - sample/paint/assets/bucket.png
166
+ - sample/paint/assets/cursor.png
167
+ - sample/paint/assets/eraser.png
168
+ - sample/paint/assets/pencil.png
169
+ - sample/paint/assets/spray.png
170
+ - sample/paint/layer.rb
171
+ - sample/paint/layer_manager.rb
172
+ - sample/paint/paint_demo.rb
173
+ - sample/paint/sparse_pixel_buffer.rb
174
+ - sample/sdl2_demo.rb
157
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
158
194
  - teek.gemspec
159
195
  homepage: https://github.com/jamescook/teek
160
196
  licenses: