Poem Quickstart

Getting started with Poem in Lua

Writing XBTs using the Lua implementation is the simplest way to get started with Poem.


You need to have a working Lua installation; if you want to run the examples from the XBT repository you should use LuaJIT since some of the libraries exist only for LuaJIT. Clone Xbt.lua from its GitHub repository and ensure that your LUA_PATH environment variable contains the path to the Xbt.lua module. If you use OS X or Linux with the bash shell, your .bashrc should probably include a line of the form

export LUA_PATH="$LUA_PATH;.../xbt/?.lua;.../xbt/?/init.lua"

(where .../xbt is the path to the Xbt.lua/src folder). On Windows you set the corresponding environment variable from the GUI.

To check whether the XBT framework works with your Lua installation, open a shell and change into the Xbt.lua/src folder and start Lua (or preferably LuaJIT) with the teacher/student example. You should see something like this (the actual numbers you see may vary depending on the version of the XBT framework you are using):

$ luajit -l example/teacher_student
XBTs are ready to go.
Robot rescue scenario (25000 steps)...
Random seed: 	lfib4 {\{-1233355477,-743959539,...},55}
Navigation graph has 200 nodes and 3960 edges.
... [several more lines]

Episode 1 	    reward:    -2928059 epsilon:     1 
  Teacher 1     samples:   0 	    bad choices: 24423  difference: 1705688
... [many more lines]
Episode 24751   reward:    9208537  epsilon: 	  0.01 
  Teacher 1     samples:   10514    bad choices:  7645  difference: 391240
Total reward = 820990052
Default with damage:
... [more of the same]

If you see this output: congratulations! You have just run your first robot swarm simulation using XBTs.

First Steps

To further explore XBTs we’ll now switch to some simpler examples. The file example/run.lua in the directory Xbt.lua/src contains several simple functions that create and tick XBTs; it includs some nodes from the file example/nodes.lua. An easy way to run the examples is from the Lua comand line. For example, if you want to execute the navigate_graph function, you proceed as follows:

$ luajit -e "ex = require('example.run'); ex.navigate_graph()"
Navigating graph...
Diameter:        	623.54149821804
Maxmin distance: 	76.837490849194	for node	21
Nodes:           	100	Edges:	984
1	->	1	[1]
1	->	2	[1->48->2]
1	->	3	[1->46->98->3]
1	->	4	[1->17->4]
1	->	5	[1->54->5]
2	->	2	[2]
2	->	3	[2->45->46->98->3]
2	->	4	[2->4]
2	->	5	[2->64->11->5]
3	->	3	[3]
3	->	4	[3->37->14->61->4]
3	->	5	[3->98->82->23->5]
4	->	4	[4]
4	->	5	[4->17->1->54->5]
5	->	5	[5]
Action table sizes: 	100	100

So, with all the preparation out of the way, let us define our first behavior tree. Obviously this tree should print Hello, world. and then succeed.

The first thing you need to do is to provide access to the XBT module. We therefore suppose that the beginning of your source file contains the line

local xbt = require("xbt")

without mentioning this for each example.

A simple XBT that executes a function and succeeds with the return value of the function as reward can be built using the function xbt.action. In the simplest case, xbt.action takes a function as argument, invokes this function with some arguments that we can ignore for now, and turns the result of the function into a successful XBT result. Let’s write this function first:

local function say_hello()
  print("Hello, world.")
  return 0

We then have to package this function into an XBT node and cause the node to be evaluated. In order to run each example in the file individually I tend to package these tasks into a function:

function ex.tick_hello_1 ()
  local node = xbt.action(say_hello)

As usual you can execute this function like this:

$ luajit -e "ex = require('example.run'); ex.tick_hello_1()"
Hello, world.

Woo Hoo! A real breakthrough in computer science! Or, …, maybe not. Well, anyway, we have successfully executed our first XBT. The call to xbt.action creates an XBT node that is stored in the local variable node; the xbt.tick function triggers the next (and only) evaluation step of the node.

You, dear alert reader, have probably spotted the number 1 that I have fiendishly tacked onto a innocent little function name and are now waiting with bated breath for the next installments of our saga. Fear not! There will be versions 2, 3, 4 and maybe even 5!

One thing you might ask is: What about the return value? Does anybody care that we have succeeded so gracefully? Of course someone cares, and that someone is called tick_hello_2:

function ex.tick_hello_2 ()
  local node = xbt.action(say_hello)
  local res = xbt.tick(node)
  print("xbt.tick() returned " .. res.status .. " with reward " .. res.reward)

In tick_hello_2 we store the return value of the xbt.tick function in the local variable res and then print the status and reward fields. There are four possible status values for results: it can have succeeded or failed, it may still be running or it may as yet be inactive. The latter is a bit of an odd duck since it is the result value of tick for a node that has never been ticked, so you might be justified in thinking it’s not particularly useful. Or that the guy who dreamt up XBTs (that would be me) is totaly bananas. And you would probably be right with the second part, but the existence of inactive states allows some advanced use cases for XBTs that are rather nice to have. For example a planning node might answer a first tick with an inactive result that returns the estimated cost of a plan, and only start executing the plan after a second tick. A reinforcement learning node sitting above a number of such planning nodes might use these estimates as part of its value function computation allowing the interleaving of planning and learning in an elaborate dance of reasoning engines. But we are getting way ahead of ourselves here.

A number of functions in the xbt module allow you to query results for their status and other properties: is_result returns true when a value is one of these four possible results, and false otherwise; is_inactive, is_running, is_succeeded and is_failed return true if their argument has the corresponding status and false otherwise; is_done and can_continue check whether a node needs to be ticked again (if and only if is_done is false) or can be ticked again to potentially improve a previous result (can_continue). These last two functions are useful if you want to implement just-in-time algorithms with XBTs. An XBT for which both is_done and can_continue are true has completed its task (because is_done is true) but might be able to improve upon its performance (because can_continue is true as well). Imagine, for example, a household robot the can vacuum you appartment and in addition perform other tasks such as cooking meals or ironing clothes. This robot is never really finished with vacuuming your apartment, since it will always be able to clean up just a little more dirt. However, at some point we want the robot to stop vacuuming and start cooking, lest we die of starvation in the cleanest apartment ever (and, to make things worse, in rumpled clothes).

Let’s try some of the queries on XBT results by writing a (slightly cumbersome) function that prints info about a result:

local function print_result_info (res)
  if xbt.is_result(res) then
    if xbt.is_running(res) then
    elseif xbt.is_failed(res) then
    elseif xbt.is_succeeded(res) then
    elseif xbt.is_inactive(res) then
    if xbt.is_done(res) then
      print("Is done.")
    if xbt.can_continue(res) then
      print("Can continue.")
    print("Reward: " .. res.reward)
    print(res:tostring() .. " is not an XBT result!")

To test the function call print_result_info on the various XBT result types. This also shows how you can create different kinds of XBT results should you need them:

function ex.test_print_result_info ()
  local res = xbt.running(1)
  res = xbt.failed(-10, "Out of patience.")
  res = xbt.succeeded(10)
  res = xbt.succeeded(5, true)
  res = xbt.inactive(0)

All result types take at least one argument, the reward for obtaining that result (which can be negative to indicate that obtaining this result incurred a net cost). Succeeded and failed results both have an optional second argument, but beware: the meaning of the second arguments differ. The additional argument of failed results simply adds a field containing a failure reason to the result; this can be helpful to figure out what went wrong when some part of the XBT does something unexpected. The second argument of succeeded is a Boolean that indicates whether the computation can be resumed to improve the result. Its default value is false.

Running this function results in the following output:

$ luajit -e "ex = require('example.run'); ex.test_print_result_info()"
Can continue.
Reward: 1

Is done.
Reward: -10

Is done.
Reward: 10

Is done.
Can continue.
Reward: 5

Can continue.
Reward: 0

There is probably little that is surprising: Nodes returning failed or succeeded results are done, nodes returning running and inactive results can continue. A succeeded node may optionally indicate that it can continue if there is a chance that it may improve its result by doing so; this is the only case where simultaneously being done and being able to continue makes sense. Obviously it makes no sense for a failed node to continue; it should have produced a result right away if it were possible to do so. A running or inactive node can never be done, since, well, they are still running or inactive.