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,7 +1,51 @@
1
- // ── puzzle.c — Atari Lynx match-3 falling-block puzzle ──────────────
2
- //
3
- // 6×12 grid drawn with tgi_bar. 3 cell colors. 1×3 active piece,
4
- // rotate / soft-drop / hard-drop / horizontal-triple clear + score.
1
+ /* ── puzzle.c — Atari Lynx falling-trio match-3 (complete example game) ───────
2
+ *
3
+ * A COMPLETE, working game title screen, score + level, in-session
4
+ * hi-score, MIKEY music + SFX, a 1P marathon falling-trio match-3 with
5
+ * cascade chains and ramping levels, AND the Lynx's signature party trick:
6
+ * HARDWARE SPRITE SCALING. When a run of gems clears, the whole well does a
7
+ * SCALE POP — Suzy redraws every surviving gem at >1.0x then eases back — a
8
+ * pure-hardware "juice" flash that costs zero CPU pixel work.
9
+ *
10
+ * The game: a trio of three coloured gems falls into a 6x12 well. LEFT/RIGHT
11
+ * move it, A/B cycle its three colours, DOWN soft-drops. When it lands, any
12
+ * straight run of 3+ same-coloured gems (horizontal, vertical, or diagonal)
13
+ * clears; survivors fall and cascades chain for multiplied score. Clearing
14
+ * gems raises the level, which speeds the fall. Stack to the rim and it's
15
+ * game over.
16
+ *
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 Lynx footgun;
20
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
21
+ * GAME LOGIC (clay) — match rules, scoring, tuning, art: reshape freely.
22
+ *
23
+ * What depends on what:
24
+ * lynx_sfx.{h,c} — MIKEY 4-voice audio (voice 0 = move/clear SFX, voice 1 =
25
+ * background melody, voice 2 = lock SFX, voice 3 = noise/game-over).
26
+ * vendor/cc65/libsrc/lynx/ — the FULL cc65 Lynx driver source shipped into
27
+ * your project. The TGI driver (tgi/lynx-160-102-16.s) is REQUIRED
28
+ * reading when graphics misbehave: every TGI call is itself a Suzy
29
+ * sprite, and our scaled gem pop rides the same engine via tgi_ioctl(0).
30
+ *
31
+ * NO HARDWARE TILEMAP (read this — it is the platform's biggest "where's the
32
+ * board renderer?" surprise): the Lynx has NO background tilemap. Suzy is a
33
+ * SPRITE BLITTER, not a tile engine. So the well is drawn the honest way:
34
+ * the full-redraw TGI loop repaints the 6x12 grid every frame as a stack of
35
+ * tgi_bar fills (one filled rect per occupied cell) — cheap because the well
36
+ * is only 48x96 px. The falling trio + the clear-pop gems are Suzy SCALABLE
37
+ * sprites layered on top. See draw_well().
38
+ *
39
+ * PLAYERS: 1. This is a handheld — head-to-head on real hardware is ComLynx,
40
+ * a cable between TWO physical Lynx units. A single emulator instance has
41
+ * nobody on the other end of the cable, so this example is honestly a
42
+ * single-player MARATHON (no fake "P2 VERSUS" that could never work here —
43
+ * contrast the NES puzzle donor, which has a real split-board 2P mode).
44
+ *
45
+ * SCREEN: 160x102. The system font is 8x8, so a full row of text is 20
46
+ * characters — the well + HUD are kept compact to fit: a 48x96 well on the
47
+ * right, a slim HUD column down the left edge.
48
+ */
5
49
 
6
50
  #include <tgi.h>
7
51
  #include <joystick.h>
@@ -9,72 +53,203 @@
9
53
  #include <stdint.h>
10
54
  #include "lynx_sfx.h"
11
55
 
12
- #define COLS 6
13
- #define ROWS 12
14
- #define CELL_PX 8
15
- #define GRID_X 56
16
- #define GRID_Y 4
56
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
57
+ * name here automatically. Keep it <=16 chars of A-Z 0-9 space dash. */
58
+ #define GAME_TITLE "QUARRY QUELL"
17
59
 
18
- static uint8_t grid[ROWS][COLS];
19
- static uint8_t piece[3];
20
- static int8_t piece_x, piece_y;
21
- static uint8_t fall_timer;
22
- static uint16_t score;
23
- static uint32_t rng = 1;
60
+ /* ── GAME LOGIC (clay — reshape freely) — well geometry (fits 160x102) ──────
61
+ * A 6x12 well of 8x8 cells = 48x96 px. We park it on the right so a slim HUD
62
+ * column lives down the left edge. WELL_PX_Y leaves a margin under the top. */
63
+ #define GRID_W 6
64
+ #define GRID_H 12
65
+ #define CELL 8
66
+ #define WELL_PX_X 92 /* left edge of the well, in pixels */
67
+ #define WELL_PX_Y 4 /* top edge of the well interior */
68
+ #define WELL_W (GRID_W * CELL) /* 48 px */
69
+ #define WELL_H (GRID_H * CELL) /* 96 px */
24
70
 
25
- static uint8_t rng_pick(void) {
26
- rng = rng * 1103515245u + 12345u;
27
- return (uint8_t)(1 + (rng >> 16) % 3);
71
+ #define EMPTY 0 /* cell colours 1..3 = white/green/red */
72
+
73
+ /* ── GAME LOGIC (clay) gem colour TGI pen. Three distinct, readable pens
74
+ * (cc65 lynx.h COLOR_* indices); EMPTY cells paint as a dim recessed speck so
75
+ * the well reads as a playfield, not raw black. */
76
+ static const uint8_t gem_pen[4] = {
77
+ COLOR_DARKGREY, /* 0 = EMPTY (recessed cell) */
78
+ COLOR_WHITE, /* 1 = white gem */
79
+ COLOR_LIGHTGREEN, /* 2 = green gem */
80
+ COLOR_RED /* 3 = red gem */
81
+ };
82
+
83
+ /* ── GAME LOGIC (clay) — board + small state ── */
84
+ #define ST_TITLE 0
85
+ #define ST_PLAY 1
86
+ #define ST_OVER 2
87
+ static uint8_t state;
88
+
89
+ static uint8_t grid[GRID_H][GRID_W]; /* locked gems, [row][col] */
90
+ static uint8_t matched[GRID_H][GRID_W]; /* scratch mask for the match scan */
91
+ static uint8_t piece_x; /* falling trio: column 0..5 */
92
+ static int8_t piece_y; /* row of its TOP cell (<0 above rim) */
93
+ static uint8_t piece_col[3]; /* trio colours, top to bottom */
94
+ static uint8_t fall_t; /* frames until the next gravity step*/
95
+ static unsigned score;
96
+ static unsigned hiscore; /* in-session only — see EEPROM note */
97
+ static unsigned cleared_total; /* gems cleared — drives the level */
98
+ static uint8_t level; /* 1..9, speeds up the fall */
99
+ static uint8_t prev_joy;
100
+ static uint8_t over_new_hi;
101
+
102
+ /* The clear-pop pulse: when >0 the well draws its gems scaled-up for a few
103
+ * frames (the SCALING signature), counting back down to the resting 1.0x. */
104
+ static uint8_t pop_timer;
105
+ #define POP_FRAMES 7
106
+
107
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
108
+ static uint16_t rng = 0xACE1;
109
+ static uint8_t rand8(void) {
110
+ uint16_t r = rng;
111
+ r ^= r << 7;
112
+ r ^= r >> 9;
113
+ r ^= r << 8;
114
+ rng = r;
115
+ return (uint8_t)r;
116
+ }
117
+ static uint8_t rand_gem(void) { return (uint8_t)(1 + rand8() % 3); }
118
+
119
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
120
+ * SUZY HARDWARE SPRITE SCALING — the Lynx signature. Suzy renders every
121
+ * sprite through a Sprite Control Block (SCB) it walks in cart/work RAM.
122
+ * Two SCB fields, HSIZE and VSIZE, are 8.8 fixed-point scale factors
123
+ * ($0100 = 1.0): the SAME 8x8 source pixels render at any size, every frame,
124
+ * for free. This game uses it two ways:
125
+ * - the FALLING TRIO gems are Suzy sprites drawn through this SCB at a
126
+ * fixed 1.0x (so forking in a depth/power-up scale is a one-line change);
127
+ * - the CLEAR POP — for POP_FRAMES after any match, every gem in the well
128
+ * is redrawn at >1.0x then eased back to 1.0x, a pure-hardware "juice"
129
+ * flash with zero CPU pixel cost (Suzy scales while it blits).
130
+ *
131
+ * The SCB, field by field (this is cc65's SCB_REHV_PAL from <_suzy.h>):
132
+ * sprctl0 bits 7-6 = bits per pixel (11 = 4bpp), bits 2-0 = sprite TYPE.
133
+ * TYPE_NORMAL (4) draws pens 1-15 and treats pen 0 as
134
+ * TRANSPARENT — that's how a round gem sits over the cell.
135
+ * sprctl1 bit 7 LITERAL (raw nybbles, no RLE) + bits 5-4 reload depth:
136
+ * REHV means "this SCB carries HPOS, VPOS, HSIZE, VSIZE". The
137
+ * reload bits ARE the struct layout — mismatch them and Suzy reads
138
+ * palette bytes as size words.
139
+ * sprcoll $20 = NO_COLLIDE. Match/lock collision is done in C on the grid
140
+ * (the collision buffer knows nothing about board cells).
141
+ * next pointer to the next SCB, 0 = end of chain (one blit per call).
142
+ * data sprite pixel data (LITERAL 4bpp format below).
143
+ * hpos/vpos signed SCREEN position of the sprite's top-left corner.
144
+ * hsize/vsize 8.8 scale — THE party trick, rewritten per draw.
145
+ * penpal[8] 16 nybbles mapping pixel values 0-15 → palette pens. We RECOLOUR
146
+ * the gem per draw here (one 8x8 art block, three gem colours) by
147
+ * pointing the art's pixel value 1 at the wanted pen — no extra art.
148
+ *
149
+ * LITERAL 4bpp data format (hand-encodable): each sprite LINE is
150
+ * [offset byte][width/2 bytes of raw nybble pixels]
151
+ * where offset = 1 + bytes of pixel data; a final offset of 0 ends the sprite.
152
+ * 8 px @ 4bpp = 4 data bytes, so every line starts with 5.
153
+ *
154
+ * Drawing: tgi_sprite(&scb) → tgi_ioctl(0, &scb) — the TGI driver's
155
+ * documented escape hatch (see CONTROL in vendor/cc65/libsrc/lynx/tgi/
156
+ * lynx-160-102-16.s). It points Suzy's SCBNEXT at your SCB, aims VIDBAS at
157
+ * TGI's current DRAW page (so scaled gems land in the same double-buffered
158
+ * frame as tgi_bar/tgi_outtextxy), fires SPRGO, and sleeps the CPU until
159
+ * SPRSYS reports the blit done.
160
+ *
161
+ * Requires: the cc65 crt0 Suzy init (already done before main()), and calls
162
+ * only between the tgi_busy() wait and tgi_updatedisplay() — i.e. while
163
+ * TGI's draw buffer is the blit target. Draw order = paint order: well
164
+ * fills first, scaled gems after, HUD text last.
165
+ */
166
+ static SCB_REHV_PAL scb = {
167
+ BPP_4 | TYPE_NORMAL, /* sprctl0: 4bpp, pen 0 transparent */
168
+ LITERAL | REHV, /* sprctl1: literal data, HV+size SCB */
169
+ 0x20, /* sprcoll: NO_COLLIDE */
170
+ 0, /* next: single-SCB chain */
171
+ 0, /* data: set per draw */
172
+ 0, 0, /* hpos, vpos */
173
+ 0x0100, 0x0100, /* hsize, vsize (8.8) */
174
+ { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF } /* identity pens */
175
+ };
176
+
177
+ /* ── GAME LOGIC (clay) — 8x8 4bpp literal gem art ───────────────────────────
178
+ * A single round gem shape in pixel value 1 (plus value $F = white glint).
179
+ * draw_gem() recolours value 1 → the wanted gem pen via the SCB penpal, so one
180
+ * art block paints all three gem colours. Each line: 5, then 4 nybble bytes;
181
+ * a final 0 byte ends the sprite. */
182
+ static unsigned char spr_gem[] = {
183
+ 5, 0x00, 0x11, 0x10, 0x00, /* . . 1 1 1 . . . round gem body */
184
+ 5, 0x01, 0x1F, 0xF1, 0x10, /* . 1 1 F F 1 1 . (white glint) */
185
+ 5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
186
+ 5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
187
+ 5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
188
+ 5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
189
+ 5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
190
+ 5, 0x00, 0x11, 0x10, 0x00, /* . . 1 1 1 . . . */
191
+ 0
192
+ };
193
+
194
+ /* Draw the gem sprite for board cell at top-left (x,y), recoloured to `col`
195
+ * (1..3), at an 8.8 scale. Recolour: point art pixel value 1 at the gem's pen
196
+ * by rewriting the first penpal byte (values 0,1 → transparent, pen). The
197
+ * clear pop scales every gem about its CELL CENTRE so the flash reads as a
198
+ * uniform swell, not a slide. */
199
+ static void draw_gem(int x, int y, uint8_t col, unsigned scale) {
200
+ unsigned w = (8u * scale) >> 8;
201
+ if (w == 0) w = 1;
202
+ scb.penpal[0] = (uint8_t)((0u << 4) | gem_pen[col]); /* val0=transparent, val1=pen */
203
+ scb.data = spr_gem;
204
+ scb.hsize = scale;
205
+ scb.vsize = scale;
206
+ scb.hpos = x + 4 - (int)(w >> 1); /* anchor the CELL CENTRE (cells are 8 wide) */
207
+ scb.vpos = y + 4 - (int)(w >> 1);
208
+ tgi_sprite(&scb);
28
209
  }
29
210
 
30
- static void new_piece(void) {
31
- piece[0] = rng_pick();
32
- piece[1] = rng_pick();
33
- piece[2] = rng_pick();
34
- piece_x = COLS / 2 - 1;
35
- piece_y = -3;
211
+ /* Current clear-pop scale: 1.0x at rest, swelling to ~1.5x at the pop peak and
212
+ * easing back. POP drives the SCALING idiom on every clear. */
213
+ #define POP_SCALE_PEAK 0x0180u /* 1.5x */
214
+ static unsigned pop_scale(void) {
215
+ if (pop_timer == 0) return 0x0100u;
216
+ /* linear ease: scale = 1.0 + (pop_timer/POP_FRAMES) * 0.5 */
217
+ return 0x0100u + ((unsigned)pop_timer * (POP_SCALE_PEAK - 0x0100u)) / POP_FRAMES;
36
218
  }
37
219
 
38
- static uint8_t collides(int8_t x, int8_t y) {
220
+ /* ── GAME LOGIC (clay) score text (no sprintf: it drags in ~6KB) ── */
221
+ static char numbuf[6];
222
+ static char *fmt5(unsigned v) {
39
223
  uint8_t i;
40
- int8_t r;
41
- if (x < 0 || x >= COLS) return 1;
42
- for (i = 0; i < 3; i++) {
43
- r = (int8_t)(y + i);
44
- if (r >= ROWS) return 1;
45
- if (r >= 0 && grid[r][x] != 0) return 1;
46
- }
47
- return 0;
224
+ for (i = 0; i < 5; i++) { numbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
225
+ numbuf[5] = 0;
226
+ return numbuf;
48
227
  }
49
228
 
50
- /* ── match / clear / gravity core (ported from the GBC reference puzzle).
51
- * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
52
- * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
53
- * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
54
- * 4 directions, clears them, applies per-column gravity, and loops so
55
- * cascades chain (score scales with chain depth). */
56
- static uint8_t matched[ROWS][COLS];
229
+ /* ── GAME LOGIC (clay) match scan: mark every straight run of 3+ same-
230
+ * coloured gems in all 4 directions (a cell can belong to several runs — the
231
+ * mask de-dupes), and return how many cells matched. ── */
57
232
  static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
58
233
 
59
234
  static uint8_t mark_and_count(void) {
60
- uint8_t r, c, d, len, k, cnt;
61
- uint8_t col;
235
+ uint8_t r, c, d, len, k, cnt, col;
62
236
  int8_t dr, dc;
63
237
  int sr, sc;
64
238
  cnt = 0;
65
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
66
- for (r = 0; r < ROWS; r++) {
67
- for (c = 0; c < COLS; c++) {
239
+ for (r = 0; r < GRID_H; r++)
240
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
241
+ for (r = 0; r < GRID_H; r++) {
242
+ for (c = 0; c < GRID_W; c++) {
68
243
  col = grid[r][c];
69
- if (col == 0) continue;
244
+ if (col == EMPTY) continue;
70
245
  for (d = 0; d < 4; d++) {
71
246
  dr = DIRS4[d][0]; dc = DIRS4[d][1];
72
247
  sr = (int)r - dr; sc = (int)c - dc;
73
- if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
74
- && grid[sr][sc] == col) continue; /* not the run's start */
248
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
249
+ && grid[sr][sc] == col) continue; /* not the run's start */
75
250
  len = 1;
76
251
  sr = (int)r + dr; sc = (int)c + dc;
77
- while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
252
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
78
253
  && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
79
254
  if (len >= 3) {
80
255
  sr = r; sc = c;
@@ -89,150 +264,315 @@ static uint8_t mark_and_count(void) {
89
264
  return cnt;
90
265
  }
91
266
 
92
- /* collapse each column so survivors rest on the floor (in place: walk
93
- * from the bottom, copying gems down to a write cursor, then zero above) */
267
+ /* Collapse each column so survivors rest on the floor. */
94
268
  static void apply_gravity(void) {
95
269
  uint8_t c;
96
- int r, w;
97
- for (c = 0; c < COLS; c++) {
98
- w = ROWS - 1;
99
- for (r = ROWS - 1; r >= 0; r--) {
100
- if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
101
- }
102
- for (; w >= 0; w--) grid[w][c] = 0;
270
+ int8_t r, w;
271
+ for (c = 0; c < GRID_W; c++) {
272
+ w = GRID_H - 1;
273
+ for (r = GRID_H - 1; r >= 0; r--)
274
+ if (grid[r][c] != EMPTY) { grid[w][c] = grid[r][c]; w--; }
275
+ for (; w >= 0; w--) grid[w][c] = EMPTY;
276
+ }
277
+ }
278
+
279
+ static void game_over(void) {
280
+ over_new_hi = 0;
281
+ if (score > hiscore) {
282
+ /* ── In-session hi-score ONLY — and here's the honest why. Real Lynx
283
+ * carts persist via a 93Cxx serial EEPROM on the cart PCB (cc65 even
284
+ * ships lynx_eeprom_read/write for it; see vendor/cc65/libsrc/lynx/
285
+ * eeprom.s). PROBED: the bundled handy core emulates CEEPROM internally
286
+ * but its libretro build exposes NO save path — retro_get_memory(
287
+ * SAVE_RAM) returns NULL/size 0, so nothing survives host.hardReset()
288
+ * and a bit-banged round-trip reads back garbage under the WASM build.
289
+ * Wiring the EEPROM to SAVE_RAM is a future core round; until then a fake
290
+ * "save" would be lying. The hi-score DOES survive title↔play cycles
291
+ * within one power-on. ── */
292
+ hiscore = score;
293
+ over_new_hi = 1;
103
294
  }
295
+ sfx_tone(2, 240, 24); /* voice 2: low game-over drone */
296
+ sfx_noise(16); /* voice 3: crunch */
297
+ state = ST_OVER;
104
298
  }
105
299
 
106
- static void resolve_board(void) {
300
+ /* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
301
+ * Returns the chain depth (0 = the lock matched nothing). Score, level, and
302
+ * the clear-pop fire here. ── */
303
+ static uint8_t resolve_board(void) {
107
304
  uint8_t n, r, c, chain;
108
- unsigned int amt;
305
+ unsigned amt;
109
306
  chain = 0;
110
- while (1) {
307
+ for (;;) {
111
308
  n = mark_and_count();
112
309
  if (n == 0) break;
113
- chain++;
114
- for (r = 0; r < ROWS; r++)
115
- for (c = 0; c < COLS; c++)
116
- if (matched[r][c]) grid[r][c] = 0;
117
- amt = (unsigned int)n * 10u;
118
- if (chain > 1) amt = amt * chain;
119
- if (score < 65500u) score += amt;
120
- sfx_tone(0, 60, 10); /* clear chime */
310
+ ++chain;
311
+ for (r = 0; r < GRID_H; r++)
312
+ for (c = 0; c < GRID_W; c++)
313
+ if (matched[r][c]) grid[r][c] = EMPTY;
314
+ amt = (unsigned)n * 10;
315
+ if (chain > 1) amt *= chain; /* cascades pay multiplied */
316
+ score += amt;
317
+ /* clear chime rises with chain depth; the SCALING clear-pop fires */
318
+ sfx_tone(0, (uint8_t)(70 + chain * 8), 8);
319
+ pop_timer = POP_FRAMES; /* trigger the hardware scale pop */
121
320
  apply_gravity();
321
+ cleared_total += n;
322
+ while (level < 9 && cleared_total >= (unsigned)level * 10) {
323
+ ++level;
324
+ }
122
325
  }
326
+ return chain;
123
327
  }
124
328
 
329
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
330
+ * (pieces enter from above); below the floor or on a gem is not. */
331
+ static uint8_t can_place(int8_t x, int8_t y) {
332
+ int8_t i, cy;
333
+ if (x < 0 || x >= GRID_W) return 0;
334
+ for (i = 0; i < 3; i++) {
335
+ cy = (int8_t)(y + i);
336
+ if (cy < 0) continue;
337
+ if (cy >= GRID_H) return 0;
338
+ if (grid[cy][x] != EMPTY) return 0;
339
+ }
340
+ return 1;
341
+ }
342
+
343
+ static void spawn_piece(void) {
344
+ piece_x = GRID_W / 2;
345
+ piece_y = -2;
346
+ piece_col[0] = rand_gem();
347
+ piece_col[1] = rand_gem();
348
+ piece_col[2] = rand_gem();
349
+ if (!can_place((int8_t)piece_x, piece_y)) game_over();
350
+ }
351
+
352
+ /* ── GAME LOGIC (clay) — land the trio, resolve, respawn. ── */
125
353
  static void lock_piece(void) {
126
- uint8_t i;
127
- int8_t r;
354
+ int8_t i, y;
128
355
  for (i = 0; i < 3; i++) {
129
- r = (int8_t)(piece_y + i);
130
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
356
+ y = (int8_t)(piece_y + i);
357
+ if (y >= 0) grid[y][piece_x] = piece_col[i];
131
358
  }
359
+ sfx_tone(2, 180, 4); /* voice 2: lock thunk */
360
+ if (piece_y < 0) { game_over(); return; } /* locked above the rim */
132
361
  resolve_board();
362
+ if (state != ST_PLAY) return;
363
+ spawn_piece();
364
+ }
365
+
366
+ /* ── GAME LOGIC (clay) — start a run ── */
367
+ static void start_game(void) {
368
+ uint8_t r, c;
369
+ for (r = 0; r < GRID_H; r++)
370
+ for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
371
+ score = 0;
372
+ cleared_total = 0;
373
+ level = 1;
374
+ fall_t = 0;
375
+ pop_timer = 0;
376
+ prev_joy = 0xFF; /* the button that started the run
377
+ * shouldn't also rotate the first trio */
378
+ sfx_tone(0, 80, 8); /* start chirp */
379
+ state = ST_PLAY;
380
+ spawn_piece();
381
+ }
382
+
383
+ /* ── GAME LOGIC (clay) — per-state frames. Each runs INSIDE the canonical
384
+ * loop below: scene already painted, tgi_updatedisplay not yet called. ── */
385
+
386
+ /* draw the locked well: frame + recessed backdrop, every occupied cell as a
387
+ * gem, empties as a faint speck. During a clear pop (`scaled`), occupied gems
388
+ * draw as SCALED Suzy sprites (the hardware flash); otherwise as flat bars
389
+ * (cheaper, and the gem read is identical at 1.0x). */
390
+ static void draw_well(uint8_t scaled) {
391
+ uint8_t r, c, v;
392
+ unsigned ps = pop_scale();
393
+ int px, py;
394
+ /* well frame + recessed backdrop so it reads as a playfield */
395
+ tgi_setcolor(COLOR_GREY);
396
+ tgi_bar(WELL_PX_X - 2, WELL_PX_Y - 2, WELL_PX_X + WELL_W + 1, WELL_PX_Y + WELL_H + 1);
397
+ tgi_setcolor(COLOR_BLACK);
398
+ tgi_bar(WELL_PX_X, WELL_PX_Y, WELL_PX_X + WELL_W - 1, WELL_PX_Y + WELL_H - 1);
399
+ /* empty-cell specks (always flat) */
400
+ tgi_setcolor(COLOR_DARKGREY);
401
+ for (r = 0; r < GRID_H; r++)
402
+ for (c = 0; c < GRID_W; c++)
403
+ if (grid[r][c] == EMPTY) {
404
+ px = WELL_PX_X + c * CELL; py = WELL_PX_Y + r * CELL;
405
+ tgi_bar(px + 3, py + 3, px + 4, py + 4);
406
+ }
407
+ /* gems */
408
+ for (r = 0; r < GRID_H; r++)
409
+ for (c = 0; c < GRID_W; c++) {
410
+ v = grid[r][c];
411
+ if (v == EMPTY) continue;
412
+ if (scaled) {
413
+ draw_gem(WELL_PX_X + c * CELL, WELL_PX_Y + r * CELL, v, ps);
414
+ } else {
415
+ px = WELL_PX_X + c * CELL; py = WELL_PX_Y + r * CELL;
416
+ tgi_setcolor(gem_pen[v]);
417
+ tgi_bar(px + 1, py + 1, px + 6, py + 6);
418
+ }
419
+ }
133
420
  }
134
421
 
135
- static uint8_t cell_color(uint8_t v) {
136
- switch (v) {
137
- case 1: return COLOR_RED;
138
- case 2: return COLOR_GREEN;
139
- case 3: return COLOR_BLUE;
140
- default: return COLOR_BLACK;
422
+ static unsigned attract_phase;
423
+
424
+ static void frame_title(uint8_t joy) {
425
+ /* attract: a lone gem in the title's clear zone pulses via the SCALING
426
+ * idiom the same swell the clear-pop uses, shown off on the menu. */
427
+ unsigned t = attract_phase < 64 ? attract_phase : (127 - attract_phase);
428
+ unsigned s = 0x00C0u + (t * (0x0200u - 0x00C0u)) / 63u; /* 0.75x..2.0x */
429
+ attract_phase = (attract_phase + 2) & 127;
430
+
431
+ draw_gem(120, 10, 2, s); /* breathing green gem, top-right zone */
432
+
433
+ tgi_setcolor(COLOR_WHITE);
434
+ tgi_outtextxy(8, 24, GAME_TITLE);
435
+ tgi_setcolor(COLOR_YELLOW);
436
+ tgi_outtextxy(28, 44, "PRESS A");
437
+ tgi_setcolor(COLOR_LIGHTGREY);
438
+ tgi_outtextxy(8, 60, "HI ");
439
+ tgi_outtextxy(32, 60, fmt5(hiscore));
440
+ tgi_outtextxy(4, 76, "1 PLAYER MARATHON"); /* handheld honesty */
441
+
442
+ if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) start_game();
443
+ }
444
+
445
+ static void frame_over(uint8_t joy) {
446
+ draw_well(0);
447
+ tgi_setcolor(COLOR_DARKGREY);
448
+ tgi_bar(6, 30, 86, 74);
449
+ tgi_setcolor(COLOR_WHITE);
450
+ tgi_outtextxy(12, 34, "GAME OVER");
451
+ tgi_setcolor(COLOR_YELLOW);
452
+ tgi_outtextxy(10, 46, "SCORE");
453
+ tgi_outtextxy(10, 56, fmt5(score));
454
+ if (over_new_hi) { tgi_setcolor(COLOR_LIGHTGREEN); tgi_outtextxy(8, 66, "NEW HI"); }
455
+ else { tgi_setcolor(COLOR_LIGHTGREY); tgi_outtextxy(8, 66, "A TITLE"); }
456
+ if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) state = ST_TITLE;
457
+ }
458
+
459
+ /* stage the falling trio (3 gems) above the locked stack, each at 1.0x */
460
+ static void draw_piece(void) {
461
+ uint8_t i;
462
+ int8_t y;
463
+ for (i = 0; i < 3; i++) {
464
+ y = (int8_t)(piece_y + i);
465
+ if (y >= 0)
466
+ draw_gem(WELL_PX_X + piece_x * CELL, WELL_PX_Y + (int)y * CELL,
467
+ piece_col[i], 0x0100);
468
+ }
469
+ }
470
+
471
+ static void frame_play(uint8_t joy) {
472
+ uint8_t newp, fd, t;
473
+
474
+ /* ── draw: well (scaled gems while the clear-pop runs), falling trio, HUD ── */
475
+ draw_well(pop_timer != 0);
476
+ draw_piece();
477
+
478
+ tgi_setcolor(COLOR_WHITE);
479
+ tgi_outtextxy(4, 2, "SC");
480
+ tgi_outtextxy(4, 12, fmt5(score));
481
+ tgi_setcolor(COLOR_LIGHTGREY);
482
+ tgi_outtextxy(4, 28, "HI");
483
+ tgi_outtextxy(4, 38, fmt5(hiscore));
484
+ tgi_setcolor(COLOR_YELLOW);
485
+ tgi_outtextxy(4, 54, "LV");
486
+ numbuf[0] = (char)('0' + level); numbuf[1] = 0;
487
+ tgi_outtextxy(28, 54, numbuf);
488
+
489
+ /* ── update: edge-triggered moves; A/B cycle the trio; held DOWN soft-drops.
490
+ * JOY_BTN_1/2(newp) test the press-EDGE mask, so one cell/cycle per press. ── */
491
+ newp = (uint8_t)(joy & (uint8_t)~prev_joy);
492
+ if ((newp & JOY_LEFT_MASK) && can_place((int8_t)(piece_x - 1), piece_y)) --piece_x;
493
+ if ((newp & JOY_RIGHT_MASK) && can_place((int8_t)(piece_x + 1), piece_y)) ++piece_x;
494
+ if (JOY_BTN_1(newp)) { /* A: cycle colours downward */
495
+ t = piece_col[2];
496
+ piece_col[2] = piece_col[1];
497
+ piece_col[1] = piece_col[0];
498
+ piece_col[0] = t;
499
+ sfx_tone(0, 110, 3);
500
+ }
501
+ if (JOY_BTN_2(newp)) { /* B: cycle colours upward */
502
+ t = piece_col[0];
503
+ piece_col[0] = piece_col[1];
504
+ piece_col[1] = piece_col[2];
505
+ piece_col[2] = t;
506
+ sfx_tone(0, 120, 3);
507
+ }
508
+ if (joy & JOY_DOWN_MASK) fall_t += 4; /* soft drop */
509
+
510
+ if (pop_timer) pop_timer--; /* ease the clear-pop back to 1.0x */
511
+
512
+ /* gravity: faster as the level climbs (29..5 frames per row) */
513
+ ++fall_t;
514
+ fd = (uint8_t)(32 - ((level << 1) + level)); /* 32 - 3*level → 29..5 */
515
+ if (fall_t >= fd) {
516
+ fall_t = 0;
517
+ if (can_place((int8_t)piece_x, (int8_t)(piece_y + 1)))
518
+ ++piece_y;
519
+ else
520
+ lock_piece(); /* may end the game */
141
521
  }
142
522
  }
143
523
 
144
524
  void main(void) {
145
- uint8_t joy, prev = 0, fall_rate, t;
146
- uint8_t r, c, i;
147
- int8_t pr;
525
+ uint8_t joy;
148
526
 
149
527
  tgi_install(&lynx_160_102_16_tgi);
150
528
  tgi_init();
151
529
  joy_install(&lynx_stdjoy_joy);
152
- sfx_init();
530
+ sfx_init(); /* MIKEY up; background melody starts on voice 1 */
153
531
 
154
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) grid[r][c] = 0;
155
- score = 0; fall_timer = 0;
156
- new_piece();
532
+ state = ST_TITLE;
533
+ prev_joy = 0;
534
+ attract_phase = 0;
535
+ hiscore = 0;
157
536
 
158
537
  for (;;) {
159
- /* Lynx frame loop: WAIT for the blitter, then clear with a full-screen
160
- * tgi_bar (NOT tgi_clear, which leaves the back page stale on this core)
161
- * — drawing while the blitter is mid-flight loses the frame black.
162
- * (Copied from the shmup scaffold, the LYNX-1 fix.) */
538
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
539
+ * CANONICAL LYNX GAME LOOP full-redraw every frame, in this order:
540
+ * 1. while (tgi_busy()) { } — WAIT for the previous frame's page flip.
541
+ * Skipping this is the #1 "Lynx screen stays blank" trap: drawing
542
+ * while the swap is pending loses the frame.
543
+ * 2. Repaint the WHOLE scene with tgi_bar fills — NOT tgi_clear()
544
+ * (which can leave the framebuffer stale on this toolchain+emulator
545
+ * path). TGI double-buffers; the back buffer holds the frame from
546
+ * two flips ago, so partial redraws ghost. With no hardware tilemap
547
+ * (header), the WELL is repainted cell-by-cell every frame.
548
+ * 3. Draw every object (every TGI call and every tgi_sprite() is a
549
+ * synchronous Suzy blit into the SAME draw page).
550
+ * 4. tgi_updatedisplay() — request the page flip at next VBL.
551
+ * 5. sfx_update() IMMEDIATELY after — MIKEY voice writes must land in
552
+ * vblank: handy reschedules its timer sweep on the spot when a voice
553
+ * CTL bit-3 write lands, and mid-frame that sweep can preempt an
554
+ * in-flight Suzy blit and eat sprites (the R57 bug — history in
555
+ * lynx_sfx.c). sfx_tone()/sfx_noise() only STAGE; sfx_update() is
556
+ * the hardware flush. */
163
557
  while (tgi_busy()) { }
164
558
 
165
- /* ── Background scene (drawn every frame). Without it the playfield is
166
- * a near-flat single colour and the render-health audit flags the
167
- * screen as blank. A framed "well" in the centre with lit side panels
168
- * keeps several distinct colours well under the threshold:
169
- * - blue cabinet backdrop
170
- * - dark-grey side panels flanking the well
171
- * - black well interior so the falling blocks read clearly
172
- * - light-grey well frame + a faint grid texture behind the cells. */
559
+ /* background: a dim field so no frame is a flat single colour (a >=92%
560
+ * single-colour frame trips the render-health audit as "blank"). */
173
561
  tgi_setcolor(COLOR_BLUE);
174
- tgi_bar(0, 0, tgi_getmaxx(), tgi_getmaxy()); /* cabinet backdrop */
175
- tgi_setcolor(COLOR_DARKGREY);
176
- tgi_bar(0, 0, GRID_X - 5, 101); /* left side panel */
177
- tgi_bar(GRID_X + COLS * CELL_PX + 4, 0, 159, 101); /* right side panel */
178
- tgi_setcolor(COLOR_BLACK);
179
- tgi_bar(GRID_X - 2, GRID_Y - 2,
180
- GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1); /* well */
181
- /* faint grid texture so the empty well is never one flat colour */
182
- tgi_setcolor(COLOR_DARKGREY);
183
- for (r = 0; r <= ROWS; r++)
184
- tgi_line(GRID_X, GRID_Y + r * CELL_PX, GRID_X + COLS * CELL_PX - 1, GRID_Y + r * CELL_PX);
185
- for (c = 0; c <= COLS; c++)
186
- tgi_line(GRID_X + c * CELL_PX, GRID_Y, GRID_X + c * CELL_PX, GRID_Y + ROWS * CELL_PX - 1);
187
- /* well frame */
188
- tgi_setcolor(COLOR_LIGHTGREY);
189
- tgi_line(GRID_X - 2, GRID_Y - 2, GRID_X - 2, GRID_Y + ROWS * CELL_PX + 1);
190
- tgi_line(GRID_X + COLS * CELL_PX + 1, GRID_Y - 2, GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1);
191
- tgi_line(GRID_X - 2, GRID_Y + ROWS * CELL_PX + 1, GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1);
192
-
193
- /* grid */
194
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) {
195
- if (grid[r][c] != 0) {
196
- tgi_setcolor(cell_color(grid[r][c]));
197
- tgi_bar(GRID_X + c * CELL_PX, GRID_Y + r * CELL_PX,
198
- GRID_X + c * CELL_PX + CELL_PX - 1, GRID_Y + r * CELL_PX + CELL_PX - 1);
199
- }
200
- }
201
- /* piece */
202
- for (i = 0; i < 3; i++) {
203
- pr = (int8_t)(piece_y + i);
204
- if (pr < 0 || pr >= ROWS) continue;
205
- tgi_setcolor(cell_color(piece[i]));
206
- tgi_bar(GRID_X + piece_x * CELL_PX, GRID_Y + pr * CELL_PX,
207
- GRID_X + piece_x * CELL_PX + CELL_PX - 1, GRID_Y + pr * CELL_PX + CELL_PX - 1);
208
- }
562
+ tgi_bar(0, 0, 159, 101);
563
+ tgi_setcolor(COLOR_PURPLE);
564
+ tgi_bar(0, 0, 159, 2); /* top accent band */
565
+ tgi_bar(0, 99, 159, 101); /* bottom accent band */
566
+
567
+ joy = joy_read(JOY_1);
568
+
569
+ if (state == ST_TITLE) frame_title(joy);
570
+ else if (state == ST_PLAY) frame_play(joy);
571
+ else frame_over(joy);
572
+
209
573
  tgi_updatedisplay();
210
574
  sfx_update();
211
575
 
212
- joy = joy_read(JOY_1);
213
- if (JOY_LEFT(joy) && !(prev & 4) && !collides(piece_x - 1, piece_y)) piece_x--;
214
- if (JOY_RIGHT(joy) && !(prev & 8) && !collides(piece_x + 1, piece_y)) piece_x++;
215
- if (JOY_BTN_1(joy) && !(prev & 0x10)) {
216
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
217
- sfx_tone(1, 40, 2);
218
- }
219
- if (JOY_BTN_2(joy) && !(prev & 0x20)) {
220
- while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
221
- lock_piece();
222
- new_piece();
223
- }
224
- prev = (JOY_LEFT(joy) ? 4 : 0) | (JOY_RIGHT(joy) ? 8 : 0)
225
- | (JOY_BTN_1(joy) ? 0x10 : 0) | (JOY_BTN_2(joy) ? 0x20 : 0);
226
-
227
- fall_rate = JOY_DOWN(joy) ? 4 : 30;
228
- if (++fall_timer >= fall_rate) {
229
- fall_timer = 0;
230
- if (collides(piece_x, (int8_t)(piece_y + 1))) {
231
- lock_piece();
232
- new_piece();
233
- } else {
234
- piece_y++;
235
- }
236
- }
576
+ prev_joy = joy;
237
577
  }
238
578
  }