// recorder -- search for a demo input stream that exercises a level. // // The C64 ships attract recordings for four screens (H, W, T, X). The // other levels have none, so a refactor of their hooks had nothing to be // checked against. This searches for a synthetic ride instead: random // restarts plus hill climbing over (joystick mask, duration) pairs, // scored on how long the cab survives and how much of the level it // reaches -- hook state bytes written, fare-machine stages entered, // distinct pictures drawn, pads landed on. // // The result is a REGRESSION fixture, not a recording: it pins current // behaviour so a refactor that changes nothing observable leaves the // trace identical. It says nothing about matching the real C64. // // Usage: recorder > stream.h // Then assemble the per-level outputs into gateStreams.h. // // Determinism: a candidate is only useful if it replays identically from // a cold start, so every evaluation clears the whole StSimT (see the note // in evaluate) -- stSimNewGame deliberately preserves some cross-screen // state, which would otherwise leak between candidates. #include #include #include #include "stSim.h" #include "stDemoStreams.h" static StLevelT gLevel; static StSimT gSim; void stAudioSfx(const uint8_t *p){(void)p;} void stAudioNoise(bool o){(void)o;} void stAudioThrustSweep(uint8_t v){(void)v;} void stAudioSpeech(uint8_t c){(void)c;} void stAudioSpeechPhrase(const uint8_t *s){(void)s;} void stAudioSilence(void){} void stAudioVoice2(uint8_t a,uint8_t b,uint8_t c){(void)a;(void)b;(void)c;} void stAudioVoice1Freq(uint8_t v){(void)v;} #define PAIRS 160 #define STREAMLEN (PAIRS * 2) #define MAXTICKS 1400 typedef struct { uint32_t score; uint32_t ticks; uint32_t cover; uint32_t lands; uint32_t stages; uint32_t scr; uint32_t near; } ResT; static uint32_t rs = 1; static uint32_t rnd(void){ rs = rs*1664525u + 1013904223u; return rs>>8; } static ResT evaluate(const uint8_t *stream, uint8_t *touchOut) { uint8_t touched[sizeof(StHookScratchT)]; uint8_t base[sizeof(StHookScratchT)]; uint8_t stageSeen[16]; uint32_t t, cover = 0, lands = 0, stages = 0, scrSeen = 0; uint32_t nearest = 255u; uint32_t scrHash[256]; uint8_t lastPad = 0; ResT r; memset(stageSeen, 0, sizeof(stageSeen)); memset(touched, 0, sizeof(touched)); // stSimNewGame deliberately PRESERVES flameParity, waveIdx and // walkParity (the C64 carries them from screen to screen), so a // search that reuses one StSimT would score each candidate against // whatever the previous ride left behind -- and the winning stream // would then not reproduce from a cold start. Clear the whole struct // so every evaluation begins exactly where the gate will. memset(&gSim, 0, sizeof(gSim)); stSimNewGame(&gSim, 1u, true); stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); stSimEnterLevel(&gSim, &gLevel); stSimSetDemoStream(&gSim, stream, STREAMLEN); memcpy(base, &gSim.hookScratch, sizeof(base)); for (t = 0; t < MAXTICKS; t++) { uint16_t k; if (stSimTick(&gSim) != ST_TICK_CONTINUE) { break; } { const uint8_t *cur = (const uint8_t *)&gSim.hookScratch; for (k = 0; k < (uint16_t)sizeof(base); k++) { if (cur[k] != base[k]) { touched[k] = 1; } } } if (gSim.activePad != 0u && lastPad == 0u) { lands++; } lastPad = gSim.activePad; if (gSim.stage < 16u) { stageSeen[gSim.stage] = 1u; } /* Shaping: how close the ride came to an actual touchdown. Some levels (R) only run their hook once a passenger has boarded, which needs a landing, and a landing is far too precise for a blind search to stumble on -- so reward getting near one. */ if (gSim.activePad == 0u && (gSim.spr[0].ptr & 1u) != 0u) { int16_t tx = (int16_t)(((uint16_t)gSim.posXmsb << 8) | gSim.posXcol); uint8_t pi; for (pi = 0u; pi < gLevel.padCount && pi < ST_MAX_PADS; pi++) { int16_t x1 = (int16_t)(((uint16_t)gSim.pads[pi].x1Hi << 8) | gSim.pads[pi].x1Lo); int16_t x2 = (int16_t)(((uint16_t)gSim.pads[pi].x2Hi << 8) | gSim.pads[pi].x2Lo); int16_t dr = (int16_t)((int16_t)gSim.spr[0].row - (int16_t)gSim.pads[pi].row); uint32_t ar = (uint32_t)(dr < 0 ? -dr : dr); uint32_t ax = 0u; uint32_t d; if (tx < x1) { ax = (uint32_t)(x1 - tx); } else if (tx > x2) { ax = (uint32_t)(tx - x2); } /* Both axes matter: the row alone is easy to hit while the cab sits at the far side of the screen. */ d = ar * 4u + ax; if (d < nearest) { nearest = d; } } } /* distinct pictures the hook has produced (screen + colour) */ { uint32_t h = 2166136261u; uint16_t c; uint32_t i2; int dup = 0; for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.screen[c]) * 16777619u; } for (c = 0; c < ST_SCREEN_CELLS; c++) { h = (h ^ gSim.color[c]) * 16777619u; } for (i2 = 0; i2 < scrSeen; i2++) { if (scrHash[i2] == h) { dup = 1; break; } } if (!dup && scrSeen < 256u) { scrHash[scrSeen++] = h; } } } { uint16_t k; for (k = 0; k < (uint16_t)sizeof(touched); k++) { if (touched[k]) { cover++; } } } { int k; for (k = 0; k < 16; k++) { if (stageSeen[k]) { stages++; } } } if (touchOut) { memcpy(touchOut, touched, sizeof(touched)); } r.ticks = t; r.cover = cover; r.lands = lands; r.stages = stages; r.scr = scrSeen; r.near = nearest; r.score = cover * 30000u + stages * 30000u + lands * 4000u + t * 100u + scrSeen * 200u + (255u - nearest) * 60u; return r; } static void randomise(uint8_t *s, int from) { int i; for (i = from; i < PAIRS; i++) { uint32_t d = rnd() % 100u; uint8_t m = 0x80u; if (d < 45u) { m |= 0x01u; } /* thrust up a lot */ else if (d < 60u) { m |= 0x04u; } else if (d < 75u) { m |= 0x08u; } else if (d < 82u) { m |= 0x02u; } else if (d < 88u) { m |= 0x10u; } if (rnd() % 5u == 0u) { m |= (uint8_t)(1u << (rnd() % 4u)); } s[i*2] = m; s[i*2 + 1] = (uint8_t)(1u + rnd() % 14u); } } int main(int argc, char **argv) { uint8_t best[STREAMLEN], cur[STREAMLEN], touched[ST_HOOK_BYTES]; ResT bestR, curR; FILE *fp; int restart, iter; if (argc < 4) { fprintf(stderr, "usage: recorder \n"); return 2; } fp = fopen(argv[1], "rb"); if (!fp || !stLevelParse(&gLevel, fp)) { fprintf(stderr, "cannot load %s\n", argv[1]); return 1; } fclose(fp); rs = (uint32_t)strtoul(argv[3], NULL, 0); randomise(best, 0); bestR = evaluate(best, NULL); for (restart = 0; restart < 120; restart++) { memcpy(cur, best, STREAMLEN); if (restart > 0) { randomise(cur, (int)(rnd() % PAIRS)); } curR = evaluate(cur, NULL); for (iter = 0; iter < 3000; iter++) { uint8_t trial[STREAMLEN]; ResT tr; int j, nmut = 1 + (int)(rnd() % 3u); memcpy(trial, cur, STREAMLEN); for (j = 0; j < nmut; j++) { int i = (int)(rnd() % PAIRS); if (rnd() % 2u) { uint8_t m = 0x80u; uint32_t d = rnd() % 100u; if (d < 45u) { m |= 0x01u; } else if (d < 62u) { m |= 0x04u; } else if (d < 79u) { m |= 0x08u; } else if (d < 88u) { m |= 0x02u; } else { m |= 0x10u; } trial[i*2] = m; } else { trial[i*2 + 1] = (uint8_t)(1u + rnd() % 14u); } } tr = evaluate(trial, NULL); if (tr.score >= curR.score) { memcpy(cur, trial, STREAMLEN); curR = tr; } } if (curR.score > bestR.score) { memcpy(best, cur, STREAMLEN); bestR = curR; } } bestR = evaluate(best, touched); fprintf(stderr, "%s: ticks=%u hookBytes=%u stages=%u pictures=%u lands=%u nearPad=%u\n", argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands, bestR.near); printf("// %s: %u ticks, %u hook bytes, %u fare stages, %u pictures, %u landings\n", argv[2], bestR.ticks, bestR.cover, bestR.stages, bestR.scr, bestR.lands); printf("static const uint8_t kGateStream%s[%d] = {", argv[2], STREAMLEN); { int i; for (i = 0; i < STREAMLEN; i++) { if (i % 12 == 0) { printf("\n "); } printf("0x%02X,%s", best[i], (i % 12 == 11) ? "" : " "); } } printf("\n};\n"); return 0; }