SimpleGamePlugin
一个用于测试与示例的简单游戏插件:2 玩家、各发 5 张牌、第一个清空手牌的玩家获胜。
概述
SimpleGamePlugin 位于 src/plugins/SimpleGamePlugin.ts。它实现了一个最小可玩的游戏:
- 2 个玩家,在
GAME_STARTED时从标准牌堆中每人发5张牌 - 玩家交替进行
PLAY_CARDaction(任意牌都可出) - 第一个清空手牌的玩家获胜
它注册了 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
初始发牌手牌数量。
export const SIMPLE_GAME_HAND_SIZE = 5| 常量 | 值 | 说明 |
|---|---|---|
SIMPLE_GAME_HAND_SIZE | 5 | 每位玩家在 GAME_STARTED 时从牌堆中抽取的手牌张数 |
SIMPLE_GAME_DISCARD_ZONE_ID
弃牌堆 zone id。
export const SIMPLE_GAME_DISCARD_ZONE_ID = 'discard'| 常量 | 值 | 说明 |
|---|---|---|
SIMPLE_GAME_DISCARD_ZONE_ID | 'discard' | 出牌时默认投往的 zone id |
createSimplePlayCardAction()
用于构造玩家派发"打出一张牌" action 的便捷工厂。
function createSimplePlayCardAction(playerId: string, cardIds: string[]): PlayCardAction参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
playerId | string | 是 | — | 出牌玩家 id |
cardIds | string[] | 是 | — | 待打出的卡牌 id 列表 |
返回值
PlayCardAction:构造好的 PLAY_CARD action,payload.cardIds 是入参数组的浅拷贝,toZoneId 未设置(由 handler 默认指向 'discard')。
示例
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 对象
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 | 见下 |
rules | 4 条规则 | 见下 |
actions | [PLAY_CARD handler] | 见下 |
events | [GAME_STARTED, CARD_PLAYED] | 见下 |
setup()
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')。
{
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)。
{
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)。
{
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)。
{
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
{
type: PLAY_CARD_ACTION,
execute(action: PlayCardAction, ctx): ActionHandlerResult
}行为
- 从
action.payload解析cardIds与toZoneId = payload.toZoneId ?? 'discard'。 - 校验:
playerId与cardIds必填,否则抛Error:PLAY_CARD requires a playerIdPLAY_CARD requires at least one cardId
- 若
toZoneIdzone 不存在,通过addZone(state, createZone({ id: toZoneId, type: 'discard' }))创建。 - 通过
playerHandZoneId(playerId)取手牌 zone id,逐个moveCard(next, cardId, handZoneId, toZoneId)移到目标 zone。 - 发出
CARD_PLAYED事件,payload 为{ playerId, cardIds: [...cardIds] },playerId 同步设置。 - 返回
{ state: next, events }。
events — GAME_STARTED handler
{
type: 'GAME_STARTED',
handle(_event, ctx): void
}行为
按 seat 升序遍历玩家,对每位玩家通过 ctx.dispatch(createDrawCardAction(playerId, SIMPLE_GAME_HAND_SIZE)) 派发抽牌 Action(每人抽 5 张)。
events — CARD_PLAYED handler
{
type: 'CARD_PLAYED',
handle(event, ctx): void
}行为
- 从
event.payload读playerId与cardIds。 - 通过
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] } })
- 否则通过
ctx.dispatch(createEndTurnAction(playerId))推进到下一位玩家(依赖TurnPlugin处理)。
示例
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(提供deckzone 与 52 张牌)、DrawPlugin(处理DRAW_CARD)与TurnPlugin(处理END_TURN);三者均需先于SimpleGamePlugin注册。 GAME_STARTEDhandler 不检查discardzone 是否存在 —— 它假设setup已经创建好。因此务必确保 setup 在 GAME_STARTED 之前被执行(这是GameEngine.createGame→start的默认顺序)。CARD_PLAYEDhandler 中调用的createEndTurnAction(playerId)走 ActionQueue 链路,会受maxActionsPerTick约束;若队满会被丢弃或抛错(具体取决于引擎实现)。- 4 条 Rule 的
appliesTo字段是数组而非字符串;playerTurnRule与playerOwnsCardRule仅作用于['PLAY_CARD'],对其他 action 不参与校验。 player-owns-cardrule 不校验toZoneId是否合法;handler 中若目标 zone 不存在会自动创建为'discard'类型。