跳到主要内容

Lattice 语法参考 · 第 3 章,共 4 章

CYOA 选择与有限 State

CYOA 支持显式路径选择和可选的有限 State。TypeApe 可以在你把故事交给读者之前,检查每一种可能路线。

不使用 State 的 CYOA

只有分支的 CYOA 可以使用 nextchoiceend,而不声明任何 State、Pool 或 Module。

自动继续

text
next -> target_block;

目标必须留在同一所有者内:主故事或当前 Module。

玩家选择

text
choice {
  option inspect_key: { 检查钥匙 } -> inspect;
  option leave_garden: { 离开花园 } -> departure;
}

一条 choice

  • 至少包含两个 option 声明;
  • 每个 option 在这条 choice 内有唯一 ID;
  • 使用正文作为面向读者的标签;
  • 指向同一所有者内已经存在的 Block;
  • 外层 } 之后不加分号。

Option ID 标识这条 choice 内的动作,它不是 State。选择 option 会改变路径,但不会持久记录“选过什么”。

结局

text
end;

end 终止整个故事。在 Module 内也是如此;Module 如果应该把控制权交还 Pool,需要使用 complete

分支图规则

对没有 Pool 或 Module 的 CYOA:

  • Story 入口存在于主故事中,而且没有入边;
  • 所有 Block 都能从入口到达;
  • 路径无环;
  • 至少存在一个结局;
  • 分支可以合流到共享 Block;
  • 每条可达路径都必须终止。

合流是它与线性故事的关键区别:几个不同选择可以指向同一个后续 Block。

有限状态边界

Lattice State 是封闭世界,并且完全来自声明。它有意不支持变量、计数器、算术、脚本、函数、递归、随机值、动态 State 或外部输入。

只有当可用性或 Pool 退出条件必须记住一个具名有限事实时,才使用 State。普通路径拓扑应该继续使用普通 choice

Predicate 声明

Predicate 声明元组模式中的实体类型:

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

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

subject 必填,objectplace 是可选模式字段。每个字段类型都是 characterlocationitem

Predicate 不是布尔变量,也不会推理。它定义使用该 Predicate 的落地 State 必须遵循的精确形态。

落地 State 声明

State 为一个 Predicate 提供完全落地的元组,并且必须有标题:

text
state st_knows_signal (
  title: "已经理解信号"
): event {
  subject: ch_mara,
  object: it_key,
  predicate: knows
};

state st_at_gate (
  title: "位于大门"
): event {
  subject: ch_mara,
  place: loc_gate,
  predicate: at
};

State 校验要求:

  • Predicate 模式要求的字段全部存在;
  • 不得出现 Predicate 模式没有声明的字段;
  • 每个实体引用都符合声明的类型;
  • predicate 引用正在落地的模式;
  • 可选 time 是一个精确的有限字面量;
  • 两个 State ID 不能表示同一个落地元组。

省略可选字段代表“不存在”,不是“任意值”。Predicate 不会推导不同 State 元组之间的关系。Guard 始终直接引用 State ID。

Flag 与 State Group

不分组的 Flag

不属于任何 group 的 State 是单调布尔标记。除非列在 story.initial_state 里,否则它初始未激活;它可以用 add 激活,但没有移除操作。

互斥 Group

State Group 定义一个有限插槽:

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

members 必填且不能为空。Group 不能重叠。运行时 State 在每个 group 中恰好激活一个成员。

Story 选择初始值:

text
story main (
  title: "信号花园",
  language: "zh-CN",
  format: novel,
  entry: arrival,
  initial_state: [st_at_gate]
);

initial_state 必须从每个 State Group 中恰好选择一个成员。没有列出的不分组 flag 初始为未激活。只有在没有 group 成员必须选择时,空列表才有效。

Guard

Module 使用 requiresrequires_anyforbids。Pool 使用对应的退出字段 exit_requiresexit_requires_anyexit_forbids

Guard 字段成立条件
requires: [a, b]列出的 State 全部激活
requires_any: [a, b]列出的 State 至少一个激活
forbids: [a, b]列出的 State 全部未激活

Guard 引用必须存在,也不能重复。同一个 State 不能既要求又禁止。Module 可以没有 Guard;Pool 的退出 Guard 必须在各个 exit 字段中至少包含一个条件。

Guard 只检查当前 State ID。它不能检查正文、叙事 setup 标记、此前的 choice option ID、完成了多少 Module,也不能检查声明顺序,除非一个具名 State 明确记录了相关事实。

Pool

Pool 是从主故事进入的有限状态检查点:

text
pool investigation (
  title: "调查花园",
  exit_requires: [st_knows_signal]
);

title 必填。每个 Pool 恰好有一个可达的主故事 enter

text
scene garden (title: "花园") {
  block arrival (title: "抵达", purpose: setup) {
    text: { 埋藏的钥匙在玛拉手下搏动。 };
    enter investigation -> departure;
  }
}

enter 后的标识符是 Pool,-> 后的标识符是退出 Guard 成立后恢复阅读的主故事 Block。

enter 只允许出现在主故事。Module 不能进入 Pool,Pool 不能嵌套,一个 Pool 也不能有多个入口位置。

Pool 计算顺序

进入 Pool 以及每次 Module 完成后,TypeApe 严格按以下顺序处理:

  1. 计算 Pool 退出 Guard。
  2. 如果成立,在 enter 记录的 Block 恢复。
  3. 否则,按稳定导入/声明顺序列出所有符合条件且未完成的 Module。
  4. 让读者选择一个 Module。
  5. 如果退出为假且没有 Module 可用,报告软锁。

退出优先于 Module 可用性。Module 顺序只是展示顺序,不是自动调度器或语义优先级。

Floating Module

Module 属于一个 Pool,并拥有嵌套的 Scene 和 Block。

text
module md_decode (
  title: "解读钥匙",
  pool: investigation,
  entry: decode,
  requires: [st_at_gate],
  forbids: [st_knows_signal]
) {
  scene decoding (title: "埋藏的编码") {
    block decode (
      title: "解读",
      purpose: development
    ) {
      text: { 光芒逐渐显现为一串耐心的序列。 };
      complete (
        add: [st_knows_signal],
        set: [st_in_garden]
      );
    }
  }
}

Module 必填字段是:

  • title:面向读者的文本;
  • pool:所属 Pool ID;
  • entry:属于该 Module 的 Block。

Guard 字段可选。只有 Guard 成立且从未完成时,Module 才可用。所有 Module 都只能完成一次。

Module 流转只能留在自身内部,不能指向主故事、其他 Module,也不能进入另一个 Pool。

完成 Module 与应用 Effect

complete 是 Module 专用终结语句:

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

它会原子地:

  1. 激活 add 中所有不分组 flag;
  2. 选择 set 中每个分组 State,替换该 group 之前的成员;
  3. 把当前 Module 标记为已完成;
  4. 把控制权交还所属 Pool;
  5. 重新计算 Pool 退出与可用性规则。

add 只能用于不分组的永久 flag;set 只能用于分组 State。一次 completion 对每个 group 最多设置一个成员。没有 removetoggle,也不会在 flag 和 group 成员之间隐式转换。

整个 completion 应用完成后才会重新计算 Guard,因此 Guard 不会观察到只更新一半的 State。

完整 Pool/Module 示例

text
story main (
  title: "信号花园",
  language: "zh-CN",
  format: novel,
  entry: arrival,
  initial_state: [st_at_gate]
);

entity character ch_mara (name: "玛拉", role: protagonist);
entity location loc_gate (name: "花园大门");
entity location loc_garden (name: "信号花园");
entity item it_key (name: "信号钥匙");

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

state st_knows_signal (title: "已经理解信号"): event {
  subject: ch_mara,
  object: it_key,
  predicate: knows
};

state st_at_gate (title: "位于大门"): event {
  subject: ch_mara,
  place: loc_gate,
  predicate: at
};

state st_in_garden (title: "位于花园内部"): event {
  subject: ch_mara,
  place: loc_garden,
  predicate: at
};

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

pool investigation (
  title: "调查信号",
  exit_requires: [st_knows_signal]
);

scene garden (title: "花园", pov: ch_mara) {
  block arrival (title: "抵达", purpose: setup) {
    text: { 埋藏的钥匙在玛拉手下搏动。 };
    enter investigation -> departure;
  }

  block departure (title: "离开", purpose: resolution) {
    text: { 理解信号之后,玛拉转身回家。 };
    end;
  }
}

module md_decode (
  title: "解读钥匙",
  pool: investigation,
  entry: decode,
  requires: [st_at_gate],
  forbids: [st_knows_signal]
) {
  scene decoding (title: "埋藏的编码") {
    block decode (title: "解读", purpose: development) {
      text: { 光芒逐渐显现为一串耐心的序列。 };
      complete (
        add: [st_knows_signal],
        set: [st_in_garden]
      );
    }
  }
}

抵达 arrival 时,st_knows_signal 未激活,所以 Pool 不能退出。玛拉位于大门且还不了解信号,因此 md_decode 可用。完成它会激活 flag、改变 location group,并回到 Pool。此时退出条件成立,所以阅读在 departure 恢复。

运行时有效性规则

完整 CYOA 校验不只检查源码引用:

  • 主故事和每个 Module 内部的 Block 图都无环;
  • 所有流转目标留在自己的所有者内;
  • 每个 Pool 都有一个入口和至少一个可达出口;
  • 每个可达非终止配置最终都能到达结局;
  • 不存在可达软锁;
  • 运行时转移不形成循环;
  • 所有读者可选的 Module 顺序都有效,而不只是源码顺序;
  • 永远不可用的 Module 产生警告;
  • TypeApe 必须能够完成对每一种可能路线的检查。

如果故事复杂到无法完整检查,Validate 会显示诊断并停止,而不会给出不完整的结果。

不要用 x_ 元数据模拟计数器、可重复事件、随机调度、可逆旅行或隐藏累积。这些能力不属于 Story DSL v1。

下一章:校验与诊断