Lattice 语法参考 · 第 3 章,共 4 章
CYOA 选择与有限 State
CYOA 支持显式路径选择和可选的有限 State。TypeApe 可以在你把故事交给读者之前,检查每一种可能路线。
不使用 State 的 CYOA
只有分支的 CYOA 可以使用 next、choice 和 end,而不声明任何 State、Pool 或 Module。
自动继续
next -> target_block;目标必须留在同一所有者内:主故事或当前 Module。
玩家选择
choice {
option inspect_key: { 检查钥匙 } -> inspect;
option leave_garden: { 离开花园 } -> departure;
}一条 choice:
- 至少包含两个
option声明; - 每个 option 在这条 choice 内有唯一 ID;
- 使用正文作为面向读者的标签;
- 指向同一所有者内已经存在的 Block;
- 外层
}之后不加分号。
Option ID 标识这条 choice 内的动作,它不是 State。选择 option 会改变路径,但不会持久记录“选过什么”。
结局
end;end 终止整个故事。在 Module 内也是如此;Module 如果应该把控制权交还 Pool,需要使用 complete。
分支图规则
对没有 Pool 或 Module 的 CYOA:
- Story 入口存在于主故事中,而且没有入边;
- 所有 Block 都能从入口到达;
- 路径无环;
- 至少存在一个结局;
- 分支可以合流到共享 Block;
- 每条可达路径都必须终止。
合流是它与线性故事的关键区别:几个不同选择可以指向同一个后续 Block。
有限状态边界
Lattice State 是封闭世界,并且完全来自声明。它有意不支持变量、计数器、算术、脚本、函数、递归、随机值、动态 State 或外部输入。
只有当可用性或 Pool 退出条件必须记住一个具名有限事实时,才使用 State。普通路径拓扑应该继续使用普通 choice。
Predicate 声明
Predicate 声明元组模式中的实体类型:
predicate knows (
subject: character,
object: item
);
predicate at (
subject: character,
place: location
);subject 必填,object 和 place 是可选模式字段。每个字段类型都是 character、location 或 item。
Predicate 不是布尔变量,也不会推理。它定义使用该 Predicate 的落地 State 必须遵循的精确形态。
落地 State 声明
State 为一个 Predicate 提供完全落地的元组,并且必须有标题:
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 定义一个有限插槽:
state group location (
members: [st_at_gate, st_in_garden]
);members 必填且不能为空。Group 不能重叠。运行时 State 在每个 group 中恰好激活一个成员。
Story 选择初始值:
story main (
title: "信号花园",
language: "zh-CN",
format: novel,
entry: arrival,
initial_state: [st_at_gate]
);initial_state 必须从每个 State Group 中恰好选择一个成员。没有列出的不分组 flag 初始为未激活。只有在没有 group 成员必须选择时,空列表才有效。
Guard
Module 使用 requires、requires_any 和 forbids。Pool 使用对应的退出字段 exit_requires、exit_requires_any 和 exit_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 是从主故事进入的有限状态检查点:
pool investigation (
title: "调查花园",
exit_requires: [st_knows_signal]
);title 必填。每个 Pool 恰好有一个可达的主故事 enter:
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 严格按以下顺序处理:
- 计算 Pool 退出 Guard。
- 如果成立,在
enter记录的 Block 恢复。 - 否则,按稳定导入/声明顺序列出所有符合条件且未完成的 Module。
- 让读者选择一个 Module。
- 如果退出为假且没有 Module 可用,报告软锁。
退出优先于 Module 可用性。Module 顺序只是展示顺序,不是自动调度器或语义优先级。
Floating Module
Module 属于一个 Pool,并拥有嵌套的 Scene 和 Block。
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 专用终结语句:
complete (
add: [st_knows_signal],
set: [st_in_garden]
);它会原子地:
- 激活
add中所有不分组 flag; - 选择
set中每个分组 State,替换该 group 之前的成员; - 把当前 Module 标记为已完成;
- 把控制权交还所属 Pool;
- 重新计算 Pool 退出与可用性规则。
add 只能用于不分组的永久 flag;set 只能用于分组 State。一次 completion 对每个 group 最多设置一个成员。没有 remove、toggle,也不会在 flag 和 group 成员之间隐式转换。
整个 completion 应用完成后才会重新计算 Guard,因此 Guard 不会观察到只更新一半的 State。
完整 Pool/Module 示例
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。
下一章:校验与诊断。
