joeylib2/tools/stsim/recorder.c

195 lines
8.7 KiB
C

// 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 <level.dat> <letter> <seed> > 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <level.dat> <name> <seed>\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;
}