(Re)learning ChoiceScript with Implicit Control Flow

I originally learned ChoiceScript several years ago, long prior to the introduction of Implicit Control Flow in 2017, then vanished from the community and stopped writing in CS for several years. During that time, I forgot basically everything about the language–basic syntax, *choice statements, temporary variables, you name it.

Then, just a few months ago, I decided to get back into ChoiceScript and began re-learning the language from scratch. I was a complete blank slate. Then I started running into some frustrations with mandatory *gotos and, while searching for a workaround, stumbled across Implicit Control Flow. Thinking I’d give it a shot, I flipped ICF on and learned the rest of ChoiceScript while using it.

Additionally, in between these two periods of my life, I acquired a computer science degree. I now code regularly and am very familiar with the traditional structure of imperative languages, but those things weren’t true when I learned ChoiceScript the first time. At that point, my only experience with “programming” had been some minimal exposure to the block-based structures used in GameMaker. I’d never coded in an actual language before. I had, however, done a lot of writing, and wrote fiction far more frequently than I do now.

These circumstances gave me the unique experience of learning ChoiceScript twice–once without ICF as a writer and non-programmer, and once with ICF, as an experienced programmer. I wanted to talk a little about that experience and see if anyone else has been in a similar position. I’ve noticed some people come into the CoG community self-identifying as either “writers first, programmers second” or “programmers/game designers first, writers second,” so I hope what I write here can speak to both of those groups and potentially bridge some gaps. I also hope to point out some ways I think ChoiceScript could be made more accessible to programmers, without making it any less accessible to non-programmers.


  1. Learning ChoiceScript with ICF

Turning on ICF was a game-changer for my productivity. My writing speed saw a huge improvement, as I was able to create much more complicated branching paths in a fraction of the time.

I experienced this speed-up for 3 reasons.

First, because 95% of my *gotos took me to *labels placed immediately after the *if/*else or *choice block within which the *goto is enclosed. Moving from explicit to implicit control flow felt like I had an extra person next to me, typing out each of those *gotos and *labels on my behalf.

Second, because coming up with new label names is a creative task that, while not very hard, does eat up a tiny amount of time whenever you have to do it. ICF cuts down the number of times your brain has to invent a new label.

Third, because keeping track of label names becomes ever more of a cognitive burden as a story gets longer. Before switching to ICF, I found myself storing a vague list of the current scene’s labels in my head, so that I could avoid repeating the same label twice. Not only did I want to avoid duplicate labels, but I also wanted to make new labels sufficiently distinguishable from the old ones. Ideally, I thought, my labels should be both descriptive of the part of the story they’re located in, and yet memorably different from labels located in adjacent/descriptively similar parts of the story. That way I won’t confuse them. This got harder and harder to do as my scenes got larger.

Coming to ChoiceScript as an experienced programmer, I originally didn’t even realize that *gotos were necessary to close out *if/*else and *choice blocks. I simply assumed that ChoiceScript operated like a typical programming language, where instructions are read one after the other in the order they’re written and where if/else statements don’t interrupt this process. And, since *choice blocks have a vaguely similar syntax to *if/*else statements (i.e. an asterisk, followed by a command name, followed by an indent), I assumed that they were no different.

My assumptions that ChoiceScript behaved this way ran so deep that despite reading the entire tutorial on the CoG website before I started coding/writing, I was still surprised when I got my first error messages. The part of the tutorial that would have corrected me somehow went in one ear and out the other. As weird as it sounds, I think this might be a pretty common experience for programmers who are new to ChoiceScript.

The big downside I experienced from switching to ICF was losing some control over whitespace. Consider the following lines of ChoiceScript:

You walk down the street, trying to look as inconspicuous as possible.
*if (ninja)
    As a secret ninja, you can't afford to draw attention to yourself.
    *goto unacceptableText
*elseif (wizard)
    It's illegal to be a wizard, of course, and you don't want to be imprisoned.
    *goto unacceptableText
*else
    The thought of being attacked by a criminal wizard or a stealthy ninja terrifies you, and you know they only target the most conspicuous of pedestrians. Do you really want to be their next victim?
    
    *goto unacceptableText
*label unacceptableText
No—that would be unacceptable.

If you’re either a wizard or a ninja, we want to print “No—that would be unacceptable” on the same line as the text which precedes it. If we are neither a wizard or a ninja, we instead want to insert whitespace between “Do you really want to be their next victim?” and “No—that would be unacceptable.” The above code accomplishes this.

But with ICF, the same task is no longer so simple.

*create implicit_control_flow true
You walk down the street, trying to look as inconspicuous as possible.
*if (ninja)
    As a secret ninja, you can't afford to draw attention to yourself.
*elseif (wizard)
    It's illegal to be a wizard, of course, and you don't want to be imprisoned.
*else
    The thought of being attacked by a criminal wizard or a stealthy ninja terrifies you, and you know they only target the most conspicuous of pedestrians. Do you really want to be their next victim?
    
No—that would be unacceptable.

ChoiceScript interprets the blank line above “No—that would be unacceptable” as lying outside the *if/*else block, creating whitespace between “No—that would be unacceptable” and the text which precedes it. This occurs regardless of whether the player is a ninja or a wizard. And since the *line_break command produces a different kind of whitespace than a traditional paragraph break, ChoiceScript has no easy way to insert whitespace in these locations. Indenting the blank line doesn’t move it inside of the else statement either–though I don’t think it should, since text editors don’t always remember the number of indents on a blank line.

This same problem occurs with the last option in a *choice block as well.

If ChoiceScript ended blocks with a closing curly brace (as in C or Java) or a keyword like “end” (as in Julia), this wouldn’t be a issue for ICF. It also isn’t a issue when using explicit control flow, since *goto statements serve as a sort of “closing bracket” for all indented blocks.

The workaround I came up with was repurposing the *comment command as a closing bracket, either on every conditional:

*create implicit_control_flow true
You walk down the street, trying to look as inconspicuous as possible.
*if (ninja)
    As a secret ninja, you can't afford to draw attention to yourself.
    *comment endcond
*elseif (wizard)
    It's illegal to be a wizard, of course, and you don't want to be imprisoned.
    *comment endcond
*else
    The thought of being attacked by a criminal wizard or a stealthy ninja terrifies you, and you know they only target the most conspicuous of pedestrians. Do you really want to be their next victim?
    
    *comment endcond
No—that would be unacceptable.

… or solely at the end of an entire *if/*else block:

*create implicit_control_flow true
You walk down the street, trying to look as inconspicuous as possible.
*if (ninja)
    As a secret ninja, you can't afford to draw attention to yourself.
*elseif (wizard)
    It's illegal to be a wizard, of course, and you don't want to be imprisoned.
*else
    The thought of being attacked by a criminal wizard or a stealthy ninja terrifies you, and you know they only target the most conspicuous of pedestrians. Do you really want to be their next victim?
    
    *comment endcond
No—that would be unacceptable.

Similarly, I use “*comment endchoice” whenever I need to close out individual options within a *choice block.

Adhering to this stylistic convention is still less labor-intensive than using explicit control flow, but I’d prefer something like this to be built into ChoiceScript rather than having to use a workaround. The alternative is that I sacrifice a little bit of (in my opinion, important) control over the paragraph breaks in my story.

(One possible solution: ChoiceScript could have (optional) no-op commands (perhaps called *endcond and *endchoice) which you could place at the end of choices and conditionals that would provide the same functionality as my *comment endcond/endchoice workaround. The syntax of the Julia language, which uses an end keyword instead of a closing bracket, does something similar. And since both Julia and ChoiceScript were built with intentionally non-threatening syntax, I figure ChoiceScript could be able to benefit from some of their design decisions.)


  1. Learning ChoiceScript without ICF

When I first learned ChoiceScript, I set out to write a short story about a man with Herculean strength who had to escape from a prison. Unfortunately, I never finished it. But I do still have the original files tucked away on my computer.

Looking back over my pre-ICF code from that era, I’m struck by how different my ChoiceScript style was. I used far fewer indents, far fewer conditionals, and far fewer branching paths. Back then, I conceptualized code not as a bunch of instructions executed in a sequence, but as pieces of fabric that I could stitch together with string.

Each branching path (created by an *if/*else or *choice block) was a different piece of fabric. I could embroider those pieces of fabric by writing more of the story inside of them, and I could stitch them to additional pieces of fabric by creating new branching paths. I never wanted to juggle too many pieces of fabric at once, however, and if two of my branches got too large, I would use a *goto to send them both to a common location–stitching them together into a single piece of fabric.

The ultimate goal, of course, was to create a giant quilt.

My mental model of programming wasn’t the only place past-me and present-me differed. Past-me also experienced none of present-me’s frustrations with mandatory *goto statements. I didn’t even think of it as a restriction on what I could do with the language. Given how past-me conceptualized coding, this makes perfect sense.

Additionally, I didn’t feel frustrated with having to remember large numbers of labels. I created new labels the same way I created new temporary variables: whenever I needed them. And mentally keeping track of existing labels never struck me as a uniquely arduous task. If past-me had been handed the option to use ICF, I’m not sure I would have taken it–not at first, anyway.

In short, I was demonstrably fine. While that might have changed if I had used ChoiceScript for a longer amount of time before I took my hiatus from the language, I was perfectly content during the period I interacted with it. Additionally, my learning curve then was no longer or shorter than my learning curve today. I reached ChoiceScript fluency in about the same time, with or without ICF.


There’s a big takeaway I got from this experience: ChoiceScript contains certain features that were, counter-intuitively, easier for me to understand as a non-programmer than a programmer.

As I explained earlier, I read CoG’s entire ChoiceScript tutorial thoroughly, top to bottom, before I even wrote a single line of ChoiceScript as a programmer. The tutorial explains mandatory *gotos very clearly. On the Introduction to ChoiceScript page, it states:

Note that every indented (nested) block must conclude with either a *finish command (which ends the scene) or a *goto line which jumps to another line in the scene.

Yet despite this information being presented right to my face, it went in one ear and out the other. When I learned ChoiceScript as a non-programmer, I grasped the concept of mandatory *goto statements no less easily than any other aspect of the language. But when I learned ChoiceScript as a programmer, my brain went kaput.

I think this speaks to some interesting ways that ChoiceScript is inaccessible to programmers. When my *if/*else and *choice blocks first started generating errors, it took me a little bit of time to diagnose what I’d done wrong. It took a fair bit longer to find out that ICF existed, and to enable it. This wasn’t the biggest stumbling block in the world on my way to re-learning ChoiceScript, but it was a stumbling block that I never encountered at all when I learned ChoiceScript as a non-programmer. I also think it’s a stumbling block that, if the programmer version of me had been introduced to ChoiceScript a little differently, I might have avoided.

At present, Implicit Control Flow isn’t mentioned in any of the any of the tutorial pages for ChoiceScript located on the main CoG website. Information about ICF can be found on the wiki, but wikis are meant to serve as reference material, and serve a very distinct purpose from tutorials.

I think an easy way to make ChoiceScript more accessible to programmers, and to help them avoid the stumbling blocks I ran into, is to make an additional page under the Make Your Own Games tab, specifically aimed at programmers and explaining how ChoiceScript differs from the languages they’re used to. That means illustrating how ICF works, how it can be activated, and why a newcomer might be interested in using it. Since using ICF significantly changes same pretty basic parts of ChoiceScript’s functionality, I think it’s really important that people who’d prefer to ICF to explicit control flow are introduced to the option as early as possible.

A lot of effort has been put into making ChoiceScript accessible to non-programmers, and for good reason. But in my opinion, the twin goals of making ChoiceScript accessible to non-programmers and programmers aren’t mutually exclusive. In fact, I think they have substantial overlap. I think what I’ve proposed here would help introduce people who might prefer ICF–regardless of whether they are programmers–to the option a little faster, and enable programmers with similar experiences to my own to avoid a hiccup or two while learning ChoiceScript.


tl;dr Implicit Control Flow should be added to the ChoiceScript tutorial on the CoG website, under a new page aimed at explaining to programmers how ChoiceScript differs from languages they’re used to. I think this is really important and could make ChoiceScript significantly more accessible. I also describe some problems with ICF and whitespace.


I’m interested in hearing whether anyone’s had a similar experience with ICF, programmer or otherwise. Did you learn ChoiceScript before or after Implicit Control Flow was released in 2017? How did you first hear about ICF, and–if you currently use it–do you wish you’d been introduced to it sooner in the ChoiceScript learning curve?

16 Likes

I learned ChoiceScript in the past few years (after ICF was released), with a decent level of programming knowledge (not to the point that I’d call myself an actual programmer), but initially without knowledge of ICF, and yes, I found the mandatory *gotos and *labels enormously tedious and creatively taxing—to the point that I started using workarounds specifically to avoid this aspect of the language. For instance, instead of doing:

*if variable
  stuff
  *goto after
*elseif other_variable
  other stuff
  *goto after
*else
  other other stuff
  *goto after
*label after

I found myself doing ridiculous stuff like this:

*if variable
  stuff
*if (not (variable)) and (other_variable)
  other stuff
*if (not (variable)) and (not (other_variable))
  other other stuff

Which technically still works, but I think a lot of programmers would frown at that practice. (Plus, figuring out the necessary logic for doing this can sometimes be even more time consuming than just succumbing to the *goto-*label format in the first place.)

And then, of course, there’s *fake_choice. Which is worth a discussion in its own right: despite the purported purpose of *fake_choice being to present just that—a choice that has no mechanical purpose—it has, over time, allowed for just about every major functionality that regular *choice has, including setting variables…without requiring the *goto. It has, in essence, become its own little facsimile of ICF, to the point that a lot of people use *fake_choice almost exclusively, just to avoid having to deal with those pesky *gotos.

When I finally discovered ICF (and started using it immediately, of course), what I realized is that I had pretty much already been trying to mimic the mechanics of the feature, except less efficiently. And like I said, it wasn’t only me—the base language itself seems to be mimicking ICF with the expansion of the *fake_choice command, to the point that its name is now an absolute misnomer.

One thing I’d note about the programmer vs. non-programmer point is that I can actually point to a specific case in which I introduced ICF to a writer who is very much not a programmer, and it was immediately apparent how much easier and more intuitive she found it than the mandatory *gotos. This is only one example, obviously, but coupled with what I mentioned earlier, about how many writers overwhelmingly use *fake_choice to “mimic” ICF, I do believe that there’s a case to be made for ICF being more intuitive in general, not just to those with programming experience.

All this to say that I absolutely agree with you: ICF should be more widely acknowledged, because it really does make a big difference to a lot of writers, programmers and non-programmers alike. I know I would be having a rougher time of things without it.

8 Likes

Just to be clear: your problem with whitespace is about readability or about printing an empty line? Because for the second one you could use *line_break.

In any case, personally, I’m not a fan of closing commands like in Julia, I think it’s verbose. On the other hand I agree that the mandatory *gotos are annoying as hell. Good thing there’s ICF.

It is just how any other programming language works by default which might explain why it’s harder for programmers to get used to it. We simply naturally expect ICF to be the default.

And I agree that it’s really not that hard. From what I’ve seen in this forum, no one who adopted it came to regret it.


About style standard, I’ve adopted the Zen of Python to ChoiceScript since both languages relies on whitespace and are meant to be accessible. I’m especially fond of the commandment: “Flat is better than nested”. Might be an unpopular opinion around here though :eyes:.

2 Likes

Yikes, that workaround with the repeated if statements was actively painful to read. You have my sympathies!

I read the post you linked:

… and with that alone, I think you’ve successfully convinced me ICF can be vastly more intuitive (and a major source of relief!) for non-programmers as well as programmers. wildelight seems downright thrilled about it here. :smiley:

What I wanted to communicate with my post was that a lack of ICF can be a hurdle during the initial learning curve for newcomers to ChoiceScript with a programming background, since programmers have certain expectations about how languages typically work. I was originally agnostic as to whether ICF is easier to use, less frustrating to use, or increases writer productivity for non-programmers after they’ve learned the basics of ChoiceScript, since I myself stopped using the language not that long after I learned it the first time around–I didn’t want to comment outside my realm of experience. But having seen wildelight’s reaction, I now think I probably would have preferred ICF if I’d been offered it.

As for the present-day SilasLock, they vastly prefer ICF. :grin:

1 Like

Preach it, 9 times out of 10 that particular commandment is spot on. :grinning:

We’re gonna have to agree to disagree about Julia’s closing commands being bad, though. I’ll admit I’m no expert on programming languages, but my personal opinion is that there are some significant advantages to a Julia with the “end” keyword to a Julia without one. Probably outside the scope of this thread, though. :sweat_smile:

In any case, whether they’d be useful for ChoiceScript is another matter altogether. I’m open to pretty much anything that gives the writer a little more control over their spacing and paragraph breaks.

The whitespace problem was about printing an empty line, not readability. But *line_break, unfortunately, doesn’t do quite the same thing as a proper blank line of code. The two create slightly different types of whitespace/line breaks.

I have a huge ChoiceScript testing suite that I set up to demonstrate how various combinations of *line_break and empty lines of code interact. And going off the tests I did, there doesn’t seem to be a way to replicate the type of spacing produced by a single blank line of code via a *line_break command alone. I took several pictures of the results and they should illustrate the problem better than I can explain it with words.

Unfortunately, my user profile is a little too new to the CoG forums to post images. :pensive: Once my account’s permissions get updated I’ll post them here.

Just slap an [n/] on it, problem solved!

1 Like

I never noticed that, but it’s going to bug me now. The gap is just slightly larger. :sweat:

Same problem!


When the text is short enough I feel like the cleanest solution is to have a bit of repeated text

You walk down the street, trying to look as inconspicuous as possible.
*if (ninja)
    As a secret ninja, you can't afford to draw attention to yourself. No—that would be unacceptable.
*elseif (wizard)
    It's illegal to be a wizard, of course, and you don't want to be imprisoned. No—that would be unacceptable.
*else
    The thought of being attacked by a criminal wizard or a stealthy ninja terrifies you, and you know they only target the most conspicuous of pedestrians. Do you really want to be their next victim?
    
    No—that would be unacceptable.

but I don’t know what you’d do if it was longer. Maybe insert the repeated text with a *gosub? I saw an example like that in the code for A Study in Steampunk.


I first learned about ChoiceScript before ICF (circa 2012, I think?). At the time I had no prior programming experience. I dabbled with CS but it never stuck, and the massive amount of labels needed were incredibly annoying. I ended up learning Twine instead.

When I rediscovered CS sometime last year, I was lucky enough to stumble across forum posts explaining ICF (and other less documented features like newline characters, arrays, and gosub parameters) early. By this point, I had some experience in a variety of languages, and can honestly say I wouldn’t have the patience to use ChoiceScript without ICF.

CS without ICF feels a bit like driving a car you have to smack at regular intervals to keep running. If I truly wanted that kind of programming experience, I’d just go back to SAS. shudder


I think it would be nice to have a place to centralize information about ChoiceScript - changes to the language, lesser known features, best practices, bugs. The info is out there, but fragmented across the forum, wiki, and Discord. Maybe some updated rumor-checking too? Like, is it still best practice to avoid double *line_breaks and nested *fake_choices? Do they still cause problems?

1 Like

What I do is just individualize the blocks until the precise moment I can converge them—even if it happens mid-sentence. Like so:

You walk down the street, trying to look as inconspicuous as possible.
*if (ninja)
    As a secret ninja, you can't afford to draw attention to yourself. No—that 
*elseif (wizard)
    It's illegal to be a wizard, of course, and you don't want to be imprisoned. No—that 
*else
    The thought of being attacked by a criminal wizard or a stealthy ninja terrifies you, and you know they only target the most conspicuous of pedestrians. Do you really want to be their next victim?
    
    No—that 
would be unacceptable.
3 Likes

@SilasLock , you should totally join the ChoiceScript wiki and make its information on this less fragmented. The forum and Discord are always going to be fragmented, by their nature; the wiki seems like the ideal place to build up a deposit of knowledge, but it depends on the energy of its contributors and can easily fall out of date, especially with CS functions that many of the less ambitious coders don’t use. I’m going to guess that right at the moment, you’ve got the energy to contribute some long and helpful sections. :slight_smile:

Getting CS to interpret your white space correctly has always needed workarounds on the margins. I forget who advised me to use *comment endif as the way to force it to recognize your intended para break, but that’s what I did for Rebels.

As a non-programmer who learned CS before ICF was a thing, I still take some comfort in my gotos. NOT with if/elseif/else blocks – there, I’ve always found the need for gotos maddening, because I almost always want those to end in the same place after a bit of variable text. But my choice blocks usually jump around, referencing different bits of code here and there, and I find that easier to manage with gotos. I tried turning on ICF but found it was actually slowing me down, requiring more attention to make sure I wasn’t accidentally sending one of my choice paths into oblivion. Starting every choice with a quick copy-paste of a label/goto combo, and then editing those as I flesh the choice blocks out, doesn’t feel like it’s taking meaningful creative energy for me, or more than a couple seconds of time.

That’s also why I still don’t use *fake_choice unless it’s a genuinely fake choice. That said, I’ve been grumbling for years that *fake_choice needs to be deprecated; it’s a needlessly confusing and even misleading name. If CoG editors are now advising their writers to always use fake_choice for all choices, then for heaven’s sake let’s make ICF the standard already and just call them all *choices. We’d get used to it…

10 Likes

*line_break and [n/] works by adding <br> element instead of <p>. In HTML, the difference is fairly big, one of them being the size of the break.

2 Likes

Nice write-up, and +2 from me on encouraging wiki contributions! PM me if you’re interested but need permissions.

3 Likes

Heretic! You mean to say you don’t stack three choices inside each other?! :joy:

1 Like

I’ll die on that hill! :joy:

2 Likes

Put two, then! [n/][n/] creates two line breaks, i.e. a paragraph break.

Hi all, and welcome @SilasLock. Very interesting discussion here. I really like the idea of *comment endblock.

Yes, as a programmer, I find CS a little heavy and inconsistent at times (and its documentation very fragmented). ICF is a blessing, coding without it would be very tedious.

Note that every indented (nested) block must conclude with either a *finish command (which ends the scene) or a *goto line which jumps to another line in the scene.

According to this, *if should require *goto as well (it doesn’t). And I have had issues when *goto points to an indented label. I work around some of these limitations with a custom preprocessor.

@will what is [n/]? Another odd undocumented CS feature?

@SilasLock I’d love to see the results of your experiments. Please keep us posted. (Your status will get bumped quickly when you read, answer and like other posts).

1 Like

I’ve been a programmer for years, and I’ve been using ChoiceScript for about two or three years. I’ve never taken to using ICF. I often see that other people speak very highly of it, but when I first read about how it worked, I could immediately tell that it wouldn’t suit me at all. ICF is about removing restrictions. My personal experience has been that tools like that introduce logical bugs that are much harder to detect. I’ve avoided it for this reason.

I’ve not found the use of *goto very onerous. Maybe it’s because it’s not required for *if statements with no *else or *elseif, which I use a lot. Now and then I’ll run into a situation where I need to slap on a new label to help manage a series of complex conditions for conditional text. Mostly, I find that it challenges me to be cautious in designing my variables, which is one of my main design stumbling blocks, so I’ve appreciated it.

5 Likes

[n/] is the same as *line_beak but inline. It’s like the especial charater \n in a string in most programming languages.

For exemple, the output for:

Roses are red [n/] Violets are blue
*line_break
The above line
*line_break
Will be split in two

Will be:

Roses are red 
Violets are blue 
The above line 
Will be split in two
4 Likes

[n/] creates a line break and it’s way more useful than *linebreak! It’s not technically undocumented, but it’s fairly hidden.

1 Like

I think I’d be interested in making a wiki contribution or two! @CJW, PM sent. :grin:

2 Likes

Still isn’t quite the same, two [n/]s creates an altogether different kind of break from that of a paragraph. It’s a little more similar in terms of how spacious it is than a single [n/], but not quite the right length. It also generates visible vertical whitespace characters when you highlight it, while a proper paragraph break does not.

And besides, from the wiki:

I haven’t tested this myself, but I’m inclined to take the wiki at its word.

Working on it! Give me a few days, I’ll get that member status eventually. :stuck_out_tongue:

It looks like that might be the best solution we have at present. The *comment endif syntax in particular is used in the queen politics file linked on the CoG website, so it’s been around since the days of Choice of the Dragon. This isn’t a new solution, it’s an old solution to a very old problem. And it’s probably the least bad solution that we have. Everything else I’ve seen proposed in this thread involves the writer having to consciously change where they place text and/or duplicate text, while using a *comment line isn’t nearly as intrusive into a writer’s workflow.

(I personally prefer my own *comment endcond to the traditional *comment endif, since it’s used for else statements as well as just if statements, but everyone has their own preference/habits for this particular piece of workaround syntax. The queenpolitics.txt file even uses *comment endif after a *fake_choice block or two (albeit with strange indenting), so it’s likely been used to force whitespace in places other than conditional blocks since the very beginning of CoG.)

1 Like