Skip to content

PluginRegistry ​

按 gameType 注册并提供完整 Plugin 装配栈的注册表。

概述 ​

PluginRegistry 维护 gameType → GamePlugin[] 的映射,是 GameSessionService.createSession 创建引擎时取出 Plugin 装配栈的来源。

设计要点:

  • 一个 gameType 对应一个完整 Plugin 栈(含 TurnPlugin / DrawPlugin 等公共依赖),因为现有 UNO / Doudizhu 都依赖 TurnPlugin(回合推进)与 DrawPlugin(手牌 zone / 抽牌重洗)。
  • GameService 在 createSession 时按 gameType 取出整栈,依次 engine.use(p)。
  • PluginRegistry 不依赖 Koa / Socket.IO / Room / HTTP。
  • 提供 register / get / has / list 四个核心方法。

PluginRegistration ​

ts
interface PluginRegistration {
  gameType: string
  plugins: GamePlugin[]
}
字段类型说明
gameTypestring游戏类型,与主插件 id 一致
pluginsGamePlugin[]完整 Plugin 栈,按注册顺序;最后一项为主插件

错误类型 ​

PluginNotFoundError ​

ts
class PluginNotFoundError extends Error {
  constructor(public readonly gameType: string)
  name: 'PluginNotFoundError'
}

当请求的 gameType 未注册时抛出(保留语义化错误类,供上层捕获)。

DuplicateGameTypeError ​

ts
class DuplicateGameTypeError extends Error {
  constructor(public readonly gameType: string)
  name: 'DuplicateGameTypeError'
}

当对同一 gameType 重复 register 时抛出。

注意

src/index.ts 中将 PluginNotFoundError 重命名为 ServerPluginNotFoundError 导出,与引擎层 PluginNotFoundError(来自 plugin/PluginManager.js)区分。

类 ​

PluginRegistry ​

ts
class PluginRegistry {
  register(gameType: string, plugins: GamePlugin[]): this
  get(gameType: string): GamePlugin[] | undefined
  getMainPlugin(gameType: string): GamePlugin | undefined
  has(gameType: string): boolean
  list(): string[]
  listInfo(): Array<{ name: string; gameType: string; version: string }>
}

方法 ​

register() ​

ts
register(gameType: string, plugins: GamePlugin[]): this

注册一个 gameType 的完整 Plugin 装配栈。

参数 ​

名称类型说明
gameTypestring游戏类型
pluginsGamePlugin[]完整 Plugin 栈(最后一项为主插件)

返回值 ​

this(链式调用)。

行为 ​

约定:

  • plugins 不可为空,否则抛 Error('Cannot register gameType '${gameType}' with empty plugins')。
  • 最后一个 Plugin 的 id 必须等于 gameType(用作主插件标识),否则抛 Error('Main plugin id '${main.id}' does not match gameType '${gameType}'')。
  • 同一 gameType 重复注册抛 DuplicateGameTypeError。

注册时拷贝 plugins 数组([...plugins]),避免外部修改影响注册表内部。

get() ​

ts
get(gameType: string): GamePlugin[] | undefined

取出该 gameType 的完整 Plugin 栈(拷贝),未注册返回 undefined。

getMainPlugin() ​

ts
getMainPlugin(gameType: string): GamePlugin | undefined

取出该 gameType 的主 Plugin(即 plugins[plugins.length - 1])。

has() ​

ts
has(gameType: string): boolean

是否已注册该 gameType。

list() ​

ts
list(): string[]

已注册的所有 gameType 列表。

listInfo() ​

ts
listInfo(): Array<{ name: string; gameType: string; version: string }>

主 Plugin 元信息列表,供 /api/plugins 使用。每项形如 { name: 'UNO', gameType: 'uno', version: '1.0.0' }。

createDefaultPluginRegistry() ​

ts
function createDefaultPluginRegistry(): PluginRegistry

创建包含 UNO 与 Doudizhu 默认装配的 PluginRegistry。

装配顺序 ​

ts
registry.register('uno', [TurnPlugin, DrawPlugin, UnoPlugin])
registry.register('doudizhu', [TurnPlugin, DrawPlugin, DoudizhuPlugin])

顺序遵循现有 examples:先 TurnPlugin → DrawPlugin → 主游戏插件。该顺序决定 RuleEngine 短路顺序——基础校验(回合)必须先于业务规则。

gameTypePlugin 栈主插件
'uno'TurnPlugin → DrawPlugin → UnoPluginUnoPlugin(id 'uno')
'doudizhu'TurnPlugin → DrawPlugin → DoudizhuPluginDoudizhuPlugin(id 'doudizhu')

在 GameSessionService 中的使用 ​

ts
// GameSessionService.createSession 内部
const plugins = this.pluginRegistry.get(room.gameType)
if (!plugins || plugins.length === 0) {
  throw new ServerError('PLUGIN_NOT_FOUND', `No plugin registered for gameType: ${room.gameType}`, 404)
}
const engine = new GameEngine({ seed: options.seed, gameId: gameSessionId })
for (const plugin of plugins) {
  engine.use(plugin)
}
engine.createGame({ players, gameId: gameSessionId })

示例 ​

使用默认注册表 ​

ts
import { createDefaultPluginRegistry } from 'decklet/server'

const registry = createDefaultPluginRegistry()
console.log(registry.list())               // ['uno', 'doudizhu']
console.log(registry.has('uno'))           // true
console.log(registry.get('uno')?.length)   // 3
console.log(registry.getMainPlugin('uno')?.id) // 'uno'

console.log(registry.listInfo())
// [
//   { name: 'UNO', gameType: 'uno', version: '1.0.0' },
//   { name: 'DOUDIZHU', gameType: 'doudizhu', version: '...' }
// ]

自定义注册表 ​

ts
import { PluginRegistry, DuplicateGameTypeError } from 'decklet/server'
import { TurnPlugin } from '../../src/plugins/TurnPlugin.js'
import { DrawPlugin } from '../../src/plugins/DrawPlugin.js'
import { UnoPlugin } from '../../src/plugins/uno/UnoPlugin.js'

const registry = new PluginRegistry()

// 注册自定义 gameType
registry.register('uno', [TurnPlugin, DrawPlugin, UnoPlugin])

// 重复注册 → 抛 DuplicateGameTypeError
try {
  registry.register('uno', [TurnPlugin, DrawPlugin, UnoPlugin])
} catch (e) {
  console.log(e instanceof DuplicateGameTypeError) // true
}

// 主插件 id 不匹配 → 抛 Error
try {
  registry.register('wrong', [TurnPlugin, DrawPlugin, UnoPlugin])
} catch (e) {
  console.log(e.message) // "Main plugin id 'uno' does not match gameType 'wrong'"
}

与 GameSessionService 协作 ​

ts
import { GameSessionService } from 'decklet/server'
import { createDefaultPluginRegistry } from 'decklet/server'
import { InMemoryGameSessionRepository } from 'decklet/server'
import { Room } from 'decklet/server'

const pluginRegistry = createDefaultPluginRegistry()
const sessionRepo = new InMemoryGameSessionRepository()
const sessionService = new GameSessionService(sessionRepo, pluginRegistry)

const room = new Room({
  id: 'room_1',
  gameType: 'uno',
  ownerId: 'p1',
  playerIds: ['p1', 'p2'],
  maxPlayers: 4
})

const session = await sessionService.createSession(room, { seed: 42 })
console.log(session.engine.getState().status) // 'waiting'(未 start)
console.log(session.playerIds) // ['p1', 'p2']

注意事项 ​

  • PluginRegistry 与 SerializerRegistry 平行:前者提供游戏规则,后者提供对外视图。两者 gameType 集合应当一致——若 PluginRegistry 注册了某 gameType 但 SerializerRegistry 未注册,SerializerRegistry 会 fallback 到 DefaultGameStateSerializer(仅最小公共字段)。
  • 注册时拷贝数组但不深拷贝 Plugin 实例——同一 Plugin 实例被多个 gameType 共享是允许的(如 TurnPlugin / DrawPlugin 在 UNO 与 Doudizhu 中都被使用)。
  • register 要求主插件 id === gameType——这是约定,用于 listInfo 与 getMainPlugin 一致地标识主插件。
  • createDefaultPluginRegistry 的装配顺序至关重要:TurnPlugin → DrawPlugin → 主插件,否则 RuleEngine 短路顺序错误会导致规则失效。