Inform 7 for Programmers/Part 3
Rulebooks: White-box Paradigm
Unlike a function, a rulebook picks and chooses which rules within itself to execute. It will execute its rules until one of them produces a Success or Failure result, distinct from any (optional) returned value. In lieu of a return statement, rules use one of "rule succeeds", "rule fails", or "make no decision" imperatives, the last of which tells the 'book to try the next rule for a result. If a rule ends without one of these three imperatives, the default imperative -- usually "make no decision" -- is invisibly added at the end of every rule. That default can be set by the rulebook.
- The pick a plan rules are a rulebook.
- The pick a plan rules have default outcome success. [ Or failure, or no outcome ]
- A pick a plan rule: say "I always fail, regardless the rulebook's default."; rule fails.
- A pick a plan rule: say "I can never make up my mind so one of my peers will now execute."; make no decision.
- A pick a plan rule: say "I exhibit the default behaviour for the rulebook."
A rulebook can return an object, a specific named value, or some text. Such named values only have scope within that rulebook. Success/failure can be attached to named outcomes, which saves typing when they stand alone as a statement.
- The audible rules have outcomes silent (failure), whisper (success), voiced (success - the default), shout, and deafening.
- [...]; rule succeeds with result the whisper outcome.
- [...]; whisper.
- [...]; rule succeeds with result my fabulous doodad.
- [...]; rule fails with result "In your dreams."
Though we only need to do so when we make a new one, rulebooks are invoked imperatively. The keywords of note are "consider", "follow", "abide by", and (rarely) "anonymously abide by". The last two also function as a return statement as well, propagating success/failure. Global variables hold all return information.
- consider the pick a plan rules;
- if the rule succeeded, say "How about [the result of the rule]?";
- if the rule failed, say "I've no plans because of [the reason the action failed].";
- follow the pick a plan rules; [invisibly prepends "consider the procedural rules" so meta-rules are re-applied]
- if the rule succeeded, [...]
- abide by the pick a plan rules;
- say "This never prints because Abide By also returns.";
- anonymously abide by the pick a plan rules;
- say "As above, but doesn't clobber the global return variables. Useful for middleman rulebooks."
Rulebooks may also have local variables. Unlike the local variables created by the imperative "let", they remain in scope over all the rules of the 'book, not just a simple imperative block. Though we don't have a specific way of initializing them like Let does (or like an action's locals do (see following section)), we can use the rule ordinal "first" to put a particular rule at the start of a rulebook. (Only "first" and "last" exist this way, and only the final "first" or "last" rule is actually first/last in the book, superseding the preferences of earlier rules that use them.) "Last" rules are used to provide a fallback outcome for a rulebook, such as the Persuasion rulebook always failing.
- The pick a plan rulebook has an object called the best plan so far.
- The first pick a plan rule (this is the initialize plan rule): change the best plan so far to the little-used do nothing rule.
- The last pick a plan rule (this is the default fallback answer rule): rule fails.
Events are Actions
In interactive fiction, the player controls the protagonist, and typed commands translate into actions within the fictional universe. Protagonist actions like Looking, Examining, or Burning have rulebooks attached to deal with those events. Specifically, three rulebooks comprise an Action: its "check" rulebook ensures the action can proceed; its "carry out" rulebook updates game state; and its "report" rulebook narrates the result.
- Check taking something held: say "You already have that!" instead.
- Carry out an actor taking something: now the actor carries the noun.
- Report taking: say "Taken."
Much of I7 programming is writing report rules for actions, though some game events like Every Turn and When Play Begins are also popular. (Game events usually comprise a single rulebook apiece).
Defining a new action is similar to a function prototype of C:
- Donating is an action applying to one thing.
- Discussing is an action applying to one topic.
- Accusing it of is an action applying to one object and one visible object.
- Tabulating is an action applying to one number and requiring light.
- Scheduling is an action applying to one time.
- Temporarily waiting is an action applying to one time period.
- Whining is an action applying to nothing.
- Teleporting to is an action applying to one room.
- Saving the game is an action out of world applying to nothing.
- Tattooing it of is an action applying to one limb and one thing, requiring light.
- Weaving is an action with past participle woven, applying to one thing.
These each create the three rulebooks that will hold the implementation details. Because actions implement an English verb that our player-character (or an NPC) will perform, verbs have a maximum arity of 3 and a minimum arity of 1. The subject, called "the person asked" (or sometimes "the actor"), isn't optional so isn't mentioned above. In the implementation, the direct and indirect objects of a sentence are called "the noun" and then "the second noun". But, if the type of the parameter isn't an object, it will be "the number understood", "the time understood", "the topic understood", and so forth.
The syntax for declaring actions is very special-case: the phrase "requiring light" refers to a worldsim precondition; the adjective "visible" places every object (such as intangible ideas and rumors) in scope, and is completely unrelated to "requiring light"; the word "thing" (or "things") actually means any object(s) -- we can't be more specific and put "person" in there, for example. The phrase "out of world" is for system-level actions; it skips a great deal of the worldsim's calculations. "With past participle X" tells Inform to use that word in source code for the past participle ("has woven") instead of replacing the -ing suffix with the -ed suffix automatically. And finally, other types such as topic, time, time period, number, and various units may each only be used once in an action.
After an action is created, the action must be implemented with rules, and then the parser must be pointed to it via Understand statements. If the parser is not pointed at it, an action can still be invoked from the code, via "try" and "try silently" (or "silently try").
- try Bob donating the jeans;
- silently try donating the red Porsche; ["silently" means the Report rules are skipped. See next section.]
- try Bob accusing the player of theft;
- try teleporting to the tattoo parlor; [subject can be dropped when it's "the player" performing the action]
- try silently the current manager tattooing the back of the player;
For actions with two parameters, at least one word (such as a preposition) is needed between the parameters, and the word "it" tells Inform where the first parameter goes. This is useful for actions with multi-part names like "teleporting to it by way of" where the parameter would otherwise have many choices of where it could go.
Finally, an action invocation can be put into a variable of type "stored action" and invoked at a later time. The phrase "the action of" must precede the invocation in order to capture it.
- An abeyance is a stored action that varies.
- [...]; now the abeyance is the action of Bob examining the player;
- [...]; try the abeyance;
- [...]; now abeyance is the action of the current manager firing the noun;
Stored actions are easy to work with when only its nouns are toggled: just use person variables. But they are difficult to deal with when it is the action itself that we want to vary. Extensions have been written to compensate. A built-in To Decide function, "the current action", returns the currently-executing action as a stored action.
(Addendum: very occasionally, the compiler will have difficulty parsing an invocation that has a NPC as a subject. In these cases, add the word "trying" before the action: "Bob trying examining the player". It frequently happens in Tables with a Stored Action column.)
No Lights, No Camera, Just Action
Processing a character's action is a multi-step process, not including parsing. Each step is its own rulebook. In order, (and indenting the NPC-only rulebooks,) those rulebooks are:
- setting action variables for <action>
- Before
- Persuasion (NPCs only)
- Instead (default outcome is failure, so only one rule will typically execute)
- Check <action>
- Unsuccessful Attempt By (only when Instead or Check fails) (NPCs only)
- Carry out <action>
- After (default outcome is success, so only one rule will typically execute, and, will also skip the Report rules in that case)
- Report <action> (skipped if the action was invoked with "try silently")
Just to be clear, every action has its own Check, Carry out, and Report rulebook. The other rulebooks are shared among all actions. Each action will also have its own Setting Action Variables For rulebook, presuming it has one at all. If it isn't needed for a particular action, it will not exist.
Each rulebook has its purpose:
- Check rulebooks catch situations which should prevent an action from occurring; it enforces preconditions, essentially. If the Check rules prevent the player from doing something, it is responsible for narrating that result.
- Carry Out rulebooks simulate the action, updating any data and running any code necessary; it should not print anything lest "try silently" won't live up to its name.
- Report rulebooks then narrate the result.
- The sole Persuasion rulebook catches player commands of the form BOB, TAKE ROCK. It decides whether Bob will do as you ask him to. If a Persuasion rule succeeds, the NPC attempts the action. If it fails, it should narrate the NPC's negative response. A default Persuasion rule, last in the rulebook, will return failure.
- Unsuccessful Attempt By will run if the NPC obeyed but a Check or Instead rule (i.e., the physical world) stopped him. It too should narrate the failure. (The standard library Check rulebooks will check all characters, but only print a particular failure message for the player; the NPCs are sent here for a single, generic "is unable to do that" response from the last, default rule in the book.)
- Before happens between parsing and game reaction. It can also trigger on groups of actions.
- Instead is useful for blocking groups of actions.
- After, when adorned with a condition or three, is useful for triggering cutscenes.
- Setting Action Variables For is responsible for initializing variables that are local to the action, as opposed to variables local to an individual function, rule, or rulebook. This rulebook should do as little as possible. It should not print anything. It is rarely needed.
All rules have an arity from 1 to 3, corresponding to the subject, direct object, and indirect object of a simple English sentence. The subject is always present, because it is the character performing the action. But a rule may apply to a specific character, to all NPCs, to only the PC, or to everyone:
- Instead of Tiny jumping: say "Tiny is too overweight to jump. You all must find another way to help him across."
- Carry out an actor jumping: now the actor is on the nearby platform. ["an actor" applies to everyone]
- Report someone jumping: say "You see [the actor] jump over." ["someone" applies to any NPC]
- Report jumping: say "You jump over." [subject-less means the player]
(Games with multiple PCs should note that rules specific to one character (such as "Tiny jumping") will not fire if that person is the PC. Only the subject-less rules will fire.)
The "setting action variables" rulebook initializes its action's local variables by pulling values from global variables and object properties, including the parameters "the actor", "the noun" and "the second noun". (It is NOT a way to pass additional parameters to a rule.) The "matched as" parenthetical is optional, but it allows rules in the rest of the gauntlet to test those variables similarly to how it tests on the actual parameters. The standard Going <direction> action uses this to provide rule hooks like Going To <a room>, Going Through <a door>, Going By <a vehicle>, etc.
- Report going from the monkey village to the lost city's entrance by the hoverboard: say "You sail into the clearing easily this time, bypassing all those nasty monkeys that like to drop on top of you."
- The jumping action has an object called the obstacle (matched as "over"). [must be a single word!]
- Setting action variables for jumping:
- now the obstacle is the current blockage of the location. [pulls data from the "current blockage" property of a room; specifically, the room the PC is in at that moment]
- Instead of Stevie Burns jumping: say "Even little Stevie hops over [the obstacle]." [ Jumping takes no parameters beyond the performing actor ]
- Instead of Stevie Burns jumping over a fiery fallen beam: say "'Help!' says Stevie. You suddenly recall him telling you about the time his house burned down." [ we can now test on the local variable with an "over" phrase ]
Because the Before, After, and Instead rulebooks are shared among all actions, they may apply to entire categories of actions. A named group of actions is called a "kind of action", and they have no special declaration syntax beyond the word "is" sitting between the action's name and the kind-of-action's name. This encourages all manner of nouns, adjectives, and adverbs to name a kind-of-action, but some read better than others when used in rule headers. In all cases, an action must be defined before attempting to classify it.
- Whining is pointless behavior.
- Temporarily waiting is pointless behavior.
- Discussing is conversation.
- Accusing it of is conversation.
- Accusing it of is drama.
- Teleporting to is drama.
- Tabulating is acting like a frickin' accountant.
- Scheduling is acting like a frickin' accountant.
Kinds of actions can then be used in rule headers. (In the following examples, remember than "when" and "during" begin additional clauses that check global variables and whatnot.)
- After pointless behavior: say "But you still feel unfulfilled."
- Before conversation when the current interlocutor is not in the location: now the current interlocutor is a random person in the location.
- Instead of drama, say "(Now is the time to lay low!)"
- Instead of acting like a frickin' accountant during the collapsing bridge scene, say "You calculate (correctly) that you're about to become a victim of natural selection."
Any rulebook that can accept a kind-of-action can accept an explicit list of actions as well. Let's then call this list Feature #1, because of the many variations on it. There is a special kind-of-action, the noun phrase "doing something/anything", that matches all actions. (Feature #2). Optionally, it may [#3] be followed by an "except" / "other than" clause that lists actions. Then, optionally, we may [4] tack on an object Description to further constrain the rule. (The Description can [5] be preceded with "with/to" if we wish; it frequently improves readability.) And of course, we may [6] always add other conditions with "when" and "during", such as the "in the presence of Person" boolean function.
- Before an actor discussing [4] a spiffy thing [6] when in the presence of Mr Blackheart: [...].
- Instead of [2] doing anything [3] except waiting [6] when the player is paralyzed, say "(Uhh... can't... move...)"
- Instead of someone [2] doing anything [3] except taking, dropping, or burning [5] with [4] something incriminating, say "[The actor] says, 'No, I must get rid of [the noun]!'"
- After [1] examining, looking under, or searching [4] anything owned by Mr Blackheart [6] during a scene needing tension: say "Suddenly, Blackheart re-enters the room. 'What are you doing.' It wasn't a question."
Just to be clear, "doing something" is a single identifier. "Doing" is not an action, and its following "something" is not a parameter Description. But "something" is a parameter Description elsewhere, as in "examining something" or "accusing something of something".
Six caveats: One, when using "doing something", we must remember that the Looking action is what primarily prints text when we enter a place -- or re-prints it when we load the game, etc. -- so we frequently will want to allow it. Two, when using "doing something to/with/-- something", actions of low arity like Sleeping and Waiting won't be caught by it, because they never apply "to/with something". Three, the docs don't cover any syntax for constraints on "the second noun", but a "when" clause can be appended. Four, when explicitly listing several actions together, they must check the same arity. (So we can't combine "sleeping" with "examining something", but we can combine "sleeping" and "examining". The arity of the check, not of the action, is important.) Five, but we can combine them using a kind of action (see immediately below). And six: in all cases, the subject of the actions is kept completely separate from the actions' description; consider it already processed by the time the action group's description starts.
- Examining something is acting like a klutz. [not "an actor examining something"]
- Dropping someone is acting like a klutz. [notice the "someone"]
- Looking is acting like a klutz. [arity of the check is lesser than the other two]
- Before someone acting like a klutz: [...]. [works for any NPC examining a Thing, dropping a Person, or Looking]
Understanding Our Player, Our Parser
The player's parser is a fairly simplistic one, little changed over a decade of use, but it makes heavy use of callbacks for some surprising extensibility. These parser callbacks are called topics (meaning, they are of type Topic). Topics are simplistic regexes in the shape of a boolean function, and if they match, they "return" what object, unit, action, or value they find by setting global variables. A given topic must "return" only one particular type; more on that in a moment. We use Understand sentences to add these topics to the pre-existing list of other topics that the parser runs down. Here are some examples of using Understand to connect topics with actions:
- Understand "whine" as whining.
- Understand "donate [something]" as donating.
- Understand "give away [something]" as donating. [we're making a synonym here]
- Understand "discuss [text]" or "talk about [text]" as discussing. [ditto, but as one topic not two]
- Understand "tabulate [a number]" as tabulating.
- Understand "answer [truth state]" as answering.
- Understand "schedule [a time]" as scheduling.
- Understand "wait for [a time period]" as temporarily waiting.
- Understand "teleport to/-- [any room]" as teleporting to. [the altercation slash can't be used on the first word]
- Understand "accuse [someone] of committing/-- [any thing]" as accusing it of.
- Understand "[any thing] committed by/via [someone]" as accusing it of (with nouns reversed).
- Understand "wear [something preferably held]" as wearing.
- Understand "put [other things] in/inside/into [something]" as inserting it into.
- Understand "deposit [something] in/into [an open container]" as inserting it into.
- Understand "go to [any adjacent visited room]" as going by name.
- Understand "tattoo [limb] of [someone]" or "tattoo [specific limb] of [someone]" as tattooing it of.
The whole text between "Understand" and "As" is a topic, and the text in the square brackets is yet another topic called by the larger topic. The called topics start their parsing where the calling topic left off, trying to decide if the words that follow match itself.
The topic "any thing" -- as opposed to "thing" or "something" -- ignores the limitations of scope, and will match any valid object found in the whole game. The word "any" in general works like this, and must be echoed in the action definition by the word "visible". You can then guess what "any room" means. Other topics are intended for a particular type -- Time, Number, Units, named values (like Limb), undigested text (which the actions reference by "applying to one topic"), etc. One of the types, Description, is quite powerful: "an open container", "any adjacent visited room", and "something related by reversed containment" are examples of descriptions.
Understand can also expose other parts of our games to our players: property adjectives, synonyms for object names, and even other topics:
- Understand "dog" as Rover.
- Understand "birds" and "ruddy ducks" as the plural of duck.
- Understand "upper [limb]" or "lower [limb]" as "[specific limb]". [this won't capture the words Upper or Lower, but allows the player to use them ]
- Understand "beneath/under/by/near/beside/alongside/against" or "next to" or "in front of" as "[nearby]". [this is a convenience for the programmer only]
Any single topic can only return one specific type of something
- Understand "colour [a colour]" or "[something]" as "[tint]". [error: is this Topic's return type a Color or Thing?]
- A pot is a kind of thing. A pot can be broken or unbroken.
- Understand the unbroken property as referring to the pot. [or, "...the broken property..."]
- Understand "shattered" or "cracked" or "smashed" as broken.
- Understand "pristine" as unbroken.
- Understand the broken property as describing a flowerpot. [describing disallows "take broken"; player must provide the noun: "taken broken pot"]
- Understand "bottle of [something related by containment]" as a bottle.
- Understand "machine" as a device. [device is a class, so "machine" will narrow down the choices if the parser must make a guess]
Finally, if-conditions can be attached to any of these Understand statements, so the parser will ignore the line if its condition isn't met.
- Understand "rouge" as red when the make-up set is visible.
- Understand "Rover" as Rover The Dog when the player knows-about Rover.
- Understand "your" as a thing when the item described is held by the person asked.
But remember that this happens during parsing of the player's command, so the condition can't always refer to information within the command itself, such as "the noun" or "the second noun". In the case of "the person asked", it works for FRODO, GIVE ME YOUR RING because Frodo has been parsed already; but it may not work if "Your" is the very first word of the command.
If we just want a simple reply with no processing, there's a shorthand.
- Understand "xyzzy" as a mistake ("The machine doesn't seem to have a button with that label on it.") when in the teleportation chamber.
- Understand "xyzzy" as a mistake ("Ah, I see you're an old hand at this.").
Mistakes can use the [text] token but little else. They can use To Say functions in what they print, so it is possible to do some simple codework from them. Mistake lines are parsed before non-mistake lines, because they're intended to cover exceptions to grammar or one-off, out-of-game remarks by the player, rather than entire categories of input.
Time for a Scene
The Room class divides space into discrete places. Scenes divide an interactive fiction into durations of time. Each scene is implemented as a boolean variable that is automatically set & cleared by a condition. Scenes may happen again unless declared "non-recurring".
- A lightsaber duel is a [non-recurring?] scene.
- A lightsaber duel begins when the location of Luke is the location of Darth.
- A lightsaber duel ends when Luke is too injured to continue or Darth is too injured to continue.
- A person can be too injured to continue. A person is rarely too injured to continue.
Rule headers and imperative code both can check to see if a scene is happening.
- Every turn during a lightsaber duel: say "BWWAAUUAAAHH".
- [...]; if a lightsaber duel is happening, [...]
Each scene provides two rules that execute when the scene begins and ends. These are just hooks we may use or ignore.
- When a lightsaber duel begins: change the command prompt to the battle command prompt.
- When a lightsaber duel ends: change the command prompt to the normal command prompt.
Scenes are not objects, but we can add boolean properties to them in the same way.
- A scene can be thrilling or dull. Train Stop is dull.
- A scene has a text called cue speech. The cue speech of Train Stop is "All aboard!".
- Every turn during a dull scene: [...].
- [...]; if a thrilling scene is happening, [...]