Asking the World
Note
Status: implemented. The pass that lets a run ask the world a question and use the answer, so a condition is evaluated instead of refused. A node's own condition, the queries standing in its speech, a jump's condition, and a block condition's arms are asked and answered. An option's condition arrives with choices.
It extends the Runner's protocol and harness, and follows the shape of its wait on the host. It applies the dialogue runtime architecture, which owns the cross-cutting decisions this note uses, and does not restate them.
Table of contents
- Goal and scope
- What the corpus fixes
- Functionality checklist
- Interfaces and abstractions
- Key design decisions
- Error and boundary cases
- Integration
- Testability
- Open questions and deferred work
Goal and scope
Six of the eleven node and edge kinds the format defines can carry a condition, so a runner that cannot read one cannot play most of what a writer can write.
This pass lets a run ask the driver what the world says, wait for the answer, and use it. Three constructs follow from that one exchange, and they are one feature rather than three — each asks the driver about state and reads back an answer.
| Construct | The question | What the answer decides |
|---|---|---|
| A guarded line or control block | Does this key hold? | Whether the node plays at all |
| A guarded jump or branch arm, and an option once choices play | Does this key hold? | Whether that way out is taken |
| A query in speech | What is this key's value? | What the line says |
In scope:
ResolveandSupply, the second pair of reverse request and answer, andAwaitingSupply, the stage a run is at between the two;- gathering the keys a node needs into one ask per moment: one before the node plays, and one before the run leaves it;
- evaluating a
keycondition on a node, on a jump, and on a block condition's arm; BranchNode, whose arms are tried in order until one holds;- substituting a
QueryFragmentin speech with what the world said; - retiring
UnansweredCondition.
Out of scope, each with the pass that owns it: choices, and an option's
condition with them (C2b); Describe (C2e); saves (C2f); and PlaySession with
its drivers, and the world a driver answers from (C2g). Dynamic weights need a
number from the world and entropy to spend it on, and wait for the pass that plays
a random choice.
What the corpus fixes
The contract is not this note's to choose. The published fixture schema defines
resolve, supply, and asked, and five corpus cases pin the exchanges, so this
note is about how the runner meets that contract.
| Case | What it fixes |
|---|---|
a-conditional-line |
One resolve naming one key, a supply of false, and then the next line is said — a failing condition steps over the node rather than stopping at it |
a-conditional-jump |
A jump with nothing to say still stops to ask: one resolve before the run walks past it, and a supply of true takes the jump, so the line after it is never said |
a-conditional-block |
The arms are tried in the order written, and a satisfied first arm means the else is never reached |
a-query-in-speech |
The supplied answer appears in the flattened speech: Hello, Robin. |
an-unavailable-option |
A failing option is offered unavailable rather than hidden. Needs choices too, so it does not play yet |
The schema also fixes the shapes: resolve is a non-empty array of key strings,
and supply is an object keyed by those strings, whose values are whatever JSON
holds — false for a guard, "Robin" for a query.
Functionality checklist
- [x] A node carrying a condition asks the world about it rather than refusing.
- [x] A node whose condition fails is stepped over, and the run carries on by its succession, never by its own jump.
- [x] A jump or a block condition's arm whose condition fails is not taken; one whose condition holds is.
- [ ] An option whose condition fails is offered unavailable. Deferred to choices (C2b).
- [x] A branch node takes the first arm, in
order, whose condition holds. - [x] A branch node with no satisfied arm and no
elsefalls through to the succession beneath the block, so the block is skipped; one with no succession either leads nowhere, and says so. - [x] Every key a moment needs is asked for in a single
Resolve: one on the way in, one at each stop inside a line, and one on the way out. - [x] A query in speech is replaced by what the world said before the line is said.
- [x] A key asked and left unanswered is refused, and so is a key answered that nobody asked about.
- [x] An answer of the wrong kind for the question is refused.
- [x] A key one node needs as a truth and as words both is refused, because a single answer can only be one of those.
- [x]
UnansweredConditionis gone, and nothing produces it.
Interfaces and abstractions
| Type | Responsibility | Collaborators |
|---|---|---|
Resolve(keys) |
A reverse request: the keys one moment at a node needs answered | Request, alongside Perform |
Supply(answers) |
The command answering it | Command |
Answer |
What the world said about one key, as a closed union | AnswerJsonConverter |
AnswerJsonConverter |
Reads and writes an answer as the bare JSON value it is | Every reader of a supply |
AwaitingSupply(node, keys, moment) |
Where a run is between the ask and its answer | Situation, alongside AwaitingDone |
Moment |
Whether the run asked before the node plays (ToPlay) or before the run leaves it (ToLeave) |
AwaitingSupply, Runner |
NodeQuestionExtensions |
Reads a node's keys, one reader per moment and per kind of answer | NodeQuestions |
Questions |
Turns a moment's two sets of keys into what goes out and what comes back is held to | NodeQuestions |
NodeQuestions |
One moment's keys, read together so the two kinds cannot drift apart | Arrival, Departure |
AnswerCheck |
Holds what came back to what was asked, and names every way they disagree | Arrival, Departure |
SupplyExtensions |
Reads a key from a supply as a truth or as words | ConditionEvaluationExtensions, Arrival |
ConditionEvaluationExtensions |
Answers whether a condition holds, and whether a guarded node or way out is allowed | Arrival, NodeTraversalExtensions |
Arrival |
Arriving at a node: asks what playing it needs, then plays it or walks past it | Runner, Departure |
Departure |
Leaving a node: asks which way out when its ways out are guarded, then arrives where it leads | Runner, Arrival |
NodeTraversalExtensions |
Where a node leads: the first jump or arm taken, then the succession | Arrival, Departure |
StepResults |
The ask and the refusal both halves produce | Arrival, Departure |
SpeechTemplate |
Names the keys standing in speech, and fills them with words (in the playbook package) | NodeQuestionExtensions, Arrival |
Key design decisions
A1 — One ask per moment
A run reads the world twice at a node, and each reading gathers every key that moment needs and asks for them together. A line with a query written after a command has one more moment at each stop inside it.
Arriving asks what decides whether the node plays and what it says: the node's own condition, answered with a truth, and the queries standing in its speech up to its first stop, answered with words. Leaving asks what decides which way out is taken: the conditions on its jump or on a block condition's arms.
The two are kept apart because the node changes the world between them. A node
that performs an effect has that effect carried out after it is played and before
the run reads on, so an arm guarded by a key the effect touches must be judged
against the world the effect left behind. Asking on the way in would read the
world as it was before, and the run would take a way out the writer did not mean.
This is the same read-your-own-writes guarantee Perform already buys, applied
to the keys that decide succession.
Within one moment the batch is a snapshot, so evaluation is a repeatable read: a menu whose options are guarded by the same key cannot offer one and refuse another. A run therefore stops at most once on the way in, once at each stop inside a line, and once on the way out, which is what keeps the protocol readable.
A moment asks about every key it might need, not only the ones that end up
deciding. A block's elseif is asked about even when its if holds and the
elseif is never reached. That is sound because a query is a pure read
(D6):
reading a key changes nothing, so reading one more costs only the read, and the
driver may answer the keys in any order, one at a time or all at once.
A2 — The runner does not remember an answer
Two lines that ask the same key produce two Resolves. The runner is a total
function of state and command, and holding an answer between steps is state.
The two asks are also genuinely two questions. The world may have changed between
them, and the driver is entitled to answer differently the second time; a runner
that reused the first answer would report something the world never said. Caching
belongs to the driver, which is free to hold an answer or serve from a snapshot as
its host requires. What the protocol guarantees is read-your-own-writes, already
bought by Perform being answered before the run goes on.
A3 — An answer is a closed union
The world seam the architecture note designs, IGameWorld, splits the world's
three questions by the type of their answers: a guard needs a truth, a weight a
number, and interpolation text. The wire agrees — supply carries false for one and "Robin" for another.
So Answer is a closed union in the manner of every other union in the format,
and its members take the same <Qualifier><Base> shape as TextFragment and
KeyCondition do. This pass needs two:
| Member | Wire | Used by |
|---|---|---|
BooleanAnswer(bool Holds) |
true / false |
A condition on a node or on a way out |
TextAnswer(string Text) |
a JSON string | A query in speech |
A third member, for numbers, joins them when dynamic weights arrive. Adding a member to a closed union is additive here, and a member nothing produces would be dead code today.
Unlike the format's unions, this one needs no class of wire tags. A supplied answer carries no discriminator, because the JSON value already is one:
{ "Alice.HasKey": false, "playerName": "Robin" }
AnswerJsonConverter is where that rule lives — a boolean is a truth, a string is
words, and anything else is refused rather than guessed at, since a number could
be a weight or a count and nothing on the wire says which. Keeping it in the
package rather than in each reader is what stops a fixture, a recorded session,
and a driver on the far side of a socket from each inventing their own.
A4 — The runner checks the answers against its own questions
A driver may answer something nobody asked, or leave something asked unanswered. Both are refused, and the check needs no memory to make: the keys the run asked about are carried in the situation, so the comparison is set equality between what was asked and what came back.
That makes it a function of two collections and nothing else, which is why it
lives in AnswerCheck rather than inside arrival or departure. NodeQuestions builds the set
going out, AnswerCheck holds the set coming back to it, and each can be tested
without a playbook in sight.
A5 — The runner is the mechanism; the driver is the policy
A driver may answer from a live world, a cache, a recorded session, or a table of defaults, and may have its own rules for a key nobody bound. None of that reaches the runner, which asks a question and reads an answer.
The separation is worth stating because the architecture note's permissive default
— a script plays with no bindings at all — is easy to mistake for something the
runner does. It is not: by the time a key arrives in Supply, the driver has
already applied whatever policy it has. This is the same line A2 draws for
caching, seen from the other side.
A6 — A failing condition routes; it does not refuse
a-conditional-line fixes this: with Alice.HasKey false, the next thing the run
says is the following line. So a node whose own condition fails is stepped over,
and the run arrives at whatever its succession leads to. It takes the succession
alone: a jump belongs to the node that carries it, so a line the world withheld
does not send the reader through the door it opens. A node the world allows is
entered as if it had needed no answers: played with the answers in it, or walked
past when it hands the host nothing.
A guarded node stops the walk to ask. So a loop that comes back round to one asks about it again, and the world may answer differently the next time; the ring bound only ever meets nodes that nothing guards.
An edge's condition is a different question with a different answer: a way out whose condition fails is simply not among the ways out. Where a node leads is one rule, read with the answers or without them: the first jump or arm taken, then the succession. With answers, a way out is taken when the world allows it. Without them, only a way out that nothing guards is taken, because nobody asked the world about the rest.
A7 — What the run is doing lives in the situation
AwaitingSupply(node, keys, moment) holds the node the run is standing at, the
keys it asked about, and the moment it asked at.
Carrying the keys is what lets the runner check that the driver answered the
question it asked. Carrying the node is what lets the step that receives Supply
finish what it started — because nothing was remembered, that step re-reads the
node and evaluates it with the answers in hand. Carrying the moment says what it
started: Runner hands an answer given before the node plays to Arrival, and
one given before the run leaves it to Departure. The keys alone cannot say which,
because one key can be asked at both moments of one node.
A8 — A branch node is walked past, not stood at
A branch says nothing and asks the host for nothing. It exists to choose an arm. So it belongs to the same family as the empty control node: the walk resolves its arms, takes the first that holds, and carries on to the target without the player ever being asked to advance past it.
The arms' order is not this pass's to decide. The reader already guarantees that a
branch's arms appear in strictly ascending order, that at least one is gated, and
that a conditionless else comes last, so the runner tries them as it finds them.
A branch has nothing to ask on the way in, so all of its reading happens on the way
out: one ask carrying every arm's key, then the first arm the answers allow. When
none holds and there is no else, the run falls through to the succession the
compiler writes beneath the block, so the block is skipped, as the guide describes.
A jump on its own line is walked past the same way, and its condition is asked
on the way out too. Both ask inside the walk rather than by leaving through
Departure, so the ring bound still counts every node the walk passes.
A9 — A query is substituted before the line is said
Said carries speech as fragments, and QueryFragment carries a key. When the
answers are in hand, each query fragment becomes a TextFragment holding what the
world said, and the line is said with that speech.
Doing the substitution in the speech itself, rather than at the point text is
flattened, means every reader of a Said sees the same words — the fixture that
compares flattened text, a host that renders fragments with styles, and a log that
replays. It also leaves SpeechText with nothing new to know.
Error and boundary cases
| Case | Behavior |
|---|---|
Supply when nothing was asked |
Refused as misplaced, as Done already is |
Next while the run awaits a supply |
Refused as misplaced |
| A key that was asked and not answered | UnansweredKey, naming the key |
| A key answered that was not asked | UnaskedKey, naming the key |
| An answer of the wrong kind for its use — text where a guard needs a truth | WrongAnswerKind, naming the key and both kinds |
A branch whose arms all fail, with no else |
Falls through to the succession beneath the block, so the block is skipped. Leads nowhere only when there is no succession either, which no script compiles to |
| A skipped node whose succession leads nowhere | Leads nowhere |
| A guarded node with nothing to say or perform, once the world allows it | Walked past, as an unguarded one is. No script compiles to this, but a reader accepts it |
| A loop the world keeps withholding every guarded node of | Each guarded node is asked about again as the walk comes back to it; the ring bound is never reached |
| A node with a condition and a query in its speech | One ask carrying both keys |
| A node naming one key as its condition and as a query | KeyNeededBothWays, naming the key, refused before anything is asked, and again when a run restored straight into the wait is answered |
The three refusals divide one driver mistake three ways on purpose. A reason is
what a fixture compares, so a port that answers the wrong question and a port that
answers with the wrong type disagree with us for reasons a reader can tell apart.
The first two are caught by AnswerCheck holding the set to what was asked; the
third by the kind each key was asked under. Each leaves the run waiting where it
asked, so a driver that misread the request can answer it again rather than lose
the conversation.
A key a node needs as a truth and as words both is a fourth reason, and not a driver mistake at all: the playbook asks one key two ways, and a single answer can only be one of those. It is refused on the way in, before a request goes out that nothing could read back.
UnansweredCondition is removed rather than left unused. It is a published
refusal reason, and a reason nothing can produce is a promise the corpus would
keep testing for no one.
Integration
| Seam | Change |
|---|---|
protocol |
Resolve joins Perform as a request; Supply joins the commands; Answer and its JSON converter are new |
situations |
AwaitingSupply joins AwaitingDone, carrying the Moment it asked at |
Arrival |
Asks what playing a node needs; steps over a node the world withholds; walks past a branch and a jump on its own line, asking about their ways out inside the walk |
Departure, StepResults |
New: leaving a node, asking first when its ways out are guarded; and the ask and the refusal it shares with Arrival |
NodeQuestions, Questions, AnswerCheck, ConditionEvaluationExtensions |
New, and each testable without a playbook |
NodeTraversalExtensions |
One rule for where a node leads: the first jump or arm taken, then the succession |
Runner.Step |
Next and Done leave through Departure; Supply goes to Arrival or Departure by the moment the run asked at |
| Harness | A ResolveMatcher, and a SupplyReader so supply is among the commands a session can send |
PlayableConformanceTests |
a-conditional-line, a-conditional-jump, a-conditional-block, and a-query-in-speech join the conforming list |
PlayableRun.IsPlayable |
Learns BranchNode, and its agreement test holds it to the runner |
PlaybookGen |
Draws guards, jumps the world must allow, block conditions, and queries, and its coverage test fails until it does; a world drawn beside each playbook answers every question a walk meets |
| Runtime core note | Its state diagram shows AwaitingSupply as a stage a run reaches and leaves |
The PlayableRun.IsPlayable and PlaybookGen rows are the ratchet working as
designed. Teaching the runner a kind makes two lists fail by name, which is the
reminder a pass is owed.
Testability
| Level | What it covers |
|---|---|
| Unit — questions | Every source of a key: a node's own condition, its ways out, and its speech; one node needing all three |
| Unit — answers | The set matching; a key missing; a key nobody asked about; a key read as the wrong kind |
| Unit — reading a supply | Each kind read and written back; a number, a null, and a structure all refused |
| Unit — evaluation | A condition that holds, and one that fails |
| Unit — arrival and departure | A withheld node stepped over by its succession, not its jump; an allowed node with nothing to hand the host walked past; a loop asked about again; a withheld jump not taken; a block asking about every arm at once |
| Unit — ways out | The first jump or arm taken, then the else, then the succession, read with answers and without |
| Unit — speech | A query substituted; a line with text and a query together; a query whose answer is empty |
| Unit — the protocol | Supply where nothing was asked; Next while awaiting a supply |
| Property | The walk property, widened: the generator draws guards, jumps the world must allow, block conditions, and queries, and a world drawn beside each playbook answers every wait. A run still only ever stands where the playbook has a node, and never turns an answer away |
| Conformance | a-conditional-line, a-conditional-jump, a-conditional-block, and a-query-in-speech conform |
Three are worth naming because they are easy to leave out. A node with a condition
and a query must produce one Resolve carrying both keys, which is the
half of A1 a single-key fixture cannot show. A branch whose arms all fail with no
else must skip the block rather than hang. And a loop that comes back to a
guarded node must ask about it again: the ring bound was written for nodes that
nothing guards, and a guarded node stops the walk before the bound is reached.
Open questions and deferred work
- An option's condition arrives with choices.
an-unavailable-optionis the one corpus case this pass touches without finishing; it stays named as not yet runnable until C2b lands. IGameSystemstill has its placeholder name. Nothing in this pass reads a world directly: the runner asks the driver. So the rename toIGameWorldwaits for C2g, where a driver answersResolvefrom a world.- A key used both ways is caught only at play time. The compiler does not yet
reject a script that uses one key as a guard and as a query on the same node, so
KeyNeededBothWaysis what catches it.