Programming in Lua for Trunk Notes

Published
2012-09-05
Tagged

Here’s a thing I’ve been working on:

I’ve got all my GMing notes in Trunk Notes for iPad, which is still awesome1. I have pages for different plot threads and factions, people linked off of those, and their relationships with one another all mapped out. Tagging helps a heap - being able to just dump a list of everything tagged “Front” or “Faction” means I don’t forget about this one thing off to the side.

In Apocalypse World, the bad things in the world (or at least, the things that confront the party) are organised into Fronts. One front is a coherent set of threats that generally makes one thematic whole. The author, Vincent Baker, is very much a fan of making up plots on the fly based around these fronts - when things are feeling slow, you look at your fronts, pick one, and have it do something bad.

The problem is, at the gaming table it can be hard to generate good plots on the fly like that. We’re not all masters at improv, and I know that my sessions benefit when I have a list of possible events to fall back on. Even if I only spend half an hour before the game thinking about what might happen, those events, thought of without deadlines or pressure can be a hundred times better than whatever dreck I can come up with at the table.

“So that’s easy,” you’re telling me right now. “Just make a new page, write out what should happen, tag it ‘plot’ or something, then link to it from the front-page.”

Well, yes I could do that. But sometimes I want a series of plots that join together to help escalate a front, you see.

“So tag all of them, and then link them on the front page. This isn’t rocket science.”

Well, okay, i could do that. But then I’d never have a chance to play around with Lua, which is what this entry is mainly about.

The setup

Each front sheet has a number of one-line plots, which are effectively events that advance the story. Here’s a sample plotline:

  • One of the engineers is found dead near an airlock.
  • People report bangs and clatters in the air vents on the north side of the base.
  • Several reports of civilians being attacked by giant insect-like beings.

If you’ve played Apocalypse World, you might recognise these as the sort of events that appear on a countdown clock. Countdown clocks are a graphical way of representing escalating stakes as fronts get more and more threatening. Here’s how I’d write one down:

Obviously harder to do on a computer, so this isn’t a bad way of going about it. The problem is that these are one-liners - they don’t have their own page. And the other problem is that on my iPad it’s hard for me to get a quick overview of everything, and work out which pot is going to bubble over right now.

Thankfully, Trunk Notes has Lua baked in.

Lua

Lua is a pretty light programming language that strives to be “paradigm-free”. It’s got a relatively small feature set, and enough nuts and bolts to let you put the things you want on top of it to get it to do whatever it needs to. I’ve seen it mentioned in a couple of places as an alternative to AppleScript for some applications, but I’ve never really looked into it.

Since Trunk Notes has Lua support, and I had some spare time, I thought I might see how hard it was to make a small lua script to:

  • Find all the pages in my wiki tagged Front, and for each of them…
  • Find the section labelled #Countdown, then
  • Grab the first bulleted-list item from this section

Output these and I have a ready-to-go list of plots for when things get boring around the table. Seems pretty easy, right?

tl;dr (the code)

1
title_list = ""
2
for _,title in pairs(wiki.titles()) do
3
  page = wiki.get(title)
4
5
  is_a_front = false
6
  for _, tag in pairs(page.tags) do
7
    if tag == "Front" then
8
      is_a_front = true
9
    end
10
  end
11
12
  if is_a_front then
13
    _, title_end = string.find(page.contents, "#Countdown\n")
14
    if title_end then
15
      _,_,first_plot = string.find(page.contents, "(%* .*)", title_end)
16
      if first_plot then
17
        title_list = title_list .. first_plot .. " ([[" .. title .. "]])\n"
18
      end
19
    end
20
  end
21
end
22
23
return title_list

Don’t worry, I plan on going through this a bit at a time.

The code review

Let’s start with a quick overview of what I’ll be doing - grabbing all the initial plot points and sticking them in title_list, which gets returned to be output to a page.

1
    title_list = ""
2
    for _,title in pairs(wiki.titles()) do

Here’s the first catch, that had me caught for ages. While Lua has a for block in the style of a number of languages, including AppleScript and Objective-C, you can’t just throw the for an array - you have to run a method on it first. pairs() will take an array or hash (actually the same thing), and output a series of pairs for the for loop to eat up. For a hash, it’ll return key,value; for an array, index,value. Since we don’t care about the index, I’ve chucked that in _, which seems to be the throwaway variable of choice.

(Of note: arrays and hashes are both tables, which are simply collections of key,value pairs. An array is really just an integer-keyed table, which is why pairs() returns the index. From now on, I’ll refer to them as tables.)

wiki.titles() is a method supplied by Trunk Notes. It’s basically a table of all the page names in the wiki. There are several methods and attributes supplied so you can interact with the wiki.

1
  page = wiki.get(title)

Here’s another Trunk Notes method. wiki.get() gets a page with the given title.

1
    is_a_front = false
2
    for _, tag in pairs(page.tags) do
3
      if tag == "Front" then
4
        is_a_front = true
5
      end
6
    end

This chunk of code determines whether the page is tagged Front. Unfortunately, there’s no convenience method to determine if a table contains a particular value, so we have to do it ourselves. Once again we use the _/pairs() combo to get the values (this time from page.tags, Trunk Notes’ table of tags for the page), and then we simply run through each tag to see if it’s Front.

1
    if is_a_front then
2
        _, title_end = string.find(page.contents, "#Countdown\n")
3
        if title_end then
4
          _,_,first_plot = string.find(page.contents, "(%* .*)", title_end)
5
          if first_plot then
6
            title_list = title_list .. first_plot .. " ([[" .. title .. "]])\n"
7
          end
8
        end
9
    end

And this is where I actually add bits on to the title_list string. First, we only need to do this if our page is a front. If it is, however, we want to find out if the page contains the #Countdown header.

All the string methods are contained in string, which I think is just a big table, but I may be wrong. Regardless, string.find() will find the first instance of its second argument in its first, returning two values - where the match starts, and where the match ends. In other words:

1
    string.find('foobar', 'foo') -->1  3
2
    string.find('foobar', 'bar') -->4  6

All I care about is where the title ends, because if the page does have a #Countdown section I’m then searching for the first bulleted list item after it:

1
        if title_end then
2
          _,_,first_plot = string.find(page.contents, "(%* .*)", title_end)

Now I’m cheating. I said that string.find() returns two values - actually, it returns two values plus any captures. Because string.find() knows regular expressions.

This isn’t quite true - the Lua tutorial I used would like to point out that what it knows are patterns, thank you very much. But a lot of the features of regular expressions are available to you, and I take advantage of them here. The string (%* .*) matches any bulleted list item, and the parentheses mean that the method will capture the match and return it as a value. I don’t care about the index this time around, so I use some throwaway underscores once again.

One further point: string.find() can take a third argument, which is where it starts searching. In this case, I want it to start searching from the end of the #Countdown header.

1
    if first_plot then
2
            title_list = title_list .. first_plot .. " ([[" .. title .. "]])\n"

So if, after all this, we happen to find a plot? Now we can add it to the title_list. The .. operator is string concatenation, and this method merely adds on our plot (it’s a bulleted list item, so we’re making a handy list as we go), and I’m adding a link (Trunk Notes’ wiki links are surrounded by double-brackets) at the end, so I can see where it comes from.

And that’s the method! It took a bit of learning, but the Lua language is pretty flexible once you accept that there aren’t any fancy methods for you to leverage. It’s not that these things aren’t doable, it’s just that no one’s done them for you. And given Lua’s goal of being a somewhat minimalistic language that you can build what you want on, that’s a perfectly good idea.

There’s whole sections on metaprogramming which allow you to introduce object orientation that I haven’t even touched on, and tables appear to do super-awesome things once you know how to trick them out. It’s something I may have to put more effort into investigating.

Installing in Trunk Notes

Making this script run is pretty simple. Create a note called countdowns.lua and post the code into the body. Now, wherever you want that text inserted, simply type {{lua countdowns.lua}}. It’ll load the script, run it on your wiki, and output the results to the page.

Further work

I’m planning on doing a lot more work on this in the future. One script I do want to write will capture any line in the wiki that has a certain tag - whenever I think of something cool or weird that needs to be answered, I put the tag @IWonder next to it, so if these are collated on a page I can have certain plot threads come back to haunt the party.

Addendum

I’m aware this place has been kind of quiet. I have a few responsibilities on my hands, the result of saying “yes” to too many people. But hopefully, with discipline and attention, I can keep posting things here and keep the place moving.


  1. And apparently they’re working on an OS X version! It’s like my dreams have been answered.