Praxix undo and multi undo questions

Hello, like so many before me, I decided to write a z machine interpreter from scratch for the hell of it. I’d stumbled across the infocom historical sources on GitHub, and a simple v3 interpreter, mojozork, inspired me to write my own.

What makes mine special? Not much, except that it’s pretty tiny and yet implements v3/4/5/8 pretty well, and it’s designed to be able to run v8 games on a system with around 256k of RAM (namely, the Raspberry Pi Pico in my PicoCalc) with standard single-level undo support.

It passes ziptest, and I managed to track down praxix after seeing it mentioned on here a few times. I cleaned up the obvious failures in praxis yesterday and this morning, but I’m a little stumped on the undo and multi undo tests.

undo nearly passes now, but it’s failing the “shouldn’t have an arg count of 3” test in the top-level code (not the one in the inner test function). I can’t figure out why though. My undo state is pretty basic - a copy of the stack, dynamic, memory, stack pointer, frame pointer, and current PC.

multi undo also doesn’t pass, but that’s because I don’t currently implement multi-level undo. I’m struggling to understand how this can possibly work though? It seems like any save_undo should overwrite any previous state. Do interpreters do something weird like set a flag any time there’s a new “turn” (presumably by watching for user input) so that it would know whether this should replace the current undo state or push onto a stack?

(I’m aware I could store undo state in a lot less space by doing the xor against original and compress runs of zeros trick, just haven’t gotten around to that yet. The original story text is intended to live in flash on a Pi Pico since it’s treated as read-only memory anyway; the two megabytes of flash on the system is more than enough to store any z8 file).

Thanks,

-Dave

1 Like

Think of “save_undo” as creating a save game, one which is usually stored in memory instead of a file. Hence you can have many of them.

Also, I think it is up to the game code to decide when to make them (e.g. if nothing really happened, the game code might skip calling save_undo). But I’m not sure exactly how they do that.

Most interpreters will have an undo stack. Last on is first off, though the oldest could also be removed if it reaches a limit (either number of undoes or memory used).

If the PicoCalc has a larger file system than available RAM then undo states could be stored in files.

From the z-machine standard:

6.1.1.2

An internal saved game for “undo” purposes (if there is one) is not part of the state of play. This is important: if a saved game file also contained the internal saved game at the time of saving, it would be impossible to undo the act of restoration. It also prevents internal saved games from growing larger and larger as they include their predecessors.

1 Like

If the limit is one, then we’ve got the situation you described up top – each @save_undo overwrites the previous one.

If the limit is more than one, that’s when you have a stack and the capability for multiple undo.

That’s explaining that the @save_undo stack should not be included in the @save file.

I got my interpreter to pass the praxix multi undo test. However, when I fired up anchor.z8 and tried to undo twice, the game itself prevented this.

I assume interpreters that implement multi-undo intercept any undo command and implement it themselves before the game sees it?

I believe certain historical versions of the Inform library produced games which disabled multiple UNDO even if the interpreter supports it. I’m not sure why this was considered desirable or when it was changed, but have you tried with a more recent z-code file?

Yeah, previous versions of Inform disabled multiple undos at the software level (i.e. in the Inform library, not the Z-machine interpreter), because this was back in the days of cruel games where being able to undo out of a mistake would ruin the ~experience~. So the really cruel ones would specifically wait one turn after a mistake to kill you, to ensure you couldn’t undo out of it.

Given that both Curses and Jigsaw include the “wait one move, then kill you” gimmick, I place the blame squarely on Graham Nelson’s game-design sensibilities. (And more broadly on the culture of the Phoenix mainframe games.) But I believe it was changed between Inform 5 and Inform 6.

In fact, some Z-machine interpreters (like Bocfel) will specifically trap an input of “/undo” (with a slash) and execute an @undo opcode, bypassing the game’s processing entirely, specifically for early Inform games like this.

(Well, not just for early Inform games. Also for pre-Z5 games that don’t include an undo command.)

Tempting, but no. :) Everybody’s head was in the space of using SAVE judiciously, rather than assuming UNDO worked at all. So one-turn delays really weren’t meaningful.

No, this was purely bug-driven. I believe it was intended to work around bugs in some very old interpreters which would crash (or behave irrationally) if you did UNDO twice in a row. The interpreter bugs were fixed pretty quickly, but the Inform library check hung around, basically because nobody thought about whether it was still necessary.

Once we noticed it, Graham removed the restriction.

(You can see this as entry 60 in the “accepted suggestion” list.)

2 Likes

Huh! I’m surprised Jigsaw features the various one-turn delays then; I was so sure they were a way to get around UNDO.

That makes sense, though; Dialog still has a workaround built into its veneer for a similar sort of bug. According to the comments, Frotz crashes if you @undo before the first @save_undo, so it sets a flag on the first @save_undo to prevent that from happening.

For those who implement an undo directly in the interpreter, do you do an implicit save_undo any time the turn counter (v3) is bumped? Read_line?

Actually, I somewhat managed to trace a good chunk of the Life and Times of the Inform Library, notwithstanding the spotty recording, from the very first known source (inform 2’s Core) up to the “accepted suggestion list” you linked and David Griffith’s Changelog, last date july 30, 2024; there’s indeed holes here and here, but I think that tracing what historical version of the inform 5/inform 6 library disables these multiple UNDO because of the interpreters bug Zarf describes. IS feasible. and from this tracing I think is feasible fine-tuning the 'terp’s trapping/patching said old workaround, which I think can be safely taken as obsolete.

Best regards from Italy,
dott. Piergiorgio.

In Bocfel, it’s done inside of @read, although it’s not exactly an implicit @save_undo since it needs to include metadata indicating it’s a “faked” version.

2 Likes

It sounds like you haven’t tried Czech yet. You should.

Thanks, I found a github repo that had it and several others interesting examples.

1 Like

I finally got around to trying Czech and it immediately spotted that @check_arg_count was wrong. Quickly figured out that I was storing the larger of (localCount,operandCount) in the stack frame (for use by @check_arg_count) when I should have just been storing operandCount.

That also fixed the one test in Praxix that was failing, so yay!

Thank you everyone.

Now to just get the rest of Czech to pass.

-Dave

1 Like

There’s some further testing advice at Terp testing game suggestions

1 Like