Skip to content

GameEvent ​

GameEvent 描述"已经发生了什么"。与 Action("我要做什么")相对,Event 由 ActionHandler 在执行成功后产出,经 EventBus 广播给所有 EventHandler。引擎自动填充 id、timestamp 与 stateVersion,业务代码只需提供 type、payload 与可选 playerId。

概述 ​

设计要点:

  • Event 描述"已经发生了什么",由 ActionHandler 产出
  • 引擎自动填充 id / timestamp / stateVersion
  • 通过 EventInit({ type, payload, playerId? })描述待发事件,eventFromInit 补全元数据
  • 内置 11 个基础事件类型常量,覆盖对局生命周期、回合、卡牌移动与玩家进出

GameEvent 接口 ​

ts
export interface GameEvent<TType extends string = string, TPayload = unknown> {
  id: string
  type: TType
  payload: TPayload
  timestamp: number
  stateVersion: number
  playerId?: string
}
属性类型必填说明
idstring是事件唯一标识,由引擎自动生成
typeTType是事件类型,通常引用本文件导出的常量以避免拼写错误
payloadTPayload是事件负载,结构由事件类型决定
timestampnumber是事件产生时间戳(毫秒),由引擎自动填充
stateVersionnumber是产出该事件时的 GameState 版本号,用于回放与一致性校验
playerIdstring否触发该事件的玩家 id,用于事件归因(系统事件可省略)

泛型参数 ​

参数默认值约束说明
TTypestringextends string事件类型
TPayloadunknown—负载类型

基础事件类型常量 ​

引擎内置的基础事件类型常量:

ts
export const GAME_CREATED = 'GAME_CREATED' as const
export const GAME_STARTED = 'GAME_STARTED' as const
export const TURN_STARTED = 'TURN_STARTED' as const
export const TURN_ENDED = 'TURN_ENDED' as const
export const CARD_DRAWN = 'CARD_DRAWN' as const
export const CARD_PLAYED = 'CARD_PLAYED' as const
export const CARD_MOVED = 'CARD_MOVED' as const
export const PLAYER_JOINED = 'PLAYER_JOINED' as const
export const PLAYER_LEFT = 'PLAYER_LEFT' as const
export const GAME_WON = 'GAME_WON' as const
export const GAME_FINISHED = 'GAME_FINISHED' as const
常量值说明
GAME_CREATED'GAME_CREATED'对局已创建(由 createGame 派发)
GAME_STARTED'GAME_STARTED'对局已开始(由 start 派发)
TURN_STARTED'TURN_STARTED'回合开始
TURN_ENDED'TURN_ENDED'回合结束
CARD_DRAWN'CARD_DRAWN'抽牌
CARD_PLAYED'CARD_PLAYED'出牌
CARD_MOVED'CARD_MOVED'卡牌移动
PLAYER_JOINED'PLAYER_JOINED'玩家加入
PLAYER_LEFT'PLAYER_LEFT'玩家离开
GAME_WON'GAME_WON'胜负已判定:先于 GAME_FINISHED 派发,此时整局可能仍未彻底结束
GAME_FINISHED'GAME_FINISHED'整局彻底结束:不再有后续回合,通常在 GAME_WON 之后派发

BasicEventType 类型 ​

ts
export type BasicEventType =
  | typeof GAME_CREATED
  | typeof GAME_STARTED
  | typeof TURN_STARTED
  | typeof TURN_ENDED
  | typeof CARD_DRAWN
  | typeof CARD_PLAYED
  | typeof CARD_MOVED
  | typeof PLAYER_JOINED
  | typeof PLAYER_LEFT
  | typeof GAME_WON
  | typeof GAME_FINISHED

所有内置基础事件类型的联合,便于对事件类型做收窄与穷举校验。

Payload 类型 ​

GameCreatedPayload ​

ts
export interface GameCreatedPayload {
  gameId: string
}
字段类型说明
gameIdstring创建出的对局 id

GameStartedPayload ​

ts
export interface GameStartedPayload {
  gameId: string
}
字段类型说明
gameIdstring开始的对局 id

TurnStartedPayload ​

ts
export interface TurnStartedPayload {
  playerId: string
  turn: number
}
字段类型说明
playerIdstring进入回合的玩家 id
turnnumber回合序号(从 1 开始递增)

TurnEndedPayload ​

ts
export interface TurnEndedPayload {
  playerId: string
  turn: number
}
字段类型说明
playerIdstring结束回合的玩家 id
turnnumber结束的回合序号

CardDrawnPayload ​

ts
export interface CardDrawnPayload {
  playerId: string
  cardIds: string[]
  count: number
}
字段类型说明
playerIdstring抽牌玩家 id
cardIdsstring[]抽到的卡牌 id 列表
countnumber抽牌数量(等于 cardIds.length,冗余便于消费方快速读取)

CardPlayedPayload ​

ts
export interface CardPlayedPayload {
  playerId: string
  cardIds: string[]
}
字段类型说明
playerIdstring出牌玩家 id
cardIdsstring[]打出的卡牌 id 列表

CardMovedPayload ​

ts
export interface CardMovedPayload {
  cardId: string
  fromZoneId: string
  toZoneId: string
  playerId?: string
}
字段类型必填说明
cardIdstring是被移动的卡牌 id
fromZoneIdstring是源区域 id
toZoneIdstring是目标区域 id
playerIdstring否卡牌归属玩家 id(区域与玩家绑定时填写)

PlayerJoinedPayload ​

ts
export interface PlayerJoinedPayload {
  playerId: string
  seat: number
}
字段类型说明
playerIdstring加入的玩家 id
seatnumber分配的座位号

PlayerLeftPayload ​

ts
export interface PlayerLeftPayload {
  playerId: string
}
字段类型说明
playerIdstring离开的玩家 id

GameWonPayload ​

ts
export interface GameWonPayload {
  winnerIds: string[]
}
字段类型说明
winnerIdsstring[]胜利者 id 列表(支持多赢家)

GameFinishedPayload ​

ts
export interface GameFinishedPayload {
  winnerIds: string[]
}
字段类型说明
winnerIdsstring[]最终赢家 id 列表

GameEventInit 接口 ​

ts
export interface GameEventInit<TType extends string = string, TPayload = unknown> {
  type: TType
  payload: TPayload
  playerId?: string
  stateVersion: number
  id?: string
  timestamp?: number
}

创建 GameEvent 所需的初始化数据。

业务侧只需提供 type、payload、stateVersion 与可选 playerId;id 与 timestamp 可省略,由工厂函数自动填充。

createGameEvent() ​

根据初始化数据构造一个完整的 GameEvent。

ts
export function createGameEvent<TType extends string, TPayload = unknown>(
  init: GameEventInit<TType, TPayload>
): GameEvent<TType, TPayload>

参数 ​

参数类型必填默认值说明
init.typeTType是—事件类型
init.payloadTPayload是—事件负载
init.stateVersionnumber是—当前的 state version
init.playerIdstring否undefined触发玩家
init.idstring否generateId('event')事件 id
init.timestampnumber否Date.now()时间戳

返回值 ​

GameEvent<TType, TPayload> —— 填充完整元数据后的 GameEvent。

行为 ​

  • 缺省时自动生成 id(event- 前缀)与 timestamp(当前时间)
  • 显式传入则覆盖原值,便于回放时复现原事件
  • 仅当传入 playerId 时才挂载该字段
ts
function createGameEvent<TType extends string, TPayload = unknown>(
  init: GameEventInit<TType, TPayload>
): GameEvent<TType, TPayload> {
  const event: GameEvent<TType, TPayload> = {
    id: init.id ?? generateId('event'),
    type: init.type,
    payload: init.payload,
    timestamp: init.timestamp ?? Date.now(),
    stateVersion: init.stateVersion
  }
  if (init.playerId !== undefined) {
    event.playerId = init.playerId
  }
  return event
}

示例 ​

ts
import { createGameEvent, GAME_WON } from 'decklet'

const event = createGameEvent({
  type: GAME_WON,
  payload: { winnerIds: ['p1'] },
  stateVersion: 42,
  playerId: 'p1'
})
console.log(event.id)            // 'event-xxxxxxxx-xxxxxxxx-N'
console.log(event.type)          // 'GAME_WON'
console.log(event.payload)       // { winnerIds: ['p1'] }
console.log(event.stateVersion)  // 42
console.log(event.playerId)     // 'p1'

eventFromInit() ​

从 ActionHandler 产出的 EventInit 构造 GameEvent。

ts
export function eventFromInit(init: EventInit, stateVersion: number): GameEvent

参数 ​

参数类型必填说明
init.typestring是事件类型
init.payloadunknown是事件负载
init.playerIdstring否触发玩家
stateVersionnumber是当前 GameState 版本号

返回值 ​

GameEvent —— 带完整元数据的 GameEvent。

行为 ​

ActionHandler 只产出 (type, payload, playerId),引擎在派发前调用本函数补齐 id、timestamp 与当前 stateVersion,使 handler 免于元数据记账。

  • id 通过 generateId('event') 自动生成
  • timestamp 取 Date.now()
  • stateVersion 由引擎传入
  • 仅当 init.playerId !== undefined 时才挂载该字段
ts
function eventFromInit(init: EventInit, stateVersion: number): GameEvent {
  const event: GameEvent = {
    id: generateId('event'),
    type: init.type,
    payload: init.payload,
    timestamp: Date.now(),
    stateVersion
  }
  if (init.playerId !== undefined) {
    event.playerId = init.playerId
  }
  return event
}

调用方 ​

主要由 GameEngine 在 processAction 内调用:

ts
for (const init of handlerEvents) {
  const event = eventFromInit(init, this.state.version)
  emittedEvents.push(event)
  this.tickEvents.push(event)
  this.eventBus.emit(event)
}

以及在 emitEvent(GameContext.emit 内部使用):

ts
emitEvent(init: EventInit): void {
  const event = eventFromInit(init, this.state.version)
  this.tickEvents.push(event)
  this.eventBus.emit(event)
}

示例 ​

在 ActionHandler 中产出事件 ​

ts
import { type ActionHandler, PLAY_CARD_ACTION, type PlayCardAction } from 'decklet'
import { moveCard } from 'decklet/core/stateUtils.js'

const handler: ActionHandler<PlayCardAction> = {
  type: PLAY_CARD_ACTION,
  execute(action, ctx) {
    const { cardIds } = action.payload
    const playerId = action.playerId!

    let next = ctx.state
    for (const cardId of cardIds) {
      next = moveCard(next, cardId, `player:${playerId}:hand`, 'discard')
    }

    // 产出 EventInit(无需 id/timestamp/stateVersion,引擎会补全)
    return {
      state: next,
      events: [
        {
          type: 'CARD_PLAYED',
          payload: { playerId, cardIds: [...cardIds] },
          playerId
        }
      ]
    }
  }
}

在 EventHandler 中产出事件(通过 ctx.emit) ​

ts
events: [
  {
    type: 'CARD_PLAYED',
    handle(event, ctx) {
      const { playerId } = event.payload as { playerId: string }
      const handZone = ctx.state.zones[`player:${playerId}:hand`]
      if (handZone && handZone.cards.length === 0) {
        ctx.addWinner(playerId)
        ctx.setStatus('finished')
        // ctx.emit 接收 EventInit,引擎补全元数据
        ctx.emit({
          type: 'GAME_WON',
          payload: { winnerIds: [playerId] },
          playerId
        })
        ctx.emit({
          type: 'GAME_FINISHED',
          payload: { winnerIds: [playerId] }
        })
      } else {
        ctx.dispatch(createEndTurnAction(playerId))
      }
    }
  }
]

订阅事件 ​

ts
import { GameEngine, CARD_PLAYED, GAME_WON } from 'decklet'

const engine = new GameEngine()

engine.subscribe(CARD_PLAYED, (event) => {
  const { playerId, cardIds } = event.payload as { playerId: string; cardIds: string[] }
  console.log(`${playerId} played ${cardIds.length} card(s)`)
})

engine.subscribe(GAME_WON, (event) => {
  const { winnerIds } = event.payload as { winnerIds: string[] }
  console.log(`winners: ${winnerIds.join(', ')}`)
})

注意事项 ​

  • 引擎内置事件常量是 as const 字面量;插件可派发自定义事件类型
  • eventFromInit 不接受 id / timestamp / stateVersion 之外的字段覆盖(与 createGameEvent 不同)
  • stateVersion 是回放与一致性校验的关键 —— 引擎会自动填充,业务侧无需关心
  • GAME_WON 与 GAME_FINISHED 通常先后派发:前者表示胜负已判定,后者表示整局彻底结束
  • 自定义事件类型应使用字符串字面量(如 'MY_EVENT' as const),避免类型放宽
  • EventInit 与 GameEvent 的差异:前者只有 type / payload / playerId,后者多了 id / timestamp / stateVersion