A modest proposal for the ChoiceScript language

I’m going to echo @trevers17 in asking how frequently you’re encountering these bugs?

My feeling was that by the time someone’s using constants and enums they’re advanced enough to write a subroutine that checks if a variable is between two numbers.

In regards to using subroutines to debug, I was imagining someone would use them every chapter or so, or when they want to double check everything is set correctly. And I don’t know why someone would leave them in the final project.

Overall, I’m just skeptical that these changes would catch bugs not immediately caught by:

  • Rereading your code
  • Frequent playtesting
  • Strategically placed *bug statements
  • Quick Test and Random Test
1 Like

Now I’m on pc, I can elaborate more on my previous comment.


Let’s say the hypothetical command is *constant (and *constant_temp for temp variation).

If constant by its definition a “locked variable” (well, it’s a constant), I won’t be able to do this

You gain the blessing of the divines. Your tasks should be easier, ahead .
*set constant -bonus

And should do this instead every time

*if skill > (constant -bonus)

This is what I’d call adding complexity (and limiting design space, to an extent). Sure, there’re workarounds, but once an author becomes skilled in CS, I think they’ll opt to leave *constant and prefer the simpler *create (compare: *line_break vs. [n/]).

In Octave, global command (equivalent of *constant) has its use to store universal value such as gravity-constant or soil’s angle of friction. This global can be modified inside of CS-equivalent the *subroutine, but only inside the scope of the subroutine. The constant will be “returned” to its global value once the program leaves the subroutine.


As for enums, either: 1) I’d rather have an actual, functional *array command; throwing another hypothetical term. Var[index] exists, but you have to *create the “child variables” first before putting them into the var[index]. Prone to mistakes, 2) Additional datatypes for floating-point number and non-negative integer. This can be something like *positive which throws error report when it goes below 0, or *decimal that stores X number behind zero (X being further modifiable?).

I do agree with your point 3, though. Fairmath is niche enough that it can use clearer separation from normal numbers (fairmath datatype). Heck, do *fairmath instead and forgo all that %+ %-.


Closure:
Despite being a “readable by non-coder,” CS is a programming language. Adding limitation to it would make it even more niche among other languages, and I feel CS already cuts out (haven’t implemented?) many elements that would add more freedom otherwise: in-line images or icons, table (or just column, for layout sake), checkbox and drop-down list (generally more GUI; we only have radio button for now), etc.

Granted, this would add complexity, but the utility of the language would dramatically increase too.

As it stands now, CS is perfectly fine for writers who aren’t too concerned with gameplay elements (you really won’t do *set FEMALE (MALE * MALE) - FEMALE anyway), but those who want to dabble more in gameplay elements will have to learn more about CS. *constant and the enums won’t add anything for these people.

Harmless commentary

I like your implementation

*create male 1
*create female 2
etc.

Looks cleaner and easier in the eyes. I always used *create gender 0 and set them to 1/2/3: male/female/nb. Very toiling when I have to @{gender him/her/them} everywhere.

2 Likes

If these are made optional, then I’m all for it. Basically, whoever wants to continue using Choicescript as it is can do so. Then, those of us who are making code-heavy games can implement them. I know for sure that options 1 and 3 would greatly help me. My latest game had 330,000 words, maybe around 25% of it code. I’ve seen Chris’ code (he is also very code heavy, and a more elegant and better programmer than me), and I can see why this would help him, and many others.

Obviously, somebody who is super-careful would not need 1 and 3… but most of us make mistakes at some point, and these can be difficult to find in long games (even with many proofreads, people playing and replaying, I still had many bugs regarding fairmath that I couldn’t locate). So, I would definitively be using 1 and 3… and the hangover today isn’t helping…)

2 Likes

I feel like constants and enums should be basic components of a language, not esoteric wizardry used by only a select few, because they make code easier to read and much easier to maintain and debug. They’re things everyone should use from the beginning.

Besides, people do use these patterns all the time, anyway, even though they don’t think of them explicitly in these terms. They write *if skill_a > 15 and *if skill_b > 16 and *if skill_c > 14 and *set char_a_relationship %+ 25, tossing out individual constants all over the place, hundreds or thousands every scene (of which there are probably only one or two dozen unique values), making for a huge mess to adjust if testing suggests any of the gameplay balance needs tweaking.

And they write

*choice
  #Bob is my trusty partner.
    *set partner "Bob"
  #Sam is always by my side.
    *set partner "Sam"
  #J.D. has been my partner through thick and thin.
    *set partner "J.D."

which is not typesafe and hence prone to typo or misremembrance errors. But they use this pseudo-enum pattern anyway because using a half-dozen boolean values instead, only one of which can be true at a time, is much more unwieldy, both to use and to ensure correct. Also because as a string its value can be printed outright; the only other reasonable alternative would be to use the integer-style pseudo-enum and a multireplace to print the name.

So, people are already using these patterns, because they make things much easier for the author, despite the risk. I’m just proposing to make them safe to use.

If you only checked every chapter, that wouldn’t catch any *temp level errors. Global pernicious bugs would be prevented, but not local ones.

I guess you could comment out all these tests while you’re not testing; you wouldn’t want to delete them because, well, you can (or at least should) never stop testing. Even after release, if you want to add a scene or change one thing to fix a bug or correct an oversight, you’d still want to run the test suite to make sure you haven’t inadvertently broken something else.

And besides, that’s still a ton of work you as an author need to plan and build and keep up-to-date to ensure your code doesn’t break, when that weight could be handled more simply and elegantly and efficiently by CS if it knew these patterns existed and what valid values for them were.

No, that’s exactly what I’m intending to use. Maybe I’m not explaining my goal properly. This wouldn’t detect any bugs that aren’t found without playtesting and Quicktest and Randomtest, because those are precisely the methods which find CS errors. My proposals would only add more error messages in more situations for playtesting and QT/RT to uncover.

Say you’re using a string-based pseudo-enum. A game using it can be in one of three states at any given moment:

  1. Performing correctly, exactly as intended.
  2. Unintentionally in an invalid state, but CS doesn’t know that and doesn’t throw an error. Depending on the future lines of code, it may never throw an explicit error based on this, making for an even more pernicious bug.
  3. Erroneous in such a way that CS (either in playtesting, QT, or RT) throws an error and quits. You misspelled a variable name or something.

My proposals aim to eliminate situation #2 entirely, turning all of those delayed timebomb cases (with potentially indefinite fuses) into #3. Thus, you would immediately hit the problem in testing, with a message pointing to the correct line of code, so you’d know precisely what you need to fix and where.

Yes, I ran into this sort of thing several times in my last big (released) project, and several times since then. I know CoG editors like Mary and Jason are very familiar with this issue, because they were talking about it at Narrascope and how hard to track down things like fairmath out-of-bound errors are thanks to the error message being generated long after the actual *set statement that was the culprit.

4 Likes

Yes, that is true, I remember also talking with Mary and Jason about this at Narrascope. So, it must be quite a significant issue… I also encounter this all the time, as I said (and drives me crazy). So, I would welcome the possibility of having them. But, I can also understand that it creates another barrier to entry for those who are not familiar with programming. Hence, I feel it would be better to make it optional… i.e. these types of variables exist, but you are free to continue to use them also in the way they are now.

What starts off optional can drift into being mandatory – in the name of coding efficiency, let alone the even more compelling cause of bug-hunting.

I’m still attracted to the idea that the language’s target user group includes authors with very limited programming nous, for whom “Advanced ChoiceScript” genuinely does include things like *line_break, *input_number, and nested conditionals. (That page doesn’t mention any features added in recent years, including multireplace or pseudo-arrays like “${them[sam_sex]}”, even in the for-programmers-only “truly bizarre” section toward the bottom of the page…)

That said, I think Chris’s most recent post does a good job of highlighting the benefits of these changes. As a non-programmer, I’ve no idea of the cost that would be involved in Dan’s time…but I’d support it if the c/b worked out favorably. As long as it remained optional rather than mandatory.

4 Likes

Well, sure, but this is a separate thing. If a value is going to change in the course of a story, it should be a global variable, not a constant. If a value is going to change in the course of a chapter, it should be a *temp, not a *temp_constant.

That’s a kind of weird idea, to be honest, though. Reducing the difficulty of all skill checks, or a certain type of skill check, is kind of the narrative-game equivalent of changing the gravitational constant of the universe or the coefficient of friction on a particular surface. It would probably be better represented by just giving the player a bonus to a certain skill, or class of skills, or all skills (for a global bonus). I know it’s technically not the same thing, but mathematically it works out the same; the difference is really only philosophical.

My feeling has been just the opposite. The more I’ve coded, the more I’ve wanted dedicated constants instead of using

  *if skill > 15
  *if skill > 16
  *if skill > 14
  *set relationship %+ 25
  *set relationship %+ 15

all over the place, and also the more I’ve used enums and wished they were typesafe.

I don’t think of variables as a “simpler” form of constants at all; they’re separate tools for separate needs. The only thing they have in common is the author names each.

Yeah, I looked that up about Octave after you mentioned it. That’s an interesting idea for subroutines, but I’m not sure it would be a good idea to implement that way for ChoiceScript…

I posit that the time for Dan to implement would be easily made up for in the time saved by other team members. The easiest to implement (I think! I’ve never written a full language parser like this) should be constants, then bounded types like fairmath.

My true concern is just the documentation, really. I suppose adding more optional features is fine, but it’d feel like a waste if everyone who uses CS doesn’t know (or even heard) about these features (how did I learn about [n/]?). I know @RETowers is working on an official(?) one, but I have no news whether we have the updated documentation or not, yet.

1 Like

IMO, this conversation would benefit from more clearly listing out the bugs that we’d want to eliminate, and then considering in more detail what it would be like to fix them.

Here are bugs that came up in the context of this thread:

  1. Accidentally forgetting to use FairMath. Using *set strength +20 or *set strength 20 instead of *set strength %+ 20 is easy to do and hard to notice.

    Today, this would only cause a QT/RT failure if this bug increases strength above 100; even then, it won’t tell you about the problem right away. The failure will only come up when you use FairMath on strength in the future, when it turns out that strength is greater than 100. Worst of all, it won’t tell you where strength increased over 100; it will just tell you that it happened at some point previously.

    Ideally, QT would fail on *set strength +20 because QT would know that strength is supposed to be a FairMath variable.

  2. Setting variables to invalid values. If you have a gender variable that you want to be "female", "male", or "nonbinary", it’s easy to accidentally write *set gender "femlae".

    QT/RT don’t really help with this much. RT can somewhat help you catch this if you review the RT output carefully and notice that *if gender = "female" never happens in tens of thousands of runs, but it’s always tricky to figure out why something didn’t happen.

    A good practice for this is not to use text for variables that should have a restricted range, but to use numbers instead, and to define some variables matching those numbers, like this:

    *create female 1
    *create male 2
    *create nonbinary 3
    
    *set gender female
    

    That way, if you accidentally write *set gender nonbnary, QT will instantly catch the problem. (It’s also more convenient to use numeric variables in multireplace.)

    But even if you intend to use numbers for gender, it would be easy to accidentally use *set gender "male", which has all of the same bugs, but no typos. It doesn’t even look like a bug when you read it out of context; you have to remember that it’s supposed to be *set gender male instead.

  3. Invalid values in *if statements. This is the other side of the coin of the previous point. *if gender = "femlae" is a bug that QT won’t catch; RT will only indicate it by pointing out unreachable lines. It’s even harder to notice when you write *if gender = "nonbinary" when you’re supposed to use a number *if gender = nonbinary.

I think these bugs absolutely can/do happen quite a bit, and that existing tools don’t help much in preventing them.

All of these problems are tricky for QT to catch because QT doesn’t know enough about what variables are allowed to do. How is QT supposed to know that *set gold +20 is allowed, but *set strength +20 is not? How would the author tell QT what the rules are?

Are these bugs serious enough that authors should jump through some hoops to prevent them? How, exactly, would we want to prevent them, if at all?

4 Likes

There’s at least one more bug I noted above:

  1. Accidentally transposing two variables in a *set statement. Take your numeric fake-enum approach. If at one point deep in your code you accidentally write *set male bob_sex instead of the converse, QT and RT will see no problem. Only when you notice some of your pronouns start going wrong, inconsistently and maybe only after a certain part of the story—or, worse, only in some playthroughs, but not in others—while playtesting will you have any idea that there’s a problem, and then you have no pointer from QT showing where to start finding the bug.

I’m not entirely sure about this. Some authors intentionally use normal math on fairmath stats; @Cataphrak for example uses + and - on fairmath stats early on in Sabres, then switches over to purely %+ and %- in a later chapter once the modifiers have piled up to the point they might approach the 0 or 100 bounds.

But maybe that should be discouraged?

2 Likes

This might cause me some problems, as @Chris_Conley’s mentioned. I tend to use “normal” math on certain “fairmath” variables when the situation calls for it (like in character generation), and I’d prefer not to be hit with a QT fail each time it happens.

Maybe some kind of alert instead?

3 Likes

So, of the four bugs listed there, which of them causes y’all the most trouble?

Other typo-related bugs I can think of:

  1. Forgetting a $ or { in ${}, e.g. Hello {name}
  2. Forgetting a * before commands, e.g. set gender female instead of *set gender female
  3. Capitalization mismatches, e.g. *set gender "Female" *if gender = "female"
  4. Messing up [b] or [i], e.g. This is supposed to be [b]bold/b]
3 Likes

I may be misreading #1 but if I create a variable and set it to true if I don’t alter the stat with the *set command wouldn’t it be constant? Likewise even if it only set once at the start of a game the value would still be constant.

I’m not a big coder so I may have just misread it.

1 Like

I screw up with all of the above, although CSIDE helps catch 5 and 6 as you type because of the color coding it uses.

2 Likes

I’m with Eric… Though the aftermath problems (where variable goes above 100 or below 0 and you can then not do aftermath) are the ones that drive me crazy…

Stricter type safety would actually interfere with the way I code my game. I have functions that rely on being able to parse strings into numbers implicitly, so that I can decrement numeric values within subroutines.

Here’s a much in demand feature for ChoiceScript language that I would appreciate being introduced first: The ability to delete saved game files. I have too many saved game files stinking up certain series.

1 Like

A ‘proper’ constant is immutable once declared. The idea is that it’s then impossible to mix up constants and variables because the compiler would flag an attempt to change the value of a constant as an error. Some languages have them, some don’t. It’s a useful way to cut down on certain hard to find errors but not an absolute necessity.

While CS seems limited in some respects, it’s really quite flexible in the way you can so easily just mix numerics and strings. I, too, tend to create stings of numeric values to cut down on the need for so many variables. Whether that’s a good idea or not, I have no idea. It’s just the way I like to do things…

2 Likes

What values are you storing in these string variables? Or could you post one of those subroutines? That sounds interesting.

1 Like

Relationship values. I have scenes that can take place with one of two NPCs, and in order to dock rel values, I need to pass negative value integers as strings and let JS do the parsing for me.

1 Like