Rock, Paper, Scissors

This is an essay in the Rewriting History series. Please read at least Introducing Herodotus and Rewriting History before proceeding.

The challenge

Show the causality features of Thucydides by designing a simulation that "runs itself" for a while after initial user interaction. The simulation is a simple rock/paper/scissors military simulation in the style of Fire Emblem. The player issues commands at the 0th hour of the day, and the enemy responds in kind. The armies of the player and enemy clash hourly as long as daylight and their respective forces hold. While I won't show the line-by-line Erlang code as before, I'll provide the commented Erlang source code for the curious.

A War of Attrition

Since creating a full strategy game was beyond the scope of this exercise, I wanted to focus on two moderately tricky aspects:

This essay will explore each in turn. But first, the rules of the system must be described:

Therefore, the units lost in an attack are both a function of one's own forces and the enemy's at the time of the attack. Furthermore, losses occur simultaneously on both sides. This complexity and interdependency has a significant consequence: There is a danger of mutually recursive fluent definitions. Even if there's no infinite recursion, a trivial query might require massive fluent evaluation, as in this example of querying {A, 2}:

  1. Fluent A-0 queries {B, 0} and adds to it
    1. Fluent B-0 returns a default value
  2. The result is added to: Fluent A-1 queries {B, 1} and adds to it
    1. Fluent B-1 queries {A, 0} and adds to it
      1. Fluent A-0 queries {B, 0} and adds to it
        1. Fluent B-0 returns a default value
  3. The two results above are added to: Fluent A-2 queries {B, 2} and adds to it
    1. Fluent B-2 queries {A, 1} and adds to it
      1. Fluent A-1 queries {B, 1} and adds to it
        1. Fluent B-1 queries {A, 0} and adds to it
          1. Fluent A-0 queries {B, 0} and adds to it
            1. Fluent B-0 returns a default value

While this might seem a problem solvable by some optimization or trick, those solutions are hacks — there's no way to support this generally. Simulation designers must be wary of mutually dependent fluents like these. There are two common solutions to this problem, and the correct one depends on your application:

Introduce constants
Do the mutually dependent calculations in a separate History or in a function outside of Herodotus proper. Then, create a new Fluent which, rather than calculating its value dynamically, returns a constant. This is generally only usable if the past is immutable.
Combine fluents
Take the two (or more) mutually dependent Fluents and turn them into a single Fluent that returns the items' respective data. One feature that might help is to use the arbitrary arguments feature to include an indicator of which data should be returned. This is not always an applicable approach, but where it is usable it is very powerful.

The latter approach was chosen for the RPS simulation because it is slightly harder to implement, and this simulation was designed to stress Herodotus/Thucydides. All of the armies correspond to a single fluent selector, with losses being calculated for all three types on both sides in a single fluent which is triggered at each skirmish.

Taking the (Battle)Field

As in the other examples, we'll start by looking at nouns. This simulation involves two Generals (the player and the enemy) and Armies, along with Daylight. It's also important to track the time of the last Skirmish. Finally, the armies have locations: in the Reserve, away from the battle; in the Field, at combat with the enemy; and in Transit, on the way there.

The last thing to consider is the set of verbs: advances, withdrawals, and skirmishes. If skirmishes happen hourly, we'll want to track the time of the last skirmish; and if we want to prevent skirmishing during a withdrawal, we should note withdrawals when they happen.

The armies are expressed in a single Fluent, stored in one big list in the following way:


[{General1Name, 
    [{UnitType1, count}, {UnitType2, count}, ...]
  }, 
 {General2Name, 
    [{UnitType1, count}, ...]
  },
  ...]

So, the fluent that sets up the armies looks like this:


f init_armies:
%We'll express the place nouns as fluent types
  type reserve.
%Grab the army definition from the bindings
  value Armies.
%Use a special function to combine army values
  combine rps:add_armies(Net, Value).

We use rps:add_armies because it handles all kinds of cases where the Net or the Value are undefined, where one or the other is a tuple and not a list, and so on. rps:add_armies is provided by the rps.erl module, which the simulation writer provided alongside the .tsdl file. Thucydides clients (either remote or local) can supply arbitrary extra Erlang modules that are made available to .hql and .tsdl files. It's up to a Thucydides provider to ensure that these are safe to execute.

The rest of the incident used to initialize the armies and the rest of the simulation follows:


f initial_generals:
%"generals" as a fluent type is the list of generals in the fight
  type generals.
%capital-G Generals is a constant list defined in the bindings
%of the incident triggering this fluent.
  value Generals.

%field and transit are the other two army locations
f empty_places:
  type [field, transit].
%Of course erlang funs work in tsdl.
%This makes one empty army for each general in the bindings.
  value lists:map(fun(G) ->
      {G, [{swords, 0}, {spears, 0}, {axes, 0}]}
    end, Generals)
  .

%"withdrawing" marks whether the given general is withdrawing.
f none_withdrawing:
  type {withdrawing, Generals}.
%"persist persist." can be abbreviated like so:
  persist.
  value false.

%"last_skirmish" marks the last time the given two generals sparred.
f no_prior_skirmish:
  type {last_skirmish, Generals, Generals}.
  persist.
%-24 just means "yesterday, before all this started".
  value -24.

%daylight is the current brightness of the battlefield
f daylight:
  type daylight.
%bring a 24-hour value into the 0 to 1 range
%0 means "pitch black"
%1 means "high noon"
  value (12 - abs(12 - (trunc(Now) rem 24))) / 12.0.

i init_rps(Generals, Armies):
%definitions can take lists like these, too!
  set [init_armies, initial_generals, empty_places,
       none_withdrawing, no_prior_skirmish, daylight].

Triggering this initialization incident will look something like this to a local client (if the Thucydides process is called rps_sim):


Bindings = dict:store(
  "Armies",
  [{player, [{swords, 30}, {spears, 30}, {axes, 30}]},
   {enemy,  [{swords, 30}, {spears, 30}, {axes, 30}]}],
    dict:store(
    "Generals", 
    [player, enemy], 
    dict:new()
  )
),
thucydides:trigger(init_rps, Now, Bindings, rps_sim)

For a remote client, the same information is passed, but the dictionary specification is a little different.

Advance Wars

In the remainder of this blog post, we'll look at two uses of the "follow" semantic mentioned in the previous post. A "follow" is a causal linkage from zero or more incidents and conditions to one or more incidents. The structure of a follow is:


follow a_follows_b_or_c_after_5_when_d:

These are (optional) "input event archetypes". When these happen, this is triggered.

  in [b,c]

Optionally, you can include requirements as in incident archetypes. If these don't hold, it doesn't trigger, and the follow will try repeatedly to check them as time goes on. Of course, there's no need to duplicate incident archetype preconditions. The follow will keep trying until it meets the preconditions.

  require d

The follow starts trying to trigger after the "delay" parameter, which defaults to "delay 0."

  delay 5

It tries for a number of time units specified by the "duration" parameter, but will only happen once for each triggering incident. If the conditions aren't met by the time (trigger_time + delay + duration), the trigger fails and the output events don't occur. Duration defaults to "duration 0", meaning it will try only once.

  duration 10

Out is a list of output incidents.

  out [a]

Bindings are seeded based on the input archetype, if any. After this seeding, they're run through the binder functions one at a time. If all the bindings are defined by the time the binder functions are done, the requirements are checked and so on. If there is no input archetype, the binder functions alone are used. A binder function returns a list of {VarName(a string), [PossibleBindings]}. Binder functions are called in-order and each is given all permutations of the possible bindings for the previous binder. Bindings are referred to in the same way as requirements, etc.

  bind get_a_bindings.

Here's an inline binding function.

  bind get_more_a_bindings:
    FavoriteFood = q({{favorite_food, BPerson}, Time}),
    [{"FavoriteFood", [FavoriteFood]}]
    .

Let's presume that "advance" is an incident archetype with bindings for the General, the number of Swordsmen (NSw), the number of Spearmen (NSp), and the number of Axmen (NAx). If a follow is defined in this way, we can express "the enemy advances as soon as the player advances" like this:


follow enemy_advance_follows_player_advance:
  in [advance].
  out [advance].
  bind new_army:
    EnemyNSw = NSp,
    EnemyNSp = NAx,
    EnemyNAx = NSw,
%"enemy" is hardcoded here, but it could certainly
%be the result of a fluent query or something.
    [{"General", [enemy]}, 
     {"OldGeneral", [General]}, 
     {"NSw", [EnemyNSw]}, 
     {"NSp", [EnemyNSp]}, 
     {"NAx", [EnemyNAx]}]
  .
  require advancer_is_player:
    check OldGeneral =:= player.
  require enemy_hasnt_advanced:
    type [transit, field].
    check 
      lists:all(fun({_, Amt}) ->
        Amt =:= 0
      end, units(General, _Val))
    .

Now, as soon as the client code triggers a player advance, the enemy advance will trigger automatically. This works for instantaneous triggers, but what about delayed triggers? For these, Thucydides's conception of time must come into play.

Thucydides stores the "current time" automatically, and can be asked for what it thinks the current time is. The message "progress_time" can be sent to a Thucydides process remotely or locally. This message takes a time delta and the resolution by which to step towards it ("progress_time_to" takes a destination time). Thucydides does no interpolation; such interpolation could be costly since fluents can contain arbitrary code. During progress_time, various follows might be triggered, and the result of the message is the list of incidents that were triggered during that time. Here's an example:


%The time argument can be left out(it will use the current time)
%and so can the bindings, if they're empty.
thucydides:trigger(my_arc, my_sim),
%The default resolution is 1.0, but any number can be used
thucydides:progress_time(5, my_sim).

A more complicated case of following is our rule for saying that a skirmish happens whenever the two armies meet on the field. We need to be careful here to prevent an unnecessary number of skirmishes. This problem of potential duplicate skirmishes is why we introduced the "last_skirmish" fluent above (which is set to Time whenever a skirmish occurs) and the follow that expresses this case looks like this:


follow skirmish_when_armies_meet:
  out [skirmish].
  require last_skirmish_yesterday:
    type {last_skirmish, [Attacker, Defender]}.
%'div' is integer division.  TSDL will automatically
%truncate both arguments.  In this case, we want to make sure that the
%previous skirmish was yesterday or earlier.
    check (Value div 24) < (Time div 24).
%Follows with no input incidents should probably
%use infinite duration, otherwise they'll stop being checked
%and never start being checked again.
  duration infinity.
  bind all_general_combinations:
%Here's another invocation of a custom erlang function.
    {Atks, Defs} = 
      rps_lists:unzipped_combinations(q({generals, Time})),
    [{"Attacker", Atks}, {"Defender", Defs}]
  .

There's more to this simulation, including rules for skirmishes following other skirmishes after an hour, and withdrawals of each side occurring when one or both sides is defeated, or night falls.

Moving On

This ends the Rewriting History series of essays. The switch to Erlang has empowered Herodotus, and the introduction of HQL/TSDL has empowered its users. Many semantic changes have been made: matching is entirely new, follows have been introduced, and simulations now have a concept of the progression of time. Thucydides can be used in whole or in part (with or without follows). Two simulations have been written in Thucydides.

The next series of essays will discuss the development of a small web-based game that will take advantage of Herodotus's unique features. In this game, the user will play as a Seer, using his supernatural powers to help or harm various political bodies by dispatching parties of heroes to fulfill or prevent prophecies, or to preserve or disrupt previous incidents. The strengths of Herodotus will be shown in three ways: player planning will be based on past and possible future events; memories of party and Seer exploits will be held by NPCs; and the events chosen for the future will be a function of the ways that the player has acted and chosen in the past.

Labels: , ,

Read more...

Joe Osborn 2008-04-29

0 Comments

Stepping Forward

This is an essay in the Rewriting History series. Please read at least Introducing Herodotus and Rewriting History before proceeding. This is also a followup to Stepping Back

The Challenge

Create the Thucydides Simulation Definition Language, or TSDL: A language for defining Fluents, Cancels, and Incidents in their archetypal form, along with causal relationships between these archetypes. Incident archetypes defined in such a way should be applicable to a Herodotus history via a Thucydides simulation. This essay will pay special attention to the TADL subset, which doesn't include the aforementioned causal relationships support.

Lingua Franca

To recap, an Incident is a container for some Fluents and some Cancels, occurring at some time, with a given set of bindings. It also has a "type". Its archetype provides functions for defining some of these dynamically. The way this looks in Herodotus is something like:


FluentArcs = [Full, Poorer],
CancelArcs = [CancelDiets],
Requirements = [ReqEnoughMoney],
I = i_arc:new(
  eat_feast,
  main,
  FluentArcs,
  ["Owner", "MealCost"],
  [Requirements],
  [],%no extra input selectors
  CancelArcs
)

This isn't wholly unpleasant, but defining Fluents sort of is:


Full = ?A_FLUENT(
  {me, hunger},
  _Time,
  _Time + 6, %I'm hungry four hours later
  1.0,
  (_Now - _Time) / 6.0,
  lists:max([0.0, _Net - _Value])
),
Tired = ?A_FLUENT(
  ... %more like the above
),
Happy = ?A_FLUENT(
  ... %more like the above
)

The irksome things about this ?A_FLUENT macro are that it's specific to the Erlang implementation, that it's order-dependent, and that it's ugly. Let's try and find a way around using it and the other macros, and make it more readable in the process.


%this defines a fluent "full", which decreases "hunger".
%We'll assume that hunger goes from 0 to 1 and is completely
%removed by the consumption of food.
%if this took an argument, it would be f full(Arg1, Arg2...)
f full:
%this is the fluent type.
%it gets the "Owner" variable from the bindings of the archetype
%in which it's triggered
  type {hunger, Owner}.
%it starts immediately, so the "start" line is omitted.
%it defaults to "start Time."
%it ends effectiveness in six hours. this could default to "end infinity."
  end Time + 6.
%it persists as 1.0. this could default to "persist undefined."
  persist 1.0.
%and its value is the portion remaining of the food at the given time.
%The default is "value true."
  value 
    (Now - Time) / 6.0
  .
%it combines by clipping Net - Value to [0, 1].
%the default is "combine Value."
  combine 
    lists:Max([0.0, Net - Value])
  .

Note especially that the ugly _ variables are gone, bindings can be accessed just as if they were variables, and the syntax is a little cleaner overall. Even better, a lot of the definition can be omitted in the common case. Let's show another fluent, which reduces the money in the Owner's bank account:


f poorer:
  type {money, Owner}.
  value
    MealCost
  .
  combine:
    Net - Value
  .

Next, we'll examine the "cancel search", which finds a set of fluents and cancels them. This is provided so that incident archetypes can cancel fluents without necessarily knowing exactly which ones they'll need to cancel beforehand.


%c defines a cancel search
%which detects fluents and
%generates cancels for them.
%in this case, we cancel any diets the Owner is on.
c cancel_diet:
%this binding also comes from the enclosing incident.
  type {diet, Owner}.
%"start 0." and "end Time." are the default values,
%so we can omit them.

A mere two lines, much reduced from the original.

Requirements are another feature provided by Thucydides:


%r defines a requirement - in this case, the Owner
%must have enough money to buy the feast
r req_money_for_feast:
%a "history main." line could be added here, but it's the default
%this gets bindings too.
  type {money, Owner}.
%"start 0." and "end Time." are the defaults.
%There could be an "args []." line in here, but we omit it
%since the list is empty.
%check is the value-checking function; it gets bindings, too.
%the default check function is "check Value == true."
%it's worth mentioning that type, start, and end will all be omitted,
%in which case only the check function will be used.  This is generally
%used to verify properties of the Bindings.
  check Value > MealCost.

This is a sight better than a six-argument ?REQ macro, surely!

The next feature we'll examine is the final element in the TADL subset of TSDL, the Incident Archetype:


%this is an incident archetype definition
i eat_feast(Owner, MealCost): 
%We could say "histories [main].", but that's the default.
%refer to previously defined f_arc "full"
  set full.
  set poorer.
%we could define a new fluent inline with "set new_fluent(Arg): ... end.",
%then defining a fluent as above.
%refer to previously defined cancel
  cancel cancel_diet.
%cancels can be defined inline too: "cancel unwanted_fluents: ... end."
%refer to previously defined req
  require req_money_for_feast.
%requirements can be inline too: "require important_req: ... end."

This lightning tour of TSDL syntax bears some explaining. The basic element of TSDL is the "definition", which can be one of seven types:

q
An HQL query, just like in a .HQL file.
f
A fluent archetype
c
A cancel search
r
A requirement
i
An incident archetype
b
A binder function
follow
A follow definition

We haven't looked at binders and follows yet, but each definition has roughly the same form:


type name(arguments, if applicable):
  parameter value.
  ...
  subdefinition name(arguments, if applicable):
    subdef-param value.
    ...
  end.
  parameter value.
  ...

The distinction between parameters and definitions is straightforward: the former begin immediately and end with a period, and the latter begin after a colon and end automatically (or, in the case of inlined definitions of types f, c, or r, with an end. tag. The end tag can be omitted for the other types when inlined). Sub-definitions can be nested.

The other thing to note is that virtually all of the parameters take functions of (Time, Bindings) as their arguments.

Since the definition of binders and follows have various semantic consequences, we'll leave the discussion of TSDL there for today.

Trigger Happy

Actually triggering incident archetypes, given bindings and time, is done by thucydides:trigger() in the local client interface and a similar message in the remote client interface. Triggering an incident also checks the requirements and triggers any incidents that might follow it at that particular time. Trigger can return three values:

not_met
The requirements for this incident aren't met at the given time.
paradox
Triggering this incident is incompatible with the future state of the world, and would cause a paradox. It isn't triggered.
{ok, TriggeredIncidents}
TriggeredIncidents is the list of incidents that were triggered as a result of triggering the given incident; this can happen if a follow is set up to immediately react to an incident. Follows will be described in a forthcoming article.

Hopefully, TSDL reads more plainly than raw Erlang, even though it incorporates some Erlang syntax. If there are any questions, please e-mail me or leave a comment below.

Labels: , , ,

Read more...

Joe Osborn 2008-04-24

0 Comments

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:

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

Read more...

Joe Osborn 2008-03-11

0 Comments

Filling in the Blanks: Procedural Glue

This is an essay in the Herodotus series, and a followup to Designing a Story. Please read at least Introducing Herodotus before proceeding.

A Sticky Situation

At the end of the last essay, we found ourselves with a problem: We had all these simulation constructs, but no clear way to, well, construct them. This essay will shed some light on how stories in Rboehm's style could be built.

Pick and Choose

The fundamental variable unit of a Scene, and therefore of a Quest, is a Goal. If we're looking to build a Asker asks Provider for Item goal, we should probably pick the Item based on the Asker. Let's assume that the only Asker for now is the Farmer, though later we can look at ways to pick those too.

Recall that Characters have desires. In that case, we might want to pick an Item from among those desires. This gives us a nice little object-oriented and time-sensitive way to pick which Item to hunt for. And if all of a Character's needs are satisfied? Then we pick a different Character.


//First, we need some new metadata for goals:
AskForItemGoal := Goal clone do(
  item ::= nil
  asker ::= nil
  askerTypes ::= list("consumer")
  provider ::= nil
  providerTypes ::= list("provider")
)
GiveItemGoal := Goal clone do(
  item ::= nil
  receiver ::= nil
  receiverTypes ::= list("consumer")
  giver ::= nil
  giverTypes ::= list("provider")
)
//Then later, in our goal creator...
createAskForItemGoal := method(asker, provider,
//In Thucydides, this could be:
//asker desires(now)
  currentDesires := history fluentValue("desires", asker, now)
  if((currentDesires == nil) or(currentDesires size == 0),
//This one is no good, pick a different one
//Other conditions for picking different characters could include
//incompatible alignment, national affiliation, location, and so on.
    newAsker := chooseDifferentCharacter(AskForItemGoal askerTypes, list(asker))
    return createAskForItemGoal(newAsker, 
                                provider)
  )
  desiredItems := currentDesires anyOne
//In Thucydides, this could be:
//SimObject clone setCategories(desiredItems) allActiveInAt(h, t)
  actualItem := history fluentValue(desiredItems, nil, now) anyOne
  AskForItemGoal clone setAsker(asker) setProvider(provider) setItem(actualItem)
)
    

And bam! We get an Item for the Quest that's relevant to the Asker. Now, in the supporting infrastructure, we can spawn in such an Item, perhaps in somebody's inventory or on a spawnLocation in some cave. Perhaps Items have some metadata saying what kinds of places they can spawn in.

About Types

This would be a good place to say something about types in Herodotus and in general. Herodotus uses a very simple mechanism for matching types, where the types requested must all be present in the type matched against. For example, [animal, cat] matches [animal], [cat], and [animal, cat], but doesn't match [animal, dog] or [construction-vehicle, cat]. In the future, Herodotus might use a matching system with rankings, but for now, this is how it works. So, if we define a Donut's type to be [item, food, sweet, round, donut], and if we have a Goal which is looking for a [food, sweet], then the Donut will surely match; but if it's looking for [food, meat], there will be no match. That is, unless there's a Donut clone of type [item, food, meat, round, donut]. In the end, the utility of this type system depends on how consistently it's applied.

Calculated Character Creation

Let's move to a slightly more complex topic: the creation of characters expressly for the purpose of a Scene. This occurs when no existing character is available to fill a role, and it could include, for instance, spawning in the particular Kobold carrying the candles required to fulfill the quest; or it could be the visiting diplomat from another nation. The benefit of doing this with Herodotus is twofold:

  • Character archetypes can be chosen from a broad pool, based on the quest, and the archetype chosen can influence the experience.
  • Spawned characters, if desired, can be implanted along with their own personal history, provided that history doesn't overlap or damage existing timelines.

To clarify the second point a bit, we'll use the example of the visiting diplomat. Instead of just spawning a "diplomat" at the requisite location, we could also place his time of birth, his nation of origin, his service record, and so on; making him effectively a permanent character that had been there all along. This way, he can be used for other quests later on, and a mystery-solving Scene which required finding out the diplomat's history could be created, too. Mid-timestream insertion is currently dangerous in Herodotus. Future versions will make it safe to use as a general technique, but for now, please just be sure that the fluents placed in the middle of a time stream don't violate the preconditions of later fluents or the invariants of prior fluents.

The synthesis of characters is a complex business; and since I'm preparing for GDC next week, this essay will have to stop here. Hopefully, the approach above shows that the fundamental technique is to query the history, then decide what kind of thing to spawn or use. Future articles will, having explored this path a bit, return to the starting-point and evaluate Herodotus as a service; a Herodotus Simulation Language and Herodotus Query Language will be proposed; and the quest generator will be revisited in a more formal framework. Then, we will explore the application of Herodotus to a game called Stranded, designed by Rob Rix and myself.

If any GDC attendees would like to get in touch with me, please send an e-mail.

Labels: , , ,

Read more...

Joe Osborn 2008-02-12

0 Comments

Designing a Story: Building Narratives from Story Atoms

This is an essay in the Herodotus series. Please read at least Introducing Herodotus before proceeding.

Rboehme's Quest Generator

Areae's Metaplace is an upcoming virtual world platform, construction kit, and social network. One of the exciting ideas in the days leading up to its launch is the plan by Rboehme and others on the Metaplace forums to write a procedural quest generator. Since this sort of algorithm is precisely what Herodotus was written for, I hope to contribute to the project by providing a Herodotus simulation back-end for it. This essay will also introduce Thucydides, a simulation toolkit for Herodotus.

The Father of History and the Father of Lies

Herodotus, the man, went by an additional epithet: "The Father of Lies". This ignominious label was conferred upon him by his critics who detested his ways of presenting multiple conflicting accounts of a story and his habit of writing on local folklore or third- or fourth-hand reports. Thucydides, a slightly later historian, introduced the "objective" approach to history that historians use today, meaning that he competes for the title of "The Father of History". Since Thucydides's work was more responsible and more restrained, I thought it just that the technology which provided a framework for history and a clear structure for the simulation framework should be called Thucydides.

Thucydides uses the context feature of Fluents and the forwarding mechanism of Io to conceal the inner workings of Herodotus from client programs. A Thucydides SimulationObject triggers an Incident when it is created or destroyed, is uniquely identifiable among other SimulationObjects, and passes on any slot requests such as age or height, parameterized with a time, to the History in which they're defined. What this means is that a lot of duplicated or error-prone Herodotus API can be replaced with the use of these SimulationObjects. Furthermore, it's possible to mix Thucydides and Herodotus code, as long as the user is careful not to violate any assumptions the SimulationObjects might have about whether their represented contexts are still valid, whether their histories still exist, etc.

In this essay, we'll only be considering a single History, and will attempt to show how Thucydides can express Rboehm's Quest Generator.

About the Simulation

As I understand it, Rboehme's basic approach is to define some terms as follows:

Story
A context in which Quests take place.
Quest
A series of Scenes designed to express a unit of Story.
Scene
A setting involving specific sets of necessary, sufficient, and optional goals.
Character
An entity placed at a specific Location, with a Memory of things which have occurred to him before in the short, medium, and long terms. Also includes a list of Goals which can be used to feed the quest generator, as well as a list of likes and dislikes and a list of relationships to other Characters.
Role
Provides a list of Goals that a Character fulfilling that Role can choose from. For instance, a Priest might choose the "Peaceful Resolution" Goal for the "Ender of the Brawl" Role, whereas a Barbarian might choose the "Pacify By Force" Goal. Also includes information about whether the Character is a major, minor, or walk-on personage. These Roles and their Goals drive AI behavior or PC options.
Goal
An abstract Goal (ROLE brings ITEM to ROLE, ROLE compels ROLE to ACTION, ROLE removes ROLES from AREA, etc). Goal slots are filled with Roles provided by the Scene.

For example, in a standard "fetch quest", the first scene, or Start, of the Quest would include the part where the player approaches the NPC and asks for a mission. Its Goal would be ROLE asks ROLE for REQUEST, with the Roles being CUSTOMER and DELIVERYMAN respectively, and the REQUEST being for a particular ITEM. This ITEM would be determined by the CUSTOMER's Location, likes/dislikes, and other attributes. This Goal would be part of the "Customer" Role, and the "Deliveryman" (our player) must wait for it to happen. It might happen as part of conversation, or the distraught NPC might run up to the player and demand help.

In turn, this Scene would seed the Goal for the next Scene: ROLE brings ITEM to ROLE, with DELIVERYMAN and CUSTOMER being the Roles in order this time, and the ITEM being the one previously decided. It is in this next scene, Finish, that the traditional MMO fetch quest takes place as the player completes that goal.

If this seems like a lot of work for a simple fetch quest, that might be excused; but consider that this description is not just one fetch quest, but every fetch quest. By varying the ITEM and its Location (or Owner), or by using an ITEM which requires some special preparation before it can transform into the "true" ITEM, all kinds of fetch quests can be produced. The primary benefit of quest synthesis is that you can script the variation into the objects, rather than into the quest itself.

In terms of an overarching Story, the player's perceived Goal can shift radically from Quest to Quest depending on the Goal used to reach completion at a given Quest. If this Goal is a "failure"-like Goal, that has ramifications on what Quests are available afterwards; if it's a success and a part of a chain, the future Quests themselves continue the theme.

In Terms of Herodotus

Now, we'll rephrase these building blocks of Rboehme's quest simulation as Herodotus constructs, building a quest where a farmer wants a donut from the player. A delicious donut is hidden in the nearby cave, but a passable donut is available on the farm itself. Once we've met the requirements of Herodotus, we'll again recast the simulation as Thucydides entities, just to show how the process works.

We'll build from the simplest components of the system up to the most complex ones.

Location, location, location

First, we'll place the static tokens in the world. A complete implementation would include a large number of Locations for quests to take place in, or Characters to live in, but for now we'll make do with three: Nowhere; a Farm; and a Cave. To implement these in straight Herodotus, we would say something like this:


history := History clone

Location := Object clone
Nowhere := Location clone
CreateNowhere := Incident clone do(
  setFluent(FAppendContext create(list("locations", "nowhere"), Nowhere))
  setTime(0)
)
CreateNowhere occurIn(history)
//And later, in our quest generator...
Farm := Location clone
Cave := Location clone
CreateFarm := Incident clone do(
  setFluent(FAppendContext create(list("locations", "farms"), Farm)) 
  setTime(0) 
)
CreateCave := Incident clone do(
  setFluent(FAppendContext create(list("locations", "caves"), Cave)) 
  setTime(0)
)
CreateFarm occurIn(history)
CreateCave occurIn(history)
  

In Thucydides:


history := History clone
//Go ahead and use this history for all SimObjects
SimObject setHistory(history)

Location := SimObject clone appendCategory("locations") setSpawnTime(0)
Nowhere := Location clone appendCategory("nowhere")
Nowhere spawn
//And later, in our quest generator...
Farm := Location clone appendCategory("farms")
Cave := Location clone appendCategory("caves")
Farm spawn
Cave spawn

From here, we can still do all the usual Herodotus queries on that History; spawn just triggers an Incident, after all.

Things and Stuff

Continuing with the simple pieces, let's consider Items next. Items can provide arbitrarily complex goals, but for now we'll treat Items as tokens to be grabbed. For now, we'll just make two Donuts of differing quality. In raw Herodotus:


Item := Object clone do(
  categories ::= list("items")
)
//And later, in our quest generator...
Donut := Item clone do(
  categories := list("items", "donuts")
//On a scale of 1-10
  baseQuality ::= 5
)
LousyDonut := Donut clone setBaseQuality(2)
TastyDonut := Donut clone setBaseQuality(8)
CreateDonut := Incident clone do(
  donut ::= nil
  loc ::= Nowhere
  setFluent(FAppendContext create(donut categories, donut))
//Establish the donut's start-position and initial quality
  setFluent(FReplaceConstant create("location", donut, loc)
  setFluent(FReplaceConstant create("quality", donut, donut baseQuality)
//Donuts degrade as time goes on.
//If one unit of time is one hour, a donut is absolutely bad
//after 24 time units, or one day.
  setFluent("quality", donut, h, t, 
    ((1 - (t - time)) / 24) max(0)
  ) stackBy(MultiplyNumber)
  setTime(0) 
)
CreateDonut clone do(
  setLocation(Farm) 
  setDonut(LousyDonut)
  occurIn(history)
)
CreateDonut clone do(
  setLocation(Cave)
  setDonut(TastyDonut)
  occurIn(history)
)
  

And in Thucydides:


Item := SimObject clone appendCategory("items")
//And later, in our quest generator...
Donut := Item clone appendCategory("donuts") do(
  setQuality(5)
  setLocation(Nowhere)
//Intrinsics are a list of Fluents that go hand-in-hand
//with a SimObject and are applied as soon as it spawns.
  intrinsics := list(
//Donuts degrade as time goes on.
//If one unit of time is one hour, a donut is absolutely bad
//after 24 time units, or one day.
    Fluent create("quality", self, h, t, 
      ((1 - (t - time)) / 24) max(0)
    ) stackBy(MultiplyNumber)
  )
)
LousyDonut := Donut clone setQuality(2) setLocation(Farm)
TastyDonut := Donut clone setQuality(8) setLocation(Cave)
LousyDonut spawn
TastyDonut spawn
  

The fascinating trick in this example is the way the "setQuality" and "setLocation" calls work. Note that those methods aren't defined on Item or Donut or, indeed, on SimObject! So, how are they resolved?

They are handled by SimObject's forward(), which deals with unrecognized messages, and the rule they use is as follows: If the message starts with "set", remove the "set" and treat the lowercase name as a fluent type. If the object hasn't spawned yet and no time argument is provided, append to the Intrinsic Fluents an FReplaceConstant Fluent with the given fluent type and using the SimObject as context. If the object has spawned already, require a time argument in the first position. If there's a time argument, create and apply a new Incident using an FReplaceConstant Fluent. SimObject's forward mechanism works similarly for messages beginning with "add", "subtract", "multiply", "divide", "append", and "remove". This removes the need for a lot of very similar Herodotus API uses.

Note that these only create one-way links - if you take two SimObjects A and B and perform A appendFriend(B), a fluent of type friend will be created with the context {A} and a result list containing B. The reverse relationship will not hold. If Friendship is always two-way, then it might be reasonable to create a Friendship Relationship and attach both A and B to it via something like Relationship clone appendCategory("friends") setParticipants(list(A, B)) setStrength(0.5). Relationship is a Thucydides SimObject clone which is responsible for maintaining simple relationship statuses between a group of SimObjects. It's very simple, but it provides a nice single point of use. A Relationship's participants, strength, and other slots are, of course, time-based, mutable, and queriable as usual.

Goals and Roles

Here is where we need to think a little more about what we're doing, exactly. A Scene's Goal is a test of whether some conditions are met. There are several ways we can express this:

  • An Invariant on a Scene. If a Goal is met, the Scene ends. This means that subsequent Scenes must handle any "mess" that results from a Scene ending abruptly. This is the only way that Herodotus can automatically handle Scene endings.
  • A plain Object property of a Scene and of various Roles. As actions are performed and Incidents are generated and added, check to see if any Scene's Goals are met. If so, permit them to terminate gracefully.
  • As above, but instead creating a Goal SimObject and associating it through Relationships with a Scene.

The difference between the first two and the last one is whether Goals are mutable during a Scene. This comes down to a question of whether a "changing goal" means "an alternate goal that was always available", "the goal of a new scene triggered by the 'failure' of this scene", or "an actual shift in goals of the same scene". Personally, I think the middle option is the best, and a Quest can potentially be many Scenes in a row, with each Scene having one static set of goals. So, if the goal "shifts" from Player delivers the Letter to the Captain to Player reports to the Captain that the Letter has been destroyed, what actually occurred is that the Goal Player discovers the Letter's destruction was achieved, which ended the first scene and segued into the second scene, which included a primary goal of Player reports to the Captain that the Letter has been destroyed. Fluent-driven objects are powerful, but add complexity. If it's possible to simplify the model by making certain parts static, feel free to do so.

Now then, let's express two Goals: Farmer asks Player to bring him a Donut and Player delivers a Donut to the Farmer


//This is a barebones Goal with a very simple completion check.
//There's no reason Goals couldn't check certain fluent values
//on their Roles, or other arbitrary behavior.
Goal := Object clone do(
  availableTime ::= 0
  satisfiedInAt := method(h, t,
    h fluentValue("satisfied", self, t)
  )
)
AskForItemGoal := Goal clone do(
  item ::= nil
  asker ::= nil
  provider ::= nil
)
GiveItemGoal := Goal clone do(
  item ::= nil
  receiver ::= nil
  giver ::= nil
)
//And later, in our quest generator...
AskForDonutGoal := AskForItemGoal clone do(
  setItem(Donut) 
  setAsker(Farmer) 
  setProvider(Player)
)
GiveDonutGoal := GiveItemGoal clone do(
  setItem(Donut)
  setReceiver(Farmer) 
  setGiver(Player)
)
    

The above example actually looks identical in Thucydides. This is to be expected, since none of this uses Herodotus. Now, just to illustrate a fancier satisfiedInAt condition, we'll show what would happen if the GiveItemGoal were to check for evidence of a Transaction between the giver and receiver involving that item.


Transaction := Object clone do(
  aItem ::= nil
  bItem ::= nil
  aParty ::= nil
  bParty ::= nil
  aItemCost ::= 0
  bItemCost ::= 0
  time ::= 0
)
//...
//This is just an example of a complex goal in GiveItemGoal.
//It could just as easily have been done on the accomplishing side
//by manually setting "goalSatisfied".
  satisfiedInAt := method(h, t,
    h fluentValue("transactions", nil, t) detect(i, v,
//Make sure this transaction is from the giver to the receiver,
//that it involves the right item type, and that it happened after
//the goal became available
      (v aParty == giver) and(
        v bParty == receiver) and(
        v aItem categories containsAll(item categories)) and(
        v time > availableTime
      )
    )
  )
  

And, in Thucydides, a couple of small changes:


Transaction := SimObject clone do(
  appendCategory("transactions")
//These are all static, so we don't need to change them,
//which means they're "real" slots and not Fluent slots.
  aItem ::= nil
  bItem ::= nil
  aParty ::= nil
  bParty ::= nil
  aItemCost ::= 0
  bItemCost ::= 0
//Time is covered by the SimObject.
  //time ::= 0
)

  satisfiedInAt := method(h, t,
//Make sure this transaction is from the giver to the receiver,
//that it involves the right item type, and that it happened after
//the goal became available
    Transaction allActiveInAt(h, t) detect(i, v,
      (v giver == giver) and(
        v receiver == receiver) and(
        v item categories containsAll(item categories)) and(
        v time > availableTime
      )
    )
  )
    

There's not much to be gained in this specific case from using this feature, though, so we'll stick with the simpler version above. As for actually fulfilling these goals, that can be done in places like the hypothetical giveItem() or converseWithNPC(), or some other place. There, too, we can record in the receiving character's inventory fluent the receipt of the good or the bad donut, and even influence their mood based on how good a donut it was.

Next, let's introduce Roles. A Role, for a Scene, includes a set of Goals, a list of characters, and some metadata. In the Scene where an Asker asks a Provider to bring him an Item, the Asker Role would have a single Goal, which is within the Scene's ending goals, and the Provider would have none, except implicitly to be asked about bringing the Item. The Item is chosen from among the likes or needs of the Character fulfilling the Asker Role.

We'll express Roles, too, as static data, and assume that once a Character is set on a Role, he won't switch or lose that Role for the duration of the Scene.


Role := Object clone do(
//Categories are metadata about the sort of role it is.
  categories ::= list()
//The characters slotted into this role.
  characters ::= list()
//Those goals that the characters should want to fulfill
//to properly execute this Role.
  goals ::= list()
)
//And later, in our quest generator...
//Scene one:
DonutAsker := Role clone do(
//This "consumer" category means "someone who takes something in"
  setCategories(list("consumer"))
//In other words, it's the Farmer in this Scene.  Generally, a Category
//is like a meta-Role that spans the whole Scene.
  setCharacters(list(Farmer))
  setGoals(list(AskForDonutGoal))
)
DonutProvider := Role clone do(
//The Provider is "someone who supplies something to a consumer".
  setCategories(list("provider"))
  setCharacters(list(Player))
)
//Scene two:
DonutReceiver := Role clone do(
  setCategories(list("consumer"))
  setCharacters(list(Farmer))
)
DonutGiver := Role clone do(
  setCategories(list("provider"))
  setCharacters(list(Player))
  setGoals(list(GiveDonutGoal))
)
    

Easy enough, and identical in Thucydides, since we assume that Roles are, basically, unchanging tokens.

Building Character

Since this example is already running long, we'll make Characters as simple as possible: a list of desired items and a location. In a full implementation, we'd include personal goals, memories, and so on, but for now we don't want to make it seem like I'm being paid by the word. We'd like Characters to move around and shift their desires, though, so let's go ahead and see that in Herodotus:


Character := Object clone
CreateCharacter := Incident clone do(
//Initial state.
  desires ::= list()
  location ::= Nowhere
  character ::= nil
  setupFluents := method(
//As usual, add the context...
    setFluent(FAppendContext("characters", character))
//Then add the initial state, like desires, and
//link it to the character...
    setFluent(FReplaceList("desires", character, desires))
//Add the location too
    setFluent(FReplaceConstant("location", character, location))
  )
)
//And later, in our quest generator...
Farmer := Character clone
Player := Character clone
//Remember, this is an Incident.
CreateFarmer := CreateCharacter clone do(
//The Farmer wants donuts
  setDesires(list("donuts")) 
//and he lives on the Farm
  setLocation(Farm) 
//and he's represented by Farmer
  setCharacter(Farmer)
)
CreatePlayer := CreateCharacter clone do(
  setLocation(Farm)
  setCharacter(Player)
)
CreateFarmer occurIn(history)
CreatePlayer occurIn(history)
    

In Thucydides:


//Thucydides makes this much smoother
Character := SimObject clone do(
  appendCategory("characters")
  setLocation(Nowhere)
  setDesires(list())
)
//And later, in our quest generator...
Farmer := Character clone do(
//Fluent appending works on a whole list at once, so it uses
//the plural.  Sorry, but it was the easiest way to do it
//with the forwarding mechanism.
  appendDesires(list("donuts"))
  setLocation(Farm)
)
Player := Character clone do(
  setLocation(Farm)
)
Farmer spawn
Player spawn
    

Note the use of "appendDesires", one of the forward-handled methods from before.

Making a Scene

The Scene level is the first place we see tying together all these independent objects. Recall that a Scene has a set of Roles and some Goals, and if certain of those Goals are met the Scene is concluded. Fortunately, scenes are static! There's nothing in a Scene that must vary with time.


Scene := Object clone do(
//The roles appearing in the scene
  roles ::= list()
//The goals that, when any are accomplished, satisfy the scene
  finishGoals ::= list()
//A list of blocks of the form (scene, oldScene, h, t)->bool
  preconditions ::= list()
//Delegate finished status to goals
  satisfiedInAt := method(h, t,
    finishGoals detect(i,v,
      v satisfiedAtIn(h, t)
    )
  )
//Is it valid to segue into this?
  canSegueFromInAt := method(oldScene, h, t,
//If any precondition block forbids it, fail to segue
    preconditions foreach(i,v,
      if(v call(self, oldScene, h, t) not,
        return false
      ))
    )
//Otherwise, segue away
    true
  )
//Pick the roles that match the given categories
  rolesFor := method(categories,
    roles select(i, v, v categories containsAll(categories))
  )
//Set up role characters based on previous scene's roles
  segueFromInAt := method(lastScene, h, t,
//You could do other stuff in this method too, like set fluents
    lastScene roles foreach(i,v,
      rolesFor(v categories) foreach(i2, v2, 
       v2 characters appendSeq(v characters)
      )
    )
  )  
)
//And later, in our quest generator...
SceneOne := Scene clone do(
  setRoles(list(DonutAsker, DonutProvider))
  setGoals(DonutAsker goals)
)
SceneTwo := Scene clone do(
  setRoles(list(DonutReceiver, DonutGiver))
  setGoals(DonutGiver goals)
  setPreconditions(list(
    block(scene, oldScene, h, t,
//Ensure that the question has been asked.
//For now, we only have these two scenes, so it doesn't matter,
//but this way shows how scene chaining like this might be done.
//In other words, "ensure that the goal which was accomplished was
//the ask-for-things goal".
      askGoal := oldScene rolesFor("consumer") first goals first
      h fluentValue("satisfied", askGoal, t)
    )
  ))
)
    

This might be all that's needed of a Scene. The important part is that Scenes are fairly self-sufficient - they look into the previous scene to see if they can execute, and to grab the characters they need for their roles. Note that the item involved between these scenes is encoded into the goals themselves.

At this point, one might be justified in saying that it's sleight of hand to simply provide these "And later..." definitions, but in the next essay, I'll show how these might be generated.

Quest and Answer

A Quest, recall, is a series of Scenes. Since Scenes handle their own entrances, and the change of currentScene over time isn't really important to track, Quest can be fairly simple:


Quest := Object clone do(
//All the Scenes that might show up in this Quest.
  possibleScenes ::= list()
//All the Scenes that count as endpoints for the Quest.
  finaleScenes ::= list()
//The currently active Scene.
  currentScene ::= nil
//The Scenes that have been played.
  completedScenes ::= list()
//Housekeeping.
  init := method(
    setCompletedScenes(list())
    self
  )
//Pick and segue to the next Scene.
  chooseNextSceneInAt := method(h, t, 
//Bail if we're in the middle of a Scene.
    if(currentScene satisfiedInAt(h, t) not, return false)
    if(satisfiedInAt(h, t),
      return true
    )
    available := possibleScenes select(i,v,
  completedScenes contains(v) not and(
   canSegueFromInAt(currentScene, h, t)
  )
 )
    completedScenes append(currentScene)
//We can choose randomly here, but a real implementation
//would be smarter and replace the simple 'canSegue...' with
//some numerical value for how good a match it is.
    next = available anyOne
    next segueFromInAt(currentScene, h, t)
    setCurrentScene(next)
    true
  )
  satisfiedInAt := method(h, t,
    finaleScenes contains(currentScene) and(
      currentScene satisfiedInAt(h, t)
    )
  )
)
//And later, in our quest generator...
DonutQuest := Quest clone do(
  setPossibleScenes(list(SceneOne, SceneTwo))
  setFinaleScenes(list(SceneTwo))
  setCurrentScene(SceneOne)
)
//And somewhere in our game code...
while(
//...
//Try picking the next Scene.
//If it returns true, something happened.
   if(currentQuest chooseNextSceneInAt(h, t),
//Is the quest over?
     if(currentQuest satisfiedInAt(h, t),
//The quest is over!
       startNextQuest()
       ,
//We must have a new scene...
       handleNextScene()
     )
  )
//...
)
 

I say that the currentScene isn't important to track historically because what's important about a Scene happening in a Quest isn't the Scene itself, but the Fluents that are set as the Scene transpires. Also, the stuff in the "game code" section above could be much nicer and written much differently - I just present it as a naive approach to using this kind of data.

So where does Herodotus really come in? That would be in the Character memories, Goal tracking, quest histories, next-Scene picking, Scene conclusion, and so on. Storing this information in Herodotus makes it a lot easier to write fancier quest generators, design better NPC simulations, and provide ays for Character actions to materially change the game world.

In the next essay, we'll look at how to generate these Donut quests and we'll make a simple text-based adventure game using Thucydides.

Thanks to Rboehme and the other participants in the questgen project.

Labels: , , ,

Read more...

Joe Osborn 2008-02-04

0 Comments

A Geological Inquiry: Grounding Herodotus's Theory

This is a follow-up to Introducing Herodotus, and expands on that essay with concrete examples. Please read it before proceeding.

The challenge

Create a simple terrain generator to show how even incidents in geological time can "count" as historical occurrences. Also, show an example of how an application's structure might, rather than access and store data directly, make use of various objects wrapping a single history to provide simple, time-centric access to data.

...

Using the library

The current development version of Herodotus is available here. Note that it will change as time goes on. It's written as an addon for a language called Io, and can be installed in the following way:

  1. Download Io
  2. Add the Herodotus, Macrobe, Funktion, and SimpleGraphics folders to Io/addons. These were all written by me, Joe Osborn, in 2007-2008, and I provide them under a BSD license. Macrobe is required for Herodotus, Funktion provides useful simulation mathematics, and SimpleGraphics is a simple GLUT-like toolkit. Other addons required for this sample include OpenGL(and its dependencies Image, Font, and Box), Range, and Random, which are all included with Io. Others can be removed from the Io/addons folder without harm.
  3. In the base Io directory, run the following commands:
    • make
    • sudo make install
    • sudo make linkInstall
    • make testaddons
  4. Ensure that the test output looks something like this:
    
    Box        - PASSED
    Funktion   - PASSED
    Herodotus  - Warning, inverting lowpass or highpass 
    stack modes cannot really work properly.  Change the 
    definition of inversion to something like "remove it 
    from prevFluents"
    PASSED
    Macrobe    - PASSED
    Random     - PASSED
    Range      - PASSED
    SimpleGraphics - PASSED
    Thucydides - PASSED
    The warning on Herodotus reflects a temporary design concern, and has no impact on the functionality of this example.

Once Herodotus is working, it should be available in any Io VM. The objects defined in Herodotus are defined in Herodotus/Herodotus/io/. For this example, it doesn't matter where the Inquiry is written, but the finished product will be available at Herodotus/Herodotus/samples/lander-final.io. So, to execute it, run cd Herodotus/Herodotus/sample; io lander-final.io.

Designing the simulation

Simulation design in Herodotus first begins with deciding what it is we want to model. In this case, we choose to model an expanse of lunar terrain marked by mountains and plateaus, subject to damage by occasional impacts. This might appear in a sci-fi game, perhaps a Lunar Lander clone.

Once we've decided what to model, we need to consider how to attach it to the application at large. In this case, a simple heightmap is probably sufficient. So, we know to design our Fluents with locational parameters, and we know to have them output numbers, for the purpose of rendering terrain at specific points. After the initial terrain generation, our inputs are the two kinds of event that can occur: A Landing or a Crash.

Finally, we need to start getting into the nuts and bolts of the simulation. For now, let's do this simply, and only use one fluent type, height. Later, we can introduce Fluents to identify particular mountains or craters, if necessary. But this is to be a bare-bones simulation, so let's treat it as such.

Fluents of type height stack using numerical addition, and are parameterized by h, t, x, and y.

Now that the Fluent alphabet is defined, we can consider the initial state Generator. This Generator will place some random number of mountains and plateaus at varying points inside a 100x100 2D plane. Each mountain is a Gaussian curve placed at a given point(centerX and centerY) and perturbed by a noise function of low amplitude. Each plateau has a plateau height, and is placed on top of a mountain, clipping to a percentage of the mountain's maximum height by means of a special stack mode, "Lowpass". To do this, we will introduce mountains and plateaus fluents to keep track of the mountains and plateaus, rather than relying only on height.

A single stone: lander-0.io


//Imports
  Macrobe
  Funktion
  Herodotus
//Create our History
world := History clone
//Define an Incident for placing mountains.
//This will be how we "create" mountains.
PlaceMountain := Incident clone do(
//The mountain information to be used
  mountain ::= nil
//This method is called automatically when the incident occurs.
//It's expected to fill out the Incident's fluents,
//requirements(require), and invariants(dependOn).
  setupFluents := method(
//Establish the context.  This is a predefined Fluent type.
    setFluent(FAppendContext create("mountains", mountain))
//This is a custom fluent of type height,
//with the context being the mountain slot of the parent Incident.
//The x and y arguments are provided after h and t.
    setFluent(Fluent create("height", mountain, h, t, x, y,
//Delegate the calculation of the height to the Gaussian mountain definition.
      mountain value(x, y)
//Use the AddNumber stack mode, which does what it sounds like.
    ) stackBy(AddNumber))
  )
)
//Create a 2D Gaussian curve with default parameters, except for X0 and Y0.
g := Gaussian clone setX0(50) setY0(50)
//We can use the curve itself as the "mountain".
PlaceMountain clone setMountain(g) setTime(0) occurIn(world)
    

Put the above in a file(such as lander-proto.io) and run io. Now, we can load that file:


Io> doFile("lander-proto.io")
==> true
    

And we can make queries on world.


Io> world fluentValue("mountains", nil, 0)
==>list(Gaussian_0x3d7330)
Io> world fluentValue("height", nil, 0, 50, 50)
==> 1
Io> world fluentValue("height", nil, 0, 25, 25)
==> 0
Io> world fluentValue("height", nil, 0, 49, 50)
==> 0.3678794411714423
    

So it looks like our dropoff is way faster than it should be. That's easy enough to fix, so after some experimentation I wound up with this:


g := Gaussian clone
g setAmp(50) 
g setA(0.01) 
g setB(0)
g setC(0.01)
g setX0(50) 
g setY0(50)
    

So it'll be taller, and less strangely steep. And now our heights look like this:


Io> world fluentValue("height", nil, 0, 50, 50)
==> 50
Io> world fluentValue("height", nil, 0, 49, 50)
==> 49.5024916874584022
Io> world fluentValue("height", nil, 0, 25, 50)
==> 0.0965227068113855
Io> world fluentValue("height", nil, 0, 25, 25)
==> 0.0001863326586039
    

One mountain in the middle of a landscape is pretty dull, so let's generate a few more like this:


seed := 0
r := Random clone
r setSeed(seed)
r value(0, 9) repeat(i,
  gaussValue := r value(0.001, 0.05)
  g := Gaussian clone
  g setAmp(r value(20, 50)) 
  g setA(gaussValue) 
  g setB(0) 
  g setC(gaussValue) 
  g setX0(r value(0, 100)) 
  g setY0(r value(0, 100))
//This is the only part where Herodotus constructs are involved.  Herodotus
//can integrate smoothly into existing simulation architectures.
  PlaceMountain clone setMountain(g) setTime(i) occurIn(world)
)
    

Replacing the old mountain generation with the above gets us to Herodotus/Herodotus/samples/lander-0.io. We can run it and make queries like these:


Io> world fluentValue("mountains", nil, 10) map(i, v,
)-> v x0 asString .. ", " .. v y0 asString .. " height: " .. v amp
)-> ) join("\n")
==> 84.4265744090080261, 60.276337037794292 height: 41.4556809491477907
84.725173725746572, 42.3654796788468957 height: 36.3464953214861453
38.4381708223372698, 43.7587209977209568 height: 39.3768234527669847
5.6712975725531578, 96.3662764057517052 height: 46.7531900526955724
47.7665111655369401, 79.1725033428519964 height: 31.5032456396147609

    

In other words, at time 10 we have a bunch of mountains kicking around, and they add up to form the landscape as a whole. We can see that effect in these queries:


Io> world fluentValue("height", nil, 10, 84, 40)
==> 27.9284088273150353
Io> world fluentValue("height", nil, 10, 84, 60)
==> 41.1351777985938796
Io> world fluentValue("height", nil, 10, 84, 50)
==> 4.617869013365393
Io> world fluentValue("height", nil, 0, 84, 50)
==> 1.7260724122134643
Io> world fluentValue("height", nil, 1, 84, 50)
==> 4.6178690133643858
    

A visual demonstration of the formation of these mountains is available in Herodotus/Herodotus/samples/lander-vis-demo.io. To execute it, run io lander-vis-demo.io and use the a and d keys to go back and forward in time. Here's what our current simulation looks like at t=3:

Screenshot of five mountains on a black plane

As a quick aside, it surely would have been possible to generate our terrain using Perlin noise or another technique. Those techniques can still work with Herodotus! For instance, we could define our height fluent by implementing such an interpolated noise function, or by applying many fluents, each for a single octave of the net noise(or we could use Funktion's Noise object). Instead of a basic mountains fluent, we might define a mountainsInRegion fluent which takes four arguments comprising a rectangle, and returns contiguous regions which have surpassed a threshold value. Or we might run a Generator that introduces mountains by examining the noise for patterns before applying it to the height fluent, and places these mountains as markers for some other game system. The point is, Herodotus is an extremely flexible system, and there's nothing preventing any existing forms of simulation design.

Noisome perturbations: lander-1.io

Just because our simulation doesn't use noise from the ground up, so to speak, doesn't mean we can't apply it. Let's modify the height Fluent of the PlaceMountain Incident. This will perturb the height according to a random value. We can easily solve this by adding an additional Fluent to that Incident, with one caveat: The height fluent value calculation must use the same context as this perturbation. If we were to apply the noise separately and read off "all fluents matching this type", it would effect all previously calculated heights at any point, regardless of which mountains were involved. Using contexts to limit the matched fluents breaks the processing into multiple steps, but allows for cleaner fluent definitions. In this particular case, however, speed concerns lead us to keep the perturbation calculation adjacent to the height calculation. Where possible, two Fluents of the same fluent type should be combined if the latter's result relies on the former's, or the dependent fluent type should only be calculated in specific contexts.


  setupFluents := method(
//Establish the context.  This is a predefined Fluent type.
    setFluent(FAppendContext create("mountains", mountain))
//This is a custom fluent of type height,
//with the context being the mountain slot of the parent Incident.
//The x and y arguments are provided after h and t.
    setFluent(Fluent create("height", mountain, h, t, x, y,
//Delegate the calculation of the height to the mountain's Gaussian,
//and perturb by the perlin noise.
      b := mountain gaussian value(x, y)
//An optimization here, since perlin noise is expensive
      if(b > 0.01,
//Vary by up to 30%
         b * (1-(mountain perturbation value(x, y) * 0.3))
        ,
        0
      )
//Use the AddNumber stack mode, which does what it sounds like.
    ) stackBy(AddNumber))
  )
)
Mountain := Object clone do(
  gaussian ::= Gaussian clone
  perturbation ::= Noise2D clone
)
seed := 0
r := Random clone
r setSeed(seed)
//A new Random and seed for the noise to keep the terrain similar
r2Seed := 1
r2 := Random clone
r2 setSeed(r2Seed)
r value(0, 9) repeat(i,
//To get broader, easier to see mountains, we've tweaked the a and c parameters
  gaussValue := r value(0.0001, 0.01)
  g := Gaussian clone
  g setAmp(r value(20, 50)) 
  g setA(gaussValue) 
  g setB(0) 
  g setC(gaussValue) 
  g setX0(r value(0, 100)) 
  g setY0(r value(0, 100))
  
  n := Noise2D clone
  n setSeed(r2 value)
  n setPersistence(r2 value(0, 1))
  n setNumberOfOctaves(4)
  mount := Mountain clone setGaussian(gaussian) setPerturbation(noise)
//This is the only part where Herodotus constructs are involved.  Herodotus
//can integrate smoothly into existing simulation architectures.
  PlaceMountain clone setMountain(mount) setTime(i) occurIn(world)
)
    

To see this in action, run lander-vis-demo.io again and press 1 to switch to this version(and 0 to return to the former version), which is the same as lander-1.io. If you want to see the intensity at a given point, click on it and the coordinates and intensity will be output to the command line. The interesting thing about this example is the introduction of the Mountain object, which wraps the Gaussian and the Noise2D. This is an example of a context object, and this style of use will be expanded on in an upcoming essay. And here's the image of lander-1 at t=3:

Screenshot of five mountains on a black plane, with perturbation

Even it out: lander-2.io

First, to make things more interesting, we'll increase the number of mountains by a factor of three. This will give the terrain a bit more character.


r2 setSeed(r2Seed)
(2*r value(0, 9)) repeat(i,
//To get broader, easier to see mountains, we've tweaked the a and c parameters
  gaussValue := r value(0.0001, 0.01)
//...
//This is the only part where Herodotus constructs are involved.  Herodotus
//can integrate smoothly into existing simulation architectures.
//Note that times don't have to be integral!
  PlaceMountain clone setMountain(mount) setTime(i/3) occurIn(world)
    

On top of that, we'll jump to t=10:

Screenshot of fifteen mountains on a black plane, with perturbation

Now, let's place some random number of plateaus:


//...
    ) stackBy(AddNumber))
  )
)

BecomePlateau := Incident clone do(
  mountain ::= nil
  heightFraction ::= .5
  height := method(
    heightFraction * mountain gaussian amp
  )
  setupFluents := method(
    setFluent(FAppendContext create("plateaus", mountain))
    setFluent(Fluent create("height", mountain, h, t, x, y,
//allow a little bit of height variation at the top
      height * (1 - (mountain perturbation value(x,y) * 0.2))
//warning, this clobbers -all- heights in the fluent stack.
//be sure to get heights context-by-context!
    ) stackBy(Lowpass))    
  )
)

Mountain := Object clone do(
//...
      PlaceMountain clone setMountain(mount) setTime(i/3) occurIn(world)
)

r2 value(5, 9) repeat(i,
  mount := world fluentValue("mountains", nil, 10 + i) anyOne
  if(world fluentValue("plateaus", nil, 10 + i) ?contains(mount),
    continue
  )
//Plateau height is a fraction of total height; let's just put it at half.
  BecomePlateau clone setMountain(mount) setHeightFraction(.5) setTime(10 + i) occurIn(world)
)
    

And here's what the world looks like at t=20

Screenshot of mountains and plateaus on a flat plane to illustrate lowpass + perturbation.

You might expect that the Lowpass stackmode would limit -any- height reading, and not just the "right" ones. In fact, this point returns to the one made earlier about mixing contexts. You're safe, as long as queries on height look something like this:


heightAt := method(x, y,
  history fluentValue(list("mountains", "plateaus", "craters"), nil, time) map(i,v,
    history fluentValue("height", v, time, x, y)
  ) sum
)
    

In other words, the guideline is something like: match as few fluents at once as possible, and try to match them as precisely as possible. This is especially important if you use Fluents which assume they're only acting within a certain context.

Game Over: lander-3.io

Now, it's high time some interactivity were added to this simulation. Since the primary activity in any Lunar Lander-style game is crashing, we're going to model it first. A Crash is an Incident which modifies height in a circle around the impact point, and acts sort of like an upside-down mountain. But craters also have those little rims, so we'll model it with a half-sphere and calculate the rim height from that, with linear dropoff. We'll also add crosshairs(moved with ijkl) to the visualizer and let the space bar trigger a crater of random size being placed at their center.

We approach this by adding a new context object(Crater), a new Incident(CreateCrater) which sets a craters and a height fluent, and some tweaks to the visualizer to create and apply these incidents. The new code is appended to lander-2.io to create lander-3.io:


Crater := Object clone do(
  radius ::= 20
  location ::= vector(0, 0)
  perturbation ::= Noise2D clone
)
CreateCrater := Incident clone do(
  crater ::= nil
  setupFluents := method(
    setFluent(FAppendContext create("craters", crater))
    setFluent(Fluent create("height", crater, h, t, x, y,
      dist := crater location distance(vector(x, y)) abs
      height := if(dist < crater radius,
//sphere part
        xPart := ((x - crater location x) squared)
        yPart := ((y - crater location y) squared)
        -((xPart + yPart - crater radius squared) abs sqrt)
        ,
//rim part
        (crater radius / dist) * (crater radius / 2)
      )
//and a little variation for flavor
      height * (1.1 - crater perturbation value(x, y) * 0.2)
    ) stackBy(AddNumber))
  )
)
    

So, here is our world at t=20 before a ship crashes:

Screenshot of mountains and plateaus on a flat plane, before ship crash in lower-left quadrant

And then, calamity strikes! Game over!

Screenshot of mountains and plateaus on a flat plane, with crater in the lower-left quadrant

Wrapping up

To recap, the concepts we covered included:

  • lander-0.io
    • Getting Io and the necessary addons up and running
    • Fluent types and contexts
    • Using built-in Fluents like FAppendContext
    • Defining custom fluents with arguments
    • Using non-standard StackModes such as AddNumber
    • Defining, creating, and applying Incidents
    • Making queries on a History
    • Flexibility of the Fluents mechanism
  • lander-1.io
    • When to combine fluents
    • Running the visualizer
    • Context objects
  • lander-2.io
    • Defining another Incident and context object
    • The importance of building the right FluentStack
    • Querying a History safely
  • lander-3.io
    • Defining another Incident, context object, and fluent type
    • Interactive creation and application of Incidents
  • lander-vis-demo.io. We didn't go over this explicitly, but it's worth looking at for more examples of using a History.
    • Dynamic loading of different Herodotus simulations(setLanderGen())
    • Use of SimpleGraphics and OpenGL toolkits
    • External fluent caching to make movements between identical time periods fast(goForwardOne() and goBackOne())
    • Proper querying of multiple fluent types and the use of context objects(resetContexts() and intensityAt())
    • Triggering Incidents as a response to user interaction(crash())

If any of these concepts seem unfamilar, please read over that section again, or send me an e-mail and I'll tweak this essay to make it clearer.

Labels: ,

Read more...

Joe Osborn 2008-01-27

0 Comments