Skip to content

Variables

Variables remember story facts across the whole life: has_pet, honest_reputation, dog_debt.

Every variable a fragment uses is declared in that fragment, in one or both of two blocks that sit between the header and the prose:

*use
crimes_committed
is_gay
*enduse
*declare
local enum shat_yourself { no, a_little, a_lot } = no
local count times_shat
*enddeclare

*use names shared state somebody else established. *declare introduces one: [local|innate] flag|count|enum <name> [{ members }] [= value]. Both are optional, take either order, and close by name: *enduse and *enddeclare.

The same two blocks declare the people and places your fragment brings with it, with a kind word in front: character kevin. See World Entities.

Using a name you did not declare is a build error. That is the point of the whole thing. A variable nobody set reads as 0, so a typo like crimes_commited does not fail - it makes a branch that never fires and a scene no player ever sees. Declaring turns silence into a message.

If you get one of these, the error tells you which block the name belongs in, and suggests the variable you probably meant.

Mark it local unless another fragment needs it. A local belongs to your fragment alone: behind the scenes it is stored as your_fragment_id.name, so your counter and somebody else’s cannot collide, and you can name it whatever reads best. Only a local can have a = default, because shared state starts unset and no fragment can know what ran before it. The one exception is an innate, which is settled before any fragment runs at all.

Shared state is a conversation, not a decision. You cannot add to the shared registry yourself; mention it in your submission thread and a maintainer records what it means. That is deliberate: a variable every future fragment can read is worth one sentence of agreement, and “what counts as a crime” is a question the type cannot answer.

It is recorded when your fragment is accepted, in the same commit, so a submission that does not make it in never leaves a reserved name behind. A shared variable nobody sets is a promise nobody kept, and the build says so.

TypeIsTakes
flagtrue or false*set: x, *set: x = false, *unset: x, *if x, *if !x
counta whole number*set: x = 3, +=, -=, *if x >= 2, and dice rolls
enumone of its members*set: x = a_little, *if x = no, *if x != a_lot

Enums compare only with = and !=. There is no >= on an enum, because the members have no order the game can see. If you want something that goes up, that is a count whose meaning you write down.

*set: has_pet # shorthand for has_pet = true
*set: has_pet = false # kept, but marked undone ("no longer")
*set: dog_debt = 2 # integers work too
*set: fate = random(6) # rolls 1..6 once, right here, and remembers it
*set: visits += 1 # counts up from what it already was
*set: purse -= 3 # and down
*set: purse -= random(4) # rolls 1..4 once and subtracts that
*unset: has_pet # erased entirely - as if it never existed

= random(N) resolves to a concrete 1..N the moment the line runs - later conditions, the display, and the saved life all agree on the same number. It’s the roll-once companion to dice checks.

  • Names are snake_case (lowercase). Casing is what separates variables from stats - HAP is a stat, hap would be a variable. age, played and pronouns are reserved.
  • Several names can share one line, comma-separated, mixing any of the forms.
  • *set:/*unset: lines are positional, like *effects: - they take effect where they stand.

Some facts about a character are not made by a scene, only found out in one. How a body takes to nicotine, which hand it favours, how well it holds a drink: your character had these before the story reached them.

*declare
innate count nicotine_affinity = random(6)
innate flag left_handed = chance(10)
*enddeclare

An innate is rolled once, when the life begins, for every life, whether or not the fragment that declared it ever plays. From then on it is an ordinary shared variable: other fragments *use it and read it like anything else.

WrittenMeans
innate count x = random(6)1 to 6, rolled at birth
innate flag x = chance(10)true in ten lives out of a hundred
innate count x = 5everyone starts at 5

Why not just roll it in the scene? Because then whichever fragment happens to be first owns a roll that is not about it, and every other fragment touching the same fact has to carry the same “roll it if nobody has yet” guard. Write the trait once, in the declaration, and the smoking scene can get on with being a smoking scene:

*if nicotine_affinity >= 5
Warmth, and then quiet, and the world a polite step further away.
*else
Your head aches and your stomach turns over. You don't get it.
*endif

A few rules follow from what an innate is:

  • local innate is a build error. An innate nobody else can read is a local with a default, which already works. The whole value is that the fact outlives the fragment that noticed it.
  • random(N) is for a count, chance(N) for a flag. One reading each, so the error tells you which one you wanted. chance takes 1 to 99: for always or never, say true or false.
  • Two fragments may declare the same innate, as long as they declare the same one. A life cannot be born on two different dice, and the build says so, naming both files.
  • An innate does not show in the Life panel. The panel is a record of what happened, and nothing has happened yet. Showing a player their nicotine affinity at age zero spoils a scene they may never reach.
  • *set: on an innate stands. It is where a life starts, not a constant the runtime keeps restoring underneath you.

Like any shared variable, an innate belongs in content/variables.json, where a maintainer records what the dice mean: 1 is a headache and a turned stomach, 6 is love at first drag.

+= and -= change a variable relative to what it already holds. The right-hand side is an integer or a random(N) roll, never true/false.

*set: visits += 1
*if visits = 1
First time here.
*else
You have been here before.
*endif

A variable that was never set counts as 0, and a boolean counts as 1 for true and 0 for false. A counter therefore needs no setup: the first += 1 produces 1. This is how you count visits, picks, coins, or patience, and it is the backbone of the hub idiom. To print the number in prose, write {visits} - see value tokens.

Some numbers only matter while the scene is being read: a nerve counter you build up over three choices, a roll you want to use twice. Those are declared where they are written, and they never leave the fragment:

*tmp count nerve = 0
*tmp flag asked = false

A transient is readable for the rest of the fragment, exactly like anything else, and then it is gone. It is never written to the save, never visible to another fragment, and never in the Life panel. It always says what it starts as, because it starts fresh every time the fragment runs.

It sits inline rather than in *declare on purpose. That block is your fragment’s contract with the rest of the corpus, and a transient has no counterpart there: nobody else can read it and nothing persists it. If you want a number that lasts the life, that is a local.

*tmp takes a flag or a count. An enum has no obvious value to start at, so it is not one of them.

Two ways to take a fact back, telling two different stories:

  • *set: has_pet = false keeps the variable as an undone fact - it happened, and was undone. For integers the convention is -1.
  • *unset: has_pet deletes it - gone from the store, and in conditions indistinguishable from never having existed. Leave no trace. Use it for bookkeeping cleanup or facts the story should genuinely forget.

In conditions, !var is true for falsy values (false, 0, negative) and for variables that don’t exist - both forms of clearing satisfy it. A variable is truthy only while it holds true or a positive integer. See Conditions.