@bct-app/game-engine
v0.3.6
Published
Game engine utilities for BCT
Readme
@bct-app/game-engine
血染钟楼(BCT)的规则引擎:从操作记录重放出游戏状态,并在配置一次发动时回答「能不能发动、候选是谁、谁会拦截、谁会响应」。 服务端、管理端、玩家端和小程序共用同一套结算逻辑。
安装
pnpm add @bct-app/game-engine运行时只依赖 @bct-app/game-dsl(条件求值),对 @bct-app/game-model 只用类型;不依赖 Node、DOM 或任何平台 API。
同时提供 ESM 与 CommonJS 入口。
核心约定
- 操作是唯一的真相来源:玩家状态、事件、光环全部由重放操作推导(
deriveState),引擎不持有可变状态。TGameLibrary(能力与角色定义)是唯一的例外,它是定义不是状态。 - 判定在提交时冻结:失能(
impairedBy)、仅宣称(claimOnly)、拦截是否放弃(declined)都在提交那一刻判定、写进操作, 重放时只跟随记录,不重新判定。之后改动更早的操作,用auditDecisions找出不再成立的判定。 - 两种效果:
TRIGGERED是落地即成事实的变更;CONTINUOUS每次推导按存续条件重新判断(投毒者死了,毒自然消失)。 - 认定分两种口径:候选、建议、匹配按「可能被认定为」(MAY)算,结算只认真实身份(STRICT),
definite的规则两边都算。 - 说书人:发动者座位
< 1表示说书人。场外角色(小怪宝、瘟疫医生让说书人获得的爪牙)的能力也由说书人执行, 拦截和事件里的「来源角色」仍是那个角色。说书人还可以裁定任何一份效果不生效(以拦截的形式记录,isStorytellerRuling)。
快速上手
import {
createLibrary, deriveState, canInvokeAbility, resolveCandidates, validateSelections,
planOutputInterceptions, findEventReactions, findPhaseEndReactions,
} from '@bct-app/game-engine';
const library = createLibrary(abilities, characters); // 能力与带能力的角色
const state = deriveState({ initialPlayers, timelines, library, offTableCharacters });
// 按「那一刻」展示或编辑每条记录:一次重放,按需取每条顶层操作之前的盘面
// before(k, i) 等同于 deriveState 重放 timelinesBefore(timelines, k, i)
const moments = deriveMoments({ initialPlayers, timelines, library, offTableCharacters });
moments.before(timelineIndex, operationIndex);
// 能不能发动(死亡、次数、首夜、发动条件)
canInvokeAbility({ abilityId, effectorSeat, state, timelines, library });
// 某个输入的候选(玩家座位或角色 id),以及提交前的校验
resolveCandidates({ input, effectorSeat, state, timelines, library, selections });
validateSelections({ abilityId, branchId, selections, effectorSeat, state, timelines, library });
// 提交一次操作前:哪些产出会被谁拦截
const { outputs } = planOutputInterceptions(operation, { state, timelines, library });
// 提交后:新产生的事件有哪些能力要响应
for (const event of state.events) findEventReactions(event, { state, timelines, library });
// 推进到下一个时段前:阶段结束时自动判定的能力(镇长、涡流)
findPhaseEndReactions({ state, timelines, library });导出一览
| 分类 | 主要导出 |
| --- | --- |
| 库与推导 | createLibrary、findBranch、abilityInputs、needsPrompt、deriveState、deriveMoments、timelinesBefore、eventTypesOf、applyEffect |
| 发动与候选 | canInvokeAbility、isClaimOnly、prefillSelections、resolveCandidates、resolveRegistrationOnlyCandidates、validateSelections、resolveReferences |
| 拦截与事件 | planOutputInterceptions、buildProposal、findInterceptors、findEventReactions、findPhaseEndReactions、findImpairments、findInvocationImpairment |
| 审计与裁定 | auditDecisions、isStorytellerRuling、findRulingAbility |
| 条件上下文 | buildBaseContext、withInputs、decoratePlayer、evaluateCondition |
| 提名与处决 | nominationVoteCount、votesRequired、executionBlock、findExecutionAbilities、isExecutionBranch |
| 提示与展示 | activeHints、hintsForInvocation、displayReminders、grimoireSnapshot、describeGrimoire、describeOperation |
| 夜序 | resolveNightOrder、toNightOrderEntries、nightAbilityIds、normalizeCharacterKey |
| 工具 | heldAbilities、isEffectivelyAlive、offTableOwner、sourceCharacterOf、toSeatArray、toIdArray、isFirstNight |
完整列表见 src/index.ts,参数与返回值以类型声明和源码注释为准。
条件能读什么
条件用 @bct-app/game-dsl 求值,上下文由引擎构建。所有条件都能读:
effector:被求值能力的持有者(拦截时是拦截方,不是发起方)players:全部玩家快照(含characterKind、registrable、impaired、effectiveAlive、邻座等)global:存活人数、今天处决次数等currentTimeline、operations(all/today/tonight/yesterdayDay/yesterdayNight)
按场景额外有:inputs(本次选择)、invocation(本次发动是否仅宣称)、proposal(拦截时被拦的变更)、
event(事件响应时的事件)、candidate(候选过滤时的候选)、source 与 cast(持续效果的施放者与施放时机)、
grantedBy(认定规则所在状态的授予者)。
deriveState 重放时,每条操作按它发生那一刻的记录结算:currentTimeline、operations、global 只包含之前的时段
和所在时段里截至该顶层操作(含其拦截与事件响应)的记录,之后才记的操作看不到。所以只重放到某一刻和重放整局,
这条操作的结算结果一致。持续效果的存续条件与被动光环是在重放结束后按当下判定的,读的是整局记录。
开发
pnpm exec turbo run build --filter=@bct-app/game-engine... # 连同 game-model、game-dsl 一起构建
pnpm --filter @bct-app/game-engine test:run规则建模的约定见仓库根目录 AGENTS.md 的「领域模型与架构修改」:新增角色能力优先用通用能力模型表达,不写角色专属逻辑。
