From 40dec7907a01b0856096b3fa0db8f3598a0f15ac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 11:58:02 +0000 Subject: [PATCH] examples: move state-scoped globals to state-local in coin_cavern + platformer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both examples declared their gameplay variables at the top level even though every read and write happened inside one specific state. That pattern hid the overlay feature from new users and kept the state-local code path from being exercised outside the dedicated `state_machine.ne` demo (which is how the "state-locals silently drop their writes" bug survived so long). `coin_cavern.ne`: the five Playing-only physics/position/inventory vars (`player_x`, `player_y`, `player_vy`, `on_ground`, `coins_left`) move onto Playing's state block. `score` stays global because GameOver-era code could reasonably grow to read it. The `on_enter` body loses its redundant resets — the declared initializers on the state-locals re-run on every entry, so retrying after `transition Title` comes back to a fresh state. `platformer.ne`: player physics, camera, liveness, animation phase, and the autopilot budget (`player_y`, `on_ground`, `rise_count`, `fall_vy`, `camera_x`, `anim_tick`, `alive`, `auto_jumps`) all move onto Playing. `frame_tick` and `stomp_count` stay global — Title reads the former to auto-advance, GameOver reads the latter to tally coins on the death screen. The analyzer now overlays Title's `blink`, Playing's eight physics vars, and GameOver's `linger` starting at the same ZP byte (`$1A`), so the three scenes share a 9-byte window instead of each claiming their own slots. Byte-level ROM bytes for both examples shift because variable addresses moved. Video goldens stay pixel-identical (the harness doesn't see Playing in coin_cavern, and the pre-transition Title→Playing timing in platformer is preserved); the platformer audio hash needed one more refresh because the now-slightly-shorter reset prologue shifts APU writes within each frame. https://claude.ai/code/session_015kvJu3iEFLSRJoShPBfm3X --- examples/coin_cavern.ne | 20 +++++---- examples/coin_cavern.nes | Bin 24592 -> 24592 bytes examples/platformer.ne | 43 ++++++++++--------- examples/platformer.nes | Bin 24592 -> 24592 bytes tests/emulator/goldens/platformer.audio.hash | 2 +- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/examples/coin_cavern.ne b/examples/coin_cavern.ne index 4d4a7f6..a5bb108 100644 --- a/examples/coin_cavern.ne +++ b/examples/coin_cavern.ne @@ -23,12 +23,7 @@ const COIN_X: u8 = 180 const COIN_Y: u8 = 100 // Global variables -var player_x: u8 = 40 -var player_y: u8 = 200 -var player_vy: u8 = 0 -var on_ground: u8 = 1 var score: u8 = 0 -var coins_left: u8 = 3 // Helper function: clamp a value to screen bounds fun clamp_x(val: u8) -> u8 { @@ -53,11 +48,20 @@ state Title { // Main gameplay state state Playing { + // Physics and position live with the state: they're only + // meaningful while Playing is active, and the analyzer + // overlays them with the locals of the Title and GameOver + // states so the idle scenes don't reserve bytes they never + // touch. Initializers re-run on every entry, so dying and + // retrying starts the player back on the ground. + var player_x: u8 = 40 + var player_y: u8 = 200 + var player_vy: u8 = 0 + var on_ground: u8 = 1 + var coins_left: u8 = 3 + on enter { - player_x = 40 - player_y = 200 score = 0 - coins_left = 3 } on frame { diff --git a/examples/coin_cavern.nes b/examples/coin_cavern.nes index 4be191b6e65a5efeef17071911aa6fee3e6fa785..d9b031965dcef97986103c4bc6843e65a8decb11 100644 GIT binary patch delta 326 zcmXX>J4*vW5Z;|Vi!^$B4|1hwqZZaS!Pws)PzbTm!vCNs!ZxXLTp?H3h*NBL5WzCH z7QyYV6;h-M+HGeOg0sP4V1{qM$IiInW9nPpEAFymra~oB2~H5O>B zO+IQcu+Bj(^7rEF?0(e{M6C~$)SoIMVXV1Iv`{JS;pGd{>t_bvRP8Sz9ps1HB`C8S zQ1aiZZ?sdAUchxxTJL^tP@en{B_9})NlfTBB7Za(W!JHama(1JVn~3VQ>>4qXtH>4 zp38|_d10ahf~9>U&nb!R)n-|SvlP>f<b}*_KXjXBk08o~WAHk?87W?HX ZbOV3tjPxA z5T^+218ilnXdz5zBjQaI94?%Jd(Jm=ikcTRSA00WPKUc%nim}!rj_A3B8`pE38_Hl zzepJh$NY}t3y}@11CsT5HmNai-osR6O>J0?L0)dcVEoa)QI@te=AYd$^Lu8#?-;z% zS$q^atnpgX27Di-Yah6zRxG+1{?-$y;%m+qqWhT)i~Qa=G#BP-HSN@ diff --git a/examples/platformer.ne b/examples/platformer.ne index 0eaf76c..687cd23 100644 --- a/examples/platformer.ne +++ b/examples/platformer.ne @@ -367,21 +367,17 @@ const AUTOPILOT_JUMPS: u8 = 2 // ── Game state ────────────────────────────────────────────── -// Player physics -var player_y: u8 = 160 -var on_ground: u8 = 1 -var rise_count: u8 = 0 // frames of upward motion remaining -var fall_vy: u8 = 0 // gravity accumulator +// `frame_tick` is shared: Title reads it to auto-advance, Playing +// reads it for animation phasing. `stomp_count` bridges +// Playing → GameOver so the death screen can tally coins. The +// rest — player physics, camera, liveness, autopilot budget — +// are only meaningful while Playing is running, so they live on +// Playing's state block and overlay with the Title / GameOver +// locals (`blink`, `linger`) at the same bytes. -// World/camera -var camera_x: u8 = 0 +// Cross-state scratch var frame_tick: u8 = 0 // free-running frame counter -var anim_tick: u8 = 0 // visual animation phase - -// Gameplay -var alive: u8 = 1 // 0 = dying/dead, 1 = playable var stomp_count: u8 = 0 // successful enemy stomps this life -var auto_jumps: u8 = 0 // proximity pre-jumps used this life // ── Helper functions ──────────────────────────────────────── @@ -520,17 +516,24 @@ state Title { } state Playing { + // Physics, camera, liveness, and autopilot budget — all of + // this is Playing-only. Declaring them inside the state block + // lets the analyzer overlay them with Title.blink and + // GameOver.linger; each variable's initializer re-runs on + // entry, so the retry loop starts each life on the ground + // with a fresh autopilot budget without any manual reset. + var player_y: u8 = GROUND_Y + var on_ground: u8 = 1 + var rise_count: u8 = 0 + var fall_vy: u8 = 0 + var camera_x: u8 = 0 + var anim_tick: u8 = 0 + var alive: u8 = 1 + var auto_jumps: u8 = 0 + on enter { - player_y = GROUND_Y - on_ground = 1 - rise_count = 0 - fall_vy = 0 - camera_x = 0 frame_tick = 0 - anim_tick = 0 - alive = 1 stomp_count = 0 - auto_jumps = 0 start_music Theme } diff --git a/examples/platformer.nes b/examples/platformer.nes index 76cea08bb67c5b6dda96cc4da68ff8d0df0db5e2..c55cdc9c6cf477ba2c643ee7ee37603293e984ef 100644 GIT binary patch delta 1174 zcmZ`&O=uHA6yBN1W?OAlwCX19ZZ=7qQZN1<6a;fnR9w6WQV=2j;ayP={-g?X3`PdT zBGeQd_35QjyeL99No_|wc+i4iZ@Qo;tQUVk&^H@xD=NF}``)~H-+OQ7+v)_V;(Lx< zymHO z(_)llNLmD>8^5!pfZqgNTRvNt301;;1Zj0Gt{lU$)|&IHo54CH5G9P=7v2dbvRMs$JOo zs|JEn)da&Yzi0qyH~iTK_I62Lin2Is4bMI0e`tU=cV_$N&rJ5kBu*1Si^Oim9NFey z9P66@;mGlXN9!_QjxF5b1(_LGrnFvy^8VM4(erc~yJ84cT8kB-Bg zKjZm@@ha~}%C?yDVG}O%en=u^zfMWcQIcNqC6VdsTaYPJir^vT#TqkbrIZ%GFZRWl zAwDI}#W;a@L`XBY-c$N#x;yEe45V8z@s{x;J(Mc*rzU}-OrGk$PRk+|oA_$FDEQUM zi^#8QT@yEwziS1ESBlwWC&qNW_uB6o`m;e@e1-Ug8`oN$y)CI0rsfs+w<1N!T1f`Q zY0GQ7Ii-F?JNz?Z#9BpW!~<*1@|RQ68X(bPY4Rm9$(QC#q4z^mJA^|}V*bwQv}7{x-wCGneb*RvPY4CdYh{d<8gHD;Vul9%sjM}XdaI67#y7B zoiNKg3No&Yz#OCAp{jHgMX(RSruURP-u^1yMK(|4-bFMzhh6SSS6OB1ov8XQ`j4Xj z#p(jiZUM;ccIk)FQb4jv-l+8E{~dUM&xP6N%~otRojsK3w>EDDJ1Yi zyfCx7lByeDEUjbLDa+M$JIPR2&@r#0X+g>V7?aM`N>xnyDZG9*pscHkfCQ$WKxqc% z(}bUZkxlOsycZ_5$iX@-`rxV-{W7CWBsE>1n6-v0r2V`ofOX+3~0hjq8M9_Os1Xf&pu)4eZ7kEpi2-dt+EVHvwrt~V0h^-;k+guVOA+``d z6eT;mHmyiNz1jr3RF#R6&A`Azrg8JA_i6%6ocbI65$P$1yCYuPDUubl_Bo8NbB&3y z$O>^I(m9v!CFj0VZTD{5Y9B6Hd6*G5qpOyEYNC|{i=@pL g%LrfMof>}`eQS|D<4