gemba 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/THIRD_PARTY_NOTICES +113 -0
  3. data/assets/JetBrainsMonoNL-Regular.ttf +0 -0
  4. data/assets/ark-pixel-12px-monospaced-ja.ttf +0 -0
  5. data/bin/gemba +14 -0
  6. data/ext/gemba/extconf.rb +185 -0
  7. data/ext/gemba/gemba_ext.c +1051 -0
  8. data/ext/gemba/gemba_ext.h +15 -0
  9. data/gemba.gemspec +38 -0
  10. data/lib/gemba/child_window.rb +62 -0
  11. data/lib/gemba/cli.rb +384 -0
  12. data/lib/gemba/config.rb +621 -0
  13. data/lib/gemba/core.rb +121 -0
  14. data/lib/gemba/headless.rb +12 -0
  15. data/lib/gemba/headless_player.rb +206 -0
  16. data/lib/gemba/hotkey_map.rb +202 -0
  17. data/lib/gemba/input_mappings.rb +214 -0
  18. data/lib/gemba/locale.rb +92 -0
  19. data/lib/gemba/locales/en.yml +157 -0
  20. data/lib/gemba/locales/ja.yml +157 -0
  21. data/lib/gemba/method_coverage_service.rb +265 -0
  22. data/lib/gemba/overlay_renderer.rb +109 -0
  23. data/lib/gemba/player.rb +1515 -0
  24. data/lib/gemba/recorder.rb +156 -0
  25. data/lib/gemba/recorder_decoder.rb +325 -0
  26. data/lib/gemba/rom_info_window.rb +346 -0
  27. data/lib/gemba/rom_loader.rb +100 -0
  28. data/lib/gemba/runtime.rb +39 -0
  29. data/lib/gemba/save_state_manager.rb +155 -0
  30. data/lib/gemba/save_state_picker.rb +199 -0
  31. data/lib/gemba/settings_window.rb +1173 -0
  32. data/lib/gemba/tip_service.rb +133 -0
  33. data/lib/gemba/toast_overlay.rb +128 -0
  34. data/lib/gemba/version.rb +5 -0
  35. data/lib/gemba.rb +17 -0
  36. data/test/fixtures/test.gba +0 -0
  37. data/test/fixtures/test.sav +0 -0
  38. data/test/shared/screenshot_helper.rb +113 -0
  39. data/test/shared/simplecov_config.rb +59 -0
  40. data/test/shared/teek_test_worker.rb +388 -0
  41. data/test/shared/tk_test_helper.rb +354 -0
  42. data/test/support/input_mocks.rb +61 -0
  43. data/test/support/player_helpers.rb +77 -0
  44. data/test/test_cli.rb +281 -0
  45. data/test/test_config.rb +897 -0
  46. data/test/test_core.rb +401 -0
  47. data/test/test_gamepad_map.rb +116 -0
  48. data/test/test_headless_player.rb +205 -0
  49. data/test/test_helper.rb +19 -0
  50. data/test/test_hotkey_map.rb +396 -0
  51. data/test/test_keyboard_map.rb +108 -0
  52. data/test/test_locale.rb +159 -0
  53. data/test/test_mgba.rb +26 -0
  54. data/test/test_overlay_renderer.rb +199 -0
  55. data/test/test_player.rb +903 -0
  56. data/test/test_recorder.rb +180 -0
  57. data/test/test_rom_loader.rb +149 -0
  58. data/test/test_save_state_manager.rb +289 -0
  59. data/test/test_settings_hotkeys.rb +434 -0
  60. data/test/test_settings_window.rb +1039 -0
  61. data/test/test_tip_service.rb +138 -0
  62. data/test/test_toast_overlay.rb +216 -0
  63. data/test/test_virtual_keyboard.rb +39 -0
  64. data/test/test_xor_delta.rb +61 -0
  65. metadata +234 -0
data/test/test_core.rb ADDED
@@ -0,0 +1,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba"
5
+ require "tempfile"
6
+ require "tmpdir"
7
+
8
+ class TestMGBACore < Minitest::Test
9
+ # Generated by: ruby gemba/scripts/generate_test_rom.rb
10
+ TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
11
+
12
+ def setup
13
+ skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
14
+ @core = Gemba::Core.new(TEST_ROM)
15
+ end
16
+
17
+ def teardown
18
+ @core&.destroy unless @core.nil? || @core.destroyed?
19
+ end
20
+
21
+ # -- Dimensions --------------------------------------------------------------
22
+
23
+ def test_width
24
+ assert_equal 240, @core.width
25
+ end
26
+
27
+ def test_height
28
+ assert_equal 160, @core.height
29
+ end
30
+
31
+ # -- Title -------------------------------------------------------------------
32
+
33
+ def test_title
34
+ assert_equal "GEMBATEST", @core.title
35
+ end
36
+
37
+ # -- ROM metadata ------------------------------------------------------------
38
+
39
+ def test_game_code
40
+ code = @core.game_code
41
+ # mGBA prefixes with platform: "AGB-" for GBA
42
+ assert_equal "AGB-BGBE", code
43
+ refute_includes code, "\0", "game_code must not contain null bytes"
44
+ end
45
+
46
+ def test_maker_code
47
+ maker = @core.maker_code
48
+ assert_equal "01", maker
49
+ refute_includes maker, "\0", "maker_code must not contain null bytes"
50
+ end
51
+
52
+ def test_title_no_null_bytes
53
+ title = @core.title
54
+ assert_equal "GEMBATEST", title
55
+ refute_includes title, "\0", "title must not contain null bytes"
56
+ end
57
+
58
+ def test_checksum
59
+ crc = @core.checksum
60
+ assert_kind_of Integer, crc
61
+ assert crc > 0
62
+ end
63
+
64
+ def test_platform
65
+ assert_equal "GBA", @core.platform
66
+ end
67
+
68
+ def test_rom_size
69
+ size = @core.rom_size
70
+ assert_kind_of Integer, size
71
+ assert size > 0
72
+ end
73
+
74
+ # -- Lifecycle ---------------------------------------------------------------
75
+
76
+ def test_not_destroyed_initially
77
+ refute @core.destroyed?
78
+ end
79
+
80
+ def test_destroy
81
+ @core.destroy
82
+ assert @core.destroyed?
83
+ end
84
+
85
+ def test_double_destroy_is_safe
86
+ @core.destroy
87
+ @core.destroy
88
+ assert @core.destroyed?
89
+ end
90
+
91
+ def test_methods_raise_after_destroy
92
+ @core.destroy
93
+ assert_raises(RuntimeError) { @core.run_frame }
94
+ assert_raises(RuntimeError) { @core.video_buffer }
95
+ assert_raises(RuntimeError) { @core.video_buffer_argb }
96
+ assert_raises(RuntimeError) { @core.audio_buffer }
97
+ assert_raises(RuntimeError) { @core.set_keys(0) }
98
+ assert_raises(RuntimeError) { @core.width }
99
+ assert_raises(RuntimeError) { @core.height }
100
+ assert_raises(RuntimeError) { @core.title }
101
+ assert_raises(RuntimeError) { @core.game_code }
102
+ assert_raises(RuntimeError) { @core.maker_code }
103
+ assert_raises(RuntimeError) { @core.checksum }
104
+ assert_raises(RuntimeError) { @core.platform }
105
+ assert_raises(RuntimeError) { @core.rom_size }
106
+ assert_raises(RuntimeError) { @core.save_state_to_file("/tmp/x.ss") }
107
+ assert_raises(RuntimeError) { @core.load_state_from_file("/tmp/x.ss") }
108
+ end
109
+
110
+ # -- Frame emulation ---------------------------------------------------------
111
+
112
+ def test_run_frame
113
+ @core.run_frame
114
+ end
115
+
116
+ def test_video_buffer_size
117
+ @core.run_frame
118
+ buf = @core.video_buffer
119
+ assert_kind_of String, buf
120
+ assert_equal 240 * 160 * 4, buf.bytesize
121
+ end
122
+
123
+ def test_video_buffer_argb_size
124
+ @core.run_frame
125
+ buf = @core.video_buffer_argb
126
+ assert_kind_of String, buf
127
+ assert_equal 240 * 160 * 4, buf.bytesize
128
+ end
129
+
130
+ def test_video_buffer_argb_swaps_r_and_b
131
+ @core.run_frame
132
+ raw = @core.video_buffer.unpack('V*')
133
+ argb = @core.video_buffer_argb.unpack('V*')
134
+ assert_equal raw.size, argb.size
135
+ # Verify R↔B swap + forced opaque alpha for first non-zero pixel
136
+ idx = raw.index { |px| px != 0 } || 0
137
+ px = raw[idx]
138
+ expected = 0xFF000000 |
139
+ ((px & 0x000000FF) << 16) |
140
+ (px & 0x0000FF00) |
141
+ ((px & 0x00FF0000) >> 16)
142
+ assert_equal expected, argb[idx]
143
+ end
144
+
145
+ def test_audio_buffer_is_stereo_int16
146
+ @core.run_frame
147
+ buf = @core.audio_buffer
148
+ assert_kind_of String, buf
149
+ assert_equal 0, buf.bytesize % 4, "should be stereo int16 (4 bytes per frame)"
150
+ end
151
+
152
+ def test_multiple_frames
153
+ 10.times { @core.run_frame }
154
+ assert_equal 240 * 160 * 4, @core.video_buffer.bytesize
155
+ end
156
+
157
+ # -- Input -------------------------------------------------------------------
158
+
159
+ def test_set_keys
160
+ @core.set_keys(Gemba::KEY_A | Gemba::KEY_START)
161
+ @core.run_frame
162
+ @core.set_keys(0)
163
+ @core.run_frame
164
+ end
165
+
166
+ # -- Save files --------------------------------------------------------------
167
+
168
+ def test_save_dir_creates_sav_file
169
+ Dir.mktmpdir("gemba-saves") do |dir|
170
+ core = Gemba::Core.new(TEST_ROM, dir)
171
+ core.run_frame
172
+ core.destroy
173
+ sav = Dir.glob(File.join(dir, "*.sav"))
174
+ assert_equal 1, sav.size, "Expected a .sav file in #{dir}"
175
+ end
176
+ end
177
+
178
+ def test_save_dir_nil_uses_rom_directory
179
+ # When save_dir is nil, saves go alongside the ROM — just verify no crash
180
+ core = Gemba::Core.new(TEST_ROM)
181
+ core.run_frame
182
+ core.destroy
183
+ end
184
+
185
+ # -- Save states -------------------------------------------------------------
186
+
187
+ def test_save_state_to_file
188
+ @core.run_frame
189
+ Dir.mktmpdir("gemba-states") do |dir|
190
+ path = File.join(dir, "test.ss1")
191
+ assert @core.save_state_to_file(path), "save_state_to_file should return true"
192
+ assert File.exist?(path), "State file should exist"
193
+ assert File.size(path) > 0, "State file should not be empty"
194
+ end
195
+ end
196
+
197
+ def test_load_state_from_file
198
+ # Run several frames to reach a known state
199
+ 10.times { @core.run_frame }
200
+
201
+ Dir.mktmpdir("gemba-states") do |dir|
202
+ path = File.join(dir, "test.ss1")
203
+
204
+ # Save state at frame 10
205
+ assert @core.save_state_to_file(path)
206
+
207
+ # Run more frames to advance past the saved state
208
+ 10.times { @core.run_frame }
209
+
210
+ # Load state — should succeed and restore core to a valid state
211
+ assert @core.load_state_from_file(path), "load_state_from_file should return true"
212
+
213
+ # Core should be functional after loading: run a frame without crashing
214
+ @core.run_frame
215
+ buf = @core.video_buffer
216
+ assert_equal 240 * 160 * 4, buf.bytesize, "Video buffer should be valid after state load"
217
+ end
218
+ end
219
+
220
+ def test_load_state_nonexistent_returns_false
221
+ result = @core.load_state_from_file("/no/such/state.ss1")
222
+ refute result, "load_state_from_file should return false for missing file"
223
+ end
224
+
225
+ def test_save_state_raises_on_bad_path
226
+ @core.run_frame
227
+ assert_raises(RuntimeError) { @core.save_state_to_file("/no/such/dir/state.ss1") }
228
+ end
229
+
230
+ def test_save_state_round_trip_preserves_state
231
+ # Run to frame 10, save, continue to frame 20, load, run 1 frame.
232
+ # If state was truly restored, the frame after load should match
233
+ # frame 11 from a fresh run.
234
+ 10.times { @core.run_frame }
235
+
236
+ Dir.mktmpdir("gemba-states") do |dir|
237
+ path = File.join(dir, "test.ss1")
238
+ assert @core.save_state_to_file(path)
239
+
240
+ # Capture frame 11 from saved state
241
+ @core.run_frame
242
+ buf_frame_11 = @core.video_buffer
243
+
244
+ # Reload state (back to frame 10)
245
+ assert @core.load_state_from_file(path)
246
+
247
+ # Run 1 frame from restored state (should be frame 11 again)
248
+ @core.run_frame
249
+ buf_restored_11 = @core.video_buffer
250
+
251
+ assert_equal buf_frame_11, buf_restored_11,
252
+ "Frame after load should match frame after save point"
253
+ end
254
+ end
255
+
256
+ # -- Color correction --------------------------------------------------------
257
+
258
+ def test_color_correction_defaults_to_false
259
+ refute @core.color_correction?
260
+ end
261
+
262
+ def test_color_correction_toggle
263
+ @core.color_correction = true
264
+ assert @core.color_correction?
265
+ @core.color_correction = false
266
+ refute @core.color_correction?
267
+ end
268
+
269
+ def test_color_correction_modifies_argb_output
270
+ @core.run_frame
271
+ buf_normal = @core.video_buffer_argb
272
+
273
+ @core.color_correction = true
274
+ buf_corrected = @core.video_buffer_argb
275
+
276
+ # The corrected buffer should differ (color values are transformed)
277
+ refute_equal buf_normal, buf_corrected,
278
+ "Color-corrected ARGB output should differ from uncorrected"
279
+ end
280
+
281
+ # -- Frame blending -----------------------------------------------------------
282
+
283
+ def test_frame_blending_defaults_to_false
284
+ refute @core.frame_blending?
285
+ end
286
+
287
+ def test_frame_blending_toggle
288
+ @core.frame_blending = true
289
+ assert @core.frame_blending?
290
+ @core.frame_blending = false
291
+ refute @core.frame_blending?
292
+ end
293
+
294
+ def test_frame_blending_modifies_argb_output
295
+ @core.run_frame
296
+ buf_normal = @core.video_buffer_argb
297
+
298
+ @core.frame_blending = true
299
+ # First call with blending on blends current frame with zeroed prev_frame
300
+ buf_blended = @core.video_buffer_argb
301
+
302
+ refute_equal buf_normal, buf_blended,
303
+ "Frame-blended ARGB output should differ from unblended"
304
+ end
305
+
306
+ def test_frame_blending_stabilizes_on_static_content
307
+ @core.frame_blending = true
308
+ @core.run_frame
309
+ @core.video_buffer_argb # prime prev_frame buffer
310
+ @core.run_frame
311
+ buf_a = @core.video_buffer_argb
312
+ @core.run_frame
313
+ buf_b = @core.video_buffer_argb
314
+
315
+ # Static content (test ROM draws nothing) should produce identical blended
316
+ # output once prev_frame is primed
317
+ assert_equal buf_a, buf_b,
318
+ "Frame blending on static content should stabilize"
319
+ end
320
+
321
+ # -- Rewind ----------------------------------------------------------------
322
+
323
+ def test_rewind_init_and_count
324
+ @core.rewind_init(3)
325
+ assert_equal 0, @core.rewind_count
326
+ @core.run_frame
327
+ @core.rewind_push
328
+ assert_equal 1, @core.rewind_count
329
+ @core.run_frame
330
+ @core.rewind_push
331
+ assert_equal 2, @core.rewind_count
332
+ end
333
+
334
+ def test_rewind_pop_restores_state
335
+ @core.rewind_init(5)
336
+ 10.times { @core.run_frame }
337
+
338
+ # Save snapshot at frame 10
339
+ @core.rewind_push
340
+ @core.run_frame
341
+ buf_11 = @core.video_buffer
342
+
343
+ # Advance to frame 20
344
+ 9.times { @core.run_frame }
345
+
346
+ # Rewind to frame 10
347
+ assert_equal true, @core.rewind_pop
348
+
349
+ # Run one frame from restored state → should match frame 11
350
+ @core.run_frame
351
+ buf_restored = @core.video_buffer
352
+ assert_equal buf_11, buf_restored
353
+ end
354
+
355
+ def test_rewind_pop_empty_returns_false
356
+ @core.rewind_init(3)
357
+ assert_equal false, @core.rewind_pop
358
+ end
359
+
360
+ def test_rewind_pop_without_init_returns_false
361
+ assert_equal false, @core.rewind_pop
362
+ end
363
+
364
+ def test_rewind_circular_buffer
365
+ @core.rewind_init(3)
366
+ 5.times do
367
+ @core.run_frame
368
+ @core.rewind_push
369
+ end
370
+ # Ring buffer capacity is 3, so count stays at 3
371
+ assert_equal 3, @core.rewind_count
372
+ end
373
+
374
+ def test_rewind_deinit_clears_buffer
375
+ @core.rewind_init(3)
376
+ @core.run_frame
377
+ @core.rewind_push
378
+ @core.rewind_deinit
379
+ assert_equal 0, @core.rewind_count
380
+ assert_equal false, @core.rewind_pop
381
+ end
382
+
383
+ def test_rewind_init_raises_on_invalid_capacity
384
+ assert_raises(ArgumentError) { @core.rewind_init(0) }
385
+ assert_raises(ArgumentError) { @core.rewind_init(-1) }
386
+ end
387
+
388
+ # -- Error handling ----------------------------------------------------------
389
+
390
+ def test_nonexistent_file
391
+ assert_raises(ArgumentError) { Gemba::Core.new("/no/such/game.gba") }
392
+ end
393
+
394
+ def test_invalid_extension
395
+ Tempfile.create(["bad", ".txt"]) do |f|
396
+ f.write("not a rom")
397
+ f.flush
398
+ assert_raises(ArgumentError) { Gemba::Core.new(f.path) }
399
+ end
400
+ end
401
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba"
5
+ require_relative "../lib/gemba/config"
6
+ require_relative "../lib/gemba/input_mappings"
7
+ require_relative "support/input_mocks"
8
+
9
+ class TestGamepadMap < Minitest::Test
10
+ def setup
11
+ @config = MockInputConfig.new
12
+ @map = Gemba::GamepadMap.new(@config)
13
+ end
14
+
15
+ def test_default_labels
16
+ labels = @map.labels
17
+ assert_equal 'a', labels[:a]
18
+ assert_equal 'b', labels[:b]
19
+ assert_equal 'start', labels[:start]
20
+ end
21
+
22
+ def test_set_remap
23
+ @map.set(:a, :y)
24
+ assert_equal 'y', @map.labels[:a]
25
+ end
26
+
27
+ def test_set_removes_old_binding
28
+ @map.set(:a, :y)
29
+ refute @map.labels.values.include?('a')
30
+ end
31
+
32
+ def test_reset
33
+ @map.set(:a, :y)
34
+ @map.set_dead_zone(12000)
35
+ @map.reset!
36
+ assert_equal 'a', @map.labels[:a]
37
+ assert_equal Gemba::GamepadMap::DEFAULT_DEAD_ZONE, @map.dead_zone
38
+ end
39
+
40
+ def test_mask_no_device
41
+ assert_equal 0, @map.mask
42
+ end
43
+
44
+ def test_mask_with_device
45
+ gp = MockGamepad.new
46
+ @map.device = gp
47
+ gp.buttons_pressed.add(:a)
48
+ mask = @map.mask
49
+ assert_equal Gemba::KEY_A, mask & Gemba::KEY_A
50
+ end
51
+
52
+ def test_mask_closed_device
53
+ gp = MockGamepad.new
54
+ @map.device = gp
55
+ gp.buttons_pressed.add(:a)
56
+ gp.close!
57
+ assert_equal 0, @map.mask
58
+ end
59
+
60
+ def test_supports_deadzone
61
+ assert @map.supports_deadzone?
62
+ end
63
+
64
+ def test_set_dead_zone
65
+ @map.set_dead_zone(12000)
66
+ assert_equal 12000, @map.dead_zone
67
+ end
68
+
69
+ def test_dead_zone_pct
70
+ @map.set_dead_zone(16384)
71
+ assert_equal 50, @map.dead_zone_pct
72
+ end
73
+
74
+ def test_load_config
75
+ gp = MockGamepad.new
76
+ @map.device = gp
77
+ @map.load_config
78
+ assert_equal 'x', @map.labels[:a]
79
+ assert_equal 'y', @map.labels[:b]
80
+ assert_equal (15 / 100.0 * 32767).round, @map.dead_zone
81
+ end
82
+
83
+ def test_load_config_no_device
84
+ @map.load_config
85
+ assert_equal 'a', @map.labels[:a]
86
+ end
87
+
88
+ def test_reload
89
+ gp = MockGamepad.new
90
+ @map.device = gp
91
+ @map.set(:a, :y)
92
+ @map.reload!
93
+ assert @config.calls.any? { |c| c[0] == :reload! }
94
+ assert_equal 'x', @map.labels[:a]
95
+ end
96
+
97
+ def test_save_to_config
98
+ gp = MockGamepad.new(guid: 'test-guid', name: 'Test Pad')
99
+ @map.device = gp
100
+ @map.save_to_config
101
+ gp_calls = @config.calls.select { |c| c[0] == :gamepad }
102
+ assert_equal 1, gp_calls.size
103
+ assert_equal 'test-guid', gp_calls[0][1]
104
+
105
+ dz_calls = @config.calls.select { |c| c[0] == :set_dead_zone }
106
+ assert_equal 1, dz_calls.size
107
+
108
+ set_calls = @config.calls.select { |c| c[0] == :set_mapping }
109
+ assert_equal 10, set_calls.size
110
+ end
111
+
112
+ def test_save_to_config_no_device
113
+ @map.save_to_config
114
+ assert_empty @config.calls.select { |c| c[0] == :set_mapping }
115
+ end
116
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "gemba/headless"
5
+ require "tmpdir"
6
+
7
+ class TestHeadlessPlayer < Minitest::Test
8
+ TEST_ROM = File.expand_path("fixtures/test.gba", __dir__)
9
+
10
+ def setup
11
+ skip "Run: ruby gemba/scripts/generate_test_rom.rb" unless File.exist?(TEST_ROM)
12
+ end
13
+
14
+ # -- Lifecycle ---------------------------------------------------------------
15
+
16
+ def test_open_and_close
17
+ player = Gemba::HeadlessPlayer.new(TEST_ROM)
18
+ refute player.closed?
19
+ player.close
20
+ assert player.closed?
21
+ end
22
+
23
+ def test_double_close_is_safe
24
+ player = Gemba::HeadlessPlayer.new(TEST_ROM)
25
+ player.close
26
+ player.close # should not raise
27
+ assert player.closed?
28
+ end
29
+
30
+ def test_block_form
31
+ result = Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
32
+ refute player.closed?
33
+ :ok
34
+ end
35
+ assert_equal :ok, result
36
+ end
37
+
38
+ def test_block_form_closes_on_exception
39
+ assert_raises(RuntimeError) do
40
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
41
+ @ref = player
42
+ raise "boom"
43
+ end
44
+ end
45
+ assert @ref.closed?
46
+ end
47
+
48
+ # -- Stepping ----------------------------------------------------------------
49
+
50
+ def test_step_single_frame
51
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
52
+ player.step # should not raise
53
+ end
54
+ end
55
+
56
+ def test_step_multiple_frames
57
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
58
+ player.step(60) # should not raise
59
+ end
60
+ end
61
+
62
+ def test_step_after_close_raises
63
+ player = Gemba::HeadlessPlayer.new(TEST_ROM)
64
+ player.close
65
+ assert_raises(RuntimeError) { player.step }
66
+ end
67
+
68
+ # -- Input -------------------------------------------------------------------
69
+
70
+ def test_press_and_release
71
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
72
+ player.press(Gemba::KEY_A | Gemba::KEY_START)
73
+ player.step
74
+ player.release_all
75
+ player.step
76
+ end
77
+ end
78
+
79
+ # -- Buffers -----------------------------------------------------------------
80
+
81
+ def test_video_buffer_argb_size
82
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
83
+ player.step
84
+ buf = player.video_buffer_argb
85
+ assert_equal 240 * 160 * 4, buf.bytesize
86
+ end
87
+ end
88
+
89
+ def test_audio_buffer_returns_data
90
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
91
+ player.step
92
+ buf = player.audio_buffer
93
+ assert_kind_of String, buf
94
+ end
95
+ end
96
+
97
+ # -- Dimensions --------------------------------------------------------------
98
+
99
+ def test_width_and_height
100
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
101
+ assert_equal 240, player.width
102
+ assert_equal 160, player.height
103
+ end
104
+ end
105
+
106
+ # -- ROM metadata ------------------------------------------------------------
107
+
108
+ def test_title
109
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
110
+ assert_equal "GEMBATEST", player.title
111
+ end
112
+ end
113
+
114
+ def test_game_code
115
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
116
+ assert_equal "AGB-BGBE", player.game_code
117
+ end
118
+ end
119
+
120
+ def test_maker_code
121
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
122
+ assert_equal "01", player.maker_code
123
+ end
124
+ end
125
+
126
+ def test_checksum
127
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
128
+ assert_kind_of Integer, player.checksum
129
+ end
130
+ end
131
+
132
+ def test_platform
133
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
134
+ assert_equal "GBA", player.platform
135
+ end
136
+ end
137
+
138
+ def test_rom_size
139
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
140
+ assert_operator player.rom_size, :>, 0
141
+ end
142
+ end
143
+
144
+ # -- Save states -------------------------------------------------------------
145
+
146
+ def test_save_and_load_state
147
+ Dir.mktmpdir do |dir|
148
+ path = File.join(dir, "test.ss1")
149
+
150
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
151
+ player.step(10)
152
+ assert player.save_state(path)
153
+ assert File.exist?(path)
154
+
155
+ player.step(60)
156
+ assert player.load_state(path)
157
+ end
158
+ end
159
+ end
160
+
161
+ # -- Rewind ------------------------------------------------------------------
162
+
163
+ def test_rewind_init_and_count
164
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
165
+ player.rewind_init(3)
166
+ assert_equal 0, player.rewind_count
167
+
168
+ player.step
169
+ player.rewind_push
170
+ assert_equal 1, player.rewind_count
171
+
172
+ player.step
173
+ player.rewind_push
174
+ assert_equal 2, player.rewind_count
175
+ end
176
+ end
177
+
178
+ def test_rewind_pop
179
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
180
+ player.rewind_init(5)
181
+ player.step(10)
182
+ player.rewind_push
183
+ player.step(10)
184
+ assert player.rewind_pop
185
+ assert_equal 0, player.rewind_count
186
+ end
187
+ end
188
+
189
+ def test_rewind_pop_empty_returns_false
190
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
191
+ player.rewind_init(3)
192
+ refute player.rewind_pop
193
+ end
194
+ end
195
+
196
+ def test_rewind_deinit
197
+ Gemba::HeadlessPlayer.open(TEST_ROM) do |player|
198
+ player.rewind_init(3)
199
+ player.step
200
+ player.rewind_push
201
+ player.rewind_deinit
202
+ assert_equal 0, player.rewind_count
203
+ end
204
+ end
205
+ end