romdevtools 0.16.0 → 0.22.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 (209) hide show
  1. package/AGENTS.md +75 -16
  2. package/CHANGELOG.md +316 -0
  3. package/examples/README.md +2 -0
  4. package/examples/atari2600/templates/platformer.asm +460 -0
  5. package/examples/atari2600/templates/racing.asm +463 -0
  6. package/examples/atari2600/templates/shmup.asm +386 -0
  7. package/examples/atari2600/templates/sports.asm +362 -0
  8. package/examples/atari7800/templates/default.c +49 -5
  9. package/examples/atari7800/templates/hello_sprite.c +48 -4
  10. package/examples/atari7800/templates/music_demo.c +47 -2
  11. package/examples/atari7800/templates/platformer.c +43 -4
  12. package/examples/atari7800/templates/puzzle.c +39 -4
  13. package/examples/atari7800/templates/racing.c +39 -4
  14. package/examples/atari7800/templates/shmup.c +40 -2
  15. package/examples/atari7800/templates/sports.c +36 -5
  16. package/examples/c64/templates/platformer.c +19 -5
  17. package/examples/c64/templates/puzzle.c +32 -2
  18. package/examples/c64/templates/shmup.c +28 -2
  19. package/examples/c64/templates/sports.c +30 -2
  20. package/examples/c64/templates/tile_engine.c +77 -27
  21. package/examples/gb/templates/default.c +110 -16
  22. package/examples/gb/templates/hello_sprite.c +15 -6
  23. package/examples/gb/templates/music_demo.c +36 -0
  24. package/examples/gb/templates/platformer.c +28 -6
  25. package/examples/gb/templates/puzzle.c +35 -4
  26. package/examples/gb/templates/racing.c +75 -10
  27. package/examples/gb/templates/shmup.c +41 -3
  28. package/examples/gb/templates/sports.c +51 -3
  29. package/examples/gb/templates/tile_engine.c +3 -2
  30. package/examples/gba/templates/gba_hello.c +29 -11
  31. package/examples/gba/templates/maxmod_demo.c +36 -2
  32. package/examples/gba/templates/platformer.c +3 -1
  33. package/examples/gba/templates/puzzle.c +15 -3
  34. package/examples/gba/templates/racing.c +65 -3
  35. package/examples/gba/templates/shmup.c +41 -4
  36. package/examples/gba/templates/sports.c +36 -2
  37. package/examples/gba/templates/tonc_hello.c +41 -5
  38. package/examples/gba/templates/tonc_hello_sprite.c +35 -1
  39. package/examples/gbc/templates/default.c +103 -26
  40. package/examples/gbc/templates/hello_sprite.c +12 -3
  41. package/examples/gbc/templates/music_demo.c +56 -12
  42. package/examples/gbc/templates/platformer.c +28 -6
  43. package/examples/gbc/templates/puzzle.c +35 -4
  44. package/examples/gbc/templates/racing.c +88 -21
  45. package/examples/gbc/templates/shmup.c +37 -3
  46. package/examples/gbc/templates/sports.c +48 -3
  47. package/examples/gbc/templates/tile_engine.c +3 -2
  48. package/examples/genesis/main.s +53 -1
  49. package/examples/genesis/templates/hello_sprite.c +25 -3
  50. package/examples/genesis/templates/puzzle.c +37 -3
  51. package/examples/genesis/templates/racing.c +44 -11
  52. package/examples/genesis/templates/sgdk_hello.c +34 -1
  53. package/examples/genesis/templates/shmup.c +31 -1
  54. package/examples/genesis/templates/shmup_2p.c +31 -0
  55. package/examples/genesis/templates/xgm2_demo.c +20 -0
  56. package/examples/gg/templates/default.c +56 -18
  57. package/examples/gg/templates/hello_sprite.c +25 -2
  58. package/examples/gg/templates/music_demo.c +24 -2
  59. package/examples/gg/templates/platformer.c +18 -12
  60. package/examples/gg/templates/puzzle.c +38 -7
  61. package/examples/gg/templates/racing.c +58 -9
  62. package/examples/gg/templates/shmup.c +47 -3
  63. package/examples/gg/templates/sports.c +57 -16
  64. package/examples/gg/templates/tile_engine.c +12 -6
  65. package/examples/lynx/templates/default.c +39 -8
  66. package/examples/lynx/templates/hello_sprite.c +15 -1
  67. package/examples/lynx/templates/music_demo.c +13 -1
  68. package/examples/lynx/templates/puzzle.c +28 -1
  69. package/examples/lynx/templates/racing.c +34 -7
  70. package/examples/lynx/templates/shmup.c +42 -3
  71. package/examples/lynx/templates/sports.c +29 -2
  72. package/examples/msx/platformer/main.c +213 -0
  73. package/examples/msx/puzzle/main.c +250 -0
  74. package/examples/msx/racing/main.c +249 -0
  75. package/examples/msx/shmup/main.c +288 -0
  76. package/examples/msx/sports/main.c +182 -0
  77. package/examples/nes/templates/default.c +67 -19
  78. package/examples/nes/templates/hello_sprite.c +35 -0
  79. package/examples/nes/templates/music_demo.c +40 -0
  80. package/examples/nes/templates/platformer.c +65 -6
  81. package/examples/nes/templates/puzzle.c +67 -6
  82. package/examples/nes/templates/racing.c +45 -13
  83. package/examples/nes/templates/shmup.c +51 -2
  84. package/examples/nes/templates/sports.c +51 -6
  85. package/examples/pce/catch_game/main.c +22 -3
  86. package/examples/pce/music_sfx/main.c +28 -1
  87. package/examples/pce/platformer/main.c +283 -0
  88. package/examples/pce/puzzle/main.c +304 -0
  89. package/examples/pce/racing/main.c +304 -0
  90. package/examples/pce/shmup/main.c +346 -0
  91. package/examples/pce/sports/main.c +254 -0
  92. package/examples/pce/sprite_move/main.c +7 -2
  93. package/examples/sms/main.c +35 -6
  94. package/examples/sms/templates/hello_sprite.c +29 -3
  95. package/examples/sms/templates/music_demo.c +18 -4
  96. package/examples/sms/templates/puzzle.c +34 -5
  97. package/examples/sms/templates/racing.c +39 -2
  98. package/examples/sms/templates/shmup.c +41 -2
  99. package/examples/sms/templates/shmup_2p.c +24 -1
  100. package/examples/sms/templates/sports.c +47 -4
  101. package/examples/snes/main.asm +108 -17
  102. package/examples/snes/templates/c-hello-data.asm +23 -0
  103. package/examples/snes/templates/c-hello.c +18 -1
  104. package/examples/snes/templates/default.c +50 -28
  105. package/examples/snes/templates/hello_sprite-data.asm +23 -0
  106. package/examples/snes/templates/hello_sprite.c +17 -1
  107. package/examples/snes/templates/music_demo-data.asm +23 -0
  108. package/examples/snes/templates/music_demo.c +22 -4
  109. package/examples/snes/templates/platformer-data.asm +22 -0
  110. package/examples/snes/templates/platformer.c +20 -2
  111. package/examples/snes/templates/puzzle-data.asm +22 -0
  112. package/examples/snes/templates/puzzle.c +21 -2
  113. package/examples/snes/templates/racing-data.asm +22 -0
  114. package/examples/snes/templates/racing.c +17 -1
  115. package/examples/snes/templates/shmup-data.asm +22 -0
  116. package/examples/snes/templates/shmup.c +20 -1
  117. package/examples/snes/templates/sports-data.asm +22 -0
  118. package/examples/snes/templates/sports.c +16 -1
  119. package/package.json +1 -1
  120. package/src/cheats/gamegenie.js +0 -1
  121. package/src/cli/smoke.js +1 -3
  122. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  123. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  124. package/src/host/LibretroHost.js +191 -16
  125. package/src/host/callbacks.js +9 -1
  126. package/src/host/chafa-render.js +2 -0
  127. package/src/host/dsp-state.js +2 -2
  128. package/src/host/gpgx-state.js +4 -0
  129. package/src/host/types.js +15 -8
  130. package/src/http/routes.js +1 -1
  131. package/src/http/tool-registry.js +26 -1
  132. package/src/mcp/server.js +1 -1
  133. package/src/mcp/state.js +36 -0
  134. package/src/mcp/tools/address-to-symbol.js +0 -1
  135. package/src/mcp/tools/art-loaders.js +1 -1
  136. package/src/mcp/tools/cart-parts.js +75 -4
  137. package/src/mcp/tools/classify-region.js +1 -1
  138. package/src/mcp/tools/diff-roms.js +1 -1
  139. package/src/mcp/tools/disasm-rebuild.js +507 -0
  140. package/src/mcp/tools/disasm.js +97 -9
  141. package/src/mcp/tools/find-references.js +1 -2
  142. package/src/mcp/tools/font-map.js +1 -1
  143. package/src/mcp/tools/frame.js +168 -3
  144. package/src/mcp/tools/index.js +0 -49
  145. package/src/mcp/tools/input-layout.js +0 -1
  146. package/src/mcp/tools/input.js +33 -3
  147. package/src/mcp/tools/lifecycle.js +18 -4
  148. package/src/mcp/tools/lospec.js +0 -19
  149. package/src/mcp/tools/platform-docs.js +1 -1
  150. package/src/mcp/tools/platform-tools.js +4 -4
  151. package/src/mcp/tools/project.js +54 -11
  152. package/src/mcp/tools/reinject.js +0 -1
  153. package/src/mcp/tools/rom-id.js +2 -2
  154. package/src/mcp/tools/snippets.js +2 -2
  155. package/src/mcp/tools/sprite-pipeline.js +1 -2
  156. package/src/mcp/tools/state.js +201 -14
  157. package/src/mcp/tools/tile-inspect.js +1 -1
  158. package/src/mcp/tools/toolchain.js +105 -12
  159. package/src/mcp/tools/watch-memory.js +137 -16
  160. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
  161. package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
  162. package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
  163. package/src/platforms/c64/MENTAL_MODEL.md +45 -1
  164. package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
  165. package/src/platforms/c64/d64.js +280 -0
  166. package/src/platforms/c64/sid.js +0 -2
  167. package/src/platforms/common/metasprite-adapters.js +1 -1
  168. package/src/platforms/common/metasprite-codegen.js +3 -3
  169. package/src/platforms/common/registers.js +5 -3
  170. package/src/platforms/gb/MENTAL_MODEL.md +10 -0
  171. package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
  172. package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
  173. package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
  174. package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
  175. package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
  176. package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
  177. package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
  178. package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
  179. package/src/platforms/msx/MENTAL_MODEL.md +10 -6
  180. package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +63 -2
  182. package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
  183. package/src/platforms/nes/image-to-tilemap.js +3 -0
  184. package/src/platforms/nes/lib/asm/famitone2.s +5 -1
  185. package/src/platforms/pce/MENTAL_MODEL.md +9 -4
  186. package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
  187. package/src/platforms/pce/lib/c/pce_video.c +1 -1
  188. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  189. package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
  190. package/src/platforms/snes/brr.js +0 -2
  191. package/src/playtest/playtest.js +0 -7
  192. package/src/rom-id/identifier.js +15 -0
  193. package/src/toolchains/asar/asar.js +0 -9
  194. package/src/toolchains/assemble-snippet.js +30 -12
  195. package/src/toolchains/cc65/ines.js +145 -0
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
  197. package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
  198. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
  199. package/src/toolchains/common/reassemble.js +10 -3
  200. package/src/toolchains/common/sdk-cache.js +1 -1
  201. package/src/toolchains/genesis-c/genesis-c.js +5 -3
  202. package/src/toolchains/index.js +27 -3
  203. package/src/toolchains/parse-errors.js +78 -1
  204. package/src/toolchains/sdcc/preflight-lint.js +5 -1
  205. package/src/toolchains/sdcc/sdcc.js +1 -1
  206. package/src/toolchains/sjasm/sjasm.js +1 -1
  207. package/src/toolchains/snes-c/snes-c.js +2 -2
  208. package/src/toolchains/vasm68k/vasm68k.js +2 -4
  209. package/src/toolchains/wladx/wladx.js +1 -1
@@ -0,0 +1,283 @@
1
+ /*
2
+ * PC Engine "platformer" — a side-scrolling platformer scaffold.
3
+ *
4
+ * Run and jump across a world wider than one screen. The d-pad moves left/right,
5
+ * button I jumps; gravity pulls you down and you land on top of solid platforms.
6
+ * The camera follows the player and the background scrolls smoothly via the VDC
7
+ * background X-scroll register (BXR/R7).
8
+ *
9
+ * The PCE BAT (background map) is a 32x32 virtual screen (256px) that WRAPS, so
10
+ * a world wider than 256px needs COLUMN STREAMING: each time the camera crosses
11
+ * an 8px boundary we rewrite the BAT column that is about to scroll into view
12
+ * with the next world column's tiles. This mirrors the SMS platformer scaffold,
13
+ * using BXR instead of SMS R8.
14
+ *
15
+ * PCE notes (see pce_hw.h / MENTAL_MODEL.md):
16
+ * - disp_enable() turns on BG + sprites + the VBlank IRQ (waitvsync needs it).
17
+ * - .bss must be non-empty (pce_video.c's _pce_keep[] covers it).
18
+ * - we set BXR every frame via vdc_set_reg(VDC_BXR, camX) for smooth scroll.
19
+ *
20
+ * cc65 is C89 — declare locals at the top of a block.
21
+ */
22
+ #include <pce.h>
23
+ #include <stdint.h> /* int8_t/int16_t/int32_t for sub-pixel physics + camera */
24
+ #include "pce_hw.h"
25
+
26
+ /* ---- VRAM layout (word addresses) --------------------------------------- */
27
+ #define BAT_VRAM 0x0000 /* 32x32 background map */
28
+ #define SKY_VRAM 0x1000 /* BG tile: sky (solid colour 1) */
29
+ #define WALL_VRAM 0x1010 /* BG tile: platform block (colour 2) */
30
+ #define WALLTOP_VRAM 0x1020 /* BG tile: platform top edge (colour 3 strip) */
31
+ #define PLAYER_VRAM 0x1800 /* 16x16 player */
32
+
33
+ #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
34
+
35
+ /* ---- world -------------------------------------------------------------- */
36
+ #define WORLD_COLS 96 /* 96 cells = 768 px world */
37
+ #define WORLD_W (WORLD_COLS * 8)
38
+ #define SCREEN_W 256
39
+ #define VIS_ROWS 28 /* 224-line display = 28 rows */
40
+
41
+ typedef struct { int16_t x, y, w, h; } Rect;
42
+
43
+ /* Platforms in WORLD pixel coords, spread across the 768px world. */
44
+ static const Rect platforms[] = {
45
+ { 0, 200, 768, 24 }, /* floor spans the world */
46
+ { 48, 168, 56, 8 },
47
+ { 140, 152, 64, 8 },
48
+ { 232, 128, 56, 8 },
49
+ { 96, 112, 40, 8 },
50
+ { 320, 160, 72, 8 },
51
+ { 416, 128, 64, 8 },
52
+ { 360, 88, 48, 8 },
53
+ { 512, 152, 80, 8 },
54
+ { 600, 120, 56, 8 },
55
+ { 672, 168, 72, 8 },
56
+ { 560, 80, 48, 8 }
57
+ };
58
+ #define N_PLATFORMS (sizeof(platforms) / sizeof(platforms[0]))
59
+
60
+ /* ---- state -------------------------------------------------------------- */
61
+ static int16_t px, py; /* player position in 1/16-px units */
62
+ static int16_t vx, vy;
63
+ static int16_t camX, lastCamCol;
64
+ static u8 pad, prev_pad;
65
+ static u16 spr_buf[64];
66
+ static u16 tile_buf[16];
67
+
68
+ static void make_solid_tile(u16 *t, u8 ci) {
69
+ u8 r;
70
+ u8 p0 = (ci & 1) ? 0xFF : 0x00;
71
+ u8 p1 = (ci & 2) ? 0xFF : 0x00;
72
+ u8 p2 = (ci & 4) ? 0xFF : 0x00;
73
+ u8 p3 = (ci & 8) ? 0xFF : 0x00;
74
+ for (r = 0; r < 8; ++r) {
75
+ t[r] = (u16)(p0 | (p1 << 8));
76
+ t[r + 8] = (u16)(p2 | (p3 << 8));
77
+ }
78
+ }
79
+
80
+ /* platform-top tile: colour 2 body with a colour-3 highlight on the top 2 rows */
81
+ static void make_walltop_tile(u16 *t) {
82
+ make_solid_tile(t, 2);
83
+ /* rows 0,1: set plane0 too so those pixels read colour 3 (planes0+1) */
84
+ t[0] = (u16)(0x00FF | (t[0] & 0xFF00));
85
+ t[1] = (u16)(0x00FF | (t[1] & 0xFF00));
86
+ }
87
+
88
+ static void make_player_sprite(void) {
89
+ static const u16 body[16] = {
90
+ 0x07E0, 0x0FF0, 0x1FF8, 0x1818, 0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE,
91
+ 0x7FFE, 0x7FFE, 0x3FFC, 0x1FF8, 0x0E70, 0x0C30, 0x0C30, 0x1818
92
+ };
93
+ static const u16 eyes[16] = {
94
+ 0x0000, 0x0000, 0x0000, 0x0000, 0x0990, 0x0990, 0x0000, 0x0000,
95
+ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
96
+ };
97
+ u8 r;
98
+ for (r = 0; r < 64; ++r) spr_buf[r] = 0;
99
+ for (r = 0; r < 16; ++r) {
100
+ spr_buf[r] = (u16)(body[r] & ~eyes[r]); /* plane0 -> colour 1 */
101
+ spr_buf[r + 16] = eyes[r]; /* plane1 -> colour 2 */
102
+ }
103
+ load_tiles(PLAYER_VRAM, spr_buf, 64);
104
+ }
105
+
106
+ /* Is world cell (col,row) inside any platform? */
107
+ static u8 cell_is_wall(int16_t col, u8 row) {
108
+ int16_t cx = (int16_t)(col << 3);
109
+ int16_t cy = (int16_t)((int16_t)row << 3);
110
+ u8 i;
111
+ const Rect *p;
112
+ for (i = 0; i < N_PLATFORMS; ++i) {
113
+ p = &platforms[i];
114
+ if (cx + 8 > p->x && cx < p->x + p->w &&
115
+ cy + 8 > p->y && cy < p->y + p->h) return 1;
116
+ }
117
+ return 0;
118
+ }
119
+
120
+ /* Is world cell the TOP row of a platform (for the highlighted edge tile)? */
121
+ static u8 cell_is_top(int16_t col, u8 row) {
122
+ int16_t cy = (int16_t)((int16_t)row << 3);
123
+ int16_t cx = (int16_t)(col << 3);
124
+ u8 i;
125
+ const Rect *p;
126
+ for (i = 0; i < N_PLATFORMS; ++i) {
127
+ p = &platforms[i];
128
+ if (cx + 8 > p->x && cx < p->x + p->w && cy >= p->y && cy < p->y + 8) return 1;
129
+ }
130
+ return 0;
131
+ }
132
+
133
+ /* Write one world column into its wrapped BAT column. */
134
+ static void paint_column(int16_t worldCol) {
135
+ u8 ntCol, row;
136
+ u16 e;
137
+ if (worldCol < 0 || worldCol >= WORLD_COLS) return;
138
+ ntCol = (u8)(worldCol & 31);
139
+ for (row = 0; row < 32; ++row) {
140
+ if (row < VIS_ROWS && cell_is_wall(worldCol, row)) {
141
+ e = cell_is_top(worldCol, row)
142
+ ? BAT_ENTRY(0, WALLTOP_VRAM)
143
+ : BAT_ENTRY(0, WALL_VRAM);
144
+ } else {
145
+ e = BAT_ENTRY(0, SKY_VRAM);
146
+ }
147
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + ntCol));
148
+ VDC_DATA_LO = (u8)(e & 0xFF);
149
+ VDC_DATA_HI = (u8)(e >> 8);
150
+ }
151
+ }
152
+
153
+ static void paint_initial(void) {
154
+ int16_t c;
155
+ for (c = 0; c < 32; ++c) paint_column(c);
156
+ }
157
+
158
+ static u8 on_platform(int16_t ipx, int16_t ipy) {
159
+ u8 i;
160
+ const Rect *p;
161
+ for (i = 0; i < N_PLATFORMS; ++i) {
162
+ p = &platforms[i];
163
+ if (ipy + 16 == p->y && ipx + 12 > p->x && ipx + 4 < p->x + p->w) return 1;
164
+ }
165
+ return 0;
166
+ }
167
+
168
+ void main(void) {
169
+ const int16_t GRAVITY = 10;
170
+ const int16_t MOVE = 22;
171
+ const int16_t JUMP = -200;
172
+ const int16_t MAXFALL = 300;
173
+
174
+ _pce_keep[0] = 0;
175
+
176
+ /* palette */
177
+ vce_set_color(0, PCE_RGB(1, 2, 5)); /* backdrop sky blue */
178
+ vce_set_color(1, PCE_RGB(2, 4, 7)); /* BG c1: sky */
179
+ vce_set_color(2, PCE_RGB(3, 2, 1)); /* BG c2: brown platform */
180
+ vce_set_color(3, PCE_RGB(1, 6, 1)); /* BG c3: green grassy top */
181
+ vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr transparent */
182
+ vce_set_color(257, PCE_RGB(7, 1, 1)); /* spr c1: red body */
183
+ vce_set_color(258, PCE_RGB(7, 7, 7)); /* spr c2: white eyes */
184
+
185
+ make_solid_tile(tile_buf, 1); load_tiles(SKY_VRAM, tile_buf, 16);
186
+ make_solid_tile(tile_buf, 2); load_tiles(WALL_VRAM, tile_buf, 16);
187
+ make_walltop_tile(tile_buf); load_tiles(WALLTOP_VRAM, tile_buf, 16);
188
+ make_player_sprite();
189
+
190
+ paint_initial();
191
+
192
+ px = (int16_t)(24 << 4);
193
+ py = (int16_t)(160 << 4);
194
+ vx = 0; vy = 0;
195
+ camX = 0; lastCamCol = 0;
196
+ prev_pad = 0;
197
+
198
+ set_sprite(0, (u16)(px >> 4), (u16)(py >> 4), PLAYER_VRAM >> 6, 0);
199
+ satb_dma();
200
+
201
+ pce_joy_init();
202
+ disp_enable();
203
+
204
+ for (;;) {
205
+ int16_t ipx, ipy, npy, sx;
206
+ int16_t camCol;
207
+ int32_t np;
208
+ u8 grounded;
209
+ u8 i;
210
+ const Rect *p;
211
+
212
+ waitvsync();
213
+ pad = pce_joy_read();
214
+
215
+ ipx = px >> 4;
216
+ ipy = py >> 4;
217
+
218
+ /* camera follows player, clamped to world */
219
+ camX = (int16_t)(ipx - (SCREEN_W / 2 - 8));
220
+ if (camX < 0) camX = 0;
221
+ if (camX > WORLD_W - SCREEN_W) camX = (int16_t)(WORLD_W - SCREEN_W);
222
+
223
+ /* stream columns entering from the edges */
224
+ camCol = camX >> 3;
225
+ while (camCol > lastCamCol) { lastCamCol++; paint_column((int16_t)(lastCamCol + 31)); }
226
+ while (camCol < lastCamCol) { lastCamCol--; paint_column(lastCamCol); }
227
+
228
+ /* smooth pixel scroll via BG X register */
229
+ vdc_set_reg(VDC_BXR, (u16)camX);
230
+
231
+ /* horizontal move */
232
+ vx = 0;
233
+ if (pad & PCE_JOY_LEFT) vx = (int16_t)(-MOVE);
234
+ if (pad & PCE_JOY_RIGHT) vx = MOVE;
235
+
236
+ grounded = on_platform(ipx, ipy);
237
+ if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I) && grounded) {
238
+ vy = JUMP;
239
+ psg_tone(0, 0x200, 24);
240
+ }
241
+ prev_pad = pad;
242
+
243
+ vy = (int16_t)(vy + GRAVITY);
244
+ if (vy > MAXFALL) vy = MAXFALL;
245
+ if (grounded && vy > 0) vy = 0;
246
+
247
+ /* horizontal integrate + clamp */
248
+ px = (int16_t)(px + vx);
249
+ if (px < 0) px = 0;
250
+ if (px > ((WORLD_W - 16) << 4)) px = (int16_t)((WORLD_W - 16) << 4);
251
+
252
+ /* vertical integrate with land-on-top */
253
+ np = (int32_t)py + (int32_t)vy;
254
+ npy = (int16_t)(np >> 4);
255
+ if (vy > 0) {
256
+ u8 landed = 0;
257
+ for (i = 0; i < N_PLATFORMS; ++i) {
258
+ p = &platforms[i];
259
+ if (ipy + 16 <= p->y && npy + 16 >= p->y &&
260
+ ipx + 12 > p->x && ipx + 4 < p->x + p->w) {
261
+ py = (int16_t)((p->y - 16) << 4);
262
+ vy = 0;
263
+ landed = 1;
264
+ break;
265
+ }
266
+ }
267
+ if (!landed) py = (int16_t)np;
268
+ } else {
269
+ py = (int16_t)np;
270
+ }
271
+ if (py > (224 << 4)) { px = (int16_t)(24 << 4); py = (int16_t)(160 << 4); vy = 0; }
272
+
273
+ /* free the jump SFX channel after it rings */
274
+ if (vy == 0) psg_off(0);
275
+
276
+ /* draw player in screen space */
277
+ sx = (int16_t)((px >> 4) - camX);
278
+ if (sx < 0) sx = 0;
279
+ if (sx > 240) sx = 240;
280
+ set_sprite(0, (u16)sx, (u16)(py >> 4), PLAYER_VRAM >> 6, 0);
281
+ satb_dma();
282
+ }
283
+ }
@@ -0,0 +1,304 @@
1
+ /*
2
+ * PC Engine "puzzle" — a match-3 falling-block scaffold.
3
+ *
4
+ * A 1x3 column of coloured blocks falls into a 6-wide x 12-tall well drawn with
5
+ * background tiles. LEFT/RIGHT slide the piece, button I rotates the three
6
+ * colours, DOWN soft-drops, button II hard-drops. When a piece locks, any
7
+ * horizontal run of three same-colour cells clears and scores. Mirrors the
8
+ * NES/Genesis/SNES/GB/SMS puzzle scaffolds, translated to the PCE helper API.
9
+ *
10
+ * The whole field is drawn from BG tiles (no sprites needed) — a grey wall
11
+ * frame around a dim field interior, with R/G/B block tiles for the cells, so
12
+ * the screen is clearly a populated playfield (clears the verify gate).
13
+ *
14
+ * PCE notes (see pce_hw.h / MENTAL_MODEL.md):
15
+ * - bg_enable() turns on the BG plane + the VBlank IRQ (waitvsync needs it).
16
+ * - .bss must be non-empty (pce_video.c's _pce_keep[] covers it).
17
+ *
18
+ * cc65 is C89 — declare locals at the top of a block.
19
+ */
20
+ #include <pce.h>
21
+ #include <stdint.h> /* int8_t for signed grid coordinates */
22
+ #include "pce_hw.h"
23
+
24
+ /* ---- VRAM layout (word addresses) --------------------------------------- */
25
+ #define BAT_VRAM 0x0000
26
+ #define BG_VRAM 0x1000 /* cabinet background (dotted, colour 6/7) */
27
+ #define RED_VRAM 0x1010 /* block colour 1 */
28
+ #define GRN_VRAM 0x1020 /* block colour 2 */
29
+ #define BLU_VRAM 0x1030 /* block colour 3 */
30
+ #define WALL_VRAM 0x1040 /* well border (colour 4) */
31
+ #define FIELD_VRAM 0x1050 /* dim empty field (colour 5) */
32
+
33
+ #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
34
+
35
+ #define COLS 6
36
+ #define ROWS 12
37
+ #define GRID_COL0 13 /* well's left BAT column (centres the 6-wide field) */
38
+ #define GRID_ROW0 4 /* well's top BAT row */
39
+
40
+ /* ---- state -------------------------------------------------------------- */
41
+ static u8 grid[ROWS][COLS]; /* 0 = empty, 1..3 = colour */
42
+ static u8 piece[3]; /* three stacked colours */
43
+ static int8_t piece_x; /* column 0..COLS-1 */
44
+ static int8_t piece_y; /* row of top cell (can be negative) */
45
+ static u8 fall_timer;
46
+ static u16 score;
47
+ static u16 rng;
48
+ static u8 pad, prev_pad;
49
+ static u16 tile_buf[16];
50
+
51
+ static void make_solid_tile(u16 *t, u8 ci) {
52
+ u8 r;
53
+ u8 p0 = (ci & 1) ? 0xFF : 0x00;
54
+ u8 p1 = (ci & 2) ? 0xFF : 0x00;
55
+ u8 p2 = (ci & 4) ? 0xFF : 0x00;
56
+ u8 p3 = (ci & 8) ? 0xFF : 0x00;
57
+ for (r = 0; r < 8; ++r) {
58
+ t[r] = (u16)(p0 | (p1 << 8));
59
+ t[r + 8] = (u16)(p2 | (p3 << 8));
60
+ }
61
+ }
62
+
63
+ /* A block tile: a solid `ci`-colour body with a 1px `frame`-colour border on
64
+ * all four edges, so adjacent same-colour blocks still read as distinct cells.
65
+ * For each of the 8 rows we pick a per-plane mask: border rows (0,7) are all
66
+ * `frame`; interior rows are `ci` body with the left/right edge pixels framed. */
67
+ static void make_block_tile(u16 *t, u8 ci, u8 frame) {
68
+ u8 r;
69
+ for (r = 0; r < 8; ++r) {
70
+ u8 edge_row = (r == 0 || r == 7);
71
+ /* body colour planes (fill the whole row) */
72
+ u8 b0 = (ci & 1) ? 0xFF : 0x00, b1 = (ci & 2) ? 0xFF : 0x00;
73
+ u8 b2 = (ci & 4) ? 0xFF : 0x00, b3 = (ci & 8) ? 0xFF : 0x00;
74
+ /* frame colour planes */
75
+ u8 f0 = (frame & 1) ? 0xFF : 0x00, f1 = (frame & 2) ? 0xFF : 0x00;
76
+ u8 f2 = (frame & 4) ? 0xFF : 0x00, f3 = (frame & 8) ? 0xFF : 0x00;
77
+ u8 p0, p1, p2, p3;
78
+ if (edge_row) {
79
+ p0 = f0; p1 = f1; p2 = f2; p3 = f3; /* whole row framed */
80
+ } else {
81
+ /* body fill, but pixels 0 and 7 (mask 0x81) use the frame colour */
82
+ p0 = (u8)((b0 & 0x7E) | (f0 & 0x81));
83
+ p1 = (u8)((b1 & 0x7E) | (f1 & 0x81));
84
+ p2 = (u8)((b2 & 0x7E) | (f2 & 0x81));
85
+ p3 = (u8)((b3 & 0x7E) | (f3 & 0x81));
86
+ }
87
+ t[r] = (u16)(p0 | (p1 << 8));
88
+ t[r + 8] = (u16)(p2 | (p3 << 8));
89
+ }
90
+ }
91
+
92
+ /* Cabinet background tile: every pixel is colour 6, with a colour-7 dot on a
93
+ * sparse lattice, so the whole screen reads as an intentional textured backdrop
94
+ * rather than the flat hardware backdrop. Colour 6 = planes 1+2; colour 7 adds
95
+ * plane 0 (so dots = planes 0+1+2). Build per-plane row bytes:
96
+ * plane0 (low byte words 0..7) = dot mask (only dot pixels)
97
+ * plane1 (high byte words 0..7) = 0xFF (colour 6 base, all pixels)
98
+ * plane2 (low byte words 8..15) = 0xFF (colour 6 base, all pixels)
99
+ * plane3 (high byte words 8..15)= 0 */
100
+ static void make_dots_tile(u16 *t) {
101
+ u8 r;
102
+ for (r = 0; r < 8; ++r) {
103
+ u8 dot = ((r & 3) == 0) ? 0x22 : 0x00; /* dot columns every 4 px */
104
+ t[r] = (u16)(dot | 0xFF00u); /* plane0=dots, plane1=base */
105
+ t[r + 8] = (u16)0x00FFu; /* plane2=base, plane3=0 */
106
+ }
107
+ }
108
+
109
+ static void upload_art(void) {
110
+ make_dots_tile(tile_buf); load_tiles(BG_VRAM, tile_buf, 16);
111
+ make_block_tile(tile_buf, 1, 6); load_tiles(RED_VRAM, tile_buf, 16);
112
+ make_block_tile(tile_buf, 2, 6); load_tiles(GRN_VRAM, tile_buf, 16);
113
+ make_block_tile(tile_buf, 3, 6); load_tiles(BLU_VRAM, tile_buf, 16);
114
+ make_solid_tile(tile_buf, 4); load_tiles(WALL_VRAM, tile_buf, 16);
115
+ make_solid_tile(tile_buf, 5); load_tiles(FIELD_VRAM, tile_buf, 16);
116
+ }
117
+
118
+ static u16 vram_for(u8 cell) {
119
+ if (cell == 1) return RED_VRAM;
120
+ if (cell == 2) return GRN_VRAM;
121
+ if (cell == 3) return BLU_VRAM;
122
+ return FIELD_VRAM; /* empty -> dim field interior */
123
+ }
124
+
125
+ static void put_cell(u8 batCol, u8 batRow, u16 vram) {
126
+ u16 e = BAT_ENTRY(0, vram);
127
+ vram_set_write_addr((u16)(BAT_VRAM + batRow * 32 + batCol));
128
+ VDC_DATA_LO = (u8)(e & 0xFF);
129
+ VDC_DATA_HI = (u8)(e >> 8);
130
+ }
131
+
132
+ /* fill the whole BAT with the cabinet background tile */
133
+ static void clear_bat(void) {
134
+ u8 r, c;
135
+ u16 e = BAT_ENTRY(0, BG_VRAM);
136
+ for (r = 0; r < 32; ++r) {
137
+ vram_set_write_addr((u16)(BAT_VRAM + r * 32));
138
+ for (c = 0; c < 32; ++c) {
139
+ VDC_DATA_LO = (u8)(e & 0xFF);
140
+ VDC_DATA_HI = (u8)(e >> 8);
141
+ }
142
+ }
143
+ }
144
+
145
+ /* draw the well frame + dim interior */
146
+ static void draw_well(void) {
147
+ int8_t r, c;
148
+ for (r = -1; r <= ROWS; ++r) {
149
+ for (c = -1; c <= COLS; ++c) {
150
+ u16 vram = (r == -1 || r == ROWS || c == -1 || c == COLS)
151
+ ? WALL_VRAM : FIELD_VRAM;
152
+ put_cell((u8)(GRID_COL0 + c), (u8)(GRID_ROW0 + r), vram);
153
+ }
154
+ }
155
+ }
156
+
157
+ static void draw_grid(void) {
158
+ u8 r, c;
159
+ for (r = 0; r < ROWS; ++r)
160
+ for (c = 0; c < COLS; ++c)
161
+ put_cell((u8)(GRID_COL0 + c), (u8)(GRID_ROW0 + r), vram_for(grid[r][c]));
162
+ }
163
+
164
+ static u16 next_rand(void) {
165
+ rng = (u16)(rng * 25173u + 13849u);
166
+ return rng;
167
+ }
168
+ static u8 rand_color(void) { return (u8)(1 + (next_rand() >> 8) % 3); }
169
+
170
+ static void new_piece(void) {
171
+ piece[0] = rand_color();
172
+ piece[1] = rand_color();
173
+ piece[2] = rand_color();
174
+ piece_x = COLS / 2 - 1;
175
+ piece_y = -3;
176
+ }
177
+
178
+ static u8 collides(int8_t col, int8_t row) {
179
+ u8 i;
180
+ int8_t r;
181
+ if (col < 0 || col >= COLS) return 1;
182
+ for (i = 0; i < 3; ++i) {
183
+ r = (int8_t)(row + i);
184
+ if (r >= ROWS) return 1;
185
+ if (r >= 0 && grid[r][col] != 0) return 1;
186
+ }
187
+ return 0;
188
+ }
189
+
190
+ static void draw_piece(u8 clear) {
191
+ u8 i;
192
+ for (i = 0; i < 3; ++i) {
193
+ int8_t r = (int8_t)(piece_y + i);
194
+ u8 v;
195
+ if (r < 0 || r >= ROWS) continue;
196
+ v = clear ? grid[r][piece_x] : piece[i];
197
+ put_cell((u8)(GRID_COL0 + piece_x), (u8)(GRID_ROW0 + r), vram_for(v));
198
+ }
199
+ }
200
+
201
+ static void clear_triples(void) {
202
+ u8 r;
203
+ int8_t c;
204
+ u8 a, b, d;
205
+ for (r = 0; r < ROWS; ++r) {
206
+ for (c = 0; c <= COLS - 3; ++c) {
207
+ a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
208
+ if (a != 0 && a == b && b == d) {
209
+ grid[r][c] = 0; grid[r][c + 1] = 0; grid[r][c + 2] = 0;
210
+ if (score < 9999) score += 30;
211
+ psg_tone(0, 0x180, 24);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ static void lock_piece(void) {
218
+ u8 i;
219
+ int8_t r;
220
+ for (i = 0; i < 3; ++i) {
221
+ r = (int8_t)(piece_y + i);
222
+ if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
223
+ }
224
+ clear_triples();
225
+ draw_grid();
226
+ }
227
+
228
+ void main(void) {
229
+ u8 r, c;
230
+ u8 sfx_timer;
231
+
232
+ _pce_keep[0] = 0;
233
+
234
+ /* palette: BG sub-pal 0 holds field/wall + R/G/B blocks + frame */
235
+ vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop navy */
236
+ vce_set_color(1, PCE_RGB(7, 1, 1)); /* c1 red block */
237
+ vce_set_color(2, PCE_RGB(1, 6, 1)); /* c2 green block */
238
+ vce_set_color(3, PCE_RGB(2, 3, 7)); /* c3 blue block */
239
+ vce_set_color(4, PCE_RGB(5, 5, 5)); /* c4 wall grey */
240
+ vce_set_color(5, PCE_RGB(1, 2, 4)); /* c5 field blue (clearly *
241
+ * distinct from backdrop) */
242
+ vce_set_color(6, PCE_RGB(1, 0, 2)); /* c6 cabinet purple base + *
243
+ * block frame */
244
+ vce_set_color(7, PCE_RGB(2, 1, 4)); /* c7 cabinet dot */
245
+
246
+ upload_art();
247
+ clear_bat();
248
+ draw_well();
249
+
250
+ for (r = 0; r < ROWS; ++r) for (c = 0; c < COLS; ++c) grid[r][c] = 0;
251
+ score = 0;
252
+ fall_timer = 0;
253
+ rng = 0x1357;
254
+ prev_pad = 0;
255
+ sfx_timer = 0;
256
+ new_piece();
257
+ draw_grid();
258
+
259
+ pce_joy_init();
260
+ bg_enable();
261
+
262
+ for (;;) {
263
+ u8 fall_rate;
264
+ waitvsync();
265
+
266
+ draw_piece(1); /* erase old piece footprint */
267
+
268
+ pad = pce_joy_read();
269
+ if ((pad & PCE_JOY_LEFT) && !(prev_pad & PCE_JOY_LEFT)
270
+ && !collides((int8_t)(piece_x - 1), piece_y)) piece_x--;
271
+ if ((pad & PCE_JOY_RIGHT) && !(prev_pad & PCE_JOY_RIGHT)
272
+ && !collides((int8_t)(piece_x + 1), piece_y)) piece_x++;
273
+ if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I)) {
274
+ u8 t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
275
+ psg_tone(1, 0x300, 18); sfx_timer = 3;
276
+ }
277
+ if ((pad & PCE_JOY_II) && !(prev_pad & PCE_JOY_II)) {
278
+ while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
279
+ lock_piece();
280
+ new_piece();
281
+ psg_tone(1, 0x140, 22); sfx_timer = 4;
282
+ prev_pad = pad;
283
+ if (sfx_timer) { --sfx_timer; }
284
+ continue;
285
+ }
286
+ prev_pad = pad;
287
+
288
+ fall_rate = (pad & PCE_JOY_DOWN) ? 4 : 30;
289
+ fall_timer++;
290
+ if (fall_timer >= fall_rate) {
291
+ fall_timer = 0;
292
+ if (collides(piece_x, (int8_t)(piece_y + 1))) {
293
+ lock_piece();
294
+ new_piece();
295
+ } else {
296
+ piece_y++;
297
+ }
298
+ }
299
+
300
+ draw_piece(0); /* draw piece at new position */
301
+
302
+ if (sfx_timer) { --sfx_timer; if (sfx_timer == 0) { psg_off(0); psg_off(1); } }
303
+ }
304
+ }