New ChoiceScript features: implicit control flow, gosub parameters

choicescript-updates

#1

Thanks to a big code contribution from @kgold, there are new features to try in the latest version of ChoiceScript up on GitHub: implicit control flow and *gosub parameters.

Implicit control flow

Normally, you have to use a *goto before the end of a *choice #option or before an *else, or you’ll get errors like this:

It is illegal to fall out of a *choice statement; you must *goto or *finish before the end of the indented block.
It is illegal to fall in to an *else statement; you must *goto or *finish before the end of the indented block.

With implicit control flow, those errors go away; ChoiceScript will just skip over subsequent #options or *else blocks automatically.

Implicit control flow is off by default. To use it, you have to *create implicit_control_flow true in startup.txt. (You can also *set implicit_control_flow true or false in other places in your scene files.)

When implicit control flow is enabled, *choice works basically the same as *fake_choice, except you can also freely nest a *choice inside another *choice with implicit control flow.

Implicit control flow is convenient, but it can also make it harder to find bugs, like this one.

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

Inside the gift box is a video game!

In this example of implicit control flow, you still get a present even if you’re very naughty. It can be hard to catch this bug when you have long #options that fill a few screens of text.

ChoiceScript’s traditional approach (“explicit control flow”) forces you to add a *goto before #Be mostly nice; if you give the label a good name, like *label present, the error should be easier to find, like this:

*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!

This bug may be easier to find, because “refuses to give you a present” is right next to *goto present which is clearly wrong.

It’s possible that everybody will love ICF so much that we’ll enable it by default at some point. It certainly makes it easier to get your ideas down on the page; we’ll see if it comes at the expense of having more bugs.

*gosub parameters and *params

This one’s for the programmer-ish folks out there. Now you can write code like this:

*gosub visit "Dracula" "garlic"

*label visit
*params
You go to visit ${param_1} and you bring ${param_2} with you.
*return

After the *gosub label, you can include any number of parameters. When you use the *params command, it sets temps named param_1, param_2, etc. for each parameter. (It also sets a param_count temp with the number of parameters; in this case, param_count would be 2.)

Since param_1 and param_2 are not very good names, you might be tempted to write some code like this:

*params
*temp person param_1
*temp gift param_2

We anticipated that; you can just write the names of the parameters after *params and we’ll set the temps for you, like this:

*label visit
*params person gift
You go to visit ${person} and you bring ${gift} with you.
*return

You can also use parameters with *gosub_scene. *gosub_scene already allows you to optionally specify a label, e.g. *gosub_scene travel visit so if you want to pass in parameters to *gosub_scene, you must specify a label name. *gosub_scene visit "Dracula" won’t use a parameter; it will try to *gosub the label “Dracula”.

Programmer-ish people should note that parameters are just ordinary *temps and are scoped to the entire file, not the subroutine. Thus, if you *gosub within a *gosub, param_1 can and will be changed in the second subroutine and will not be restored when you *return.

However, *gosub_scene defines a new scope for *temp, so if you want your *params to be scoped to the subroutine, (for example if you want to use recursion,) you can use *gosub_scene instead of *gosub.

Here’s an example of a naive recursive Fibonacci subroutine. (Note that it can only work with *gosub_scene; it wouldn’t work with *gosub because of parameter scopes.)

*create return 0

*gosub_scene startup fib 6
${return}

*finish

*label fib
*params n
*if n < 2
  *set return 1
  *return
*gosub_scene startup fib (n-1)
*temp prev return
*gosub_scene startup fib (n-2)
*set return +prev
*return

Let us know if you run into bugs with these new features!


[CSIDE] The ChoiceScript IDE
Things that make it worth writing using choicescript
Non-existent variable (warning lots of text!)
Global or local namespace for *temp with *gosub_scene?
Choice based text in the middle of a page?
Need help makeing a inventory for my game
Stats that kill you?
Randomtest issue? Possible incorrect 'illegal to fall out of a *choice' message
Is this too unfair (ingame riddle using random)
Help me understand "illegal to fall in to *else statement" error
New features in ChoiceScript: change text size/color, hyphen spacing, @{} variable replacement
*goto not going where expected
Is it not possible to have an *if statement for a choice that checks two variables?
Randomtest weirdness cannot extract another token
Any possibility of gosub returning values?
#2

Hfffttt… Woowheeee! It’s finally here! :astonished: :grin:

My eyes enjoy reading every single letter of this… update? Patchnote? Changelog?

Anyway, I’d like to give @dfabulich, @kgold, and anyone that helps on making this change possible my personal reserve of :coffee: coffees and :cookie: cookies.


#3

I’ll point out that my WIP Choice of Magics has been using this change for a while, is at 320,000 words so far, and has had friends playtest several chapters. So, it’s pretty robust. The thing that I use the gosub parameters for is mostly stat changes, where you avoid bugs by having a single gosub handle both the change in stat and the reporting to the player. So if I say

*gosub bump “vivomancy” 2

then the player sees (Gained Vivomancy) and vivomancy increases 2, and if I ever decide that it’s better to say “(Gained 2 Vivomancy)” I just have to change this in one place instead of all over the place.

There’s also a tiny bug (?) that would be a pain to fix but is very easy to work around. You can’t goto into the middle of a different choice from the one you’re in and expect that to work properly; the interpreter needs to hit the original *choice in order to know where to jump out to. This is always fixable by using gosub instead of goto. So don’t do this:


*choice
      #A 
             *goto mylabel
      #B
             blah blah

*choice
      #C 
             *label mylabel
             more blah blah
      #D
              blah de blah

If you want to reuse that “more blah blah” text, do this instead:


*choice
      #A 
             *gosub mysub
      #B
             blah blah

*choice
      #C 
             *gosub mysub
      #D
              blah de blah
*finish

*label mysub
more blah blah
*return

Gosub should always be your preferred way of repeating code anyway, since it makes it easier for a reader to see the structure of the code, and this forces you to not be lazy about this.


#4

I just wanted to point out some “limitation” so if you encounter it, you can make a work around for it (or avoid it).

This doesn’t work :point_down:t4:

*temp loop <Insert some number here>

*label up
*choice
   *if loop = <number1>
      #Choice A 
   *if loop = <number2>
      #Choice B
   *if loop = <number3>
      #Choice C
   *else
      #4th option is always different from the others
         *goto somethingentirelyelse
*goto up
Instead, do it like this to make it work
*temp loop <Insert some number here>

*label up
*choice
   *if loop = <number1>
      #Choice A 
   *elseif loop = <number2>
      #Choice B
   *elseif loop = <number3>
      #Choice C
   *else
      #4th option is always different from the others
         *goto somethingentirelyelse
*goto up

However, if you’re not fancy with such complicated choice-tree, I guess you can just ignore what I post. Just a food for thought.


#5

Hey, I was giving this a try and I got stuck in how to make a variable receive the value that was sent to a *gosub_scene with parameteres. Here is the example I was trying, I made a separate file called “dollartopound” to convert a number I sent from pounds to dollars.

*label dollar_to_pound_calc
*params
round(param_1 * 1.33)
*return

Then when I want to use it, I was trying to set a variable in the scene I’m in to receive it, but it won’t read if the *gosub_scene is on the same line. And if I try to edit the dollar_to_pound from the *gosub_scene it doesn’t recognize the variable.

*set dollar_pound 0
*set dollar_pound *gosub_scene dollartopound dollar_to_pound_calc price_gau5

Price: ${dollar_pound}, which amounts to about £${price_gau5}.

If I try to call it from the same line as the Price: it won’t work either. I solved this by putting the converter in the same file and using a *gosub instead.

*label dollar_to_pound_calc
*params
*set dollar_pound 0
*set dollar_pound round(param_1 * 1.33)
*return

On this way it worked, which is fine, I have no problem it being in the same file, however how do I make a variable receive a value that is returning from the *gosub_scene? I managed to make it display just fine, I just can’t set it.


#6

I assume these codes belong to the “parent” scene, am I right?

You just set it up wrong. That’s why you get the unrecognizable var error.

The wrong
  1. IIRC, any *command should be on their own line.
  2. I don’t recall *gosub works even thought it’s placed on a same line with any code.
  3. I see that in your subroutine, you’re lacking *set <<var>> round... command on the rounding operator.

The rule is basically this :point_down:t4:

  1. In your “parent” scene
*set food 0       <--- (this is a global variable)
*gosub_scene <<the subroutine.txt>> <<the label>> 1

The food is @{(food+1) nothing|cookies}.
  1. Now, in the subroutine scene “the subroutine.txt”
*label <<the label>>
*params type
*set food type
*return

If you want this converter subroutine available to not just a variable (which in my example is food), you can use “variable referring” with the {referred var} tag.


#7

Ah, I did not thought of setting dollar_pound this as a global variable (it was temp), this should work.

The variable was dollar_pound actually. But now that it is global it should work. I was trying to set the temp variable as receiving the value that was calculated by the gosub_scene, as it is possible to do so in Java, did not know it was more restricted in choicescript.

In Java whatever method you call that has a return will be the return value when you called it, like replacing that big string of the gosub_scene there with just a number, but here it is different.


#8

I’m finally getting around to trying params, and quickest isn’t happy with it unless I define param_1, param_2, etc. in startup. Is that normal? Just surprised me and took a minute to figure out why I was getting an error…

EDIT: more unexpected errors…

If I try to rename parameters (i.e. *params messagenumber), Quicktest fails with the following error:
TypeError: Cannot read property ‘length’ of undefined

*if (param_1 = 1) is read as true even when I’ve defined it as 2

SECOND EDIT:
I was forgetting I needed the *params command even if I didn’t want to rename the variables, it is running properly now. However, quicktest still calls up an error whenever the command *params is included in the project. Is this just something else we need to remember to use *if choice_quicktest to work around?


#9

*gosub visit “Dracula” “garlic”

*label visit
*params
You go to visit {param_1} and you bring {param_2} with you.
*return

Quicktest passes on this for me on a local copy of CSIDE, I’m not sure if this is the very latest version of CS though.


#10

I’m working with a *gosub_scene; maybe the problem’s there?

Here's what I've got

A very abridged version of what I’d actually like to do, trying to find the source of the problem…
Startup:

*create contacted 0
*label top
Params do not seem to be working...
*choice 
	#Check for a new message
		*gosub ghostmakescontact
		*goto top


*label ghostmakescontact
*if (contacted > 0)
	*goto areyoulookingforsomething

*comment prints the first message
*gosub_scene ghost printamessage 1
*set contacted + 1
*return

*comment prints a second message
*label areyoulookingforsomething
*gosub_scene ghost printamessage 2
*return

subroutine file

*label printamessage
*params 
*temp thingtodisplay "default message should never be seen"
*if (param_1 = 1)
	*set thingtodisplay "This message displays the first time."
*if (param_1 = 2)
	*set thingtodisplay "This message displays the second time"
${thingtodisplay}
*return


#11

I was talking crap, sorry. I actually get a ‘couldn’t find scene ghost.txt’ error with your example, if I use *gosub_scene (*goto_scene works fine). And then a “TypeError: Cannot read property ‘length’ of undefined”, regardless of if *params is left empty or not. This is only in QuickTest, the game itself runs fine.

Are you on a fresh copy of CS?


#12

Oh yeah, I had the ‘couldn’t find scene’ error, too, but listing ghost in startup fixed it. I’m using CSIDE 1.1.2 (development version, I believe – I’m on a mac and had the deletion bug)


#13

All scenes have to be listed in startup.txt for compiling to work properly anyway, right? I seem to remember it being required when the game goes to CoG/HG…


#14

Update on the quicktest problem:
It turns out, apparently, that telling quicktest to navigate around the *params command still results in an error:

*if not (choice_quicktest)
    *params

is not valid.
However, it seems the problem is only with params used with *gosub_scene, as @CJW’s example above within a scene worked fine. So, I tried putting a completely useless subroutine in the malfunctioning scene around the code including the *params command, and now it works… :face_with_raised_eyebrow:

*label printamessage
*if (choice_quicktest)
	*gosub sillyquicktest 1
*label sillyquicktest
*params 
*temp thingtodisplay "default message should never be seen"
*if (param_1 = 1)
	*set thingtodisplay "This message displays the first time."
*if (param_1 = 2)
	*set thingtodisplay "This message displays the second time"
${thingtodisplay}
*return
*return


#15

I recommend you always add *params after the label, so that they are loaded already, even if you do not use them (this won’t result in an error, the params will just be loaded but not used)

I was having problems with CSIDE trying to use *params, but it was because I was using an old version. After updating to latest or dev version it worked fine in quicktest.

My other *params attempts worked succesfully, either with *gosub or *gosub_scene.


#16

Ow?
I got an error when you add *params on a *label if you don’t give any value for those parameters.

Here’s my sample, on my startup.txt

*label skrinrid
*params as df gh
@{repeat So, d|D}o you wish to activate screenreader mode?
@{repeat [n/][[i]Cannot be changed until game over or restart[/i]]|}
*choice
	#Option 1
		saef
	#Option 2
		dsfa

But then, why would you put *params if you’re not going to use it? ¯\_(ツ)_/¯


#17

Well I didn’t try renaming the *params though. In one instance I had 2 calculations in this *gosub, and one requires 3 params, another requires just 2. I was able to use either just fine, giving the required number of *params depending on which I wanted to use.

*label misc_calcs_index
*params
*gotoref param_1

*label dollar_to_pound_calc
*set dollar_pound 0
*set dollar_pound round(param_2 * 1.33)
*return

*label accCalculate
*set finalAcc round(param_2 + ((2*param_3)/1.5))
*return

So I use param_1 to select what calculation I need. If its the dollar_pound I just need param_2, for the other one I need param_2 and param_3. So when I use this *gosub I just need to include either 2 or 3 params when I require it. So far it all worked fine in quicktest.


#18

Ooh, I see. You had the value ready to be assigned to those parameters.

I thought what you meant with

is to literally add *params to every *label you can find, which surely tingles my rage nerve :"


#19

I think I found a minor bug with *params and *gosub_scene

This works:

*gosub stat- "silly" 2 "${var} blabla" 2

But this doesn’t:

*gosub_scene code stat- "silly" 2 "${var} blabla" 2

The error is: Invalid expression at char 10, expected NUMBER, STRING, VAR or PARENTHETICAL, was: OPERATOR [-]

So obviously that’s from the label name “stat-”. Removing the minus sign from the label makes it work, but apparently including a ${var} or [b][/b] in one of the parameters breaks it, because this works:

*gosub_scene code stat- "silly" 2 "blabla" 2

I can rename the labels, I just prefer to use + or -.

But since it works with normal *gosub I don’t see why it can’t work with *gosub_scene.


#20

Operators in label names is not supported. The “bug” is that it works with *gosub in the first place.