stateUtils
stateUtils 命名空间包含纯函数状态变更辅助方法。每个函数都返回一个全新的 GameState 对象(不可变更新),不会 bump state.version。Action handler 会组合多个此类方法来完成一个 action;GameEngine 负责对每个 action 仅 bump 一次版本。
概述
stateUtils 通过 src/index.ts 以命名空间形式导出:
export * as stateUtils from './core/stateUtils.js'使用方式:
import { stateUtils } from 'decklet'
const next = stateUtils.bumpVersion(state)
const next2 = stateUtils.setVariable(state, 'phase', 'bidding')或直接导入单个函数(如果调用方在内部模块间传递):
import { bumpVersion, setVariable } from 'decklet/core/stateUtils.js'设计要点
- 所有函数都是纯函数:相同输入产出相同输出
- 不修改入参 state,返回新对象
- 不 bump version —— 调用方决定是否 bump
- 通常组合使用:
bumpVersion(setVariable(prev, key, value)) - 严格校验:找不到实体时抛错并保持原 state 不变(Plugin 信赖的状态一致性前提)
函数列表
addCard()
向状态注册一张卡牌。
function addCard<S extends GameState>(state: S, card: Card): S参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
state | S extends GameState | 是 | 当前 state |
card | Card | 是 | 待注册的卡牌 |
返回值
新 GameState(cards 表新增条目)。
抛错
当 card.id 已存在时抛 Error: Card ${card.id} already exists in state,避免覆盖导致数据丢失。
removeCard()
从状态移除一张卡牌。
function removeCard<S extends GameState>(state: S, cardId: string): S注意
本函数只更新 cards 表,不会从任何 zone 的 cards 列表中清理引用。调用方若需要保持一致性,应自行先调用 removeCardFromZone。
抛错
当卡牌不存在时抛 Error: Card ${cardId} not found in state。
addZone()
向状态注册一个 zone。
function addZone<S extends GameState>(state: S, zone: CardZone): S抛错
当 zone.id 已存在时抛 Error: Zone ${zone.id} already exists in state。
removeZone()
从状态移除一个 zone。
function removeZone<S extends GameState>(state: S, zoneId: string): S注意
本函数不校验该 zone 内是否仍持有卡牌引用,调用方应确保已先清空其 cards 列表以避免悬挂引用。
抛错
当 zone 不存在时抛 Error: Zone ${zoneId} not found in state。
addPlayer()
向状态注册一名玩家。
function addPlayer<S extends GameState>(state: S, player: Player): S抛错
当 player.id 已存在时抛 Error: Player ${player.id} already exists in state。
removePlayer()
从状态移除一名玩家。
function removePlayer<S extends GameState>(state: S, playerId: string): S抛错
当玩家不存在时抛 Error: Player ${playerId} not found in state。
updatePlayer()
用纯函数 updater 增量更新一名玩家。适用于仅修改玩家局部字段而不替换整对象的场景,例如状态切换、计分累计。
function updatePlayer<S extends GameState>(
state: S,
playerId: string,
updater: (player: Player) => Player
): S参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
state | S extends GameState | 是 | 当前 state |
playerId | string | 是 | 玩家 id |
updater | (player: Player) => Player | 是 | 增量更新函数 |
抛错
当玩家不存在时抛 Error: Player ${playerId} not found in state。
addCardToZone()
将卡牌 id 追加到指定 zone 的卡牌列表末尾。
function addCardToZone<S extends GameState>(
state: S,
cardId: string,
zoneId: string
): S注意
- 不校验
cardId是否存在于state.cards中 - 允许同一卡牌 id 同时位于多个 zone(如需要独占归属,调用方需自行处理)
抛错
- zone 不存在:
Error: Zone ${zoneId} not found in state - 卡牌已在该 zone 中:
Error: Card ${cardId} already in zone ${zoneId}
removeCardFromZone()
从指定 zone 的卡牌列表中移除一张卡牌。
function removeCardFromZone<S extends GameState>(
state: S,
cardId: string,
zoneId: string
): S抛错
- zone 不存在:
Error: Zone ${zoneId} not found in state - 卡牌不在该 zone 中:
Error: Card ${cardId} not found in zone ${zoneId}
moveCard()
在两个 zone 之间移动一张卡牌。
function moveCard<S extends GameState>(
state: S,
cardId: string,
fromZoneId: string,
toZoneId: string
): S行为
在一次不可变更新中同时修改源 zone 与目标 zone,避免中间状态被外部观察到。
严格的五步校验(任一失败立即抛错并保持原状态不变):
- 卡牌存在(
state.cards[cardId]) - 源 zone 存在
- 目标 zone 存在
- 卡牌在源 zone 中
- 卡牌不在目标 zone 中
抛错
Error: Card ${cardId} not found in stateError: Source zone ${fromZoneId} not found in stateError: Target zone ${toZoneId} not found in stateError: Card ${cardId} is not in zone ${fromZoneId}Error: Card ${cardId} already in zone ${toZoneId}
示例
import { moveCard, bumpVersion } from 'decklet/core/stateUtils.js'
const next = bumpVersion(moveCard(
state,
'card-001',
'deck',
'player:p1:hand'
))setVariable()
设置一个自定义变量。已存在的 key 会被覆盖。
function setVariable<S extends GameState>(
state: S,
key: string,
value: unknown
): SremoveVariable()
删除一个自定义变量。当 key 不存在时返回原 state(保持引用不变),让调用方可以无副作用地调用而无需先做存在性判断。
function removeVariable<S extends GameState>(state: S, key: string): SsetStatus()
设置对局生命周期状态。
function setStatus<S extends GameState>(
state: S,
status: GameState['status']
): S示例
const next = setStatus(state, 'finished')setCurrentPlayer()
设置当前玩家 id;传 undefined 表示清除当前玩家。
function setCurrentPlayer<S extends GameState>(
state: S,
playerId: string | undefined
): SsetTurn()
设置当前回合序号。
function setTurn<S extends GameState>(state: S, turn: number): SaddWinner()
添加一名获胜者。
function addWinner<S extends GameState>(state: S, playerId: string): S行为
幂等:若玩家已在 winnerIds 中则返回原 state(保持引用不变),否则追加。幂等性让 EventHandler 在多次触发同一胜负判定时安全调用。
bumpVersion()
将 state.version 递增 1。
function bumpVersion<S extends GameState>(state: S): S用途
ActionExecutor / GameContext mutation primitive 中唯一会修改版本号的函数:
- Action 处理路径:引擎统一在
ActionExecutor返回后调用一次 bump(参见GameEngine.processAction) - GameContext mutation primitive 路径:每次调用都各自 bump,以便事件链上的每个事件携带不同的
stateVersion
示例
import { bumpVersion, moveCard } from 'decklet/core/stateUtils.js'
// 在 ActionHandler 内组合多个 stateUtils 调用
const handler: ActionHandler = {
type: 'PLAY_CARD',
execute(action, ctx) {
const next = moveCard(ctx.state, 'card-001', 'hand', 'discard')
// 引擎会在 execute 返回后统一 bump version
return { state: next, events: [] }
}
}
// 在 GameContext mutation primitive 内(GameContextImpl 实现)
ctx.engine.setState(prev => bumpVersion(moveCard(prev, cardId, from, to)))完整示例
import {
stateUtils, createGameState, createPlayer, createCard, createZone
} from 'decklet'
const state = createGameState({
gameId: 'game-1',
players: [createPlayer({ id: 'p1', name: 'Alice', seat: 1 })],
cards: [createCard({ id: 'c1', type: 'number' })],
zones: [
createZone({ id: 'deck', type: 'deck', cards: ['c1'] }),
createZone({ id: 'hand:p1', type: 'hand', ownerId: 'p1' })
]
})
// 在 ActionHandler 内组合多个 mutation
let next = state
next = stateUtils.moveCard(next, 'c1', 'deck', 'hand:p1')
next = stateUtils.setVariable(next, 'phase', 'playing')
// 引擎会在 execute 返回后统一 bump version注意事项
- 所有函数都是纯函数,不修改入参 state
- 不会自行 bump
version—— 调用方决定是否 bump - Action 路径:引擎对每个 Action 仅 bump 一次(在
ActionExecutor.execute返回后) - GameContext mutation primitive 路径:每次调用各自 bump(在
GameContextImpl实现中) - 找不到实体时抛错并保持原 state 不变(Plugin 信赖的状态一致性前提)