Proposal: A better way to (refuse $)

While I love most of the Dialog standard library’s design, (refuse $) has always struck me as distinctly inelegant. This is the predicate that checks if an action is feasible, and rejects it (taking no time) if it’s not—if you try to take something intangible, for example, or examine something that’s not present.

Here’s how it’s currently implemented:

(refuse $Action)
	*($Obj is one of $Action)
	(object $Obj)
	~(direction $Obj)
	~(relation $Obj)
	{
		(when $Obj is not here)
		(or) (when $Obj is out of reach)
	}

In other words, reject the action if any of the objects in it is not present, or is out of reach.

If you want to adjust this for any particular action, you have to write your own (refuse $) rule with (just):

(refuse [look $ $Obj])
	(just)
	{
		(when $Obj is not here)
		(or) (when $Obj is out of sight)
	}

And this means duplicating all the standard logic in your own rule:

(refuse [throw $Obj at $Target])
	(just)
	{
		(when $Obj is not here)
		(or) (when $Target is not here)
		(or) (when $Obj is out of reach)
	}

Which even the standard library sometimes gets wrong! If the standard library itself can’t keep this straight, it seems unfair to expect end users to.

So in the next release of Dialog, I’d like to replace this implementation. Here’s my proposal for how to do it.

First, (refuse $) will have three default rules:

(refuse $Action)
	*($Action requires $Obj to be present)
	(when $Obj is not here)

(refuse $Action)
	*($Action requires $Obj to be seen)
	(when $Obj is out of sight)

(refuse $Action)
	*($Action requires $Obj to be touched)
	(when $Obj is out of reach)

Each of these rules calls out to a new predicate:

($Action requires $Obj to be present)
	*($Obj is one of $Action)
	(object $Obj)
	~(direction $Obj)
	~(relation $Obj)

($Action requires $Obj to be seen)
	*($Action requires $Obj to be present)

($Action requires $Obj to be touched)
	*($Action requires $Obj to be seen)

That is:

  • By default, an action requires all physical objects to be present
  • By default, an action requires all (required to be present) objects to be seen
  • By default, an action requires all (required to be seen) objects to be touched

Now these individual predicates can be overridden for more fine-grained control, without worrying about the others:

~([look $ $] requires $ to be touched)

([throw $Obj at $] requires $Obj to be touched) (just)

~([examine $] requires $ to be seen)

And you can still add your own refuse rules if you want:

(refuse [drop (sacred $)]) That's unthinkable!

Plus, you can check these predicates elsewhere, too!

(prevent $Action)
    *($Action requires $Obj to be touched)
    (burning $Obj)
    You reach out for (the $Obj), but pull your hand away just in time—
    (no space) (it $Obj is) on fire, and too hot to touch!

It is, I will admit, a bit less efficient than the previous implementation, and requires a couple more queries. But I believe that cost is more than outweighed by making the whole system less error-prone, and requiring less duplication of code.

What do you all think?

  • This is a great idea!
  • No, keep it the old way
  • I want something else (not the old way or this new proposal)
0 voters
1 Like

My appreciation of Dialog is not particularly tied to its efficiency on retro systems, but I know there are a few people who are really into that; do you have any rough estimate of how much the change you’re talking about would cost in terms of performance?

2 Likes

When I tested with the Amiga version of Frotz it was already unusably slow with a test game that just implements a single room using the standard library (as a reference, the system requirements to get a reasonable speed would be similar to what you would have required to play Quake).

I don’t think it is feasible on retro hardware for anything except the C64 Å-machine interpreter.

4 Likes

Probably very little. Z5 and Z8 are already very slow on retro hardware; Å-machine is quite fast. So I don’t expect this to make much of a difference.

Really, the (the single $) optimization we made in the Standard Library last year probably hurts more than this, though I haven’t benchmarked it. I’ve been meaning to take that one back out. (The bug it was working around has been fixed in the meantime.)

1 Like

this all looks good to me. i really haven’t had any issues with (refuse $) as it is, probably because i think i’ve gotten into the habit of maybe leaning on (out of reach $) more than is healthy.

if i want to make a game for retro hardware i’ll use punyinform. i use dialog for it’s smooth and creamy awesomeness and if there’s a way to make it even better the considerations of efficiency should be, i don’t want to say ‘ignored’ but i really mean ‘ignored’.

3 Likes

I love the shape of your proposed solution. I have only two questions. First, how would the standard library handle cases in which the player directs the PC to do the impossible (e.g. cut flint knife with flint knife) (assuming there is only one flint knife)? Considering that the PC cannot so much as start to carry out this sort of action, it shouldn’t advance the story a tick. That in turn suggests that the library should handle such cases before (prevent $), but the issue is not a matter of presence, visibility, or reachability.

Second, which actions would require visibility by default? In line with Linus’s philosophy on whether objects should be kept in scope once #darkness has entered the building, I wouldn’t love it if the standard library right out of the box determined that my IF should be unwinnable when it is pitch black.

1 Like

Very good thoughts!

The first case is currently handled with (prevent $) rules, meaning by default it advances time. But that could be changed with an explicit (stop) if desirable. Where exactly the line falls between “you can attempt it but it won’t work” and “you can’t even attempt it” is pretty vague, imo, so I’m not sure there will ever be a universally-accepted decision there.

For example, is CUT KNIFE WITH KNIFE less attempt-able than CUT KNIFE WITH BREAD? What would an attempt to cut something with a slice of bread look like? I think for now, keeping the standard library implementation of (refuse $) limited to presence, visibility, and reachability is a good general rule; that’s the same thing Inform does, so people are generally used to it. (But of course, individual authors are free to change it.)

For the second case, that’s a good catch! I thought all actions required visibility by default, but indeed they do not. So the default rule for ($ requires $ to be seen) should just fail, and individual actions requiring light (like reading) can define their own rules for it.

My overall goal is to make the new (refuse $) implementation reject exactly the same actions as the old implementation (modulo bugfixes like examining currently not requiring presence); it should only change the interface authors and developers are working with.

3 Likes

In fact, it looks like the only action in the standard library that checks line of sight is [look $Rel $Obj]. A couple others check for light as a whole, but not for the visibility of objects. Which means it might be easier to just discard the visibility checks entirely, and let this one specific action add its own (refuse $) rule to deal with it.

I also considered adding a ($Action requires light) predicate to the default refuse rule, but only a handful of actions currently require that, and they all have their own failure messages. I don’t see a strong reason to centralize that right now.

1 Like

I’m going to give people a few more days to weigh in before anything changes, but you can now view the changes as a pull request. The automated test suite still passes, including all of the manual examples and the entirety of The Impossible Stairs, which is a good sign that no behavior will actually change as a result.

…so we’re heading for the need for a “MinisculeDialog” library variant in the future…

I’m honestly not sure how small you can get a Dialog program on the Z-machine. It’s extremely efficient with RAM (far more so than Inform), but uses a ton of ROM for all the support routines (manipulating lists and words and such). But, it doesn’t include those support routines in the output unless they’re actually used in the program, so clever design could cut that down by a lot.

There’s also a bunch of “free” space-saving that could be done without reducing functionality by implementing string abbreviations; currently they’re not used at all. If someone wants to fit Dialog games onto retro machines, that’s a good place to start.

2 Likes