I’ve been playing with this idea for a while now, it’s a sort of esoteric language geared toward authoring IF. The language itself looks similar to Inform7, but less magical. I’d love to get feedback on this thing from the IF community. I’ve tried posting this to various places on the internet, but without any response, so now I’m trying here.
Below is a description of the language, along with a reference implementation written in Lua, a set of core rules, and a port of Roger Firth’s Cloak of Darkness to the language.
The project is tentatively entitled “RIFE,” for “Rules-based Interactive Fiction Engine.”
First, here’s a writeup describing the language. It aims to be thorough, but not overwhelming. Let me know if anything is confusing or should be described in more detail.
A program is composed of statements and rules. Statements define the current state of the program, and rules describe state transformations which may occur as the program runs, when the appropriate conditions are met. State transformations are realized by creating new statements, or removing old ones, or both.
Each statement is represented as a tuple, composed of elements. There are two kinds of syntax for elements in a statement tuple: words are sequences of non-space characters, and
phrases are sequences of characters enclosed by double quotes. There is no difference between them other than the more flexible syntax for phrases, which may span multiple lines and include spaces.
Each statement tuple contains at least one element. Elements in a tuple are separated by space characters. Statement tuples are separated from one another either with line breaks, or with the
. character. A line containing statements must not have any leading white space.
Here’s an example of four statements:
player is in foyer. cloakroom is west of foyer. cloakroom is called "the cloakroom" player moves west
Each rule has two parts, a query part followed by a result part. The query and result parts each contain a list of tuples, similar to the tuples described above.
In addition to words and phrases, these tuples may contain variables:
Scalar variables look like regular words, prefixed with a
List variables also look like regular words, prefixed with a
Each tuple in the query part is terminated by either
Tuples terminated by
,are called reactants.
Tuples terminated by
;are called reagents.
Tuples terminated by
?are called catalysts.
The query part of a rule starts at the beginning of a line, and the result part has leading whitespace. The result part of a rule contains zero or more products. Each product looks similar to a statement, except that it must be indented and may contain variables. If a rule contains no products, a single dot should be written in the result part to indicate the end of the rule.
Here’s an example of a rule:
player moves $direction, player is in $place, $destination is $direction of $place? player is in $destination.
The example rule above matches the example statements described earlier. Here’s how it works.
Tuples in the query are matched in left-to-right order. The
player moves $direction tuple matches
player moves west, and the
$direction variable is bound to the word
player is in $place matches, leaving
$place bound to
The last tuple in the query is
$destination is $direction of $place. Since
$direction is now bound to
$place is bound to
foyer, this is equivalent to
$destination is west of foyer, which matches the
cloakroom is west of foyer statement, binding
Since every part of the query matched a statement, the rule is successfully applied.
List variables work in a similar way to scalar variables, but they can successfully match any number of elements (including zero). No more than one list variable is allowed per query tuple (that is, one per reactant, reagent, or catalyst).
Here’s an example of a rule containing a list variable:
you @act, player wants to @act.
This will match statements like
you look at message, or simply
Reactants and Catalysts
When a rule is successfully applied, all statements matching reactants (query parts terminated by
,) are removed from the program. Statements matching catalysts (query parts terminated by
?) remain in the in the program. The results of the rule are appended as new statements,
with any variables replaced by their bound values.
Use a catalyst to check for the existence of a statement without removing it. Use a reactant to remove the matched statement on successful rule application.
Continuing with our example, applying the rule results in the removal of the
player moves west and
player is in foyer statements, and the addition of a new
player is in cloakroom statement.
Multiple variables may exist in a single element of a result; all of them will be replaced with their bound values. This is typically used to build dynamic phrases, but is also allowed in word elements.
Reagents (query parts terminated by
;) are similar to reactants, except that when a rule containing reagents is successfully applied, the rule is immediately applied again repeatedly, until it can no longer match anything new.
Statements matching query parts containing reagents are not removed until after these repeated applications of the rule. When used together with catalysts, reagents are suitable for iterating over multiple similar statements.
Here’s an example of a rule containing a reagent:
$somebody tries to drop all; $somebody has $item? $somebody wants to drop $item.
Bob tries to drop all,
Bob has apple, and
Bob has banana
are all existing statements. When the rule is applied,
Bob wants to drop apple and
Bob wants to drop banana are created,
Bob tries to drop all is removed.
It’s possible to assign a default value to a scalar variable. Consider the following rules:
game ends because you $left, turn count is $turns, your score is $score, say "You $left the game after $turns turns with a final score of $score." issue game command quit game ends because you $left, turn count is $turns, say "You $left the game after $turns turns with a final score of 0." issue game command quit game ends because you $left, your score is $score, say "You $left the game after 0 turns with a final score of $score." issue game command quit game ends because you $left, say "You $left the game after 0 turns with a final score of 0." issue game command quit
It’s possible that no statements exist matching
turn count is $turns or
your score is $score, so we need four rules to handle all possible combinations. If we needed to test for a third statement that may not exist, the number of rules would double.
To handle this, variables can be given a default value by placing the value after the variable name, separated by a
Here’s the previous example, using a single rule:
game ends because you $left, turn count is $turns|0, your score is $score|0, say "You $left the game after $turns turns with a final score of $score." issue game command quit
A default value should only be placed on the first appearance of a variable in a rule. When a part of a rule’s query fails to match anything, if it contains variables with default values, the default values are bound to those variables and that part of the match is considered successful.
Normally, a variable appearing in a product of a rule expands to whatever value it is bound to. This behavior can be modified with some special syntax:
$var#expands to the decimal representation of the number of characters in the string bound to
$var. This can be used for unary to decimal conversion.
$var#2expands to the second character of the string bound to
$var+2expands to all characters from the second character in
$var; it trims off the first character.
$varis bound to a value representing a positive integer expands to a string containing that number of “c” characters. This can be used for decimal to unary conversion.
A program executes from within a host application. The host application is responsible for loading and running the program. Typically the host application will load a program once, and then run it many times in a loop.
When the program loads, all statements and rules are parsed. These are stored in two separate lists, in the order they appear in the source file. When the program runs, each rule is tested against each statement. When a rule matches a statement successfully, it is applied. If the rule contains no reagents, then application is simple. Statements which matched reactants are removed, and products (result parts) of the rule are placed at the end of the list of statements.
If the rule contains any reagents:
Any statements matching catalysts are removed from their current positions, and placed at the end of the list of statements.
The products of the rule are placed at the end of the list of statements.
Another match is attempted with the same rule. If it matches something new, go back to step 1. Otherwise, continue to step 4.
Finally, remove any statements matching the rule’s reagents.
Note that in step 3, “matches something new” means that the rule matches something different from the statements matched with catalysts on the first iteration of the current rule application.
This process is repeated for each rule until no more rules can be applied.
At this point, control is returned to the host application, which will typically extract a few statements, output something based on those statements, poll for user input, create new statements from the input, and run the program again.
To try the game out, grab a copy of Lua or LuaJIT, save all four files using the indicated filenames, and run
lua rife.lua in your terminal. I’ve hit the character limit here, so I’ll post the files next.
I feel like this language is close to being very useful for general IF authoring, but still has some rough edges. My goal is to bang out any dents in the language design first, then improve the interpreter (writing a “proper” parser, giving more useful error messages for syntactically incorrect code, look at ways to improve performance, and so on). I would really appreciate any feedback from more experienced IF authors!