romdevtools 0.28.0 → 0.30.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 (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -1,341 +1,591 @@
1
- /* ── puzzle.c — Game Boy Advance Tonc match-3 falling-block scaffold
1
+ /* ── puzzle.c — Game Boy Advance falling-jewel match-3 (complete game) ────────
2
2
  *
3
- * 6-wide x 12-tall grid. A 1x3 active piece (3 colours, randomly
4
- * chosen from the cell palette) drops from the top; LEFT/RIGHT
5
- * shifts, A rotates the colour order, DOWN soft-drops, START
6
- * triggers a hard-drop. Horizontal triples clear and bump score.
3
+ * FACET FALL a COMPLETE, working game: press-start title, a falling-trio
4
+ * match-3 in a 6x12 well, cascade chains, levels that speed the fall, score +
5
+ * persistent hi-score (cartridge SRAM), and DMA/PSG music + SFX. The jewels
6
+ * are VIVID — the GBA's 15-bit palette gives 32768 colours, so each gem reads
7
+ * as a faceted stone (a bright glint, a mid body, a dark rim) rather than a
8
+ * flat block, and the well sits in a framed cabinet.
7
9
  *
8
- * The grid lives in plain RAM (u8 grid[12][6]) and is drawn into BG0's
9
- * 32x32 tile map. We redraw on landings + clears, not every frame.
10
+ * The game: a vertical trio of three jewels (each its own colour) falls into
11
+ * the well. LEFT/RIGHT move it, A/B cycle its three colours (the classic
12
+ * trio "rotate"), DOWN soft-drops, START hard-drops. When it lands, any
13
+ * straight run of 3+ same-coloured jewels (horizontal, vertical, OR diagonal)
14
+ * clears; survivors fall and cascades chain for multiplied score. Clear enough
15
+ * and the level ticks up and the fall quickens. Stack to the rim and it's over.
10
16
  *
11
- * Cell size: 8x8 (one BG tile per cell). Grid origin at tile (4, 2)
12
- * so the playfield sits at pixel (32, 16) with room for score above
13
- * and instructions below.
17
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
18
+ * very different one. The markers tell you what's what:
19
+ * HARDWARE IDIOM (load-bearing) — dodges a documented GBA footgun; reshape
20
+ * your gameplay around it (see TROUBLESHOOTING before changing).
21
+ * GAME LOGIC (clay) — match rules, tuning, art, scoring: reshape freely.
14
22
  *
15
- * Cells:
16
- * 0 = empty 1 = red 2 = green 3 = blue
23
+ * What depends on what:
24
+ * gba_sfx.{h,c} PSG sound: sfx_tone/sfx_noise one-shots + the music loop
25
+ * (sfx_music_tick once per frame — forget it and the game is silent).
26
+ * libtonc (the build links it) — VBlankIntrWait/key_poll/TTE/tonccpy.
17
27
  *
18
- * Yours to extend: vertical-triple clear, T/L shape pieces, gravity
19
- * after clears (current code just deletes; no settle), game-over.
28
+ * HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P versus on the GBA means a
29
+ * link cable between two units a second emulator instance this environment
30
+ * can't provide. So FACET FALL is a 1P MARATHON: clear, chain, level up, and
31
+ * push your hi-score. (Contrast the NES/Genesis puzzle templates, which ARE
32
+ * split-screen 2P versus — two controllers on one machine.)
33
+ *
34
+ * BANDWIDTH NOTE — and a TEACHING POINT vs the GB/NES version of this game
35
+ * (examples/nes/templates/puzzle.c): on the NES a full-board repaint must
36
+ * squeeze through a ~16-entry vblank tile queue, BUDGETED across 12 frames of
37
+ * dirty-row-bitmask tricks. The GBA has no such famine — its BG tilemap is
38
+ * plain VRAM you write whenever you like. So FACET FALL just REPAINTS THE
39
+ * WHOLE WELL (72 cells = 72 u16 SE writes) every time the board changes, in
40
+ * one go, no queue, no dirty-row gymnastics. Same genre, two bandwidth
41
+ * worlds — fork accordingly.
20
42
  */
21
43
 
22
44
  #include <tonc.h>
23
45
  #include "gba_sfx.h"
24
46
 
25
- /* draw a 5-digit score WITHOUT tte_printf (broken in this libtonc — GBA-1). */
26
- static void draw_score(int x, unsigned v) {
27
- char buf[24];
28
- int i, n = 0;
29
- buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
30
- if (x >= 100) buf[n++] = '0' + (x/100)%10;
31
- if (x >= 10) buf[n++] = '0' + (x/10)%10;
32
- buf[n++] = '0' + x%10;
33
- buf[n++]=','; buf[n++]='8'; buf[n++]='}';
34
- for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
35
- n += 5; buf[n] = 0;
36
- tte_write(buf);
47
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
48
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
49
+ #define GAME_TITLE "FACET FALL"
50
+
51
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
52
+ * Board geometry. The well is 6 wide x 12 tall, each cell ONE 8x8 BG tile.
53
+ * Origin in BG-tile coords: the playfield sits at pixel (well_tx*8, WELL_TY*8)
54
+ * with the HUD above and the controls hint below. */
55
+ #define GRID_W 6
56
+ #define GRID_H 12
57
+ #define WELL_TY 3 /* top tile row of the well interior */
58
+ #define WELL_TX 16 /* left tile col of the well interior (centered)*/
59
+
60
+ #define EMPTY 0 /* cell colours 1..3 = ruby / emerald / sapphire */
61
+
62
+ /* ── GAME LOGIC (clay) — BG tile art (regular Mode-0 4bpp BG tiles).
63
+ * Each 8x8 4bpp tile is 8 u32 rows; each nibble is a palette index within the
64
+ * BG palbank we use (bank 0). Index 0 = transparent → shows the backdrop
65
+ * colour. KEY TRICK: the three jewel tiles are the SAME faceted SHAPE drawn on
66
+ * three different palette indices (glint/body/rim per colour), so a cell
67
+ * changes colour by changing its TILE id — no palette rewrite, no attribute
68
+ * juggling. The faceted look (a 1-3-1 nibble gradient) is what makes the
69
+ * jewels pop on the GBA's wide palette. */
70
+ #define BG_BACK 1 /* cabinet backdrop dither */
71
+ #define BG_BAND 2 /* flat band behind the HUD text */
72
+ #define BG_FRAME 3 /* well frame / border */
73
+ #define BG_INNER 4 /* empty well cell (recessed, faint speck) */
74
+ #define BG_GEM1 5 /* jewel colour 1 (ruby) */
75
+ #define BG_GEM2 6 /* jewel colour 2 (emerald) */
76
+ #define BG_GEM3 7 /* jewel colour 3 (sapphire) */
77
+
78
+ /* cell colour (1..3) → BG tile id; empty shows the recessed inner cell. */
79
+ static u16 bg_tile_for(u8 col) {
80
+ return col ? (u16)(BG_GEM1 - 1 + col) : BG_INNER;
37
81
  }
38
82
 
39
- #define COLS 6
40
- #define ROWS 12
41
-
42
- #define TILE_BLANK 0
43
- #define TILE_RED 1
44
- #define TILE_GREEN 2
45
- #define TILE_BLUE 3
46
-
47
- /* Grid origin in BG-tile coords. */
48
- #define GRID_TX 4
49
- #define GRID_TY 2
50
-
51
- static const u32 tile_red[8] = {
52
- 0x11111111, 0x11111111, 0x11111111, 0x11111111,
53
- 0x11111111, 0x11111111, 0x11111111, 0x11111111,
54
- };
55
- static const u32 tile_green[8] = {
56
- 0x22222222, 0x22222222, 0x22222222, 0x22222222,
57
- 0x22222222, 0x22222222, 0x22222222, 0x22222222,
83
+ static const u32 bg_tile_back[8] = { /* steel dither cabinet */
84
+ 0x12121212, 0x21212121, 0x12121212, 0x21212121,
85
+ 0x12121212, 0x21212121, 0x12121212, 0x21212121,
58
86
  };
59
- static const u32 tile_blue[8] = {
87
+ static const u32 bg_tile_band[8] = { /* solid band behind the HUD */
60
88
  0x33333333, 0x33333333, 0x33333333, 0x33333333,
61
89
  0x33333333, 0x33333333, 0x33333333, 0x33333333,
62
90
  };
63
- /* Backdrop tile (colour index 4 = steel grey): a dither so the whole screen
64
- * reads as a "cabinet" behind the playfield instead of flat black — a lone
65
- * 6x12 grid floating on black looks blank to a human (frame verify <92%). */
66
- /* Solid light-grey wall tile for the well border. */
67
- static const u32 tile_wall[8] = {
68
- 0x55555555, 0x55555555, 0x55555555, 0x55555555,
69
- 0x55555555, 0x55555555, 0x55555555, 0x55555555,
91
+ static const u32 bg_tile_frame[8] = { /* bevelled steel border */
92
+ 0x44444444, 0x43333334, 0x43333334, 0x43333334,
93
+ 0x43333334, 0x43333334, 0x43333334, 0x44444444,
70
94
  };
71
- #define TILE_WALL 5
72
-
73
- static const u32 tile_back[8] = {
74
- 0x40404040, 0x04040404, 0x40404040, 0x04040404,
75
- 0x40404040, 0x04040404, 0x40404040, 0x04040404,
95
+ static const u32 bg_tile_inner[8] = { /* recessed empty cell + speck */
96
+ 0x00000000, 0x00000000, 0x00000000, 0x00011000,
97
+ 0x00011000, 0x00000000, 0x00000000, 0x00000000,
98
+ };
99
+ /* One faceted-jewel SHAPE per colour. The nibbles are a tiny gradient:
100
+ * 2 = glint (bright), 1 = body (mid), 3 = rim (dark). build_gem_tiles()
101
+ * remaps {1,2,3} into each colour's three palette indices so the single
102
+ * shape becomes three distinct, vivid stones. */
103
+ static const u32 gem_shape[8] = {
104
+ 0x00033300, 0x00321230, 0x03212210, 0x32122130,
105
+ 0x32112130, 0x03211230, 0x00321330, 0x00033300,
76
106
  };
77
- #define TILE_BACK 4
78
-
79
- static u8 grid[ROWS][COLS];
80
-
81
- static u8 piece[3];
82
- static s16 piece_x;
83
- static s16 piece_y;
84
- static u16 fall_timer;
85
- static u16 score;
86
- static u32 rng_state = 1;
87
-
88
- static u32 xorshift(void) {
89
- rng_state ^= rng_state << 13;
90
- rng_state ^= rng_state >> 17;
91
- rng_state ^= rng_state << 5;
92
- return rng_state;
107
+ static u32 gem_ram[3][8]; /* colours 1..3, built at boot */
108
+
109
+ /* ── GAME LOGIC (clay) — remap the one jewel shape into three vivid colours.
110
+ * Shape nibble n in {1,2,3} → palette index (3*k + n) for colour k, so each
111
+ * jewel uses its own 3-index slice of the palbank (glint/body/rim). */
112
+ static void build_gem_tiles(void) {
113
+ int k, r, i;
114
+ for (k = 0; k < 3; k++)
115
+ for (r = 0; r < 8; r++) {
116
+ u32 v = gem_shape[r], out = 0;
117
+ for (i = 0; i < 8; i++) {
118
+ u32 nib = (v >> (i * 4)) & 0xF;
119
+ if (nib) nib = (u32)(3 * k + nib); /* into this colour's slice */
120
+ out |= nib << (i * 4);
121
+ }
122
+ gem_ram[k][r] = out;
123
+ }
93
124
  }
94
125
 
95
- static u8 random_colour(void) { return 1 + (xorshift() % 3); }
126
+ /* ── GAME LOGIC (clay — reshape freely) game state (plain BSS; the GBA has
127
+ * 256 KB of EWRAM + 32 KB of IWRAM, so none of the NES version's
128
+ * absolute-address scratch-page gymnastics).
129
+ * NOTE for headless verification: unlike the Genesis template (whose work-RAM
130
+ * globals are readable by symbol name), the GBA libretro core exposes NO
131
+ * IWRAM/EWRAM region, so a headless agent reads game state from what's ON
132
+ * HARDWARE — the BG0 tilemap (the locked well, in VRAM), OAM (the falling
133
+ * trio), and save_ram (the hi-score). The verify harness decodes those. */
134
+ #define ST_TITLE 0
135
+ #define ST_PLAY 1
136
+ #define ST_OVER 2
137
+ static u8 state;
138
+
139
+ static u8 grid[GRID_H][GRID_W]; /* the well: 0 = empty, 1..3 = colour */
140
+ static s16 piece_x; /* falling trio: column 0..5 */
141
+ static s16 piece_y; /* row of its TOP cell (<0 = above rim) */
142
+ static u8 piece_col[3]; /* trio colours, top to bottom */
143
+ static u16 score, hiscore;
144
+ static u16 cleared_total; /* gems cleared, drives the level */
145
+ static u8 level; /* 1..9, speeds up the fall */
146
+
147
+ static u8 matched[GRID_H][GRID_W];/* match scan scratch */
148
+ static u16 fall_t; /* frames until next gravity step */
149
+
150
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (a handful of ARM instructions) ── */
151
+ static u16 rng = 0xACE1;
152
+ static u8 random8(void) {
153
+ u16 r = rng;
154
+ r ^= r << 7;
155
+ r ^= r >> 9;
156
+ r ^= r << 8;
157
+ rng = r;
158
+ return (u8)r;
159
+ }
96
160
 
97
- static void new_piece(void) {
98
- piece[0] = random_colour();
99
- piece[1] = random_colour();
100
- piece[2] = random_colour();
101
- piece_x = COLS / 2 - 1;
102
- piece_y = -3;
161
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
162
+ * PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
163
+ * 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
164
+ * access doesn't fault, it just reads the same byte mirrored (and a
165
+ * wide write stores one byte), so your data "almost" round-trips and
166
+ * then the checksum never matches. Every access below is via vu8.
167
+ * 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
168
+ * image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
169
+ * the cart NO save memory at all and writes to 0x0E000000 vanish.
170
+ * The aligned, (used)-attributed const below plants that marker —
171
+ * delete it and persistence dies even though this code is untouched.
172
+ * Layout: 'V' 'X' score-lo score-hi checksum (xor ^ 0xA5) — magic+checksum
173
+ * so a fresh (0xFF-filled) cart reads as "no record" instead of garbage.
174
+ * requires: nothing else — self-contained; safe to transplant whole. */
175
+ #define SRAM_BYTE ((volatile u8 *)0x0E000000)
176
+ __attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
177
+
178
+ static u16 hiscore_load(void) {
179
+ u8 lo, hi;
180
+ if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
181
+ lo = SRAM_BYTE[2];
182
+ hi = SRAM_BYTE[3];
183
+ if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
184
+ return (u16)(lo | (hi << 8));
103
185
  }
104
186
 
105
- static int tile_for(u8 cell) {
106
- switch (cell) {
107
- case 1: return TILE_RED;
108
- case 2: return TILE_GREEN;
109
- case 3: return TILE_BLUE;
110
- default: return TILE_BACK; /* empty cell shows the backdrop, not black */
111
- }
187
+ static void hiscore_save(u16 v) {
188
+ SRAM_BYTE[0] = 'V';
189
+ SRAM_BYTE[1] = 'X';
190
+ SRAM_BYTE[2] = (u8)v;
191
+ SRAM_BYTE[3] = (u8)(v >> 8);
192
+ SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
112
193
  }
113
194
 
114
- static void draw_cell(int col, int row) {
115
- if (row < 0 || row >= ROWS) return;
116
- u8 v = grid[row][col];
117
- SCR_ENTRY *map = se_mem[28];
118
- map[(GRID_TY + row) * 32 + (GRID_TX + col)] =
119
- SE_BUILD(tile_for(v), 0, 0, 0);
195
+ /* ── GAME LOGIC (clay) TTE text helpers ────────────────────────────────────
196
+ * Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
197
+ * bundled libtonc's tte_printf with a %d conversion is broken (it routes
198
+ * through a vsnprintf path that isn't wired in this build — it garbles
199
+ * output AND wedges the loop when called per-frame, GBA-1). We build the
200
+ * string ourselves and use tte_write, which processes the #{P:x,y} position
201
+ * command but does NO format conversion → safe every frame. */
202
+ static void draw_num(int x, int y, unsigned v, int digits) {
203
+ char buf[24];
204
+ int i, n = 0;
205
+ buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
206
+ if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
207
+ if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
208
+ buf[n++] = (char)('0' + x % 10);
209
+ buf[n++] = ',';
210
+ if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
211
+ if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
212
+ buf[n++] = (char)('0' + y % 10);
213
+ buf[n++] = '}';
214
+ for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
215
+ n += digits; buf[n] = 0;
216
+ tte_write(buf);
217
+ }
218
+
219
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
220
+ * THE WELL IS BACKGROUND TILES on BG0 (Mode 0, a REGULAR text BG). A 32x32
221
+ * map (BG_REG_32x32) is one screenblock; each map entry is a u16: tile id
222
+ * (10 bits) + hflip/vflip + a 4-bit palbank. SE_BUILD(tile, palbank, hf, vf)
223
+ * packs it. Footguns this dodges:
224
+ * - VRAM IGNORES BYTE WRITES (a u8 store duplicates the byte into both
225
+ * halves of the 16-bit lane). We only ever write whole u16 SE entries
226
+ * (via this helper) and tonccpy() tile data — both VRAM-safe.
227
+ * - TTE owns BG1 (CBB 2 / SBB 30). Keep this map (SBB 28) and our tile
228
+ * graphics (CBB 0) clear of those blocks or text and well corrupt each
229
+ * other.
230
+ * Unlike the NES, there is NO vblank-queue famine here — set_cell can run
231
+ * any time, so paint_well() just rewrites all 72 cells when the board changes.
232
+ * requires: REG_BG0CNT → CBB 0 / SBB 28 (set in main), DCNT_BG0 enabled. */
233
+ static SCR_ENTRY *const well_map = se_mem[28];
234
+ static void set_cell(int tx, int ty, u16 tile) {
235
+ well_map[ty * 32 + tx] = SE_BUILD(tile, 0, 0, 0);
120
236
  }
121
237
 
122
- static void draw_grid(void) {
123
- for (int r = 0; r < ROWS; r++)
124
- for (int c = 0; c < COLS; c++)
125
- draw_cell(c, r);
238
+ /* Repaint the whole well interior from the grid (cheap on GBA — see idiom). */
239
+ static void paint_well(void) {
240
+ int r, c;
241
+ for (r = 0; r < GRID_H; r++)
242
+ for (c = 0; c < GRID_W; c++)
243
+ set_cell(WELL_TX + c, WELL_TY + r, bg_tile_for(grid[r][c]));
126
244
  }
127
245
 
128
- static void draw_piece(int col, int row, int clear) {
129
- /* Transient overlay: when clearing, restore from grid (so we don't
130
- * blow away locked cells behind the piece). */
131
- SCR_ENTRY *map = se_mem[28];
132
- for (int i = 0; i < 3; i++) {
133
- int r = row + i;
134
- if (r < 0 || r >= ROWS) continue;
135
- u8 v = clear ? grid[r][col] : piece[i];
136
- map[(GRID_TY + r) * 32 + (GRID_TX + col)] =
137
- SE_BUILD(tile_for(v), 0, 0, 0);
246
+ /* Paint the static cabinet: backdrop dither, HUD band, well frame. Done once
247
+ * per state entry (the interior is then repainted by paint_well). */
248
+ static void paint_cabinet(void) {
249
+ int r, c, x0 = WELL_TX - 1;
250
+ for (r = 0; r < 32; r++)
251
+ for (c = 0; c < 32; c++)
252
+ set_cell(c, r, (r < 2) ? BG_BAND : BG_BACK); /* rows 0-1 = HUD band */
253
+ /* well frame: a border box around the interior */
254
+ for (c = 0; c <= GRID_W + 1; c++) {
255
+ set_cell(x0 + c, WELL_TY - 1, BG_FRAME);
256
+ set_cell(x0 + c, WELL_TY + GRID_H, BG_FRAME);
138
257
  }
258
+ for (r = 0; r < GRID_H; r++) {
259
+ set_cell(x0, WELL_TY + r, BG_FRAME);
260
+ set_cell(x0 + GRID_W + 1, WELL_TY + r, BG_FRAME);
261
+ }
262
+ }
263
+
264
+ /* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
265
+ static void draw_hud_numbers(void) {
266
+ tte_erase_rect(28, 4, 70, 12); draw_num(28, 4, score, 5);
267
+ tte_erase_rect(116, 4, 158, 12); draw_num(116, 4, hiscore, 5);
268
+ tte_erase_rect(210, 4, 220, 12); draw_num(210, 4, level, 1);
139
269
  }
140
270
 
141
- static int collides(int col, int row) {
142
- if (col < 0 || col >= COLS) return 1;
143
- for (int i = 0; i < 3; i++) {
144
- int r = row + i;
145
- if (r >= ROWS) return 1;
146
- if (r >= 0 && grid[r][col] != 0) return 1;
271
+ static void draw_hud_labels(void) {
272
+ tte_erase_screen();
273
+ tte_write("#{P:8,4}SC");
274
+ tte_write("#{P:96,4}HI");
275
+ tte_write("#{P:196,4}LV");
276
+ }
277
+
278
+ static void enter_title(void) {
279
+ state = ST_TITLE;
280
+ paint_cabinet();
281
+ for (int r = 0; r < GRID_H; r++)
282
+ for (int c = 0; c < GRID_W; c++) { grid[r][c] = EMPTY; set_cell(WELL_TX + c, WELL_TY + r, BG_INNER); }
283
+ tte_erase_screen();
284
+ tte_write("#{P:64,40}" GAME_TITLE);
285
+ tte_write("#{P:76,72}PRESS START");
286
+ tte_write("#{P:88,92}HI");
287
+ draw_num(112, 92, hiscore, 5);
288
+ tte_write("#{P:32,116}DPAD MOVE - AB SPIN");
289
+ tte_write("#{P:48,128}START DROP - 1P MARATHON");
290
+ }
291
+
292
+ static void spawn_piece(void);
293
+ static void enter_play(void) {
294
+ int r, c;
295
+ state = ST_PLAY;
296
+ for (r = 0; r < GRID_H; r++)
297
+ for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
298
+ score = 0; level = 1; cleared_total = 0; fall_t = 0;
299
+ /* Stir the PRNG with time-on-title so each run differs. */
300
+ rng ^= (u16)REG_VCOUNT ^ ((u16)REG_VCOUNT << 7);
301
+ if (rng == 0) rng = 0xACE1;
302
+ paint_cabinet();
303
+ paint_well();
304
+ draw_hud_labels();
305
+ draw_hud_numbers();
306
+ /* No need to swallow the START that began the run: key_hit only fires on a
307
+ * fresh press (curr & ~prev), so the held START doesn't also hard-drop. */
308
+ spawn_piece();
309
+ }
310
+
311
+ static void enter_over(void) {
312
+ state = ST_OVER;
313
+ if (score > hiscore) {
314
+ hiscore = score;
315
+ hiscore_save(hiscore); /* byte-wise SRAM write — see the SRAM idiom */
316
+ draw_hud_numbers();
147
317
  }
148
- return 0;
318
+ sfx_noise(20); /* game-over rumble */
319
+ tte_write("#{P:84,60}GAME OVER");
320
+ tte_write("#{P:76,80}PRESS START");
149
321
  }
150
322
 
151
- /* ── match / clear / gravity core (ported from the GBC reference puzzle).
152
- * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
153
- * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
154
- * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
155
- * 4 directions, clears them, applies per-column gravity, and loops so
156
- * cascades chain (score scales with chain depth). */
157
- static u8 matched[ROWS][COLS];
323
+ /* ── GAME LOGIC (clay reshape freely) ──────────────────────────────────────
324
+ * Match scan: mark every straight run of 3+ same-coloured jewels in all 4
325
+ * directions (a cell can belong to several runs — the mask de-dupes), and
326
+ * return how many cells matched. Runs flat-out on the ARM7 no need to smear
327
+ * it across frames like the cc65 (NES) version. */
158
328
  static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
159
329
 
160
330
  static u8 mark_and_count(void) {
161
- u8 r, c, d, len, k, cnt;
162
- u8 col;
163
- s8 dr, dc;
164
- int sr, sc;
165
- cnt = 0;
166
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
167
- for (r = 0; r < ROWS; r++) {
168
- for (c = 0; c < COLS; c++) {
169
- col = grid[r][c];
170
- if (col == 0) continue;
171
- for (d = 0; d < 4; d++) {
172
- dr = DIRS4[d][0]; dc = DIRS4[d][1];
173
- sr = (int)r - dr; sc = (int)c - dc;
174
- if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
175
- && grid[sr][sc] == col) continue; /* not the run's start */
176
- len = 1;
177
- sr = (int)r + dr; sc = (int)c + dc;
178
- while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
179
- && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
180
- if (len >= 3) {
181
- sr = r; sc = c;
182
- for (k = 0; k < len; k++) {
183
- if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
184
- sr += dr; sc += dc;
185
- }
331
+ u8 r, c, d, len, k, cnt, col;
332
+ s8 dr, dc;
333
+ s16 sr, sc;
334
+ cnt = 0;
335
+ for (r = 0; r < GRID_H; r++)
336
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
337
+ for (r = 0; r < GRID_H; r++) {
338
+ for (c = 0; c < GRID_W; c++) {
339
+ col = grid[r][c];
340
+ if (col == EMPTY) continue;
341
+ for (d = 0; d < 4; d++) {
342
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
343
+ sr = (s16)r - dr; sc = (s16)c - dc;
344
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
345
+ && grid[sr][sc] == col) continue; /* not the run's start */
346
+ len = 1;
347
+ sr = (s16)r + dr; sc = (s16)c + dc;
348
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
349
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
350
+ if (len >= 3) {
351
+ sr = r; sc = c;
352
+ for (k = 0; k < len; k++) {
353
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
354
+ sr += dr; sc += dc;
355
+ }
356
+ }
357
+ }
186
358
  }
187
- }
188
359
  }
189
- }
190
- return cnt;
360
+ return cnt;
191
361
  }
192
362
 
193
- /* collapse each column so survivors rest on the floor (in place: walk
194
- * from the bottom, copying gems down to a write cursor, then zero above) */
363
+ /* Collapse each column so survivors rest on the floor (walk from the bottom,
364
+ * copying jewels down to a write cursor, then zero everything above it). */
195
365
  static void apply_gravity(void) {
196
- u8 c;
197
- int r, w;
198
- for (c = 0; c < COLS; c++) {
199
- w = ROWS - 1;
200
- for (r = ROWS - 1; r >= 0; r--) {
201
- if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
366
+ u8 c;
367
+ s16 r, w;
368
+ for (c = 0; c < GRID_W; c++) {
369
+ w = GRID_H - 1;
370
+ for (r = GRID_H - 1; r >= 0; r--) {
371
+ if (grid[r][c] != EMPTY) { grid[w][c] = grid[r][c]; w--; }
372
+ }
373
+ for (; w >= 0; w--) grid[w][c] = EMPTY;
374
+ }
375
+ }
376
+
377
+ /* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
378
+ * Returns the chain depth (0 = the lock matched nothing). Score, level and the
379
+ * full-well repaint happen here. */
380
+ static u8 resolve_board(void) {
381
+ u8 n, r, c, chain;
382
+ u16 amt;
383
+ chain = 0;
384
+ for (;;) {
385
+ n = mark_and_count();
386
+ if (n == 0) break;
387
+ ++chain;
388
+ for (r = 0; r < GRID_H; r++)
389
+ for (c = 0; c < GRID_W; c++)
390
+ if (matched[r][c]) grid[r][c] = EMPTY;
391
+ amt = (u16)n * 10;
392
+ if (chain > 1) amt *= chain; /* cascades pay multiplied */
393
+ if (score < 65000u) score += amt;
394
+ /* clear chime — pitch rises with chain depth (bigger freq code = higher) */
395
+ sfx_tone(1, (u16)(1500 + ((u16)chain << 6)), 8);
396
+ apply_gravity();
397
+ cleared_total += n;
398
+ while (level < 9 && cleared_total >= (u16)level * 10) ++level;
399
+ }
400
+ if (chain) { paint_well(); draw_hud_numbers(); }
401
+ return chain;
402
+ }
403
+
404
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
405
+ * (pieces enter from above); below the floor or on a jewel is not. */
406
+ static u8 can_place(s16 x, s16 y) {
407
+ s16 i, cy;
408
+ if (x < 0 || x >= GRID_W) return 0;
409
+ for (i = 0; i < 3; i++) {
410
+ cy = y + i;
411
+ if (cy < 0) continue;
412
+ if (cy >= GRID_H) return 0;
413
+ if (grid[cy][x] != EMPTY) return 0;
202
414
  }
203
- for (; w >= 0; w--) grid[w][c] = 0;
204
- }
415
+ return 1;
205
416
  }
206
417
 
207
- static void resolve_board(void) {
208
- u8 n, r, c, chain;
209
- unsigned int amt;
210
- chain = 0;
211
- while (1) {
212
- n = mark_and_count();
213
- if (n == 0) break;
214
- chain++;
215
- for (r = 0; r < ROWS; r++)
216
- for (c = 0; c < COLS; c++)
217
- if (matched[r][c]) grid[r][c] = 0;
218
- amt = (unsigned int)n * 10u;
219
- if (chain > 1) amt = amt * chain;
220
- if (score < 65500u) score += amt;
221
- sfx_tone(1, 1700, 10); /* clear chime */
222
- apply_gravity();
223
- }
418
+ static void spawn_piece(void) {
419
+ piece_x = GRID_W / 2;
420
+ piece_y = -2;
421
+ piece_col[0] = (u8)(1 + random8() % 3);
422
+ piece_col[1] = (u8)(1 + random8() % 3);
423
+ piece_col[2] = (u8)(1 + random8() % 3);
424
+ if (!can_place(piece_x, piece_y)) enter_over(); /* well full → game over */
224
425
  }
225
426
 
427
+ /* ── GAME LOGIC (clay) — land the trio, resolve, respawn. ── */
226
428
  static void lock_piece(void) {
227
- for (int i = 0; i < 3; i++) {
228
- int r = piece_y + i;
229
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
429
+ s16 i, y;
430
+ for (i = 0; i < 3; i++) {
431
+ y = piece_y + i;
432
+ if (y >= 0) grid[y][piece_x] = piece_col[i];
230
433
  }
434
+ paint_well();
435
+ sfx_tone(2, 900, 4); /* lock thunk */
436
+ if (piece_y < 0) { enter_over(); return; } /* locked above the rim */
231
437
  resolve_board();
232
- draw_grid();
438
+ if (state != ST_PLAY) return;
439
+ spawn_piece();
233
440
  }
234
441
 
235
- int main(void) {
236
- /* TTE first it may touch pal_bg_mem + REG_BG#CNT, so set up
237
- * our BG0 grid resources AFTER its init runs. */
238
- tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
239
- tte_write("#{P:88,8}SCORE 00000");
240
- tte_write("#{P:8,148}LR MOVE A ROT START DROP");
241
-
242
- /* BG palette for grid cells. */
243
- pal_bg_mem[0] = CLR_BLACK;
244
- pal_bg_mem[1] = CLR_RED;
245
- pal_bg_mem[2] = CLR_LIME;
246
- pal_bg_mem[3] = CLR_BLUE;
247
- pal_bg_mem[4] = RGB15(6, 6, 9); /* steel grey backdrop */
248
- pal_bg_mem[5] = RGB15(20, 20, 22); /* well border grey */
249
-
250
- /* BG tile graphics in char-block 3 (separate from TTE which used 2). */
251
- tonccpy(&tile_mem[3][TILE_RED], tile_red, sizeof(tile_red));
252
- tonccpy(&tile_mem[3][TILE_GREEN], tile_green, sizeof(tile_green));
253
- tonccpy(&tile_mem[3][TILE_BLUE], tile_blue, sizeof(tile_blue));
254
- tonccpy(&tile_mem[3][TILE_BACK], tile_back, sizeof(tile_back));
255
- tonccpy(&tile_mem[3][TILE_WALL], tile_wall, sizeof(tile_wall));
256
-
257
- /* Fill screen-block 28 (BG0 map) with the backdrop tile so the whole
258
- * screen is covered; the grid cells draw over it. (A blank/black map left
259
- * the playfield floating on black — reads as blank.) */
260
- SCR_ENTRY *map = se_mem[28];
261
- for (int i = 0; i < 32 * 32; i++) map[i] = SE_BUILD(TILE_BACK, 0, 0, 0);
262
- /* Well border — playtest: "needs border around play area". One wall
263
- * cell left/right of the grid columns + a floor row underneath. */
264
- for (int r = 0; r <= ROWS; r++) {
265
- map[(GRID_TY + r) * 32 + (GRID_TX - 1)] = SE_BUILD(TILE_WALL, 0, 0, 0);
266
- map[(GRID_TY + r) * 32 + (GRID_TX + COLS)] = SE_BUILD(TILE_WALL, 0, 0, 0);
442
+ /* ── GAME LOGIC (clay) — input + gravity. Edge-triggered moves (key_hit = one
443
+ * cell per press), held DOWN soft-drops, A/B cycle the trio's colours, START
444
+ * hard-drops. libtonc's key_poll() (called once per frame in main) maintains
445
+ * the curr/prev key state that key_hit/key_held read — that's the idiomatic
446
+ * Tonc edge-trigger, no hand-rolled prev-mask needed. May end the game
447
+ * (lock top-out). ── */
448
+ static void update_play(void) {
449
+ u8 t, fd;
450
+
451
+ if (key_hit(KEY_LEFT) && can_place(piece_x - 1, piece_y)) --piece_x;
452
+ if (key_hit(KEY_RIGHT) && can_place(piece_x + 1, piece_y)) ++piece_x;
453
+ if (key_hit(KEY_A)) { /* cycle colours downward */
454
+ t = piece_col[2]; piece_col[2] = piece_col[1];
455
+ piece_col[1] = piece_col[0]; piece_col[0] = t;
456
+ sfx_tone(2, 1300, 3);
457
+ }
458
+ if (key_hit(KEY_B)) { /* cycle colours upward */
459
+ t = piece_col[0]; piece_col[0] = piece_col[1];
460
+ piece_col[1] = piece_col[2]; piece_col[2] = t;
461
+ sfx_tone(2, 1400, 3);
462
+ }
463
+ if (key_hit(KEY_START)) { /* hard drop */
464
+ while (can_place(piece_x, piece_y + 1)) ++piece_y;
465
+ lock_piece();
466
+ return;
267
467
  }
268
- for (int c = -1; c <= COLS; c++)
269
- map[(GRID_TY + ROWS) * 32 + (GRID_TX + c)] = SE_BUILD(TILE_WALL, 0, 0, 0);
270
468
 
271
- REG_BG0CNT = BG_CBB(3) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(0);
272
- /* Bump TTE's BG1 to a LOWER priority so the grid (BG0, prio 0) renders
273
- * in front. (Higher prio number = drawn behind.) */
274
- REG_BG1CNT |= BG_PRIO(1);
469
+ if (key_held(KEY_DOWN)) fall_t += 4; /* soft drop */
470
+ ++fall_t;
471
+ fd = (u8)(32 - ((level << 1) + level)); /* 29 (lv1) .. 5 (lv9) */
472
+ if (fall_t >= fd) {
473
+ fall_t = 0;
474
+ if (can_place(piece_x, piece_y + 1)) ++piece_y;
475
+ else lock_piece(); /* may end the game */
476
+ }
477
+ }
275
478
 
276
- REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1;
479
+ /* ── GAME LOGIC (clay) stage the falling trio's sprites. The LOCKED well is
480
+ * BG tiles (only what moves every frame earns OAM slots): 3 sprites for the
481
+ * trio. Cells above the rim aren't drawn — they'd poke out over the HUD band.
482
+ * Off-screen / inactive slots park at y=200. ── */
483
+ static OBJ_ATTR obj_buffer[128];
484
+ #define TILE_TRIO 1 /* OBJ tile 1 = the faceted jewel sprite (4bpp 8x8) */
485
+
486
+ static void stage_sprites(void) {
487
+ int i;
488
+ int playing = (state == ST_PLAY);
489
+ for (i = 0; i < 3; i++) {
490
+ s16 r = piece_y + (s16)i;
491
+ u8 col = piece_col[i] ? piece_col[i] : 1;
492
+ if (playing && r >= 0) {
493
+ obj_set_attr(&obj_buffer[i], ATTR0_SQUARE, ATTR1_SIZE_8,
494
+ (u16)(ATTR2_PALBANK(col - 1) | TILE_TRIO));
495
+ obj_set_pos(&obj_buffer[i], (WELL_TX + piece_x) * 8, (WELL_TY + r) * 8);
496
+ } else {
497
+ obj_set_attr(&obj_buffer[i], ATTR0_SQUARE, ATTR1_SIZE_8,
498
+ (u16)(ATTR2_PALBANK(0) | TILE_TRIO));
499
+ obj_set_pos(&obj_buffer[i], 250, 200);
500
+ }
501
+ }
502
+ }
277
503
 
278
- for (int r = 0; r < ROWS; r++)
279
- for (int c = 0; c < COLS; c++)
280
- grid[r][c] = 0;
504
+ int main(void) {
505
+ int k;
506
+
507
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
508
+ * Init order: tiles/palettes → oam_init → irq_init + II_VBLANK → TTE init
509
+ * → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the vblank IRQ
510
+ * registered (the #1 "frozen on frame 1" cause), and enabling DISPCNT
511
+ * layers before their tiles/maps exist flashes garbage. TTE owns BG1
512
+ * (CBB 2 / SBB 30) — keep other layers off those blocks.
513
+ * requires: nothing prior; this IS the boot. */
514
+
515
+ /* BG palette (bank 0). Vivid faceted jewels: each colour gets a 3-index
516
+ * slice — body / glint / rim. The GBA's 15-bit RGB gives saturated stones
517
+ * the GB/NES can only hint at. */
518
+ pal_bg_mem[0] = RGB15(2, 2, 5); /* backdrop / transparent base */
519
+ pal_bg_mem[1] = RGB15(7, 7, 10); /* cabinet dither A */
520
+ pal_bg_mem[2] = RGB15(4, 4, 7); /* cabinet dither B */
521
+ pal_bg_mem[3] = RGB15(2, 2, 3); /* HUD band near-black */
522
+ pal_bg_mem[BG_FRAME] = RGB15(10, 11, 14); /* frame steel */
523
+ pal_bg_mem[BG_FRAME+1] = RGB15(18, 19, 23); /* frame lip */
524
+ /* jewel palette slices: index (3*k + n), n in 1..3 → body/glint/rim.
525
+ * colour 1 = ruby, 2 = emerald, 3 = sapphire. */
526
+ pal_bg_mem[3*0+1] = RGB15(26, 4, 8); pal_bg_mem[3*0+2] = RGB15(31, 18, 20); pal_bg_mem[3*0+3] = RGB15(13, 1, 3);
527
+ pal_bg_mem[3*1+1] = RGB15(4, 24, 8); pal_bg_mem[3*1+2] = RGB15(20, 31, 18); pal_bg_mem[3*1+3] = RGB15(1, 11, 4);
528
+ pal_bg_mem[3*2+1] = RGB15(6, 12, 30); pal_bg_mem[3*2+2] = RGB15(20, 24, 31); pal_bg_mem[3*2+3] = RGB15(2, 4, 14);
529
+
530
+ /* BG tile graphics → char-block 0 (TTE uses CBB 2 — kept clear). */
531
+ tonccpy(&tile_mem[0][BG_BACK], bg_tile_back, sizeof(bg_tile_back));
532
+ tonccpy(&tile_mem[0][BG_BAND], bg_tile_band, sizeof(bg_tile_band));
533
+ tonccpy(&tile_mem[0][BG_FRAME], bg_tile_frame, sizeof(bg_tile_frame));
534
+ tonccpy(&tile_mem[0][BG_INNER], bg_tile_inner, sizeof(bg_tile_inner));
535
+ build_gem_tiles();
536
+ tonccpy(&tile_mem[0][BG_GEM1], gem_ram[0], sizeof(gem_ram[0]));
537
+ tonccpy(&tile_mem[0][BG_GEM2], gem_ram[1], sizeof(gem_ram[1]));
538
+ tonccpy(&tile_mem[0][BG_GEM3], gem_ram[2], sizeof(gem_ram[2]));
539
+
540
+ /* Trio sprite: the same faceted jewel shape at OBJ tile 1, drawn per
541
+ * colour via three OBJ palbanks (0/1/2) that mirror the BG jewel slices. */
542
+ tonccpy(&tile_mem[4][TILE_TRIO], gem_shape, sizeof(gem_shape));
543
+ /* The sprite tile uses the RAW gem_shape (nibbles 1/2/3 = body/glint/rim),
544
+ * so the trio's colour is picked by the OBJ PALBANK at draw time: bank k
545
+ * carries colour-(k+1)'s body/glint/rim at indices 1/2/3. One tile, three
546
+ * vivid jewels — exactly mirroring the BG jewel palette slices. */
547
+ for (k = 0; k < 3; k++) {
548
+ pal_obj_bank[k][1] = pal_bg_mem[3*k+1]; /* body */
549
+ pal_obj_bank[k][2] = pal_bg_mem[3*k+2]; /* glint */
550
+ pal_obj_bank[k][3] = pal_bg_mem[3*k+3]; /* rim */
551
+ }
281
552
 
282
- score = 0;
283
- fall_timer = 0;
553
+ REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(2);
554
+
555
+ oam_init(obj_buffer, 128); /* hides all 128 */
284
556
 
285
- /* IRQ setup — required for VBlankIntrWait() to function. */
286
557
  irq_init(NULL);
287
558
  irq_add(II_VBLANK, NULL);
288
559
 
289
- sfx_init();
290
- new_piece();
291
- draw_grid();
560
+ sfx_init(); /* APU on; music loop ticks below */
561
+
562
+ /* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so text
563
+ * draws over everything. Mode 0 = all four BGs regular/tiled. */
564
+ tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
565
+ REG_BG1CNT |= BG_PRIO(0);
566
+ REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_OBJ | DCNT_OBJ_1D;
292
567
 
293
- u16 prev = 0;
568
+ hiscore = hiscore_load(); /* cartridge SRAM — 0 on first boot */
569
+ enter_title();
294
570
 
295
571
  while (1) {
572
+ /* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then
573
+ * commit OAM while still inside vblank (the update is far quicker than
574
+ * the 4.9ms vblank window). */
296
575
  VBlankIntrWait();
297
576
  key_poll();
298
-
299
- u16 now = key_curr_state();
300
-
301
- /* Erase the active piece visual. */
302
- draw_piece(piece_x, piece_y, 1);
303
-
304
- if ((now & KEY_LEFT) && !(prev & KEY_LEFT)
305
- && !collides(piece_x - 1, piece_y)) piece_x--;
306
- if ((now & KEY_RIGHT) && !(prev & KEY_RIGHT)
307
- && !collides(piece_x + 1, piece_y)) piece_x++;
308
- if ((now & KEY_A) && !(prev & KEY_A)) {
309
- u8 t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
310
- sfx_tone(2, 1400, 3); /* rotate click */
311
- }
312
- if ((now & KEY_START) && !(prev & KEY_START)) {
313
- while (!collides(piece_x, piece_y + 1)) piece_y++;
314
- lock_piece();
315
- new_piece();
316
- prev = now;
317
- tte_erase_rect(88 + 6*8, 8, 88 + 11*8, 16);
318
- draw_score(88 + 6*8, score);
319
- continue;
577
+ sfx_music_tick(); /* forget this → silent game */
578
+
579
+ if (state == ST_TITLE) {
580
+ if (key_hit(KEY_START | KEY_A)) enter_play();
581
+ } else if (state == ST_OVER) {
582
+ if (key_hit(KEY_START)) enter_title();
583
+ } else {
584
+ update_play();
320
585
  }
321
- prev = now;
322
-
323
- u16 fall_rate = (now & KEY_DOWN) ? 4 : 30;
324
- if (++fall_timer >= fall_rate) {
325
- fall_timer = 0;
326
- if (collides(piece_x, piece_y + 1)) {
327
- lock_piece();
328
- new_piece();
329
- } else {
330
- piece_y++;
331
- }
332
- }
333
-
334
- /* Re-draw piece in its new position. */
335
- draw_piece(piece_x, piece_y, 0);
336
586
 
337
- tte_erase_rect(88 + 6*8, 8, 88 + 11*8, 16);
338
- draw_score(88 + 6*8, score);
587
+ stage_sprites();
588
+ oam_copy(oam_mem, obj_buffer, 128);
339
589
  }
340
590
  return 0;
341
591
  }