On Twine (and my first Twine project)

Twine is, by its own definition, “an open-source tool for telling interactive, nonlinear stories”. I, personally, would call it a templating language for HTML-based interactive fiction. I have finally gotten around to experimenting with it, and… I find it to be missing the mark in the way that many templating systems tend to, and the way that many ‘friendly’ languages tend to.

Before I dive into my struggles with Twine, I’d just like to drop a link to the result of this experiment: yum yum you are a bread, which I guess I’ll just call a bread simulator? I don’t know. It’s silly, it’s fun, it’s bread. Also, minor spoilers in the rest of the post.

My biggest problem with templating systems is that they tend to just replace work with other work. For everything that’s made simpler or more concise, some simple programmatic task is made awkward and verbose. They also tend to lack necessary programmatic structures, leading to even more verbose workarounds. Twine, for instance, has no way of creating one’s own functions. For yum yum you are a bread, I needed to check a bunch of things against a bunch of other things to properly construct the variable $you. For instance, if you start in the fridge, you are stale bread. If you make a cheese sandwich with stale bread, you are a stale cheese sandwich. Grilling/toasting removes the staleness. There are a bunch of rules that need to be run whenever an action is taken, and this would ideally be done via a function.

Since Twine does not have user-defined functions, my initial plan was to create a passage1 that displayed nothing, it just generated the $you variable and then went to the appropriate destination passage. This was a bad idea; among other things it breaks the back button. The best solution that I could find was to essentially do the same thing, but with the (display:) macro. This simply runs a passage and inserts the result into the current passage’s text. It is an incredibly clumsy workaround.

Much like an older interactive fiction darling, Inform, Twine’s issues go beyond templating and into that of ‘friendly’ programming languages. I’ve only dealt with a handful of ‘friendly’ languages, but they all have one thing in common: they’re incredibly unfriendly to programmers experienced in more typical languages. I know plenty of folks will say that this is, in fact, the point – these languages are so much more accessible to non-programmers! I don’t believe that’s true. One still has to learn the fundamentals of branching, conditionals, variable and string manipulation, and one still has to learn a specific syntax. And while this syntax may be sort-of-kind-of human-readable, it is by no means perfect and is now nothing like anything else. Take array addressing in Twine: it’s handled like $array's 3rd2. Verbosity and uniqueness aside, is that truly easier to grok than $array[3]?3

The lack of functions ties in perfectly to how ‘friendly’ languages struggle to teach programming concepts. A tutorial for any more traditional language would almost immediately introduce ways to reuse code. Without that concept even properly existing in Twine, a user is unlikely to even know what to search for. They may, in fact, resort to simply copying and pasting code over and over, unaware that the concept even exists. If they do figure out the (display:) trick, they’ve now learned a clumsy strategy that they’ll have to unlearn if they want to branch out into more traditional languages.

It’s actually hard to say how much of this to blame on Twine, and how much to blame on Harlowe. This is another issue, Twine itself is more of a framework, and there are a bunch of different ‘formats’ that one can use for projects. The default4 is Harlowe, and to get answers to questions, one must bounce back and forth between the Twine documentation and the Harlowe documentation. None of this is really documented well5, and it took me quite a while to figure out that in order to do basic things like setting variables, I didn’t have to actually load any additional code – Harlowe was there by default.

For all of its issues, Twine does a great job working within constraints. Final projects are just single HTML files with the CSS and engine script embedded – there’s a limit to how much of this you really want to stuff into a single page. I definitely prefer the hyperlinked HTML format for interactive fiction over Inform and its need for an external interpreter. And as much as Twine’s code quirks annoyed me, they’re nothing compared to the ‘friendly’ absurdity that is Inform. I’m glad the world of interactive fiction seems to be having a comeback, and I think tools like Twine and hypertext experiences like those it provides are largely responsible for that. I intend to try a few more short narratives with it, and hopefully will retain some of its odd syntax in my mind as it matures.

Below is my (display:) passage for generating the bread’s description. I think yum yum you are a bread is a cute, quirky thing, and I’d recommend you play through it one or two or three times before looking at this (spoilers).

(set: $you to "bread")
(if: $stat's 3rd > 0)[(set: $you to "cheese sandwich")(if: $stat's 5th is 1)[(set: $you to "grilled " + $you)]](else-if: $stat's 6th >0)[(set: $you to "toast")]
(if: $stat's 3rd is 4)[(set: $you to "duper-super extra cheeserrific " + $you)](else-if: $stat's 3rd > 1)[(set: $you to "extra cheesy " + $you)(if: $stat's 3rd is 3)[(set: $you to "extra " + $you)]]
(if: $stat's 6th is 0)[(if: $stat's 1st is 1)[(set: $you to "stale " + $you)]](else-if: $stat's 6th is 2)[(if:$stat's 5th is 1)[(set: $you to "golden brown " + $you)](else:)[(set: $you to "toasty " + $you)]](else-if: $stat's 6th is 3)[(set: $you to "burnt " + $you)]
(if: $stat's 2nd > 0)[(set: $you to "buttery " + $you)(if: $stat's 2nd > 1)[(set: $you to "very very " + $you)]]
(if: $stat's 7th is 1)[(set: $you to "bisected " + $you)](else-if: $stat's 7th is 2)[(set: $you to "quadrisected " + $you)]
(if: $stat's 8th is 1)[(set: $you to "chatty " +$you)]
(if: $stat's 10th > 0)[(set: $you to "already-been-chewed " + $you)]
(if: $stat's 4th > 0)[(set: $you to "wet " + $you)]
(if: $stat's 9th > 9)[(set: $you to "oh-so vain " + $you)]
(if: $you's 1st is "o" or $you's 1st is "a" or $you's 1st is "e")[(set:$a to "an")](else:)[(set:$a to "a")]
(if: $grueTime > 3)[(set: $you to (uppercase:$you))]

  1. ‘Passage’ is Twine’s term for any given branched page, essentially. ↩︎
  2. Also, it’s 1-indexed. Vom. ↩︎
  3. Makes it hard to change these after the fact as well. Can’t just increment or decrement a number, you have to potentially change a suffix. ↩︎
  4. I’m sure some will disagree, but to me, being the default makes Harlowe the canonical (or at least blessed) format. Who knows how well-maintained any of the non-default formats will be down the line. ↩︎
  5. I could’ve gone on and on in the post itself, but really wanted to address the core issues that seem to present themselves due to being template-based and ‘friendly’. I do think it’s worth mentioning that the documentation is… not great, either. It feels very incomplete to me, and I ended up finding most of the answers I needed elsewhere. ↩︎