Loose, Tight, Flat, and Bumpy Stats in ChoiceScript Games

coglogo2Previously, I wrote about setting, checking, and gating content in stat-heavy choice-based games such as ChoiceScript content.

In that post, I didn’t get into the question of stat distribution: over the course of the game, how possible is it to reach particular stat distributions? Can you reach the top and bottom of each personality stat (raise it to 95+, or lower it to 5-)? Can you do this easily/immediately, or only with persistent (or even perfect) choice selection? If you run a bunch of random plays and look at what levels the random player can reach for a given stat, is the distribution reasonably bell-curve-like, or is it more flat or multi-modal?

If it is easy to reach the ends of the stat spectrum, I’ll refer to this as having a loose stat system: plenty of buffs or nerfs are floating around the system and it’s easy to get to one end or another. A player doesn’t necessarily have to play with purpose in order to achieve a particular character build. The problem here is that the resulting experience may feel fairly low-agency, because there are so many ways to get a particular stat profile that it seems like the player’s particular choice blend doesn’t matter all that much.

The extreme case of a loose stat spectrum is on where being at the ends of the spectrum isn’t even that unusual: by the end of the game, the distribution looks nearly flat, with little or no bump at the middle. This is a very unresponsive stat system, and it’s not going to reward the player at all for playing one personality stat consistently.

The opposite is a tight stat system: buffs and nerfs occur less frequently or in smaller increments, and it takes time to reach very high or very low levels. The problem here is that the game may feel quite difficult, especially if there are special endings or consequences associated with the extreme ends of the spectrum; it may be that most players never manage to reach ideal outcomes without relying on wiki guides to help them map their choices. This is especially frustrating if there is one “main” outcome that is boring/losing and all the special/winning endings require near-perfect execution.

A system where the stat distribution has a couple of distinct bumps in it may reflect a situation where there are just a handful of really big ways to buff/nerf that stat. This is not necessarily as problematic as a flat stat spectrum, but it may mean either that there are so far not very many choices in the system overall, or that the stat system is being asked to handle a function for which it’s not best suited. In my experience, if you’re giving a 50% or greater stat bump/nerf for a single choice, you may have a situation where it would be more to the point to track that choice with a flag, because it’s a very significant narrative event and reducing it to numbers isn’t going to be enough to let you really reflect the consequences back to the player. (Sometimes you might want to track with numbers as well, though.)

Related to this is the question of situational difficulty. For each choice we encounter, what is the distribution of stats for people facing that particular choice?

This matters because we might overall think that a choice qualifies as “difficult” if it requires a stat of 80+ out of a possible 100 (for instance). But if you can only get to that choice by previously passing a gate requiring 75+/100, then the choice may not be all that difficult for most of the players who get there. (Depending, again, on how flat your stat distribution is.)

From a design perspective, I know that on my current Choice of Games project, I want my stats to be fairly tight: enough to feel responsive and provide a real sense of accomplishment for reaching the extremes of the scale, but not so tight that a player has to play perfectly to unlock a given outcome. I also want there to be some unique narrative outcomes that will be accessible only to (say) 10-15% of all characters encountering a particular choice.

So clearly there are a couple of related issues here: how often are we giving away buffs/nerfs (and how much are we giving, when we do so); and how do our checks relate to where the players are on the spectrum?

For any reasonably sophisticated stats-based system, this quickly becomes much too complicated for the designer to just calculate on their own, so we need a way to see into the data. We also want reasonably neat ways of altering the code for tuning purposes, so that we can build something and then quickly rerun the visualization to see whether we’ve met those tuning goals yet.

Here’s the approach I’ve taken with my current Choice of Games work in progress:

Represent amounts of stat change with standard change variables rather than numbers. Thus *set some_stat %+small_quirk_change rather than *set some_stat %+10. If I’m getting a distribution that’s too flat, I can lower these quantities all at once; meanwhile, the code remains readable; it always lets me see what the intent was when I added stat changes.

Also represent thresholds with standard variables. I use stat_lo_pass, stat_mid_pass, and stat_hi_pass to represent threshold values, and then I know when I write *if stat > stat_hi_pass, I’m looking for a player who has done the effort to be towards the top end of that particular stat.

At the beginning of each chapter, and possibly in major subsections of the plot, reset the stat thresholds. It may be that hardly anyone is at stat > 70 in chapter 1, but that loads of people have reached stat > 90 by chapter 10, so we need to move the goalposts in order to achieve the kind of distribution we want.

In the same place, include some comment code that will show their distribution when that threshold is tested in ChoiceScript’s random testing, like so:

*set stat_lo_pass 25
*set stat_mid_pass 50
*set stat_hi_pass 75

*if (stat <= stat_lo_pass)
*comment LOW STAT
*if (stat > stat_lo_pass) and (stat < stat_mid_pass)
*comment MID-LOW STAT
*if (stat > stat_mid_pass) and (stat < stat_hi_pass)
*comment MID-HI STAT
*if (stat > stat_hi_pass)
*comment HI STAT


When we run randomtest, we get counts on how many times out of 10000 each of these lines are reached, giving results like

chapter_2 10000: *set stat_lo_pass 25
chapter_2 10000: *set stat_mid_pass 50
chapter_2 10000: *set stat_hi_pass 75
chapter_2 10000:
chapter_2 10000: *if (stat <= stat_lo_pass)
chapter_2 1406: *comment LOW stat
chapter_2 10000: *if (stat > stat_lo_pass) and (stat < stat_mid_pass)
chapter_2 3653: *comment MID-LOW stat
chapter_2 10000: *if (stat > stat_mid_pass) and (stat < stat_hi_pass)
chapter_2 3454: *comment MID-HI stat
chapter_2 10000: *if (stat > stat_hi_pass)
chapter_2 1263: *comment HI stat

This is the rough distribution I want — roughly 50/50 above and below mid_pass; around 15% in each of the high and low regions. If I’m not getting that distribution, I can move the thresholds.


Josh Sawyer on tuning attributes in Pillars of Eternity (free video GDC Vault content, coming to the problem from RPG perspectives). One of my favorite observations here:

A huge part of balance is actually player perception… if people don’t care about dumping it, it’s not that important.

In other words, even if your stat matters to the system, if the player doesn’t understand how it matters or appreciate the advantage it gives, it is still lacking.

He talks a fair amount about how people are bad at understanding percentages, and that makes it difficult for them to perceive the value of stat changes (which is why I love the illustration of chance and outcome in Orion Trail).

3 thoughts on “Loose, Tight, Flat, and Bumpy Stats in ChoiceScript Games”

  1. I’ve been taught to shun “magic numbers” when coding, but for some reason it never hit me that this applies to stat tests in interactive fiction. Thanks for the ‘aha’ moment!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: