OpenSpire is an AI-native turn-based runtime that completely separates rules from engine. Cards, enemies, statuses — every mechanic lives in hot-pluggable Lua scripts. Slay the Spire is just the bundled demo. OpenSpire 是一个 AI 原生的回合制运行时,将规则与引擎彻底分离。 卡牌、敌人、状态——所有机制都活在热插拔 Lua 脚本里。 以热门肉鸽游戏《杀戮尖塔》为内置演示。
The Engine, Not the Game 是引擎,不是游戏
Most “game engines” bundle rules with runtime. OpenSpire inverts that. The core runtime knows nothing about cards or statuses — it only understands events, state, and pipelines.
大多数“游戏引擎”把规则和运行时耦合在一起,OpenSpire 反过来。 核心运行时对卡牌和状态一无所知—— 它只理解事件、状态和流水线。
The STS ruleset is just a Lua-defined layer on top. Swap it out for a TRPG, a tactics game, or a poker variant — the engine doesn't care.
STS 规则集只是叠在上面的 Lua 数据层。 换成 TRPG、战棋、扑克变体——底层引擎无需任何改动。
Because all logic is data, AI can read it, write it, and balance it without touching source code.
因为所有逻辑都是数据,AI 可以直接读、写、调整数值平衡, 无需触碰任何源代码。
Why OpenSpire为什么选 OpenSpire
Six design decisions that set OpenSpire apart from game frameworks.六个设计决策,让 OpenSpire 有别于传统游戏框架。
Every rule is a Lua string. AI agents can read the full ruleset, generate new content, and fix balance — all without touching engine source code.每条规则都是 Lua 字符串。AI 可以读取完整规则集、生成新内容、修复数值平衡——无需碰引擎源码。
Built-in JSON/stdio interface. Every player action and game state transition is expressible as a structured command — perfect for programmatic control or AI players.内置 JSON/stdio 接口。每个玩家动作和状态转换都可表达为结构化命令——天然支持程序化控制和 AI 玩家。
Add new cards, enemies, and statuses by defining data objects with embedded Lua scripts. The registry rejects duplicates, wires event handlers automatically, and fails fast on any conflict.通过定义 JS 对象(内嵌 Lua 脚本)添加新 def。引擎自动拒绝重复 id 和重复规则,写错即崩溃,快速失败保证规则集的一致性。
All game actions flow through a priority-ordered event pipeline. Reactions, counters, and interrupts all resolve in a consistent, auditable order. No hidden state.所有游戏动作流经按 order 字段排序的事件管道。反应、反制、中断全部深度优先同步结算,过程可通过 Bundle 完整审计。
The core scheduler is fully synchronous. No async surprises mid-combat. Turn phases, event handlers, and status ticks all run in a predictable deterministic loop.Scheduler 完全同步且深度优先。内层 State.emit 会立即递归运行至完成再返回,没有异步回调和队列延迟。回合阶段与事件因果链完全可预测。
The engine has zero STS-specific code. Build a TRPG, a deckbuilder, a tactics game, or a poker variant — the same core handles any turn-based event model.引擎没有任何 STS 专有代码。构建 TRPG、卡组构筑、战棋或扑克变体——同一套状态树与事件管道即可承载。
How It Works工作原理
Every game action is an event dispatched through a priority-ordered handler chain.每个游戏动作都是一个事件,流经按优先级排序的处理器链。
State.emit 派发一个命名事件,附带本次上下文参数order 字段排序,形成本次管道State.emit 时嵌套管道深度优先同步跑完State.set 写入状态树,每次变更记录为 PatchState.bind 将 def(卡牌/状态/敌人)绑定到运行时实例,注册其 hooks;解绑时自动注销State.emit runs depth-first and synchronously: the inner pipeline completes before the outer handler resumes. The same handler cannot recursively re-enter itself within the same dispatch.
嵌套 State.emit 深度优先同步执行:内层管道跑完后,外层处理器才继续。同一处理器在同一独立分发中不会递归进入自身。
API
Everything from a card to a boss shares one shape: an id, some metadata, and Lua hooks. All logic enters the event pipeline. Enemies usually act through enemy:action, refresh intent through enemy:update, and react to damage through shared facts like entity:loss.从一张卡牌到一个 Boss,共用同一种结构:id、元数据,以及 Lua hooks。所有逻辑都通过事件管线驱动。敌人通常用 enemy:action 执行行动,用 enemy:update 刷新 intent,用 entity:loss 这类通用事实事件响应受伤。
// Cards usually provide event hooks that participate in the shared pipeline. export const bash = { id: 'bash', cost: 2, targetType: 'enemy', display: { name: 'Bash重击', type: 'attack', desc: 'Deal 8 damage. Apply 2 Vulnerable.造成 8 点伤害。施加 2 层易伤。' }, hooks: { 'event:card:effect': ` State.emit('entity:attack', { target = Event.target, amount = 8, source = 'player' }) State.emit('status:apply', { target = Event.target, typeId = 'vulnerable', stacks = 2 }) ` } };
// Statuses listen to events with ordering. Use match: to filter by payload field. export const metallicize = { id: 'metallicize', display: { name: 'Metallicize金属化', desc: 'Gain Block at the end of each turn equal to stacks.每回合结束获得等同层数的格挡。' }, hooks: { 'event:actor:turn:end': { order: 400, match: { target: 'self' }, script: ` local stacks = State.get('entities', Ctx.self, 'statuses', 'metallicize', 'stacks') or 0 State.emit('entity:block', { target = Ctx.self, amount = stacks }) ` } } };
// Enemies keep intent display in actions, then use enemy:action / enemy:update to run logic. export const cultist = { id: 'cultist', display: { name: 'Cultist狂信者' }, actions: { incantation: { type: 'buff', desc: 'Apply 3 Ritual.施加 3 层仪式。' }, dark_strike: { type: 'attack', desc: 'Deal 6 damage.造成 6 点伤害。' }, }, hooks: { 'event:enemy:action': { match: { target: 'self' }, script: ` local a = Event.action if a == 'incantation' then State.emit('status:apply', { target = Ctx.self, typeId = 'ritual', stacks = 3 }) elseif a == 'dark_strike' then State.emit('entity:attack', { target = 'player', amount = 6, source = Ctx.self, action = a }) end ` }, 'event:enemy:update': { match: { target: 'self' }, script: ` if Event.cause == 'init' then State.set('entities', Ctx.self, 'intent', 'incantation') else State.set('entities', Ctx.self, 'intent', 'dark_strike') end ` }, } };