dse-core · 字典场景引擎(Python 版)
Dictionary Scene Engine —— 场景即字典,能力即插件。 本仓库是引擎的 Python 原生实现(cordis-py),Node/Cordis 版见姊妹仓库 dse-core。立项计划书见 PLAN.md。
PyPI:https://pypi.org/project/dse-core/ | 发行名 dse-core / 导入名 dse_core
(连字符 vs 下划线;CLI 命令是 dse。⚠ 别和 Gitee 上的 Node 版仓库 siwpan/dse-core 弄混 ——
PyPI 上的 dse-core 就是本仓,见 pyproject.toml 的 name。)
版本已对齐(2026-10-20):PyPI 最新 =
pyproject.toml=dse_core.__version__= 0.5.0。 此前存在漂移 —— 0.5.0 上传时只改了工作树版本号、未随提交入库,仓库一度停在 0.4.0; 已按分发包比对结果补发版提交对齐(详见 PLAN.md §7.4)。
30 秒上手
pip install dse-core # 从 PyPI 安装(开发安装见文末)
dse play # 玩自带示例内容包(战斗线最小可玩版)
dse play --input "1,1,a,a,a" # 脚本模式:无交互跑完整条线路(CI 友好)
dse scenes # 列出场景
dse packs # 列出内容包(manifest / 依赖声明)
dse check # 静态校验内容(Schema / 悬空跳转),有问题退出码 1
dse traverse # 动态遍历:真的跑引擎走遍全图,执行每条 goto 边/效果/条件
dse coverage # 结局覆盖门:每个结局须有 traverse(T) 或 e2e(E) 来源,双缺退出码 1
dse doctor # 体检 + 内容模式 vs 已装插件交叉核对
dse saves # 离线列出存档位
dse new my-pack --template hub # 脚手架生成 YAML 内容包并挂进 dse.yml
dse graph --format mermaid # P7:导出场景图(Mermaid / DOT,Twine 图谱的 CLI 对应物)
dse test --script "1,1,a" --expect-scene end-rich --expect "gold >= 10" # P7:脚本化回归(CI 友好)
dse export-html -o game.html # P7:导出单文件 HTML 播放器(normal 模式工程,双击即玩)
自带示例工程随包发布(src/dse_core/example/):在含 dse.yml 的目录里
运行 dse play 玩你自己的工程,否则玩包内示例。
两道内容门:check 管结构,traverse 管跑得通
dse check 是静态的(毫秒级):看 Schema、看悬空跳转、看图的连通性,不执行任何条件或效果。
dse traverse 是动态的(秒级):真的把引擎跑起来,在每个场景用当时的真实状态求值条件、
执行效果、跟随实际跳转。两者抓的问题不重叠:
| 问题 | dse check |
dse traverse |
|---|---|---|
| 悬空跳转、Schema 不符 | ✅ | — |
| 条件表达式写炸(求值抛异常) | ❌ | ✅ |
| 选项条件恒假,场景实际是死路 | ❌(结构上边是通的) | ✅ |
| 新加了结局但没有任何路径能到 | ❌ | ✅(配合 dse coverage) |
dse traverse --json # 结构化输出,便于 CI 解析
dse coverage --e2e end-secret # 声明某个结局已由 e2e 用例覆盖(可重复)
traverse 会从 entry 做 DFS,用 state.snapshot()/restore() 隔离分支,结束后还原现场 ——
只读操作,不留副作用。时间循环内容(@loop)靠 --max-revisit(缺省 3)与
--max-steps(缺省 500)收敛,撞限的分支会如实报「截断」而不是静默跳过。
内容包 = 数据,不是代码
与 Node 版最大的差异:内容包是 YAML 数据文件,不是可执行模块。
条件表达式经 AST 白名单求值(仅比较/布尔/算术/下标,禁调用、禁下划线属性)——
不需要 import 任何用户代码,也没有 new Function。
# content/starter.yml(节选)
pack:
id: starter
name: 启程
entry: start
scenes:
start:
title: 村口 · 启程
text: '天没亮透……三条路摆在你面前。'
choices:
- { text: '往北进山', do: { 'flags.starter.mountain': true }, goto: forest }
forest:
text: '……一只独眼狼从阴影里站起来。'
choices:
- { text: '拔剑', goto: wolf-fight }
wolf-fight:
mode: battle # 非 normal 模式由能力插件接管
battle: { enemy: wolf, onWin: forest-win, onLose: forest-lose, onFlee: start }
P6 叙事增强(对标 Ink/Twine):
scenes:
crossroad:
text: '两条路摆在你面前。'
onEnter: { visits: +1 } # 进入时执行(已有)
onExit: { gold: +10 } # P6:离开场景时结算,先于新场景 onEnter
extraText: # P6:条件文本段,求值为真才拼接
- if: 'flags.met_wolf'
text: '你认得林间那道爪痕。'
choices:
- text: 掷命运硬币 # P6:加权随机跳转
goto:
- { to: lucky, weight: 1 }
- { to: unlucky, weight: 3 }
加权随机的确定性来源:dse.yml 里 engine.config.seed 固定后结果可复现
(测试 / CI 友好);缺省真随机。rng 状态不入存档,读档后重新洗牌。
P8 叙事结构(对标 Ink tunnels / 一次性选项 / 访问计数):
scenes:
plaza:
text: '这是你第 {visits.plaza} 次站在这里。' # visits 内建计数,先于 onEnter
choices:
- text: 逛商店
goto: { call: shop } # tunnels:压栈跳转,可复用内容块
- text: 看一眼告示牌
once: true # 选过即消失;once: <key> 自定义稳定键(推荐)
do: { gold: +1, emit: notice/read } # emit:事件桥接宿主插件,不写状态
goto: plaza
shop:
text: '老板:随便看。'
choices:
- { text: 离开商店, goto: '@return' } # 弹栈返回调用点;空栈响亮报错
once 记录于 state.onceSeen,随存档持久化;@restart / @loop: 会清空
调用栈与 once 记录(全量重置语义)。
P9 体验与数据(文本变体 / 回滚 / 埋点分析):
scenes:
hall:
text: '兜底文本'
variants: ['大厅空荡荡的。', '大厅里点起了灯。', '大厅传来回声。']
variantMode: cycle # 按 visits 轮转;shuffle = 随机(seed 可复现)
- rollback:游戏内输入
rollback [n](别名rb/back)回退 n 个选择点。 历史快照栈上限engine.config.maxHistory(缺省 50);语义是时间回溯 (重进场景会再跑 visits/onEnter,净效果与「未曾离开」一致);历史属会话, 读档后清空。导出的 HTML 播放器自带「↩ 回滚一步」按钮。 - telemetry 埋点:dse.yml 加
- id: telemetry后,游玩过程的事件 (会话/进场景/选项/结局)JSONL 落盘telemetry.jsonl(限工程目录内,DSE_TELEMETRY=0一键关闭);dse analyze输出场景访问 Top、选项分布 百分比、结局到达率 —— 独立作者的「选择埋点 → 迭代闭环」。
P10 运行时深水区(对齐 Cordis rc.10 机制):
child = ctx.isolate("state", "scenes") # 隔离域:列出服务独立注册
child.plugin(StateService, {...}) # 同名不同实例,互不干扰
child.start() # 未隔离服务沿父链解析;事件总线全局共享
- ctx.isolate:服务隔离域(测试隔离 / 多会话并存);隔离名单内的服务
不向上穿透(未注册时响亮 AttributeError),
ctx.inspect()聚合子域 fiber 并标注 realm 深度 - 批量热更:
scenes.reload_many(rels)—— 防抖窗口内多文件变更只 rebuild 一次;组合校验失败整体回滚,不留「一半新一半旧」中间态 - dse i18n-extract:扫场景文本生成
i18n/<locale>.yml待翻译表 (key =<场景>.<字段>[.<序号>]);增量合并保留已译值,下线 key 进注释块 不静默丢翻译;已用@key@接管的内容自动跳过
架构
dse_core/
├── modes.py # 模式注册表(scene.mode 唯一真源)
├── graph.py # 场景图静态分析(可达性/悬空/结局/最短路径,纯函数)
├── traverse.py # 场景图动态遍历(真跑引擎:DFS + 状态隔离 + 结局覆盖门)
├── manifest.py # 内容包元数据(规范化/校验)
├── loader.py # 工程加载(dse.yml + YAML 内容包合并)
├── schema.py # JSON Schema 校验(schema/scene.schema.json)
├── safety.py # 条件表达式 AST 白名单求值
├── runtime.py # 插件运行时(Service/Context/依赖拓扑/事件)—— Cordis 的 Python 原生替代
├── sdk.py # 编程式门面(create_engine / play / inspect_project)
├── cli.py # 命令行 dse:play / scenes / info / check / traverse / coverage / packs / saves / doctor / new
└── plugins/ # 内核层 scenes/state/console_io/engine
# 能力层 combat/trade/diary/dialogue(各接管一种 scene.mode)
# 系统层 save/save-migrate/achievements/i18n
# 表现层 sound(程序化合成,静音/清理)· 开发层 hot-reload(轮询热更)
插件用声明式依赖组装,引擎零改动即可扩展:
from dse_core.runtime import Service
class MyPlugin(Service):
name = "my-plugin"
inject = ("state", "io") # 等价于 Cordis 的 static inject
config_schema = { # P5:配置校验(required/properties,缺键响亮报错)
"properties": {"level": {"type": "int", "default": 1}}
}
def setup(self):
self.on("scene/enter", lambda scene_id, scene: ...) # P5:随 dispose 自动摘除
self.effect(lambda: open_resource()) # P5:返回的 disposer 随 dispose 自动回滚
运行时增强(P5,对齐 Cordis 4.0):
ctx.effect()/self.on()—— 可逆副作用原语:插件卸载时 disposer 自动逆序 回滚、监听器自动摘除(热重载不再累积残留)- fiber 生命周期状态机(PENDING → LOADING → ACTIVE / FAILED → DISPOSED),
ctx.inspect()快照巡检;dse doctor静态输出插件拓扑启动顺序 - 事件分发五模式齐备:
emit(观察)/parallel(容错扇出)/serial(遇错中断)/bail(委托)/pipeline(即 waterfall,返回值链)
系统层插件(P2,dse.yml 里按 id 启用):
| 插件 | 能力 |
|---|---|
save |
save/load/slots 命令;slot 白名单 + 原子落盘 + 读档结构校验 + 场景改名兜底 |
save-migrate |
存档版本链迁移(config.migrations 挂模块,旧档不失效) |
achievements |
事件驱动成就解锁(scene/enter + choice/made),随档持久化 |
i18n |
@key@ 查表替换(未命中原样保留)、lang 命令切换语言 |
与 Node 版的关系
- Node 版持续维护;本仓库是移植版,内容格式不互通(YAML ↔ ESM)。
- 移植路线图:P1 可发布最小引擎 ✅ → P2 系统层与完整 CLI ✅ → P3 能力层 ✅ → P4 表现/开发层 ✅(sound/hot-reload;Node→YAML 迁移工具随 dict-scene-engine 重构交付)。详见 PLAN.md。
- 已知边界:内容用了某模式但没装对应插件时,引擎响亮报错(而非 Node 版的静默回退)——
dse doctor可静态交叉核对。
安全模型
- 内容包是数据;表达式求值禁调用与下划线属性访问。
dse.yml与内容包路径均限制在工程目录内(路径白名单)。
License
MIT · Copyright (c) 2026 全哥 (siwpan)
Metadata
Release files for dse-core 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dse_core-0.6.0.tar.gz | 112.6 kB | Without provenance | Details for dse_core-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dse_core-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for dse_core-0.6.0-py3-none-any.whl |
Total release size: 213.9 kB
Release files / dse_core-0.6.0.tar.gz
| Download URL | dse_core-0.6.0.tar.gz |
|---|---|
| Size | 112.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6e1b7d94a0252b5bcf2e2cb0d7d3ca998413c22abbc132d617a0cf52f09aeabf
|
|
BLAKE2b-256 checksum How to use checksums |
031ea55ba98ac980e42a86b0fd1c04a07606d0af2ff950ff31df1b7bf1258fdb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / dse_core-0.6.0-py3-none-any.whl
| Download URL | dse_core-0.6.0-py3-none-any.whl |
|---|---|
| Size | 101.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ffc86d43730bd54dc3ef749a3fcc304207b09aa43b20f60a688c5a193c645f78
|
|
BLAKE2b-256 checksum How to use checksums |
d9efdd432f0ae8dd87431e40f919bb6b1b01df2173856b7e61ef7a4bb16dc187
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for dse-core 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.