In Zork I, does THIEF-ENGROSSED actually work?

In Zork I, there’s supposed to be a mechanic whereby you can give the thief a treasure and temporarily reduce his strength to 2. The thief’s strength is initially 5, so doing this would offer a big advantage in combat. The trouble is, it doesn’t seem to work. I can see the flag getting set, but it always becomes unset again by the time the game accepts the next player input. Has anyone been able to make this game mechanic work as it is evidently intended to?

There’s a global flag at the center of this called THIEF-ENGROSSED. It becomes set in ROBBER-FUNCTION when you give the thief a treasure (an object with P?TVALUE > 0):

1actions.zil#L1949

<MOVE ,PRSO ,THIEF>
<COND (<G? <GETP ,PRSO ,P?TVALUE> 0>
       <SETG THIEF-ENGROSSED T>
       <TELL
"The thief is taken aback by your unexpected generosity, but accepts
the " D ,PRSO " and stops to admire its beauty." CR>)
      (T
       <TELL
"The thief places the " D ,PRSO " in his bag and thanks
you politely." CR>)>

The commentary in The Visible Zorker has this to say:

glob:THIEF-ENGROSSED
Set if you have distracted the thief by giving him a treasure. This reduces his effective strength to 2 (see VILLAIN-STRENGTH), but only for the next blow.
rtn:VILLAIN-STRENGTH
Determine the combat strength of an enemy. This is their STRENGTH property, with a couple of adjustments. The thief and troll are weaker against certain weapons (see VILLAINS), so their strength is decreased if the current action is ...WITH SWORD or ...WITH KNIFE. Also you can distract the thief and temporarily reduce his strength by giving him a treasure!

We see the THIEF-ENGROSSED flag taking effect in the VILLAIN-STRENGTH routine:

1actions.zil#L3333

<COND (<AND <EQUAL? .VILLAIN ,THIEF> ,THIEF-ENGROSSED>
       <COND (<G? .OD 2> <SET OD 2>)>
       <SETG THIEF-ENGROSSED <>>)>

This says: if the thing being attacked is the thief, and the THIEF-ENGROSSED flag is set, set the effective strength variable OD to be no greater than 2, and unset THIEF-ENGROSSED. It’s not a permanent change to the thief’s strength: it only affects the return value of one call to VILLAIN-STRENGTH (which is called from HERO-BLOW and from VILLAIN-BLOW).

But I cannot get THIEF-ENGROSSED to take effect for even one action. You can see it for yourself in The Visible Zorker. Click on the State button and scroll down to THIEF-ENGROSSED. Copy and paste the below two lines (one at a time, because of input length limits) to fetch the jeweled egg and deliver it to the thief. You may have to restart a few times until you win the troll fight and avoid getting insta-killed by the thief.

n.n.u.get egg.d.s.e.open.w.w.get all.light.tug rug.open it.d.n.hit troll.g.g.g
w.w.w.u.sw.e.s.se.odysseus.u.give egg

At the end, you will see the output associated with THIEF-ENGROSSED getting set,

The thief is taken aback by your unexpected generosity, but accepts the jewel-encrusted egg and stops to admire its beauty.

but by the time the game prompts you for your next turn, the State tab shows THIEF-ENGROSSED:0. I suspect it’s because of the one of the two places in I-FIGHT where THIEF-ENGROSSED is unconditionally cleared.

With the player at strength 2, and the thief at strength 2, attacking with the sword (which gives no advantage against the thief), the combat roll should use the first 9 elements of the DEF2B table, in which it is just possible (1/9 chance) to get an UNCONSCIOUS result. With the player at strength 2, and the thief at strength 5, an UNCONSCIOUS result is impossible.

As far as I can tell, the only real effect of the THIEF-ENGROSSED mechanic is that you won’t be attacked on the turn that you give the thief a treasure. It doesn’t confer a combat advantage on the next turn. But I would love to be wrong about this.

5 Likes

Please note that my commentary comes from inspecting the source, not from live testing. I did not realize that the flag is immediately unset!

2 Likes

The reason I’m asking about this is that I’m working on a tool-assisted speedrun of the Apple II version of Zork I. If THIEF-ENGROSSED worked, it would enable a 2-turn kill on the thief in the early game. Without THIEF-ENGROSSED, it requires 4 turns. (Fighting with the knife, rather than the sword, would reduce both of these counts by 1, but having to get the knife from the attic would cost more time than it saves.)

The BizHawk emulator supports Lua scripting, so we can write a script to report whenever THIEF-ENGROSSED changes. (The lookup is a little tricky because THIEF-ENGROSSED is relative to the GLOBAL table, whose address is stored at runtime.)

thief-engrossed.lua
-- GLOBAL is a 16-bit pointer to the table of 16-bit global variables.
-- https://github.com/erkyrath/infocom-zcode-terps/blob/d5ac95a838/apple/zip/eq.l#L76
local GLOBAL = 0xac
-- THIEF-ENGROSSED is at index 30 in the table.
local THIEF_ENGROSSED = 30

-- Whenever the 16-bit value at addr is written to, schedule the given callback
-- to be called, once, at the end of the frame, with addr as its argument. At
-- most one such callback will occur, even if the value is written to more than
-- once. This is because, on the Virtu core, the on_bus_write callback doesn't
-- tell us what the value to be written is. Waiting until end of frame gives
-- the value a chance to be committed to memory. Returns the on_bus_write event
-- ids for the first and second byte of the 16-bit value.
local function on_write_16_once(callback, addr)
	local onframeend_id = nil
	local function written()
		if onframeend_id ~= nil then
			-- The end-of-frame event is already scheduled.
			return
		end
		onframeend_id = event.onframeend(function ()
			assert(event.unregisterbyid(onframeend_id))
			onframeend_id = nil
			callback(addr)
		end)
	end
	return {
		event.on_bus_write(written, addr + 0),
		event.on_bus_write(written, addr + 1),
	}
end

local prev_thief_engrossed = nil
local function thief_engrossed_written(addr)
	local thief_engrossed = memory.read_u16_be(addr)
	if thief_engrossed ~= prev_thief_engrossed then
		prev_thief_engrossed = thief_engrossed
		print(string.format("frame %d\tTHIEF-ENGROSSED=%d",
			emu.framecount(), thief_engrossed))
	end
end

local thief_engrossed_write_id = nil
-- Unregister any existing THIEF-ENGROSSED write event handlers, and register
-- new THIEF-ENGROSSED write handlers if GLOBAL is nonzero.
local function update_thief_engrossed_write_event()
	if thief_engrossed_write_id ~= nil then
		assert(event.unregisterbyid(thief_engrossed_write_id[1]))
		assert(event.unregisterbyid(thief_engrossed_write_id[2]))
		thief_engrossed_write_id = nil
	end
	local global = memory.read_u16_le(GLOBAL)
	if global ~= 0 then
		local thief_engrossed_addr = global + THIEF_ENGROSSED*2
		thief_engrossed_write_id = on_write_16_once(thief_engrossed_written, thief_engrossed_addr)
	end
end

on_write_16_once(update_thief_engrossed_write_event, GLOBAL)
update_thief_engrossed_write_event()

Run the script on a prerecorded input file and Apple II disk image like so:

./EmuHawkMono.sh --movie=100%-80-brief.bk2 --lua=thief-engrossed.lua Zork_I_r88.dsk

This is the output of the script. Frame 285 is the interpreter initializing itself. Frames 23114 and 23133 are at the beginning of the thief battle.

frame 285	THIEF-ENGROSSED=0
frame 23114	THIEF-ENGROSSED=1
frame 23133	THIEF-ENGROSSED=0

Combining the above frame timestamps with a timestamped transcript of the run, we see that THIEF-ENGROSSED indeed gets set upon giving the egg to the thief, but becomes unset again before the player’s next turn.

23013	>giVe eGG
23070	(to the thief)
23114 	[THIEF-ENGROSSED=1]
23114	The thief is taken aback by your unexpected generosity, but accepts the
23118	jewel-encrusted egg and stops to admire its beauty.
23133 	[THIEF-ENGROSSED=0]
23144	
23147	>hIT MAN
23183	(with the sword)

(The funny capitalization of commands is RNG manipulation to get favorable combat results.)

4 Likes

Yeah,honestly that looks like a bug to me. As in, they made it work fine - but then they were like “oh, let’s also make first turn of combat affected as well”, and then forgot to amend the code. If I were to fix it (just for fun, not saying you should) I’d probably make THIEF-ENGROSSED an integer set to 0, then set it to 2 when given the egg, and instead of setting it to <> immediately I’d probably just <DEC THIEF-ENGROSSED>. That way it would run through two checks, the second most likely either being the attack or a second turn.

The idea that the flag only affects the next VILLAIN-STRENGTH call is bug-prone in itself. It’s easier to manage the system if the flag is only cleared (or decremented) at end-of-turn.

This is a general recommendation about code style, not a proposal for changing Zork. :)

3 Likes