Skip to main content

Using Twine for Games Research (Part II)

This preliminary discussion introduced my thoughts on using Twine as a tool for creating prototypes for games research. I'll start with documenting my first case study: a hack-and-slash RPG-like setting where the player character has a record of attributes ("stats") that evolve through actions that turn certain resources (money, health, items) into others. I've selected this hack-and-slash example because it falls outside the canonical "branching story" domain thought to be Twine's primary use case, but it is not too much trickier to implement. It relies crucially on the management of state in ways that simple branching stories would not, but it does so in a fairly straightforward way.

If all goes well, this post may also serve as a tutorial on the "basics" of Twine (links + variables + expressions). In particular, I'll be using Twine 2/Harlowe, and I haven't seen many tutorials for this new version published yet.

To me, the main "research questions" to ask about this kind of game are:
- What are the feedback loops? When I can do certain groups of actions over and over again, what is their cumulative effect?
- Does play ever stagnate -- i.e. is there a fixed point? Or, what does it mean for a game to stagnate even when I can always increase some stat? ("Click this button to increment a counter" is not very fun if that's all there is to it.)
- Where are the interesting strategic tensions? That is, at which points in the game does the player need to make a difficult decision? What makes it difficult?

All of these questions can be stated in terms of (though not necessarily answered by) a Twine prototype, even if the overall vision for the game involves much more complex rendering and controls. This is the same principle that leads to paper prototyping, but with computational support, which makes things like analysis and playtesting a lot less human-labor-intensive.


Twine Overview


Twine is a programming language for making browser-based link-clicky programs. But isn't that just, like, a webpage?, you ask. Yes! You could make all of the things you can make in Twine with HTML, CSS and JavaScript, and you sometimes need to know a little of all those languages anyway to get it to work and look the way you want. However, what Twine does is a small enough subset of webpages-in-general that it can give you just a few lightweight tools to create interestingly-playful experiences -- or at least, that's the idealized version of Twine that I'm going to try to target. :)

The main concepts in Twine are:

  • Passages. These are units of text that get rendered as HTML, and they can also contain code. Passages can have links to other passages, and Twine's editor will render your game as a graph where passages are nodes and links between them are edges. While playing the game, there is a "current passage" whose contents are displayed (executed + rendered), and clicking a link changes the current passage and displays it.
  • Links, via markdown syntax. A passage named "foo" containing the text "[[bar]]" will create a link from foo to bar that the player can follow. "[[Go to bar->bar]]" creates a link to the "bar" passage with the text "Go to bar."
  • State, via assignable variables. The code that a passage can contain includes ways of setting a variable to a particular value and conditionally displaying text based on variables' current values. State is manipulated with macros such as (set: $foo to 4) and (if: $foo > 3)[...]. ($foo is a variable; all variables must start with "$".)
With this prelude, you should be able to follow along with the following steps. If you like, open up the story editor and try to reproduce the results.


Step 1: Create a Skeleton 


The architecture of this Twine game (and most of my examples in this series) will be as follows:

- An init passage to set up the game's initial state;
- A main passage to control the top-level game loop;
- Several appendage passage-subgraphs branching off of main to control the subsidiary components of the game.

So what I did first is:

Create a new story (I named mine "rpg").



Double-click the "Untitled Passage" and change its name to "init." Add a link "[[main]]" in the body of the passage.







Click the "x" to close the passage editor and notice that the editor has created a "main" passage for us with an arrow indicating the link from init to main.



Let's edit main to include three main game actions: resting, adventuring, and shopping. Double-click on "main" and add the following text:


You've been adventuring for $days days. You have $coins coins and your health is $hp. 
[[rest]] 
[[adventure]] 
[[shop]]

Edit the text of the newly-created rest, adventure, and shop passages to contain a link back to [[main]]. Now we have a skeletal structure that looks something like this:



At this point, you can test out what you made! Designate init as the starting point of the game, by hovering over the passage and clicking on the rocket ship, like so:



and then click the bug-shaped "test" button in the lower right hand corner of the screen. You should get a new browser tab to open, rendering the init passage with a link to main, which, when clicked, takes you to a main screen that looks like this:



Step 2: Initialize Variables


Ok, so we snuck some variables into our main passage (a naked variable "$foo" within text is actually shorthand for the macro "(print: $foo)"). But we never initialized them! It turns out that this means they get set to 0 by default, which is why your main passage has those 0s. But let's use our init passage to set them up right. Edit its text (before the [[main]] link) to:


(set: $hp to 10) 
(set: $coins to 10) 
(set: $days to 0)

Great! Now try testing the game again to see it print the new values of $hp and $coins.

Also, let's do one more thing before we continue: replace the [[main]] link with the text (display: "main"). This (display: ...) notation is a macro that displays (executes code + renders text) the passage whose name is given (in quotes!) after the colon. It simply removes the indirection created by the link. Testing the game now should plop you right into the main passage.

Step 3: Create the Adventure branch

Edit the adventure passage to contain the following text:


A monster appears! (set: $monster_hp to 3) 
[[fight]] 
[[flee->main]]

Now edit the fight passage. This one will be our most complex so far -- which makes sense, because we need to resolve an interaction between the player character and the monster. We will do so with a bit of randomness, introduced through the (either:) macro, which chooses uniformly at random between a comma-separated list of values given after the colon. At a first pass, we'll just simulate a 50-50 coin flip:


(if: (either: 0, 1) is 0)[ The monster bites you! ]
(else:)[ You hit the monster! ]

Between each of those pairs of [brackets], we'll need to write the logic for determining if the player or the monster survives and what to do in each case. In the first branch:


(if: (either: 0, 1) is 0)[  
  The monster bites you! 
  (set: $hp to $hp - (either: 1,2,3))  
  (if: $hp < 1)[ You are [[dead]]! ]  
  (else:)[ Your health is $hp. 
    [[fight]] 
    [[flee|main]] ]  
]

And in the second branch:


(else:)[ You hit the monster!  
  (set:$monster_hp = $monster_hp - 1)  
  (if: $monster_hp < 1)[ The [[monster is dead]]! ]  
  (else:)[    
    Its health is $monster_hp.
    [[fight]] 
    [[flee|main]] ]  
]

Finally, let's flesh out the new passages we made, the player death and monster death. Player death can just be a dead end:

You are dead! [[restart?|init]]

Monster death should drop some coins (let's say, a random number from 3 to 10) for our player to collect:

(set: $drop to (random: 3,10)) 
(set: $coins to $coins + $drop) 
You collect $drop coins from the monster carcass! You have $coins coins and your health is $hp. 
[[continue|adventure]] 
[[go home|main]]

Playtest this a couple of times and see if the constants -- the player character's health, the monster's health, and the weapon damage -- create an appropriate level of tension. You probably have some ideas for how to make this little combat loop more interesting, including different sizes of monster (with varying health and damage). Feel free to play around with those ideas.

At this point, we still have to implement the rest action and the shop action, but before we do that, let's think about how those actions should interact with adventuring. Adventuring can wear down your health, so resting should be an action that restores your health. Adventuring also yields coins, and shopping should put those coins to use.

Ok, let's think about resting first. Resting should refresh the player's health and increment the number of days. Change the "rest" passage's text to:

(set: $hp to 10) (set: $days to $days + 1) 
You feel refreshed. 
(display: "main")

However, if a player can simply rest to restore full health, then there's no incentive for them to do anything other than flee to rest after each monster kill. To create some kind of interesting decision tension, let's create a risk and corresponding reward for taking higher risk: on any single adventure, the adventurer carries her "spoils" from monster drops so far. If she abandons the adventure, all spoils are lost, but if she keeps pressing on, the drops get bigger.

First, let's change "[[adventure]]" in our main passage to "[[adventure->start adventure]]", then edit the newly-created "start adventure" passage to give some initial state for each adventure and display the main adventure passage:

(set: $spoils to 0) (set: $streak to 0) 
(display: "adventure")

Now change the "monster is dead" passage text to:

(set: $streak to $streak + 1) 
(set: $drop to (random: 3,10) * $streak) 
(set: $spoils to $spoils + $drop) 
You collect $drop coins from the monster carcass!  Your spoils are currently $spoils coins and your health is $hp. You've killed $streak monster(if: not($streak is 1))[s] on this adventure.
[[continue|adventure]] 
[[go home|collect spoils]]

And edit the newly-created "collect spoils" passage to update our primary pool of coins:


(set: $coins to $coins + $spoils) 
(display: "main")

Playtest a few times to try it out, tweaking numbers (and perhaps the drop formula) as needed.

Let's step back just a bit (by literally zooming to the "just passage titles" zoom level) and look at the structure we've created so far:



A few edges are missing due to the use of "(display:)"s rather than [[links]], but otherwise this gives us a pretty clear picture of the control flow in our game! It's not quite all we need to reason about resource feedback loops, since those happen in variable changes "behind the scenes" of the link clicks -- but this view at least lets us approximate which game states are reachable from others.

We haven't implemented the shop action yet, but I'd like to save it for a later post so that I can gab about datatypes for representing "items" (like weapons) and the difference between what I'd consider ideal, and what Twine 2 offers us currently -- without making this post too egregiously long.

Comments

  1. Hello, I found your article and I'm new to coding but of all the parts in the modules, I can't seem to get the 'fight' one right. It keeps getting errors. What am I doing wrong?

    ReplyDelete
    Replies
    1. Hi there, I just came across this comment (must've missed it earlier). Did you ever resolve this issue? If not, can you tell me what kind of errors you're getting?

      Delete
    2. Hello, firstly, thanks for this article. I have never coded before, like Amanda H and have been struggling with the fight passage too!
      Today, a new release has been made for Twine 2 which fixes a bug with (else:).

      Really glad I stumbled upon your blog. So interesting, even to a novice like myself.

      Delete
  2. wow, awesome, thank you for this =D

    ReplyDelete
  3. Great project. It helped my class big time. Thanks heaps =D

    ReplyDelete
  4. This is a truly brilliant article and has helped me out a great deal. Thank you very much for this!

    ReplyDelete
  5. Hello

    I am attempting to make a text game but with a different set in mind; what I want to is create a countdown, so that each time the character enters a passage the countdown depletes, even if they're backtracking into one they've been to before; sort of like a 'number of turns until defeat'. Is there any way to do this?

    ReplyDelete
    Replies
    1. There are a few different ways to do it! Here are two I just tried.

      1. This way is more in keeping with Twine idioms, but a bit more fragile and tedious. You can create a "count" passage that does your increment-and-check logic, then be sure to (display: "count") at the beginning of every story passage. This wouldn't allow you to *remove* the normal story text when the timer runs out, but it would allow you to change what appears before (or after) each passage in a uniform way depending on whether time has run out.

      2. This way works better, but Twine doesn't inherently accommodate it very well. You can make a passage called "main" w/this text:

      (set: $counter to $counter + 1)
      (if: $counter > $timeout)[out of time!]
      (else:)[$text]

      And then in every story passage, do
      (set: $text to "whatever the text of the passage should be, including [[links]]")
      (display: "main")

      Does that make sense?

      Delete
    2. I tried the #2 way since that looked to be what I was looking for. I mean they look pretty straight forward but when I tried them, every passage immediately displays 'out of time' at the top so the countdown's not showing up. I laid them out exactly as you displayed above but it doesn't seem to work; did I go wrong somewhere?
      This is what I put in to the 'main' passage as you instructed:

      (set: $counter to $counter + 11)
      (if: $counter > $timeout) [Out of time!]
      (else:)[$Failed]

      And in each subsequent story passage, which is where I guess I went wrong, I did this:

      (set: $counter to $counter -1)
      (display: "Main")

      Delete
    3. Hi again,

      What's the $Failed variable, and where is it getting set? It seems likely that you have a loop somewhere, so you're displaying main repeatedly, which runs the (set: $counter[...]) code repeatedly and makes it go over the max. Instead, the "main" passage should only be displayed when a link is traversed, i.e. at the top of every individual story passage.

      Delete
  6. This was great, really helpful. Thanks for not being a total jerk and using a whole bunch of terms you didn't define in your post. I have seen things that do that and while they obviously aren't for beginners, they don't help one like me. You truly are starting from basics and I greatly appreciate that!

    ReplyDelete
  7. Thanks. A really great introduction to "twinery".

    ReplyDelete
  8. This is very helpful. Thank you! Excuse me if my question is answered in what you've already provided. Is there a way that the outcome of an (either: ) macro in a parent passage could automatically activate an offspring passage, that is, without having to click on a link associated with the outcome? So for example if the outcome of (either: 0, 1) is 0, is there a way to automatically activate a passage connected to 0? Hope my question makes sense

    ReplyDelete
    Replies
    1. Yes, this is doable with "hooks" (the [ ] syntax) and the "display" macro, as follows:

      (set: $outcome to (either: 0,1))
      (if: $outcome is 0)[ (display: "Passage0") ]
      (else:)[ (display: "Passage1") ]

      Delete
  9. Best tutorial EVER ! U rock !

    Even with no coding experience, I understood everything.

    I had always thought what that $ is ,thanx for telling its the symbol for variable.

    ReplyDelete
  10. Article is nice.

    Still it seems to me it's better to just use normal programming language than this framework because if you really need to create something not usual it wont help you.

    ReplyDelete

Post a Comment

Popular posts from this blog

Why I don't like the term "AI"

Content note: I replicate some ableist language in this post for the sake of calling it out as ableist.

In games research, some people take pains to distinguish artificial intelligence from computational intelligence (Wikipedia summary), with the primary issue being that AI cares more about replicating human behavior, while CI is "human-behavior-inspired" approaches to solving concrete problems. I don't strongly identify with one of these sub-areas more than the other; the extent to which I hold an opinion is mainly that I find the distinction a bit silly, given that the practical effects seem mainly to be that there are two conferences (CIG and AIIDE) that attract the same people, and a journal (TCIAIG - Transactions on Computational Intelligence and Artificial Intelligence in Games) that seems to resolve the problem by replacing instances of "AI" with "CI/AI."

I have a vague, un-citeable memory of hearing another argument from people who dislike the…

Using Twine for Games Research (Part III)

Where we last left off, I described Twine's basic capabilities and illustrated how to use them in Twine 2 by way of a tiny hack-and-slash RPG mechanic. You can play the result, and you should also be able to download that HTML file and use Twine 2's "import file" mechanism to load the editable source code/passage layout.

Notice that, in terms of game design, it's not much more sophisticated than a slot machine: the only interesting decision we've incorporated is for the player to determine when to stop pushing her luck with repeated adventures and go home with the current spoils.

What makes this type of RPG strategy more interesting to me is the sorts of decisions that can have longer-term effects, the ones where you spend an accumulation of resources on one of several things that might have a substantial payoff down the road. In a more character-based setting, this could be something like increasing skill levels or adding personality traits.

Often, the game-…