Let The Struggles Fall Away: Coding Notes
Jan. 1st, 2021 11:05 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
I had only briefly met Twine (and never met Harlowe) before I started programming in it, so these are some notes I put together so that the next person trying to write in Twine with Harlowe (Twine/Harlowe from this point on) might have something that's useful for their story. Note that Harlowe calls its built-in functions "macros" (which they are, in some sense), so that's what I'll be calling them.
(I looked at parser-based systems - which in some ways would have worked better for the word-intensive Epistory - and various and sundry other story systems (coding languages of sorts) for Twine, like Sugarcube - but decided that I'd deal with Harlowe's relative lack of power in return for learning it as fast as I could.)
Word lists and variables
At the beginning of the game, I set up 18 different word lists for the words that were randomly chosen for each room. Most of the words were chosen from Epistory's word list, but I did stick in a few others. These word lists were set up as arrays in the format: (set: $wordlist to (a: "insert", "words", "here"))
In addition, I set up an array to track the pottery/bramble/rock words that the player clicked on so they couldn't click on them multiple times, fitting with canon. This was originally set up as $vae1, $vae2, etc but I figured it would be easier to keep track of as an array ($vae).
Other variables that I set up included the order of the greek letters for the one room puzzle, the words and letters that would display when you were interacting with the chests, the variable that kept track of how many things you'd clicked on with your magic on, the variable that told the game which memory to display when you'd finished opening a chest, a variable that kept track of whether the ice had been frozen in one room, whether you'd successfully solved the statue puzzles in four areas, and three variables that had to do with room states.
Coding for magic
Every room had a "turn on magic" link even if there was nothing the magic would reveal in the area, to obscure the rooms that did have stuff you could click on when you turned on magic. I found an answer that used what Harlowe calls a "hidden hook" to store the magic text in each room, a link to display it, and a replace macro to hide it again, as Harlowe doesn't have anything that really hides text you've already seen.
The general format for this was:
(link: "Turn on magic")[(link: "Turn off magic")[(replace: ?sowsow)[]]<br>(show: ?sowsow)]
|sowsow)[You turn on your magic. No words appear.]
There was one specific room where this hook didn't happen. The first time you enter the area past the hallway (and the 2nd ice puzzle), clicking "turn on magic" will (temporarily) kill you.
Vases, brambles, and rocks, oh my!
Coding for rooms that had magic was far more complex. I had variables checking how many of the pottery/stone/brambles had been clicked on in the room to display the correct text. These were the items most likely to fail in playtests - partially because of the early switch I made between the individual $vae variables and the array. All rooms with pottery originally said "you see words appear over piles of pottery" and used forward slashes between them, but I eventually figured out how to display the number of unclicked on items and how to get commas to appear when they were needed, and not when they were not. This is a copy of the code for the first sets of pottery you could click on:
(link:"Turn on magic")[(show: ?soso)(link: "Turn off magic")[(replace: ?soso)[]]]
|soso)[(if: _total is 3)[You turn on your magic. No words appear.] (else:)[You turn on your magic. You see _tota word(if: _tota > 1)[s] above (if: _tota > 1)[piles](else:)[a pile] of pottery: (if: $vae's 1st is 0)[(link: _vase1)[(t8n: "pulse")[_vase1](set: $insp to $insp + 1)(set: $vae's 1st to 1)](if: $vae's 2nd is 0 or $vae's 3rd is 0)[, ]] (if: $vae's 2nd is 0)[(link: _vase2)[(t8n: "pulse")[_vase2](set: $insp to $insp + 1)(set: $vae's 2nd to 1)](if: $vae's 3rd is 0)[, ]] (if: $vae's 3rd is 0)[(link: _vase3)[(t8n: "pulse")[_vase3](set: $insp to $insp + 1) (set: $vae's 3rd to 1)]]]]
In this case, the game first checks to see if you've clicked on the words for all three vases. If so, you get the message that there is nothing to click on ("You turn on your magic. No words appear.") If you haven't, it goes through some checks, first to see how many words so it displays "words" instead of "word" if you have more than one word left. It then displays words you can click on with commas inbetween. Thankfully most rooms are 3 or less so it's easy to check the coding on them, but one room (the first ice puzzle room) had five and sometimes the game got confused on what commas should be displayed.
The words that actually appear on the pottery ("vases"), rocks, and brambles were randomly determined each time the player visited.
The (t8n: "pulse") code is the code that causes the words to bounce up and down after you click on them, implemented based on beta feedback.
What about those statues?
Originally, the player only had to click on two statues, the ones at the beginning. For players familiar with the canon, it was a nice callback to it. For players that weren't, it was meant as a tutorial on how clicking on words went. One of my beta testers who was familiar with canon suggested that I implement more statue "puzzles" in the game to make it more interactive.
For each area that had a statue puzzle, I had to code the rooms around it so that you couldn't actually get the link to the room it until you'd clicked on both statues using magic. Here is the code for the first set of statues you encounter inside:
(link: "Turn on magic")[(link: "Turn off magic")[(replace: ?soso)[]]<br>(show: ?soso)]
//|soso)[(if: $5spear is 1)[You turn on your magic. No words appear.] (else:)[Words appear above the statues nearby with spears: (link: _statue1)[(t8n: "pulse")[_statue1](set: _tot to it + 1)] and (link: _statue2)[(t8n: "pulse")[_statue2](set: _tot to it + 1)] (event: when _tot is 2)[<br>*The statues raise their spears in salute, and you can now go into the ice puzzle room.*(set: $5spear to 1)(set: $insp to it + 2)]]]
(event: when $5spear is 1)[<br>[[Go to the ice puzzle room |Room5]]]
There is a global variable in the example code called $5spear (room 5's spears, basically) that lets the game know if you've clicked on both spears. If you haven't, it displays the link you can click on for each statue word, and then when you have, it does two things using a macro called "event": one is to display the message that the statues have raised their spears and in the section where you can go different directions, it pops up the link to enter the room guarded by the two statues. Note that _tot is a local variable (global variables use $, local variables use _) because in the game, if you don't lift up both spears in short order, the one that's lifted will drop again. In addition, it gives you two inspiration points (which I'll go into later) for lifting them.
I can't get there from here!
Harlowe has this neat array called "History", which tells you if the player has visited certain rooms. I used it extensively to tell whether or not the player had visited a room (so that if you walked outside from the very first inside room, for example, you wouldn't get the entire introductory text). I could have used the visits variable, but by the time I figured that out I didn't want to recode every area. Here's an example of the code I used to check which version of the room passage the player got. (I split out almost every room with a main passage, a first visit passage, and a subsequent visit passage in order to make it easier to troubleshoot errors.)
(if: (history:) contains "Room1")[(display: "Room1-sub")](else:)[(display: "Room1-1st")]
History is also useful to check if the player has been in certain rooms. For example, you can't get to half the areas in the Crossroads before you visit a certain room (Room 9, in my map, where you pick up ice magic). Getting to the hallway before the 2nd ice puzzle (Room 6) requires you to visit the room with the 1st ice puzzle (Room 5).
(if: (history:) contains "Room5")[<br>[[Go through the opposite door|Room6]]]
This if statement is different than the until statement above. Until statements check for a condition until it happens, while if statements only check once when the code runs. So the if statements are useful for "you've visited the right room, so display these links that are now available" and until is useful for "you need to watch for this condition happening, and then display the link/item".
Chests and their memories
I wanted to replicate the game's insistence that you type one word after the other (the nerve!) for the chests. The chests had some of the most complex (but as it turns out, far less buggy than most of the magic) code in the game. To replicate the feel for the chests, I used the (event: when) coding to check to see if the player had clicked on the previous word. All the chest words and the count of which words the player had clicked were global variables because (event:) does not like local variables very much and (if:) doesn't do what I wanted it to do. For chests, I went with the 3 words for the chests that had those and 6 letters - 3 pairs each - to give the feel of typing for the letter chests but not making it drag on. Here is code from the first chest with words:
|soso)[(if: $vae's 21st is 1)[You turn on your magic. No words appear.](else:)[You see something form above the chest: (event: when $chestb's 13th is 0)[(link: $chest141)[(t8n: "pulse")[$chest141](set: $chestb's 13th to 1)]] (event: when $chestb's 13th is 1 and $chestb's 14th is 0)[(link: $chest142)[(t8n: "pulse")[$chest142](set: $chestb's 14th to 1)]] (event: when $chestb's 14th is 1 and $chestb's 15th is 0)[(link: $chest143)[(t8n: "pulse")[$chest143](set: $chestb's 15th to 1) (set: $memoro to it + 1) (set: $vae's 21st to 1) <br&rt;<br&rt;The chest folds out and a floating book appears. You step forward to claim it.]]
(event: when $vae's 21st is 1)[display: "Memories")]]]
For these, the first set of code checks as usual to see if you've already cleared the chest. The next set of code checks to see if you've clicked on the first word (event: when $chestb's 13th is 0)and the second (event: when $chestb's 14th is 1 and $chestb's 15th is 0) makes sure you've clicked on the first but not the 2nd, and the third checks to see if you've clicked on the 2nd but not the 3rd. ($chestb is the array that keeps track of what has been clicked on for each of the chests, like $vae does for pottery. $vae is also here so that the event that displays the memories knows the player has clicked on all of them, and $memoro is the variable that keeps track of how many of the memories you've found. "Memories" is the passage that handles the text about the various memories and it has if statements that make sure the right one is displayed to the player.
Random thoughts
There are several rooms where you get a chance at either extra or different content. For each of them, I just put in an extra bit of code to generate a random number and then an if statement to display text if needed. For example, this bit referencing the game Colossal Cave has a random chance to show up in the corridor next to the 2nd ice puzzle room:
{(set: _twisty to (random: 1,3))
(if: _twisty is 3)[*You are in a maze of twisty little passages, all alike.* <br>
...No, that's not quite right. Where did you get that from? Anyway....<br>]
//}
(The curly brackets tell Harlowe to place this all on one line so there aren't mysterious spaces if you don't happen to get this 1 in 3 chance text.)
Inspiration and battles
In canon, experience is called "inspiration" and you get to go places/get perks by gaining inspiration (fitting for this canon). I decided to implement a version of this. The player gains inspiration for every piece of pottery, one of the chests, an automatic place or two where this game bursts stone/brambles for you, and getting through all but the first set of statues. These don't give you perks in this game like in canon, but they do play into the two boss battles. In each battle, the more things you've clicked on, the better result you get (everywhere from "you die a few times before beating this" to "you did very well"). There is also code to display your total points in the end. I thought about putting it on every page as a beta tester suggested, but it didn't look right so it doesn't display until the very end (along with the number of memory chests you found. Here is the code for increasing inspiration from clicking on the two statues in a room:
(set: $insp to it + 2)
To display the battle result flavor text, I used cond: as shown below:
(cond: $insp < 10, "You manage to fight them off, but it is hard. You have to restart the battle a few times before you finally defeat them all. But you do, and you slump a little, tired from your victory.", $insp <= 16, "You nearly have to restart a few times, but you manage to get all of them before they reach you and Red. It is stressful, but you are victorious, at last.", $insp >= 17, "It's a hard battle, but your confidence kept your enemies from getting near and you celebrate your victory with a fist pump.")
The related score was a simple print statement:
*Score: you accumulated $insp inspiration points from using your magic on things and found $memoro of the 4 memories.*
That one puzzle
I took the original puzzle from canon, changed it up to be Greek letters instead of numbers, and placed the solution across the way to make it harder! Each displayed square had two parts: the actual room code, which displayed the text of the square in question, and a second set of code in all rooms that functioned as a map (and showed the clue if you'd been in another room as an anti-frustration feature).
The way the puzzle worked in canon, and how I implemented it, is if you'd already been on a square, you could walk over it again without making it go dark, but if you were out of sequence it would go dark, and if you walked on it after you solved the puzzle it would stay lit. So, if you had stepped on the first four correctly in sequence and then stepped on the 3rd square again, it would stay lit up. For example, this is the code for the square with the 3rd origami:
(set: _puztotal to $greekl's 1st + $greekl's 2nd + $greekl's 3rd + $greekl's 4th + $greekl's 5th + $greekl's 6th)(if: $18door is 1)[You are standing on a folded-up origami.](else:)[You are standing on a square with the (print: $greek's 3rd) symbol on it. (if: $greekl's 2nd is 1 and $greekl's 3rd is 0)[It cheerfully lights up under your feet. (set: $greekl's 3rd to 1)](elseif: $greekl's 3rd is 1)[It stays bright and does not turn dark.](elseif: _puztotal is 0)[The origami does not light up.](else:)[All the origami that had been lit go dark. (set: $greekl to (a: 0,0,0,0,0,0))]]
$greekl was the array that kept track of which squares had lit up (the associated $greek's 3rd is the actual Greek letter). _Puztotal added up the currently lit-up squares so if the player misstepped, the puzzle would correctly mention that the origami does not light up. $18door was the variable that was set when the player stepped on all 6 in sequence (it was also used to check stuff in the corridor across from it, I'll talk about that shortly). The rest of the code checks to see if you'd stepped on the 2nd origami, and light the square up if you had, the next check was to state that it was lit up if you already had, and if none of the above fit, the game reset the entire sequence.
The other code displayed the correct state of the squares you could reach, because the origami folded up when you solved the puzzle.. This is again from the square with the 3rd origami:
(if: $18door is 1)[[[Square across from you with a folded up origami|Square5]]](else:)[[[Square across from you with the (print: $greek's 4th) origami|Square5]]]
The actual map that is displayed is an html table because otherwise the map wouldn't display properly. To keep my sanity, I coded two different tables - one for before you solve the puzzle, and one after, even though it probably would have been more efficient to code everything into one table. I might still do that as I continue to patch/add features to the code. Here is a snippet of the code used in one (presolved) table cell. (The solved table cell is similar, but substitutes "origami" for the "print" statement.)
(if: $greekl's 4th is 1)[(colour: Yellow) + (text-style: "bold")[(print: $greek's 4th)]](else:)[(print: $greek's 4th)](if: (passage:)'s name is "Square5")[^]
This checks to see if the 4th origami square has been stepped on (and if so displays it in yellow, bold text) and if the player is currently on Square 5, it prints a ^ next to it.
The last bit, the anti-frustration feature, is rather neat. This is the hint that displays if you have seen the right sequence of letters:
(if: (history:) contains "Room16" and $18door is 0)[
(link-reveal: "Oh! I remember something!")[ The sequence is: (print: $greek's 1st), (print: $greek's 2nd), (print: $greek's 3rd), (print:$greek's 4th), (print:$greek's 5th), (print: $greek's 6th)!]]
Basically, if you've been to room 16 (the corridor with 2 doors, where the sequence is) and you haven't solved the puzzle (and therefore you don't need the hint anymore), the game will print the right seqnece for you. (For some reason, you can reference items in arrays like $greek's 1st in conditional statements but you have to use a print statement to display them.)
Making it pretty
Twine lets you define all your game CSS in one place. I used various bits of CSS to style various things in the game, using examples from other Twine users to get the CSS right since I'm not really familiar with it. One of the snagged bits that I won't be going into but is rather handy is the code that allows the game to display at a better size for mobile browsers, using "@media only"
For the background, I used this CSS snippet to size the text, make it black, and change the background color from Twine/Harlowe's white on black text:
tw-story {
color: black;
font-size: 1.2em;
line-height: 1.25em;
background-color: white;}
To hide the undo link on the sidebar:
tw-sidebar tw-icon.undo {
display: none;}
Default link colors (.visited and the hover for .visited are the same colors as below)
tw-link, .enchantment-link {
color: #672f15; }
tw-link:hover, .enchantment-link:hover {
color: #8c4721; }
And finally, the stuff I used to color some links mid-game (red for the brambles and later on, grey for the stones), using span class in the code:
.red tw-link { color : #e85231; }
.grey tw-link { color : #404040; }