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-design goals of these features are multi-faceted: consider the shopping mode of Kim Kardashian Hollywood, for a recent and topical example.

(Screenshot via Michelle Dean.)

The clothing the player buys does have mechanical function (in terms of completing projects faster, for example), but it also has the less tangible function of giving a player a sense of control over which combination of in-game artifacts persist with their avatar as they move through the world.* Because these artifacts are subdivided into independent components (like real clothing), the player can combine them in ways that are not pre-anticipated by the game -- no one enumerated every possible combination; components have to systematically make sense in combination. In other words, clothing is a composable mechanism for customizing player experience.

Lest you think that this kind of visual composition wouldn't translate well to a textual medium, I invite you to play a few Twine games like Porpentine's UNTIL OUR ALIEN HEARTS BEAT AS ONE and Whisperbat's Candy Ant Princess.





These examples probably use an old Twine 1 macro called cyclinglink, which doesn't (AFAICT) have a great replacement in Twine 2 -- but aside from the nice in-line interface, all that's really going on is that the text that's shown once the player confirms her choices gets bound to variables that are then rendered later in the game.

Note again that, while each category of item has to be hand-enumerated by the game designer, the combinations do not. For some deeper thoughts related to this idea, take a look at the Icebound team's post on combinatorial narrative and Allison Parrish's work investigating various units of text for which it makes sense to deconstruct and recombine.

In our hack-and-slash game, we'll want to associate certain traits or properties to certain items in the game, such as a numeric damage trait to a weapon. In the rest of this post, I'll walk you through using Twine 2's datamap construct to do so. (Note: I am trying not to assume pre-existing programming experience in this tutorial, but I hope that experienced programmers can gain something from the explanation anyway.)


Ok - so if we were drafting some weapon ideas on paper, we might write it out as a table:

weapon      | cost | damage
dagger      | 10   |  1
small sword | 30   |  3
big sword   | 50   |  5

In my experience, the way a game prototype (or jam game) starts feeling really fun and productive is once I have the skeleton "infrastructure" to it and my main task is, essentially, populating tables like this, adding both new rows and columns which create complex interactions. So ideally you don't want any of your infrastructure code to depend on what's in these tables!

Our first attempt at codifying this table in Twine will do exactly that, however. Let's edit our initial passage to add a $weapon variable and make the player's intial weapon a dagger by adding this line somewhere:

(set: $weapon to "dagger")

Then let's edit our shop passage to include the option to buy fancier weapons:


Your current weapon is a $weapon.
(if: $weapon is "dagger")[  [[buy small sword]] (30 coins, does 3 damage) ](if: $weapon is "small sword")[  [[buy big sword]] (50 coins, does 5 damage) ]
[[go back home|main]]


Edit the "buy small sword" passage to check for the appropriate coinage:


(if: $coins > 30)[  (set: $weapon to "small sword")  (set: $coins to $coins - 30)   (display: "shop")](else:)[You don't have enough money! 
  (display: "shop")]

and the "buy big sword" passage similarly:


(if: $coins > 50)[  (set: $weapon to "big sword")  (set: $coins to $coins - 50)   (display: "shop")](else:)[You don't have enough money! 
  (display: "shop")]


Now, to make the damage take effect in adventuring, edit the fight passage code to replace (set:$monster_hp = $monster_hp - 1) with:


(if: $weapon is "dagger")[(set: $damage to 1)](else-if: $weapon is "small sword")[(set: $damage to 3)](else-if: $weapon is "big sword")[(set: $damage to 5)](set:$monster_hp = $monster_hp - $damage)

Notice how many if statements we're using and how much of our code has the exact same structure copied in multiple places. Wouldn't it be nice if instead of conditioning on the exact identity of the weapon variable, we simply asked for its cost and damage properties? In general in programming, the kind of thing that would let us do this is an aggregate data structure such as an array or list, or, more specifically in this case, a map from keys to values. We can create such structures with the (datamap:) expression constructor in Twine 2, so that a weapon is not just a string but a mapping from properties to values:


(set: $dagger to (datamap: "id", "dagger", "cost", 10, "damage", 1))
(set: $small_sword to (datamap: "id", "small sword", "cost", 30, "damage", 3))
(set: $big_sword to (datamap: "id", "big sword", "cost", 50, "damage", 5))

(set: $weapon to $dagger)


As you can infer from this example, datamaps are comma-separated lists of alternating key-value pairs. We've defined each weapon as a separate datamap, each of which looks like a row in our original table.

Now we can rewrite shop to refer to the id property of a weapon:


Your current weapon is a (print: $weapon's id).

(if: ($weapon's id) is "dagger")[ [[buy small sword]] (30 coins, does 3 damage) ]
(if: ($weapon's id) is "small sword")[ [[buy big sword]] (50 coins, does 5 damage) ]


...and we can similarly refer to the "damage" property in the fight passage:

(set:$monster_hp = $monster_hp - ($weapon's damage))

There's still just one bit of conditioning and code duplication that we have yet to get rid of: figuring out the right "upgrade" to show to a player based on their current weapon. Turns out, as long as we order our table rows backwards from what we first had, we can add a field that itself refers to a datamap (where the "..." below refer to the pre-existing properties):


(set: $big_sword to (datamap: ..., "next", 0))
(set: $small_sword to (datamap: ..., "next", $big_sword))
(set: $dagger to (datamap: ..., "next", $small_sword))


Finally, we can replace our two buy passages with a generic one that's predicated on a $next_weapon variable:


(if: $coins > ($next_weapon's cost))[(set: $weapon to $next_weapon) (set: $coins to $coins - ($weapon's cost)) (display: "shop")]
(else:)[You don't have enough money! 

(display: "shop")]


And our shop passage accordingly:


(set: $next_weapon to $weapon's next)
(if: not ($next_weapon is 0))[
(set: $nwn to $next_weapon's id)

[[buy $nwn->buy]] ((print: $next_weapon's cost) coins, does (print: $next_weapon's damage) damage)]

Test out this code (note: I recommend adding a debugging hatch to give yourself 1000 coins or something) or play/import my instantiation if you like.

At this point, I'm going to call the tutorial code complete, but I'd like to finish by remarking on some important things that are not yet possible in Twine 2:


  •  I can't do datamap lookups with anything other than literal key values. So, for example, a nicer way to implement the progression of weapon upgrades might have been to simply store the *id* of the upgrade, then look up the actual weapon datamap in another table. But if that table is called $weapon_ids, then I can't say $weapon_ids's ($weapon's next), because that would require being able to use a complex expression like ($weapon's next) as a datamap accessor.
  • I can't iterate over a datamap or an array. So for example, if I wanted to let the player choose between multiple available weapons (to achieve the kind of customization choices I mentioned at the beginning of this post), I might want to toss them all into one big datamap and then have a passage iterate over all of them and print certain parts of their table rows. But there's no way to do so with Twine 2 syntax at the moment -- although it is possible to hack up loops using (set:) and (display:)  (which I'll leave as an exercise to the intrepid reader).

To reiterate: the important idea behind aggregate structures like datamap is a form of parametricity, a way of designing the core of the game without reference to the specific items and entities, so that the latter can be developed freely without significant (and error-prone) changes to the former. This is already a lot of what we need to experiment with sophisticated design ideas like the dynamics of resource accumulation, feedback loops, and combinatorial narrative.

Incidentally, what I've covered in this tutorial encompasses most of the basic ideas behind imperative programming. (If you're a programmer, note that passages somewhat supplant the notion of function.) To those who say, "I'm not a programmer, I just use Twine," or worse, "I am a programmer, so I'm above using something like Twine," I hope this post encourages you to reconsider your perspective on what is and isn't programming. Twine 2 is a (relatively minimal) programming language for prototyping games, and viewing it that way can give us a clearer sense for the relationship between programming affordances and design constraints.

Comments

  1. Cool!

    If I were to ever use Harlowe, this is exactly the type of stuff I would want to know. Thanks for the info.

    On the forums, we get asked a lot about adding a player name input. That's a pretty basic RPG thing, so you might mention it in a future edition of this tutorial.

    Also, you might consider including the link to the tutorial game so far in your "Part II" post.

    — Sharpe

    ReplyDelete
  2. Just want to say thank for this three part tutorial. This was really helpful in my understanding Twine compared to other sites I've been to. I do hope that you write some more articles around this topic.

    Thanks again,

    Nic

    ReplyDelete
    Replies
    1. I'm so glad you found it helpful! Thanks for the kind comment.

      Delete
    2. Could you possibly elaborate on the if then statements? For instance, if I want to stack the odds against the player, and I want something like
      (if: (either: 0, 1, 2, 3) is 0, 1, 2)[
      The creature bites you!

      How would I do it? Basically, I want a 75% chance the player gets bitten.

      Delete
    3. One way of doing your specific example would be:
      (if: (random: 0, 3) < 3) [...]

      (This would work for your "either" enumeration, too.)

      In general, though, if you want to test whether a value is this value OR that value, you can write it like:

      (if: (either: 0, 1, 2, 3) is 0 or it is 1 or it is 2)

      "or" and "it" are keywords in TwineScript.

      Does that help?

      Delete
    4. That's perfect! I just want higher percentages/lower percentages than 50% in my value. So if someone has a sword, they may have a 60% chance to hit instead of 50%.

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. I can down load your file but when I try to import it to twine 2.0.8 it throws out an error that it cant import it do to an undefined value. I'm lost because I had it all working before in 2.0.6

    ReplyDelete
    Replies
    1. Ahh, that's a bummer. They may not've made the versions backwards-compatible. I'd contact Chris Klimas about that, though; it might be a bug.

      I know it's a pain, but there aren't too many passages in this example; it should be feasible just to copy and paste code into a new editor session.

      Delete
  5. Disclaimer: I have no programming exp.
    Thank you for the tutorial.

    I had to fight with a bug,
    {You can only use positions ('4th', 'last', '2ndlast', (2), etc.) and 'length' with the string}
    i got rid of it but i don't know how i did it.
    Could it be that something changed with datamap 2.0.8?
    Thanks again.

    ReplyDelete
    Replies
    1. rpg


      (set: $hp to 10)
      (set: $coins to 1000)
      (set: $days to 0)
      (set: $big_sword to (datamap: "Id", "big sword", "Cost", 50, "Damage", 5, "Next", 0))
      (set: $small_sword to (datamap: "Id", "small sword", "Cost", 30, "Damage", 3, "Next", $big_sword))
      (set: $dagger to (datamap: "Id", "dagger", "Cost", 10, "Damage", 1, "Next", $small_sword))
      (set: $char to (datamap: "Cute", 4, "Wit", 7))
      (set: $weapon to $dagger)
      (display: "main")

      You've been adventuring for (print: $days) (if: $days is 1)[day](else:)[days]. You have (print: $coins) (if: $coins is 1)[coin](else:)[coins] and your health is (print: $hp).
      (print: $char's Wit) Wit test
      (print: $weapon's Damage) Damage test
      [[rest]]
      [[adventure->start adventure]]
      [[shop]]
      (set: $hp to 10) (set: $days to $days + 1)
      You feel refreshed.
      (display: "main")
      [[main]]
      A monster appears! (set: $monster_hp to 3)
      [[fight]]
      [[flee->main]]

      [[main]]
      Your current weapon is a (print: $weapon's Id).

      (set: $next_weapon to $weapon's Next)
      (if: not ($next_weapon is 0))[
      (set: $nwn to $next_weapon's Id)

      [[buy $nwn->buy]] ((print: $next_weapon's Cost) coins, does
      (print: $next_weapon's Damage) damage)]
      [[go back home|main]]




      (if: (either: 0, 1) is 0)[
      The monster bites you!
      (set: $hp to $hp - (either: 1,2))
      (if: $hp < 1)[ You are [[dead]]! ]
      (else:)[ Your health is $hp.
      [[fight]]
      [[flee|main]] ]
      ]
      (else:)[ You hit the monster!
      (set:$monster_hp = $monster_hp - ($weapon's Damage))
      (if: $monster_hp < 1)[ The [[monster is dead]]! ]
      (else:)[
      Its health is $monster_hp.
      [[fight]]
      [[flee|main]] ]
      ]
      (set: $spoils to 0) (set: $streak to 0)
      (display: "adventure")
      (set: $coins to $coins + $spoils)
      (display: "main")
      You are dead! [[restart?|init]]
      (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]]
      (if: $coins > ($next_weapon's Cost))[(set: $weapon to
      $next_weapon) (set: $coins to $coins - ($weapon's Cost))
      (display: "shop")]
      (else:)[You don't have enough money!

      (display: "shop")]

      Delete

Post a Comment

Popular posts from this blog

Reading academic papers while having ADHD

Using Twine for Games Research (Part II)