Getting Started with Perplexity Rules
In the previous section I described the basics of how to describe a world “declaratively” using the Perplexity language, meaning “declaring what is true about the world”. You can go quite far with this model because Perplexity knows how to manipulate a basic level of physics. It understands how to move around in a world based on what it is connected to what, how to get things, has a notion of “on top of”, “inside”, etc.
There are some things that are hard to do just by declaring facts, though. In this section, I’ll walk through how Perplexity rules work. Whereas the Perplexity language describes the world and lets the engine “do the right thing”, Rules work “imperatively”, they tell the engine what to do in certain situations. They are more like a “classic” programming language, but are designed more around the notion of “planning” than most (any?) of the classic programming languages like “C”, “Python”, or “JScript”. This is because many of the rules you may want to write in a physics based environment have to deal with real world problems like “If the user says to grab the book, but it is under two other things, how do I tell Perplexity to pull the book out while leaving the other things on the table”. That’s a classic AI planning problem. Perplexity rules make planning problems like this easier to write.
“Winning a Game” Rule
One problem that you can’t solve “declaratively” is telling the user “You won!” in whatever form makes sense for your game. Let’s build a rule that tells a user they won the example game we built in the previous section if they open the jewelry box successfully.
At its most basic level, a Perplexity rule says “when some event happens: if a set of conditions things are true, then perform a set of actions.” Outside of the event, it works exactly like an “If…Then” rule in just about any programming language.
Here is a rule that you would place in Testworld.pl
since it is Prolog. It prints out some glorious text when the jewelry box is open, the following sections will explain all the parts of it and how they work together to do this:
% TestWorld.pl
...
systemInitialize() :- registerEvent(openItemAfter, jewelryBox_openItemAfter, [first]).
jewelryBox_openItemAfter(ActorID, OpenWhatID, _OpenablePartID,
_PropertyID, OpenState, _, Context) :- htnMethod([], Context,
if([
defaultActor(ActorID),
OpenWhatID == idJewelryBox1,
OpenState == 'closed'
]),
do([sayText("[The, sayName(idJewelryBox1)] opens to reveal an entire world of adventures that await you now that you've learned to build games with Perplexity!{p}{p}Congratulations, now it is time to build your own game!")]) ).
systemInitialize() :- registerEvent(describeOpeningDefault, jewelryBoxOff_describeOpeningDefault, [first]).
jewelryBoxOff_describeOpeningDefault(_WhatID, OpenablePart, CurrentOpenState, _NextOpenState, _, Context) :- htnMethod([], Context,
if([
OpenablePart == idJewelryBox1Top,
CurrentOpenState == 'closed'
]),
do([]) ).
The room is bare, lacking even the most basic furniture. You see a door, a frog.
? open the door
The door is now open.
? s
You see a bathroom, a door, a wooden cabinet, a jewelry box.
? open the drawer
The drawer is now open. Inside is a pearl.
? open the jewelry box with the pearl
The jewelry box is now unlocked.
The jewelry box opens to reveal an entire world of adventures that await you now that you've learned to build games with Perplexity!
Congratulations, now it is time to build your own game!
Events
All of the actions that happen in Perplexity generate events that you can attach a rule to. To see the events, turn on tracing to list them as they happen using the /events true
Perplexity command:
The room is bare, lacking even the most basic furniture. You see a door, a frog.
? /events true
Show events is now True
? open the door
Prolog: % [Thread language_server1_conn1_goal] Action: openItem(idAdam,idBathroom1Door,idBathroom1Door,idBathroom1Door_prop_idOpenState,closed)
Prolog: % [Thread language_server1_conn1_goal] Action: describeOpening(idBathroom1Door,idBathroom1Door,closed,open)
Prolog: % [Thread language_server1_conn1_goal] Action: playerTurn(1,2)
The door is now open.
“open the door” takes an obvious action (opening the door), but you can see from the traces that three actions are actually happening:
- The
openItem
action to actually open the door - The
describeOpening
action to describe what happened to the user - The
playerTurn
action to move from turn 1 to turn 2
You can write rules to handle any (or none) of the events that are generated from these actions for your own purposes.
When an action happens, it actually fires three events that can be used for different purposes. Let’s take the openItem
action as an example:
openItemBefore
is fired before the action occurs. Your rule will be run, but whether it succeeds or fails is ignored.openItemDefault
is fired to actually do the action. The first rule that succeeds will be what actually happens and no other rules will be run.openItemAfter
is fired after that game world has been modified by the action. Like*Before
events, whether it succeeds or fails is ignored.
These three events (*Before
, *Default
, *After
) happen for every action that occurs. *Before
and *After
are mostly used for sending messages to the user. *Default
is used to change, augment or stop the action from occurring.
Now we can explain the first line of the code above:
% TestWorld.pl
...
systemInitialize() :- registerEvent(openItemAfter, jewelryBox_openItemAfter, [first]).
systemInitialize()
is a normal Prolog rule that gets called when your code is first loaded. There can be many of these in your *.pl
sources and they are all called, in order.
The registerEvent
predicate tells the system you have a rule that you want to run when an event happens. In this case openItemAfter
. This means it will be called after anything in the world is opened (we’ll see how to handle that next). The second argument gives the name of the rule that should be run: jewelryBox_openItemAfter
and the last argument gives options. The only options at this point are first
and last
specifying where in the list of registered rules this should get added. first
means put it first in the list.
All of that registers the jewelryBox_openItemAfter
rule to be run after the openItem
action is accomplished. Now let’s look at the rule:
jewelryBox_openItemAfter(ActorID, OpenWhatID, _OpenablePartID,
_PropertyID, OpenState, _, Context) :- htnMethod([], Context,
if([
defaultActor(ActorID),
OpenWhatID == idJewelryBox1,
OpenState == 'closed'
]),
do([sayText("The jewelry box opens to reveal an entire world of adventures that await you now that you've learned to build games with Perplexity!{p}{p}Congratulations, now it is time to build your own game!")]) ).
The rule will get invoked just like any other Prolog rule: the arguments will get filled in and the body of the rule will be run. Unlike Prolog rules which can have a list of terms, the body of a Perplexity rule always calls:
htnMethod([], Context, if([<condition list>]), do([<action list>]))
[<condition list>]
is a normal Prolog list of terms to check, and [<action list>]
is a list of other rules to run if all of the conditions are all true. The condition list can use any Prolog terms including things like or
(i.e. ( ; )
). In our example we:
- Check to make sure the ActorID is our main actor using a built-in predicate
defaultActor
- Make sure the thing being opened is
idJewelryBox1
- Make sure it is being opened from a closed state (and not opened twice in a row without closing)
We need to do all of these checks because, as shown above, the rule will get called if anything is opened and we only want the message to be fired for the jewelry box. Here is an example of the openItem
event happening when a door is opened. We don’t want the message being shown in that case:
? open the door
Prolog: % [Thread language_server1_conn1_goal] Action: openItem(idAdam,idBathroom1Door,idBathroom1Door,idBathroom1Door_prop_idOpenState,closed)
Prolog: % [Thread language_server1_conn1_goal] Action: describeOpening(idBathroom1Door,idBathroom1Door,closed,open)
Prolog: % [Thread language_server1_conn1_goal] Action: playerTurn(1,2)
The door is now open.
[<action list>]
is a list of other rules to fire if all of the conditions are true. In this case we only run one other rule: sayText
. This prints out text for the user. There are a lot of options for what can be in the string (described in another post), but here we are only using plain text with a couple of {p}
commands. These put a line break in the text.
The only thing left to describe is the second rule that we’ve registered:
systemInitialize() :- registerEvent(describeOpeningDefault, jewelryBoxOff_describeOpeningDefault, [first]).
jewelryBoxOff_describeOpeningDefault(_WhatID, OpenablePart, CurrentOpenState, _NextOpenState, _, Context) :- htnMethod([], Context,
if([
OpenablePart == idJewelryBox1Top,
CurrentOpenState == 'closed'
]),
do([]) ).
describeOpening
is the action Perplexity takes to describe when something is opened. By default it prints out something like “X is now open”. You’ll notice that we have registered the *Default
trigger and made the action list be empty. Since this is the *Default
trigger it will replace the default behavior of the system (printing that message) with whatever we do, which is nothing. So, this stops the default message from being printed. Without it, you’d see this:
The jewelry box is now unlocked.
The jewelry box opens to reveal an entire world of adventures that await you now that you've learned to build games with Perplexity!
Congratulations, now it is time to build your own game!The on top of a jewelry box is now open.
Notice the final “The on top of a jewelry box is now open.” – that’s what we are getting rid of.