Skip to content

Lattice reference · 3 of 4

CYOA choices and finite State

CYOA supports explicit route choices and optional finite State. TypeApe can check every possible route before you share the story with readers.

CYOA without State

A branching-only CYOA may use next, choice, and end without declaring any State, Pool, or Module.

Automatic continuation

text
next -> target_block;

The target must remain inside the same owner: main story or the current Module.

Player choice

text
choice {
  option inspect_key: { Inspect the key } -> inspect;
  option leave_garden: { Leave the garden } -> departure;
}

A choice:

  • contains at least two option declarations;
  • gives every option a unique ID within that choice;
  • uses prose for the reader-facing label;
  • targets an existing Block with the same owner;
  • has no semicolon after the closing }.

The option ID identifies the action inside that choice; it is not a State. Selecting an option changes the route but does not persist a fact about the selection.

Ending

text
end;

end terminates the whole story. This is also true inside a Module; use complete when the Module should return control to its Pool.

Branching graph rules

For a CYOA with no Pool or Module:

  • the Story entry exists in the main story and has no incoming edge;
  • all Blocks are reachable from the entry;
  • routes are acyclic;
  • at least one ending exists;
  • branches may converge into a shared Block;
  • every reachable route must terminate.

Convergence is the key difference from Linear story: several choices may target the same later Block.

The finite-state boundary

Lattice State is closed-world and declaration-derived. It intentionally has no variables, counters, arithmetic, scripts, functions, recursion, random values, dynamic State, or external input.

State is used only when availability or Pool exit must remember a named finite fact. Ordinary route topology should remain an ordinary choice.

Predicate declarations

A Predicate declares the entity kinds in a tuple schema:

text
predicate knows (
  subject: character,
  object: item
);

predicate at (
  subject: character,
  place: location
);

subject is required. object and place are optional schema fields. Each field type is one of character, location, or item.

A Predicate is not a Boolean variable and does not perform inference. It defines the exact shape that grounded State declarations using that Predicate must follow.

Ground State declarations

A State gives one Predicate a fully grounded tuple and requires a title:

text
state st_knows_signal (
  title: "Signal understood"
): event {
  subject: ch_mara,
  object: it_key,
  predicate: knows
};

state st_at_gate (
  title: "At the gate"
): event {
  subject: ch_mara,
  place: loc_gate,
  predicate: at
};

State validation requires:

  • every schema-required field to be present;
  • no field absent from the Predicate schema;
  • every entity reference to match the declared kind;
  • predicate to reference the schema being grounded;
  • an optional time to be one exact finite literal;
  • no two State IDs to represent the same grounded tuple.

Omitting an optional field means “absent,” not “any value.” Predicates do not imply relationships between different State tuples. Guards always reference State IDs directly.

Flags and State Groups

Ungrouped flags

A State that belongs to no group is a monotone Boolean flag. It starts inactive unless named by story.initial_state, can be activated with add, and has no removal operation.

Mutually exclusive groups

A State Group defines one finite slot:

text
state group location (
  members: [st_at_gate, st_in_garden]
);

members is required and non-empty. Groups cannot overlap. Runtime State contains exactly one active member from every group.

The Story selects initial values:

text
story main (
  title: "Signal Garden",
  language: "en",
  format: novel,
  entry: arrival,
  initial_state: [st_at_gate]
);

initial_state must select exactly one member from every State Group. Any unlisted ungrouped flag begins inactive. An empty list is valid only when there are no required group members to select.

Guards

Modules use requires, requires_any, and forbids. Pools use the corresponding exit fields exit_requires, exit_requires_any, and exit_forbids.

Guard fieldMatches when
requires: [a, b]every listed State is active
requires_any: [a, b]at least one listed State is active
forbids: [a, b]every listed State is inactive

Guard references must exist and cannot be duplicated. The same State cannot be both required and forbidden. A Module may have no Guard. A Pool exit Guard must contain at least one condition across its exit fields.

Guards inspect only current State IDs. They cannot inspect prose, narrative setup markers, the ID of an earlier choice option, the number of completed Modules, or declaration order unless a declared State explicitly records the relevant fact.

Pools

A Pool is a finite-state checkpoint entered from the main story:

text
pool investigation (
  title: "Investigate the garden",
  exit_requires: [st_knows_signal]
);

title is required. Every Pool has exactly one reachable main-story enter:

text
scene garden (title: "The garden") {
  block arrival (title: "Arrival", purpose: setup) {
    text: { The buried key pulses beneath Mara's hand. };
    enter investigation -> departure;
  }
}

The identifier after enter is the Pool. The identifier after -> is the main-story Block where reading resumes after the exit Guard matches.

enter is main-story only. A Module cannot enter a Pool, Pools cannot nest, and a Pool cannot have several entry sites.

Pool evaluation order

On entry and after each Module completion, TypeApe follows this exact order:

  1. Evaluate the Pool exit Guard.
  2. If it matches, resume at the Block recorded by enter.
  3. Otherwise, list every eligible unfinished Module in stable import/declaration order.
  4. Let the reader choose one Module.
  5. If the exit is false and no Module is eligible, report a soft lock.

Exit has priority over Module eligibility. Module order is presentation order, not an automatic scheduler or semantic priority.

Floating Modules

A Module belongs to one Pool and owns its nested Scenes and Blocks.

text
module md_decode (
  title: "Decode the key",
  pool: investigation,
  entry: decode,
  requires: [st_at_gate],
  forbids: [st_knows_signal]
) {
  scene decoding (title: "The buried code") {
    block decode (
      title: "Decode",
      purpose: development
    ) {
      text: { The light resolves into a patient sequence. };
      complete (
        add: [st_knows_signal],
        set: [st_in_garden]
      );
    }
  }
}

Required Module fields are:

  • title: reader-facing text;
  • pool: the owning Pool ID;
  • entry: a Block owned by this Module.

The Guard fields are optional. A Module is eligible only while its Guard matches and it has not completed before. Every Module is one-shot.

Module flow stays inside that Module. It cannot target the main story, another Module, or enter another Pool.

Completing a Module and applying Effects

complete is a Module-only terminator:

text
complete (
  add: [st_knows_signal],
  set: [st_in_garden]
);

It atomically:

  1. activates every ungrouped flag in add;
  2. selects every grouped State in set, replacing the prior member of that group;
  3. marks the current Module completed;
  4. returns control to the owning Pool;
  5. reevaluates the Pool exit and eligibility rules.

Use add only for ungrouped permanent flags. Use set only for grouped States. One completion may set at most one member of each group. There is no remove, toggle, or implicit conversion between flags and group members.

Effects are reconsidered only after the entire completion is applied, so Guards never observe a half-updated State.

Complete Pool/Module example

text
story main (
  title: "Signal Garden",
  language: "en",
  format: novel,
  entry: arrival,
  initial_state: [st_at_gate]
);

entity character ch_mara (name: "Mara", role: protagonist);
entity location loc_gate (name: "Garden gate");
entity location loc_garden (name: "Signal Garden");
entity item it_key (name: "Signal key");

predicate knows (subject: character, object: item);
predicate at (subject: character, place: location);

state st_knows_signal (title: "Signal understood"): event {
  subject: ch_mara,
  object: it_key,
  predicate: knows
};

state st_at_gate (title: "At the gate"): event {
  subject: ch_mara,
  place: loc_gate,
  predicate: at
};

state st_in_garden (title: "Inside the garden"): event {
  subject: ch_mara,
  place: loc_garden,
  predicate: at
};

state group location (
  members: [st_at_gate, st_in_garden]
);

pool investigation (
  title: "Investigate the signal",
  exit_requires: [st_knows_signal]
);

scene garden (title: "The garden", pov: ch_mara) {
  block arrival (title: "Arrival", purpose: setup) {
    text: { The buried key pulses beneath Mara's hand. };
    enter investigation -> departure;
  }

  block departure (title: "Departure", purpose: resolution) {
    text: { With the signal understood, Mara turns home. };
    end;
  }
}

module md_decode (
  title: "Decode the key",
  pool: investigation,
  entry: decode,
  requires: [st_at_gate],
  forbids: [st_knows_signal]
) {
  scene decoding (title: "The buried code") {
    block decode (title: "Decode", purpose: development) {
      text: { The light resolves into a patient sequence. };
      complete (
        add: [st_knows_signal],
        set: [st_in_garden]
      );
    }
  }
}

At arrival, st_knows_signal is inactive, so the Pool cannot exit. md_decode is eligible because Mara is at the gate and does not yet know the signal. Completing it activates the flag, changes the location group, and returns to the Pool. The exit now matches, so reading resumes at departure.

Runtime validity rules

Full CYOA validation checks more than source references:

  • main-story and every Module-local Block graph are acyclic;
  • all flow targets stay within their owner;
  • every Pool has one entry and a reachable exit;
  • every reachable nonterminal configuration can eventually reach an ending;
  • no reachable configuration is soft-locked;
  • runtime transitions do not cycle;
  • all reader-selectable Module orders are valid, not only source order;
  • a never-eligible Module produces a warning;
  • TypeApe must be able to finish checking every possible route.

If a story becomes too complex to check completely, Validate stops with a diagnostic instead of presenting an incomplete result.

Do not simulate counters, repeatable encounters, random scheduling, reversible travel, or hidden accumulation with x_ metadata. Those capabilities are outside Story DSL v1.

Next: validation and diagnostics.