What would you like to learn next?

Game-State Formulas

Chapter 4 by Friedman Friedman

Game-State Formulas

Game-State formulas calculate a Number, Percent, or Age from current game values, arithmetic, conditions, existence checks, error fallbacks, rounding, and random results. Use Set by formula when the calculation should happen as the reader enters a chapter. A reveal, code lock, ordering puzzle, or action button can instead run a formula when the reader uses that element. Every formula calculation needs an active game.

Build your first formula

Suppose a shop should subtract an item's discounted price from the reader's money. First create three Number game variables as described in Game State Variables and Inventory, with these initial values:

  • money = 100
  • item_price = 30
  • discount = 5

Then edit the shop chapter:

  1. Turn on Changes to game state.
  2. Choose the money variable.
  3. Choose Set by formula.
  4. Enter game:money - (game:item_price - game:discount).
  5. Save the chapter and test it in a fresh game.

The formula calculates 100 - (30 - 5), so the reader enters the chapter with money set to 75. A formula may read the same value it changes, as this one does, but that value must already exist in the current game.

Values a formula can use

Write game:name to read a game variable. The name and capitalization must exactly match Story variables.

  • Number, Percent, and Age values can be used as numbers.
  • Yes/No values can be used with true, false, comparisons, AND, OR, NOT, and IF.
  • EXISTS(game:name) can check any Game State type, including Text and Dropdown, without using its value in the calculation. It produces a true-or-false check for IF, AND, or OR.
  • Text and Dropdown values cannot be calculated with or returned by a formula, and reader variables cannot be referenced at all.
  • The completed formula must return a number, even when it uses a Yes/No check.
  • Hidden Game State values remain available to formulas; a hidden value is different from a missing or removed value.

Every referenced name must be a known Game State variable when the chapter is saved. During play, every reference whose value the formula actually calculates must exist in the reader's current game. Define important inputs with starting values so a fresh game has them. A variable added to the story after a reader began playing can still be missing from that existing game until the reader resets it or a chapter creates the value. If an earlier chapter can remove a value, recreate it before the formula or use EXISTS with IF to supply a fallback.

EXISTS accepts one direct, declared Game State variable, such as EXISTS(game:bonus). It returns false when that value is missing from the current game or has been removed. It returns true when the value exists, including Number 0, Yes/No No, empty Text or Dropdown, and hidden values. It does not accept a reader variable, fixed value, calculation, comparison, or another function.

For example, IF(EXISTS(game:codename); 1; 0) checks whether the Text value codename exists but still returns a number: 1 when it exists and 0 when it is missing.

Useful formula patterns

These examples use common game-story tasks. Replace their variable names with the exact names from your story:

Task Formula What it does
Keep health in range CLAMP(game:health + game:healing; 0; 100) Adds healing, but never returns less than 0 or more than 100
Give a conditional bonus IF(game:has_bonus = true; game:score + 10; game:score) Adds 10 only when has_bonus is Yes
Supply a missing default IF(EXISTS(game:bonus); game:bonus; 10) Keeps the current bonus when it exists and otherwise returns 10
Divide safely IF(game:item_count = 0; 0; ROUND(game:total / game:item_count)) Returns 0 for an empty group and otherwise returns a rounded average
Recover from a calculation error ROUND(IFERROR(game:total / game:item_count; 0)) Returns 0 if the division cannot be calculated and otherwise returns a rounded average
Make a random skill check IF(DICE(20) + game:bargain_skill >= 20; 5; 0) Returns 5 when the die result plus skill reaches 20, otherwise 0

To give bonus a default of 10 without replacing an existing value, choose bonus as the Set by formula target and enter IF(EXISTS(game:bonus); game:bonus; 10). IF calculates only the selected result, so a missing bonus is not read after EXISTS returns false. In the safe-division example, the division is not attempted when item_count is 0. AND and OR also stop once the result is known, so a skipped right side is not calculated.

Run a formula from an interactive element

Place the formula after a numeric Game State assignment when the reader's action should trigger the calculation. For example:

{action "Gain income" set game:wealth += game:income}

Selecting Gain income calculates the current income and adds that result to wealth. The three assignment forms are:

  • game:target = formula replaces the target with the formula result;
  • game:target += formula adds the formula result to the current target;
  • game:target -= formula subtracts the formula result from the current target.

The target must be a Number, Percent, or Age. The formula may use the complete syntax and every function described below, including IF, IFERROR, EXISTS, MIN, MAX, DICE, and RANDOM. Fixed assignments to Yes/No, Text, and Dropdown variables still use their ordinary values, such as game:has_key = true or game:route = "forest"; formulas cannot target those types.

An action may contain several comma-separated changes. They run from left to right, so a later formula sees the results of earlier changes in that same action. A reveal has one change. A code lock or ordering puzzle has at most one success change and one failure change, and only the change for the reader's result is calculated.

Changes run from top to bottom

A formula sees the results of changes before it. In Changes to game state, suppose one row sets bonus to 5 and the next formula adds game:bonus to score; that formula sees 5. Use Move Up and Move Down to put dependent rows in the required order. Inside an action button, comma-separated changes run from left to right.

After each row, CHYOA keeps the result within the target type's range: Number -100000 through 100000, Percent 0 through 100, and Age 18 through 150. The next row sees that adjusted value.

CHYOA checks formula syntax, variable names, and compatible types when the chapter is saved. Some problems depend on the reader's current game, such as a missing value, division by zero, a fractional final result, or calculated random bounds in the wrong order. If any formula cannot be completed when the reader enters the chapter, the reader sees a Game State calculation error, none of that chapter's change rows are kept, and the previous Game State remains intact. If the failed calculation is on the first chapter, CHYOA leaves Game Mode for that attempt so the reader is not trapped there.

If an interactive formula cannot be completed, none of that interaction's changes are saved and the interaction is not marked Used, Unlocked, Failed, or Solved. The reader can attempt it again. Random choices from the failed calculation are discarded with the rest of the changes.

Arithmetic and checks

Syntax Meaning
+, -, * Add, subtract, and multiply
/ Divide exactly; round the result when it may be fractional
// Divide and round down to a whole number
% Find the remainder using the same round-down division; MOD(value; divisor) does the same calculation
^ Raise the left value to a whole-number power; POW(value; power) does the same calculation
( ) Group a calculation and make its order clear
=, ==, !=, <>, <, <=, >, >= Compare two numbers, or compare two Yes/No values with equality
AND, OR, NOT Combine or reverse true-or-false checks

The usual arithmetic order applies: powers, multiplication and division, then addition and subtraction. Powers group from right to left, so 2 ^ 3 ^ 2 means 2 ^ (3 ^ 2). Use parentheses whenever the intended order might not be obvious.

POW(game:level; 2) is the same calculation as game:level ^ 2. MOD(game:coins; 5) is the same calculation as game:coins % 5. Use the function or operator form that makes the formula easiest to read.

Write formula logic with the words AND, OR, and NOT. The &&, ||, and ! aliases accepted by some chapter conditions are not formula syntax.

Decimal values may use a point or comma, so 1.5 and 1,5 mean the same exact value. Function arguments are always separated with semicolons, not commas.

Formula functions

Function names are not case-sensitive.

Function Result
IF(check; when_true; when_false) Uses one result when the check passes and the other when it does not; only the selected result is calculated
IFERROR(value; alternative) Uses alternative when calculating value causes a recoverable error during play; only then is alternative calculated
EXISTS(game:name) true when the direct, declared Game State variable exists in the current game; false when it is missing or removed
MIN(value; ...) The smallest of one or more numeric values
MAX(value; ...) The largest of one or more numeric values
ABS(value) The value without a negative sign
POW(value; power) Raises value to a whole-number power; this is the function form of value ^ power
MOD(value; divisor) Finds the remainder using round-down division; this is the function form of value % divisor
ROUND(value) The nearest whole number; halfway values round away from zero
ROUND(value; places) The value rounded to the requested decimal place, from -12 through 12
FLOOR(value) The next whole number at or below the value
CEIL(value) The next whole number at or above the value; CEILING also works
CLAMP(value; minimum; maximum) The value kept between the supplied minimum and maximum
DICE(sides) A whole number from 1 through sides, including both ends
RANDOM(minimum; maximum) A whole number between the two bounds, including both ends

Recover from calculation errors

Use IFERROR when a formula is valid when the chapter is saved but a reader's current game might make part of its calculation fail. For example:

  • IFERROR(game:bonus; 10) returns bonus when that declared value exists in the current game and returns 10 when it is missing or has been removed.
  • IFERROR(MOD(game:turns; game:cycle); 0) returns the remainder when cycle is not 0 and returns 0 when the modulo calculation would divide by zero.
  • ROUND(IFERROR(game:total / game:item_count; 0)) returns a rounded average when the division succeeds and returns 0 when it cannot be calculated.

IFERROR calculates its first value before deciding whether the alternative is needed. It calculates the alternative only after a recoverable calculation error. The value and alternative must have the same type: either both numeric values or both true-or-false checks.

The function does not make invalid formula syntax, an unknown story variable, incompatible types, an invalid number of arguments, or a value outside a formula safety limit valid. Those problems must be corrected before the chapter can be saved or the formula can succeed.

The completed formula must still return a whole number. For example, IFERROR(1 / 2; 0) does not return 0: 1 / 2 is a valid exact calculation, so IFERROR returns one-half, and the completed formula then fails the whole-number requirement. Put the appropriate rounding function around IFERROR, as in the average example above, when a successful calculation may be fractional.

Whole-number results

The completed formula must produce a whole number. game:total / 4 works only when that division has no remainder. When a fraction is possible, choose the behavior the story needs:

  • ROUND(game:total / 4) uses the nearest whole number.
  • FLOOR(game:total / 4) rounds down.
  • CEIL(game:total / 4) rounds up.

Rounding is needed only before the completed result is saved. A formula may use exact fractions in the middle of a longer calculation.

Random results inside formulas

Use a random function when the result also depends on Game State or needs custom bounds:

  • DICE(20) chooses a whole number from 1 through 20, including both ends.
  • RANDOM(-2; 2) chooses -2, -1, 0, 1, or 2.
  • game:score + DICE(6) adds a six-sided result to the current score.
  • RANDOM(game:minimum; game:maximum) uses current numeric game values as inclusive bounds.

Each call makes a separate choice. DICE(6) + DICE(6) therefore produces 2 through 12, with results near 7 occurring more often.

An interactive formula makes its random choices when the reader uses the element. One successful interaction calculates each call once and saves the resulting change. Reloading a completed reveal, once-only action, or final lock or ordering-puzzle result does not choose another number. A new use of a repeatable action or retryable failure is a new interaction and can choose a new result.

DICE needs a whole-number side count from 1 through 1000000. RANDOM needs whole-number bounds from -1000000 through 1000000, and its minimum cannot be greater than its maximum.

If no calculation is needed and a fixed upper bound is enough, use Random Values in Chapter Changes. For a reader-controlled roll button, use Dice Rolls. For a themed choice from written outcomes, use Random Draws.

Fix common formula problems

Problem What to check
Unknown variable while saving Match the name and capitalization shown on Story variables
Missing value while playing Use EXISTS with IF or use IFERROR to supply a fallback, or create the value on an earlier route
Formula returns a fraction Use ROUND, FLOOR, or CEIL for the completed result
Division or modulo by zero Use IF to avoid the calculation when the divisor is 0, or use IFERROR to supply an alternative
Function arguments are rejected Separate them with semicolons, such as MIN(2; 5), rather than commas
A range comparison is rejected Write two complete checks, such as game:score > 0 AND game:score < 10; do not write 0 < game:score < 10
CLAMP is rejected while playing Put the minimum before the maximum
IFERROR does not use its alternative It catches only recoverable calculation errors in its first value, not invalid syntax, incompatible types, safety limits, or a valid fraction rejected after the function finishes
A value has the wrong type Use only numeric game values for calculations and Yes/No game values for true-or-false checks

Formula limits

Most story calculations stay well below these limits:

Limit Maximum
Formula length 4096 bytes; ordinary formula characters use one byte each
Formula pieces, including values, names, operators, separators, and parentheses 512
Nested expressions 32 levels
Arguments in one function call 32
Calculated steps during one use 1024
Digits after a decimal separator 12
Power used by ^ or POW -32 through 32
Exact whole or fraction component during a calculation 1000000000000000 in either direction
DICE sides 1 through 1000000
RANDOM bounds -1000000 through 1000000

What the reader experiences

A Set by formula row runs before the chapter is shown. A formula in chapter syntax runs when the reader opens the reveal, selects the action, submits the lock answer, or checks the ordering puzzle. The reader sees the result in chapter text, Game State, inventory, conditions, and follow-up choices, but does not see the formula itself.

Every successful calculation is saved as a game step. Going Back past that step restores the earlier Game State and makes the rollback-aware interaction available again. After going Back past that step, entering the chapter again or using that interaction again runs the formula anew and makes new choices for every DICE or RANDOM call.

Before publishing

Test the chapter with both a fresh game and an existing game. Check that:

  1. every referenced value either exists on each route that can reach the chapter or has the intended EXISTS, IF, or IFERROR fallback;
  2. change rows are in the required order;
  3. division, modulo, powers, error fallbacks, rounding, minimums, and maximums behave as intended;
  4. when the formula uses DICE or RANDOM, its smallest and largest results fit the story;
  5. Percent and Age results stay meaningful at their supported limits;
  6. interactive formulas run only from the intended reveal, answer, check, or action;
  7. several changes in one action run in the intended left-to-right order;
  8. going Back and entering the chapter or interaction again gives the expected result.

Older game-variable names

New variable names use letters, numbers, and underscores, so game:name is the normal formula form. An older story may have a game-variable name with spaces or punctuation. Put that exact name in double quotes inside brackets:

game["Money on hand"] - game:price

Inside the quoted name, write \" for a double quote and \\ for a backslash. Braces and control characters are not allowed. Keep a working older name unchanged unless the story is being carefully revised.

The same direct reference can be checked with EXISTS(game["Money on hand"]).

Start your own immersive adult AI roleplay story
Ad

You've reached the end of this Guide topic.

  • No further chapters
Back Start Over View Story Map

1 comment