romdevtools 0.26.0 → 0.28.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 (144) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +322 -3
  3. package/README.md +1 -1
  4. package/examples/README.md +1 -1
  5. package/examples/atari2600/templates/platformer.asm +18 -9
  6. package/examples/atari2600/templates/racing.asm +25 -4
  7. package/examples/atari2600/templates/shmup.asm +30 -5
  8. package/examples/atari2600/templates/sports.asm +41 -9
  9. package/examples/atari7800/templates/hello_sprite.c +8 -4
  10. package/examples/atari7800/templates/platformer.c +12 -8
  11. package/examples/atari7800/templates/puzzle.c +7 -4
  12. package/examples/atari7800/templates/racing.c +5 -2
  13. package/examples/atari7800/templates/shmup.c +8 -4
  14. package/examples/atari7800/templates/sports.c +6 -3
  15. package/examples/c64/templates/platformer.c +28 -24
  16. package/examples/c64/templates/puzzle.c +77 -16
  17. package/examples/c64/templates/racing.c +9 -0
  18. package/examples/c64/templates/shmup.c +13 -1
  19. package/examples/c64/templates/sports.c +9 -4
  20. package/examples/gb/templates/platformer.c +6 -2
  21. package/examples/gb/templates/puzzle.c +279 -101
  22. package/examples/gb/templates/racing.c +13 -1
  23. package/examples/gb/templates/shmup.c +13 -1
  24. package/examples/gb/templates/sports.c +9 -3
  25. package/examples/gba/templates/platformer.c +7 -13
  26. package/examples/gba/templates/puzzle.c +93 -15
  27. package/examples/gba/templates/racing.c +13 -1
  28. package/examples/gba/templates/shmup.c +13 -1
  29. package/examples/gba/templates/sports.c +17 -5
  30. package/examples/gbc/templates/platformer.c +6 -2
  31. package/examples/gbc/templates/puzzle.c +878 -178
  32. package/examples/gbc/templates/racing.c +13 -1
  33. package/examples/gbc/templates/shmup.c +13 -1
  34. package/examples/gbc/templates/sports.c +9 -3
  35. package/examples/genesis/templates/puzzle.c +76 -15
  36. package/examples/genesis/templates/racing.c +13 -1
  37. package/examples/genesis/templates/shmup_2p.c +13 -1
  38. package/examples/gg/templates/platformer.c +4 -0
  39. package/examples/gg/templates/puzzle.c +80 -14
  40. package/examples/gg/templates/racing.c +17 -1
  41. package/examples/gg/templates/shmup.c +17 -1
  42. package/examples/gg/templates/sports.c +4 -0
  43. package/examples/lynx/templates/platformer.c +25 -6
  44. package/examples/lynx/templates/puzzle.c +77 -14
  45. package/examples/lynx/templates/shmup.c +13 -1
  46. package/examples/lynx/templates/sports.c +5 -2
  47. package/examples/msx/platformer/main.c +2 -0
  48. package/examples/msx/puzzle/main.c +78 -15
  49. package/examples/msx/racing/main.c +1 -0
  50. package/examples/msx/shmup/main.c +1 -0
  51. package/examples/msx/sports/main.c +3 -2
  52. package/examples/nes/templates/platformer.c +11 -3
  53. package/examples/nes/templates/puzzle.c +81 -21
  54. package/examples/nes/templates/racing.c +15 -1
  55. package/examples/nes/templates/shmup.c +1 -0
  56. package/examples/nes/templates/sports.c +1 -0
  57. package/examples/pce/platformer/main.c +3 -1
  58. package/examples/pce/puzzle/main.c +78 -12
  59. package/examples/pce/racing/main.c +1 -0
  60. package/examples/pce/shmup/main.c +5 -4
  61. package/examples/pce/sports/main.c +4 -3
  62. package/examples/sms/templates/platformer.c +4 -0
  63. package/examples/sms/templates/puzzle.c +80 -14
  64. package/examples/sms/templates/racing.c +17 -1
  65. package/examples/sms/templates/shmup.c +17 -1
  66. package/examples/sms/templates/shmup_2p.c +17 -1
  67. package/examples/sms/templates/sports.c +4 -0
  68. package/examples/snes/templates/platformer.c +32 -15
  69. package/examples/snes/templates/puzzle.c +84 -16
  70. package/examples/snes/templates/racing.c +20 -1
  71. package/examples/snes/templates/shmup.c +20 -2
  72. package/examples/snes/templates/sports.c +7 -0
  73. package/package.json +12 -12
  74. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  75. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  76. package/src/cores/wasm/fceumm_libretro.js +1 -1
  77. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  78. package/src/cores/wasm/gambatte_libretro.js +1 -1
  79. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  80. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  81. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  82. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  83. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  84. package/src/cores/wasm/handy_libretro.js +1 -1
  85. package/src/cores/wasm/handy_libretro.wasm +0 -0
  86. package/src/cores/wasm/mgba_libretro.js +1 -1
  87. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  88. package/src/cores/wasm/prosystem_libretro.js +1 -1
  89. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  90. package/src/cores/wasm/snes9x_libretro.js +1 -1
  91. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  92. package/src/cores/wasm/stella2014_libretro.js +1 -1
  93. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  94. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  95. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  96. package/src/host/LibretroHost.js +245 -10
  97. package/src/mcp/server.js +6 -0
  98. package/src/mcp/tools/disasm-rebuild.js +315 -65
  99. package/src/mcp/tools/disasm.js +149 -28
  100. package/src/mcp/tools/find-references.js +216 -51
  101. package/src/mcp/tools/frame.js +11 -4
  102. package/src/mcp/tools/index.js +15 -1
  103. package/src/mcp/tools/input.js +26 -3
  104. package/src/mcp/tools/memory.js +208 -39
  105. package/src/mcp/tools/playtest.js +56 -4
  106. package/src/mcp/tools/project.js +35 -9
  107. package/src/mcp/tools/toolchain.js +43 -10
  108. package/src/mcp/tools/watch-memory.js +172 -25
  109. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  110. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  111. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  112. package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
  113. package/src/platforms/gb/MENTAL_MODEL.md +16 -1
  114. package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
  115. package/src/platforms/gb/lib/c/patch-header.js +7 -4
  116. package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
  117. package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
  118. package/src/platforms/gbc/lib/c/font.h +43 -0
  119. package/src/platforms/gbc/lib/c/patch-header.js +7 -4
  120. package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
  121. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  122. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  123. package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
  124. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  125. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  126. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  127. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  128. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  129. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  130. package/src/platforms/msx/lib/c/msx_hw.h +2 -0
  131. package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
  132. package/src/platforms/nes/MENTAL_MODEL.md +10 -3
  133. package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
  134. package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
  135. package/src/platforms/pce/MENTAL_MODEL.md +9 -0
  136. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  137. package/src/platforms/pce/lib/c/pce_hw.h +2 -1
  138. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  139. package/src/platforms/sms/MENTAL_MODEL.md +5 -0
  140. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  141. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  142. package/src/platforms/snes/MENTAL_MODEL.md +5 -0
  143. package/src/playtest/playtest.js +73 -3
  144. package/src/toolchains/index.js +37 -8
@@ -116,14 +116,15 @@ MAIN:
116
116
  STA VSYNC
117
117
 
118
118
  ; ── VBLANK (37 lines) — game logic ────────────────────────────────
119
- ; 34 here + the 3 STA WSYNC in the P0/P1 positioning block below = 37 VBLANK
120
- ; lines total. (Bug fix: this loop used to be 37 AND the positioning added 3
121
- ; more 265 scanlines/frame the TV/emulator can't lock vsync rolling /
122
- ; black picture. Exactly 262 lines = 3 VSYNC + 37 VBLANK + 192 visible + 30
123
- ; overscan; the positioning WSYNCs MUST be counted against the 37.)
119
+ ; 32 here + the 5 STA WSYNC in the positioning block below (P0, P1,
120
+ ; HMOVE, ball, ball-HMOVE) = 37 VBLANK lines total. (Bug fix history:
121
+ ; this loop used to be 37 AND the positioning added more>262
122
+ ; scanlines/frame the TV/emulator can't lock vsync rolling /
123
+ ; black picture. Exactly 262 lines = 3 VSYNC + 37 VBLANK + 192 visible
124
+ ; + 30 overscan; every positioning WSYNC MUST be counted against the 37.)
124
125
  LDA #2
125
126
  STA VBLANK
126
- LDX #34
127
+ LDX #32
127
128
  .vb:
128
129
  STA WSYNC
129
130
  DEX
@@ -133,16 +134,22 @@ MAIN:
133
134
  LDA FRAME
134
135
  AND #$01
135
136
  BNE .skip_pad
137
+ ; Kernel convention: Y counts 192->2 top-to-bottom, so LARGER Y is
138
+ ; HIGHER on screen. DOWN must therefore DECREASE P0_Y (the old code
139
+ ; had it backwards — stick down moved the paddle up). 2 px/frame so
140
+ ; the paddle isn't sluggish.
136
141
  LDA SWCHA
137
142
  ASL ; right (unused)
138
143
  ASL ; left (unused)
139
144
  ASL ; down
140
145
  BCS .nd
141
- INC P0_Y
146
+ DEC P0_Y
147
+ DEC P0_Y
142
148
  .nd:
143
149
  ASL ; up
144
150
  BCS .nu
145
- DEC P0_Y
151
+ INC P0_Y
152
+ INC P0_Y
146
153
  .nu:
147
154
  ; Clamp paddle within bounds
148
155
  LDA P0_Y
@@ -246,10 +253,35 @@ MAIN:
246
253
  DEX
247
254
  BNE .p1d ; ~64 cycles in (< 76) → P1 lands near the right
248
255
  STA RESP1
249
- ; Line 3 of 3: apply HMOVE on a FRESH line, right after WSYNC
256
+ ; Line 3: apply HMOVE on a FRESH line, right after WSYNC
250
257
  STA WSYNC
251
258
  STA HMOVE
252
259
 
260
+ ; Lines 4-5: position the BALL horizontally at BALL_X.
261
+ ; THE "ball never moves" fix: RESBL was defined but never strobed, so
262
+ ; the ball sat at whatever column the TIA powered up with, forever.
263
+ ; Standard divide-by-15 coarse strobe + HMBL fine offset, re-done every
264
+ ; frame so BALL_X changes actually show on screen.
265
+ STA WSYNC
266
+ LDA BALL_X
267
+ CLC
268
+ ADC #14 ; +14: compensate the loop's minimum latency
269
+ SEC
270
+ .bdiv:
271
+ SBC #15
272
+ BCS .bdiv ; A = remainder - 15 (in -15..-1)
273
+ STA RESBL ; coarse: ball lands at the loop-exit column
274
+ EOR #$FF ; fine: remainder -> HMBL nibble
275
+ ASL
276
+ ASL
277
+ ASL
278
+ ASL
279
+ STA HMBL
280
+ STA WSYNC
281
+ STA HMOVE ; apply the fine offset on a fresh line
282
+ LDA #0
283
+ STA HMBL ; don't re-shift on later HMOVEs
284
+
253
285
  LDA #0
254
286
  STA VBLANK
255
287
 
@@ -27,10 +27,14 @@
27
27
  #define SWCHA (*(volatile uint8_t*)0x280)
28
28
 
29
29
  /* SWCHA bit pattern (port A, active LOW — invert before testing) */
30
- #define JOY_UP 0x80
31
- #define JOY_DOWN 0x40
32
- #define JOY_LEFT 0x20
33
- #define JOY_RIGHT 0x10
30
+ /* SWCHA P0 nibble, active-low after the ~SWCHA invert. The bit order is
31
+ * Right/Left/Down/Up from bit7 down — the OLD defines here were exactly
32
+ * REVERSED (UP=0x80 etc.), which made up/down move the sprite left/right
33
+ * on every 7800 scaffold. */
34
+ #define JOY_RIGHT 0x80
35
+ #define JOY_LEFT 0x40
36
+ #define JOY_DOWN 0x20
37
+ #define JOY_UP 0x10
34
38
 
35
39
  /* 16-pixel-wide ball (= 4 bytes in 160A mode), 8 rows tall. */
36
40
  static const uint8_t sprite_row0[4] = { 0x05, 0x55, 0x55, 0x50 };
@@ -25,10 +25,14 @@
25
25
  #define SWCHA (*(volatile uint8_t*)0x280)
26
26
  #define INPT4 (*(volatile uint8_t*)0x0C)
27
27
 
28
- #define JOY_UP 0x80
29
- #define JOY_DOWN 0x40
30
- #define JOY_LEFT 0x20
31
- #define JOY_RIGHT 0x10
28
+ /* SWCHA P0 nibble, active-low after the ~SWCHA invert. The bit order is
29
+ * Right/Left/Down/Up from bit7 down — the OLD defines here were exactly
30
+ * REVERSED (UP=0x80 etc.), which made up/down move the sprite left/right
31
+ * on every 7800 scaffold. */
32
+ #define JOY_RIGHT 0x80
33
+ #define JOY_LEFT 0x40
34
+ #define JOY_DOWN 0x20
35
+ #define JOY_UP 0x10
32
36
 
33
37
  /* 16-pixel-wide (= 4 bytes in 160A), 8 rows tall player ball. */
34
38
  static const uint8_t player_row0[4] = { 0x05, 0x55, 0x55, 0x50 };
@@ -126,10 +130,10 @@ static void vblank_wait(void) {
126
130
  }
127
131
 
128
132
  /* Physics in 4.4 fixed point — 16 = 1 px, allows half-pixel velocity. */
129
- #define GRAVITY 8
130
- #define MOVE_PX 1
131
- #define JUMP_VEL (-48)
132
- #define MAXFALL 48
133
+ #define GRAVITY 6
134
+ #define MOVE_PX 2 /* 1 px/frame read as 'doesn't move' — 2 is snappy */
135
+ #define JUMP_VEL (-80) /* was -48: a 10px hop over ~12 frames read as 'jumps very slowly' — this is ~27px in the same time */
136
+ #define MAXFALL 64
133
137
  #define GROUND_Y 200 /* DLL index of the ground line */
134
138
 
135
139
  void main(void) {
@@ -31,8 +31,11 @@
31
31
  #define SWCHA (*(volatile uint8_t*)0x280)
32
32
  #define INPT4 (*(volatile uint8_t*)0x0C)
33
33
 
34
- #define JOY_LEFT 0x20
35
- #define JOY_RIGHT 0x10
34
+ /* SWCHA bit order is Right(0x80)/Left(0x40)/Down(0x20)/Up(0x10) — the
35
+ * old 0x20/0x10 masks here were the DOWN/UP bits, so the stick's
36
+ * vertical axis steered horizontally. */
37
+ #define JOY_LEFT 0x40
38
+ #define JOY_RIGHT 0x80
36
39
 
37
40
  #define COLS 8
38
41
  #define CELL_W_PIX 8
@@ -181,11 +184,11 @@ void main(void) {
181
184
  if (pad & JOY_RIGHT && piece_x_col < COLS - 1) { piece_x_col++; set_x((uint8_t)(60 + piece_x_col * CELL_W_PIX)); }
182
185
 
183
186
  btn = (INPT4 & 0x80) ? 0 : 1;
184
- if (btn && !prev_btn) { fall_timer = 30; sfx_tone(0, 4, 4); }
187
+ if (btn && !prev_btn) { fall_timer = 18; sfx_tone(0, 4, 4); }
185
188
  prev_btn = btn;
186
189
 
187
190
  fall_timer++;
188
- if (fall_timer >= 30) {
191
+ if (fall_timer >= 18) { /* was 30 — 'moving down very slowly' */
189
192
  fall_timer = 0;
190
193
  piece_y++;
191
194
  if (piece_y >= BOT_Y) {
@@ -27,8 +27,11 @@
27
27
  #define CTRL (*(volatile uint8_t*)0x3C)
28
28
  #define SWCHA (*(volatile uint8_t*)0x280)
29
29
 
30
- #define JOY_LEFT 0x20
31
- #define JOY_RIGHT 0x10
30
+ /* SWCHA bit order is Right(0x80)/Left(0x40)/Down(0x20)/Up(0x10) — the
31
+ * old 0x20/0x10 masks here were the DOWN/UP bits, so the stick's
32
+ * vertical axis steered horizontally. */
33
+ #define JOY_LEFT 0x40
34
+ #define JOY_RIGHT 0x80
32
35
 
33
36
  /* 16-pixel-wide (= 4 bytes in 160A) × 8 row car sprite. */
34
37
  static const uint8_t car_row0[4] = { 0x05, 0x55, 0x55, 0x50 };
@@ -31,10 +31,14 @@
31
31
  #define SWCHA (*(volatile uint8_t*)0x280)
32
32
  #define INPT4 (*(volatile uint8_t*)0x0C)
33
33
 
34
- #define JOY_UP 0x80
35
- #define JOY_DOWN 0x40
36
- #define JOY_LEFT 0x20
37
- #define JOY_RIGHT 0x10
34
+ /* SWCHA P0 nibble, active-low after the ~SWCHA invert. The bit order is
35
+ * Right/Left/Down/Up from bit7 down — the OLD defines here were exactly
36
+ * REVERSED (UP=0x80 etc.), which made up/down move the sprite left/right
37
+ * on every 7800 scaffold. */
38
+ #define JOY_RIGHT 0x80
39
+ #define JOY_LEFT 0x40
40
+ #define JOY_DOWN 0x20
41
+ #define JOY_UP 0x10
38
42
 
39
43
  /* Ship sprite — 16 px wide × 8 rows. */
40
44
  static const uint8_t ship_row0[4] = { 0x00, 0x05, 0x50, 0x00 };
@@ -31,8 +31,11 @@
31
31
  #define CTRL (*(volatile uint8_t*)0x3C)
32
32
  #define SWCHA (*(volatile uint8_t*)0x280)
33
33
 
34
- #define P1_UP 0x80
35
- #define P1_DOWN 0x40
34
+ /* SWCHA bit order is Right(0x80)/Left(0x40)/Down(0x20)/Up(0x10) — the
35
+ * old 0x80/0x40 masks were the RIGHT/LEFT bits, so the stick's
36
+ * horizontal axis moved the paddle vertically. */
37
+ #define P1_UP 0x10
38
+ #define P1_DOWN 0x20
36
39
  #define P2_UP 0x08
37
40
  #define P2_DOWN 0x04
38
41
 
@@ -169,7 +172,7 @@ static void serve_ball(uint8_t to_left) {
169
172
  bx = 76;
170
173
  by = 120;
171
174
  bdx = to_left ? -2 : 2;
172
- bdy = 1;
175
+ bdy = 2; /* was 1 — the rally felt 'very slow' */
173
176
  }
174
177
 
175
178
  void main(void) {
@@ -62,17 +62,6 @@ static const uint8_t platforms[][3] = {
62
62
  };
63
63
  #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
64
64
 
65
- /* Is world char-cell (col,row) a platform block? */
66
- static uint8_t world_is_wall(uint8_t col, uint8_t row) {
67
- uint8_t i;
68
- for (i = 0; i < N_PLATFORMS; i++) {
69
- if (row == platforms[i][2]
70
- && col >= platforms[i][0]
71
- && col < platforms[i][1]) return 1;
72
- }
73
- return 0;
74
- }
75
-
76
65
  static void wait_vblank(void) {
77
66
  while (PEEK(VIC_RASTER) < 250) { }
78
67
  while (PEEK(VIC_RASTER) >= 250) { }
@@ -88,33 +77,48 @@ static void copy_sprite(uint8_t slot, const uint8_t *data) {
88
77
  * starting at world column `coarseCol`. Called once per coarse-scroll step
89
78
  * (every 8 px of camera movement) — NOT every frame. */
90
79
  static void render_view(uint8_t coarseCol) {
91
- uint8_t sc, r;
92
- uint16_t wc;
80
+ uint8_t sc, r, i, c8;
81
+ uint16_t wc, off;
82
+ uint8_t wallrow[VIS_ROWS];
83
+ /* PERFORMANCE IS LOAD-BEARING HERE. The old version re-scanned the
84
+ * 10-entry platform table for EVERY CELL and computed a 16-bit modulo
85
+ * (a cc65 library call) per sky cell — ~2 SECONDS per re-render at
86
+ * 1 MHz, re-run on every 8-px coarse step. The game spent nearly all
87
+ * its time in here: scrolling froze, jump presses were eaten between
88
+ * the multi-second loop passes, and the sprite setup after the first
89
+ * renders didn't run for hundreds of frames. This version flags each
90
+ * column's platform rows ONCE (40 table scans total, not 1000) and
91
+ * uses mask arithmetic for the sky texture. ~20x faster. */
93
92
  for (sc = 0; sc < VIS_COLS; sc++) {
94
93
  wc = (uint16_t)coarseCol + sc;
94
+ c8 = (uint8_t)wc;
95
+ for (r = 0; r < VIS_ROWS; r++) wallrow[r] = 0;
96
+ if (wc < WORLD_COLS) {
97
+ for (i = 0; i < N_PLATFORMS; i++) {
98
+ if (c8 >= platforms[i][0] && c8 < platforms[i][1])
99
+ wallrow[platforms[i][2]] = 1;
100
+ }
101
+ }
102
+ off = sc;
95
103
  for (r = 0; r < VIS_ROWS; r++) {
96
- uint16_t off = (uint16_t)r * 40 + sc;
97
- if (wc < WORLD_COLS && world_is_wall((uint8_t)wc, r)) {
104
+ if (wallrow[r]) {
98
105
  SCREEN[off] = 0xA0; /* reverse-space solid block */
99
106
  COLORS[off] = 0x0C; /* mid grey platform */
100
107
  } else if (r >= 22) {
101
- /* Ground fill below the floor row: dithered earth so the lower
102
- * band reads as solid terrain, not void. */
108
+ /* dithered earth below the floor row */
103
109
  SCREEN[off] = 0xA0;
104
- COLORS[off] = (((uint8_t)wc ^ r) & 1) ? 0x09 : 0x08; /* brown / orange */
110
+ COLORS[off] = ((c8 ^ r) & 1) ? 0x09 : 0x08; /* brown / orange */
105
111
  } else {
106
- /* Textured sky so two colours share the backdrop and neither the
107
- * sky nor the border dominates the frame. Sparse '.' stars on a
108
- * coarse lattice add detail; reverse-space everywhere else gives a
109
- * filled (non-blank) sky band that scrolls with the world. */
110
- if (((wc * 3u + r * 7u) % 23u) == 0u) {
112
+ /* textured sky: sparse '.' stars on a cheap AND-mask lattice */
113
+ if (((uint8_t)(c8 + (r << 2)) & 15) == 0) {
111
114
  SCREEN[off] = 0x2E; /* '.' distant detail */
112
115
  COLORS[off] = 0x01; /* white */
113
116
  } else {
114
117
  SCREEN[off] = 0xA0; /* solid block sky */
115
- COLORS[off] = (((uint8_t)wc ^ (r >> 1)) & 1) ? 0x06 : 0x0E; /* blue / light blue */
118
+ COLORS[off] = ((c8 ^ (r >> 1)) & 1) ? 0x06 : 0x0E; /* blue / light blue */
116
119
  }
117
120
  }
121
+ off += 40;
118
122
  }
119
123
  }
120
124
  }
@@ -133,28 +133,89 @@ static void new_piece(void) {
133
133
  piece_y = -3;
134
134
  }
135
135
 
136
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
137
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
138
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
139
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
140
+ * 4 directions, clears them, applies per-column gravity, and loops so
141
+ * cascades chain (score scales with chain depth). */
142
+ static uint8_t matched[ROWS][COLS];
143
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
144
+
145
+ static uint8_t mark_and_count(void) {
146
+ uint8_t r, c, d, len, k, cnt;
147
+ uint8_t col;
148
+ int8_t dr, dc;
149
+ int sr, sc;
150
+ cnt = 0;
151
+ for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
152
+ for (r = 0; r < ROWS; r++) {
153
+ for (c = 0; c < COLS; c++) {
154
+ col = grid[r][c];
155
+ if (col == 0) continue;
156
+ for (d = 0; d < 4; d++) {
157
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
158
+ sr = (int)r - dr; sc = (int)c - dc;
159
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
160
+ && grid[sr][sc] == col) continue; /* not the run's start */
161
+ len = 1;
162
+ sr = (int)r + dr; sc = (int)c + dc;
163
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
164
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
165
+ if (len >= 3) {
166
+ sr = r; sc = c;
167
+ for (k = 0; k < len; k++) {
168
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
169
+ sr += dr; sc += dc;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ return cnt;
176
+ }
177
+
178
+ /* collapse each column so survivors rest on the floor (in place: walk
179
+ * from the bottom, copying gems down to a write cursor, then zero above) */
180
+ static void apply_gravity(void) {
181
+ uint8_t c;
182
+ int r, w;
183
+ for (c = 0; c < COLS; c++) {
184
+ w = ROWS - 1;
185
+ for (r = ROWS - 1; r >= 0; r--) {
186
+ if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
187
+ }
188
+ for (; w >= 0; w--) grid[w][c] = 0;
189
+ }
190
+ }
191
+
192
+ static void resolve_board(void) {
193
+ uint8_t n, r, c, chain;
194
+ unsigned int amt;
195
+ chain = 0;
196
+ while (1) {
197
+ n = mark_and_count();
198
+ if (n == 0) break;
199
+ chain++;
200
+ for (r = 0; r < ROWS; r++)
201
+ for (c = 0; c < COLS; c++)
202
+ if (matched[r][c]) grid[r][c] = 0;
203
+ amt = (unsigned int)n * 10u;
204
+ if (chain > 1) amt = amt * chain;
205
+ if (score < 65500u) score += amt;
206
+ sfx_tone(0, 0x80, 0x10, 12); /* clear chime */
207
+ apply_gravity();
208
+ }
209
+ }
210
+
136
211
  static void lock_piece(void) {
137
- uint8_t i, c;
212
+ uint8_t i;
138
213
  int8_t r;
139
- uint8_t a, b, d;
140
214
  for (i = 0; i < 3; i++) {
141
215
  r = (int8_t)(piece_y + i);
142
216
  if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
143
217
  }
144
- for (i = 0; i < 3; i++) {
145
- r = (int8_t)(piece_y + i);
146
- if (r < 0 || r >= ROWS) continue;
147
- for (c = 0; c <= COLS - 3; c++) {
148
- a = grid[r][c]; b = grid[r][c+1]; d = grid[r][c+2];
149
- if (a != 0 && a == b && b == d) {
150
- grid[r][c] = 0;
151
- grid[r][c+1] = 0;
152
- grid[r][c+2] = 0;
153
- if (score < 65500u) score += 30;
154
- sfx_tone(0, 0x80, 0x10, 12);
155
- }
156
- }
157
- }
218
+ resolve_board();
158
219
  draw_grid();
159
220
  }
160
221
 
@@ -82,6 +82,15 @@ void main(void) {
82
82
  POKE(VIC_BORDER, 0x00);
83
83
  POKE(VIC_BG0, 0x09); /* brown road */
84
84
 
85
+ /* Clear screen RAM: a .prg starts over the BASIC screen, so the
86
+ * KERNAL's startup text (the leftover the playtest saw at the top)
87
+ * stays visible until someone wipes it. */
88
+ {
89
+ uint16_t k;
90
+ volatile uint8_t *scr = (volatile uint8_t*)0x0400;
91
+ for (k = 0; k < 1000; k++) scr[k] = 0x20;
92
+ }
93
+
85
94
  player.x = LANE1_X; player.y = 220; player.alive = 1;
86
95
  for (i = 0; i < MAX_OBS; i++) obstacles[i].alive = 0;
87
96
  spawn_timer = 0;
@@ -91,11 +91,23 @@ static void fire(void) {
91
91
  }
92
92
  }
93
93
 
94
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
95
+ * The old code derived the spawn column from spawn_timer, but the caller
96
+ * resets spawn_timer just before calling here, so it was CONSTANT and
97
+ * every enemy spawned in the same left column/lane. */
98
+ static uint8_t rng_state = 0xA5;
99
+ static uint8_t rand8(void) {
100
+ uint8_t lsb = (uint8_t)(rng_state & 1);
101
+ rng_state >>= 1;
102
+ if (lsb) rng_state ^= 0xB8;
103
+ return rng_state;
104
+ }
105
+
94
106
  static void spawn(void) {
95
107
  uint8_t i;
96
108
  for (i = 0; i < MAX_ENEMIES; i++) {
97
109
  if (!enemies[i].alive) {
98
- enemies[i].x = (uint8_t)(48 + ((spawn_timer * 37) & 0xFF) % 240);
110
+ enemies[i].x = (uint8_t)(24 + (rand8() % 232));
99
111
  enemies[i].y = 30;
100
112
  enemies[i].alive = 1;
101
113
  return;
@@ -96,7 +96,11 @@ void main(void) {
96
96
 
97
97
  /* P1 paddle on left, P2 on right. */
98
98
  POKE(VIC_SPRITE_X(0), 30);
99
- POKE(VIC_SPRITE_X(1), 240);
99
+ /* P2 at X=310 — the right side of the REAL court. The old 240 sat only
100
+ * ~2/3 across the screen ("computer player too close to center"): C64
101
+ * sprite X is 9-bit, the extra bit lives in $D010, and the court design
102
+ * had been squeezed into 8 bits. */
103
+ POKE(VIC_SPRITE_X(1), 310 - 256);
100
104
 
101
105
  sfx_init();
102
106
  POKE(VIC_SPR_ENA, 0x07);
@@ -122,16 +126,17 @@ void main(void) {
122
126
  bdx = -bdx; sfx_tone(0, 0x40, 0x20, 3);
123
127
  }
124
128
  /* Paddle 2 collision */
125
- if (bdx > 0 && bx > 232 && bx < 248 && by > p2y - 8 && by < p2y + 22) {
129
+ if (bdx > 0 && bx > 296 && bx < 314 && by > p2y - 8 && by < p2y + 22) {
126
130
  bdx = -bdx; sfx_tone(0, 0x40, 0x20, 3);
127
131
  }
128
132
  /* Score */
129
- if (bx < 5) { p2_score++; if (p2_score > 9) p2_score = 0; sfx_noise(20); bx = 150; by = 130; bdx = 2; }
130
- if (bx > 250) { p1_score++; if (p1_score > 9) p1_score = 0; sfx_tone(0, 0x80, 0x10, 16); bx = 150; by = 130; bdx = -2; }
133
+ if (bx < 5) { p2_score++; if (p2_score > 9) p2_score = 0; sfx_noise(20); bx = 170; by = 130; bdx = 2; }
134
+ if (bx > 330) { p1_score++; if (p1_score > 9) p1_score = 0; sfx_tone(0, 0x80, 0x10, 16); bx = 170; by = 130; bdx = -2; }
131
135
 
132
136
  POKE(VIC_SPRITE_Y(0), p1y);
133
137
  POKE(VIC_SPRITE_Y(1), p2y);
134
138
  POKE(VIC_SPRITE_X(2), (uint8_t)bx);
139
+ POKE(VIC_SPRITES_X8, (uint8_t)(0x02 | ((bx > 255) ? 0x04 : 0x00)));
135
140
  POKE(VIC_SPRITE_Y(2), (uint8_t)by);
136
141
  }
137
142
  }
@@ -116,10 +116,11 @@ void main(void) {
116
116
  const Rect *p;
117
117
  const int16_t GRAVITY = 10;
118
118
  const int16_t MOVE = 20;
119
- const int16_t JUMP = -180;
119
+ const int16_t JUMP = -140; /* was -180: ~100px peak (most of the screen) — 'jumps a little too high' */
120
120
  const int16_t MAXFALL = 280;
121
121
 
122
122
  lcd_init_default();
123
+ sound_init();
123
124
  enable_vblank_irq(); /* MANDATORY: HALT-driven wait_vblank. Without this,
124
125
  * busy-poll wait_vblank runs ~1/30 speed on the WASM
125
126
  * emulator and the game loop appears to hang. */
@@ -169,7 +170,10 @@ void main(void) {
169
170
  if (pad & PAD_RIGHT) vx = MOVE;
170
171
 
171
172
  grounded = on_platform(ipx, ipy);
172
- if ((pad & PAD_A) && !(prev & PAD_A) && grounded) vy = JUMP;
173
+ if ((pad & PAD_A) && !(prev & PAD_A) && grounded) {
174
+ vy = JUMP;
175
+ sound_play_tone(1, 1750, 8); /* jump blip (ch2 square) */
176
+ }
173
177
  prev = pad;
174
178
 
175
179
  vy += GRAVITY;