Adding a @discardundo opcode

I’ve been looking at Inform’s parser loop. (See thread https://intfiction.org/t/working-on-a-unified-glulx-input-extension/8676/1 .)

It’s got a lot of grotty bits. One of the obvious, which many people have stumbled over, is that the blank-input error (“I beg your pardon?”) is wrapped up inside the Keyboard() routine. It doesn’t make it out to the “reading a command” activity.

Why is this? One motivation must have been to short-circuit blank inputs before the @saveundo. That is, if the player whacks the enter key a bunch of times and then types “UNDO”, we want to jump back to an undo point before the last valid command. Making them “UNDO” each blank input in turn is silly.

Notice that this doesn’t extend as far as erroneous inputs. If the player types “DFGHS” and gets an “I don’t recognize that verb” error, and then does “UNDO”, the game undoes the error input. (By the time the parser figures out that it’s an error, the undo point is already saved.) This is silly but we put up with it.

One way to fix this would be to delay saving an undo point until the parser has parsed out a valid command. That’s okay in theory but it’s risky. The parser does a lot of work, potentially extended by the game author. We don’t want to run that work before the undo point, because it could affect future commands. Then UNDO becomes unreliable and players become sad. It’s cleanest to save the undo point as soon as possible after the input event.

A nicer solution, I think, is to add a VM opcode @discardundo. This simply throws away the last undo point.

Now we have this layout:

Keyboard:
	KeyboardPrimitive()
	if UNDO command:
		@restoreundo
	@saveundo

Parser:
	(before reading a command...)
	Keyboard()
	(after reading a command...)
	if input is blank:
		"I beg your pardon?"
		@discardundo
		continue
	parse parse parse
	if any parser error:
		show error
		@discardundo
		continue
	perform action

Now blank-input errors are handled exactly like all other parser errors. The Keyboard() routine is simpler. (In fact we can make it even simpler by moving the @saveundo above the KeyboardPrimitive() line, which is where intuition wants it.)

The down side, of course, is that it’s an interpreter change. Many interpreters are unmaintained these days so the @discardundo opcode will not be available.

(Consequence: I don’t want to tie my Unified Glulx Input work to this. But maybe as an optional enhancement, and suck up the regression in blank-line handling on old interpreters…)

Comments?

Is this at all related to the issue where if you type UNDO enough times, you get “The use of “undo” is forbidden in this game.” even though it’s not forbidden? That seems kind of silly to me. I never realized that UNDO undoes typed errors, but yes, that seems kind of silly too.

No, it’s not related.

The error for that case is “You can’t “undo” what hasn’t been done!” and as far as I know that works correctly.

I get this:

[spoiler]Place

look
Place

g
Place

g
Place

g
Place

g
Place

g
Place

g
Place

g
Place

g
Place

g
Place

undo
Place
[Previous turn undone.]

undo
Place
[Previous turn undone.]

undo
Place
[Previous turn undone.]

undo
Place
[Previous turn undone.]

undo
Place
[Previous turn undone.]

undo
Place
[Previous turn undone.]

undo
Place
[Previous turn undone.]

undo
Place
[Previous turn undone.]

undo
The use of “undo” is forbidden in this game.

[/spoiler]

I always figured it was just a quirk that people put up with. If it’s a bug maybe I should report it.

Sounds good overall.

So the discard and ‘I beg your pardon?’ happens after ‘after reading a command’, right? That means I can still get in there before it and make a blank-input repeat the player’s previous command, which is something I do in all my games.

-Wade

Oh, hm. Yeah, that’s the wrong error message for when you try to undo farther than your interpreter supports.

Yes.

Quick thought: Would it be feasible/safe to move the saveundo call as shown below?

Keyboard:
	KeyboardPrimitive()
	if UNDO command:
		@restoreundo

Parser:
	(before reading a command...)
	Keyboard()
	(after reading a command...)
	if input is blank:
		"I beg your pardon?"
		continue
	parse parse parse
	if any parser error:
		show error
		continue
>	@saveundo
	perform action

That’s the paragraph up top labelled “risky”. The farther you go, the more chance that some game code will make a real state change that’s not protected by the undo. (Think how many people on the I7 forum wind up putting parse-and-action code in a “after reading a command” rule, even though that’s not recommended.)

(Or, on the TADS forum, a common newbie question is “something’s happening when it shouldn’t be” and the answer is “you made changes in a verify method, Don’t Do That”. Life is better for everyone when this stuff works automatically for everybody.)

Only a small thing, but I feel like Keyboard() could be more generically used if it didn’t always do the undo code. What about if you had to pass in a parameter for it to do so?

Keyboard is basically a thin wrapper around KeyboardPrimitive that adds four features: prompt display, undo handling, oops handling, and reject-blank-lines. This is what’s needed for the main command input – Keyboard is called only by the parser.

If you want a generic entry point, you call KeyboardPrimitive. YesOrNo and the final game question do this. (Those two routines print their own prompts, do their own bad-answer rejection, and don’t want undo/oops handling.)

Ah, fair enough then.