How should I approach an ending with 600+ possible outcomes?

Hi guys,

I’m creating a narrative game that could potentially have 600+ different outcomes/endings based on the path you chose. Is Twine capable of holding such capacity? What is the best way to approach this?

I have a Google sheet with all the outcomes and values I’d like to insert into variables. Is it possible to pull these values from a Google sheet?

Would it better if I just created 600+ passages with all its values?

2 Likes

Hi Ruby, welcome to the forum!

600+ endings sounds very ambitious. I imagine Twine can hold more passages than that. My WIP is not Twine but I’m at 569 passages, and they aren’t all endings. I think my last game had about 1300 passages.

Do you have 600 completely separate endings, or do you mean there are 600 potential routes to the ending? There is a choice-narrative structure called a Time Cave in which every single choice creates a new branch. They can be rather difficult to write due to combinatorial explosion, but it sounds like you may be doing it procedurally.

1 Like

Hi Ruby,

The question of whether Twine is the appropriate tool for your project depends on what you are thinking of making. Can you give us more information?

Also, how did you want to use variables in your game? I don’t think it is that common in Twine to put a lot of text inside of variables when you could instead put that text inside of passages and refer to that passage with a link (this is handy for reusing text that appears in multiple places and is easier to find and edit this way). Can you give us an example of how you wanted to use variables?

2 Likes

Hey Hanon! Thank you, glad to have joined :grinning:

With 1300 passages, did you feel like the performance of your game slowed down?

Anyways, my story is more of like a “Branch and Bottleneck” narrative rather than a “Time Cave”. The player makes choices that collects variables along the way and they will always ultimately end up in court with a case hearing. These variables/choices will determine which judge hears their case. For example, if they end up in a city in Los Angeles, there could be 10 different judges that they end up with. We are making this as realistic as possible so there are many different cities (with at least 5-10 judges per city) that the player can chose to settle in.

I think what I might end up doing is using if/else statements with (display:) macros. I am just hoping that this won’t completely slow down my game considering it’ll be 600+ passages just for the ending alone.

Hey Nicholas!

So for the final ending let’s say I have this paragraph:

The highest denial rate at this court was $MAX percent. The lowest denial rate was $MIN percent. That’s a spread of $SPREAD percent. The highest grant rate was $GRANTED_MAX percent. The lowest grant rate was $GRANTED_MIN percent — a spread of $GRANTED_SPREAD percent.

These statistics would be different for each (600+) ending outcome. Ideally, it would be cool if I can create one passage for the ending that has all these variables that are being called from a Google sheet. But upon speaking to many different people and hearing their advice, it seems like I’m going to have to create the 600+ passages :frowning: as I don’t really have the time to learn how to use Twee.

Twine can write the value of variables right into text using basically the syntax that you just used. Why would you need to copy and paste it 600 times? I’m confused as to why others told you that was necessary.

Well I have 600 different judges a player can end up with, each having their own statistics. So that’s why it would be easier if those numbers were grabbed from a spreadsheet.

That’s fine… But couldn’t you just import the spreadsheet values into a few arrays and then print the value when you need it?

For example, even if you had 600 different judge names, you’d only have to print the name with “Judge $judge_name is presiding.” after you set $judge_name to the current name of the judge. This is a lot less work than rewriting the paragraph 600 times and changing only a few words.

4 Likes

Okay, I think I’m following along … so if I put something like this at the beginning of the passage, would this work?

(if: $judge_name is “Joe”)[(set: $list to (array: “92”, “57”, “35”)](elseif: $judge_name is “Jane”)[(set: $list to (array: “62”, “6”, “56”) … and so on

Yeah. Something like that should work.

I agree, the syntax you wrote is close to what Twine expressions are, though you’d probably need to put it inside a print macro in your ending passage. See the wiki for more information or check out my modifications of your example below:

https://twinery.org/wiki/print
https://twinery.org/wiki/expression

Instead of having everything inside of one giant print macro, I used one for each variable to leave your text outside and more legible.

Twine expressions can also do inline math. Depending on how you set up your arrays, probably with set macros (?), this could save you same typing / data re-entry to just calculate the spread at the end passage rather than manually entering every spread difference.

1 Like

Had a thought about one way to set this up. Instead of loading all 600+ judges into an array at the beginning or at the end passage, you should break it up and do it just before the recombine while the branches are still separate and are about to come together for your ending statistic passage. That way when you finish the Los Angeles branch, you just write the 10 different judges for Los Angeles, randomly pick from them, and then send the player to the ending and it should just work. Separated out like that, you can test the whole thing with only one branch and 10 judges entered. As you add different branches / city paths later, you just add the judges related to that city, etc., until you are finished. Done this way you’ll have something to test without having to do the whole thing first.

2 Likes

I work in AXMA which is somewhat different from Twine. So my subjective impressions:

1300 passages was no problem at all when running the game. I noticed slowdowns when compiling to HTML, and when running in the IDE (since it has to compile exactly what’s there each time.) And to be honest, the text in the passages doesn’t seem to slow things down on compile as much as when I add sounds or a background image to the game that get hard-coded into the HTML.

AXMA uses an external file folders for most graphic images and long pieces of background music, but the game background image and any ad-hoc shorter sounds get written into the “StoryStyle” passage which contains all the CSS…which holds the background and the sounds which can be called up instantly and played over currently running background music.

Just the background is no problem, but I’ve noticed as I add more sounds and long strings of code into the StoryStyle, that slows down compile. Once the game is exported, I’ve noticed that these bigger games take maybe 15-20 seconds to load for the first time from a website, and then it’s pretty instantaneous. Same for my external music - some longer pieces seem to get cached and on the first run a large music file will delay a moment before playing. After that it seems to be in the cache and runs without any problems.

As I said, this is particular to my specific system and uses and may not reflect at all how Twine works.

That said, text is cheap and stories that don’t involve multimedia probably will have minimal slowdowns and have lower file-sizes that load pretty quickly.

I’m glad you’re here to discuss your methods also, because there are tricks to doing the thing you’re describing using text-variation and other streamlining methods. For example, if you let a person choose their name from a selection of five at the beginning of the story, you probably don’t want to rewrite the story five times with a name change. That’s what you use variables for - to vary the text inline and display it when the player sees it based on stored choices as opposed to having 600 actual different passages with all the permutations.

1 Like

I use sugarcube2 instead of harlowe3.

That said I have something equally complicated. I use a lengthy multi-dimensional array to store everything, including the criteria, say like this

set $GameData to
[
[location_id, condition, outcome],
[1, “($game.var1 is “John”) && ($game.var2 is “Mary”)”,“Invite them to dinner in Rocky Cafe”],
[1, “($game.var1 is “John”) && ($game.var2 is “Cindy”)”,“Invite them to lunch in Rocky Cafe”],
[2, “($game.var1 is “John”) && ($game.var2 is “Mary”)”,“Invite them to dinner in Pacific Coffee”],
[2, “($game.var1 is “John”) && ($game.var2 is “Cindy”)”,“Invite them to lunch in Pacific Coffee”]
]

That way I can concentrate on the story and the writing instead of writing 600 if cases. If you have 600 cases already written on a google worksheet, it isn’t hard to do a conversion.

1 Like

@egaskrad

You should probably put that in setup.game_data. It looks like you’re saving a large unchanging array to disk storage, which really makes no sense to me. If you put it in the setup object, you’d get the same functionality without increasing the size of your save files.

Bear with me as the following is from a fighting game I was experimenting with in Harlowe but I think will be relevant for your situation.

Since I didn’t want to re-write the same fighting sequence in multiple passages, I instead I did the following.

I set all the following variables with datamaps at the beginning of the story:

(set: $noenemy to (dm: "name","none","title","none","LP",0,"CLP",0,"hit",6,"damage",0,"armour",0,"status",0,"magic",true,"TFR",true,"bribe",false,"BA",0))
(set: $enemy to $noenemy)

$noenemy was so I could clear out the $enemy datamap quickly so may not be necessary for your case. $enemy is the variable used in the fighting sequence passages.

Then I added the rest

(set: $blackknight to (dm: "name","Black Knight","title","the Black Knight","LP",25,"CLP",25,"hit",6,"damage",10,"armour",6,"status",2,"magic",false,"TFR",true,"bribe",false,"BA",0))
(set: $ogre to (dm: "name","Ogre","title","the ogre","LP",40,"CLP",40,"hit",6,"damage",15,"armour",0,"status",2,"magic",false,"TFR",true,"bribe",false,"BA",0))
(set: $wolf to (dm: "name","Wolf","title","the wolf","LP",20,"CLP",20,"hit",6,"damage",3,"armour",0,"status",2,"magic",true,"TFR",true,"bribe",false,"BA",0))
(set: $boar to (dm: "name","Boar","title","the boar","LP",25,"CLP",25,"hit",6,"damage",4,"armour",0,"status",2,"magic",true,"TFR",true,"bribe",false,"BA",0))
(set: $savageHound1 to (dm: "name","Savage Hound","title","the first savage hound","LP",20,"CLP",20,"hit",5,"damage",3,"armour",0,"status",2,"magic",true,"TFR",true,"bribe",false,"BA",0))
(set: $savageHound2 to (dm: "name","Savage Hound","title","the second savage hound","LP",20,"CLP",20,"hit",5,"damage",3,"armour",0,"status",2,"magic",true,"TFR",true,"bribe",false,"BA",0))

and if I was going to fight the boar just added the following line in the passage before displaying the fight passages:

(set: $enemy to $boar)

so if I wanted to see what the boars life points are in the fight:

(print: $enemy's LP)

So for your judges it could look like the following:

(set: $noJudge to (dm: "name","none","max",0,"min",0))
(set: $judge to $noJudge)
(set: $joeLA to (dm: "name","Joe","max",92,"min",57))
(set: $janeLA to (dm: "name","Jane","max",62,"min",6))
(set: $bobLA to (dm: "name","Bob","max",87,"min",43))
(set: $louiseLA to (dm: "name","Louise","max",90,"min",63))
...

You can check if a judge has been assigned with this

(if: $judge's name is $noJudge's name)

Assign a Judge like this

(set: $judge to $louiseLA)

and print out the details for the judge

The judge's name is (print: $judge's name). The maximum denial rate is (print: $judge's max)%. The minimum denial rate is (print: $judge's min)%. That's a spread of (print: ($judge's max - $judge's min))%.
1 Like

@tayruh

Good point. Never thought about that.

Hey Nicholas, thank you for the follow up.

I was going to print my arrays like this (which works):

The highest denial rate at this court was (print: $list’s 3) percent.

If I were going to do it your way <<print $DENIAL_MAX>>, how do I tell Twine what $DENIAL_MAX’s value is from the array?

Also, your suggestion of continue “branching and bottlenecking” the story map off the cities is a great idea too. It could be the most organized way. Thank you --I hadn’t thought about this!

1 Like

This is such a great approach as well. I’m new to Twine so I haven’t really explored my options with datamaps. You have provided a great and understandable example of it. I will give it a shot! Thank you.

1 Like

Hi Ruby, I forget sometimes that Harlow is the default now because I used SugarCube in the past so use this link instead for the Harlow story format syntax: Twine Cookbook


I like the example James shared with datamaps for readability reasons, so I would suggest creating a judge object with a datamap to do what I was suggesting in the end passage.

I would use arrays for the 600+ judge data, however, because your spreadsheet should be able to make a plain text file CSV (comma separated values) export that you can copy and paste from into the Harlow array syntax. If you can get comma separation by copying and pasting directly from your spreadsheet than you can skip the export step.


For example, in Twine in the branch you are working on, let’s assume we know the reader is going to get Judge Joe. Near the end of the branch, when it is certain the Judge will be Joe but before we send the reader to the common ending passage, put this into the passage before the link which sends the reader to the ending.

(set: $joe to (array: 92, 57, 35, 88, 66, 22))
(set: $placeholder to $joe)
(set: $judge to (dm: “NAME”, “Joe”, “DENIAL_MAX”, $placeholder’s 1, “DENIAL_MIN”, $placeholder’s 2, “DENIAL_SPREAD”, $placeholder’s 3, “GRANTED_MAX”, $placeholder’s 4, “GRANTED_MIN”, $placeholder’s 5, “GRANTED_SPREAD”, $placeholder’s 6))

Array syntax is easier to copy and paste into when you have a csv, so I create an array for judge Joe’s statistics. I am making the assumption that Twine array’s are type strict - if they aren’t then you should be able to do this instead assuming I have guessed the order of your spreadsheet columns correctly:

(set: $joe to (array: “Joe”, 92, 57, 35, 88, 66, 22))
(set: $placeholder to $joe)
(set: $judge to (dm: “NAME”, $placeholder’s 1, “DENIAL_MAX”, $placeholder’s 2, “DENIAL_MIN”, $placeholder’s 3, “DENIAL_SPREAD”, $placeholder’s 4, “GRANTED_MAX”, $placeholder’s 5, “GRANTED_MIN”, $placeholder’s 6, “GRANTED_SPREAD”, $placeholder’s 7))

I like the readability of datamaps so I use array $placeholder for the data and then reference that array by index in the datamap for the $judge object. By having the intermediate $placeholder array this code is more portable and easier to copy and paste between branches. When you do, you only need to replace the bold parts above for this work.

The $joe array can be here in the branch, but it may be wiser to have it in the setup passage to keep it all in one place, though each judge would still need their own array because Harlow array syntax appears to be one-dimensional only, meaning it assumes a single row while your spreadsheet data assumes one row per judge.

In your end passage you reference the judge object like this:

The denial rate at this court by Judge (print: $judge’s NAME) ranges from (print: $judge’s DENIAL_MAX) percent to (print: $judge’s DENIAL_MIN) percent. That’s a spread of (print: $judge’s DENIAL_SPREAD) percent.

Earlier I mentioned the possibility of using inline math to calculate the spread but I’m not going to recommend doing that because it looks more readable without it and your spreadsheet already has it calculated.

Organization is key for large projects. Geography seemed like a sensible way to group things because the judges affect your endings numbers. The structure of your story could take other forms depending on how you are writing it, such as Quest or Sorting Hat:

1 Like