More debugging and tests.
This commit is contained in:
parent
cfda4c52db
commit
45a82bef2f
9 changed files with 891 additions and 28 deletions
2
Makefile
2
Makefile
|
|
@ -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
4
agi.c
|
|
@ -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();
|
||||
|
|
|
|||
7
agiVm.c
7
agiVm.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
1
tests/expected/probes/distance.txt
Normal file
1
tests/expected/probes/distance.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 4f6182a8
|
||||
1
tests/expected/probes/egoblocked.txt
Normal file
1
tests/expected/probes/egoblocked.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a 299bf80c
|
||||
1
tests/expected/probes/newroom.txt
Normal file
1
tests/expected/probes/newroom.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
a eecb58af
|
||||
5
tests/expected/probes/printkeys.txt
Normal file
5
tests/expected/probes/printkeys.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
a 4f293179
|
||||
b 4f293179
|
||||
c 2b4619ed
|
||||
d 2b4619ed
|
||||
e 7662eaad
|
||||
732
tests/hostPort.c
732
tests/hostPort.c
|
|
@ -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;
|
||||
|
|
|
|||
166
tests/probes.py
166
tests/probes.py
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue