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 trigger happens: if a set of conditions things are true, then perform a set of actions.” Outside of the trigger, 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!

Triggers

All of the actions that happen in Perplexity generate triggers that you can attach a rule to. To see the triggers, turn on Prolog tracing to list them as they happen using the debug(perplexity(trigger, setup)) Prolog predicate:

The room is bare, lacking even the most basic furniture. You see a door, a frog. 

? /q debug(perplexity(action, start))
Prolog: Warning: [Thread language_server1_conn1_goal] perplexity(action, start): no matching debug topic (yet)

? 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:

You can customize any (or none) of the triggers that are generated from these actions for your own purposes. When an action happens, it actually fires three triggers that can be used for different purposes. Let’s take the openItem action as an example:

These three triggers (*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 a trigger happens. In this case openItemAfter. This means it will be fired 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:

We need to do all of these checks because, as shown above, the rule will get fired if anything is opened and we only want the message to be fired for the jewelry box. Here is an example of the openItem action happening when a door is opened. We don’t want the message firing 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 to 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.