Skip to content

SimpleGamePlugin ​

一个用于测试与示例的简单游戏插件:2 玩家、各发 5 张牌、第一个清空手牌的玩家获胜。

概述 ​

SimpleGamePlugin 位于 src/plugins/SimpleGamePlugin.ts。它实现了一个最小可玩的游戏:

  • 2 个玩家,在 GAME_STARTED 时从标准牌堆中每人发 5 张牌
  • 玩家交替进行 PLAY_CARD action(任意牌都可出)
  • 第一个清空手牌的玩家获胜

它注册了 4 条 Rule(game-not-won / game-started / player-turn / player-owns-card)、一个 PLAY_CARD action handler,以及驱动发牌(GAME_STARTED)与胜负判定 + 回合推进(CARD_PLAYED)的 event handler。

导出常量 ​

SIMPLE_GAME_HAND_SIZE ​

初始发牌手牌数量。

ts
export const SIMPLE_GAME_HAND_SIZE = 5
常量值说明
SIMPLE_GAME_HAND_SIZE5每位玩家在 GAME_STARTED 时从牌堆中抽取的手牌张数

SIMPLE_GAME_DISCARD_ZONE_ID ​

弃牌堆 zone id。

ts
export const SIMPLE_GAME_DISCARD_ZONE_ID = 'discard'
常量值说明
SIMPLE_GAME_DISCARD_ZONE_ID'discard'出牌时默认投往的 zone id

createSimplePlayCardAction() ​

用于构造玩家派发"打出一张牌" action 的便捷工厂。

ts
function createSimplePlayCardAction(playerId: string, cardIds: string[]): PlayCardAction

参数 ​

参数类型必填默认值说明
playerIdstring是—出牌玩家 id
cardIdsstring[]是—待打出的卡牌 id 列表

返回值 ​

PlayCardAction:构造好的 PLAY_CARD action,payload.cardIds 是入参数组的浅拷贝,toZoneId 未设置(由 handler 默认指向 'discard')。

示例 ​

ts
import { createSimplePlayCardAction } from 'decklet/plugins'

const action = createSimplePlayCardAction('p1', ['std-spade-A', 'std-heart-5'])
// action.type === 'PLAY_CARD'
// action.payload.cardIds === ['std-spade-A', 'std-heart-5']
// engine.dispatch(action)

SimpleGamePlugin 对象 ​

ts
export const SimpleGamePlugin: GamePlugin = {
  id: 'simple-game',
  name: 'Simple Card Game',
  version: '1.0.0',
  setup, rules, actions, events
}

字段 ​

字段值说明
id'simple-game'插件唯一标识
name'Simple Card Game'插件名
version'1.0.0'插件版本
setup创建 discard zone见下
rules4 条规则见下
actions[PLAY_CARD handler]见下
events[GAME_STARTED, CARD_PLAYED]见下

setup() ​

ts
setup(ctx: GameContext): void

行为 ​

若 state.zones['discard'] 不存在,通过 ctx.addZone(createZone({ id: 'discard', type: 'discard' })) 创建(幂等)。

rules ​

注册了 4 条 Rule,作用于 GAMEPLAY_ACTIONS = ['PLAY_CARD', 'DRAW_CARD', 'END_TURN', 'PASS'] 或其子集:

gameStartedRule ​

校验游戏已开始(status === 'playing')。

ts
{
  id: 'simple-game-started',
  name: 'Game Started',
  appliesTo: GAMEPLAY_ACTIONS,  // 全部 4 种
  validate(c) {
    return c.state.status === 'playing'
      ? ok()
      : deny('game has not started', 'NOT_STARTED')
  }
}

gameNotWonRule ​

校验游戏未结束(winnerIds.length === 0)。

ts
{
  id: 'simple-game-not-won',
  name: 'Game Not Won',
  appliesTo: GAMEPLAY_ACTIONS,
  validate(c) {
    return c.state.winnerIds.length === 0
      ? ok()
      : deny('game already won', 'ALREADY_WON')
  }
}

playerTurnRule ​

校验当前是 action.playerId 的回合(仅作用于 PLAY_CARD)。

ts
{
  id: 'simple-player-turn',
  name: 'Player Turn',
  appliesTo: ['PLAY_CARD'],
  validate(c) {
    if (!c.action.playerId) return deny('action has no playerId', 'NO_PLAYER')
    if (c.state.currentPlayerId !== c.action.playerId) {
      return deny(`not ${c.action.playerId}'s turn`, 'NOT_YOUR_TURN')
    }
    return ok()
  }
}

playerOwnsCardRule ​

校验玩家持有待出的牌(仅作用于 PLAY_CARD)。

ts
{
  id: 'simple-player-owns-card',
  name: 'Player Owns Card',
  appliesTo: ['PLAY_CARD'],
  validate(c) {
    const payload = (c.action.payload ?? {}) as PlayCardPayload
    const cardIds = payload.cardIds ?? []
    if (cardIds.length === 0) return deny('no cards to play', 'NO_CARDS')
    if (!c.action.playerId) return deny('no player', 'NO_PLAYER')
    const handZoneId = playerHandZoneId(c.action.playerId)
    const handZone = c.state.zones[handZoneId]
    if (!handZone) return deny(`player ${c.action.playerId} has no hand`, 'NO_HAND')
    for (const cardId of cardIds) {
      if (!handZone.cards.includes(cardId)) {
        return deny(`player does not own card ${cardId}`, 'NOT_OWNED')
      }
    }
    return ok()
  }
}

actions — PLAY_CARD handler ​

ts
{
  type: PLAY_CARD_ACTION,
  execute(action: PlayCardAction, ctx): ActionHandlerResult
}

行为 ​

  1. 从 action.payload 解析 cardIds 与 toZoneId = payload.toZoneId ?? 'discard'。
  2. 校验:playerId 与 cardIds 必填,否则抛 Error:
    • PLAY_CARD requires a playerId
    • PLAY_CARD requires at least one cardId
  3. 若 toZoneId zone 不存在,通过 addZone(state, createZone({ id: toZoneId, type: 'discard' })) 创建。
  4. 通过 playerHandZoneId(playerId) 取手牌 zone id,逐个 moveCard(next, cardId, handZoneId, toZoneId) 移到目标 zone。
  5. 发出 CARD_PLAYED 事件,payload 为 { playerId, cardIds: [...cardIds] },playerId 同步设置。
  6. 返回 { state: next, events }。

events — GAME_STARTED handler ​

ts
{
  type: 'GAME_STARTED',
  handle(_event, ctx): void
}

行为 ​

按 seat 升序遍历玩家,对每位玩家通过 ctx.dispatch(createDrawCardAction(playerId, SIMPLE_GAME_HAND_SIZE)) 派发抽牌 Action(每人抽 5 张)。

events — CARD_PLAYED handler ​

ts
{
  type: 'CARD_PLAYED',
  handle(event, ctx): void
}

行为 ​

  1. 从 event.payload 读 playerId 与 cardIds。
  2. 通过 playerHandZoneId(playerId) 取手牌 zone;若手牌已空,执行:
    • ctx.addWinner(playerId)
    • ctx.setStatus('finished')
    • ctx.emit({ type: 'GAME_WON', payload: { winnerIds: [playerId] }, playerId })
    • ctx.emit({ type: 'GAME_FINISHED', payload: { winnerIds: [playerId] } })
  3. 否则通过 ctx.dispatch(createEndTurnAction(playerId)) 推进到下一位玩家(依赖 TurnPlugin 处理)。

示例 ​

ts
import { GameEngine } from 'decklet'
import {
  StandardDeckPlugin,
  DrawPlugin,
  TurnPlugin,
  SimpleGamePlugin,
  createSimplePlayCardAction,
  SIMPLE_GAME_HAND_SIZE
} from 'decklet/plugins'
import type { Player } from 'decklet'

const engine = new GameEngine({ seed: 42, gameId: 'g1' })
engine.use(StandardDeckPlugin)
engine.use(DrawPlugin)
engine.use(TurnPlugin)
engine.use(SimpleGamePlugin)

const players: Player[] = [
  { id: 'p1', name: 'Alice', seat: 0, status: 'active', data: {} },
  { id: 'p2', name: 'Bob', seat: 1, status: 'active', data: {} }
]
engine.createGame({ players })
engine.start()
// GAME_STARTED -> 每位玩家抽 5 张牌(SIMPLE_GAME_HAND_SIZE)

const state = engine.getState()
const p1Hand = state.zones[`player:p1:hand`].cards  // 5 张牌的 id 列表

// p1 出 1 张牌
engine.dispatch(createSimplePlayCardAction('p1', [p1Hand[0]]))
// -> CARD_PLAYED,p1 手牌变 4 张;p1 手牌未空 -> 派发 END_TURN -> 回合转到 p2
// 重复直到 p1 手牌清空 -> 触发 GAME_WON / GAME_FINISHED

注意事项 ​

  • 该插件依赖 StandardDeckPlugin(提供 deck zone 与 52 张牌)、DrawPlugin(处理 DRAW_CARD)与 TurnPlugin(处理 END_TURN);三者均需先于 SimpleGamePlugin 注册。
  • GAME_STARTED handler 不检查 discard zone 是否存在 —— 它假设 setup 已经创建好。因此务必确保 setup 在 GAME_STARTED 之前被执行(这是 GameEngine.createGame → start 的默认顺序)。
  • CARD_PLAYED handler 中调用的 createEndTurnAction(playerId) 走 ActionQueue 链路,会受 maxActionsPerTick 约束;若队满会被丢弃或抛错(具体取决于引擎实现)。
  • 4 条 Rule 的 appliesTo 字段是数组而非字符串;playerTurnRule 与 playerOwnsCardRule 仅作用于 ['PLAY_CARD'],对其他 action 不参与校验。
  • player-owns-card rule 不校验 toZoneId 是否合法;handler 中若目标 zone 不存在会自动创建为 'discard' 类型。