Recognizing keywords with (no space)'s

Hi; this may border on a feature request if there’s no way to do this currently, but I am wondering if there is a way to recognize keywords with apostrophes? As a motivating example, the player can be the Hero or the Villain, both wear capes, which are named either “your cape” or “(name #actor)(no space)'s cape” depending on which you are:

#hero
(name *)
 Hero
(animate *)
(singleton *)

#villain
(name *)
 Villain
(animate *)
(singleton *)

#cape-of-hero
(name *)
 (if) (current player #hero) (then)
  your
 (else)
  (the #hero)(no space)'s
 (endif)
 cape
(descr *)
 A heroic red cape!
(proper *)
(* is #wornby #hero)

#cape-of-villain
(name *)
 (if) (current player #villain) (then)
  your
 (else)
  (the #villain)(no space)'s
 (endif)
 cape
(descr *)
 A villainous black cloak!
(proper *)
(* is #wornby #villain)

#room
(room *)
(#hero is #in #room)
(#villain is #in #room)

(understand [become | $words] as [become $person])
 *(understand $words as single object $person preferably animate)

(prevent [become $thing])
 ~(animate $thing)
 You can't become (that $thing)!

(prevent [become $myself])
 (current player $myself)
 You are already (the $myself)!

(perform [become $person])
 You are now (the $person).
 (select player $person)

(intro)
 (select player #hero)

Then disambiguating the cape works, EXCEPT when you use an apostrophe:

> examine cape
Did you want to examine your cape or the Villain's cape?
> your
A heroic red cape!

> examine cape
Did you want to examine your cape or the Villain's cape?
> Villain
A villainous black cloak!

> examine cape
Did you want to examine your cape or the Villain's cape?
> Villain's
(I'm sorry, I didn't understand what you wanted to do.)

I tested and hardcoding “Villain’s cape” works just fine with “Villain’s” as the disambiguation, so the problem has to be with the (no space). I guess (no space) still counts as space for the purpose of being a word delimiter? If that’s the case I would argue that it shouldn’t.

2 Likes

Yes, you are perfectly right about what’s going on, but this is a deliberate design choice. When you print hello (no space) world, those two words are printed back-to-back, with no space in between, so it looks like helloworld, but it’s still two words internally.

The rationale is this: Dialog works with a fixed game dictionary, which is generated automatically at compile-time. In addition to any word constants that appear in the source code itself, the compiler will gather the output words from every predicate that’s reachable from inside a (collect words) or (determine object) statement, and add them to the dictionary. Having a fixed, alphabetized table of words at runtime is very efficient, because a word is merely an index into this data structure.

If we allow words to be constructed from parts at runtime, then either the dictionary has to grow dynamically (which requires a substantial redesign of the runtime system, especially on the Z-machine), or we’d have to add a combinatorial explosion of amalgamated words to the dictionary. In your example, it might be possible for a compiler to deduce that it’s just the words hero and villain—the output from (the #hero) and (the #villain)—that needs to be combined with 's. But it’s easy to imagine that somebody writes (* is #partof $Owner) (the $Owner) (no space) 's. Since the object tree changes at runtime, the compiler would then have to assume that (the $) could be invoked for any object. And down that path lies a very large dictionary—possibly infinite.

Now, as it happens, Dialog can actually deal with unknown words typed by the player: They can be printed back, stored, and compared, just like ordinary dictionary words. But they are slower to work with, so they should be used sparingly.

There could indeed be a problem with how the library handles disambiguation. When the player is asked “Did you want to examine your cape or the Villain’s cape?”, what you see is the full name of each object, as given by (the full $). The full name can contain extra information to differentiate various objects, e.g. a door could be called “the iron door” but have the full name “the iron door to the east”. So when the player answers the question, the answer is checked against those full names, exactly as they appear. The (dict $) rule isn’t involved at all in this situation, and this was done to reduce the risk of the answer still being ambiguous. But it leads to a problem in the case of the cape, because what the player sees (“Hero’s”) is different from the list of words that Dialog sees (“hero” and “'s”). And the usual fixup, adding a (dict $) synonym, doesn’t work here. Perhaps it should; I’ll look into that.

Here are four different approaches to modelling the cape. The first—and this is what I recommend—is pragmatic:

#cape-of-hero
(name *) (if) (current player #hero) (then) your (else) Hero's (endif) cape

#cape-of-villain
(name *) (if) (current player #villain) (then) your (else) the Villain's (endif) cape

The second approach adds a new predicate called (their $), which adds a layer of abstraction, in case you want to change these characters’ names later:

#hero
(name *) Hero
(proper *)
(their *) Hero's

#villain
(name *) Villain
(their *) the Villain's

#cape-of-hero
(name *) (if) (current player #hero) (then) your (else) (their #hero) (endif) cape

#cape-of-villain
(name *) (if) (current player #villain) (then) your (else) (their #villain) (endif) cape

The third approach relies on extra dict synonyms, but this will not work in response to a disambiguation question with the current version of the library:

#cape-of-hero
(name *) (if) (current player #hero) (then) your (else) (the #hero) (no space) 's (endif) cape
(dict *) hero's

#cape-of-villain
(name *) (if) (current player #villain) (then) your (else) (the #villain) (no space) 's (endif) cape
(dict *) villain's

Finally, a more esoteric approach is to use word endings, which is a language feature that was originally added to deal with German adjective endings. The idea is that we declare a game-wide list of suffixes that the player is allowed to tack on to any dictionary word, as long as there isn’t a longer dictionary that’s a better match. For instance, if “-er” is one of the declared suffixes, and “blau” is in the dictionary but “blauer” isn’t, then if the player types “blauer” it will be recognized as blau+er, which will print as blauer but unify with blau. Thus, we could declare:

(removable word endings) 's

Now the player can type “hero’s”, and it will match “hero”. However, this only works if “hero’s” doesn’t appear in the source code, in any predicate that’s reachable through (collect words) or (determine object)! That’s a pretty big downside to this approach, because it creates a “spooky action at a distance” effect that makes the code hard to maintain. In a typical German game, the word-endings technique would be combined with explicit (dict $) synonyms as necessary. So, again, this would work much better if (dict $) synonyms were allowed in response to disambiguation questions. Again, I’ll look into that.

The second example can be refined to this:

(their (current player $)) your

#hero
(name *) Hero
(proper *)
(their *) Hero's

#villain
(name *) Villain
(their *) the Villain's

#cape-of-hero
(name *) (their #hero) cape

#cape-of-villain
(name *) (their #villain) cape

Very cool, makes perfect sense now why you chose to do things the way you did. Thank you for your time in addressing my question!