Skip to content

stateUtils ​

stateUtils 命名空间包含纯函数状态变更辅助方法。每个函数都返回一个全新的 GameState 对象(不可变更新),不会 bump state.version。Action handler 会组合多个此类方法来完成一个 action;GameEngine 负责对每个 action 仅 bump 一次版本。

概述 ​

stateUtils 通过 src/index.ts 以命名空间形式导出:

ts
export * as stateUtils from './core/stateUtils.js'

使用方式:

ts
import { stateUtils } from 'decklet'

const next = stateUtils.bumpVersion(state)
const next2 = stateUtils.setVariable(state, 'phase', 'bidding')

或直接导入单个函数(如果调用方在内部模块间传递):

ts
import { bumpVersion, setVariable } from 'decklet/core/stateUtils.js'

设计要点 ​

  • 所有函数都是纯函数:相同输入产出相同输出
  • 不修改入参 state,返回新对象
  • 不 bump version —— 调用方决定是否 bump
  • 通常组合使用:bumpVersion(setVariable(prev, key, value))
  • 严格校验:找不到实体时抛错并保持原 state 不变(Plugin 信赖的状态一致性前提)

函数列表 ​

addCard() ​

向状态注册一张卡牌。

ts
function addCard<S extends GameState>(state: S, card: Card): S

参数 ​

参数类型必填说明
stateS extends GameState是当前 state
cardCard是待注册的卡牌

返回值 ​

新 GameState(cards 表新增条目)。

抛错 ​

当 card.id 已存在时抛 Error: Card ${card.id} already exists in state,避免覆盖导致数据丢失。

removeCard() ​

从状态移除一张卡牌。

ts
function removeCard<S extends GameState>(state: S, cardId: string): S

注意 ​

本函数只更新 cards 表,不会从任何 zone 的 cards 列表中清理引用。调用方若需要保持一致性,应自行先调用 removeCardFromZone。

抛错 ​

当卡牌不存在时抛 Error: Card ${cardId} not found in state。

addZone() ​

向状态注册一个 zone。

ts
function addZone<S extends GameState>(state: S, zone: CardZone): S

抛错 ​

当 zone.id 已存在时抛 Error: Zone ${zone.id} already exists in state。

removeZone() ​

从状态移除一个 zone。

ts
function removeZone<S extends GameState>(state: S, zoneId: string): S

注意 ​

本函数不校验该 zone 内是否仍持有卡牌引用,调用方应确保已先清空其 cards 列表以避免悬挂引用。

抛错 ​

当 zone 不存在时抛 Error: Zone ${zoneId} not found in state。

addPlayer() ​

向状态注册一名玩家。

ts
function addPlayer<S extends GameState>(state: S, player: Player): S

抛错 ​

当 player.id 已存在时抛 Error: Player ${player.id} already exists in state。

removePlayer() ​

从状态移除一名玩家。

ts
function removePlayer<S extends GameState>(state: S, playerId: string): S

抛错 ​

当玩家不存在时抛 Error: Player ${playerId} not found in state。

updatePlayer() ​

用纯函数 updater 增量更新一名玩家。适用于仅修改玩家局部字段而不替换整对象的场景,例如状态切换、计分累计。

ts
function updatePlayer<S extends GameState>(
  state: S,
  playerId: string,
  updater: (player: Player) => Player
): S

参数 ​

参数类型必填说明
stateS extends GameState是当前 state
playerIdstring是玩家 id
updater(player: Player) => Player是增量更新函数

抛错 ​

当玩家不存在时抛 Error: Player ${playerId} not found in state。

addCardToZone() ​

将卡牌 id 追加到指定 zone 的卡牌列表末尾。

ts
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 的卡牌列表中移除一张卡牌。

ts
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 之间移动一张卡牌。

ts
function moveCard<S extends GameState>(
  state: S,
  cardId: string,
  fromZoneId: string,
  toZoneId: string
): S

行为 ​

在一次不可变更新中同时修改源 zone 与目标 zone,避免中间状态被外部观察到。

严格的五步校验(任一失败立即抛错并保持原状态不变):

  1. 卡牌存在(state.cards[cardId])
  2. 源 zone 存在
  3. 目标 zone 存在
  4. 卡牌在源 zone 中
  5. 卡牌不在目标 zone 中

抛错 ​

  • Error: Card ${cardId} not found in state
  • Error: Source zone ${fromZoneId} not found in state
  • Error: Target zone ${toZoneId} not found in state
  • Error: Card ${cardId} is not in zone ${fromZoneId}
  • Error: Card ${cardId} already in zone ${toZoneId}

示例 ​

ts
import { moveCard, bumpVersion } from 'decklet/core/stateUtils.js'

const next = bumpVersion(moveCard(
  state,
  'card-001',
  'deck',
  'player:p1:hand'
))

setVariable() ​

设置一个自定义变量。已存在的 key 会被覆盖。

ts
function setVariable<S extends GameState>(
  state: S,
  key: string,
  value: unknown
): S

removeVariable() ​

删除一个自定义变量。当 key 不存在时返回原 state(保持引用不变),让调用方可以无副作用地调用而无需先做存在性判断。

ts
function removeVariable<S extends GameState>(state: S, key: string): S

setStatus() ​

设置对局生命周期状态。

ts
function setStatus<S extends GameState>(
  state: S,
  status: GameState['status']
): S

示例 ​

ts
const next = setStatus(state, 'finished')

setCurrentPlayer() ​

设置当前玩家 id;传 undefined 表示清除当前玩家。

ts
function setCurrentPlayer<S extends GameState>(
  state: S,
  playerId: string | undefined
): S

setTurn() ​

设置当前回合序号。

ts
function setTurn<S extends GameState>(state: S, turn: number): S

addWinner() ​

添加一名获胜者。

ts
function addWinner<S extends GameState>(state: S, playerId: string): S

行为 ​

幂等:若玩家已在 winnerIds 中则返回原 state(保持引用不变),否则追加。幂等性让 EventHandler 在多次触发同一胜负判定时安全调用。

bumpVersion() ​

将 state.version 递增 1。

ts
function bumpVersion<S extends GameState>(state: S): S

用途 ​

ActionExecutor / GameContext mutation primitive 中唯一会修改版本号的函数:

  • Action 处理路径:引擎统一在 ActionExecutor 返回后调用一次 bump(参见 GameEngine.processAction)
  • GameContext mutation primitive 路径:每次调用都各自 bump,以便事件链上的每个事件携带不同的 stateVersion

示例 ​

ts
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)))

完整示例 ​

ts
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 信赖的状态一致性前提)