As work slowed down for the summer, and inspired by the thriving interactive fiction (IF) community detailed in last month’s newsletter, I decided to spend more time on interactive fiction (IF) tools, so that’s what this newsletter is about. In one sense, its a specialized application area, but the issues with IF tools apply to other areas of AI as well, such as:
pitfalls with “intelligent” interfaces; when to use rules versus when to use objects; and how to augment logic for a particular application area. The IF tools developed for this newsletter are available in the download area of www.ainewsletter.com. As always, any and all feedback is welcome.
Intelligent and Deceptive User Interfaces
The Design of Everyday Things by Donald A. Norman is a fantastic book that provides insight into the user interfaces of all sorts of things. He talks about doors, phones, light switches and the like, but there is a lot to be learned from him that applies to software as well.
A key concept in user interface is that the interface lead the user to a good conceptual model of the thing. For example, if you approach a door that has a handle you can grab, then you form a mental image of a door you can pull, and you pull. If it was a push door, then you pull in vain, feel like a fool, before pushing the door open.
On the other hand, a metal plate clearly indicates a door that can be pushed. But if that plate runs across the whole door, then you don’t get a good mental image of which way the door opens, and you might wind up pushing on the hinge side.
We often run into doors that take a couple of tries to open because of bad interface design.
I recently took a shower in a house that had a fiendishly clever way of changing the water from the spigot to the shower head. Instead of an ugly lever on top of the spigot, the actual bottom of the spigot head could be rotated. It was a startling clean, very beautiful design. But to someone, like myself, first encountering it, I was left baffled for the longest time wondering how to get the shower to go.
The interface was deliberately deceptive, hiding the ugly workings by making the shower/spigot toggle lever look like something else. Pretty, but very frustrating to the first time user.
Taking this to software then, a goal in user interface design should be to lead the user to a good understanding of how the particular software works. Interfaces that lead the user away from that understanding are deceptive and frustrating, particularly to first time users.
The reason is we, as people, act based on our mental models. When we get a mental model of something, such as software, that is incorrect, we are led to try things which won’t work, and wind up frustrated. On the other hand, when an interface leads to a good mental model, and we try things that work, it is satisfying, and leads us to continue to explore the software further, which, with a good interface leads to an even better understanding, etc. etc.
While pondering a good example, I deleted some text in this article, and in addition to deleting the text, the tags of an adjacent heading changed as well. The delete key is a deceptive interface in this editor. It leads one to believe it deletes, yet it also changes adjacent HTML tags as well. I can’t honestly say as I have a good mental model of exactly when and why that happens.
Which is why notepad and other pure text editors are often the editors of choice for HTML. While it is a pain to have to manually type all the tags, it is a user interface that much more clearly leads the user to a good conceptual model of how HTML works.
An HTML editor is deceptive in that it hides the ugly tags. Yet the insertion point is often ambiguous leading to strange behavior that is frustrating. It seems most good HTML tools, like Dream Weaver which I’m using, also provide the ability to edit the tagged text directly. This will be necessary up until the the day when they finally work out an interface that intuitively exposes all of the power of HTML.
Rule-based languages have a similar problem. They present an interface, the rule syntax, which makes them appear as declarative chunks of knowledge. But how does the rule engine choose which rules to fire when? Without this knowledge a developer cannot effectively write rules, yet there is usually nothing in the syntax of the rules that even hints at that information.
The LEX and MIA strategies of OPS5 production (forward-chaining) rules are a perfect example. The designers wanted to have the rules fire in the most intuitive manner possible, so they coded complex algorithms that decided which rule to pick when many could fire. One strategy opted for rules with conditions referring to recently updated facts; the other opted for rules with greatest “specicifity”, meaning the most specific rule of those available would be chosen.
This was clever, but led to frustration as a developer’s rules didn’t execute as expected. Only after getting a full understanding of how LEX or MIA works could a programmer effectively use OPS5. In other words, the interface was deceptive in that it made it appear as if the rules were executing in an intuitive manner, but as soon as they didn’t, the developer was left scratching his/her head in confusion.
KBMS was another production rule system inspired by OPS5, but it had an interface that better lead a developer to understanding its conflict resolution strategies. The first rules fired before the latter rules if there was a conflict. In other words, what you saw was what you got. And, that order could be overridden by specifying priorities on rules. While not as elegant as LEX or MIA, it was a lot easier for developers to understand.
Natural Language and Interactive Fiction (IF)
And finally to IF, which is what this newsletter is about. Many people criticize IF because it is frustrating to play because you wind up thrashing about trying things that don’t seem to have any effect. I believe this is because the classic IF user interface is deliberately deceptive, and those that enjoy playing IF have developed a good understanding of how IF works despite the interface.
The underlying data model for an IF game is a collection of information about the people, places and things that are important in the game. This might include where the player is at, whether a room is lit or not, and what items a player is carrying.
And there are, internally, formal commands that manipulate that data. For example “put cloak on hook” will change the data model so the cloak, which was associated with the player, is now associated with the hook.
Now what makes a game fun is the puzzles. These are bits of the game that don’t work in a standard manner, and the player needs to figure out some sequence of moves that will solve the puzzle. For example, in the very simple demonstration game, Cloak of Darkness, the barroom is dark and the reason is the cloak absorbs light.
When the player gets to the barroom, then the game informs him/her it is dark. OK. So the player naturally tries to turn on the light, but is told that’s not a good idea. Hmmmm. This is where the problem arises. Is the player failing to turn on the light because of a misunderstanding of the game’s vocabulary and commands? Or is this the meat of the puzzle?
There is nothing in the user interface of clever text to give the player even a clue. The experienced gamer might know what sorts of things should and shouldn’t work, and will quickly move on. But the newbie?
This is often discussed on the IF discussion groups as the guess-the-verb problem. Numerous suggestions for menus and the like are often shot down because of a fear the menus will take away from the exploratory nature of the game, and also create a boring click-on-all-alternatives approach to solving the game.
Another example of the deliberately deceptive appears in one implementation of Cloak of Darkness where the hook in the cloak room, where you are supposed to hang the cloak, is described in the main body of text of the cloak room. In other words, it is not indicated as being an object in the room that the player can manipulate, but rather appears like part of the general description.
This too is deliberately deceptive, forcing the player to guess at which of the text in the description is meaningful for the game and which isn’t.
It seems to me that IF would be better off if the games kept all of the wonderful text and descriptions output to the player, but also provided solid information about the internal game model and how the player might manipulate it. The idea would be to give the player the required tools for playing the game, but not the solution to the puzzle.
In reality, this is exactly the information that experienced IF games carry in their heads. It’s just unavailable to the new player. See the code corner for some possible solutions.
Rules vs Objects
Object-oriented (OO) programming was a major breakthrough in software development, allowing a developer to easily keep data, and the procedures that work on that data, together. This was a more natural approach than older programming styles that forced a programmer to keep data and procedures separate.
But there are times when you would rather keep the data and procedures separate. Typically this occurs when you have operations that work on two or more objects that are peers. In other words, the operation is happening at a higher conceptual level than any of the objects.
The ugliness associated with C = A + B in a pure OO language reveals this weak knee. Because A and B are objects, you can’t simply add them. You need to send the + message to one of them or the other.
In other words, the developer must make an arbitrary decision as to whether the addition will happen in the A object or the B one. And when that happens, the code loses the clarity of the idea that the addition is actually happening at a higher conceptual level than in either of the two objects.
Rules are often used to express relationships between different data elements in an application. They, like math operations, express concepts that do not rightly belong in any one data element or another. In this sense, they have a lot in common with database queries, where a query ties together data from different tables in a database.
Because a rule relates two or more data structures, it is most naturally represented as something external to both. In an OO application, however, the rule must be shoehorned into one object or the other. As with any procedural approach, if there are a large number of rules, this quickly causes the logic of the rules to be lost in arbitrary procedural decisions.
A pricing application, for example, is difficult to write procedurally because a price is related to many different data tables. It is easier to code a pricing application with a set of rules that are, in a sense, above the data structures and objects of the application, and which are triggered by patterns and interrelationships between those data structures and objects.
Coding IF presents a similar problem. The commands of the game, in general, change the relationships between the objects describing the game. The puzzles of the game invariably are defined in terms of relationships between the objects of the game.
Consider a command to turn on the light in the room. Whether or not this is possible requires data from the room object, the light object, and the player object which invariably has something to do with these puzzles.
In an OO game implementation the developer must artificially decide if that logic should that be associated with a light source, or the room, or the player. In a rule-based implementation, the logic is more clearly expressed externally to the various elements.
So objects are great for representing the data structures of a game, but the commands and puzzles are more naturally represented as rules.
In the case of IF, there is another advantage to having the rules represent the actions and separate from the data. It makes it very easy to implement meta rules that can provide a mechanism of hints to help steer the player into a good conceptual model of how the game works.
Application Specific Logicbases
Logic is often a good way to express rules, especially if the rules are crisp, that is, without uncertainty or fuzziness. But it is rare that the required knowledge all fits neatly into logical relationships. There’s always some ugly bits that aren’t expressed cleanly using logic. (Sigh, just like OO isn’t right for everything, neither is logic.)
In order to get the benefits of logic for an application, it is a good idea to create a library of helper, or application-specific predicates that carry out the work that is not easily expressed in logical relationships. The implementation of these predicates can be hidden, so that the knowledge engineers only need to work with the crisp, cleaner portions of the rules.
An example of this was the vaccination system mentioned a few issues back. In that application, application-specific predicates that handled date arithmetic and managed information gathering were hidden, so that the vaccination rules themselves could simply refer to them, checking, for example, if the second measles vaccination was after the second birthday or not.
The same approach can be used to create a tool for developing interactive fiction.
Code Corner - A Logicbase Approach to Interactive Fiction (IF)
This month’s code corner serves as initial documentation for the interactive fiction tool set and examples that are available for download from the newsletter downloads.
Logicbase refers to the subset of logic programming that is most closely related to relational database. The facts/data in a logicbase are similar to relational data records; and the rules in a logicbase are similar to database queries. The tool for developing IF described here takes advantage of these attributes. The state of the game is represented in logical data structures, and logical relationships are used to describe the permissible actions.
There is a tremendous advantage in the logicbase approach, in that it offers, like other rule-based technologies, up to a 10-1 productivity gain for the right sort of application. And the rules that govern IF are the right sort of application.
This means that it is practical to create new and different sets of rules for governing the manipulation of objects and the movements of players in a game. It is not necessary to use a tool that has hard-coded into it all of the sorts of features one might like, such as support for lights, burnable objects, weights, tunneling, time travel, or whatever, because it is easy to add the features one needs as needed.
There were some other design goals in this system:
- Separate the text from the game logic, so translations to other national languages are easily supported.
- Separate the allowed input vocabulary and grammar from the game logic, again to support different national languages.
- Create a verb/action oriented coding environment that clearly brings out the logic of the commands.
- Use a simple rule language that makes it easy to model any sort of game environment, allowing easy experimentation with game models that are different from the conventional IF ones.
- Take advantage of the verb/action oriented coding to support context-sensitive hints and help that lead the player to a good understanding of the game mechanics without revealing the solutions to puzzles.
- Use the input vocabulary and grammar to create the short form descriptions of items in the game, which ensures that the player is presented with text tokens that can be directly reused in commands.
The basic flow of control of the game is:
- Player enters natural language like command in a supported national language.
- The grammar rules are used to parse the input to a formal command using the internal tokens of the game.
- The formal command is passed to the game engine
- The rules of the game engine are used to manipulate the data structures of the game.
- Output tokens are generated as appropriate.
- The national language text associated with the output tokens is presented to the player.
The player might enter: “hang the velvet cloak on the brass hook”.
This gets parsed to the command: [put_on, cloak, hook] which is passed to the game engine.
The game engine then calls the rule: put_on(cloak,hook).
The pattern rule which fires might be: put_on(X,Y) :- delete( me, [wearing = X] ), add( Y, [contains = X] ), output(put_on(X,Y)). Where X and Y are variables that are bound to cloak and hook for the example.
The output token put_on(X,Y) might have the English text pattern:
The , X,
is now on the, Y,
To create a game then requires the creation of two modules. One is the game logic, containing the commands and the mapping between commands and the rules of the game. The other is the national language module that has both the input vocabulary and grammar, and the text for the various output tokens. However, the national language module can be added later, as the game can be played using just the internal tokens during development.
The Data Model
Frames are a subset of objects, without the procedures, that provide a very flexible representation mechanism for the state of a game. Frames are not a standard part of logicbase, so support for frames becomes part of the set of application-specific support predicates.
A frame is represented by a name, and a list of slots and values. Rather than manipulate the frame structures directly, a set of predicates are provided to manipulate them:
make( Name, Slots ) - create a new frame of the given name with the specified slots. query( Name, QueryPatterns ) - find values for slots delete( Name, DeletePatterns ) - delete certain slot values add( Name, AddPatterns ) - add certain slot values change( Name, ChangePatterns ) - change certain values.
For example, to create a frame to represent a place:
make( duck_pen, [gate=close, contains=egg, contains=ducks, connects=yard] ).
To ask if it contains an egg:
query( duck_pen, [contains=egg] ).
To ask where it connects (tokens beginning with upper case are variables) the query
query( duck_pen, [connects=X] )
will bind X with ‘yard’.
To change the gate status to open:
change( duck_pen, [gate=open] ).
The Game Rules
The rules are written using the logicbase syntax of Prolog. The basic format is:
RulePattern :- Goal1, Goal2, … GoalN.
- RulePattern has the name of the rule and its arguments;
- :- is read as if;
- The commas separating the list of goals are read as and; and
- The goals are other query patterns.
Here’s an example initialization rule for a game that creates:
a game frame for keeping track of the number of moves;
two frames describing places in the game and their contents and relationships; and
a frame for player, called you, specifying the initial location.
initialize :- clear, make( game, [ moves=0 ] ), make( duck_pen, [connects=yard, contains=egg, contains=ducks] ), make( yard, [connects=duck_pen] ), make( you, [at=yard] ).
Two commands in the game engine that manipulate these structures might be go and take. These commands are implemented as rules. Variables, indicated by initial upper case letters, make the rules general.
go(Place) :- change( you, [at=Place] ), output( now_at(Place) ), !. take(Thing) :- query( you, [at=Here] ), delete( Here, [contains=Thing] ), add( you, [carrying=Thing] ), output( now_have(Thing) ).
A table of actions maps commands, represented as lists of tokens, to the rules. So far the two rules these would be:
action( [go, Place], go(Place) ). action( [take, Thing], take(Thing) ).
The application-specific supporting predicates allow the game to be run even if there are no national language text files. When supporting text is missing, the token is written as-is; and when the input vocabulary is missing, the internal command is created directly from the input tokens.
So, with just the code we’ve written so far, the game can be played:
> go duck_pen now_at(duck_pen) > take egg now_have(egg) > go yard now_at(yard) > go outer_space now_at(outer_space)
Well it works, but it lets us go into outer space, when that isn’t really somewhere we should be able to go. Additional rules are added to define the constraints and put out appropriate messages. The rules are considered in the order they are written.
The exclamation mark at the end of each rule tells the game engine not to consider any other rules once this one has completed.
So in the following example, the first rule will succeed if it is NOT true that there is a connection. Notice that it just puts out a message and does not change the state of the game. But, if the first rule fails, meaning there IS a connection, then the second rule will be used which does change the state of the game to reflect the move.
go(Place) :- query( you, [at=Here] ), not query( Here, [connects=Place] ), output( cannot_go(Place) ), !. go(Place) :- change( you, [at=Place] ), output( now_at(Place) ), !.
Now the game keeps us in line:
> go outer_space cannot_go(outer_space) > go duck_pen now_at(duck_pen)
The rules for go can then be further modified to support other constraints that we might want to model in the game. For example, if we wanted to add the idea of a gate that needs to be opened, the initialization and rules would change like this:
initialize :- make( duck_pen, [connects=yard, contains=egg, contains=ducks, gate=closed] ), make( yard, [connects=duck_pen] ), make( you, [at=yard] ). go(Place) :- query( you, [at=Here] ), not query( Here, [connects=Place] ), output( cannot_go(Place) ), !. go(Place) :- query( Place, [gate=closed] ), output( gate_closed ), !. go(Place) :- change( you, [at=Place] ), output( now_at(Place) ), !.
Of course, now we have to add a command to let the player open the gate:
open_passage(Passage) :- query( you, [at=Here] ), query( Here, [connects=Place, Passage=closed] ), change( Place, [Passage=open] ), output( opened ), !. open_passage(Passage) :- output( cant_open(Passage) ), !.
We use open_passage since open is a reserved word, but this doesn’t effect the player because we map the input commands to the rule with:
action( [open, X], open_passage(X) ).
Compass navigation digression
Notice that this example implements a navigation mechanism based on connections between places, which the player will use with commands like “go to the duck pen”. This is a bit different from the normal IF navigation mechanism that uses compass directions, where the player navigates with commands like “go west”.
We could have implemented compass navigation just as easily:
initialize :- make ( duckpen, [east = yard, contains = egg, contains = ducks] ), make( yard, [west = duckpen] ) make (you, [at = yard] ). go(Direction) :- query( you, [at = Here]), not query( Here, [ Direction = _ ] ), output( cannot_go ), !. go(Direction) :- query( you, [at = Here] ), query( Here, [ Direction = NewPlace] ), change( you, [at = NewPlace] ).
Now “go west” will get the player from the yard to the duck pen.
While sometimes compass navigation leads to interesting puzzles, for the most part, in my opinion, it simply adds a level of overhead in moving about the game, so I prefer the direct navigation. But this is the key point.
Because it is so easy to create the game mechanics, the author can implement whatever mechanics make the most sense to him/her.
So I like connections, someone else likes north/south, another might support both, and yet another might use X,Y,Z coordinates and let the player turn, step, etc. An X,Y,Z coordinate game model would allow support for interesting game options like tunneling when two places are geometrically close, or allowing explosives to open up new connections.
An X,Y,Z model would also make more sense if the game engine was used to support graphics instead of text.
Now a problem with this flexibility in game mechanics is that, using navigation as an example, an experienced IF player might expect navigation to be by compass direction. But if there were good hints and help, then the mechanics of a particular game would be much more transparent, and the author would not have to rely on the player having in depth experience with IF games and conventions.
The actual output text is stored in a separate module, usually in a separate file. It is a collection of logical relations between the internal tokens and text to be displayed. The text can be a simple string, or a list (denoted by square brackets) of strings and variables (initial upper case letter) used to dynamically construct the output text. Strings are denoted by back quotes and can contain anything. Here’s some examples for the game so far:
text( gate_closed, `The gate is closed.` ). text( cannot_go(Place), [`You can't get to the `, Place, ` from here.`] ). text( now_at(Place), [`You are now at the `, Place, `.`] ). text(now_have(X), [`You are now carrying the `, X, `.`] ).
The support for text is part of the application-specific extensions for the IF authoring tool. The ‘output’ predicate does the dirty work of finding the right national language file, if any, and doing the text substitution, if required, using for the variables the input name if it can be found, or the internal name if not.
The internal name is the name that is used in the game logic, such as duck_pen. It never contains spaces and always begins with a lower case letter.
The input name of a game token is the first choice of text that the player can use to refer to the item. It is derived directly from the input vocabulary. This is a key point. It means that when the game outputs a message about an item, it uses the exact wording that the player can then use to refer to that item.
So, the vocabulary for duck_pen might have “duck pen” as the first of multiple allowed input phrases, in which case it will be what is displayed. So the message now_at(duck_pen) will expand to: “You are now at the duck pen”.
In the Spanish version of the game (excuse my weak Spanish), the input vocabulary for duck_pen might be “el corral por patos” (Is corral right for ducks?) and the text for now_at(X) would be:
text( now_at(Place), [`Ahora esta a `, Place, `.`]).
so the message comes out: “Ahora esta a el corral por patos”.
For input, you define both the grammar rules and the vocabulary. Here’s the grammar rule for commands with either a verb and an object, or just a verb. It allows for an optional article as well, being ‘the’ or ‘a’. These basic rules would be changed for languages that had different verb, object orders.
player( [V, O] ) --> verb(V), article, object(O). player( [V] ) --> verb(V).
The verbs and objects are defined with the actual words. So the player can input either “take” or “pick up” to invoke the verb take.
verb(go) --> [go]. verb(go) --> [go, to]. verb(take) --> [take]. verb(take) --> [pick, up]. object(O) --> thing(O). object(O) --> place(O). thing(egg) --> [duck,egg]. thing(egg) --> [egg]. thing(ducks) --> [small, flock, of, ducks]. thing(ducks) --> [ducks]. thing(gate) --> [gate]. place(duck_pen) --> [duck, pen]. place(duck_pen) --> [pen]. place(yard) --> [yard].
And finally the articles, which are just ignored:
article --> [the]. article --> [a]. article --> .
The grammar can be further modified to make it easier for the player. For example, supposed we wanted to the let the player invoke the go verb by just entering a place. By making a distinction between things and places, as we did, this is possible by simply adding another grammar rule:
player( [go, P] ) --> place(P). player( [V, O] ) --> verb(V), article, object(O). player( [V] ) --> verb(V).
Playing the game with the new inputs and outputs from the previous section:
> go to the duck pen The gate is closed. > open gate opened > pen You are now at the duck pen. > pick up the egg You are now carrying the duck egg
Notice how in each case the output uses the phrases “duck pen” and “duck egg”, no matter what was input. These are the first choices for input phrases for the internal names duck_pen and egg. This interaction makes it much less likely that the player will stumble about using the wrong words to refer to a game element.
A Spanish version of the program might have these as part of the input vocabulary:
verb(go) --> [vaya, a]. verb(take) --> [levante]. verb(take) --> [toma]. thing(egg) --> [huevo, de, pato]. thing(egg) --> [huevo].
And a play would look like:
> vaya a el corral por patos Ahora esta a el corral por patos. > levante el huevo de pato Tiene el huevo de pato.
User Friendly Information
IF games always provide some tools for examining the surroundings. Here’s the step-by-step way to add a command to the game, in this case look. It will display information about the place, its connections and contents. It will use the input names for the game elements so the player can easily use them in following commands.
1 Add the new verb and vocabulary to the inputs:
`verb(look) --> [look].`
2 Map the new command to a rule:
`action( [look], look ).`
3 Implement the rule. There are a number of points to note here.
- We keep wanting to know where ‘here’ is, so we add a helper rule that other rules can use to make that easier.
- The application-specific predicates include get_input_text and get_output_text which can be used to more precisely control the output, in this case displaying both the input name and output text for a place.
- Fail is a goal that fails. It causes the rule engine to backtrack, looking for other solutions. In this case that behavior is used to force the queries for connects= and contains= to find all the connections and contents.
here(H) :- query( you, [at=H] ). look :- here(H), look(H). look(Place) :- get_input_text(Place, ShortText), get_output_text(Place, LongText), output( [ShortText, `: `, LongText] ), look_connections(Place), look_items(Place), !. look_connections(Place) :- query( Place, [connects=X] ), output_nospace( can_go(X) ), fail. look_connections(_). look_items(Place) :- query( Place, [contains=X] ), output_nospace( can_see(X) ), fail. look_items(_).
Add the new required output text.Playing the game it looks like this:
text(yard, `A yard with grass and dandelions about.` ). text(duck_pen, `A smelly noisy pen for ducks.` ). text(can_go(X), [`You can go to the: `, X] ). text(can_see(X), [`You can see a: `, X] ).
look yard: A yard with grass and dandelions about.
You can go to the: duck pen
duck pen The gate is closed.
Open gate opened
go to the pen You are now at the duck pen.
look duck pen: A smelly noisy pen for ducks.
You can go to the: yard You can see a: duck egg You can see a: small flock of ducks
take the small flock of ducks You are now carrying the small flock of ducks.
OK, maybe we shouldn’t let the player take the ducks. Special handling and the light touch of many IF games, which makes them so entertaining, can be easily added. We also add another useful helper rule to determine if a thing is ‘here’ or not. It’s here if either the player is carrying it, or its contained at the place where the player is. The text for duck_problems will contain some wise remarks about the problems with trying to carry a bunch of ducks.
is_here(X) :- query(you, [carrying=X] ). is_here(X) :- here(H), query(H, [contains=X] ). take(ducks) :- is_here(ducks), output( duck_problems ), !. take(Thing) :- here(H), delete( H, [contains=Thing] ), add( you, [carrying=Thing] ), output( now_have(Thing) ), !. take(Thing) :- output( not_here(Thing) ), !.
Help, Context-Sensitive Hints, and Options
It’s easy to add a help command that outputs the basics of the game. This should describe how to use the standard commands of the game, such as look. It should also suggest that if the player gets stuck, or is learning their way around the game, then hints and options will help.
text(help, [`The basic commands are:\n`, ` look - look about\n`, ` hint - get possible verbs\n`, ` options - get all options\n`, ` quit - quit the game\n` ]).
Implementing context-sensitive hints is relatively straight forward. First we define a rule called option that for each command defines when that command is an option. It is purely up the game designer on how tight or loose to make these options which will be shown to the player if he/she asks. For the sample game we might define the options for the commands like this:
- go - is an option when there is a connection to somewhere, but we don’t check for gates or other obstacles.
- take - is an option when there is something contained in the place the player is at, but we don’t check for things that might not be able to be taken.
- drop - is an option if the player is carrying something.
- open - is an option if there is something that can be opened.
Here’s the code that implements these rules:
option( [go, X] ) :- here(H), query( H, [connects=X] ). option( [take, X] ) :- here(H), query( H, [contains=X] ). option( [drop, X] ) :- query( you, [carrying=X] ). option( [open, X] ) :- here(H), query( H, [connects=Y] ), query( Y, [X=closed] ).
A rule called options can then be implemented that walks through all the possible options. Because we didn’t use the exclamation point to tell the rule engine to stop after finding a solution, the fail in options will cause the rule engine to backtrack through all the options.
options :- option(Command), get_input_text(Command, Text), output_nospace(Text), fail. Options :- output_nospace(``).
Hints are a little trickier. For hints we don’t want to lay out all the options, but just the verbs that are in play, and it would be nice to have a more condensed sort of output. To do this we use the syntax [V|_] which lets us just pick up the first token of a list, and some more application-specific extensions to find all of the verbs and their text first, then remove the duplicates and display the list on one line.
hint :- findall(T, ( option( [V|_] ), get_input_text(V, T) ), Verbs ), remove_dups(Verbs, VerbsNoDups), output(verbs_to_try(VerbsNoDups)).
We can now run the game and watch how the hints and options change with the situation.
> help The basic commands are: look - look about hint - get possible actions options - get all options quit - quit the game > hint Try one of these actions: go to, open > options go to duck pen open gate > open gate opened > go pen You are now at the duck pen. > Hint Try one of these actions: go to, take > options go to yard take duck egg take small flock of ducks > take egg You are now carrying the duck egg. > Options go to yard take small flock of ducks drop duck egg > hint Try one of these actions: go to, take, drop
This facility makes it easy to create a game that a player can learn as he/she plays. The hints and options are voluntary, but they could be very useful for the first few moves in a new game. The player would very quickly get the hang of things and be able to start playing. They wouldn’t be needed again until the player got stuck.
Note that the options as coded here would not give away the solutions to any of the puzzles often found in IF. They simply lay out the possible moves at a point and place in the game. They don’t suggest the complex sequence of moves that might be necessary to solve some puzzle.
They can also be deliberately misleading, suggesting actions that lead to the funny messages at the various dead ends. For example, in the sample game the option to take the ducks is clearly presented for the player to try. But, and this is the main point, they steer the player away from thrashing about trying to figure out what’s possible and what’s not.
The application-specific predicates provide other services for the game as well, such as save and restore (they just write all the frames out to a file and/or read them back).
Other mechanics, such as keeping score and counting moves, and using different displays based on whether a place has been visited or not, are all easily coded into the game as well. They are, like everything else, just tokens in the frames that are manipulated by the rules.
Here’s a general game frame that can be used to keep score:
make( game, [ moves=0, score = 0 ] )
This is the entry point to the game logic that is called for each move. It can be used to implement moves by non-playing characters and the like. For example, the ducks might move about on each turn. Here we just keep track of the moves:
tick(Command) :- take_action(Command), change( game, [moves+1] ).
We could similarly increment the score after certain key moves, such as if the player takes the duck egg.
In any case, the game continues to evolve, limited only by the imagination and creativity of the author.
Graphical interfaces are nice, and the tool described here comes with the option to run in a graphical environment.
The help and language menus are driven from the game, and the names of the menu items are driven from the national language module used for the game. The player can switch back and forth from one language to another.
A beta version of this tool for developing IF is available in the download section of the newsletter archives. It contains two small sample games, one is Duck World which is similar to what was discussed here but with non-playing characters, and the other is Cloak of Darkness, and equally trivial game designed to be used to compare various IF authoring tools.
Any and all comments are welcome.
Until next month.