Your game's state
The story asks questions about your game. Is it night, does the player have the key, how
much gold? @world is where those answers live. It’s the set of properties your game
owns and the story’s conditions read, and it’s usually the first thing you wire up after
loading a bundle.
Everything on this page is the same in all four runtimes. Only the spelling changes.
Declaring it
Section titled “Declaring it”@world properties are declared on the project, in Project Settings, with a name, a type,
and a default. Declaring is the designer’s half. It tells the editor which names exist, so a
condition can be checked as it’s written and a typo is caught when the bundle is published,
not in your game.
Your game doesn’t declare anything. It fills in what’s already declared.
A game with more than one editing tool (Storyletter and Patterpad, say) keeps @world in one
place instead: game.scopes.json in its
shared scopes folder. Storyletter’s
World settings then edit that file, and each project keeps a synced copy so it still compiles
on its own.
@world.is_night boolean default false@world.gold number default 0@world.chapter string default "one" read-onlyRead-only
Section titled “Read-only”Four things, and they are the same four in Patter, in the same order, so a game running both engines sees one rule:
- Read-only is the story’s promise, declared on the property (
writable: falsein the shard, the Read-only switch in Storyletter). A condition can still read@world.chapter; an outcome that sets it is a compile error, so a card cannot move the game’s clock by mistake. - The runtime keeps the promise too. If a bundle somehow carries such a write,
play()refuses it with'@world.chapter' is read-onlyand nothing changes. Your resolver’ssetis never called for a read-only property. - A resolver with no
setmakes the whole of@worldread-only to the story, whatever the declarations say. That is the game’s policy rather than the story’s promise, and both apply. - Your game is never bound by it, resolver or none. The promise is the story’s, not the
game’s.
setProperty("world.chapter", ...)writes, whether you bound a resolver or let the engine keep its stand-in bag, and so do the coverage driver and the CLI’s--set, which exist to move exactly these values. The examiner still shows the property as read-only, because that is what the flag means: read by the story, moved by the game. (Until 2026-09-05 the stand-in bag refused the game as well, which locked the game’s own tools out of its own clock. That was a bug in the shared kernel, fixed in scoperegistry 0.6.0 and here.)
There’s no write-only, because a declared property can always be read by the story. If the game holds a value the story shouldn’t see, don’t declare it.
A name that isn’t declared can’t be referenced. The compiler refuses @world.isNight if
nothing declares isNight. Property names are lower case (see the format).
Binding it
Section titled “Binding it”@world lives on the engine, and every flow sees the same values. It’s the game’s own
state, and the game is one thing however many playthroughs run over it. You can hand the
engine a resolver when you build it (new Engine(bundle, { world: { get, set } })), so
conditions read your live game state directly. Or hand it nothing, and the engine backs
@world itself from the declared defaults, as a property it saves, which is fine for most games.
A game running more than one engine registers @world once in its registry instead, and hands
the registry to each engine (Running it with Patter).
Writing it from your game
Section titled “Writing it from your game”Before you deal, tell the flow what’s true:
flow.setProperty("world.is_night", true);flow.setProperty("world.gold", 120);
const dealt = flow.deal("tavern-encounters");The path form is world.is_night, not @world.is_night. The @ belongs to the
expression language a designer writes in; the API takes a plain path. The paths are:
| Path | Reaches |
|---|---|
world.<name> |
your game’s state |
story.<name> |
the story’s own global state |
box.<box>.<name> |
a box’s properties |
deck.<deck>.<name> |
a deck’s properties |
hand.<hand>.<name> |
a hand’s properties |
value.<tag>.<name> |
a tag’s own properties (see below when two boxes share a tag name) |
The owner is named by the name you gave it. A box, deck, hand, or tag is addressed by the same
gameId you write in a shard and read on a card. So the Elder’s zone is
hand.the-elder.zone, and the docks’ danger is value.docks.danger.
Two boxes may name a tag the same way. Box, deck, and hand names are unique across a
project, but a tag’s name only has to be unique within its group, and a group’s within its
box, so a harbour box and a cellar box can each have a docks. Where that happens, say which
box: value.harbour/docks.danger. The slash sits inside the owner segment, so the address
still has its three parts. You can always write the long form, whether or not you need it. The
short one is refused where it would name two tags at once, and the refusal tells you both
addresses to choose from. A project whose tag names happen to be unique sees none of this.
getProperty reads the same paths, and listProperties() returns every declared property
with its path, type, current value and default. That list is what the in-engine
examiners are built from.
When to write it
Section titled “When to write it”Write before you deal. A card’s condition is evaluated at the moment you deal or peek,
against the state as it stands then. Set @world.is_night after dealing and the hand you
already have won’t change; the next deal will.
For the same reason, a dealt card doesn’t carry its outcomes’ availability. Ask
outcomes(card, hand) when you’re about to show them and you get the current answer. A card
can sit in a hand for many turns while your game moves underneath it.
Which way state flows
Section titled “Which way state flows”@world is yours, and the story reads it. An outcome can write it too: “playing this card
makes the player poorer” is a normal thing to want. So treat @world as shared, and read it
back after a play if your game holds its own copy.
If you’d rather the story never touched a value, keep it in your own code and push it in with
setProperty. Nothing forces you to declare state you don’t want written.
Shared or per flow
Section titled “Shared or per flow”Every property is either shared (one value across every flow) or a copy per flow,
set on the declaration with a shared flag, never by a different name. The defaults follow
the scopes:
| Scope | Default |
|---|---|
@world |
always shared (the game owns it, not the player) |
@story |
shared |
| box, deck, hand, and tag properties | a copy per flow |
A single-flow game never notices any of this. With several flows, the narrow scopes are where
personal experience lives (this participant’s danger in the docks), while @story and any
property flagged shared is the world every flow moves together. Flows meet only through
shared state. There’s no message-passing between them, and no flow can read another’s copies.
Cards can be shared too. The same word on a deck (or a single card) makes the cards themselves scarce across flows rather than the state they read, so there’s one goblin in the whole world, to whoever finds it first. That’s How a deal is decided.
Durable: state that outlives a run
Section titled “Durable: state that outlives a run”Sharing says whose a value is. durable says how long it lasts. It sits beside shared
on the same declarations, and the two are independent:
| run-scoped (the default) | durable: true |
|
|---|---|---|
| shared | world truth for this run: the act, the well opened | the installation’s memory: trolls defeated since it opened |
| per flow | this visit: my danger in the docks | the player’s pocket: visits, allegiance, what they earned |
The engine never reads the flag. It partitions by shared alone, and durability is what
whoever runs the engine does at a run boundary: read the declarations, lift the durable values
out with getProperty before the world restarts, and write them back with setProperty into
the fresh one. Everything that needs is already public.
durable is a compile error on @world, for the reason shared is. @world is the game’s
own state, and how long the game keeps it is the game’s business.
A card can be durable too, on the deck or on the card, and it means one thing. Its
redraw: never spend survives the run. Only never can (a finite cooldown is an absolute
turn of a box’s clock, and the clock resets with the run), so durable on any other redraw is
a compile warning. A durable per-flow spend rides in that flow’s cooldowns. A durable shared
one rides the engine’s spent set, and both go back with openFlow(id, { restore }) and
markTaken.
Saving it
Section titled “Saving it”saveGame() returns the whole run: the shared state once (with anything a shared one-shot has
taken out of the world), then every flow’s own state, turn counters, cooldowns, board contents,
and random stream position. loadGame(envelope) restores it, rebuilding every flow, so
re-take your handles with getFlow, never openFlow, which would replace the restored flow
and its dealt hand (see JavaScript, Save and load).
A load is deliberately forgiving about content that has moved underneath a save, which is what
lets a save survive an edit, and is also what hides the cost of one. So it tells you what it
did. loadGame returns a report of everything it dropped, defaulted, or reset, and
previewLoad(envelope) computes the same report without applying anything. saveFlow(id) and
openFlow(id, { restore }) do the same for ONE flow, for a playthrough that steps away and
comes back.
A @world your game binds is deliberately not in the envelope. It’s your game’s state (the
engine only borrows it), so your game saves it, next to the envelope. A self-backed @world is
different: nobody else holds those values, so the engine’s own registry stores them and they ride
in the save. A game running Patter and the Storylet Engine side by side keeps every property,
@world included, in one registry and saves it once, so nothing is written twice. The
.storyletsave file format carries a bound world’s values beside the envelope, and play-helpers
ships a ready-made container for games with no world state of their own (see
JavaScript, Save and load).
- The API, in one table: every call, in one place.
- Your engine’s page is JavaScript, Unity, Unreal, or Godot.
- Dev tools: the examiner that shows you these properties live while the the game runs.
Open source under the MIT licence, made by Ian Thomas.