Deprecate *fake_choice?

Thanks for keeping CS growing, @dfabulich.

Something I’ve been wondering for a few years from my perspective as a non-programmer: Isn’t it past time to remove the restrictions on *choice and deprecate *fake_choice? What does the distinction still achieve, after *fake_choice was opened up to allow stat-setting and everything else?

I understand the concern about not pushing novice coders straight to ICF lest they get confused and buggy. But apparently CoG editors’ practice for years has been pushing authors to use *fake_choice wherever possible in the name of coding efficiency.

So what exactly is achieved by keeping a separate *choice command if writers are being actively discouraged from using it, and end up using it only in places where they would still need to under ICF (and are allowed to under *fake_choice)?

I understood the original distinction, which I still use out of habit in my own work, between choices that have consequence (including stat changes) and those that vary only flavor text. But if authors are being officially pushed to use just one of the commands as much as possible, it feels to me like the only thing *fake_choice adds is five extra keystrokes and confusion, and that in practice CoG editors are carrying out a tacit deprecation of what should be the simpler, clearer command.

Like I said a couple years back:

Let me know why I’m wrong. :slight_smile:

7 Likes

*fake_choice literally uses *choice under the hood. There’s no performance difference. About editors pushing one over the other, I think this is a cargo cult thing (when people do things out of habit or because they’ve seen others doing it but they don’t really understand why so they just repeat the pattern).

:point_up:This is ChoiceScript source code. In the last line, fake_choice calls choice and relays its parameters.

4 Likes

There’s maybe a semantic argument; using both commands can make it obvious, at a glance, for an editor what the intended behaviour of that choice is.

1 Like

When *fake_choice can do anything *choice can do, though, including branching code, doesn’t that cut into the semantic use?

I’ve personally found it useful in the original distinction – helping me quickly see which choices affect a stat or branch the code, and which are flavor text – but most people don’t seem to be using it that way, and it feels kind of absurd that the longer and explicitly “fake” command has turned into the standard.

1 Like

Oh, absolutely. I’m not arguing against deprecation or simplification, just positing one advantage. But yes, if most people don’t actually use them consistently then the advantage is lost.

2 Likes

I use them that way, for the same reason. But probably half of those options end in *gotos anyway and they basically all adjust stats so the difference is certainly academic at this point.

1 Like

Moved to a separate topic to reply here.

The core problem with delayed-branching multiple-choice games is that they’re really hard to test, because individual blocks of code can typically be hundreds or thousands of lines long. (In most programming languages, having functions/subroutines that are more than a few dozen lines long is a bad idea.)

My goal was to make ChoiceScript code as easy as possible to review for bugs, and as easy as possible to test automatically.

In my experience working on Alter Ego, the most common bug looked like this:

*choice
   #Be very naughty.
       Santa refuses to give you a present.
       [ ... 100 lines of text ... ]
   #Be mostly nice.
       Santa gives you a present reluctantly.
       [ ... 100 lines of text ... ]
   #Be as nice as can be.
       Santa gives you a present enthusiastically.
       [ ... 100 lines of text ... ]

Inside the gift box is a video game!

This code is buggy; if you’re very naughty, you still get a present. But a naive automated test would have no way of knowing that the code is buggy. The only way to find this type of bug is by manual testing (ugh) or supernaturally careful code review.

So, ChoiceScript doesn’t allow code like that. Instead, you must use a *goto or *finish before the next #option:

*choice
   #Be very naughty.
       Santa refuses to give you a present.
       *goto present
   #Be mostly nice.
       Santa gives you a present reluctantly.
       *goto present
   #Be as nice as can be.
       Santa gives you a present enthusiastically.

*label present
Inside the gift box is a video game!

The automated ChoiceScript tests can guarantee that the rule is followed, but still can’t automatically catch the bug. However, if the label is named well, code review can then catch the bug without superheroic powers of memory. (“Wait, Santa refuses to give you a present, but then you *goto present???”)

I think, in hindsight, that I shouldn’t have called it *fake_choice, but perhaps should have called it *short_choice or *quick_choice or something, but the point stands that writing a lot of code with *fake_choice (or with ICF) increases your risk of bugs like these.

When I first announced ICF, I claimed that it offered a quicker way of getting your ideas down on the page, but that it increased the risk of bugs. I still think that!

As I alluded above, one way to use ICF safely is to avoid having large code blocks, and to instead prefer to put all long blocks of code in subroutines. That does work (and some games are written that way), but IMO those games are significantly harder to read. (You have to keep the *gosub “stack” in your head at all times, so you remember to where you’re going to *return)

7 Likes

I think this particular bug will always be a risk even with ICF, and I don’t necessarily agree that the difference and ability to test for that bug is significant enough to justify not making ICF the default.

That bug will be there in all complex games; I think what is more important is to simply make people aware of it.

What helped most of all for me, if I’ve got a massive branching path with hundreds of lines of code, was just doing this:

*choice
  # Big Option A
    *goto a
  # Big Option B
    *goto b

*label a
...
*goto c

*label b
...
*goto c

*label c

The whole discussion on ICF reminds me quite strongly of this video:

3 Likes

Hey Dan–thanks for the response, appreciate your time.

I also appreciate the bug-screening intent of the *choice / *fake_choice distinction. But even using it, it’s still tricky not to lose yourself in big code blocks. Forcing people to use *gotos doesn’t affect those 100 lines of code per choice, so our CS example shouldn’t leave them out:

*choice
   #Be very naughty.
       [...100 lines of text later...]
       Santa punishes your inefficient coding practice.
       *goto present

Requiring a "goto doesn’t shrink the distance between the choice text and the branch point. If your last line of code before the *goto doesn’t happen to include a clear reminder that this is the end of the block following the “naughty” choice, a code reviewer still needs a good memory to catch the bug.

In general, whether a choice starts with *choice or *fake_choice, the code reviewer still needs to be reading attentively enough to answer: Should this choice lead to different immediate outcomes? Is that being done using a *goto or a stat adjustment? (With delayed branching as the “special sauce” for CS works, most branches don’t happen using *goto immediately following a *choice anyway, but *set followed at some remove by *if or similar.)

So I do wonder how many bugs are actually getting caught by keeping *goto mandatory. It feels to me like a minor debugging improvement that most authors and editors seem to be forgoing anyway (by preferential use of *fake_choice even if they don’t toggle on ICF).

Regardless, making *goto optional for *choice wouldn’t force coders to abandon the extensive use of *goto as a safety rail. And if there are still CoG editors who join you in prioritizing debugging over efficiency, it wouldn’t stop them from asking their authors to write that way.

It would just rescue us from the current absurdity of the tacit deprecation of *choice, and reduce the obtusity of the language for novice programmers. (“Why can I use *fake_choice for literally every kind of choice, not just fake ones…and why am I being pushed to use it as much as possible?”)

I’m not a programmer, so my instinct against manual testing isn’t as develoepd as it no doubt should be. But as an author, it seems to me that extensive “manual testing” – i.e. reading how your text will actually look in the UI – is inevitable with CS, at least if you care about pacing, dialogue, prose style, and overall readability. All the more so as CS’s growing arsenal of bells and whistles moves us away from txt files that look like this:

*if a
   big block of prose
*if b
   block of rather different prose

to something more like:

${char_name} verb${s} @{a some text|other text} before @{b some text|other text}.

which looks nothing like it will on the “page.”

Thankfully, CSIDE’s console feature now makes it way easier to manually test CS games. But from my perspective, the need for plenty of manual testing should also somewhat reduce our reliance on mandatory *gotos to avoid bugs.

3 Likes

Another option to throw out here is, rather than (or perhaps as well as?) making any changes to CS itself, to use intellisense. Tools like CSIDE and @Sargent 's vscode extension can provide code linting with modifiable profiles.

By which I mean these tools can add squiggles to your code as you type to warn you about certain things without completely forbidding you from doing them (and you can configure those squiggles off, based on your preferences).

Two polar examples:

  • Squiggle fake_choice commands as “please prefer choice, with implicit_control_flow”
  • Squiggle missing gotos

I’ve been thinking for a while it would be great if the CoG staff would consider providing us with a list of “officially recommended” lint rules. Other examples might be not using double line_break (if I recall correctly?), and I think the vscode extension already warns you against using three periods instead of a proper ellipsis.

Again, these tools can allow to turn off rules you disagree with, but I think having a default set would definitely help prevent a selection of bugs (for users of those tools).

6 Likes

In this example:

*choice
   #Be very naughty.
       Santa refuses to give you a present.
       [ ... 100 lines of text ... ]
       Santa punishes you.
   #Be mostly nice.
       Santa gives you a present reluctantly.
       [ ... 100 lines of text ... ]
   #Be as nice as can be.
       Santa gives you a present enthusiastically.
       [ ... 100 lines of text ... ]

Inside the gift box is a video game!

The problem isn’t the 100 lines of text between #Be very naughty and Santa punishes you; the problem is the 200 lines of text between Santa punishes you and Inside the gift box is a video game.

In ICF, right at the line Santa punishes you, you can’t see where you’re going, and, in this example, where you’re going is very, very far away. Automatically jumping hundreds of lines of code to an unlabeled location is what makes this code so risky. Forcing *goto requires you to at least label it.

I have mused that perhaps *choice and *else should automatically use ICF when the distance it’s jumping is short. So quick little examples where you can see all of the options right there on the screen wouldn’t require a *goto, but when the #options get long enough that you can’t see where you’re going, then we’d require a *goto.

When Kevin implemented ICF for Choice of Magics, he didn’t see much/any benefit in having “short” choices use ICF and long ones use *goto. IMO, that’s because Kevin’s approach is never to have 100 lines of text in an option; he’d pretty much always use a *gosub, even if the code was only intended to be called once, ensuring that you could always see the last line of every *choice. (But IMO that makes the code harder to read, not easier.)

1 Like

That sounds fantastic to me. :slight_smile:

Edit: and I’m with you on this one:

Mirroring the behavior of standalone (that is, *else-less) *if blocks in non-ICF. That’s kind of interesting.

In terms of complexity and ease of learning the language, though, I don’t like the idea of fundamental commands in the language having vastly different behavior depending on surrounding context. Especially when it’s based on a fuzzy measure like “length of intervening code”, such that adding a line or even a character somewhere could break semi-nearby code that was working perfectly happily before. Standalone *if blocks are a weird enough special case already.

What would that measure be, anyway? Character count? What if somebody writes lots of tiny sentences and paragraphs? Or, line count? Is that with word wrap, or without? How many characters per line, and how many lines per screen in your dev window? Fixed width or proportionally spaced?

Absolutely. On a related note, which might be getting too off-topic (worth a spin-off spin-off thread?), but at this point I feel like the lack of a proper transcript feature is really holding back development in ChoiceScript. It would be an invaluable testing tool, both for authors to use in writing and coding themselves, and for testers to be able to provide an exact record of what they did and what happened. And then once you’re logging it anyway, you might as well provide it to every player to scroll back and jog their memory, too.

Randomtest gets close, kinda, sorta. But I wish there were an equivalent transcript recorded for manual playthroughs as well.

5 Likes