romdevtools 0.16.0 → 0.21.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 (110) hide show
  1. package/AGENTS.md +60 -12
  2. package/CHANGELOG.md +258 -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/platformer.c +43 -4
  10. package/examples/atari7800/templates/puzzle.c +39 -4
  11. package/examples/atari7800/templates/racing.c +39 -4
  12. package/examples/atari7800/templates/shmup.c +40 -2
  13. package/examples/atari7800/templates/sports.c +36 -5
  14. package/examples/c64/templates/platformer.c +19 -5
  15. package/examples/c64/templates/puzzle.c +32 -2
  16. package/examples/c64/templates/shmup.c +28 -2
  17. package/examples/c64/templates/sports.c +30 -2
  18. package/examples/gb/templates/default.c +110 -16
  19. package/examples/gb/templates/platformer.c +25 -4
  20. package/examples/gb/templates/puzzle.c +32 -2
  21. package/examples/gb/templates/racing.c +72 -8
  22. package/examples/gb/templates/shmup.c +38 -1
  23. package/examples/gb/templates/sports.c +48 -1
  24. package/examples/gba/templates/gba_hello.c +29 -11
  25. package/examples/gba/templates/puzzle.c +15 -3
  26. package/examples/gba/templates/racing.c +65 -3
  27. package/examples/gba/templates/shmup.c +41 -4
  28. package/examples/gba/templates/sports.c +36 -2
  29. package/examples/gba/templates/tonc_hello.c +41 -5
  30. package/examples/gbc/templates/default.c +103 -26
  31. package/examples/gbc/templates/platformer.c +25 -4
  32. package/examples/gbc/templates/puzzle.c +32 -2
  33. package/examples/gbc/templates/racing.c +85 -19
  34. package/examples/gbc/templates/shmup.c +34 -1
  35. package/examples/gbc/templates/sports.c +45 -1
  36. package/examples/genesis/templates/puzzle.c +37 -3
  37. package/examples/genesis/templates/racing.c +44 -11
  38. package/examples/genesis/templates/sgdk_hello.c +34 -1
  39. package/examples/genesis/templates/shmup.c +31 -1
  40. package/examples/gg/templates/default.c +56 -18
  41. package/examples/gg/templates/platformer.c +18 -12
  42. package/examples/gg/templates/puzzle.c +38 -7
  43. package/examples/gg/templates/racing.c +51 -5
  44. package/examples/gg/templates/shmup.c +47 -3
  45. package/examples/gg/templates/sports.c +46 -3
  46. package/examples/lynx/templates/default.c +39 -8
  47. package/examples/lynx/templates/puzzle.c +28 -1
  48. package/examples/lynx/templates/racing.c +34 -7
  49. package/examples/lynx/templates/shmup.c +42 -3
  50. package/examples/lynx/templates/sports.c +29 -2
  51. package/examples/msx/platformer/main.c +213 -0
  52. package/examples/msx/puzzle/main.c +250 -0
  53. package/examples/msx/racing/main.c +249 -0
  54. package/examples/msx/shmup/main.c +288 -0
  55. package/examples/msx/sports/main.c +182 -0
  56. package/examples/nes/templates/default.c +67 -19
  57. package/examples/nes/templates/platformer.c +65 -6
  58. package/examples/nes/templates/puzzle.c +67 -6
  59. package/examples/nes/templates/racing.c +45 -13
  60. package/examples/nes/templates/shmup.c +51 -2
  61. package/examples/nes/templates/sports.c +51 -6
  62. package/examples/pce/platformer/main.c +283 -0
  63. package/examples/pce/puzzle/main.c +304 -0
  64. package/examples/pce/racing/main.c +304 -0
  65. package/examples/pce/shmup/main.c +346 -0
  66. package/examples/pce/sports/main.c +254 -0
  67. package/examples/sms/main.c +35 -6
  68. package/examples/sms/templates/puzzle.c +34 -5
  69. package/examples/sms/templates/racing.c +39 -2
  70. package/examples/sms/templates/shmup.c +41 -2
  71. package/examples/sms/templates/sports.c +43 -2
  72. package/examples/snes/templates/default.c +50 -28
  73. package/examples/snes/templates/platformer-data.asm +22 -0
  74. package/examples/snes/templates/platformer.c +16 -1
  75. package/examples/snes/templates/puzzle-data.asm +22 -0
  76. package/examples/snes/templates/puzzle.c +17 -1
  77. package/examples/snes/templates/racing-data.asm +22 -0
  78. package/examples/snes/templates/racing.c +17 -1
  79. package/examples/snes/templates/shmup-data.asm +22 -0
  80. package/examples/snes/templates/shmup.c +20 -1
  81. package/examples/snes/templates/sports-data.asm +22 -0
  82. package/examples/snes/templates/sports.c +16 -1
  83. package/package.json +1 -1
  84. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  85. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  86. package/src/host/LibretroHost.js +122 -1
  87. package/src/host/callbacks.js +9 -1
  88. package/src/host/types.js +15 -8
  89. package/src/http/tool-registry.js +26 -1
  90. package/src/mcp/tools/cart-parts.js +75 -3
  91. package/src/mcp/tools/disasm-rebuild.js +507 -0
  92. package/src/mcp/tools/disasm.js +95 -6
  93. package/src/mcp/tools/frame.js +168 -3
  94. package/src/mcp/tools/lifecycle.js +4 -2
  95. package/src/mcp/tools/project.js +54 -9
  96. package/src/mcp/tools/state.js +201 -14
  97. package/src/mcp/tools/toolchain.js +76 -3
  98. package/src/mcp/tools/watch-memory.js +125 -14
  99. package/src/platforms/c64/MENTAL_MODEL.md +45 -1
  100. package/src/platforms/c64/d64.js +281 -0
  101. package/src/platforms/gb/MENTAL_MODEL.md +10 -0
  102. package/src/platforms/msx/MENTAL_MODEL.md +10 -6
  103. package/src/platforms/nes/MENTAL_MODEL.md +63 -2
  104. package/src/platforms/pce/MENTAL_MODEL.md +9 -4
  105. package/src/platforms/pce/lib/c/pce_video.c +1 -1
  106. package/src/rom-id/identifier.js +15 -0
  107. package/src/toolchains/cc65/ines.js +145 -0
  108. package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
  109. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
  110. package/src/toolchains/common/reassemble.js +10 -2
@@ -0,0 +1,346 @@
1
+ /*
2
+ * PC Engine "shmup" — a vertical shoot-'em-up scaffold.
3
+ *
4
+ * Fly a ship around the bottom of the screen with the d-pad and fire upward
5
+ * with button I. Enemies spawn at the top in waves and drift down; a bullet
6
+ * that overlaps an enemy destroys it and scores 10. The HUD shows the score
7
+ * with background digit tiles. A scrolling starfield BG keeps the screen full
8
+ * (so it clears the verify gate and the sprites read clearly).
9
+ *
10
+ * Mirrors the NES/Genesis/SNES/GB/SMS shmup scaffolds, translated to the PCE
11
+ * helper API:
12
+ * - object pools (player + bullets + enemies) updated each frame
13
+ * - AABB collision
14
+ * - a wave spawner on a frame counter
15
+ * - 64-sprite shadow SATB + satb_dma()
16
+ *
17
+ * PCE notes (see pce_hw.h / MENTAL_MODEL.md):
18
+ * - disp_enable() turns on BG + sprites AND the VBlank IRQ so waitvsync()
19
+ * actually returns (without the IRQ bit the loop spins forever).
20
+ * - .bss must be non-empty; pce_video.c's _pce_keep[] covers that, and we
21
+ * touch _pce_keep[0] for clarity.
22
+ * - sprites get the SPBG-front bit from set_sprite(), so they draw over the
23
+ * opaque starfield BG.
24
+ *
25
+ * cc65 is C89 — declare locals at the top of a block.
26
+ */
27
+ #include <pce.h>
28
+ #include "pce_hw.h"
29
+
30
+ /* ---- VRAM layout (word addresses) --------------------------------------- */
31
+ #define BAT_VRAM 0x0000 /* 32x32 background map */
32
+ #define FONT_VRAM 0x1000 /* digit/glyph tiles (8x8, 16 words each) */
33
+ #define STAR0_VRAM 0x1400 /* BG tile: empty space (solid colour 1) */
34
+ #define STAR1_VRAM 0x1410 /* BG tile: space band (solid colour 2) */
35
+ #define STAR2_VRAM 0x1420 /* BG tile: space + a star pixel */
36
+ #define SHIP_VRAM 0x1800 /* 16x16 player ship */
37
+ #define BULLET_VRAM 0x1840 /* 16x16 bullet */
38
+ #define ENEMY_VRAM 0x1880 /* 16x16 enemy */
39
+
40
+ #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
41
+
42
+ #define MAX_BULLETS 6
43
+ #define MAX_ENEMIES 6
44
+
45
+ /* ---- 5x7 glyph font (digits + a few letters for the HUD) ----------------- */
46
+ #define G_BLANK 0
47
+ #define G_0 1 /* digits 0..9 -> tiles 1..10 */
48
+ #define G_S 11
49
+ #define G_C 12
50
+ #define G_O 13
51
+ #define G_R 14
52
+ #define G_E 15
53
+ #define NUM_GLYPHS 16
54
+
55
+ static const u8 FONT5x7[NUM_GLYPHS][7] = {
56
+ /* BLANK */ {0,0,0,0,0,0,0},
57
+ /* 0 */ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E},
58
+ /* 1 */ {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
59
+ /* 2 */ {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F},
60
+ /* 3 */ {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
61
+ /* 4 */ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02},
62
+ /* 5 */ {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
63
+ /* 6 */ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E},
64
+ /* 7 */ {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
65
+ /* 8 */ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E},
66
+ /* 9 */ {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
67
+ /* S */ {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E},
68
+ /* C */ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E},
69
+ /* O */ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E},
70
+ /* R */ {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
71
+ /* E */ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F},
72
+ };
73
+
74
+ /* ---- game state --------------------------------------------------------- */
75
+ typedef struct { u16 x, y; u8 alive; } Obj;
76
+
77
+ static Obj player;
78
+ static Obj bullets[MAX_BULLETS];
79
+ static Obj enemies[MAX_ENEMIES];
80
+ static u16 score;
81
+ static u8 spawn_timer;
82
+ static u16 rng;
83
+ static u8 pad, prev_pad;
84
+ static u8 sfx_timer;
85
+
86
+ static u16 tile_buf[16]; /* scratch for one 8x8 tile */
87
+ static u16 spr_buf[64]; /* scratch for one 16x16 sprite */
88
+
89
+ /* ---- tile/sprite builders ----------------------------------------------- */
90
+ static void make_solid_tile(u16 *t, u8 ci) {
91
+ u8 r;
92
+ u8 p0 = (ci & 1) ? 0xFF : 0x00;
93
+ u8 p1 = (ci & 2) ? 0xFF : 0x00;
94
+ u8 p2 = (ci & 4) ? 0xFF : 0x00;
95
+ u8 p3 = (ci & 8) ? 0xFF : 0x00;
96
+ for (r = 0; r < 8; ++r) {
97
+ t[r] = (u16)(p0 | (p1 << 8));
98
+ t[r + 8] = (u16)(p2 | (p3 << 8));
99
+ }
100
+ }
101
+
102
+ /* space tile with one star pixel in colour index 3 at (row 2, col 5) */
103
+ static void make_star_tile(u16 *t) {
104
+ u8 r;
105
+ for (r = 0; r < 8; ++r) { t[r] = 0x00FF; t[r + 8] = 0x0000; } /* base = colour 1 */
106
+ /* star = colour 3 (planes 0+1) at row 2: set plane1 bit too for that row */
107
+ t[2] = (u16)(0x00FF | (0x04 << 8)); /* plane0 row + plane1 single pixel */
108
+ }
109
+
110
+ /* upload one 16x16 sprite from a 16-row body mask in colour `ci` */
111
+ static void make_sprite(u16 vram, const u16 *body, u8 ci) {
112
+ u8 r;
113
+ for (r = 0; r < 64; ++r) spr_buf[r] = 0;
114
+ for (r = 0; r < 16; ++r) {
115
+ if (ci & 1) spr_buf[r] = body[r]; /* plane0 */
116
+ if (ci & 2) spr_buf[r + 16] = body[r]; /* plane1 */
117
+ if (ci & 4) spr_buf[r + 32] = body[r]; /* plane2 */
118
+ if (ci & 8) spr_buf[r + 48] = body[r]; /* plane3 */
119
+ }
120
+ load_tiles(vram, spr_buf, 64);
121
+ }
122
+
123
+ static void upload_font(void) {
124
+ u8 g, row, bits, plane0;
125
+ for (g = 0; g < NUM_GLYPHS; ++g) {
126
+ for (row = 0; row < 16; ++row) tile_buf[row] = 0;
127
+ for (row = 0; row < 7; ++row) {
128
+ bits = FONT5x7[g][row];
129
+ plane0 = 0;
130
+ if (bits & 0x10) plane0 |= 0x40;
131
+ if (bits & 0x08) plane0 |= 0x20;
132
+ if (bits & 0x04) plane0 |= 0x10;
133
+ if (bits & 0x02) plane0 |= 0x08;
134
+ if (bits & 0x01) plane0 |= 0x04;
135
+ tile_buf[row] = (u16)plane0;
136
+ }
137
+ load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
138
+ }
139
+ }
140
+
141
+ static void upload_art(void) {
142
+ /* ship: an upward-pointing arrow */
143
+ static const u16 ship[16] = {
144
+ 0x0180, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x0FF0, 0x0FF0,
145
+ 0x1FF8, 0x1FF8, 0x3FFC, 0x7FFE, 0xFFFF, 0xE187, 0xC003, 0x8001
146
+ };
147
+ /* bullet: a small vertical pellet */
148
+ static const u16 bullet[16] = {
149
+ 0x0000, 0x0180, 0x03C0, 0x03C0, 0x07E0, 0x07E0, 0x07E0, 0x07E0,
150
+ 0x07E0, 0x07E0, 0x03C0, 0x03C0, 0x0180, 0x0000, 0x0000, 0x0000
151
+ };
152
+ /* enemy: a downward, blocky invader */
153
+ static const u16 enemy[16] = {
154
+ 0x0000, 0x4002, 0x6006, 0x7FFE, 0x7FFE, 0xFDBF, 0xFFFF, 0xFFFF,
155
+ 0xFFFF, 0x7FFE, 0x3FFC, 0x1FF8, 0x300C, 0x6006, 0x4002, 0x0000
156
+ };
157
+ upload_font();
158
+ make_solid_tile(tile_buf, 1); load_tiles(STAR0_VRAM, tile_buf, 16);
159
+ make_solid_tile(tile_buf, 2); load_tiles(STAR1_VRAM, tile_buf, 16);
160
+ make_star_tile(tile_buf); load_tiles(STAR2_VRAM, tile_buf, 16);
161
+ make_sprite(SHIP_VRAM, ship, 1); /* white */
162
+ make_sprite(BULLET_VRAM, bullet, 1); /* white (sub-pal 1 = yellow) */
163
+ make_sprite(ENEMY_VRAM, enemy, 1); /* white (sub-pal 2 = red) */
164
+ }
165
+
166
+ /* ---- BAT / HUD ---------------------------------------------------------- */
167
+ static void draw_starfield(void) {
168
+ u8 r, c;
169
+ u16 e0 = BAT_ENTRY(0, STAR0_VRAM);
170
+ u16 e1 = BAT_ENTRY(0, STAR1_VRAM);
171
+ u16 e2 = BAT_ENTRY(0, STAR2_VRAM);
172
+ u16 e;
173
+ for (r = 0; r < 32; ++r) {
174
+ vram_set_write_addr((u16)(BAT_VRAM + r * 32));
175
+ for (c = 0; c < 32; ++c) {
176
+ e = (r & 2) ? e1 : e0; /* depth bands */
177
+ if (((r * 7 + c * 5) & 7) == 0) e = e2; /* sparse stars */
178
+ VDC_DATA_LO = (u8)(e & 0xFF);
179
+ VDC_DATA_HI = (u8)(e >> 8);
180
+ }
181
+ }
182
+ }
183
+
184
+ static void put_glyph(u8 col, u8 row, u8 glyph) {
185
+ u16 e = BAT_ENTRY(0, (u16)(FONT_VRAM + glyph * 16));
186
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
187
+ VDC_DATA_LO = (u8)(e & 0xFF);
188
+ VDC_DATA_HI = (u8)(e >> 8);
189
+ }
190
+
191
+ static void draw_hud_label(void) {
192
+ static const u8 lbl[5] = { G_S, G_C, G_O, G_R, G_E };
193
+ u8 i;
194
+ for (i = 0; i < 5; ++i) put_glyph((u8)(1 + i), 1, lbl[i]);
195
+ }
196
+
197
+ static void draw_score(void) {
198
+ u16 v = score;
199
+ u8 d0, d1, d2, d3;
200
+ d3 = (u8)(v % 10); v /= 10;
201
+ d2 = (u8)(v % 10); v /= 10;
202
+ d1 = (u8)(v % 10); v /= 10;
203
+ d0 = (u8)(v % 10);
204
+ put_glyph(7, 1, (u8)(G_0 + d0));
205
+ put_glyph(8, 1, (u8)(G_0 + d1));
206
+ put_glyph(9, 1, (u8)(G_0 + d2));
207
+ put_glyph(10, 1, (u8)(G_0 + d3));
208
+ }
209
+
210
+ /* ---- gameplay helpers --------------------------------------------------- */
211
+ static u8 aabb(Obj *a, Obj *b) {
212
+ return (u8)(a->x < b->x + 14 && a->x + 14 > b->x &&
213
+ a->y < b->y + 14 && a->y + 14 > b->y);
214
+ }
215
+
216
+ static u16 next_rand(void) {
217
+ rng = (u16)(rng * 25173u + 13849u);
218
+ return rng;
219
+ }
220
+
221
+ static void fire(void) {
222
+ u8 i;
223
+ for (i = 0; i < MAX_BULLETS; ++i) {
224
+ if (!bullets[i].alive) {
225
+ bullets[i].x = player.x;
226
+ bullets[i].y = (u16)(player.y - 10);
227
+ bullets[i].alive = 1;
228
+ psg_tone(2, 0x180, 26);
229
+ sfx_timer = 4;
230
+ return;
231
+ }
232
+ }
233
+ }
234
+
235
+ static void spawn(void) {
236
+ u8 i;
237
+ for (i = 0; i < MAX_ENEMIES; ++i) {
238
+ if (!enemies[i].alive) {
239
+ enemies[i].x = (u16)(8 + (next_rand() >> 8) % 224);
240
+ enemies[i].y = 8;
241
+ enemies[i].alive = 1;
242
+ return;
243
+ }
244
+ }
245
+ }
246
+
247
+ void main(void) {
248
+ u8 i, j;
249
+
250
+ _pce_keep[0] = 0;
251
+
252
+ /* palette: BG sub-pal 0 + sprite sub-pals 0/1/2 */
253
+ vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop dark blue */
254
+ vce_set_color(1, PCE_RGB(0, 0, 3)); /* BG c1: deep space blue */
255
+ vce_set_color(2, PCE_RGB(1, 1, 4)); /* BG c2: lighter space band */
256
+ vce_set_color(3, PCE_RGB(7, 7, 7)); /* BG c3: star white */
257
+ vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr pal0 transparent */
258
+ vce_set_color(257, PCE_RGB(2, 6, 7)); /* spr pal0 c1: cyan ship */
259
+ vce_set_color(272, PCE_RGB(0, 0, 0)); /* spr pal1 transparent */
260
+ vce_set_color(273, PCE_RGB(7, 7, 0)); /* spr pal1 c1: yellow bullet */
261
+ vce_set_color(288, PCE_RGB(0, 0, 0)); /* spr pal2 transparent */
262
+ vce_set_color(289, PCE_RGB(7, 1, 1)); /* spr pal2 c1: red enemy */
263
+
264
+ upload_art();
265
+ draw_starfield();
266
+ draw_hud_label();
267
+
268
+ player.x = 120; player.y = 180; player.alive = 1;
269
+ for (i = 0; i < MAX_BULLETS; ++i) bullets[i].alive = 0;
270
+ for (i = 0; i < MAX_ENEMIES; ++i) enemies[i].alive = 0;
271
+ score = 0;
272
+ spawn_timer = 0;
273
+ rng = 0xC0DE;
274
+ prev_pad = 0;
275
+ sfx_timer = 0;
276
+ draw_score();
277
+
278
+ pce_joy_init();
279
+ disp_enable();
280
+
281
+ for (;;) {
282
+ waitvsync();
283
+ pad = pce_joy_read();
284
+
285
+ /* move ship */
286
+ if ((pad & PCE_JOY_LEFT) && player.x > 2) player.x -= 3;
287
+ if ((pad & PCE_JOY_RIGHT) && player.x < 238) player.x += 3;
288
+ if ((pad & PCE_JOY_UP) && player.y > 8) player.y -= 3;
289
+ if ((pad & PCE_JOY_DOWN) && player.y < 208) player.y += 3;
290
+ if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I)) fire();
291
+ prev_pad = pad;
292
+
293
+ /* advance bullets */
294
+ for (i = 0; i < MAX_BULLETS; ++i) {
295
+ if (!bullets[i].alive) continue;
296
+ if (bullets[i].y < 6) { bullets[i].alive = 0; continue; }
297
+ bullets[i].y -= 6;
298
+ }
299
+
300
+ /* advance enemies */
301
+ for (i = 0; i < MAX_ENEMIES; ++i) {
302
+ if (!enemies[i].alive) continue;
303
+ enemies[i].y += 1;
304
+ if (enemies[i].y >= 224) enemies[i].alive = 0;
305
+ }
306
+
307
+ /* spawn waves */
308
+ spawn_timer++;
309
+ if (spawn_timer >= 36) { spawn_timer = 0; spawn(); }
310
+
311
+ /* bullet vs enemy */
312
+ for (i = 0; i < MAX_BULLETS; ++i) {
313
+ if (!bullets[i].alive) continue;
314
+ for (j = 0; j < MAX_ENEMIES; ++j) {
315
+ if (!enemies[j].alive) continue;
316
+ if (aabb(&bullets[i], &enemies[j])) {
317
+ bullets[i].alive = 0;
318
+ enemies[j].alive = 0;
319
+ if (score < 9999) score += 10;
320
+ draw_score();
321
+ psg_tone(3, 0x040, 28);
322
+ sfx_timer = 6;
323
+ break;
324
+ }
325
+ }
326
+ }
327
+
328
+ /* free the SFX channels so they're blips, not drones */
329
+ if (sfx_timer) {
330
+ --sfx_timer;
331
+ if (sfx_timer == 0) { psg_off(2); psg_off(3); }
332
+ }
333
+
334
+ /* push sprites: player(0), bullets(1..6), enemies(7..12) */
335
+ set_sprite(0, player.x, player.y, SHIP_VRAM >> 6, 0);
336
+ for (i = 0; i < MAX_BULLETS; ++i) {
337
+ u16 by = bullets[i].alive ? bullets[i].y : 0x1F0; /* park off-screen */
338
+ set_sprite((u8)(1 + i), bullets[i].x, by, BULLET_VRAM >> 6, 1);
339
+ }
340
+ for (i = 0; i < MAX_ENEMIES; ++i) {
341
+ u16 ey = enemies[i].alive ? enemies[i].y : 0x1F0;
342
+ set_sprite((u8)(7 + i), enemies[i].x, ey, ENEMY_VRAM >> 6, 2);
343
+ }
344
+ satb_dma();
345
+ }
346
+ }
@@ -0,0 +1,254 @@
1
+ /*
2
+ * PC Engine "sports" — a Pong-style two-paddle scaffold.
3
+ *
4
+ * Two paddles and a bouncing ball on a netted court. The d-pad moves player 1's
5
+ * (left) paddle up/down. Player 2's (right) paddle follows the ball with a
6
+ * chase-AI so the game is playable solo. The ball deflects off paddles and the
7
+ * top/bottom court lines; a ball past either edge scores for the other side and
8
+ * re-serves. Score is shown with background digit tiles. Mirrors the
9
+ * NES/Genesis/SNES/GB/SMS sports scaffolds.
10
+ *
11
+ * Paddles + ball are hardware sprites; the court (green field, white border
12
+ * lines, dashed centre net) is the BG tilemap, so the screen is clearly a
13
+ * sports court (clears the verify gate).
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
+ *
19
+ * cc65 is C89 — declare locals at the top of a block.
20
+ */
21
+ #include <pce.h>
22
+ #include <stdint.h> /* int8_t/int16_t for ball velocity + positions */
23
+ #include "pce_hw.h"
24
+
25
+ /* ---- VRAM layout (word addresses) --------------------------------------- */
26
+ #define BAT_VRAM 0x0000
27
+ #define FONT_VRAM 0x1000 /* digit tiles */
28
+ #define GREEN_VRAM 0x1400 /* court field (colour 1) */
29
+ #define LINE_VRAM 0x1410 /* court line / border (colour 2) */
30
+ #define NET_VRAM 0x1420 /* dashed centre net */
31
+ #define PADDLE_VRAM 0x1800 /* 16x16 paddle segment */
32
+ #define BALL_VRAM 0x1840 /* 16x16 ball */
33
+
34
+ #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
35
+
36
+ #define COURT_TOP 24
37
+ #define COURT_BOT 216
38
+ #define PADDLE_H 48 /* 3 stacked 16px sprite segments */
39
+ #define BALL_SIZE 12
40
+ #define PADDLE_X1 16
41
+ #define PADDLE_X2 224
42
+
43
+ /* ---- font (digits only) ------------------------------------------------- */
44
+ #define NUM_GLYPHS 10
45
+ static const u8 FONT5x7[NUM_GLYPHS][7] = {
46
+ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, /* 0 */
47
+ {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}, /* 1 */
48
+ {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, /* 2 */
49
+ {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E}, /* 3 */
50
+ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, /* 4 */
51
+ {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}, /* 5 */
52
+ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, /* 6 */
53
+ {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}, /* 7 */
54
+ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, /* 8 */
55
+ {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C} /* 9 */
56
+ };
57
+
58
+ /* ---- state -------------------------------------------------------------- */
59
+ static int16_t p1y, p2y, bx, by;
60
+ static int8_t bdx, bdy;
61
+ static u8 score_p1, score_p2;
62
+ static u8 serve_timer;
63
+ static u8 pad;
64
+ static u16 tile_buf[16];
65
+ static u16 spr_buf[64];
66
+ static u8 sfx_timer;
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
+ /* net tile: green field (colour 1) with a colour-2 vertical dash centre column */
81
+ static void make_net_tile(u16 *t) {
82
+ u8 r;
83
+ for (r = 0; r < 8; ++r) {
84
+ u8 dash = (r < 5); /* dashed: top 5 rows of each tile are the dash */
85
+ u8 p1 = dash ? 0x18 : 0x00; /* centre 2 px -> colour 2 (plane1) */
86
+ t[r] = (u16)(0x00FF | (p1 << 8)); /* plane0 full (green) + dash */
87
+ t[r + 8] = 0x0000;
88
+ }
89
+ }
90
+
91
+ static void make_paddle_sprite(void) {
92
+ u8 r;
93
+ for (r = 0; r < 64; ++r) spr_buf[r] = 0;
94
+ /* a solid 8px-wide vertical bar centred in the 16px cell, colour 1 */
95
+ for (r = 0; r < 16; ++r) spr_buf[r] = 0x0FF0;
96
+ load_tiles(PADDLE_VRAM, spr_buf, 64);
97
+ }
98
+
99
+ static void make_ball_sprite(void) {
100
+ static const u16 ball[16] = {
101
+ 0x0000, 0x0000, 0x07E0, 0x0FF0, 0x1FF8, 0x1FF8, 0x3FFC, 0x3FFC,
102
+ 0x3FFC, 0x3FFC, 0x1FF8, 0x1FF8, 0x0FF0, 0x07E0, 0x0000, 0x0000
103
+ };
104
+ u8 r;
105
+ for (r = 0; r < 64; ++r) spr_buf[r] = 0;
106
+ for (r = 0; r < 16; ++r) spr_buf[r] = ball[r]; /* colour 1 */
107
+ load_tiles(BALL_VRAM, spr_buf, 64);
108
+ }
109
+
110
+ static void upload_font(void) {
111
+ u8 g, row, bits, plane0;
112
+ for (g = 0; g < NUM_GLYPHS; ++g) {
113
+ for (row = 0; row < 16; ++row) tile_buf[row] = 0;
114
+ for (row = 0; row < 7; ++row) {
115
+ bits = FONT5x7[g][row];
116
+ plane0 = 0;
117
+ if (bits & 0x10) plane0 |= 0x40;
118
+ if (bits & 0x08) plane0 |= 0x20;
119
+ if (bits & 0x04) plane0 |= 0x10;
120
+ if (bits & 0x02) plane0 |= 0x08;
121
+ if (bits & 0x01) plane0 |= 0x04;
122
+ tile_buf[row] = (u16)plane0;
123
+ }
124
+ load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
125
+ }
126
+ }
127
+
128
+ static void draw_court(void) {
129
+ u8 r, c;
130
+ u16 g = BAT_ENTRY(0, GREEN_VRAM);
131
+ u16 ln = BAT_ENTRY(0, LINE_VRAM);
132
+ u16 nt = BAT_ENTRY(0, NET_VRAM);
133
+ u16 e;
134
+ for (r = 0; r < 32; ++r) {
135
+ vram_set_write_addr((u16)(BAT_VRAM + r * 32));
136
+ for (c = 0; c < 32; ++c) {
137
+ if (r <= 2 || r >= 27) e = ln; /* top/bottom border */
138
+ else if (c == 1 || c == 30) e = ln; /* sidelines */
139
+ else if (c == 16) e = nt; /* centre net */
140
+ else e = g; /* field */
141
+ VDC_DATA_LO = (u8)(e & 0xFF);
142
+ VDC_DATA_HI = (u8)(e >> 8);
143
+ }
144
+ }
145
+ }
146
+
147
+ static void put_glyph(u8 col, u8 row, u8 digit) {
148
+ u16 e = BAT_ENTRY(0, (u16)(FONT_VRAM + digit * 16));
149
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
150
+ VDC_DATA_LO = (u8)(e & 0xFF);
151
+ VDC_DATA_HI = (u8)(e >> 8);
152
+ }
153
+
154
+ static void draw_scores(void) {
155
+ put_glyph(12, 1, (u8)(score_p1 % 10));
156
+ put_glyph(19, 1, (u8)(score_p2 % 10));
157
+ }
158
+
159
+ static void serve_ball(u8 to_left) {
160
+ bx = 120; by = 110;
161
+ bdx = to_left ? -2 : 2;
162
+ bdy = ((score_p1 + score_p2) & 1) ? -1 : 1;
163
+ serve_timer = 40;
164
+ }
165
+
166
+ void main(void) {
167
+ u8 i;
168
+
169
+ _pce_keep[0] = 0;
170
+
171
+ /* palette */
172
+ vce_set_color(0, PCE_RGB(0, 1, 0)); /* backdrop dark green */
173
+ vce_set_color(1, PCE_RGB(0, 4, 1)); /* BG c1: court green */
174
+ vce_set_color(2, PCE_RGB(7, 7, 7)); /* BG c2: white lines/net/digit */
175
+ vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr pal0 transparent */
176
+ vce_set_color(257, PCE_RGB(7, 7, 7)); /* spr pal0 c1: white paddle */
177
+ vce_set_color(272, PCE_RGB(0, 0, 0)); /* spr pal1 transparent */
178
+ vce_set_color(273, PCE_RGB(7, 7, 0)); /* spr pal1 c1: yellow ball */
179
+
180
+ upload_font();
181
+ make_solid_tile(tile_buf, 1); load_tiles(GREEN_VRAM, tile_buf, 16);
182
+ make_solid_tile(tile_buf, 2); load_tiles(LINE_VRAM, tile_buf, 16);
183
+ make_net_tile(tile_buf); load_tiles(NET_VRAM, tile_buf, 16);
184
+ make_paddle_sprite();
185
+ make_ball_sprite();
186
+
187
+ draw_court();
188
+
189
+ p1y = 90; p2y = 90;
190
+ score_p1 = 0; score_p2 = 0;
191
+ sfx_timer = 0;
192
+ serve_ball(0);
193
+ draw_scores();
194
+
195
+ pce_joy_init();
196
+ disp_enable();
197
+
198
+ for (;;) {
199
+ u8 slot;
200
+ int16_t target;
201
+ waitvsync();
202
+
203
+ /* stage sprites: P1 paddle (3 segs), P2 paddle (3 segs), ball */
204
+ slot = 0;
205
+ for (i = 0; i < 3; ++i)
206
+ set_sprite(slot++, PADDLE_X1, (u16)(p1y + i * 16), PADDLE_VRAM >> 6, 0);
207
+ for (i = 0; i < 3; ++i)
208
+ set_sprite(slot++, PADDLE_X2, (u16)(p2y + i * 16), PADDLE_VRAM >> 6, 0);
209
+ set_sprite(slot++, (u16)bx, (u16)by, BALL_VRAM >> 6, 1);
210
+ satb_dma();
211
+
212
+ pad = pce_joy_read();
213
+
214
+ /* P1 control */
215
+ if ((pad & PCE_JOY_UP) && p1y > COURT_TOP) p1y -= 3;
216
+ if ((pad & PCE_JOY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 3;
217
+
218
+ /* P2 chase-AI */
219
+ target = (int16_t)(by - PADDLE_H / 2 + BALL_SIZE / 2);
220
+ if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 2;
221
+ else if (p2y > target && p2y > COURT_TOP) p2y -= 2;
222
+
223
+ if (serve_timer > 0) {
224
+ serve_timer--;
225
+ } else {
226
+ bx = (int16_t)(bx + bdx);
227
+ by = (int16_t)(by + bdy);
228
+
229
+ if (by < COURT_TOP) { by = COURT_TOP; bdy = (int8_t)(-bdy); psg_tone(1, 0x280, 18); sfx_timer = 4; }
230
+ if (by + BALL_SIZE > COURT_BOT) { by = (int16_t)(COURT_BOT - BALL_SIZE); bdy = (int8_t)(-bdy); psg_tone(1, 0x280, 18); sfx_timer = 4; }
231
+
232
+ /* left paddle */
233
+ if (bdx < 0 && bx <= PADDLE_X1 + 12 && bx + BALL_SIZE >= PADDLE_X1 &&
234
+ by + BALL_SIZE > p1y && by < p1y + PADDLE_H) {
235
+ bdx = (int8_t)(-bdx);
236
+ bx = PADDLE_X1 + 12;
237
+ psg_tone(0, 0x200, 22); sfx_timer = 4;
238
+ }
239
+ /* right paddle */
240
+ if (bdx > 0 && bx + BALL_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + 12 &&
241
+ by + BALL_SIZE > p2y && by < p2y + PADDLE_H) {
242
+ bdx = (int8_t)(-bdx);
243
+ bx = (int16_t)(PADDLE_X2 - BALL_SIZE);
244
+ psg_tone(0, 0x200, 22); sfx_timer = 4;
245
+ }
246
+
247
+ /* scoring */
248
+ if (bx < 2) { if (score_p2 < 9) score_p2++; draw_scores(); psg_tone(0, 0x100, 24); sfx_timer = 8; serve_ball(0); }
249
+ if (bx > 246) { if (score_p1 < 9) score_p1++; draw_scores(); psg_tone(0, 0x100, 24); sfx_timer = 8; serve_ball(1); }
250
+ }
251
+
252
+ if (sfx_timer) { --sfx_timer; if (sfx_timer == 0) { psg_off(0); psg_off(1); } }
253
+ }
254
+ }
@@ -47,9 +47,10 @@ static void vdp_init(void) {
47
47
  }
48
48
 
49
49
  /* ─── Palette + tile data ─────────────────────────────────────────── */
50
- /* SMS CRAM: 2-2-2 BGR. Entry 0 = backdrop. */
50
+ /* SMS CRAM: 2-2-2 BGR. Entry 0 = backdrop. Entries: 1 = yellow 'H',
51
+ * 2 = blue panel, 3 = dark-blue panel (the checkerboard fill colours). */
51
52
  static const uint8_t palette[32] = {
52
- 0x30, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* BG: blue, yellow */
53
+ 0x10, 0x0F, 0x30, 0x14, 0x00, 0x00, 0x00, 0x00, /* BG: backdrop, yellow, blue, dk-blue */
53
54
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
54
55
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* sprite palette unused */
55
56
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -68,6 +69,23 @@ static const uint8_t tile_h[32] = {
68
69
  0x00, 0x00, 0x00, 0x00,
69
70
  };
70
71
 
72
+ /* Tile 2 = solid colour 2 (blue), tile 3 = solid colour 3 (dark blue).
73
+ * The whole 32×24 screen is painted as a 2-colour checkerboard so the
74
+ * screen is obviously not blank and no single colour dominates. Plane 1
75
+ * set → colour 2; planes 0+1 set → colour 3. */
76
+ static const uint8_t tile_fill2[32] = {
77
+ 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00,
78
+ 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00,
79
+ 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00,
80
+ 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00,
81
+ };
82
+ static const uint8_t tile_fill3[32] = {
83
+ 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
84
+ 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
85
+ 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
86
+ 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
87
+ };
88
+
71
89
  /* ─── Upload helpers ──────────────────────────────────────────────── */
72
90
  static void load_palette(void) {
73
91
  uint8_t i;
@@ -77,16 +95,27 @@ static void load_palette(void) {
77
95
 
78
96
  static void load_tile(void) {
79
97
  uint8_t i;
80
- /* Tile 1 at VRAM offset 32 (= tile_idx * 32). Tile 0 left blank. */
98
+ /* Tile 1 = 'H' at VRAM offset 32 (= tile_idx * 32). Tile 0 left blank. */
81
99
  vdp_set_addr(32, VDP_VRAM_WRITE);
82
100
  for (i = 0; i < 32; i++) PORT_VDP_DATA = tile_h[i];
101
+ /* Tile 2 = blue fill (offset 64), tile 3 = dark-blue fill (offset 96). */
102
+ vdp_set_addr(64, VDP_VRAM_WRITE);
103
+ for (i = 0; i < 32; i++) PORT_VDP_DATA = tile_fill2[i];
104
+ vdp_set_addr(96, VDP_VRAM_WRITE);
105
+ for (i = 0; i < 32; i++) PORT_VDP_DATA = tile_fill3[i];
83
106
  }
84
107
 
108
+ /* Paint the whole 32×24 visible screen as a blue/dark-blue checkerboard so
109
+ * the screen is obviously not blank and no single colour dominates. */
85
110
  static void clear_name_table(void) {
86
- uint16_t i;
111
+ uint8_t row, col;
87
112
  vdp_set_addr(0x3800, VDP_VRAM_WRITE);
88
- /* 32 cols × 28 rows × 2 bytes = 1792 entries. */
89
- for (i = 0; i < 1792; i++) PORT_VDP_DATA = 0;
113
+ for (row = 0; row < 24; row++) {
114
+ for (col = 0; col < 32; col++) {
115
+ PORT_VDP_DATA = ((row ^ col) & 1) ? 2 : 3; /* checkerboard tiles 2/3 */
116
+ PORT_VDP_DATA = 0; /* attr: BG palette, no flip */
117
+ }
118
+ }
90
119
  }
91
120
 
92
121
  static void place_h(void) {