For the tutorial we will create a new game, called Portals. It will explore a navigation scheme based on the portals between places. Places are connected to portals, rather than each other, and to get to another place the player issues a command to go through that portal.
The strategy for creating any new game is to copy the template game provided with the system, and modify it to create a new game. (If there was a wizard, the template game is what it would create.)
You can also copy and modify one of the other more developed games and use it as a starting point instead. This would allow you to take advantage of whatever logic that game contains that you might want to reuse.
Tactically, there are three basic approaches to developing a new game:
The tutorial will provide details on the first two development methods, but for most of the tutorial it doesn't matter which one you use.
With a text editor, you start with the template files in the AIFT directory.
Edit portals.bat so that it's one line of code is:
amzi_if portals portals_english
Click on portals.bat and make sure a game that does nothing appears.
In portals.pro, find these two lines (not necessarily together in the file):
title_version(`Template`, 1) file_extension(`.tmp`)
and change them to:
title_version(`Portals`, 1). file_extension(`.ptl`).
This will give the new game a title and an extension to use when a game is saved.
Test portals.bat again, and see if the title has changed to Portals.
From here continue to use your text editor make the changes as described in the rest of the tutorial.
Eclipse is a very powerful, feature-rich, development environment that takes a little getting used to, but once you understand it, it provides a wealth of features that make development easier. The most confusing part is often just getting the projects set up right and, unfortunately, that's the first thing that must be done.
The plan is to create three projects. Two are imported projects that already exist in the sources. These are the libraries that support the games, called if_src, and the blank game template, called if_template. Your games will use the shared files in if_src, and often be started by making a copy of if_template.
These are the steps to implement the plan.
File/Import - select the directory aift/src. It should create a project for you called if_src. This project has the shared resources used by all games.
File/Import - select the directory aift/template. It should create a project for you called if_template. It has the template game that can be copied to create new games.
Select the if_template project and right click. Select copy from the pop-up menu.
Select paste and then put the copy of the project into a folder in aift/games, called if_portals.
Right click on the if_portals project and select project properties at the bottom.
In Build/Executable libraries make sure the list library is checked and the LSX atcltk is checked.
In Build/Executable libraries change the name of the .xpl file to be if_portals.xpl.
In Project/References click in the box next to the if_src project.
Close the properties window.
Select the if_tutorial project. Then from the main menu select run/run as interpreted project.
A Prolog listener window will open at the bottom, with a ?- prompt.
Type main. (The ending period is required at the ?- prompt.)
The GUI for the game should start. Close it with the window close icon.
Type quit. at the ?- prompt and the listener will close.
Next, make initial modifications to the name of the game, and run it both in the GUI and command line.
Expand the if_portals project.
Right click on the template.pro file and rename it to portals.pro.
Right click on the template_english.pro file and rename it to portals_english.pro
Double click on portals.pro, an editor window will appear.
In portals.pro, find these two lines (not necessarily together in the file):
title_version(`Template`, 1) file_extension(`.tmp`)
and change them to:
title_version(`Portals`, 1). file_extension(`.ptl`).
This will give the new game a title and an extension to use when a game is saved.
Save the file using file/save or the disk icon in the tool bar.
Hint: In the outline window on the right you will see title_version/2 and file_extension/1 - you can use this outline to quickly fine facts and rules of the game logic. Clicking on the outline entires will bring you to the corresponding rule in the file.
To run in the GUI select Run/Run As/Interpreted Project. This will load the project in the listener window. Type main. to start the game.
The GUI should appear with the new title. Exit using the window close icon. Then quit the listener.
For development, its easier to run the game directly in the listener, which is console mode. To do that change the line:
Run it again. This time it should run entirely in the listener window.
You will probably run in console mode during most of development, but use the gui mode to check how it appears in the GUI from time-to-time.
A Game has the following components:
It is specified using:
See the Adventure in Prolog tutorial that comes with Amzi! to learn more about Prolog. (Adventure in Prolog also uses a simple IF game for teaching, but does not make use of the tool kit. It uses a simpler game model designed to illustrate Prolog.)
A logic base is composed of three basic entities:
The syntactic building blocks of a logic base are:
They are all referred to by the catch-all word term.
The syntax of a fact is: PredicateName(Arg1, Arg2, ..., ArgN).
PredicateName is an atom and the arguments are other legal terms.
text(hello, `Hello`). text(good_bye, `Good Bye`). text(hello, `Hola`). text(good_bye, `Adios`).
Facts are not used too much in creating games, since a built-in frame system is used for most data modelling. This will be discussed later.
The syntax of a rule is: PredicateName(Arg1, Arg2, ..., ArgN) :- Goal1, Goal2, ... GoalN.
The PredicateName and arguments is called the head of the rule. The :- symbol is called the neck and means if. The goals are patterns used to match other predicate names in rules and facts.
Note that there is no real distinction between facts and rules. A predicate is defined by one or more facts and/or rules with the same predicate name.
A fact then, is an assertion that something is true.
A rule is an assertion that something is true if some other things are true. For example:
hello_defined :- text(hello, _).
Is a rule that says hello_defined is true if there is a fact that matches the goal pattern text(hello, _).
Sometimes the truth/falsity of a rule is not really relevant, and the purpose of the rule is to get to a goal which is a built-in predicate that has some effect that has nothing to do with logic, such as:
speak(Token) :- text(Token, Text), write(Text).
Logically, this rule says: speak(Token) is true if there is a predicate (fact or rule) that matches the pattern text(Token, Text) and write(Text) is true.
The way the pattern matching works is, if there are logical variables in the pattern they become bound to a value that makes the pattern match work.
So in the example, if a query speak(hello) was made, then Token would be bound to hello, and the goal text(hello, Text) would be tried. It would match the text fact text(hello, `Hello`) if Text was bound to the string `Hello`, which is what happens. The final goal is then write(`Hello`) which really doesn't have anything to do with logic but simply writes Hello to the console window.
The speak rule can be used to write out the text associated with a given atom, using the text facts in the earlier example.
There might be more than one fact or rule that matches a goal pattern, in which case the logical execution engine notes that as a choice point. If something else is not true, fails, then execution backs up to the choice point and another match is tried.
Failure can be forced with the built-in predicate fail.
We can use that to make our speak rule write both the English and Spanish words for hello:
speak(Token) :- text(Token, Text), write(Text), fail.
Now the goal speak(hello) will cause first `Hello` to be written, and then failure would go back to the choice point and Text would be bound to `Hola` instead and it would be written as well. Similarly, speak(good_bye) will write `Good Bye` and then `Adios`.
Prolog implementations come with many built-in predicates that can be used for various purposes.
The Amzi! IF Toolkit has a number of application-specific built-in predicates that are useful for building IF games.
The Amzi! IF Toolkit contains support for a frame system which is used for creating the data model of the game. A frame basically has a name and then an arbitrary number of slots. Some frames in a game might look like:
frame(kitchen, [place, north=hall, lights=off, contains=table]). frame(table, [support, at=kitchen, contains=newspaper, contains=napkins]).
The first frame is named kitchen, and it has four slots describing it. The second frame is named table and also has four slots. The power of the frame system is a frame can have any number of slots in any order, and the slots can be freely used by the pattern-matching of the logic engine.
The game designer uses whatever slots suit his/her purpose. So maybe slots like north=hall are used for connections, or maybe the game uses direct connections represented by connects=hall. In Portals, we'll see frames for portals and places connecting indirectly through those portals.
The frames are not referenced directly, but rather through the application-specific predicates provide by the toolkit. These are:
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.
dump( Name ) - for debugging, display the frame.
dump - for debugging, display all frames.
You can experiment with the frame tools directly from the listener. First, select run/run_as/interpreted project. Then import the frames module, which was already loaded with the project, so that it's predicates can be used directly, and try these experiments:
?- import(frames). yes ?- make( kitchen, [ lights = on ] ). yes ?- query( kitchen, [lights = X] ). X = on yes ?- add( kitchen, [connects = porch, contains = table ] ). yes ?- make( table, [at=kitchen, supporting=newspaper] ). yes ?- dump. kitchen connects = porch contains = table lights = on table at = kitchen supporting = newspaper yes ?- change( kitchen, [lights=off] ). yes ?- dump(kitchen). kitchen lights = off connects = porch contains = table yes ?- add(kitchen, [ceiling_fan = on] ). yes ?- breezy(X) :- query(X, [ceiling_fan = on] ). Term asserted ?- breezy(kitchen). yes ?- change(kitchen, [ceiling_fan = off] ). yes ?- breezy(kitchen). no ?- is_in(Place, Item) :- query(Place, [contains = X]), query(X, [supporting=Item] ). Term asserted ?- is_in(kitchen, newspaper). yes ?- delete(table, [supporting=newspaper]). yes ?- is_in(kitchen, newspaper). no ?- dump. kitchen ceiling_fan = off lights = off connects = porch contains = table table at = kitchen yes ?-
The IF Toolkit provides for the basic runtime environment of the game and also provides services for the game.
Execution begins in the toolkit, which uses either the console or gui user interface depending on which is set in the game module.
The user interface module then waits for user commands and calls begin(new) when the game is to begin. This is automatic in the console, but requires the user to select file/new in the GUI.
So the game has to implement, in the game module:
begin(new) :- ...
Typically, begin(new) will call a predicate to initialize the data model, and then display some initial information.
The user interface then waits for commands from the player. When it receives a command, it calls the language module's player grammar rule to parse the command.
So the game has to implement, in the language module a player grammar rule (grammar rules will be discussed a later):
player( Command ) --> ...
Note that this is not strictly necessary to define the grammar rules during initial game development. They can be added later. If there are no grammar, the input is assume to be exactly the commands. So, for example, if "go north" is entered it is interpreted as the command [go, north].
It is grammar rules that allow "north", "n", "go to the north", etc. to all mean, as well, [go, north].
After getting the command, the engine calls the games tick predicate to execute a move in the game.
So the game has to implement, in the game module:
tick(Command) :- take_action(Command), ...
There is a hand-shake between the engine and the game logic. tick calls take_action, which is defined in the engine.
take_action makes use of an action predicate defined in the game that maps commands to rules.
This architecture makes it possible for the engine to know what the verbs of the game are, and provide general purpose hooks to those verbs.
So the game has to implement:
action( Command1, Rule1 ). action(Command2, Rule2 ). ...
Now the game can get other services from the engine.
output( X ) - is used to output tokens, and fill in the blanks as necessary
get_input_name(Token, Text) - uses the grammar rules to find the preferred way for the player to refer to something.
get_output_name(Token, Text) - uses the text facts to find the description associated with something.
And there are all the built-in predicates of Prolog and its libraries.
These include useful things like:
is - evaluates arithmetic functions (do not use =). ex. X is 2 + 2.
> - tests values, also < >= and =<. X > Y
findall - findall the items that meet a criteria and store them in a list
See the Prolog documentation for further details.
Portals will be a simple game that illustrates one approach to getting from place to place. The idea will be that each place has portals that connect it to other places, and those portals have properties. This architecture makes it easy to implement puzzles associated with the passages from place to place.
The places will be:
kitchen - connected via the kitchen door portal to the porch, which is jammed one way and locked the other.
hall - connected via the hall door portal to the kitchen, and the cellar stairs portal to the cellar. The cellar stairs are dangerous in the dark.
cellar - connected via the cellar stairs back to the hall, and a bulk head door, which is locked, to the porch.
porch - connect by the kitchen door portal which is locked.
So the trick is to go from the kitchen to the kitchen, via all the places.
The game needs to first define begin(new) which typically calls initialize and displays a welcome. We can modify the template's original begin(new) so it looks like this:
begin(new) :- initialize, output( welcome ).
And then add welcome text to portals_english.pro like this:
text(welcome, [`Welcome to Portals. You're in the kitchen but the door to `, `the back porch is locked and the lock is jammed on the inside. `, `You need to open it from the outside. ` ]).
Notice that text can either be a string, or a list of strings, or, as we'll see, a list of strings and variables that are used to dynamically fill in the text. If there is a list of strings, they are concatenated and the result is word-wrapped in the output.
Run the game (run/run_as/interpreted project). We can't do much yet, except see the welcome and quit.
?- main. Welcome to Portals. You're in the kitchen but the door to the back porch is locked and the lock is jammed on the inside. You need to open it from the outside. > quit good_bye yes ?- quit.
Having come up with the idea for the game, we need to create a data model that supports it. In this case we will have frames that represent the places in the game, and each place will have as attributes the portals that are available to that place. Here is how we initalize the game with this idea:
initialize :- clear, % Clear the frame database. make( kitchen, [ portal=hall_door, portal=kitchen_door ] ), make( hall, [portal=hall_door, portal=cellar_stairs] ), make( cellar, [ portal=cellar_stairs, portal=bulk_head] ), make( porch, [ portal=kitchen_door, portal=bulk_head] ), make( you, [at=kitchen] ).
For diagnostic purposes, we are probably going to want to see the state of the game so we can add a command (which we won't tell the player about) that dumps the frames we've created. We create a verb for it in portals_english.pro and an action associated with it in portals.pro.
verb(dump) --> [dump]. % in portals_english.pro action( [dump], dump ). % in portals.pro
Now testing the game.
?- main. Welcome to Portals. You're in the kitchen but the door to the back porch is locked and the lock is jammed on the inside. You need to open it from the outside. > dump cellar portal = cellar_stairs portal = bulk_head hall portal = hall_door portal = cellar_stairs kitchen portal = hall_door portal = kitchen_door porch portal = kitchen_door portal = bulk_head you at = kitchen > quit yes ?- quit.
Next we need to implement a command that lets us get around, but first a digression on grammar rules.
We're starting with a command go, that will let the player directly go to another place. They will, of course, pass through a portal, but for now we are just looking at the grammar rules.
Commands are represented internally in the game by a list. The first token in the list is the verb of the command. The other tokens, if any, are the arguments of the command. So the command to go to the kitchen internally is [go, kitchen]. The command to look is: [look].
The grammar rules let us parse the player's input into these commands. So, depending on how we specify the rules, the player might be able to type: "go to the kitchen", or maybe just "kitchen" and the command [go,kitchen] is picked up in either case.
Grammar rules are written with a --> symbol. They are similar to logic rules, except they hide some of their arguments which contain the input stream of tokens. The simple grammer rules provided in the template are:
player( [V, O] ) --> verb(V), object(O). player( [V] ) --> verb(V).
The first one can be read as saying: "The input command is [V, O] if a verb V can be parsed from the beginning of the input followed by an object O." As with logic rules, a leading upper case letter indicates a variable.
The second one reads: "The input is a single verb [V] if all that there is in the input stream is a verb V."
Other grammer rules then define verb and object. When a grammar rule does not refer to other grammar rules, but instead identifies actual input words, it is called a terminal. The terminal grammar rules are used to define the input vocabulary using list notation..
So for example, we might allow both "go" and "go to" to signify the command go. This would be specified:
verb(go) --> [go]. verb(go) --> [go,to].
We might want to let the player add an article (the, a) before a noun. In that case we might create a secondary grammar rule for objects saying "there is an object, O, if there is an article followed by a noun O."
object(O) --> article, noun(O).
And then define article to either be the, a, or nothing:
article --> [the]. article --> [a]. article --> .
noun(kitchen) --> [kitchen].
Now the grammar will accept "go kitchen", "go to the kitchen", and "go a kitchen" all to mean [go, kitchen].
What if we want to be able to allow the player to simply type the name of a place and have it interpret that as a command to go to that place? Well the first thing is to distinguish place nouns from other nouns:
noun(N) --> thing(N). noun(N) --> place(N).
And then replace noun(kitchen) --> [kitchen] with:
place(kitchen) --> [kitchen].
And add a new grammer rule at the top:
player( [go, P] ) --> place(P).
Each of the grammar rules for player will be tried in order. If other rules fail, and the first and only word is a place name, then the new rule will succeed, and the command [go, kitchen] will be parsed from the input "kitchen".
Now, down to the business of actually implementing go. To begin with there won't be any contraints other than the fact that to get from one place to another there has to be a connecting portal.
First we add the required items to the input grammar, using somewhat simpler rules than discussed in the preceding section:
verb(go) --> [go]. object(kitchen) --> [kitchen]. object(cellar) --> [cellar]. object(cellar) --> [basement]. object(hall) --> [back, hall]. object(hall) --> [hall]. object(porch) --> [back, porch]. object(porch) --> [porch].
Note that there are multiple input words that can be used to indicate the hall, or the cellar.
The grammar rules can be used in reverse to find the words used to specify, say hall. When used this way they will find the first option. For hall in this case, the first option is "back hall". In the game, when the input_name for hall is needed for output, "back hall" will be used.
Next we add the new action to the game logic:
action( [go, X], go(X) ).
And finally the logic rule that implements the action:
go(X) :- query( you, [at=H] ), query( H, [portal=P] ), query( X, [portal=P] ), change( you, [at=X] ), output( now_at(X) ), !. go(X) :- output( cannot_get_to(X) ).
There are two rules for go. The first is the one that is used if all the conditions for going someplace are met. The second is used if the first one fails. Why might the first one fail? Well most likely because there isn't a portal P that is the same P for both the here place, H, and the there place X.
Notice how the bindings of logical variables work. The first goal winds up binding the value of the logical variable H to the place where you is at. The second goal winds up binding the value of P to the first portal available for the place H. The third goal uses that binding of P to see if the place X also has the same portal. If it doesn't, execution backtracks to the second goal which might find a second portal, and P is now bound to that second portal. The third goal is tried again.
If it succeeds, then the change goal is executed, moving the player to place X, and the output is generated. The ! (cut) tells the logic engine to forget about any choice points, because we've got a solution we're happy with.
Now, the way we wrote go is OK, but sometimes there are goals that keep getting repeated, and it makes sense to create helper rules. For example, we often want to know where here is. So we can write a rule:
here(H) :- query( you, [at=H] ).
And then use that instead in the rule:
go(X) :- here(H), query( H, [portal=P] ), query( X, [portal=P] ), change( you, [at=X] ), output( now_at(X) ), !. go(X) :- output( cannot_get_to(X) ).
We can also implement a general test for whether we can go somewhere:
can_go(Here, There) :- query(Here, [portal=P]), query(There, [portal=P]).
The can_go rule will succeed or fail depending on whether there is a common portal between the two places. Using it in our rule:
go(X) :- here(H), can_go(H, X), change( you, [at=X] ), output( now_at(X) ), !. go(X) :- output( cannot_get_to(X) ).
So rules can refer to other rules, which can refer to other rules, etc.
We can defer the output text to later and test it now. When output is requested for a token without any associated text in portals_english.pro, then just the token is output. This is good for testing.
> go back hall now_at(hall) > go porch cannot_get_to(porch) > go cellar now_at(cellar)
Adding the output text patterns in portals_english.pro:
text(now_at(X), [`You are now at the `, X, `.`] ). text(cannot_get_to(X), [`You can't get to the `, X, ` from here.` ]).
> go back hall You are now at the back hall. > go porch You can't get to the back porch from here.
We can now implement look that will describe the player's surroundings. For Portals, this means describing the portals and other things.
First we add the vocabulary and the command in portals_english.pro and portals.pro:
verb(look) --> [look]. % in portals_english.pro action( [look], look ). % in portals.pro
Then for look, we can copy the code from the Cloak of Darkness game, and modify it to show portals instead of connections.
look :- here(Place), get_input_text(Place, ShortText), get_output_text(Place, LongText), output( [ShortText, `:\n`, LongText] ), look_portals(Place), !. look_portals(Place) :- output( portals ), query( Place, [portal=P] ), get_input_text(P, PText), get_output_text( portal(P, Place), LongText ), output( [PText, `: `, LongText] ), fail. look_portals(_).
Note that as we display the portals we use a structure token for output, portal(P, Place), which has two arguement, the first being the portal and the second the place we're at. This lets us use different text for a portal depending on which side we're looking at it from.
text( portals, `You can see various portals to other places:` ). text(portal( hall_door, kitchen), `An doorway to the hall.` ). text(portal( hall_door, hall), `An doorway to the kitchen.` ). text(portal(kitchen_door, kitchen), `An old door that leads outside.` ). text(portal(kitchen_door, porch), `A screen door and old fashioned door that leads inside.` ). text(portal(bulk_head, cellar), `A clumsy metal overhead bulkhead door, with a lock on it.` ). text(portal(bulk_head, porch), `A blue painted bulk head door leading under the house.` ). text(portal(cellar_stairs, hall), `Some ill-kept stairs lead down.` ). text(portal(cellar_stairs, cellar), `Some ill-kept stairs lead up.` ).
> look kitchen: The kitchen with a cheap linoleum floor. You can see various portals to other places: hall door: An doorway to the hall. kitchen door: An old door that leads outside. > go hall You are now at the back hall. > look back hall: A small crowded hall filled with lots of junk. You can see various portals to other places: hall door: An doorway to the kitchen. cellar stairs: Some ill-kept stairs lead down. >
The next step is to let the player navigate by passing through the portals. So we create a new command, pass, that takes a portal as an argument. It's basically the same as go, but we make a few changes.
First, we change can_go so that it has three arguments. If can_go is called with Here and There bound, it will return a value for P, being the portal between here and there. If Here and P are bound, then it will return the value of There. This is the way logical variables work. They just match patterns, and they can be matched with any combinations of input variables bound.
We also add There \= Here, because otherwise the rule will succeed allowing a place to connect to itself.
can_go(Here, There, P) :- query(Here, [portal = P] ), query(There, [portal = P] ), There \= Here, !. can_go(Here, There, _) :- output( cannot_get_to(There) ), !, fail.
Then we create pass which is like go, but takes a portal as an argument. can_go returns, and now we can call a new rule, safe_passage, that checks to see if we can actually use the portal that we now know exists.
pass(P) :- here(H), can_go(H, X, P), safe_passage(P, H, X), change( you, [at=X] ), output( now_at(X) ), !. pass(_).
Here's the rules for safe_passage that basically define all the portal puzzles in the game:
safe_passage(Portal, Here, There) :- query( Portal, [locked] ), output( locked(Portal) ), !, fail. safe_passage(Portal, Here, There) :- query( Portal, [from(Here) = locked]), output( locked(Portal) ), !, fail. safe_passage(Portal, Here, There) :- query( Portal, [from(Here) = broken]), output( broken(Portal) ), !, fail. safe_passage(cellar_stairs, hall, _) :- query( cellar_stairs, [dark] ), output( fall_down_stairs(hall) ), !. safe_passage(cellar_stairs, cellar, _) :- query( cellar_stairs, [dark] ), output( fall_down_stairs(cellar) ), !, fail. safe_passage(_, _, _).
Notice that we've made some other changes. We've put the problem handling messages in both can_go and safe_passage, and we've used !, fail for the cases that are a problem for the player. This means that can_go or safe_passage might fail if the player can't actually use the portal. When they fail, the first rules of go and pass also fail.
It's cleaner for the game if a command always succeeds, so we add a second rule for go and pass that simply does nothing. It's called in those cases where the first rule failed because can_go or safe_passage failed.
On the language side we add nouns which are portals:
portal(hall_door) --> [hall,door]. portal(kitchen_door) --> [kitchen,door]. portal(cellar_stairs) --> [cellar,stairs]. portal(cellar_stairs) --> [stairs]. portal(bulk_head) --> [bulk,head].
Verbs that mean pass:
verb(pass) --> [pass,through]. verb(pass) --> [go,through]. verb(pass) --> [go,down]. verb(pass) --> [go,up]. verb(pass) --> [go].
A grammar rule that insists the object of the pass verb must be a portal:
player( [pass, P] ) --> verb(pass), portal(P).
Add a clause to get_input_words, so the game can display the input names of portals:
get_input_words(Name, Words) :- portal(Name, Words, ), !.
And finally the new text for various portal situations:
text( portals, `You can see various portals to other places:` ). text(portal( hall_door, kitchen), `An doorway to the hall.` ). text(portal( hall_door, hall), `An doorway to the kitchen.` ). text(portal(kitchen_door, kitchen), `An old door that leads outside.` ). text(portal(kitchen_door, porch), `A screen door and old fashioned door that leads inside.` ). text(portal(bulk_head, cellar), `A clumsy metal overhead bulkhead door, with a lock on it.` ). text(portal(bulk_head, porch), `A blue painted bulk head door leading under the house.` ). text(portal(cellar_stairs, hall), `Some ill-kept stairs lead down.` ). text(portal(cellar_stairs, cellar), `Some ill-kept stairs lead up.` ). text( locked(P), [`The `, P, ` is locked.`] ). text( broken(P), [`The `, P, ` is broken, maybe try from the other side.`] ). text( fall_down_stairs(hall), [`The stairs were cluttered with stuff, a clear home safety violation, `, `and you trip and tumble to the bottom. If this were a more serious `, `game you might have been hurt.` ]). text( fall_down_stairs(cellar), `You trip on all the clutter on the stairs, and wind up back in the cellar.` ).
> look kitchen: The kitchen with a cheap linoleum floor. You can see various portals to other places: hall door: An doorway to the hall. kitchen door: An old door that leads outside. > go through kitchen door The kitchen door is broken, maybe try from the other side. > go through hall door You are now at the back hall. > look back hall: A small crowded hall filled with lots of junk. You can see various portals to other places: hall door: An doorway to the kitchen. cellar stairs: Some ill-kept stairs lead down. > go down cellar stairs The stairs were cluttered with stuff, a clear home safety violation, and you trip and tumble to the bottom. If this were a more serious game you might have been hurt. You are now at the cellar. > look cellar: The cellar of an old house, damp and dingy. You can see various portals to other places: cellar stairs: Some ill-kept stairs lead up. bulk head: A clumsy metal overhead bulkhead door, with a lock on it. > go up stairs You trip on all the clutter on the stairs, and wind up back in the cellar.
Next we provide the player with the tools to solve the puzzles. First the verbs and objects, including a grammar rule for object(X) that lets a portal be a general purpose object as well. This is so unlock can refer to a portal.
verb(unlock) --> [unlock]. verb(turn_on) --> [turn, on]. verb(turn_off) --> [turn, off]. object(light_switch) --> [light, switch]. object(light_switch) --> [light]. object(light_switch) --> [switch]. object(X) --> portal(X).
Then the actions:
action( [unlock,X], unlock(X) ). action( [turn_on, X], turn_on(X) ). action( [turn_off, X], turn_off(X) ).
And we put the light switch for the cellar stairs in the hall:
make( hall, [portal=hall_door, portal=cellar_stairs, light_switch=off] ),
Then we add a bunch of rules for turning things on and off, and unlocking portals. For turn on and off we choose to handle special cases in a separate rule, called effects. It sets any side effects of an action, in this case making the cellar stairs light or dark.
There are many other ways to implement the same thing. There could have been a rule named is_dark that, for the cellar stairs checks the status of the switch. That way we wouldn't need to change the dark property of the cellar stairs. It's a matter of style which method is used.
For unlocking, we recognize the two-sided unlock problem as well as the single lock. Here are the various rules:
turn_on(X) :- here(H), query(H, [X=off]), change(H, [X=on]), effects(H, [X=on]). turn_on(_) :- output( cannot_do ). turn_off(X) :- here(H), query(H, [X=on]), change(H, [X=off]), effects(H, X=off). turn_off(_) :- output( cannot_do ). effects(hall, light_switch=on) :- delete( cellar_stairs, [dark] ), !. effects(hall, light_switch=off) :- add( cellar_stairs, [dark] ), !. unlock(P) :- here(H), query(H, [portal=P] ), query(P, [locked]), delete(P, [locked]), !. unlock(P) :- here(H), query(H, [portal=P]), query(P, [from(H)=locked] ), change(P, [from(H) = unlocked] ).
And add an ending condition for the game:
done :- here(kitchen), query( kitchen_door, [from(porch) = unlocked] ), output( solved ), !.
And the final two messages:
text( cannot_do, `Can't do that for some reason.` ). text( solved, `You got that door open, now we can fix it.`).
> turn on light switch Can't do that for some reason.
Uh oh. It didn't work. We can study the code or use the source code debugger. To use the debugger, quit this run and select run/debug_as/interpreted project.
This will put you in the debugger screen. There are many things that can be done in the debugger and you'll need to read the documentation to learn all its features. For now:
Notice that H picked up the value of 'hall', which is correct, and X is light_switch, which it should be. Continuing
Now sometimes a failure is normal, and if we continued we would see the source code debugger trace the execution backwards and restart with a new rule that might then succeed.
But in this case we expected the call to effects to succeed. And it didn't. Examining this code more closely we see that the problem is we called the goal effects with a square brackets, but we implemented the head of the rule without a square brackets. So we remove the square brackets from the call to effects.
And now we can run the game to completion:
?- main. Welcome to Portals. You're in the kitchen but the door to the back porch is locked and the lock is jammed on the inside. You need to open it from the outside. > go through hall door You are now at the back hall. > look back hall: A small crowded hall filled with lots of junk. There is a light switch which is off. You can see various portals to other places: hall door: An doorway to the kitchen. cellar stairs: Some ill-kept stairs lead down. > turn light on The template game did not understand the input. > turn on light > go down cellar stairs You are now at the cellar. > look cellar: The cellar of an old house, damp and dingy. You can see various portals to other places: cellar stairs: Some ill-kept stairs lead up. bulk head: A clumsy metal overhead bulkhead door, with a lock on it. > go through bulk head The bulk head is locked. > unlock bulk head > go through bulk head You are now at the back porch. > look back porch: A porch with a grill and a wonderful place to watch fireflies. You can see various portals to other places: kitchen door: A screen door and old fashioned door that leads inside. bulk head: A blue painted bulk head door leading under the house. > unlock kitchen door > go kitchen You are now at the kitchen. You got that door open, now we can fix it. yes ?-
The last step is to check the GUI version. Change the line that specifies the user_interface to user_interface(gui). Run interpreted, type main and you should be able to start playing:
To distribute the game, you can copy portals.pro, portals_english.pro to the runtime AIFT directory, and create an portals.bat file like the other .bat files.
Add help, hints and options to Portals to make it easy for a new player. See other sample games for ideas.