Conditions
Note
Status: implemented. The compiler recognizes a condition (`key?`),
binds it to the jump, line, control line, choice option, or control branch it
fronts, and carries it into the playbook. The runner asks the world about it
(see Asking the World), except on a choice
option, which waits until the runner plays choices.
Table of contents
- Ubiquitous language
- The primitive
- Where a condition attaches
- Conditional jump
- Conditional line
- Conditional choice option
- Resolution
- Key design decisions
- Diagnostics
- Error and boundary cases
- Deferred work
Ubiquitous language
| Term | Meaning |
|---|---|
| Condition | A game-state query read as a boolean: `key?`. The one word for it in code, diagnostics, the guide, and the changelog. |
| Guard-first | The condition is written before what it guards, so it reads "if … then …". |
| Peel | Removing a leading condition code span from a block before the rest is parsed, so the condition becomes a property of the block rather than its content. |
| Bound | A condition that is exactly the Condition its parent jump, line, control line, option, or branch references. Any other condition guards nothing. |
The primitive
A condition is the query the writer already knows, with a ? sigil. It is the
third member of the query-and-sigil family:
| Syntax | Meaning |
|---|---|
`"key"` |
Insert the query's value into speech (always quoted). |
`key%` |
Weight a random option by the value. |
`key?` |
Read the value as a boolean condition. |
Condition = "`" , Key , "?" , "`" ;
Key = UnquotedKey | QuotedString ; (* unquoted is the default *)
UnquotedKey = NonSigilText ; (* trimmed, non-empty; spaces allowed *)
The key is everything before the ?, so `Is Alice happy?` reads the key
Is Alice happy. Quotes are the escape for a key that ends in ?:
`"Rainy?"?` reads the key Rainy?. Unquoted Keys
owns the key grammar; ConditionReader recognizes the span through the shared
QueryKeyReader.
Where a condition attaches
One primitive, five attachment points. The construct decides where the condition is recognized and what a false answer means.
| Attach point | Where the condition is bound | Playbook field | When false | Example |
|---|---|---|---|---|
| Jump | Inline fragment, bound to the following => in desugar (JumpAssembler) |
condition on the divert edge |
The jump does not fire; reading continues with the next block | `FoundKey?` => [Open the vault](#the-vault) |
| Line | Peeled at the block start, before the speaker (LineBuilder) |
condition on the line node |
The line is skipped whole | `Angry?` Guard: You again? Get out. |
| Control line | Peeled as for a line, then carried by the control line | condition on the control node |
The effect is not performed | `GateJammed?` `("force the gate")` |
| Choice option | Peeled at the list item, before the weight and body (ChoiceConditionRecognition) |
condition on the option or random-option edge |
A player option is shown unavailable; a random option is excluded and the rest re-normalized | - `HasKey?` Use the key on the lock. |
| Control branch | After the `if` / `elseif` marker (Block Controls) |
condition on the branch edge |
The next branch, or the `else`, is tried |
> `if` `Rich?` |
The attach points differ because the constructs differ: a jump can sit mid-line, so its condition travels with it through the inline stream, while a line and an option always start their block, so their condition is peeled there.
Conditional jump
`FoundKey?` => [Open the vault](#the-vault)
=> [Search the study](#the-study)
If FoundKey is true the reader takes the vault; otherwise the jump is skipped
and the unconditional jump to the study runs. The condition must sit on the same
line, immediately before =>; spaces between them are allowed. A conditional
jump inherits every other rule of a jump.
Conditional line
`Angry?` Guard: You again? Get out.
`NotAngry?` Guard: Back so soon? Go on through.
The condition sits before the speaker, and Guard is still recognized as the
speaker. A speaker-less line may be conditional too (`Returned?` Welcome back.).
LineBuilder peels the leading condition only when non-jump content follows it:
- when a
=>follows, the condition is left for the jump to claim; - when nothing follows, the condition is left in place and reported as guarding
nothing (
DLG1106).
Conditional choice option
- `IsAngry?` `50%` The guard glares and blocks your path.
- `30%` The guard waves you through.
- `20%` The guard ignores you.
The condition comes first, before the weight. RandomChoiceRecognition
peeks past it to find the weight, so a condition-first option still makes the
list a random choice. When IsAngry is false the first option is excluded and
30 and 20 re-normalize to 60% and 40%.
The option condition is peeled at the list item, before the body is built, so it guards the whole option and takes precedence over the inner line and jump handling:
| Option written | Reads as |
|---|---|
- `c?` Bob: Attack |
a conditional option; its body line is unconditional |
- `c?` => [x](#x) |
a conditional option whose body is a plain jump |
- `50%` `c?` Bob: Attack |
a random option whose body line is conditional |
Resolution
The contract every runtime honors, for every attach point:
- The runtime reads the key from the world as a boolean.
truelets the construct happen;falseapplies the construct's false behavior from the table above.- An unknown key is
false, so a flag that was never set does not fire.
The host interface that ships, IGameSystem, exposes only a string Query and
an Execute. A dedicated boolean read is part of the proposed world seam in the
runtime architecture,
and its name is not settled.
Key design decisions
D1 — A condition is a read, not a command
A condition reads game state, so it belongs with queries rather than with effects.
A command form such as `If("Rainy")` would borrow the command grammar for a
read, reserve If out of the game's command names, and invite an expression
language.
D2 — The ? sigil joins the query-and-sigil family
A writer who knows `"key"` and `key%` already knows the shape. The
sigil after the key is the operator, and quotes escape a key that ends in one.
D3 — Guard-first placement
Written before what it guards, a condition reads "if … then …" and is scannable
at the start of the construct. One placement rule serves every attach point,
matching Ink's {cond} …. Placing it after (Guard: `Angry?` Leave.) buries
it mid-line.
D4 — A dedicated boolean read
A condition resolves through a boolean read of its key, so the runtime never
parses "true" out of a string and there is no truthiness ladder. Dynamic
weights still read a value, because a number in a string is natural where a
boolean is not.
D5 — One spanned, reusable node
Condition is its own spanned ScriptNode, and every guarded construct holds it
through the IConditional interface (Line, ControlLine, Jump, Choice,
RandomOption, Branch). Tooling can point at the exact condition, and one node
and one reader serve every attach point.
D6 — An option condition is peeled at the list item and wins
A condition on a menu item is meant to guard the menu item, so the list-item peel
runs before the inner builders and they never bind it again (see the precedence
table above). The line and the option share ConditionReader.TryPeel; each
applies its own binding policy.
D7 — False falls through; no inline else
A condition guards exactly one construct. The alternative is written on the next line, often as a condition on an inverse flag. Grouped, mutually exclusive branches with a fallback are the separate block control.
D8 — A conditional random option defers the weight total
A conditional option may be excluded at play time, so the achievable total is
unknown at compile time. WeightTotalRule skips DLG3003 and DLG2010 for a
random choice with any conditional option, exactly as it does for a dynamic
weight. A conditional option still needs a weight (DLG1104).
D9 — A player option is shown unavailable, not removed
A false player option is reported as unavailable in the menu rather than dropped,
so the host decides whether to hide or disable it. The runtime architecture owns
this decision
(D8 there),
and the an-unavailable-option conformance case pins it.
D10 — No negation, no expressions
There is no not, and, or comparison. "Unless" is a game-defined inverse flag
(`NotRainy?`), and the game composes logic behind one key. A prefix !
was rejected as cryptic for non-technical writers and can be added later without
changing ?.
Diagnostics
| Code | Meaning | Severity | When |
|---|---|---|---|
DLG1106 |
A condition guards nothing | Error | The condition is not bound: not immediately before a =>, not at the start of a line or option with content, and not after an `if` / `elseif` marker. |
A code span that is not a clean condition falls back to game-call recognition,
and if that fails it is DLG1102 and kept as literal text. There is no
invalid-value diagnostic: a condition always resolves to true or false.
Error and boundary cases
| Input | Result |
|---|---|
`K?` => [L](#a) |
Conditional jump. |
`K?` Guard: Hi |
Conditional line; Guard is the speaker. |
`K?` Hello |
Conditional line with the default speaker. |
`K?` alone on a line |
DLG1106. |
Guard: You `K?` there |
DLG1106; a condition inside speech guards nothing. |
`"Rainy?"?` Guard: Hi |
The key is Rainy?. |
`"a" "b"?` Guard: Hi |
Not a condition; DLG1102, literal text, the line is unguarded. |
- `K?` `50%` … |
Conditional random option with both a condition and a weight. |
| Every option in a random choice conditional | Accepted; the weight total is deferred, and an all-false pool selects nothing at play time. |
| A condition in a heading | Read as heading text; a heading cannot hold a jump. |
Deferred work
- A condition on a choice option. It is compiled and carried into the playbook, and the runner evaluates it once it plays choices.
- Negation and expressions. Deferred by D10.