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
interface PluginRegistration {
gameType: string
plugins: GamePlugin[]
}| 字段 | 类型 | 说明 |
|---|---|---|
gameType | string | 游戏类型,与主插件 id 一致 |
plugins | GamePlugin[] | 完整 Plugin 栈,按注册顺序;最后一项为主插件 |
错误类型
PluginNotFoundError
class PluginNotFoundError extends Error {
constructor(public readonly gameType: string)
name: 'PluginNotFoundError'
}当请求的 gameType 未注册时抛出(保留语义化错误类,供上层捕获)。
DuplicateGameTypeError
class DuplicateGameTypeError extends Error {
constructor(public readonly gameType: string)
name: 'DuplicateGameTypeError'
}当对同一 gameType 重复 register 时抛出。
注意
src/index.ts 中将 PluginNotFoundError 重命名为 ServerPluginNotFoundError 导出,与引擎层 PluginNotFoundError(来自 plugin/PluginManager.js)区分。
类
PluginRegistry
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()
register(gameType: string, plugins: GamePlugin[]): this注册一个 gameType 的完整 Plugin 装配栈。
参数
| 名称 | 类型 | 说明 |
|---|---|---|
gameType | string | 游戏类型 |
plugins | GamePlugin[] | 完整 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()
get(gameType: string): GamePlugin[] | undefined取出该 gameType 的完整 Plugin 栈(拷贝),未注册返回 undefined。
getMainPlugin()
getMainPlugin(gameType: string): GamePlugin | undefined取出该 gameType 的主 Plugin(即 plugins[plugins.length - 1])。
has()
has(gameType: string): boolean是否已注册该 gameType。
list()
list(): string[]已注册的所有 gameType 列表。
listInfo()
listInfo(): Array<{ name: string; gameType: string; version: string }>主 Plugin 元信息列表,供 /api/plugins 使用。每项形如 { name: 'UNO', gameType: 'uno', version: '1.0.0' }。
createDefaultPluginRegistry()
function createDefaultPluginRegistry(): PluginRegistry创建包含 UNO 与 Doudizhu 默认装配的 PluginRegistry。
装配顺序
registry.register('uno', [TurnPlugin, DrawPlugin, UnoPlugin])
registry.register('doudizhu', [TurnPlugin, DrawPlugin, DoudizhuPlugin])顺序遵循现有 examples:先 TurnPlugin → DrawPlugin → 主游戏插件。该顺序决定 RuleEngine 短路顺序——基础校验(回合)必须先于业务规则。
| gameType | Plugin 栈 | 主插件 |
|---|---|---|
'uno' | TurnPlugin → DrawPlugin → UnoPlugin | UnoPlugin(id 'uno') |
'doudizhu' | TurnPlugin → DrawPlugin → DoudizhuPlugin | DoudizhuPlugin(id 'doudizhu') |
在 GameSessionService 中的使用
// 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 })示例
使用默认注册表
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: '...' }
// ]自定义注册表
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 协作
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短路顺序错误会导致规则失效。