romdevtools 0.27.0 → 0.29.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 (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,213 +1,702 @@
1
- /* ── platformer/main.c — MSX SIDE-SCROLLING platformer scaffold ──────
1
+ /* ── platformer/main.c — MSX side-scrolling platformer (complete example game) ─
2
2
  *
3
- * Mirrors the SMS/GB/etc platformer scaffolds, translated to the MSX VDP
4
- * via the romdev helper lib (msx_hw.h + msx_vdp.c).
3
+ * MESA HOPPER a COMPLETE, working game: title screen, 1P mode and 2P
4
+ * ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
5
+ * has its OWN score and OWN 3 lives; player 2 plays on JOYSTICK PORT 2),
6
+ * coins + traversal scoring, session hi-score, music + SFX on the AY-3-8910
7
+ * PSG, gravity/jump/one-way-platform physics — and the MSX's signature
8
+ * SCREEN-2 PER-ROW COLOR: the level's depth bands (far sky, mid air, near
9
+ * ground) and the title recolor come ENTIRELY from the three independent
10
+ * color thirds + a one-tile vertical gradient, costing zero extra tiles.
5
11
  *
6
- * The world is 512 px wide (64 cells); the screen-2 name table is only 32
7
- * cells (256 px) and wraps, so a world wider than one screen needs COLUMN
8
- * STREAMING: each time the camera crosses an 8-px boundary we rewrite the
9
- * name-table column about to scroll into view with the next world column's
10
- * tiles. Screen 2 has no smooth-pixel-scroll register, so the camera moves
11
- * in whole 8-px cells — the streaming gives a clean tile-by-tile scroll.
12
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game even a
13
+ * very different one. The markers tell you what's what:
14
+ * HARDWARE IDIOM (load-bearing) dodges a documented MSX footgun; reshape
15
+ * your gameplay around it (see TROUBLESHOOTING before changing).
16
+ * GAME LOGIC (clay) level layout, physics tuning, scoring, art: reshape
17
+ * freely.
12
18
  *
13
- * Subpixel state (x/y in 1/16-pixel units) for fine gravity/acceleration;
14
- * the player sprite draws in SCREEN space ((worldX>>4) - camX).
19
+ * What depends on what:
20
+ * msx_hw.h / msx_vdp.c VDP + PSG + joystick helpers (direct Z80 ports;
21
+ * the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
22
+ * read msx_vdp.c before adding your own PSG pokes).
23
+ * msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
24
+ * INIT must NEVER return, so main() ends in for(;;).
15
25
  *
16
- * Controls: joystick LEFT/RIGHT walks, trigger A jumps (only when grounded).
26
+ * The level: a FIXED 32-cell-wide screen-2 arena (NOT a scroller — see the
27
+ * "no hardware scroll" idiom below). The column map gives every screen column
28
+ * a ground height + an optional one-way platform; pits (gaps in the ground)
29
+ * are instant death, spikes patrol the ground, a coin floats over a platform.
30
+ * The player walks the full width of the screen, hops platforms, and banks
31
+ * coins + a traversal "distance" tick. A run ends when all of the current
32
+ * player's lives are gone.
17
33
  *
18
- * Cartridge rule: INIT must never return main() ends in for(;;).
34
+ * Controls: JOYSTICK PORT 1 (or keyboard cursors) LEFT/RIGHT walks, trigger A
35
+ * jumps (only when grounded). In 2P alternating-turns mode, player 2 plays
36
+ * on JOYSTICK PORT 2 when it is their turn. On the title screen trigger A
37
+ * starts 1P; trigger B (port-1 button 2) starts 2P turns.
38
+ *
39
+ * Hi-score honesty: the bundled bluemsx core build exposes NO battery save
40
+ * path (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so the
41
+ * hi-score lives in plain RAM: it survives title↔game cycles but NOT a
42
+ * power cycle / hardReset. Never fake persistence — if you need real saves,
43
+ * that's a future core round (ASCII8-SRAM mapper carts exist; the core just
44
+ * doesn't surface their RAM yet).
19
45
  */
20
46
  #include "msx_hw.h"
21
47
 
22
- /* ── interrupt-free vblank sync (poll VDP status S#0 bit 7) ────────────── */
48
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
49
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
50
+ #define GAME_TITLE "MESA HOPPER"
51
+
52
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
53
+ * Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
54
+ * the port ALSO clears the flag, so one read per frame = one game step per
55
+ * frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
56
+ * works even with interrupts masked, and never depends on the BIOS ISR
57
+ * keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
58
+ * flag first and this loop just waits for the next one; a one-frame hiccup,
59
+ * never a hang.) */
23
60
  __sfr __at 0x99 VDPSTATUS;
24
61
  static void vsync(void) {
25
- (void)VDPSTATUS;
62
+ (void)VDPSTATUS; /* throw away a possibly-stale flag */
26
63
  while (!(VDPSTATUS & 0x80)) {
27
64
  }
28
65
  }
29
- /* jump uses the BIOS GTTRIG wrapper (gttrig) provided by msx_hw.h. */
30
66
 
31
- #define T_OPEN 0
32
- #define T_WALL 1
67
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
68
+ * NO HARDWARE SCROLL ON SCREEN 2. The TMS9918 GRAPHIC-II mode has no smooth
69
+ * pixel-scroll register at all (the V9938's R23 is a whole-screen vertical
70
+ * line shift, not a per-layer camera, and MSX1 lacks even that). The ONLY way
71
+ * to "scroll" is to rewrite the name table column-by-column every 8-px step —
72
+ * a heavy per-frame VRAM burst that ALSO fights the per-row color idiom below
73
+ * (each scrolled column needs its color third re-evaluated).
74
+ *
75
+ * So this platformer uses a FIXED single-screen arena: the camera never moves,
76
+ * the whole level is one 32-cell painting, and the player traverses it left to
77
+ * right. That keeps the screen-2 per-row color signature CHEAP (the color
78
+ * tables upload ONCE) and the frame budget roomy. If you want a true scroller,
79
+ * budget a column-streaming routine and re-upload the affected third's color
80
+ * slice as columns enter — see TROUBLESHOOTING; it is the single biggest MSX
81
+ * platformer footgun. */
82
+
83
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
84
+ * Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
85
+ * level tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits draw in
86
+ * the tile's FOREGROUND color, clear bits in its BACKGROUND color (both come
87
+ * from the screen-2 color table — see the per-row-color idiom below). */
88
+ #define T_SPACE 0
89
+ #define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
90
+ #define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
91
+ #define T_DASH 37
92
+ #define T_SKY 38 /* empty air cell (pattern all 0 = all bg) */
93
+ #define T_CLOUD 39 /* faint cloud puff in the sky band */
94
+ #define T_GRASS 40 /* platform / ground surface (grassy top) */
95
+ #define T_DIRT 41 /* ground body below the surface */
96
+ #define T_HORIZON 42 /* the per-8x1-row gradient strip (see below) */
97
+ #define NUM_TILES 43
98
+
99
+ static const uint8_t font[NUM_TILES][8] = {
100
+ /* SPACE */ {0,0,0,0,0,0,0,0},
101
+ /* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
102
+ /* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
103
+ /* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
104
+ /* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
105
+ /* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
106
+ /* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
107
+ /* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
108
+ /* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
109
+ /* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
110
+ /* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
111
+ /* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
112
+ /* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
113
+ /* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
114
+ /* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
115
+ /* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
116
+ /* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
117
+ /* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
118
+ /* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
119
+ /* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
120
+ /* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
121
+ /* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
122
+ /* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
123
+ /* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
124
+ /* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
125
+ /* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
126
+ /* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
127
+ /* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
128
+ /* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
129
+ /* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
130
+ /* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
131
+ /* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
132
+ /* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
133
+ /* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
134
+ /* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
135
+ /* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
136
+ /* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
137
+ /* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
138
+ /* 38 SKY (all bg) */ {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
139
+ /* 39 CLOUD (soft puff) */ {0x00,0x00,0x18,0x3C,0x7E,0x00,0x00,0x00},
140
+ /* 40 GRASS (grassy top) */ {0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00},
141
+ /* 41 DIRT (solid body) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
142
+ /* 42 HORIZON(solid fg — its COLOR bytes paint the gradient) */
143
+ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
144
+ };
33
145
 
34
- #define WORLD_COLS 64
35
- #define WORLD_W (WORLD_COLS * 8)
36
- #define SCREEN_W 256
37
- #define VIS_ROWS 24
146
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
147
+ * SCREEN-2 PER-ROW COLOR — the MSX's signature background trick.
148
+ *
149
+ * Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
150
+ *
151
+ * 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
152
+ * (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
153
+ * pattern table slice and its OWN 2KB color table slice:
154
+ * patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
155
+ * The SAME tile index can look completely different in each third —
156
+ * we exploit exactly that for the depth-banded level below.
157
+ *
158
+ * 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
159
+ * row — each packing (foreground<<4)|background from the fixed TMS9918
160
+ * palette. So one tile can carry an 8-color vertical gradient
161
+ * (T_HORIZON's whole "glow horizon" line is a single tile, colors only).
162
+ *
163
+ * Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
164
+ * R4=0x03 — the "thirds" configuration), and pattern + color uploads to
165
+ * EVERY third a tile is used in. Tile N's slot is pattern[N*8] / color[N*8].
166
+ *
167
+ * Depth scheme taught here (TMS9918 fixed palette: 1 black, 4 dark blue,
168
+ * 5 light blue, 6 dark red, 8 cyan/medium-red, 12 dark green, 14 gray,
169
+ * 15 white — the high nibble is fg, low nibble is bg of each row byte):
170
+ * third 0 (top) = far sky: light-blue field, white clouds — the HUD
171
+ * text band (row 0) also lives here in its own colors.
172
+ * third 1 (middle) = mid air: black field, gray clouds — the play space.
173
+ * third 2 (bottom) = near ground: green grass on brown dirt, the platforms
174
+ * and the one-tile horizon gradient seam.
175
+ * The HUD text band (row 0, third 0) gets white-on-blue, distinct from the
176
+ * sky below it, WITHOUT costing any extra tiles. */
177
+ static const uint8_t col_text[3] = { 0xF4, 0xF1, 0xF1 }; /* HUD/title white-on-blue; mid+near white-on-black */
178
+ static const uint8_t col_sky[3] = { 0x55, 0x11, 0x18 }; /* the 3 depth bands (bg shows: pattern is all 0) */
179
+ static const uint8_t col_cloud[3] = { 0xF5, 0xE1, 0x55 }; /* cloud puff per band: white/gray over its band bg */
180
+ static const uint8_t col_grass[3] = { 0xC5, 0xC1, 0xC6 }; /* grassy top: dark-green fg over band bg / over brown */
181
+ static const uint8_t col_dirt[3] = { 0x66, 0x66, 0x66 }; /* solid dirt body: dark-red/brown everywhere */
182
+ /* T_HORIZON: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row glow
183
+ * gradient (dark blue → light blue → cyan → white and back down). The pattern
184
+ * is solid 0xFF so only the fg nibbles show. Drawn as the seam row between the
185
+ * air thirds and the ground. */
186
+ static const uint8_t col_horizon[8] = { 0x44,0x55,0x88,0xF8,0x85,0x54,0x41,0x11 };
38
187
 
39
- /* background tile patterns (8x8) */
40
- static const uint8_t TILE_OPEN[8] = {0,0,0,0,0,0,0,0};
41
- static const uint8_t TILE_WALL[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
188
+ static void load_tiles(void) {
189
+ uint8_t third, i;
190
+ uint16_t patbase, colbase;
191
+ for (third = 0; third < 3; third++) {
192
+ patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
193
+ colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
194
+ for (i = 0; i < NUM_TILES; i++) {
195
+ uint8_t col;
196
+ /* pattern bits are the same in every third — only COLOR varies */
197
+ msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
198
+ if (i == T_HORIZON) { /* the one per-pixel-row gradient */
199
+ msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_horizon, 8);
200
+ continue;
201
+ }
202
+ if (i == T_SKY) col = col_sky[third];
203
+ else if (i == T_CLOUD) col = col_cloud[third];
204
+ else if (i == T_GRASS) col = col_grass[third];
205
+ else if (i == T_DIRT) col = col_dirt[third];
206
+ else col = col_text[third];
207
+ msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
208
+ }
209
+ }
210
+ }
42
211
 
43
- #define COL_OPEN 0x14 /* light-blue "open" (sky) on dark blue */
44
- #define COL_WALL 0xE4 /* grey wall on dark blue */
212
+ /* ── GAME LOGIC (clay — reshape freely) name-table drawing helpers ────────
213
+ * Screen 2 VRAM writes are safe at any point in the frame at C speed: the
214
+ * TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
215
+ * and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
216
+ * are the thing that outruns the VDP — see TROUBLESHOOTING.) */
217
+ static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
218
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
219
+ }
45
220
 
46
- /* player sprite (8x8) */
47
- static const uint8_t SPR_PLAYER[8] = {0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C};
48
- #define COL_PLAYER 9 /* red-ish */
221
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
222
+ uint8_t buf[32];
223
+ uint8_t n = 0;
224
+ while (*s && n < 32) {
225
+ char c = *s++;
226
+ if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
227
+ else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
228
+ else if (c == '-') buf[n] = T_DASH;
229
+ else buf[n] = T_SPACE;
230
+ n++;
231
+ }
232
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
233
+ }
49
234
 
50
- typedef struct { int16_t x, y, w, h; } Rect;
235
+ static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
236
+ uint8_t buf[4];
237
+ buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
238
+ buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
239
+ buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
240
+ buf[3] = (uint8_t)(T_0 + v % 10);
241
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
242
+ }
51
243
 
52
- /* platforms in WORLD coords, spread across the 512-px world */
53
- static const Rect platforms[] = {
54
- { 0, 176, 512, 16 }, /* floor spans the world */
55
- { 32, 144, 56, 8 },
56
- { 120, 144, 64, 8 },
57
- { 200, 112, 48, 8 },
58
- { 56, 96, 40, 8 },
59
- { 288, 136, 64, 8 },
60
- { 384, 104, 56, 8 },
61
- { 440, 152, 48, 8 },
62
- { 320, 72, 48, 8 }
244
+ /* ── GAME LOGIC (clay reshape freely) — the level (a fixed 32-cell map) ───
245
+ * One entry per screen column:
246
+ * ground_row[c] name-table row of the ground's grassy top; NO_GROUND = a
247
+ * bottomless pit (fall in = lose the turn).
248
+ * plat_row[c] — row of a one-way floating platform, 0 = none.
249
+ * Name-table rows: 0 = HUD band, 1..GROUND_ROW-1 = air, then ground.
250
+ * The level is one fixed screen — reshape these two tables freely. */
251
+ #define NO_GROUND 0xFF
252
+ #define HORIZON_ROW 17 /* the one-tile gradient seam row */
253
+ #define GROUND_ROW 20 /* default grassy ground top */
254
+ static const uint8_t ground_row[32] = {
255
+ 20, 20, 20, 20, 20, 20, /* start ledge */
256
+ NO_GROUND, NO_GROUND, NO_GROUND, /* pit 1 (24 px) */
257
+ 20, 20, 20, 20, 20, /* mid ledge */
258
+ NO_GROUND, NO_GROUND, /* pit 2 (16 px) */
259
+ 20, 20, 20, 20, 20, 20, /* long ledge */
260
+ NO_GROUND, NO_GROUND, NO_GROUND, /* pit 3 (24 px) */
261
+ 20, 20, 20, 20, 20, 20, /* finish ledge */
262
+ };
263
+ static const uint8_t plat_row[32] = {
264
+ 0, 0, 0, 0, 0, 0,
265
+ 0, 14, 14, /* slab spanning pit 1 */
266
+ 0, 0, 0, 0, 0,
267
+ 13, 13, /* slab over pit 2 */
268
+ 0, 0, 0, 0, 0, 0,
269
+ 0, 14, 14, /* slab over pit 3 */
270
+ 0, 0, 0, 0, 0, 0,
63
271
  };
64
- #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
65
272
 
66
- static int16_t px, py; /* player pos, 1/16-px units */
67
- static int16_t vx, vy;
68
- static int16_t camX; /* camera X in pixels (cell-aligned) */
69
- static int16_t lastCamCol;
273
+ /* ── GAME LOGIC (clay reshape freely) — sprites ────────────────────────────
274
+ * 8x8 one-color hardware sprites. Plane layout (lower plane = on top):
275
+ * 0 player, 1 coin, 2-3 spikes. */
276
+ static const uint8_t spr_player_idle[8] = {0x18,0x3C,0x7E,0x7E,0xFF,0xFF,0x66,0x66};
277
+ static const uint8_t spr_player_jump[8] = {0x18,0x7E,0xFF,0xE7,0xC3,0x81,0x42,0x24};
278
+ static const uint8_t spr_coin[8] = {0x3C,0x7E,0xDB,0xFF,0xFF,0xDB,0x7E,0x3C};
279
+ static const uint8_t spr_spike[8] = {0x10,0x10,0x38,0x38,0x7C,0x7C,0xFE,0xFE};
280
+ #define PAT_IDLE 0
281
+ #define PAT_JUMP 1
282
+ #define PAT_COIN 2
283
+ #define PAT_SPIKE 3
284
+ #define COL_PLAYER1 15 /* white */
285
+ #define COL_PLAYER2 10 /* dark yellow */
286
+ #define COL_COIN 10 /* dark yellow (gold-ish) */
287
+ #define COL_SPIKE 9 /* light red */
70
288
 
71
- static void load_tiles(void) {
72
- uint8_t third;
73
- uint16_t poff;
74
- for (third = 0; third < 3; third++) {
75
- poff = (uint16_t)((uint16_t)third << 11);
76
- msx_vram_write((uint16_t)(VRAM_PATTERN + poff + 0), TILE_OPEN, 8);
77
- msx_vram_write((uint16_t)(VRAM_PATTERN + poff + 8), TILE_WALL, 8);
78
- msx_fill_vram((uint16_t)(VRAM_COLOR + poff + 0), 8, COL_OPEN);
79
- msx_fill_vram((uint16_t)(VRAM_COLOR + poff + 8), 8, COL_WALL);
289
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
290
+ * Sprite limits + the Y=208 terminator:
291
+ * - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
292
+ * attribute table every higher-numbered plane vanishes, not just that
293
+ * one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
294
+ * END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
295
+ * PARK_Y (192 = first line below the display) never at 0xD0.
296
+ * (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
297
+ * is "just offscreen" code that leans on that breaks on MSX1.)
298
+ * - Per scanline the TMS9918 draws only 4 sprites (V9938: 8); the rest drop
299
+ * out for that line. This game peaks at 4 planes (player + coin + 2
300
+ * spikes) so a row pileup is rare. */
301
+ #define PARK_Y 192
302
+
303
+ #define NUM_SPIKES 2
304
+
305
+ /* ── GAME LOGIC (clay — reshape freely) — physics + tuning ──────────────────
306
+ * Player position is screen-pixel X; Y is Q4.4 fixed point so gravity can add
307
+ * <1 px/frame near the jump apex. */
308
+ #define GRAVITY_Q44 6 /* +6/16 px per frame per frame */
309
+ #define JUMP_VEL_Q44 (-56) /* launch vy (Q4.4) → ~4-tile apex */
310
+ #define MAX_VY_Q44 64 /* terminal velocity, 4 px/frame — MUST stay *
311
+ * under 6: the landing probe's window can't *
312
+ * catch a faster fall (tunnelling) */
313
+ #define MOVE_SPEED 2 /* px/frame walk */
314
+ #define GROUND_TOP (GROUND_ROW * 8) /* 160: grassy top pixel row */
315
+ #define PLAYER_LEFT 8 /* walk bounds */
316
+ #define PLAYER_RIGHT 240
317
+
318
+ /* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────── */
319
+ static uint8_t px; /* player screen x (pixels) */
320
+ static uint16_t py_q44; /* player y, Q4.4 fixed point */
321
+ static int8_t vy_q44;
322
+ static uint8_t on_ground;
323
+ static uint8_t coin_x, coin_y, coin_live;
324
+ static uint8_t spike_x[NUM_SPIKES];
325
+ static int8_t spike_vx[NUM_SPIKES];
326
+
327
+ /* Players: index 0 = P1 (joystick port 1), 1 = P2 (joystick port 2 —
328
+ * alternating turns, arcade-classic style). Each has its own score + own
329
+ * lives; the HUD shows the CURRENT player's numbers. */
330
+ static uint8_t two_player; /* mode chosen on the title screen */
331
+ static uint8_t cur_player;
332
+ static uint8_t p_lives[2];
333
+ static uint16_t p_score[2];
334
+ static uint16_t hiscore; /* SESSION-ONLY: plain RAM. The bundled
335
+ * bluemsx build exposes no SAVE_RAM region,
336
+ * so there is nothing battery-backed to
337
+ * write — survives title↔game cycles, not a
338
+ * power cycle (honest, not faked). */
339
+ static uint8_t turn_pause; /* freeze frames after a turn change */
340
+ static uint8_t dist_sub; /* sub-counter: traversal pays a point */
341
+ static uint16_t rng;
342
+
343
+ #define ST_TITLE 0
344
+ #define ST_PLAY 1
345
+ #define ST_OVER 2
346
+ static uint8_t state;
347
+ static uint8_t prev_t1, prev_t2; /* trigger edge detection across states */
348
+ static uint8_t prev_jump; /* per-turn jump edge (cur_player's port) */
349
+
350
+ /* xorshift16 PRNG — a few dozen cycles, no tables. */
351
+ static uint8_t next_rand(void) {
352
+ rng ^= (uint16_t)(rng << 7);
353
+ rng ^= (uint16_t)(rng >> 9);
354
+ rng ^= (uint16_t)(rng << 8);
355
+ return (uint8_t)(rng & 0xFF);
356
+ }
357
+
358
+ /* ── GAME LOGIC (clay — reshape freely) — music + SFX on the AY-3-8910 ──────
359
+ * Channel plan: A = jump/coin/land blips, B = death noise, C = music. The PSG
360
+ * has 3 tone channels + ONE shared noise generator, mixed per-channel in
361
+ * reg 7. All register traffic goes through msx_psg_tone/noise/off — they wrap
362
+ * the PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT ISR clobbers the
363
+ * PSG address latch every frame (the bug that once silenced every MSX scaffold
364
+ * — see msx_vdp.c).
365
+ *
366
+ * The tune: one period entry per half-beat, 0 = rest. AY period =
367
+ * 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame; a
368
+ * note advances every 8 frames. The lib's built-in demo loop (msx_music_tick)
369
+ * also uses channel C, so we switch it OFF in main() and run THIS table
370
+ * instead — edit this table to rescore. */
371
+ static const uint16_t tune[32] = {
372
+ 427, 0, 339, 0, 285, 0, 339, 0, /* C4 E4 G4 E4 (bright major bounce) */
373
+ 254, 0, 285, 339, 285, 0, 0, 0, /* A4 G4 E4 G4 rest */
374
+ 320, 0, 285, 0, 254, 0, 285, 0, /* F4 G4 A4 G4 */
375
+ 339, 0, 285, 0, 427, 0, 0, 0, /* E4 G4 C4 rest */
376
+ };
377
+ static uint8_t music_step, music_timer;
378
+ static uint8_t sfx_a_t, sfx_b_t; /* frames left on the A/B SFX channels */
379
+
380
+ static void music_tick(void) {
381
+ if (music_timer == 0) {
382
+ uint16_t p = tune[music_step & 31];
383
+ if (p) msx_psg_tone(2, p, 9);
384
+ else msx_psg_off(2);
385
+ music_step++;
80
386
  }
387
+ music_timer++;
388
+ if (music_timer >= 8) music_timer = 0;
81
389
  }
82
390
 
83
- static uint8_t cell_is_wall(int16_t col, uint8_t row) {
84
- int16_t cx = (int16_t)(col << 3);
85
- int16_t cy = (int16_t)((int16_t)row << 3);
86
- uint8_t i;
87
- const Rect *p;
88
- for (i = 0; i < N_PLATFORMS; i++) {
89
- p = &platforms[i];
90
- if (cx + 8 > p->x && cx < p->x + p->w
91
- && cy + 8 > p->y && cy < p->y + p->h) return 1;
391
+ static void sfx_tick(void) {
392
+ if (sfx_a_t) { sfx_a_t--; if (!sfx_a_t) msx_psg_off(0); }
393
+ if (sfx_b_t) { sfx_b_t--; if (!sfx_b_t) msx_psg_noise(1, 0, 0); }
394
+ }
395
+
396
+ static void sfx_jump(void) { msx_psg_tone(0, 0x110, 11); sfx_a_t = 4; }
397
+ static void sfx_coin(void) { msx_psg_tone(0, 0x090, 12); sfx_a_t = 6; }
398
+ static void sfx_land(void) { msx_psg_tone(0, 0x250, 8); sfx_a_t = 3; }
399
+ static void sfx_die(void) { msx_psg_noise(1, 24, 14); sfx_b_t = 18; }
400
+
401
+ /* ── GAME LOGIC (clay — reshape freely) — HUD ──────────────────────────────
402
+ * Row 0 = the HUD band (third 0's text colors make it a distinct strip).
403
+ * P#=current player, SC=score, HI=hi-score, LV=lives. */
404
+ static void draw_hud_labels(void) {
405
+ draw_text(1, 0, "P");
406
+ draw_text(5, 0, "SC");
407
+ draw_text(15, 0, "HI");
408
+ draw_text(25, 0, "LV");
409
+ }
410
+ static void draw_player(void) { put_tile(2, 0, (uint8_t)(T_0 + 1 + cur_player)); }
411
+ static void draw_score(void) { draw_num4(8, 0, p_score[cur_player]); }
412
+ static void draw_hi(void) { draw_num4(18, 0, hiscore); }
413
+ static void draw_lives(void) { put_tile(28, 0, (uint8_t)(T_0 + p_lives[cur_player])); }
414
+ static void draw_hud(void) { draw_player(); draw_score(); draw_hi(); draw_lives(); }
415
+
416
+ /* ── GAME LOGIC (clay — reshape freely) — paint the fixed level ─────────────
417
+ * The whole arena is one 32x24 painting. Row 0 is the HUD band; the sky thirds
418
+ * get scattered clouds; HORIZON_ROW is the one-tile gradient seam; the
419
+ * ground/platforms come from the column map. The per-third color tables (set
420
+ * once in load_tiles) give the depth bands for free. */
421
+ static void paint_field(void) {
422
+ uint8_t row, col, t;
423
+ uint8_t buf[32];
424
+ msx_fill_vram(VRAM_NAME, 32, T_SPACE); /* row 0: HUD band */
425
+ for (row = 1; row < 24; row++) {
426
+ for (col = 0; col < 32; col++) {
427
+ uint8_t g = ground_row[col];
428
+ t = T_SKY;
429
+ if (row == HORIZON_ROW) t = T_HORIZON;
430
+ else if (row == plat_row[col]) t = T_GRASS; /* slab */
431
+ else if (g != NO_GROUND) {
432
+ if (row == g) t = T_GRASS; /* surface */
433
+ else if (row > g) t = T_DIRT; /* body */
434
+ }
435
+ if (t == T_SKY && row < HORIZON_ROW) { /* clouds */
436
+ if (((row * 7 + col * 5) & 15) == 0) t = T_CLOUD;
437
+ }
438
+ buf[col] = t;
439
+ }
440
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32), buf, 32);
92
441
  }
93
- return 0;
94
442
  }
95
443
 
96
- static uint8_t on_platform(int16_t ipx, int16_t ipy) {
444
+ /* ── GAME LOGIC (clay reshape freely) — screens ──────────────────────────
445
+ * Title rows land in third 1 (white-on-black) and third 2 — the same glyph
446
+ * tiles as the HUD, recolored for free by the thirds idiom. */
447
+ static void paint_title(void) {
448
+ uint8_t len = 0, col;
449
+ const char *p = GAME_TITLE;
450
+ while (*p++) len++;
451
+ col = (uint8_t)((32 - len) / 2);
452
+ paint_field();
453
+ draw_text(col, 6, GAME_TITLE);
454
+ draw_text(7, 10, "1P START - FIRE A");
455
+ draw_text(7, 12, "2P TURNS - FIRE B");
456
+ draw_text(12, 14, "HI 0000"); /* the space blanks the cell between */
457
+ draw_num4(15, 14, hiscore);
458
+ }
459
+
460
+ /* ── GAME LOGIC (clay — reshape freely) — start a turn / a run ── */
461
+ static void begin_turn(void) {
97
462
  uint8_t i;
98
- const Rect *p;
99
- for (i = 0; i < N_PLATFORMS; i++) {
100
- p = &platforms[i];
101
- if (ipy + 8 == p->y && ipx + 8 > p->x && ipx < p->x + p->w) return 1;
463
+ px = 24;
464
+ py_q44 = (uint16_t)((uint16_t)(GROUND_TOP - 8) << 4);
465
+ vy_q44 = 0;
466
+ on_ground = 1;
467
+ coin_x = 120; coin_y = (uint8_t)(13 * 8); coin_live = 1;
468
+ for (i = 0; i < NUM_SPIKES; i++) {
469
+ spike_x[i] = (uint8_t)(64 + i * 96);
470
+ spike_vx[i] = (i & 1) ? -1 : 1;
102
471
  }
103
- return 0;
472
+ turn_pause = 24; /* "P# ready" breather (blinks ~0.6-0.8s) */
473
+ prev_jump = 1; /* swallow a held jump across the turn */
474
+ paint_field();
475
+ draw_hud_labels();
476
+ draw_hud();
104
477
  }
105
478
 
106
- /* write one world column into its wrapped name-table column */
107
- static void paint_column(int16_t worldCol) {
108
- uint8_t ntCol, row, tile;
109
- uint16_t addr;
110
- if (worldCol < 0 || worldCol >= WORLD_COLS) return;
111
- ntCol = (uint8_t)(worldCol & 31);
112
- for (row = 0; row < VIS_ROWS; row++) {
113
- tile = cell_is_wall(worldCol, row) ? T_WALL : T_OPEN;
114
- addr = (uint16_t)(VRAM_NAME + (uint16_t)row * 32 + ntCol);
115
- msx_vram_write(addr, &tile, 1);
479
+ static void start_game(uint8_t players) {
480
+ two_player = players;
481
+ cur_player = 0;
482
+ p_score[0] = p_score[1] = 0;
483
+ p_lives[0] = 3;
484
+ p_lives[1] = players ? 3 : 0;
485
+ dist_sub = 0;
486
+ begin_turn();
487
+ sfx_coin(); /* start chirp */
488
+ state = ST_PLAY;
489
+ }
490
+
491
+ static void game_over(void) {
492
+ uint16_t best = p_score[0];
493
+ if (two_player && p_score[1] > best) best = p_score[1];
494
+ if (best > hiscore) { hiscore = best; }
495
+ draw_text(11, 9, "GAME OVER");
496
+ draw_text(9, 11, "P1"); draw_num4(13, 11, p_score[0]);
497
+ if (two_player) { draw_text(9, 13, "P2"); draw_num4(13, 13, p_score[1]); }
498
+ draw_text(8, 15, "FIRE FOR TITLE");
499
+ prev_t1 = prev_t2 = 1; /* swallow a fire still held from play */
500
+ state = ST_OVER;
501
+ }
502
+
503
+ /* ── GAME LOGIC (clay — reshape freely) — death + alternating-turn handoff ── */
504
+ static void kill_player(void) {
505
+ uint8_t other;
506
+ sfx_die();
507
+ if (p_lives[cur_player] > 0) --p_lives[cur_player];
508
+ if (two_player) {
509
+ other = (uint8_t)(cur_player ^ 1);
510
+ if (p_lives[other] > 0) cur_player = other; /* swap turn */
511
+ else if (p_lives[cur_player] == 0) { game_over(); return; }
512
+ } else if (p_lives[0] == 0) {
513
+ game_over();
514
+ return;
116
515
  }
516
+ begin_turn();
117
517
  }
118
518
 
119
- static void paint_initial(void) {
120
- int16_t c;
121
- for (c = 0; c < 32; c++) paint_column(c);
519
+ /* ── GAME LOGIC (clay — reshape freely) — landing probe against the map ─────
520
+ * One-way platforms: only catch the player's feet while FALLING through a
521
+ * narrow window at a surface top. Window is top-1..top+4 (the -1 slack keeps
522
+ * on_ground stable while the sub-pixel gravity trickle hasn't moved integer Y
523
+ * yet; the +4 stops a 4 px/frame fall stepping over a 1-row surface). */
524
+ static uint8_t land_top(uint8_t c, uint8_t feet) {
525
+ uint8_t r, top;
526
+ r = plat_row[c];
527
+ if (r) {
528
+ top = (uint8_t)(r << 3);
529
+ if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
530
+ }
531
+ r = ground_row[c];
532
+ if (r != NO_GROUND) {
533
+ top = (uint8_t)(r << 3);
534
+ if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
535
+ }
536
+ return 0;
537
+ }
538
+
539
+ /* ── GAME LOGIC (clay — reshape freely) — AABB ── */
540
+ static uint8_t aabb(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
541
+ return ax < (uint8_t)(bx + 8) && (uint8_t)(ax + 8) > bx
542
+ && ay < (uint8_t)(by + 8) && (uint8_t)(ay + 8) > by;
543
+ }
544
+
545
+ /* Push every object to its sprite plane. A dead object parks at PARK_Y
546
+ * (offscreen), NEVER 0xD0 — see the sprite idiom block above. */
547
+ static void push_sprites(uint8_t player_y) {
548
+ uint8_t i;
549
+ msx_set_sprite(0, px, player_y, on_ground ? PAT_IDLE : PAT_JUMP,
550
+ cur_player ? COL_PLAYER2 : COL_PLAYER1);
551
+ msx_set_sprite(1, coin_x, coin_live ? coin_y : PARK_Y, PAT_COIN, COL_COIN);
552
+ for (i = 0; i < NUM_SPIKES; i++)
553
+ msx_set_sprite((uint8_t)(2 + i), spike_x[i],
554
+ (uint8_t)(GROUND_TOP - 8), PAT_SPIKE, COL_SPIKE);
122
555
  }
123
556
 
124
557
  void main(void) {
125
- const int16_t GRAVITY = 10;
126
- const int16_t MOVE = 24;
127
- const int16_t JUMP = -200;
128
- const int16_t MAXFALL = 280;
129
- uint8_t dir, trig, prev_trig, grounded, blip;
558
+ uint8_t dir, jump, y8, feet, c0, c1, top, i;
130
559
 
560
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
561
+ * Init order: set the video mode FIRST (INIGRP also clears VRAM — any
562
+ * upload done before it is wiped), then tiles, then sprites. The crt0's
563
+ * INIT contract means main() must NEVER return — the BIOS has nothing
564
+ * sane to fall back to — hence the for(;;) below. */
131
565
  msx_set_screen2();
132
566
  msx_clear_sprites();
133
567
  load_tiles();
134
- msx_fill_vram(VRAM_NAME, 32 * 24, T_OPEN);
135
- msx_vram_write((uint16_t)(VRAM_SPRPAT + 0), SPR_PLAYER, 8);
136
-
137
- px = (int16_t)(16 << 4);
138
- py = (int16_t)(64 << 4);
139
- vx = 0; vy = 0;
140
- camX = 0; lastCamCol = 0;
141
- prev_trig = 0; blip = 0;
568
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_IDLE * 8), spr_player_idle, 8);
569
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_JUMP * 8), spr_player_jump, 8);
570
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_COIN * 8), spr_coin, 8);
571
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_SPIKE * 8), spr_spike, 8);
142
572
 
143
- paint_initial();
573
+ msx_music(0); /* the lib's demo loop also owns channel C —
574
+ * hand the channel to OUR tune table instead */
575
+ hiscore = 0; /* session hi-score (no SAVE_RAM on this core) */
576
+ rng = 0xACE1;
577
+ (void)next_rand; /* PRNG kept for forks that add random spawns */
578
+ music_step = music_timer = 0;
579
+ sfx_a_t = sfx_b_t = 0;
580
+ prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
581
+ state = ST_TITLE;
582
+ paint_title();
144
583
 
145
584
  for (;;) {
146
- int16_t ipx, ipy, npy, sx, camCol;
147
- int32_t np;
148
- uint8_t i, landed;
149
- const Rect *p;
150
-
151
585
  vsync();
586
+ music_tick();
587
+ sfx_tick();
588
+
589
+ if (state == ST_TITLE) {
590
+ /* ── GAME LOGIC (clay) — title: trig A = 1P; trig B (port-1
591
+ * button 2, gttrig 3) = 2P alternating turns. */
592
+ uint8_t t1 = (uint8_t)(gttrig(1) || gttrig(0));
593
+ uint8_t t2 = gttrig(3);
594
+ if (t2 && !prev_t2) start_game(1);
595
+ else if (t1 && !prev_t1) start_game(0);
596
+ prev_t1 = t1; prev_t2 = t2;
597
+ continue;
598
+ }
152
599
 
153
- ipx = (int16_t)(px >> 4);
154
- ipy = (int16_t)(py >> 4);
155
-
156
- /* camera follows the player, centered, clamped, snapped to a cell */
157
- camX = (int16_t)(ipx - (SCREEN_W / 2 - 4));
158
- if (camX < 0) camX = 0;
159
- if (camX > WORLD_W - SCREEN_W) camX = (int16_t)(WORLD_W - SCREEN_W);
160
- camX = (int16_t)(camX & ~7);
161
-
162
- camCol = (int16_t)(camX >> 3);
163
- while (camCol > lastCamCol) { lastCamCol++; paint_column((int16_t)(lastCamCol + 31)); }
164
- while (camCol < lastCamCol) { lastCamCol--; paint_column(lastCamCol); }
165
-
166
- sx = (int16_t)(ipx - camX);
167
- if (sx < 0) sx = 0;
168
- if (sx > 248) sx = 248;
169
- msx_set_sprite(0, (uint8_t)sx, (uint8_t)ipy, 0, COL_PLAYER);
170
-
171
- dir = msx_read_joystick(1);
172
- if (dir == STICK_CENTER) dir = msx_read_joystick(0);
173
- trig = (uint8_t)(gttrig(1) || gttrig(2));
174
-
175
- vx = 0;
176
- if (dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL) vx = (int16_t)(-MOVE);
177
- if (dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR) vx = MOVE;
178
-
179
- grounded = on_platform(ipx, ipy);
180
- if (trig && !prev_trig && grounded) { vy = JUMP; msx_psg_tone(0, 0x180, 12); blip = 6; }
181
- prev_trig = trig;
182
-
183
- vy = (int16_t)(vy + GRAVITY);
184
- if (vy > MAXFALL) vy = MAXFALL;
185
- if (grounded && vy > 0) vy = 0;
186
-
187
- px = (int16_t)(px + vx);
188
- if (px < 0) px = 0;
189
- if (px > (int16_t)((WORLD_W - 8) << 4)) px = (int16_t)((WORLD_W - 8) << 4);
190
-
191
- np = (int32_t)py + (int32_t)vy;
192
- npy = (int16_t)(np >> 4);
193
- if (vy > 0) {
194
- landed = 0;
195
- for (i = 0; i < N_PLATFORMS; i++) {
196
- p = &platforms[i];
197
- if (ipy + 8 <= p->y && npy + 8 >= p->y
198
- && ipx + 8 > p->x && ipx < p->x + p->w) {
199
- py = (int16_t)((p->y - 8) << 4);
200
- vy = 0;
201
- landed = 1;
202
- break;
203
- }
600
+ if (state == ST_OVER) {
601
+ /* Freeze the final frame; any fire button returns to the title. */
602
+ uint8_t t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
603
+ if (t1 && !prev_t1) {
604
+ state = ST_TITLE;
605
+ msx_clear_sprites();
606
+ paint_title();
204
607
  }
205
- if (!landed) py = (int16_t)np;
608
+ prev_t1 = t1; prev_t2 = t1;
609
+ continue;
610
+ }
611
+
612
+ /* ── ST_PLAY — GAME LOGIC (clay) from here down ─────────────────── */
613
+ y8 = (uint8_t)(py_q44 >> 4);
614
+
615
+ if (turn_pause) { /* "P# ready" breather: blink + freeze */
616
+ push_sprites((turn_pause & 4) ? y8 : PARK_Y);
617
+ turn_pause--;
618
+ continue;
619
+ }
620
+
621
+ /* Input — the CURRENT player's joystick (P1 port 1 + keyboard;
622
+ * P2 port 2). GTSTCK returns 0=center then 1-8 clockwise from up. */
623
+ if (cur_player == 0) {
624
+ dir = msx_read_joystick(1);
625
+ if (dir == STICK_CENTER) dir = msx_read_joystick(0);
626
+ jump = (uint8_t)(gttrig(1) || gttrig(0));
206
627
  } else {
207
- py = (int16_t)np;
628
+ dir = msx_read_joystick(2);
629
+ jump = gttrig(2);
630
+ }
631
+
632
+ if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
633
+ && px > PLAYER_LEFT) px = (uint8_t)(px - MOVE_SPEED);
634
+ if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
635
+ && px < PLAYER_RIGHT) px = (uint8_t)(px + MOVE_SPEED);
636
+ if (jump && !prev_jump && on_ground) {
637
+ vy_q44 = JUMP_VEL_Q44;
638
+ on_ground = 0;
639
+ sfx_jump();
640
+ }
641
+ prev_jump = jump;
642
+
643
+ /* Traversal pays a point every few right-steps (cheap distance). */
644
+ if (dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR) {
645
+ if (++dist_sub >= 24) {
646
+ dist_sub = 0;
647
+ if (p_score[cur_player] < 9999) p_score[cur_player]++;
648
+ draw_score();
649
+ }
650
+ }
651
+
652
+ /* Physics: gravity + sub-pixel Y. */
653
+ if (vy_q44 < MAX_VY_Q44) vy_q44 = (int8_t)(vy_q44 + GRAVITY_Q44);
654
+ py_q44 = (uint16_t)(py_q44 + (uint16_t)(int16_t)vy_q44);
655
+ y8 = (uint8_t)(py_q44 >> 4);
656
+
657
+ /* Fell into a pit (below the floor line) → lose the turn. */
658
+ if (y8 >= GROUND_TOP + 16) {
659
+ kill_player();
660
+ continue;
661
+ }
662
+
663
+ /* Landing — probe the two columns under the player's feet. */
664
+ if (vy_q44 >= 0) {
665
+ feet = (uint8_t)(y8 + 8);
666
+ c0 = (uint8_t)(px >> 3);
667
+ c1 = (uint8_t)((px + 7) >> 3);
668
+ top = land_top(c0, feet);
669
+ if (top == 0) top = land_top(c1, feet);
670
+ if (top) {
671
+ py_q44 = (uint16_t)((uint16_t)(top - 8) << 4);
672
+ vy_q44 = 0;
673
+ if (!on_ground) sfx_land();
674
+ on_ground = 1;
675
+ } else {
676
+ on_ground = 0; /* walked off an edge */
677
+ }
678
+ }
679
+
680
+ /* Coin (collect). */
681
+ if (coin_live && aabb(px, y8, coin_x, coin_y)) {
682
+ coin_live = 0;
683
+ p_score[cur_player] += 10;
684
+ sfx_coin();
685
+ draw_score();
686
+ }
687
+
688
+ /* Spikes patrol the ground and kill on touch. */
689
+ for (i = 0; i < NUM_SPIKES; i++) {
690
+ spike_x[i] = (uint8_t)(spike_x[i] + spike_vx[i]);
691
+ if (spike_x[i] <= PLAYER_LEFT) spike_vx[i] = 1;
692
+ if (spike_x[i] >= PLAYER_RIGHT) spike_vx[i] = -1;
693
+ if (aabb(px, y8, spike_x[i], (uint8_t)(GROUND_TOP - 8))) {
694
+ kill_player();
695
+ break;
696
+ }
208
697
  }
209
- if (py > (int16_t)(192 << 4)) { py = (int16_t)(64 << 4); vy = 0; }
698
+ if (state != ST_PLAY) continue; /* kill_player may have ended it */
210
699
 
211
- if (blip) { blip--; if (!blip) msx_psg_off(0); }
700
+ push_sprites(y8);
212
701
  }
213
702
  }