joeyagi/STATUS.md
2026-10-06 16:59:41 -05:00

33 KiB

JoeyAGI status

Where JoeyAGI stands against Sierra's own interpreter, subsystem by subsystem and command by command, with the evidence. Written from a clean-room deep dive (October 2026): the public AGI specifications for the formats and commands, and Sierra's interpreter (King's Quest III 2.00, interpreter 2.435, in DOSBox Staging at 386SX-16 speed) run as a black box for everything the specifications leave open. No ScummVM, NAGI or Sarien source was read.

In short

  • King's Quest III plays. The opening runs in step with Sierra's (title, credits over Manannan, the house, the story windows: checked with single screenshots at 9, 17 and 84 seconds); play starts in the hall with walking, typing, the parser and its unknown-word replies, the menus, the inventory screen, the F1 help screen, save, restore, restart and KQ3's debug mode and teleports all behaving as on Sierra's interpreter. Where frames still differ it is in things that move with time or chance (Manannan's sparkles, the cat, the chickens, wandering), because DOSBox runs in real time and the harness in virtual time, and the two random generators differ.
  • Every picture is exact. All 97 KQ3 pictures (visual and priority, 194 frames) match Sierra's pixel for pixel, as does a synthetic picture using every drawing action and all 16 pens, and a long diagonal that shows Sierra's byte-sized line error (see "Interpreter versions and quirks").
  • shake.screen is Sierra's, measured frame by frame from a recording of Sierra's interpreter and reproduced beat for beat with JoeyLib's existing API (see "shake.screen").
  • Sierra's window and input-line quirks are reproduced: a print.at window at column 0 or 1 is drawn and only partly erased as Sierra's byte arithmetic does it, and set.cursor.char changed mid-line moves the echo as on Sierra's. Both, and the picture-line quirk, are gated on the interpreter version read from the game's AGIDATA.OVL.
  • 23 probe games, 93 frames and one recording: 89 match Sierra exactly. The others are follow.ego's random detours (its deterministic half matches) and the clock probe's four frames, which all show one measurement: a 1-second timed window that closes one cycle (50 ms) later than Sierra's (the 3-, 2- and half-second ones match).
  • All six ports build; the IIgs disk boots and draws (it was blank when the dive started), and the ST, Amiga and X68000 builds draw KQ3 correctly (screenshots/port-*.png).
  • Gates: make check passes: host 28/28 (resources, 23 probes, 4 KQ3 sessions), disk PASS.

Not verified: playing KQ3 to the end, death scenes, the later lands (Llewdor, the ship, Daventry) beyond their pictures and a few teleports, audio by ear, and speed on the 8 MHz machines.

How it was checked

Tool What it does
tests/hostPort.c, tests/agiHost.c The whole interpreter on the build machine (JoeyLib's BLANK port): virtual 60 Hz time, a scripted keyboard, every frame resolved through the palette and hashed (FNV-1a) or saved as PPM. Fully reproducible.
tests/agiGame.py, tests/probes.py Builds AGI games from scratch (LOGIC assembler from the spec's command table, PICTURE byte lists, VIEW from text art, SOUND, WORDS.TOK, OBJECT). Each probe records what the interpreter did cycle by cycle into variables and prints them, so its frames do not depend on timing.
scripts/oracle.sh Runs a probe (or a KQ3 session) on Sierra's interpreter in DOSBox under a private Xvfb, driven by xdotool, and on JoeyAGI, then compares every shot with tests/tools/compareFrames.py (EGA colours, text read with each interpreter's own font -- JoeyAGI's font is its own -- and Sierra's 40-column text mode compared cell by cell). A probe whose script says record is recorded instead: DOSBox's video capture (every 70 Hz frame) on Sierra's side, every frame on JoeyAGI's, compared by tests/tools/shakeMeasure.py.
scripts/test-agi.sh The host gate: resources, probes and KQ3 sessions against tests/expected/.

A caution learned on the way: DOSBox's screenshot key (Ctrl+F5) reaches the game as a key press, and KQ3's opening hurries along when it sees one. The KQ3 checkpoints below were each taken with a single screenshot.

The state found

The interpreter at the start (commit 96c72ca) was a VM/host split with many pieces stubbed: sound set its flag at once; save.game, restore.game, status, show.obj, menu.input, obj.status.v did nothing; restart.game quit; object collisions and block were not modelled; posn tests were approximations; ego stepped a fixed 2 pixels; and an argv "starting room" let you skip "a title screen the VM can't yet advance through". The IIgs disk gate passed, but on the IIgs the old engine was the one drawing.

Nearly every module was rewritten (agi.c, agiRes, agiPic, agiView, agiObj, agiText, agiVm) or written new (agiGfx, agiFont, agiInput, agiSound, agiSave), each behaviour settled against the specifications and then against Sierra's interpreter.

Subsystems

Subsystem Status Evidence
Resources (directories, VOL, WORDS.TOK, OBJECT) Works. Files read whole into buffers of their own size from the Memory Manager on the IIgs; a missing or empty directory just means none of that type. resources-kq3: 125 logics, 97 pictures, 216 views, 34 sounds load and decode
PICTURE Exact: the spec's line (exact half rounds away from the start) with Sierra's byte-sized error term, corners, relative lines, scanline fill (visual, priority-only and both), solid and splatter pens of every size with the texture wrap at bit 254 194/194 KQ3 frames pixel-exact; pictures probe visual + priority exact (screenshots/probe-pictures-*.png); winedge probe's long diagonal
VIEW Works: loops, mirrored loops, cel transparency, descriptions; one allocation per view resources-kq3 (every cel decoded), setview, parser (show.obj)
Object drawing Exact: blits stacked with save-unders, not-updated objects under updated ones, sorted by baseline (or band top for fixed priority), control lines take the priority below them, add.to.pic margins (an outline of the control value from the baseline up to its band's top) priority probe incl. the priority screen exact
Motion and cycling Exact: step and cycle counters, the lost first step after draw/new.room/reposition, end.of.loop and reverse.loop flag timing, move.obj arrival (slack = step size), follow.ego distance timing, motion, counters, redraw, steering, cycling exact; follow deterministic rows exact
Ego steering Exact: v6 steers ego before and after logic 0 under player and program control; set.dir on ego has no effect; arrow keys toggle v6 egodir, steering, ego exact
Control lines, collisions, position fixing Exact: barriers, conditional barriers and ignore.blocks, signal lines (f3), water/land, priority 15 ignoring control, the block rectangle, objects' baselines (touching counts, crossing counts), the spiral position fix control, collide exact
Windows and text Exact: print/print.at geometry and Sierra's wrap rule, display wrapping, text attributes in picture and text mode, clear.lines/clear.text.rect, the status line, timed windows (v21); print.at windows at columns 0 and 1 as Sierra's byte arithmetic draws and erases them; the screen under a window put back exactly to the pixel windows (12 frames), winedge (18 frames), textscreen, clock exact
Input line Exact: row 23 unless configure.screen moves it, no cursor until set.cursor.char, typed characters drawn in place, Enter and accept.input redraw it, prevent.input clears it; the echo keeps its own position as Sierra's does, so a cursor changed mid-line behaves as there parser probe exact (17 frames); cursor probe exact (20 frames)
shake.screen Exact: 4 beats per unit of the whole screen moved (8, 4) for 4 frames and back for 4, blocking shake recording: beats, frames and offset identical to Sierra's
Interpreter version Read from AGIDATA.OVL ("Version 2.435"), else the AGI file, else 2.435; the measured quirks follow it resources-kq3 (KQ3: 2435)
Parser and said Exact: separators, lower case, longest dictionary match incl. multi-word entries, ignored words, unknown words (stop, stay in the list for any-word/rest-of-line, v9 = position), all-ignored lines say nothing, v9 cleared each cycle parser exact; a 10-input x 9-pattern said() matrix matched Sierra
Menus Exact: bar, items, disabled items, keyboard navigation; kept across restart parser m01..m07 exact; screenshots/kq3-menu-after-restart.png
Inventory (status) and show.obj Exact layout (Sierra draws the inventory in text mode; compared cell by cell) parser p10, m05; screenshots/kq3-inventory-tab.png
Sound Flag timing exact: the flag is set when the first voice ends (Sierra's PC interpreter plays only that voice), whatever the other voices do; an empty first voice ends at once; f9 off sets it at once; stop.sound sets it. JoeyAGI plays all four voices on JoeyLib. sound probe exact (tunes of 3 to 240 ticks, 1 to 60 notes, long tails, no first voice); audio itself not compared (PC speaker vs. JoeyLib voices)
Clock (v11..v14), timed windows (v21) Clock exact; v21 windows of 3, 2 and 0.5 seconds measured identical to Sierra's against v11, the 1-second one one cycle longer clock probe; KQ3's once-a-second countdowns traced in step with Sierra's
have.key Works: a key this cycle that nothing else took; a logic that asks again in the same cycle (KQ3's help screen spins on it) waits a frame per ask and takes any key pictures, priority, textscreen, ego probes; screenshots/kq3-help-f1.png
Save / restore / restart Work: restore rebuilds the picture from the script and ends the cycle so logic 0 sees f12 next (KQ3 relies on it); restart keeps menus and set.key mappings (KQ3 builds them only once) kq3-saverestore session; restart + menu matches Sierra
Memory Logics other than 0 are freed at new.room and so are the views no object shows, as Sierra frees a room's resources ASan run of every session clean
Display on planar machines Picture updates go out as colour runs through jlFillRect (two jlDrawPixel calls per AGI pixel before) host frames identical either way; ST/Amiga/X68000 snapshots

Bugs found and fixed in this dive, with causes

  1. IIgs: blank screen. Every resource came from malloc, and on the IIgs the C heap is the small window left in the entry bank (1926 bytes with 22 KB of BSS). Logic 0 could not be allocated, so nothing ran. All interpreter memory now comes from jlAlloc (the Memory Manager); files are read into buffers of their exact size; a view is one allocation; BSS fell from 22 KB to 4 KB (byte-sized fill stack, one shared line buffer, the picture script moved into the jlAlloc'd state).
  2. ST, Amiga, X68000: picture output. Planar stages have no chunky bytes, and the fallback drew two jlDrawPixel calls per AGI pixel. Now runs of one colour per row through jlFillRect. (An 8x8 tile path was tried first and came out striped: jlTileT's bytes are in each port's own layout.)
  3. Text attributes. set.text.attribute in picture mode stores a background of white (or black); text.screen clears to that; on the text screen the colours are used as given; clear.lines and clear.text.rect on the text screen take the colour as a PC attribute byte (so 0..15 clear to black). The old rule (background | 8) was wrong.
  4. Sound on at start. Sierra's interpreter sets f9 itself (its status line reads "Sound:on" in a game that never touches f9).
  5. Input line. Default row 23, not 22; no cursor before set.cursor.char; typing draws only the changed cell, so text displayed on the input row stays until Enter or accept.input redraws the line.
  6. Unknown words. said() failed whenever v9 was set; Sierra keeps the unknown word in the list, where any-word and rest-of-line match it, so "open the door now" matches said(open, rol). A line of only ignored words no longer sets f2, and v9 is cleared each cycle.
  7. Ego steering. v6 is copied to ego's direction before and after logic 0 in both control modes (the old code copied ego's direction into v6 under player control, so set.dir moved ego); v6 set by a logic now moves ego that same cycle.
  8. reposition / reposition.to skip the object's step in that cycle.
  9. Sound flag came after the longest voice; Sierra's comes after the first voice. This alone put KQ3's opening seconds behind (its title waits on the tune's flag).
  10. Restore let the rest of the cycle run (objects stepped once past the saved state) and cleared f12 before any logic saw it; KQ3's logic 0 tests f12 after a restore to fix up its menu.
  11. Restart wiped the menus and set.key mappings, but KQ3 builds both only on a first start (f6 clear), so after a restart Esc and the function keys did nothing.
  12. Resource memory grew without bound (every logic and view ever loaded stayed); now freed at new.room.
  13. Games without SNDDIR (or any empty directory) failed to start.
  14. Frame order. Each idle frame polled the keys, pumped the sound and ticked the clock before waiting for the vertical blank, so a cycle that started right after the wait saw them a frame stale: a short sound's flag arrived a cycle late. The frame now passes first.
  15. have.key let a frame pass every time a logic asked, and took any key that arrived then -- so a probe asking once a cycle swallowed the arrow key meant to steer ego. Now only a logic asking twice in one cycle waits.
  16. shake.screen was a pause. It now shakes as Sierra's does (below).
  17. print.at at column 0 or 1 drew a clipped window and erased it whole; Sierra's interpreter draws and erases it displaced (below).
  18. Window save-under went through jlSurfaceCopyRect, which widens a copy to 16-pixel groups, so closing a window also put back up to 15 stale pixels on each side: harmless while nothing beside an open window changed, wrong for the column-1 window, whose own frame and text sit there. Now exact: a chunky surface copies bytes (an AGI pixel each), a planar one copies the whole groups and the pixels at the ends one by one. Checked on all five machine ports against the host frame (0 pixels differ on ST, Amiga, X68000, DOS and IIgs).
  19. set.cursor.char mid-line echoed from the end of the typed text; Sierra's echo keeps its own position (below).
  20. Picture lines were exact to the fraction; Sierra's interpreter carries the error in a byte, which a line long on both sides overflows (below). No KQ3 picture has such a line.

Fixed earlier in the dive (before the gates existed), all verified against Sierra's interpreter: the picture decoder (line algorithm, fill semantics, pens, splatter textures); window geometry and the wrap rule; display wrapping; the status line; menus; the inventory layout; show.obj placement; add.to.pic and its margin boxes; sprite priority masking; control-line blocking; object collisions; the position-fixing spiral; motion and cycle counter timing; move.obj arrival; follow.ego; set.view/set.loop resets of missing loops and cels; animate.obj leaving step and cycle settings alone; %-codes in messages (%v with |width, %s, %w, %0, %m, %g); the parser's longest match; the intro's timing; v21 window timeouts; a guard for KQ3's room 43 call.v(0) recursion (Sierra hangs there); Alt and Ctrl chords no longer type characters.

shake.screen

Measured on Sierra's interpreter from DOSBox's video capture, every 70 Hz VGA frame of probe games calling shake.screen(1), (2) and (3) (the shake probe) and (64), (65) and (0) (one-off probes); tests/tools/shakeMeasure.py reads the shakes out of a recording:

  • What moves: the whole screen -- picture, status line, displayed text and the input line -- by exactly 8 screen pixels right (4 AGI pixels) and 4 rows down, a plain shift; the uncovered strips are black. No other offset or direction occurs.
  • A beat is the moved screen for 4 frames, then the screen in place for 4, locked to the vertical blank (114 ms at 70 Hz).
  • Beats: 4 per unit: 1, 2, 3 gave 4, 8, 12 beats; 64 gave 256 and 65 gave 260, so the count is 16 bits, and shake.screen(0) ran past 1090 beats before the recording stopped (65536 beats, about two hours).
  • It blocks: no cycles run and nothing animates until it ends.

JoeyAGI (gfxShakeScreen in agiGfx.c, JoeyLib's existing API, no per-port code): it copies the stage to one surface and builds a second with that screen moved (8, 4) over black (a chunky surface moves its bytes; a planar one draws the screen as one 40x25-tile sprite at (8, 4)); each beat copies the moved one to the stage, presents and lets 4 display frames pass (timed with jlMillisElapsed against jlFrameHz, so a slow copy is not added on top), then the same with the screen in place. shake.screen(0) is 65536 beats, as on Sierra's. If the two surfaces cannot be allocated the shake is a pause of the same length. The host's recording reads the same as Sierra's: 4, 8 and 12 beats, moved 4 frames, in place 4 frames, offset (8, 4); the host gate pins every frame of it.

IIgs cost: a phase is a 32 KB jlSurfaceCopy into the stage (about 108 ms) and a full-screen present (about 74 ms): 11 frames (183 ms) where Sierra's takes 4, so a beat is 367 ms against Sierra's 133 ms at 60 Hz and a shake lasts 2.75 times as long. The other ports were not timed.

A JoeyLib addition that would make it much better (not made): a display offset -- show the stage moved by (dx, dy), uncovered edges black -- as one call (say jlStagePresentOffset(dx, dy)). Where the hardware scrolls it is free: Amiga bitplane pointers plus BPLCON1, the X68000's graphic scroll registers, the VGA CRTC start address; the ST (an STF's video base moves in 256-byte steps) and the IIgs (no hardware scroll) would present the stage shifted, one present's worth of copying instead of a 32 KB copy and a present -- about halving the IIgs's beat.

Interpreter versions and quirks

KQ3's AGIDATA.OVL names its interpreter ("Adventure Game Interpreter", "Version 2.435"); the AGI executable carries no readable version (2.00 is KQ3's own version, shown by the game). At startup detectVersion (agiRes.c) reads the version from AGIDATA.OVL, else from the AGI file, else takes 2.435, the version measured, into game.interpreterVersion; agiQuirk() answers from one table of version ranges (kQuirkVersions), the one place per-version behaviour is decided. The IIgs disk now carries AGIDATA.OVL, and resources-kq3 checks KQ3 reads as 2435.

Only one Sierra interpreter, 2.435, was at hand, so whether the quirks differ between versions could not be measured: all three are on for 2.000 to 2.999 (every version JoeyAGI plays), and narrowing a range is a one-line change once another interpreter is measured. Off, each falls back to the plain behaviour (a clipped window, an echo from the end of the typed text, an exact line).

  • AGI_QUIRK_WINDOW_BYTE_X -- print.at at column 0 or 1. A window's box is x = 4 * col - 5 AGI pixels, y = 8 * row - 5 screen rows, 4 * width + 10 by 8 * lines + 10, the red frame one pixel in; its sides stop a row short of the top and bottom lines. Saving the screen under it, the white fill, the frame and putting the screen back all address the screen as one line of 160-pixel rows with x in a byte (row * 160 + (x & 255)), each span running on into the next row; the text is drawn where it belongs. At column 1 (x = -1, so 255) the save, fill and restore land a row down at x 95 and spill into the row below from x 0, while the frame (x + 1 = 0) and the text are in place: the window opens with its white running off to the right, and closing it erases only its leftmost 4 * width - 55 pixels, leaving the rest of the frame and text on screen. At column 0 (x = 251) the box and its top, bottom and left lines are drawn a row down, 91 in, and the right side and the text stay in place. Columns 2 and on are not affected. winedge probe: nine windows at columns 0 to 3, one to three lines, on a coloured picture, open and closed: 18/18 frames identical.
  • AGI_QUIRK_ECHO_STEPS_BACK -- set.cursor.char mid-line. The input line echo keeps its own position: a redraw (Enter, accept.input) leaves it after the prompt, the text and the cursor; with a cursor set, a typed character first steps back one -- whether or not a cursor was drawn there -- then draws itself and the cursor; Backspace steps back two and draws the cursor and a blank (without a cursor: back one, a blank). So a cursor set mid-line makes the next character overwrite the last one typed (">w", then '' set and "yz" typed, shows ">yz" while the line holds "wyz"), a cursor taken away stays on the line (">pq_" then "r" shows ">pq_r"), and rubbing that line out with the cursor set runs back over the prompt (three Backspaces leave just "*"). cursor probe: 20/20 frames identical.
  • AGI_QUIRK_LINE_BYTE_ERROR -- picture lines (found by the winedge picture): the line's error term is a byte, so a line long on both sides loses steps when it passes 255: (0,0) to (159,167) stalls two rows in ten and ends at (134,167). No KQ3 picture has such a line (all 194 frames are unchanged).

What remains, most important first

  1. A full play-through. KQ3 has not been finished on JoeyAGI. The rooms, puzzles, spells, deaths and endings past the house are untested beyond their resources decoding and a few teleports.
  2. Speed on the 8 MHz machines. Not measured. The run-length output is much faster than before; picture decoding and flood fill run in C.
  3. Audio by ear. Note timing and the flag are exact; how the four voices and noise sound on each port's JoeyLib audio has not been judged.
  4. The IIgs beyond booting. The disk gate proves it boots and draws; the opening was checked by framebuffer dumps, play on the IIgs was not.
  5. Save/restore look. JoeyAGI has its own slot window (slots 1-9, a description) instead of Sierra's path prompt and list, and its own file format, which is per machine type (byte order and padding).
  6. Random sequences differ from Sierra's (wander, follow.ego detours, KQ3's Manannan schedule): expected, not a bug.
  7. Other interpreter versions. Only 2.435 was available to measure; the quirks are assumed for every 2.x interpreter (see below).
  8. The font is JoeyAGI's own, not the PC's ROM font.
  9. shake.screen on the IIgs takes 2.75 times as long as on Sierra's (see below); a JoeyLib display offset would fix it.
  10. The flood fill's seed stack (1024 seeds) would truncate a fill that needs more; no KQ3 picture does.

Commands

Every AGI v2 command is implemented. "KQ3 uses" counts the command in KQ3's logics; "Probe" marks commands the probe games exercise against Sierra's interpreter (everything KQ3 uses is also exercised by the KQ3 sessions or checkpoints). FIXED marks behaviour corrected in this dive's later half; the earlier rewrite touched nearly every command.

# Command KQ3 uses Probe Status
0x00 return 262 yes works
0x01 increment 186 yes works
0x02 decrement 144 works
0x03 assignn 2659 yes works
0x04 assignv 111 yes works
0x05 addn 106 works
0x06 addv 10 works
0x07 subn 29 yes works
0x08 subv 7 works
0x09 lindirectv 1 works
0x0A rindirect 1 works
0x0B lindirectn 1 works
0x0C set 923 yes works
0x0D reset 737 yes works
0x0E toggle 12 works
0x0F set.v 4 works
0x10 reset.v 5 works
0x11 toggle.v 0 works
0x12 new.room 263 works
0x13 new.room.v 14 works
0x14 load.logics 135 works
0x15 load.logics.v 1 works
0x16 call 253 works
0x17 call.v 5 works
0x18 load.pic 98 yes no-op (draw.pic loads)
0x19 draw.pic 102 yes works
0x1A show.pic 109 yes works
0x1B discard.pic 91 yes no-op (pictures are not kept)
0x1C overlay.pic 9 works
0x1D show.pri.screen 1 yes works
0x1E load.view 375 yes works
0x1F load.view.v 84 works
0x20 discard.view 11 no-op: views go at new.room
0x21 animate.obj 200 yes works
0x22 unanimate.all 2 works
0x23 draw 318 yes works
0x24 erase 153 yes works
0x25 position 306 yes works
0x26 position.v 112 works
0x27 get.posn 48 yes works
0x28 reposition 12 yes works; FIXED (skips that cycle's step)
0x29 set.view 477 yes works
0x2A set.view.v 84 works
0x2B set.loop 227 yes works
0x2C set.loop.v 7 works
0x2D fix.loop 44 yes works
0x2E release.loop 19 works
0x2F set.cel 219 yes works
0x30 set.cel.v 18 works
0x31 last.cel 15 works
0x32 current.cel 23 yes works
0x33 current.loop 11 yes works
0x34 current.view 8 yes works
0x35 number.of.loops 0 works
0x36 set.priority 246 yes works
0x37 set.priority.v 11 works
0x38 release.priority 59 works
0x39 get.priority 10 works
0x3A stop.update 83 yes works
0x3B start.update 72 works
0x3C force.update 19 works
0x3D ignore.horizon 59 works
0x3E observe.horizon 5 works
0x3F set.horizon 57 works
0x40 object.on.water 6 yes works
0x41 object.on.land 2 yes works
0x42 object.on.anything 1 works
0x43 ignore.objs 126 yes works
0x44 observe.objs 15 works
0x45 distance 23 works
0x46 stop.cycling 150 yes works
0x47 start.cycling 100 yes works
0x48 normal.cycle 1 works
0x49 end.of.loop 100 yes works
0x4A reverse.cycle 0 works
0x4B reverse.loop 23 yes works
0x4C cycle.time 139 yes works
0x4D stop.motion 95 yes works
0x4E start.motion 72 yes works
0x4F step.size 19 yes works
0x50 step.time 8 yes works
0x51 move.obj 155 yes works
0x52 move.obj.v 59 works
0x53 follow.ego 30 yes works
0x54 wander 39 works
0x55 normal.motion 35 works
0x56 set.dir 6 yes works; FIXED (no effect on ego: v6 steers it)
0x57 get.dir 4 yes works
0x58 ignore.blocks 127 yes works
0x59 observe.blocks 52 works
0x5A block 10 works
0x5B unblock 6 works
0x5C get 155 yes works
0x5D get.v 2 works
0x5E drop 80 works
0x5F put 30 works
0x60 put.v 10 works
0x61 get.room.v 12 works
0x62 load.sound 111 yes no-op (sound loads)
0x63 sound 125 yes works; FIXED (flag when the first voice ends)
0x64 stop.sound 19 yes works; flag set
0x65 print 1695 yes works
0x66 print.v 27 works
0x67 display 70 yes works
0x68 display.v 5 works
0x69 clear.lines 17 yes works; FIXED (text-screen colour is an attribute byte)
0x6A text.screen 3 yes works; FIXED (clears to the attribute background)
0x6B graphics 3 yes works
0x6C set.cursor.char 1 yes works (no cursor before it: FIXED; changed mid-line moves the echo as Sierra's: FIXED)
0x6D set.text.attribute 31 yes works; FIXED (picture-mode background means white)
0x6E shake.screen 8 yes works; FIXED (measured: 4 beats per unit, (8, 4) for 4 frames and back for 4)
0x6F configure.screen 1 works
0x70 status.line.on 6 yes works
0x71 status.line.off 6 works
0x72 set.string 51 works
0x73 get.string 56 works
0x74 word.to.string 0 works
0x75 parse 0 works
0x76 get.num 13 works
0x77 prevent.input 75 works; clears the input line
0x78 accept.input 45 yes works; redraws the input line
0x79 set.key 39 yes works
0x7A add.to.pic 66 yes works
0x7B add.to.pic.v 8 works
0x7C status 2 yes works: the inventory screen
0x7D save.game 1 works: JoeyAGI's own slot window and file format
0x7E restore.game 1 works: JoeyAGI's own slot window; replays the picture script; FIXED (cycle ends, f12 reaches the next cycle)
0x7F init.disk 0 no-op (nothing to do on these machines)
0x80 restart.game 1 works; FIXED (keeps menus and set.key)
0x81 show.obj 61 yes works
0x82 random 200 works
0x83 program.control 80 yes works
0x84 player.control 44 yes works
0x85 obj.status.v 2 works: a debug window
0x86 quit 3 works
0x87 show.mem 1 works: a JoeyAGI message
0x88 pause 1 works: a message window
0x89 echo.line 1 works
0x8A cancel.line 1 works
0x8B init.joy 1 no-op (nothing to do on these machines)
0x8C toggle.monitor 1 no-op (nothing to do on these machines)
0x8D version 2 works: a JoeyAGI message
0x8E script.size 1 no-op (nothing to do on these machines)
0x8F set.game.id 1 no-op (nothing to do on these machines)
0x90 log 8 no-op (nothing to do on these machines)
0x91 set.scan.start 22 works
0x92 reset.scan.start 18 works
0x93 reposition.to 49 yes works; FIXED (skips that cycle's step)
0x94 reposition.to.v 58 works; FIXED (as reposition.to)
0x95 trace.on 0 no-op (nothing to do on these machines)
0x96 trace.info 1 no-op (nothing to do on these machines)
0x97 print.at 31 yes works; FIXED (columns 0 and 1 drawn and erased as Sierra's byte arithmetic does)
0x98 print.at.v 2 works (as print.at)
0x99 discard.view.v 0 no-op: views go at new.room
0x9A clear.text.rect 0 works; FIXED (as clear.lines)
0x9B set.upper.left 0 no-op (nothing to do on these machines)
0x9C set.menu 5 yes works
0x9D set.menu.item 21 yes works
0x9E submit.menu 1 yes works
0x9F enable.item 1 works
0xA0 disable.item 3 yes works
0xA1 menu.input 1 yes works
0xA2 show.obj.v 1 works
0xA3 open.dialogue 16 no-op (nothing to do on these machines)
0xA4 close.dialogue 7 no-op (nothing to do on these machines)
0xA5 mul.n 0 works
0xA6 mul.v 0 works
0xA7 div.n 0 works
0xA8 div.v 0 works
0xA9 close.window 0 works
# Test KQ3 uses Probe Status
0x01 equaln 2281 yes works
0x02 equalv 35 works
0x03 lessn 157 yes works
0x04 lessv 12 works
0x05 greatern 449 works
0x06 greaterv 10 works
0x07 isset 1631 yes works
0x08 isset.v 1 works
0x09 has 362 works
0x0A obj.in.room 144 works
0x0B posn 307 works
0x0C controller 39 yes works
0x0D have.key 4 yes works
0x0E said 2991 yes works; FIXED (an unknown word only matches any-word and rest-of-line; all-ignored lines say nothing)
0x0F compare.strings 28 works
0x10 obj.in.box 5 works
0x11 center.posn 0 works
0x12 right.posn 0 works

Gates

make check (build every port, then scripts/verify.sh):

Gate Result
host (scripts/test-agi.sh) 28/28: resources-kq3 (the interpreter version and 472 lines of resource hashes), 23 probes (shake pins every frame of its recording), 4 KQ3 sessions (opening, play, saverestore, teleport)
disk (IIgs AGI.2mg on a CFFA2, MAME) PASS (boots, launches, draws: distinctNibbles=6)

scripts/oracle.sh (not part of make check; needs Sierra's interpreter in games/kq3, Xvfb, xdotool): probes 89 of 94 shots identical to Sierra's (the rest: follow.ego's random detours, and the clock probe's 1-second window one cycle long, shown on all four of its frames), including the shake recording and the new winedge (18/18) and cursor (20/20) probes. Its KQ3 sessions (several shots each) drift from Sierra's with real time and with the screenshot key reaching the game (see "How it was checked"), which is why the KQ3 checkpoints above were taken one screenshot a run.

Screenshots

screenshots/ (git-ignored: they show Sierra's game) holds Sierra's frame and JoeyAGI's side by side for the probes (probe-*.png) and the KQ3 checkpoints (kq3-*.png), and the ports running KQ3 (port-*.png).