Homage à Jones
This is an essay in the Rewriting History series. Please read at least Introducing Herodotus and Rewriting History before proceeding.
The challenge
Use a simple text-based adventure puzzle to show the basics of Thucydides. The player will be the only actor of import in this world, and there will be a one to one correspondence between player actions and historical events. This game will showcase two features of Thucydides: Archetypes and Requirements.
The puzzle is trivial: The player starts in a treasure room, with a valuable statue on a pressure plate. If the statue is removed, the exit will close immediately. There is also a statue-shaped rock in the room. The player must escape the room with the statue in hand. The actions available to the player are:
- look
- leave when the door is open
- take item when the named item is on the floor and the player's hands are free
- lift when an item is on the pressure plate and the player's hands are free
- drop (to the floor) when the player is holding something
- place (on the pressure plate) when the plate is empty and the player is holding something
Therefore, the solution will be lift; drop; take rock; place; take statue; leave. The following will show how to implement such a puzzle using Thucydides, and sample code will be provided for Thucydides/Erlang.
Setting the Stage
First, the nouns must be examined. This system does no dynamic generation of new actors, so it's sufficient to use the entities suggested by the puzzle description: A door to open and close, a statue and a rock to lift and drop, and a floor, plate, and player to hold items.
Nouns are the items bound into Archetypes to create an Incident when one should occur. The drop command will fill in a Drop Archetype with the dropper and the item to be dropped, for instance.
Now that the nouns are in place, the fluent types should be decided. We should think of the states caused by the interactions of the commands and the nouns. For instance, inventory is a Fluent covering the floor, plate, and player. open is a Fluent covering the door. finished is a Fluent of interest to the game, and is set when the player leaves the room.
Next, the subject of Cancels should be approached. We can imagine that if the door is open, it will be so forever. But when the plate is emptied, the door's openness should be terminated. While it would be possible to express "the door is open" as "the plate is full", this is a less flexible approach that doesn't take into account the fact that there could be other ways to open the door or keep it propped open.
We can make a simple mapping of commands onto Archetypes, so let's do that. That gives us Look, Leave, Take, Lift, Drop, and Place. Leave will set finished. Take will set inventory with the item as value, stacking with an append in the context of the taker(the player) and a delete in the context of the old possessor(the floor). Lift will set inventory as Take did, but will also Cancel the door's open state. Drop is not just Take with reversed arguments; the player can only hold one thing, but the floor can hold many. To keep things simple, rather than give inventories a size, we'll have a special Drop Incident that encodes this idea that a Taker can hold one thing and a Drop destination can hold many. Finally, Place acts like Lift, with the exception that it sets {open, door} rather than cancels it.
The final step in designing such a simple simulation is to look at the preconditions for actions, hinted at in the list of verbs above. Each of those preconditions will be expressed as a Requirement on the corresponding Archetype. Drop and Place both require that the taker's hands are full; Place further requires that the destination is empty; Take and Lift both require that the object is at the corresponding location and that the taker's hands are empty; Leave requires that the exit is open.
Send in the Code
First, we'll write a quick and dirty adventure game interface:
%No code! We're going to use the Erlang shell for I/O,
%and let our input alphabet be function calls
%and our outputs be strings.
With that out of the way, we can move on to the interesting parts. I'll only show excerpts here, but here's the whole file. The Herodotus/Erlang implementation also includes this demo, but that's not ready to distribute yet. Now, on to the tasks.
Speak Fluent Erlang
First, we'll look at simply applying Fluents in a Herodotus history. In this example, we create a few Fluents, jam them into an Incident, and trigger that.
%... jones.erl 42-47: reset()
%This macro creates a Fluent with the given type, context,
%start, end, persist-value, value code, and stack code.
%These up the initial state of the room.
%Note that since we know these are the first fluents, we can just
%use the current fluent's value for the new net value in the stack mode.
RockOnFloor = ?FLUENT(inventory, floor, 0, infinity, removed, [rock], _Val),
StatueOnPlate = ?FLUENT(inventory, plate, 0, infinity, removed, [statue], _Val),
EmptyHands = ?FLUENT(inventory, player, 0, infinity, removed, [], _Val),
DoorOpen = ?FLUENT(open, door, 0, infinity, closed, open, _Val),
%Now we put all the Fluents into a list...
Fluents = [RockOnFloor, StatueOnPlate, EmptyHands, DoorOpen],
%And trigger a new incident using those fluents and no cancels.
%We trigger them in the main history of the jones_hero herodotus process.
herodotus:trigger(incident:new(init, Fluents, [], 0), main, jones_hero),
%...
Incident Archetypes
Now that the initial state is set, we can start looking at Thucydides. We want to create five incident archetypes, but we'll only show a couple. The first will be the Look archetype, which has no requirements.
%... jones.erl 65-87: setup_iarc(look)
%This macro creates an 'Adder Fluent Archetype'. All of the arguments
%are in the context of a function. The available vars are
%(_Time, _Bindings) for the first five code arguments, where
%_Time is the time the Incident is triggered; and
%(Self, _Now, _Args, _Time, _EndTime) for the sixth code argument.
%Self is the Fluent being evaluated.
%This macro provides its own stack mode, so that definition isn't shown yet.
%A Fluent Archetype can be 'cloned' into a Fluent, given time and bindings.
%This is similar to the way incident archetypes are cloned into incidents.
%modify times_looked
FA = ?A_ADD(times_looked,
%for the looker - we use bindings for flexibility
dict:fetch("Looker", _Bindings),
%at the given time
_Time,
%forever
infinity,
%if this gets ended somehow, it won't change the value
persist,
%increment by one
1),
Fluents = [FA],
%This is primarily for documentation and
%discovery purposes, and may be removed later
Vars = ["Looker"],
%Look requires nothing in particular
Reqs = [],
%Look doesn't depend on any fluents
ExtraInputs = [],
%Look doesn't cancel anything
Cancels = [],
%Create the archetype
Look = i_arc:new(look, Fluents, Vars, Reqs, ExtraInputs, Cancels),
%Add the archetype to the simulation
simulation:add_iarc(Look, jones_sim)
%...
The second archetype we examine will be Lift, which cancels the {open, [door, plate]} Fluent set initially(or set by Place). We use a list context there, since we assume that the door could potentially be opened by a lever, even if the plate were empty. Really, this is primarily to show the cancellation mechanism - it could just as well have been done with a fluent-set. One example of a place to certainly prefer cancels over simple fluent replacement is when growth changes from linear to exponential, or some similar massive shift like that.
%... jones.erl 132-192: setup_iarc(lift)
%A complete fluent archetype, including stack mode.
TakerAddItem = ?A_FLUENT(
%modify inventory
inventory,
%of the taker
dict:fetch("Taker", _Bindings),
%at the given time
_Time,
%forever
infinity,
%on cancellation, it's "removed"
removed,
%the item to be added is the result of a query
herodotus:value(
%when you make a query from within a fluent, you should provide
%the fluent making the query.
Self,
%this query is against the inventory of the source.
%we can assume there's only one item in it.
%note that we use _Time, the time the fluent
%was set, and not _Now, the time it is evaluated
selector:new(inventory, dict:fetch("Source", _Bindings), _Time),
%no extra args
[],
%the history is the main history
main,
%of the jones_hero herodotus process.
jones_hero
),
%and it's merged into the taker's inventory
fluent:fmerge(_Net, _Val)),
%Now for the next fluent:
SourceRemoveItem = ?A_FLUENT(
%modify inventory
inventory,
%of the source
dict:fetch("Source", _Bindings),
%right now
_Time,
%until forever
infinity,
%if it is canceled, it will be "returned"
returned,
%lift the topmost item, whatever that means
[hd(_Net)],
%and subtract it from the old inventory
fluent:fsubtract(_Net, _Val)
),
%these are the fluents for this archetype
Flus = [SourceRemoveItem, TakerAddItem],
%these are the variables
Vars = ["Taker", "Source",
"Trigger", "TriggerFluent",
"TriggeredState", "UntriggeredState"],
%Now for the requirements
SourceHasItem =
%REQ is a macro which creates a Requirement, given three bits of
%selector information, arguments, and a function to check the
%value resulting from querying that selector with those args.
?REQ(
%Require that the value of the inventory
inventory,
%of the Source
dict:fetch("Source", _Bindings),
%at the time of application
_Time,
%with no extra args
[],
%contains an item
is_list(_Val) andalso length(_Val) > 0
)
,
TakerHandsFree =
?REQ(
%Require that the inventory
inventory,
%of the taker
dict:fetch("Taker", _Bindings),
%at the time of application
_Time,
%with no extra args
[],
%is empty.
_Val =:= []
)
,
%gather the requirements
Reqs = [SourceHasItem, TakerHandsFree],
%no extra dependencies
Extras = [],
%This finds the "door opened by plate" fluents and kills them.
%A_SEL creates a selector archetype, which follows the same
%rules as other archetypes.
TriggerFinder = ?A_SEL(
dict:fetch("TriggerFluent", _Bindings),
[dict:fetch("Trigger", _Bindings), dict:fetch("Source", _Bindings)],
_Time,
infinity
),
%Gather the cancels
Cancels = [TriggerFinder],
%and create the archetype
Lift = i_arc:new(lift, Flus, Vars, Reqs, Extras, Cancels),
%then add the archetype
simulation:add_iarc(Lift, jones_sim)
%...
Trigger Action
Now it's time to figure out how to trigger these incident archetypes.
%... jones.erl 349: look()
%This asks the simulation jones_sim to trigger the
%"look" incident in the given history(main)
%at the given time with the given bindings.
%We use special bindings for the player just
%to show a little more of the Archetype system.
simulation:trigger(look,
main,
current_time(),
dict:store("Looker", player, dict:new()),
jones_sim)
Triggering is simple! This will return ok if it completes successfully, not_met if any requirements are not met, or paradox if a paradox would result from applying that incident. Thucydides handles all of the requirement checking, cancellations, paradox checking, and so on. Also, note that if any non-ok value is returned, the history is as it was before the trigger: a trigger either succeeds or fails, and intermediate states will not pollute other actions.
Mission accomplished!
While Thucydides/Erlang is still unfinished, I'd rather not put it up for download. However, the above examples and the jones.erl file suggest how a simple simulation might look. I plan to eventually replace the archetype definitions and the fluent queries with domain-specific languages, but until I know the capabilities that will need to be supported, I want to stick with this approach.
Something worth noting is that in about one week's worth of total work I've reproduced every feature of Herodotus/Io, only better-defined, faster, and safer. Having the Io version around for comparison definitely helped, but I feel like the Erlang version will scale far, far better.
The next essay will implement a trivial military simulation. Player involvement will be limited to army formation and the issuance of high-level commands, and the outcome of the fight will be decided by Thucydides's rules of causality.
If there are any questions about the new Herodotus or Thucydides, or about the examples in this essay, please post them in the comments.
Labels: example, herodotus, theory, thucydides
Joe Osborn 2008-03-11
0 Comments:
Post a Comment
<< Home