Turn-Based Event Orchestration Engine 回合制事件编排引擎

Model Any Turn-Based
Game. No Engine Code
Required.
模拟任意回合制游戏,
无需修改
引擎代码。

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 脚本里。 以热门肉鸽游戏《杀戮尖塔》为内置演示。

openspire — iron_plague scenario
== Slay the Spire == Turn 3 Draw4 Disc3 Exh1
Player
HP ████████████  68/75
Energy  1/3   Block 5
Plague Mage [slot 1]
HP ████████████  110/130
[DEB]  Apply 5 Poison.
Iron Golem [slot 2]
HP ████████████  150/200
[ATK]  Deal 20 damage.
[Thorns×8] [Metallicize×5] [Vulnerable×2]
Hand (3)
[1]  (1)  ATK  Cleave  Deal 8 damage to ALL enemies.
[2]  (1)  PWR  Rupture  Whenever you lose HP directly, gain 1 Strength.  [Exhaust]
[3]  (0)  SKL  Offering  Lose 6 HP. Gain 3 Energy. Draw 3 cards.  [Exhaust]
Battle Log
 └─ Draw: Bash
 └─ Draw: Cleave
 └─ Draw: Rupture
 └─ Draw: Offering
▷ Play: Defend
  Player gains 13 block
▷ Play: Bash
  Player deals 8 damage to Iron Golem
   └─ Iron Golem gains 2 Vulnerable
  Thorns: Iron Golem deals 8 damage to Player (blocked 8)
   └─ Player block 13 → 5
[1-3] play [e] end turn [u] undo [s] save [i] dict [q] quit

One Runtime.
Any Turn-Based 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 可以直接读、写、调整数值平衡, 无需触碰任何源代码。

⚙️
evt/core/
Event pipeline · State · Lua runtime · Registry
事件流水线 · 状态管理 · Lua 运行时 · 注册中心
GENERIC
🎮
evt/game/
Session orchestration · CLI / JSON adapter
场次编排 · CLI / JSON 适配器
GENERIC
🃏
evt/sts/
Cards · Enemies · Statuses · STS ruleset
卡牌 · 敌人 · 状态 · STS 规则集
RULESET
🖥️
ui/
Ink terminal UI · replaceable with any frontend
Ink 终端 UI · 可替换任意前端
SHELL

Built for Rules, Not Just Games为规则而生,不只是游戏

Six design decisions that set OpenSpire apart from game frameworks.六个设计决策,让 OpenSpire 有别于传统游戏框架。

🤖

AI-Native by Design为 AI 而生

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 可以读取完整规则集、生成新内容、修复数值平衡——无需碰引擎源码。

📡

Programmable Actions动作 CLI 化

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 玩家。

🔌

Hot-Pluggable Rules规则热插拔

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 和重复规则,写错即崩溃,快速失败保证规则集的一致性。

🔄

Deterministic Pipeline确定性事件管道

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 完整审计。

⚡

Synchronous Runtime同步深度优先运行时

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 会立即递归运行至完成再返回,没有异步回调和队列延迟。回合阶段与事件因果链完全可预测。

🧩

Any Turn-Based Ruleset适配任意回合制规则集

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、卡组构筑、战棋或扑克变体——同一套状态树与事件管道即可承载。

The Event Pipeline事件流水线

Every game action is an event dispatched through a priority-ordered handler chain.每个游戏动作都是一个事件,流经按优先级排序的处理器链。

01
emit()
Player or rule dispatches a named event with a payload玩家或规则调用 State.emit 派发一个命名事件,附带本次上下文参数
02
Scheduler
Collects all registered handlers sorted by priority快照当前所有已注册处理器,按 order 字段排序,形成本次管道
03
Lua Script
Each handler runs its Lua logic — may emit child events每个处理器在 Lua 沙盒中执行脚本;内部调用 State.emit 时嵌套管道深度优先同步跑完
04
State.set
State mutations are applied through the path-based store处理器通过路径段调用 State.set 写入状态树,每次变更记录为 Patch
05
State.bind
When a def is bound (status applied, card enters hand), its event hooks are registered; when unbound, they are removedState.bind 将 def(卡牌/状态/敌人)绑定到运行时实例,注册其 hooks;解绑时自动注销
06
View
Presenter reads final state and renders the updated UIPresenter 读取最终状态树快照,构建 Bundle 并推送给 CLI / UI 层渲染
Nested 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 深度优先同步执行:内层管道跑完后,外层处理器才继续。同一处理器在同一独立分发中不会递归进入自身。

Rules Are Just Data规则即数据

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 这类通用事实事件响应受伤。

evt/sts/cards/ironclad.js
// 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
    ` },
  }
};