A "tuple" of a verb, object, and indirect object.

I was first interested in Interactive Fiction back in the 80’s, and also had a basic understanding of object-oriented programming at the time.

Whether you’re writing ZIL, or TADS, or something else, it is common to put your behaviors in three different places:

  • The verb (which typically encodes a prepositional phrase as well - #putOn, #putIn, #openWith, etc)
  • The direct object, which might check the verb and a possible indirect object via “before” and “after” handlers
  • The indirect object, as above

So for example, if you had a weird verb that only made sense in one place, it would be easier to put all the logic in the verb’s handler directly. It can check for valid combinations it cares about and issue failures on the rest.

Common actions like taking or dropping will likely have logic spread across many different objects “before” and “after” handlers.

Something I was wondering about way back in the day, and just crossed my brain again for the first time in many years, is a concept of a tuple. Specifically, you’d want a way to “intercept” a particular combination of verb/object/indirectObject without having to modify any specific verb, before, or after handler.

I guess the first question is… maybe, who cares? Just put the logic wherever it makes the most sense. Plenty of frameworks already seem to work this way.

But I wonder if anybody has tried to formalize this already in an existing authoring language? Just realized even a “tuple” might not be enough, since the location might matter too, but I feel like that could be just part of the logic that runs when the tuple fires.

The main advantage I can see is that it gives you a way to define specialized behavior (typically from new verbs) without having to either:

  • Add a bunch of new special cases to the verb handler
  • Add more code to the before or after functions of an object or indirect object.

At some point it might just be syntactic sugar? Specifying a tuple separately at an appropriate place in the code could be exactly identical to adding that logic to the verb handler directly

(in the case below, assume the parser has already determined that both the object and indirect object are available to the player)

routine OpenWith [directObject indirectObject] {
  if (directObject == canOfTuna && indirectObject == canOpener) {
     canOfTuna.open = true;
     print_ret "The cat comes bolting into the room, expecting dinner!"
  }
  .... other logic ...
}

tuple #openWith,canOfTuna,canOpener {
   canOfTuna.open = true;
   print_ret "The cat comes bolting into the room, expecting dinner!";
}

Whether the language physically inserts the equivalent of the first block when it encounters the second, or it records a list of object pairs associated with each verb and walks that implicitly is an implementation detail.

I’m just wondering if having this level of separation makes sense or not?

Thanks,

-Dave

I think Inform (7) and Dialog effectively already do this:

Instead of opening the can of tuna with the can opener: [...]
(instead of [open #canOfTuna with #canOpener])
    %% ...

Inform automatically orders these for you according to its internal specificity rules, whereas Dialog will try all instead of predicates in source code order. Inform really needs another word in between the direct and indirect object, but the Dialog example could theoretically be shortened to (instead of [openWith #canOfTuna #canOpener]).

5 Likes

Interesting. I’ve never looked into Inform 7 much because it’s too complicated to run on ancient hardware, and I’ve never looked at Dialog because Prolog gave me sanity damage in my college days. I ended up writing my own language because as much as I want to like “weird” languages like FORTH, LISP, or Prolog, my brain just cannot grok them.

Thank you!

-Dave

Rule-based languages, like those mentioned by Adrian, are definitely a step up from Inform-6 way of doing things (imho). The syntax is a lot nicer too.

The downside is managing the order that the rules will be triggered in. As mentioned, Inform-7 has some heuristics that work well most of the time, and Dialog uses source code order which is intuitive but can be a pain sometimes, e.g. you want a rule to occur in between some standard library rules.

3 Likes

Yeah, rule-based systems are often good at that: a while back I ran across Emily Short’s old article on why she found Inform 7 more powerful than Inform 6 for complex projects, which touches on this a little bit: http://brasslantern.org/writers/iftheory/i7observations.html

4 Likes

The fun part about making something is learning how to use it. My game engine project is state driven and rules based and it operates in mostly source code order. Because it is a game engine with dispatched events, the order that ends up mattering most became the event order. The source rule order is second. If one rule handles an early event and updates something that a later events rule logic is based on, but only for the next iteration, it creates a problem. It is formally called a RAW hazard or read-after-write hazard. I’ve used two different strategies. For some events putting the source logic in reverse order where the later source code ordering affects something that an earlier rule used, works. This only works for a certain class of logic that works in the scope of a single event or events that happened after earlier events. The solution that I worked up to because I’m still learning my own system is called double buffering. It is the way I will likely promote in the documentation for best practices. Any updates happen to temporary objects and at the top of the game loop those values are copied to the objects whose rules will process them. In this way RAW hazards are avoided.

2 Likes

Tuples, as you are using them here, are a representation of predicate logic and are the basis of rules based systems (Expert Systems) such as Inform and Dialog and possibly some other systems that I’m not aware of.

I really like this approach to IF and I’m working on my own rules based system called Reify. I hope to have something that I can share with folks around January.

3 Likes

For the Z machine in particular, there’s a very good reason to compile rules into actual code somewhere - the 64k limit

We can have up to 448k of code, but only 64k of data. All world objects, object descriptions, object properties, and the entire game dictionary come out of that 64k.

It would be trivial to have an array of structures with { verb, object, indirectObject, routineAddress } that a simple loop would walk looking for matches, but that would eat a lot of lower memory, and a fair amount of processing time. Or you could generate one of these lists per verb and only store the latter three; that would be a decent compromise. But the actual amount of code to check object and indirectObject is pretty tiny as well (two negated, short-form @je instructions mostly, at about four bytes each) and takes no lower memory.

Verb handlers could be marked up with sections like // INSERT PRIORITY 3 HANDLERS HERE so the compiler knows where to inject the collected code. A simple priority mechanism would let you deviate from source order and insert some rules before standard library ones, although I suppose the limit case there is the entire standard library is also just rules, and the standard library would intentionally leave some gaps in its priority declarations.

I imagine with a rule system though you’d need a way to inspect the generated code in a human-readable form. My own language started off trying to be as simple as possible, essentially like a C compiler for the Z machine, because I wanted as little as possible happening behind the scenes.

Rule-based processing would be a significant departure from that! But at least I now have a name for it!

-Dave

The approach I’m taking: count the number terms in the logic that aren’t variables. This is the specificity. For example: player carries ring has a specificity of 2. player carries [something]has a specificity of 1. player carries [something] and player is wizard has a specificity of 3. The most specific applicable rule applies and if there is a tie, it is broken by the most recent rule in the source code. This is similar to the approach taken for CSS. Additionally, within the production part of the rule there’s an option to process the next most specific rule either before or after the main body of the production.

3 Likes

Mind blown. Obviously rules can be more than just “player typed open tuna can with can opener”

Although now you have to deduce from the rule itself when it should be evaluated. Arguably “player carries ring” could be evaluated at the start of every turn, and/or when the player’s inventory changes regardless of how it changed. Which could very quickly ramp up the complexity and amount of code running per turn, which is probably a lot of the reason why Inform7 doesn’t work so well on older hardware.

-Dave

1 Like

Ideally you’re doing some sort of indexing so that you are only evaluating applicable rules when the tuples change. You may be interested in the Rete algorithm.

2 Likes

Like others have said, both I7 and Dialog currently do this. But it’s not too hard to add something like this into a non-rule-based language either!

The trick is that, effectively, both Inform and Dialog are letting a single Z-code routine be split into dozens of individual “rules”, which can be placed anywhere in the source code. It’s pretty much just syntactic sugar, but it’s really useful syntactic sugar.

When an I7 author writes:

Instead of taking something when the player is weakened:
    [code here]

Or a Dialog author writes:

(instead of [take $])
    (player is weakened)
    %% code here

The end result is something like (pseudocode):

routine instead_of() {
    ...
    if(action == Take && player_weakened) { /* code here */ }
    ...
}

All the rules are strung together into a big routine that tests the conditions for each one in turn, but they don’t all have to be grouped together in the original source.

Note also that those conditions can be as complex as you want! Dialog stores actions as lists containing the verb and objects; I7 stores the current action in some global variables (same as ZIL and I6) but can convert it into a tuple when needed. But that representation doesn’t limit what kind of conditions you can put on an individual rule.

5 Likes

It’s worth another look. It’s Prolog(-like) under the hood, and you can do some powerful and more succinct things if you wrap your head around the Prolog paradigm. However, there’s enough syntactic sugar in there that you can mostly write in a style that resembles other languages that you’re more familiar with, if you prefer. You’ll need to use (tail-)recursion rather than loops, though.

2 Likes

Indeed, it is very useful. Rule systems do a nice job of flattening out the logic instead having a giant if hairball thereby reducing cognitive load on the author.

4 Likes

This is how my game engine works although the effect is achieved differently. There is a main curated action, like take, and it can chain other rules to it. Chained rules only get run when they are called and can be in any order. A human readable grocery list style order helps a lot.

1 Like

Would love to see a tutorial that takes this approach. I’ve tried to understand Dialog, but I only get so far in the documentation and then I give up.

1 Like

I’ve been curious: have you benchmarked this enough to have a feel for the size at which the naive approach with minor (or more manual) indexing starts being slower? I’ve been surprised several times the last few years how high the crossover point is where asymptotically slower algorithms are faster due to them being simpler (constant factors being lower)… like I came to the surprising (to me) conclusion that with storylet matching against simple range conditions, for any plausible hobbyist IF story size, the Vose alias method of weighted random selection is never going to be worth it, and even binary search is probably a waste of time versus a straight linear search (at least in JS on present-day hardware where pipelining means failed condition checks are expensive).

3 Likes

One of the things on my to-do list is a one-page introduction document that shows how to get started without getting bogged down in theory. I’ll get to it one of these months…

3 Likes

I have not done any benchmarking. I decided to just do the alpha node network from the Rete algorithm and not the full alpha + beta network. I agree that you could probably just naively check all the conditions every turn and be fine, but the alpha network is pretty simple to implement and is pretty useful for pattern matching anyhow. Honestly, I don’t think the beta network is that useful for the way most rules are written in IF.

player carries ring translates into a JavaScript object as plot.player.carrying.ring and player carries [something] translates into plot.player.carrying.__. Scene objects containing fact selection and production functions are the payload at the end of plot tree. When facts update in the model, scenes are selected and acted upon. This explanation maybe a little wrong or vague. It’s actually what I’m working on right now so some details still need to iron themselves out…

1 Like

Besides rule systems, this is a pretty good description of multiple dispatch, available in several programming languages. Common Lisp and Julia are probably the best known of these.

Single-dispatch object-oriented programming, very familiar from Java, C++, Javascript, Python, TADS—it’s basically the default way to design a language now—chooses the method to run based on the first argument to it.

Multiple dispatch extends this to several arguments, so you can do exactly what you described—add more code without editing existing handlers.

I’ve thought it would be a nice thing to implement in ZIL. Probably just a few easy macros, kind of a mini CLOS (Common Lisp Object System).

4 Likes