More debugging and tests.

This commit is contained in:
Scott Duensing 2026-10-13 13:55:58 -05:00
parent cfda4c52db
commit 45a82bef2f
9 changed files with 891 additions and 28 deletions

View file

@ -4,7 +4,7 @@
# interpreter to them.
#
# make every target whose toolchain is installed
# make <port> one target: iigs amiga atarist dos x68000 dvx
# make <port> one target: iigs amiga atarist dos x68000 dvx sdl3
# make iigs-game-disk-agi the bootable IIgs volume with a game on it
# (games/kq3; AGI_GAME=NAME for games/NAME)
# make check build, then run the gates (scripts/verify.sh)

4
agi.c
View file

@ -69,13 +69,15 @@ static uint32_t gClockLastMs;
// ----- Internal helpers (alphabetical) -----
// new.room's work (specification 3.1 and 4.2): everything off the
// new.room's work (specification 3.1 and 4.2): the sound stopped (its
// flag set at once, as Sierra's interpreter does), everything off the
// screen, the logics forgotten, the player in control, ego moved to the
// edge it left by, and the new room's logic loaded.
static void changeRoom(void) {
AgiObjectT *ego;
gAgi->newRoomPending = false;
soundStop();
objUnanimateAll();
vmDiscardLogics();
agiViewDiscardUnused();

View file

@ -631,7 +631,12 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
d = DISTANCE_FAR;
if (o != NULL && p != NULL && (o->flags & OBJ_DRAWN) != 0u && (p->flags & OBJ_DRAWN) != 0u) {
d = (uint16_t)(((o->x > p->x) ? o->x - p->x : p->x - o->x) + ((o->y > p->y) ? o->y - p->y : p->y - o->y));
// From the middle of one baseline to the middle of the
// other, as Sierra's interpreter measures it.
int16_t ox = (int16_t)(o->x + o->width / 2u);
int16_t px = (int16_t)(p->x + p->width / 2u);
d = (uint16_t)(((ox > px) ? ox - px : px - ox) + ((o->y > p->y) ? o->y - p->y : p->y - o->y));
if (d > DISTANCE_MAX) {
d = DISTANCE_MAX;
}

View file

@ -0,0 +1 @@
a 4f6182a8

View file

@ -0,0 +1 @@
a 299bf80c

View file

@ -0,0 +1 @@
a eecb58af

View file

@ -0,0 +1,5 @@
a 4f293179
b 4f293179
c 2b4619ed
d 2b4619ed
e 7662eaad

View file

@ -12,12 +12,39 @@
// key NAME [HOLD] press key NAME (xdotool names) for HOLD seconds
// down NAME / up NAME hold / release a key
// type TEXT press each character's key in turn, typing it
// say TEXT type TEXT, press Return, give the game a second
// to answer and clear its message windows
// shot NAME write the displayed frame to OUTDIR/NAME.ppm
// clear press Return until no message window is open
// record NAME note every frame from here ...
// stop ... to here: OUTDIR/NAME.frames lists each
// frame's hash and OUTDIR/NAME-HASH.ppm holds each
// different frame once
// until [SECONDS] COND wait until COND holds (at most SECONDS, 120 by
// default; longer fails the run)
// await [SECONDS] COND as until, but running out of time is no failure:
// a goto after it can test what happened
// check COND fail the run unless COND holds now
// walk X Y steer ego with the arrow keys to (X, Y) along
// the straight line from where it is, zig-zagging
// on a diagonal; '-' leaves an axis alone. Over
// when ego gets there or the room changes; an ego
// that stops moving on the way fails the run
// walk oN steer ego to within 8 of object N (as the
// distance command measures), following it
// state NAME print the game's progress (room, score, items
// carried, flags) and hash it like a shot
// show THING... print variables (vN), flags (fN), items' rooms
// (iN), objects (oN) and ego's place (x, y), for
// following a run
// label NAME a place goto can go back (or on) to
// goto NAME [if COND] go on from 'label NAME' (when COND holds)
// A COND is terms joined by 'and': vN, iN (item N's room, 255 carried),
// x or y (ego's position) compared with == != < > <= >= to a number, or
// fN / !fN, or window / !window (a message window is open). While an until waits, a message window left open for 25
// seconds (longer than any that closes by itself) is dismissed with
// Return, and while a walk walks one left open for a second: the game's
// chatter at random moments must not stall them.
// '#' starts a comment. When the script ends the run stops.
//
// Every shot prints its frame's FNV-1a hash (the gate compares those) and,
@ -48,6 +75,7 @@
#define HOST_PIXELS_PER_BYTE 2u
#define HOST_CLEAR_GAP_FRAMES 36u // 0.6 s, dosDrive.py's pause between Returns
#define HOST_CLEAR_TRIES 8u // dosDrive.py's limit on Returns per clear
#define HOST_SAY_SETTLE_FRAMES 60u // 1 s, dosDrive.py's wait for an answer to a 'say'
#define HOST_NO_FILES "-"
#define FNV_OFFSET_BASIS 2166136261u
#define FNV_PRIME 16777619u
@ -55,6 +83,29 @@
#define BYTE_BITS 8u
#define BYTE_MASK 0xFFu
#define RECORD_DISTINCT_MAX 64u
#define HOST_EXIT_FAILED 3
#define HOST_COND_TERMS_MAX 8u
#define HOST_UNTIL_SECONDS 120.0 // an until's limit unless it names one
#define HOST_WALK_ANY INT16_MIN // a walk axis left alone ('-')
#define HOST_WALK_STUCK_FRAMES 600u // 10 s without getting nearer: ego is blocked
#define HOST_WALK_SIDESTEP_FRAMES 30u // a first step aside round something in the way
#define HOST_WALK_BUMP_FRAMES 24u // a pressed way that gets nowhere this long is blocked
// A walk's presses are short, so it looks again before ego (a step a
// cycle, 6 frames at normal speed) can pass the place it is going to.
#define HOST_WALK_HOLD_FRAMES 2u
#define HOST_WALK_GAP_FRAMES 1u
#define HOST_WALK_SETTLE_FRAMES 15u // a press has surely been seen by then
#define HOST_WALK_MAX_FRAMES 7200u // 2 minutes for one walk at most
// A window left open this long while an until waits (longer than any
// window that closes by itself) or a walk walks is dismissed.
#define HOST_UNTIL_PATIENCE_FRAMES 1500u
#define HOST_WALK_PATIENCE_FRAMES 60u
#define HOST_FLAG_BITS 8u
#define HOST_GOTO_MAX 1000u // more jumps than this is a script stuck in a loop
#define HOST_WALK_NO_OBJECT 0xFFu // a walk to a place, not to an object
#define HOST_WALK_NEAR 8 // how near (as distance measures) a walk to an object goes
#define HOST_WALK_BAND 1 // pixels a walk may stray from its line
#define HOST_FLAG_TOP_BIT 0x80u // flag 0 of a byte
typedef struct {
@ -63,15 +114,69 @@ typedef struct {
uint8_t ch;
} HostKeyNameT;
// What a condition term reads.
typedef enum {
HOST_TERM_VAR = 0,
HOST_TERM_FLAG = 1,
HOST_TERM_ITEM = 2,
HOST_TERM_EGO_X = 3,
HOST_TERM_EGO_Y = 4,
HOST_TERM_WINDOW = 5
} HostTermKindE;
typedef enum {
HOST_OP_EQ = 0,
HOST_OP_NE = 1,
HOST_OP_LT = 2,
HOST_OP_GT = 3,
HOST_OP_LE = 4,
HOST_OP_GE = 5
} HostOpE;
// One term of a condition: kind[index] op value.
typedef struct {
HostTermKindE kind;
uint8_t index;
HostOpE op;
int16_t value;
} HostTermT;
// Where a 'say' is: typing, Return pressed, the answer cleared away.
typedef enum {
HOST_SAY_NONE = 0,
HOST_SAY_TYPING = 1,
HOST_SAY_SETTLE = 2,
HOST_SAY_CLEAR = 3
} HostSayE;
// What the script is waiting on besides time.
typedef enum {
HOST_WAIT_NONE = 0,
HOST_WAIT_UNTIL = 1,
HOST_WAIT_WALK = 2
} HostWaitE;
// ----- Prototypes -----
static bool hostClearStep(void);
static bool hostCondHolds(void);
static bool hostCondParse(char *text);
static void hostDump(const char *name);
static void hostGoto(const char *label);
static jlKeyE hostEgoKey(uint8_t dir);
static void hostFail(const char *why);
static uint32_t hostFrameHash(void);
static uint32_t hostHashBytes(uint32_t hash, const uint8_t *p, size_t n);
static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh);
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames);
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames, uint32_t gapFrames);
static void hostRecordFrame(void);
static void hostShow(char *things);
static void hostState(const char *name);
static void hostStep(void);
static bool hostTermHolds(const HostTermT *t);
static bool hostWaitStep(void);
static bool hostWalkStep(void);
static void hostWritePpm(const char *path);
@ -108,6 +213,9 @@ static const HostKeyNameT kKeyNames[] = {
{ "exclam", KEY_NONE, '!' }
};
// The comparison operators of a condition, in HostOpE order.
static const char *const kOps[] = { "==", "!=", "<", ">", "<=", ">=" };
static FILE *gScript;
static char gOutDir[HOST_PATH_MAX];
static uint32_t gFrame;
@ -119,6 +227,7 @@ static uint8_t gDisplay[SURFACE_HEIGHT][SURFACE_WIDTH][3];
static char gTypeBuf[HOST_LINE_MAX];
static long gLinePos;
static uint16_t gTypePos;
static HostSayE gSay;
static uint8_t gClearTries;
// A recording: its name, its frame list, the hash of its frame sequence
// and the frames written so far.
@ -129,10 +238,142 @@ static uint32_t gRecordHash;
static uint32_t gRecordFrames;
static uint32_t gRecordSeen[RECORD_DISTINCT_MAX];
static uint8_t gRecordSeenCount;
// The script line being run, for failure messages.
static char gOpLine[HOST_LINE_MAX];
// An until's condition or a walk's target, and how long it may take.
static HostWaitE gWait;
static HostTermT gTerms[HOST_COND_TERMS_MAX];
static uint8_t gTermCount;
static uint32_t gWaitDeadline;
// An until fails the run when its time runs out; an await does not.
static bool gWaitMayFail;
static int16_t gWalkX;
static int16_t gWalkY;
static int16_t gWalkFromX;
static int16_t gWalkFromY;
static uint8_t gWalkObject;
static uint8_t gWalkRoom;
static int16_t gWalkLastX;
static int16_t gWalkLastY;
static int16_t gWalkBest;
static uint32_t gWalkMovedFrame;
static uint32_t gWalkTriedFrame;
static bool gWalkAxisY;
// A step aside round something in the way: which way, until when, and
// how many there have been since ego last got nearer.
static uint8_t gWalkSideDir;
static uint32_t gWalkSideUntil;
static uint8_t gWalkSideTries;
// The direction ego should take up after the walk's last press, and when.
static uint8_t gWalkExpectDir;
static uint32_t gWalkPressFrame;
// A message window seen open during a wait, and since when.
static bool gWindowSeen;
static uint32_t gWindowSince;
// AGI_HOST_TRACE in the environment: log every key a walk presses.
static bool gTrace;
static uint16_t gGotoCount;
// ----- Internal helpers (alphabetical) -----
// Dismiss message windows: Return while one is open, a few times at most
// (a window that wants another key stays). True when none is left.
static bool hostClearStep(void) {
if (gAgi != NULL && gAgi->windowOpen && gClearTries < HOST_CLEAR_TRIES) {
gClearTries++;
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
gResumeFrame = gFrame + HOST_CLEAR_GAP_FRAMES;
return false;
}
gClearTries = 0u;
return true;
}
// Does every term of the condition (gTerms) hold?
static bool hostCondHolds(void) {
uint8_t i;
for (i = 0u; i < gTermCount; i++) {
if (!hostTermHolds(&gTerms[i])) {
return false;
}
}
return true;
}
// Parse a condition, terms joined by 'and', into gTerms. False when it
// does not read.
static bool hostCondParse(char *text) {
char *tok;
char *opTok;
char *valueTok;
HostTermT *t;
uint8_t op;
bool negated;
gTermCount = 0u;
tok = strtok(text, " \t");
while (tok != NULL) {
if (gTermCount >= HOST_COND_TERMS_MAX) {
return false;
}
t = &gTerms[gTermCount++];
negated = (tok[0] == '!');
if (strcmp(tok + (negated ? 1 : 0), "window") == 0) {
t->kind = HOST_TERM_WINDOW;
t->index = 0u;
t->op = negated ? HOST_OP_EQ : HOST_OP_NE;
t->value = 0;
} else if (tok[negated ? 1 : 0] == 'f') {
t->kind = HOST_TERM_FLAG;
t->index = (uint8_t)atoi(tok + (negated ? 2 : 1));
t->op = negated ? HOST_OP_EQ : HOST_OP_NE;
t->value = 0;
} else {
if (tok[0] == 'v') {
t->kind = HOST_TERM_VAR;
} else if (tok[0] == 'i') {
t->kind = HOST_TERM_ITEM;
} else if (strcmp(tok, "x") == 0) {
t->kind = HOST_TERM_EGO_X;
} else if (strcmp(tok, "y") == 0) {
t->kind = HOST_TERM_EGO_Y;
} else {
return false;
}
t->index = (uint8_t)atoi(tok + 1);
opTok = strtok(NULL, " \t");
valueTok = strtok(NULL, " \t");
if (opTok == NULL || valueTok == NULL) {
return false;
}
for (op = 0u; op < sizeof(kOps) / sizeof(kOps[0]) && strcmp(kOps[op], opTok) != 0; op++) {
}
if (op == sizeof(kOps) / sizeof(kOps[0])) {
return false;
}
t->op = (HostOpE)op;
t->value = (int16_t)atoi(valueTok);
}
tok = strtok(NULL, " \t");
if (tok == NULL) {
break;
}
if (strcmp(tok, "and") != 0) {
return false;
}
tok = strtok(NULL, " \t");
if (tok == NULL) {
return false;
}
}
return gTermCount > 0u;
}
static void hostDump(const char *name) {
char path[HOST_FILE_MAX];
@ -145,14 +386,66 @@ static void hostDump(const char *name) {
}
static uint32_t hostFrameHash(void) {
const uint8_t *p;
uint32_t hash;
size_t i;
// Go on from the line after 'label NAME'.
static void hostGoto(const char *label) {
char line[HOST_LINE_MAX];
char *p;
hash = FNV_OFFSET_BASIS;
p = &gDisplay[0][0][0];
for (i = 0; i < sizeof(gDisplay); i++) {
if (++gGotoCount > HOST_GOTO_MAX) {
hostFail("too many jumps: the script is going round in a loop");
}
rewind(gScript);
while (fgets(line, sizeof(line), gScript) != NULL) {
line[strcspn(line, "#\r\n")] = '\0';
p = line + strspn(line, " \t");
if (strncmp(p, "label", 5) != 0 || (p[5] != ' ' && p[5] != '\t')) {
continue;
}
p += 5;
p += strspn(p, " \t");
p[strcspn(p, " \t")] = '\0';
if (strcmp(p, label) == 0) {
return;
}
}
hostFail("no such label");
}
// The arrow key that turns ego to dir (north for a diagonal, which the
// arrows cannot name: pressing it again then stops ego).
static jlKeyE hostEgoKey(uint8_t dir) {
switch (dir) {
case AGI_DIR_E:
return KEY_RIGHT;
case AGI_DIR_S:
return KEY_DOWN;
case AGI_DIR_W:
return KEY_LEFT;
default:
return KEY_UP;
}
}
// The script cannot go on: say why and end the run without 'script done'.
static void hostFail(const char *why) {
printf("[%6.1f] FAIL '%s': %s (room %u, ego at %d,%d)\n", (double)gFrame / (double)HOST_FRAME_HZ, gOpLine, why,
(unsigned)gAgi->vars[VAR_ROOM], (int)gAgi->objects[AGI_EGO].x, (int)gAgi->objects[AGI_EGO].y);
exit(HOST_EXIT_FAILED);
}
static uint32_t hostFrameHash(void) {
return hostHashBytes(FNV_OFFSET_BASIS, &gDisplay[0][0][0], sizeof(gDisplay));
}
// FNV-1a: hash carried on over n more bytes.
static uint32_t hostHashBytes(uint32_t hash, const uint8_t *p, size_t n) {
size_t i;
for (i = 0; i < n; i++) {
hash = (hash ^ p[i]) * FNV_PRIME;
}
return hash;
@ -197,7 +490,7 @@ static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh) {
}
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames) {
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames, uint32_t gapFrames) {
if (key != KEY_NONE) {
gHoldUntil[key] = gFrame + holdFrames;
}
@ -205,7 +498,7 @@ static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames) {
if (ch != 0u && !gHeld[KEY_LALT] && !gHeld[KEY_LCTRL]) {
jlInputCharPush(ch);
}
gResumeFrame = gFrame + holdFrames + HOST_KEY_GAP_FRAMES;
gResumeFrame = gFrame + holdFrames + gapFrames;
}
@ -213,13 +506,15 @@ static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames) {
// and written out the first time it is seen.
static void hostRecordFrame(void) {
char path[HOST_FILE_MAX];
uint8_t bytes[HASH_BYTES];
uint32_t hash;
uint8_t i;
hash = hostFrameHash();
for (i = 0u; i < HASH_BYTES; i++) {
gRecordHash = (gRecordHash ^ ((hash >> (i * BYTE_BITS)) & BYTE_MASK)) * FNV_PRIME;
bytes[i] = (uint8_t)((hash >> (i * BYTE_BITS)) & BYTE_MASK);
}
gRecordHash = hostHashBytes(gRecordHash, bytes, HASH_BYTES);
gRecordFrames++;
if (gRecordLog == NULL) {
return;
@ -239,6 +534,83 @@ static void hostRecordFrame(void) {
}
// Print what the script names: vN, fN, iN, oN, x, y.
static void hostShow(char *things) {
char *tok;
printf("[%6.1f] show", (double)gFrame / (double)HOST_FRAME_HZ);
for (tok = strtok(things, " \t"); tok != NULL; tok = strtok(NULL, " \t")) {
switch (tok[0]) {
case 'v':
printf(" %s=%u", tok, (unsigned)gAgi->vars[(uint8_t)atoi(tok + 1)]);
break;
case 'f':
printf(" %s=%u", tok, (unsigned)gAgi->flags[(uint8_t)atoi(tok + 1)]);
break;
case 'i':
printf(" %s=%u", tok, (unsigned)gAgi->itemRoom[(uint8_t)atoi(tok + 1)]);
break;
case 'o': {
const AgiObjectT *o = &gAgi->objects[(uint8_t)atoi(tok + 1) % AGI_MAX_OBJECTS];
printf(" %s=%d,%d dir %u motion %u flags %04x", tok, (int)o->x, (int)o->y, (unsigned)o->direction, (unsigned)o->motion,
(unsigned)o->flags);
break;
}
case 'x':
printf(" x=%d", (int)gAgi->objects[AGI_EGO].x);
break;
case 'y':
printf(" y=%d", (int)gAgi->objects[AGI_EGO].y);
break;
default:
printf(" %s=?", tok);
break;
}
}
printf("\n");
}
// The game's progress: the room, the score, the items carried, every
// item's room and the flags (a bit each, flag 0 the top bit of the first
// byte), printed and hashed like a shot; where ego is, printed only.
static void hostState(const char *name) {
uint8_t bits[AGI_NUM_FLAGS / HOST_FLAG_BITS];
uint32_t hash;
uint16_t i;
double now;
memset(bits, 0, sizeof(bits));
for (i = 0u; i < AGI_NUM_FLAGS; i++) {
if (gAgi->flags[i] != 0u) {
bits[i / HOST_FLAG_BITS] = (uint8_t)(bits[i / HOST_FLAG_BITS] | (HOST_FLAG_TOP_BIT >> (i % HOST_FLAG_BITS)));
}
}
hash = hostHashBytes(FNV_OFFSET_BASIS, &gAgi->vars[VAR_ROOM], 1u);
hash = hostHashBytes(hash, &gAgi->vars[VAR_SCORE], 1u);
hash = hostHashBytes(hash, gAgi->itemRoom, gAgi->game.itemCount);
hash = hostHashBytes(hash, bits, sizeof(bits));
now = (double)gFrame / (double)HOST_FRAME_HZ;
printf("[%6.1f] state %s room=%u score=%u ego=%d,%d carried=", now, name, (unsigned)gAgi->vars[VAR_ROOM], (unsigned)gAgi->vars[VAR_SCORE],
(int)gAgi->objects[AGI_EGO].x, (int)gAgi->objects[AGI_EGO].y);
for (i = 0u; i < gAgi->game.itemCount; i++) {
if (gAgi->itemRoom[i] == AGI_ITEM_CARRIED) {
printf("%u,", (unsigned)i);
}
}
printf(" items=");
for (i = 0u; i < gAgi->game.itemCount; i++) {
printf("%02x", (unsigned)gAgi->itemRoom[i]);
}
printf(" flags=");
for (i = 0u; i < sizeof(bits); i++) {
printf("%02x", (unsigned)bits[i]);
}
printf("\n[%6.1f] shot %s %08x\n", now, name, (unsigned)hash);
}
// Run script lines until one asks for time to pass.
static void hostStep(void) {
char line[HOST_LINE_MAX];
@ -249,6 +621,13 @@ static void hostStep(void) {
double secs;
while (gScript != NULL && gFrame >= gResumeFrame) {
if (gWait != HOST_WAIT_NONE) {
if (gAgi == NULL || !hostWaitStep()) {
return;
}
gWait = HOST_WAIT_NONE;
continue;
}
if (gTypeBuf[gTypePos] != '\0') {
char one[2];
@ -256,9 +635,25 @@ static void hostStep(void) {
one[1] = '\0';
gTypePos++;
if (one[0] == ' ') {
hostPress(KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES);
hostPress(KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
} else if (hostKeyLookup(one, &key, &ch)) {
hostPress(key, ch, HOST_KEY_HOLD_FRAMES);
hostPress(key, ch, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
}
continue;
}
if (gSay == HOST_SAY_TYPING) {
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
gSay = HOST_SAY_SETTLE;
continue;
}
if (gSay == HOST_SAY_SETTLE) {
gResumeFrame = gFrame + HOST_SAY_SETTLE_FRAMES;
gSay = HOST_SAY_CLEAR;
continue;
}
if (gSay == HOST_SAY_CLEAR) {
if (hostClearStep()) {
gSay = HOST_SAY_NONE;
}
continue;
}
@ -268,12 +663,15 @@ static void hostStep(void) {
exit(0);
}
line[strcspn(line, "\r\n")] = '\0';
strcpy(gOpLine, line);
op = line;
while (*op == ' ' || *op == '\t') {
op++;
}
if (strncmp(op, "type ", 5) == 0) {
strncpy(gTypeBuf, op + 5, sizeof(gTypeBuf) - 1u);
if (strncmp(op, "type ", 5) == 0 || strncmp(op, "say ", 4) == 0) {
printf("[%6.1f] > %s\n", (double)gFrame / (double)HOST_FRAME_HZ, op);
gSay = (op[0] == 's') ? HOST_SAY_TYPING : HOST_SAY_NONE;
strncpy(gTypeBuf, op + ((op[0] == 's') ? 4 : 5), sizeof(gTypeBuf) - 1u);
gTypeBuf[sizeof(gTypeBuf) - 1u] = '\0';
gTypePos = 0u;
continue;
@ -290,6 +688,7 @@ static void hostStep(void) {
if (*op == '\0') {
continue;
}
printf("[%6.1f] > %s\n", (double)gFrame / (double)HOST_FRAME_HZ, gOpLine + strspn(gOpLine, " \t"));
if (strcmp(op, "wait") == 0) {
secs = atof(arg);
gResumeFrame = gFrame + (uint32_t)(secs * HOST_FRAME_HZ + 0.5);
@ -307,7 +706,7 @@ static void hostStep(void) {
fprintf(stderr, "agiHost: unknown key '%s'\n", arg);
exit(2);
}
hostPress(key, ch, (hold > 0.0) ? (uint32_t)(hold * HOST_FRAME_HZ + 0.5) : HOST_KEY_HOLD_FRAMES);
hostPress(key, ch, (hold > 0.0) ? (uint32_t)(hold * HOST_FRAME_HZ + 0.5) : HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
} else if (strcmp(op, "down") == 0 || strcmp(op, "up") == 0) {
if (!hostKeyLookup(arg, &key, &ch) || key == KEY_NONE) {
fprintf(stderr, "agiHost: unknown key '%s'\n", arg);
@ -338,16 +737,89 @@ static void hostStep(void) {
gRecordLog = NULL;
}
printf("[%6.1f] shot %s %08x\n", (double)gFrame / (double)HOST_FRAME_HZ, gRecordName, (unsigned)gRecordHash);
} else if (strcmp(op, "clear") == 0) {
// Dismiss message windows: Return while one is open, a few
// times at most (a window that wants another key stays).
if (gAgi != NULL && gAgi->windowOpen && gClearTries < HOST_CLEAR_TRIES) {
gClearTries++;
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES);
gResumeFrame = gFrame + HOST_CLEAR_GAP_FRAMES;
fseek(gScript, gLinePos, SEEK_SET);
} else if (strcmp(op, "until") == 0 || strcmp(op, "await") == 0 || strcmp(op, "check") == 0) {
char *cond;
double limit;
cond = arg;
limit = HOST_UNTIL_SECONDS;
if (op[0] != 'c' && *arg >= '0' && *arg <= '9') {
limit = strtod(arg, &cond);
}
if (!hostCondParse(cond)) {
fprintf(stderr, "agiHost: bad condition in '%s'\n", gOpLine);
exit(2);
}
if (op[0] == 'c') {
if (!hostCondHolds()) {
hostFail("it does not hold");
}
} else {
gClearTries = 0u;
gWait = HOST_WAIT_UNTIL;
gWaitDeadline = gFrame + (uint32_t)(limit * HOST_FRAME_HZ + 0.5);
gWaitMayFail = (op[0] == 'u');
gWindowSeen = false;
}
} else if (strcmp(op, "walk") == 0) {
char *xTok;
char *yTok;
xTok = strtok(arg, " \t");
yTok = strtok(NULL, " \t");
gWalkObject = HOST_WALK_NO_OBJECT;
if (xTok != NULL && xTok[0] == 'o') {
gWalkObject = (uint8_t)atoi(xTok + 1);
} else if (xTok == NULL || yTok == NULL) {
fprintf(stderr, "agiHost: walk wants X Y or oN in '%s'\n", gOpLine);
exit(2);
} else {
gWalkX = (strcmp(xTok, "-") == 0) ? HOST_WALK_ANY : (int16_t)atoi(xTok);
gWalkY = (strcmp(yTok, "-") == 0) ? HOST_WALK_ANY : (int16_t)atoi(yTok);
}
gWalkFromX = gAgi->objects[AGI_EGO].x;
gWalkFromY = gAgi->objects[AGI_EGO].y;
gWalkRoom = gAgi->vars[VAR_ROOM];
gWalkLastX = gAgi->objects[AGI_EGO].x;
gWalkLastY = gAgi->objects[AGI_EGO].y;
gWalkMovedFrame = gFrame;
gWalkBest = INT16_MAX;
gWalkTriedFrame = gFrame;
gWalkExpectDir = gAgi->vars[VAR_EGO_DIR];
gWalkSideUntil = gFrame;
gWalkSideTries = 0u;
gWalkPressFrame = gFrame;
gWaitDeadline = gFrame + HOST_WALK_MAX_FRAMES;
gWindowSeen = false;
gWait = HOST_WAIT_WALK;
} else if (strcmp(op, "state") == 0) {
hostState(arg);
} else if (strcmp(op, "show") == 0) {
hostShow(arg);
} else if (strcmp(op, "label") == 0) {
// Only goto reads it.
} else if (strcmp(op, "goto") == 0) {
char *name;
char *cond;
name = strtok(arg, " \t");
cond = strtok(NULL, "");
if (name == NULL) {
fprintf(stderr, "agiHost: goto wants a label in '%s'\n", gOpLine);
exit(2);
}
if (cond != NULL) {
cond += strspn(cond, " \t");
if (strncmp(cond, "if", 2) != 0 || !hostCondParse(cond + 2)) {
fprintf(stderr, "agiHost: bad condition in '%s'\n", gOpLine);
exit(2);
}
}
if (cond == NULL || hostCondHolds()) {
hostGoto(name);
}
} else if (strcmp(op, "clear") == 0) {
if (!hostClearStep()) {
fseek(gScript, gLinePos, SEEK_SET);
}
} else {
fprintf(stderr, "agiHost: bad script op '%s'\n", op);
@ -357,6 +829,215 @@ static void hostStep(void) {
}
static bool hostTermHolds(const HostTermT *t) {
int16_t have;
switch (t->kind) {
case HOST_TERM_VAR:
have = gAgi->vars[t->index];
break;
case HOST_TERM_FLAG:
have = gAgi->flags[t->index];
break;
case HOST_TERM_ITEM:
have = gAgi->itemRoom[t->index];
break;
case HOST_TERM_EGO_X:
have = gAgi->objects[AGI_EGO].x;
break;
case HOST_TERM_EGO_Y:
have = gAgi->objects[AGI_EGO].y;
break;
default:
have = gAgi->windowOpen ? 1 : 0;
break;
}
switch (t->op) {
case HOST_OP_EQ:
return have == t->value;
case HOST_OP_NE:
return have != t->value;
case HOST_OP_LT:
return have < t->value;
case HOST_OP_GT:
return have > t->value;
case HOST_OP_LE:
return have <= t->value;
default:
return have >= t->value;
}
}
// One frame of an until or a walk: true when it is over. A message
// window left open meanwhile is dismissed; the time limit runs on.
static bool hostWaitStep(void) {
if (gWait == HOST_WAIT_UNTIL && hostCondHolds()) {
return true;
}
if (gFrame >= gWaitDeadline) {
if (gWait == HOST_WAIT_UNTIL && !gWaitMayFail) {
return true;
}
hostFail((gWait == HOST_WAIT_UNTIL) ? "it never held" : "ego never got there");
}
if (gAgi->windowOpen) {
gWalkMovedFrame = gFrame;
if (!gWindowSeen) {
gWindowSeen = true;
gWindowSince = gFrame;
} else if (gFrame - gWindowSince >= ((gWait == HOST_WAIT_UNTIL) ? HOST_UNTIL_PATIENCE_FRAMES : HOST_WALK_PATIENCE_FRAMES)) {
char path[HOST_FILE_MAX];
printf("[%6.1f] dismissed a window (dismissed-%u.ppm)\n", (double)gFrame / (double)HOST_FRAME_HZ, (unsigned)gFrame);
if (strcmp(gOutDir, HOST_NO_FILES) != 0) {
snprintf(path, sizeof(path), "%s/dismissed-%u.ppm", gOutDir, (unsigned)gFrame);
hostWritePpm(path);
}
gWindowSeen = false;
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
}
return false;
}
gWindowSeen = false;
return (gWait == HOST_WAIT_WALK) && hostWalkStep();
}
// Steer ego one frame towards the walk's target with the arrow keys,
// pressing ego's own arrow again to stop it there. A walk to a place
// keeps to the straight line from where it started, within
// HOST_WALK_BAND pixels: it stays on one axis until it strays that far,
// then takes the other (stairs on a diagonal); an axis that has bumped
// into something gives way to the other while there is any of that way
// left to go (ego feels its way along a narrow diagonal), and with one
// way left and that blocked, ego steps aside and tries again. A walk to
// an object heads for it until it is near. Over when ego is there and
// stopped, or the room changed.
static bool hostWalkStep(void) {
const AgiObjectT *ego;
int16_t slack;
int16_t dx;
int16_t dy;
int16_t left;
int32_t err;
int32_t runX;
int32_t runY;
uint8_t wantX;
uint8_t wantY;
uint8_t want;
uint8_t dir;
bool bumped;
if (gAgi->vars[VAR_ROOM] != gWalkRoom) {
return true;
}
ego = &gAgi->objects[AGI_EGO];
if (gWalkObject != HOST_WALK_NO_OBJECT) {
const AgiObjectT *o = &gAgi->objects[gWalkObject];
// Middle to middle, as the distance command measures: near
// enough is over.
gWalkX = (int16_t)(o->x + o->width / 2u - ego->width / 2u);
gWalkY = o->y;
if (abs(gWalkX - ego->x) + abs(gWalkY - ego->y) <= HOST_WALK_NEAR) {
gWalkX = ego->x;
gWalkY = ego->y;
}
}
// Progress is getting nearer (a logic that pushes ego back, or an
// overshoot, is no progress); following an object, any step is.
left = (int16_t)(((gWalkX == HOST_WALK_ANY) ? 0 : abs(gWalkX - ego->x)) + ((gWalkY == HOST_WALK_ANY) ? 0 : abs(gWalkY - ego->y)));
if (left < gWalkBest || (gWalkObject != HOST_WALK_NO_OBJECT && (ego->x != gWalkLastX || ego->y != gWalkLastY))) {
gWalkBest = left;
gWalkMovedFrame = gFrame;
gWalkSideTries = 0u;
}
gWalkLastX = ego->x;
gWalkLastY = ego->y;
// A step size of n can always land within n / 2.
slack = (int16_t)(ego->stepSize / 2u);
dx = (gWalkX == HOST_WALK_ANY || abs(gWalkX - ego->x) <= slack) ? 0 : (int16_t)(gWalkX - ego->x);
dy = (gWalkY == HOST_WALK_ANY || abs(gWalkY - ego->y) <= slack) ? 0 : (int16_t)(gWalkY - ego->y);
wantX = (dx > 0) ? AGI_DIR_E : AGI_DIR_W;
wantY = (dy > 0) ? AGI_DIR_S : AGI_DIR_N;
dir = gAgi->vars[VAR_EGO_DIR];
// A press takes effect at the next cycle: until ego's direction shows
// it (or a cycle or two passed), another press would undo it.
if (dir != gWalkExpectDir && gFrame - gWalkPressFrame < HOST_WALK_SETTLE_FRAMES) {
return false;
}
bumped = gFrame - gWalkMovedFrame >= HOST_WALK_BUMP_FRAMES && gFrame - gWalkTriedFrame >= HOST_WALK_BUMP_FRAMES;
if (dx == 0 && dy == 0) {
if (dir == AGI_DIR_NONE) {
return true;
}
gWalkExpectDir = AGI_DIR_NONE;
gWalkPressFrame = gFrame;
hostPress(hostEgoKey(dir), 0u, HOST_WALK_HOLD_FRAMES, HOST_WALK_GAP_FRAMES);
return false;
}
if (gFrame - gWalkMovedFrame >= HOST_WALK_STUCK_FRAMES) {
hostFail("ego is blocked");
}
if (bumped && (dx == 0 || dy == 0) && gFrame >= gWalkSideUntil) {
// Something stands in the only way left to go: step aside, one
// side then the other, further each time round, and try again.
gWalkSideTries++;
gWalkSideUntil = gFrame + HOST_WALK_SIDESTEP_FRAMES * ((gWalkSideTries + 1u) / 2u);
if (dx == 0) {
gWalkSideDir = (gWalkSideTries & 1u) ? AGI_DIR_E : AGI_DIR_W;
} else {
gWalkSideDir = (gWalkSideTries & 1u) ? AGI_DIR_S : AGI_DIR_N;
}
}
if (gFrame < gWalkSideUntil) {
want = gWalkSideDir;
} else if (dy == 0) {
want = wantX;
} else if (dx == 0) {
want = wantY;
} else if (bumped) {
// Whichever way was tried last got nowhere: the other one.
want = (gWalkAxisY) ? wantX : wantY;
} else if (gWalkObject != HOST_WALK_NO_OBJECT) {
// The object moves, so there is no line: keep on while ego goes
// either right way, else take the longer one.
want = (dir == wantX || dir == wantY) ? dir : ((abs(dx) >= abs(dy)) ? wantX : wantY);
} else {
// How far ego is off the line, times the line's length: keep
// going while within the band, else take the axis that closes it.
runX = (int32_t)gWalkX - gWalkFromX;
runY = (int32_t)gWalkY - gWalkFromY;
err = ((int32_t)ego->x - gWalkFromX) * runY - ((int32_t)ego->y - gWalkFromY) * runX;
if ((dir == wantX || dir == wantY) && err * err <= (int32_t)HOST_WALK_BAND * HOST_WALK_BAND * (runX * runX + runY * runY)) {
want = dir;
} else if (err == 0) {
want = (abs(dx) >= abs(dy)) ? wantX : wantY;
} else {
// A step along x changes err by runY per pixel (sign of dx).
want = (((err > 0) == ((runY > 0) == (dx > 0))) && runY != 0) ? wantY : wantX;
}
}
if (dir != want || bumped) {
if (gTrace) {
printf("[%6.1f] walk: ego at %d,%d dir %u, pressing for %u%s\n", (double)gFrame / (double)HOST_FRAME_HZ, (int)ego->x, (int)ego->y,
(unsigned)dir, (unsigned)want, bumped ? " (bumped)" : "");
}
// Pressing the same way again (a logic stopped ego) is still the
// same try: only a change of axis starts a new one.
if (gWalkAxisY != (want == wantY && dy != 0) || bumped) {
gWalkAxisY = (want == wantY) && dy != 0;
gWalkTriedFrame = gFrame;
}
gWalkExpectDir = (dir == want) ? AGI_DIR_NONE : want;
gWalkPressFrame = gFrame;
hostPress(hostEgoKey(want), 0u, HOST_WALK_HOLD_FRAMES, HOST_WALK_GAP_FRAMES);
}
return false;
}
static void hostWritePpm(const char *path) {
FILE *fp;
@ -456,6 +1137,7 @@ bool hostScriptOpen(const char *scriptPath, const char *outDir) {
return false;
}
strncpy(gOutDir, outDir, sizeof(gOutDir) - 1u);
gTrace = getenv("AGI_HOST_TRACE") != NULL;
// Line by line, so a run killed by a timeout still shows how far it got.
setvbuf(stdout, NULL, _IOLBF, 0);
return true;

View file

@ -1212,6 +1212,167 @@ def probeCursor():
return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}), script
def probeEgoBlocked():
# Ego walked into a barrier with the arrow keys: does it keep its
# direction (v6 and get.dir) or stop? Shown a few cycles after it hit.
pic = bytes([PIC_COLOR, 0, PIC_PRIORITY, 0, PIC_LINE, 40, 0, 40, 100, PIC_END])
def body(L):
with L.If(("isset", F_NEW_ROOM)):
L.assignn(V_CYCLE_DELAY, 2)
showPic(L)
L.load_view(0)
L.animate_obj(0)
L.set_view(0, 0)
L.position(0, 60, 80)
L.draw(0)
L.show_pic()
with L.If(("have.key",)):
L.get_posn(0, 194, 195)
L.get_dir(0, 196)
L.display(1, 0, L.m("at %v194,%v195 v6 %v6 dir %v196"))
script = "wait 2\nkey Left\nwait 6\nkey a\nwait 1\nshot a\n"
return dict(logics=roomLogic(body), pics={1: pic}, views=VIEWS), script
def probePrintKeys():
# Keys pressed while a print window waits: a letter (does it close the
# window, or reach the input line once it has closed?), then Return;
# then the same over a window that closes by itself (v21).
def body(L):
with L.If(("isset", F_NEW_ROOM)):
L.assignn(V_CYCLE_DELAY, 2)
showPic(L)
L.show_pic()
L.set_cursor_char(L.m("_"))
L.accept_input()
L.increment(V_CYCLE)
with L.If(("equaln", V_CYCLE, 5)):
L.print(L.m("A window waiting for a key."))
# Cycles stop while the first window waits: this one opens as
# soon as it is closed.
with L.If(("equaln", V_CYCLE, 6)):
L.assignn(V_PRINT_TIMEOUT, 6)
L.print(L.m("A window for three seconds."))
script = ("wait 2\nkey a\nwait 0.5\nshot a\nkey b\nwait 0.5\nshot b\nkey Return\nwait 1\nshot c\n"
"key d\nwait 0.5\nshot d\nwait 4\nshot e\n")
return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}), script
def probeEdgeWalk():
# Ego walked off the right edge with the arrow keys. In the next room:
# where new.room put it (cycle 1); where it is and which way it goes 6
# cycles later, not yet animated again; and 6 cycles after an
# animate.obj (KQ3 animates ego in every new room); and after Right is
# pressed again there.
def body(L):
with L.If(("isset", F_NEW_ROOM)):
L.assignn(V_CYCLE_DELAY, 2)
showPic(L)
L.load_view(0)
L.animate_obj(0)
L.set_view(0, 0)
L.position(0, 140, 100)
L.draw(0)
L.show_pic()
with L.If(("!", "equaln", V_EGO_EDGE, 0)):
L.new_room(PROBE_ROOM + 1)
logics = roomLogic(body)
L = Logic()
with L.If(("isset", F_NEW_ROOM)):
showPic(L)
L.load_view(0)
L.set_view(0, 0)
L.draw(0)
L.show_pic()
L.increment(V_CYCLE + 1)
with L.If(("equaln", V_CYCLE + 1, 1)):
L.get_posn(0, 220, 221)
with L.If(("equaln", V_CYCLE + 1, 7)):
L.get_posn(0, 222, 223)
L.get_dir(0, 224)
L.assignv(225, V_EGO_DIR)
L.animate_obj(0)
with L.If(("equaln", V_CYCLE + 1, 13)):
L.get_posn(0, 226, 227)
L.get_dir(0, 228)
L.assignv(229, V_EGO_DIR)
with L.If(("equaln", V_CYCLE + 1, 30)):
L.get_posn(0, 230, 231)
L.get_dir(0, 232)
L.assignv(233, V_EGO_DIR)
L.display(1, 0, L.m("in at %v220,%v221"))
L.display(2, 0, L.m("then %v222,%v223 dir %v224 v6 %v225"))
L.display(3, 0, L.m("animated %v226,%v227 dir %v228 v6 %v229"))
L.display(4, 0, L.m("pressed %v230,%v231 dir %v232 v6 %v233"))
L.return_()
logics[PROBE_ROOM + 1] = L.build()
return dict(logics=logics, pics={1: EMPTY_PIC}, views=VIEWS), "wait 2\nkey Right\nwait 3.5\nkey Right\nwait 3\nshot a\n"
def probeNewRoom():
# A sound still playing when the room changes: a 2-second tune started
# with f60 three cycles before new.room. Shown: the cycle in the new
# room at which f60 was first seen set (1: at once, 0: never).
tune = {3: sound([[(120, 254, 2)]])}
def body(L):
with L.If(("isset", F_NEW_ROOM)):
L.assignn(V_CYCLE_DELAY, 1)
showPic(L)
L.show_pic()
L.load_sound(3)
L.increment(V_CYCLE)
with L.If(("equaln", V_CYCLE, 3)):
L.sound(3, 60)
with L.If(("equaln", V_CYCLE, 6)):
L.new_room(PROBE_ROOM + 1)
logics = roomLogic(body)
L = Logic()
with L.If(("isset", F_NEW_ROOM)):
showPic(L)
L.show_pic()
L.increment(V_CYCLE + 1)
with L.If(("isset", 60), ("equaln", 220, 0)):
L.assignv(220, V_CYCLE + 1)
with L.If(("equaln", V_CYCLE + 1, 80)):
L.display(1, 0, L.m("flag at %v220"))
L.return_()
logics[PROBE_ROOM + 1] = L.build()
return dict(logics=logics, pics={1: EMPTY_PIC}, sounds=tune), "wait 6\nshot a\n"
def probeDistance():
# distance between objects of different widths: side by side, one
# above the other, apart both ways (each way round), and to an object
# that is not drawn.
places = [(1, 0, 20, 100), (2, 1, 40, 100), (3, 0, 60, 80), (4, 0, 60, 100), (5, 1, 100, 130)]
def body(L):
with L.If(("isset", F_NEW_ROOM)):
showPic(L)
L.load_view(0)
L.load_view(1)
for o, v, x, y in places:
L.animate_obj(o)
L.set_view(o, v)
L.position(o, x, y)
L.ignore_objs(o)
L.stop_cycling(o)
L.draw(o)
L.animate_obj(6)
L.set_view(6, 0)
L.position(6, 120, 60)
L.show_pic()
L.increment(V_CYCLE)
with L.If(("equaln", V_CYCLE, 3)):
for i, (a, b) in enumerate(((1, 2), (2, 1), (3, 4), (1, 5), (5, 1), (1, 6))):
L.distance(a, b, 220 + i)
L.display(1, 0, L.m("1-2 %v220 2-1 %v221 3-4 %v222"))
L.display(2, 0, L.m("1-5 %v223 5-1 %v224 1-6 %v225"))
return dict(logics=roomLogic(body), pics={1: EMPTY_PIC}, views=VIEWS), "wait 3\nshot a\n"
PROBES = {
"font": probeFont,
"pictures": probePictures,
@ -1236,6 +1397,11 @@ PROBES = {
"shake": probeShake,
"winedge": probeWindowEdge,
"cursor": probeCursor,
"egoblocked": probeEgoBlocked,
"printkeys": probePrintKeys,
"newroom": probeNewRoom,
"edgewalk": probeEdgeWalk,
"distance": probeDistance,
}