Game Server Runtime 总览
CardGameEngine 内置的多人在线游戏服务器运行时(Phase 6),基于引擎核心 GameEngine 提供房间 / 会话 / 玩家连接 / 实时事件分发等能力。
作为子路径模块使用
Game Server Runtime 以独立子路径 decklet/server 发布,与纯引擎 decklet 分离。仅当需要多人在线对局运行时(HTTP + WebSocket 房间 / 会话 / 实时事件分发)时才引用,纯引擎用户无需安装相关依赖。
安装与可选依赖
server 依赖 koa / @koa/router / koa-bodyparser / socket.io 四个包,它们在 decklet 的 package.json 中声明为 optionalDependencies:
默认
npm install decklet会一并安装这四个可选依赖,decklet/server开箱即用。若只想要纯引擎、不需要 server,可显式跳过可选依赖:
bashnpm install --omit=optional decklet此时
decklet主入口(引擎 API)完全可用,但引用decklet/server会在运行时报模块缺失(如Cannot find module 'koa')。
引用方式
// 整体引用 server 运行时
import { createServer } from 'decklet/server'
// 或按需引用具体类型 / 服务
import { GameService, RoomService, SocketServer } from 'decklet/server'
import type { ServerRuntime, ServerOptions } from 'decklet/server'TypeScript 类型
decklet/server 的 .d.ts 引用了 koa / @koa/router 等的类型。若消费方项目需要这些类型信息,请自行安装对应的 @types/* 包:
npm install -D @types/koa @types/koa__router @types/koa-bodyparser概述
Game Server Runtime 是一个分层架构:
┌─────────────────────────────────────────────────────────────┐
│ Client (浏览器 / App) │
└─────────────────────────────────────────────────────────────┘
│ │
HTTP (Koa) WebSocket (Socket.IO)
│ │
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────────┐
│ HTTP Controllers │ │ SocketServer │
│ (RoomController / │ │ ├── RoomGateway │
│ GameController / │ │ └── GameGateway │
│ PluginController) │ │ BroadcastPort 实现 │
└────────────┬─────────────┘ └────────────┬────────────────┘
│ │
└────────────┬───────────────────┘
▼
┌────────────────────────┐
│ Application 层 │
│ ├── RoomService │
│ ├── GameService │
│ ├── GameSessionService│
│ ├── PlayerSessionService│
│ └── GameQueryService │
└────────────┬───────────┘
│
▼
┌──────────────────────────────────────────┐
│ Domain 层 │
│ ├── Room / GameSession / PlayerSession │
│ ├── GameActionQueue │
│ └── enums (RoomStatus / GameSessionStatus)│
└────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ GameEngine (核心引擎) │
│ ├── GameState │
│ ├── Plugins (Turn/Draw/Uno/Doudizhu) │
│ └── EventBus │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Registry / Serializers │
│ ├── PluginRegistry │
│ ├── SerializerRegistry │
│ └── GameEventSerializer │
└──────────────────────────────────────────┘调用链(一次玩家出牌的完整流程):
Client
│ Socket.IO: game:action { gameId, actionId, action: { type, payload } }
▼
SocketServer.handleConnection → GameGateway.registerHandlers
│
▼
GameGateway.handleAction
│ playerId 来自 socket.data(绝不来自客户端 payload)
▼
GameService.executeAction(gameId, playerId, actionId, actionSpec)
│
├─→ GameSessionService.getSessionOrThrow(gameId)
├─→ session.getProcessedResult(actionId) // 幂等预检
└─→ session.enqueue(async () => {
session.dispatch(action) // engine.dispatch
│
▼
RuleEngine → ActionHandler → Effect → EventBus
│
▼
GameEventPublisher.handle(event) // 实时广播
│ GameEventSerializer.serialize // 脱敏
▼
BroadcastPort.broadcastToGame('game:event', serialized)
│
▼
SocketServer → game:${gameId} 房间 → 所有客户端
│
└→ broadcastSyncToPlayers → sendToPlayer('game:state', perPlayerView)
})分层说明
| 层 | 路径 | 职责 |
|---|---|---|
| shared | src/server/shared/ | 跨层共享的 DTO / 协议类型 / 错误 / 日志 / ID 生成 / 广播占位 |
| domain | src/server/domain/ | 领域实体:Room / GameSession / PlayerSession / GameActionQueue / 枚举 |
| application | src/server/application/ | 应用服务:RoomService / GameService / GameSessionService / PlayerSessionService / GameQueryService |
| registry | src/server/registry/ | 插件注册表:PluginRegistry |
| serializers | src/server/serializers/ | 状态 / 事件序列化器:GameStateSerializer / SerializerRegistry / GameEventSerializer |
| infrastructure | src/server/infrastructure/ | 基础设施:repositories / http / websocket |
核心概念
Room ≠ GameSession
- Room:大厅概念,承载玩家列表与开始按钮,玩家在此聚集。
- GameSession:一局对局概念,承载引擎实例(
GameEngine)。 - Room 通过
gameSessionId与 GameSession 弱关联(不直接持有引用,避免循环)。
PlayerSession ≠ Socket
- playerId:玩家身份(与业务关联)。
- socketId:物理连接(断线重连时变化)。
- PlayerSession 把两者解耦——
GameSession/Room通过playerId引用玩家,不依赖socketId。
BroadcastPort 接口
Application 层通过 BroadcastPort 接口向 Socket 层推送事件,避免直接依赖 Socket.IO。SocketServer 实现 BroadcastPort;测试时可注入 mock 实现。
BroadcastHub:循环依赖打破
GameService 需要 BroadcastPort(构造期)
SocketServer 实现 BroadcastPort(但需要 GameService 处理 socket 事件)解决:先构造 BroadcastHub 作为占位 BroadcastPort 注入 GameService;待 SocketServer 构造完毕后调用 broadcastHub.setTarget(socketServer) 把转发目标指向它。
createServer:统一入口
function createServer(options?: ServerOptions): ServerRuntimecreateServer 装配全部依赖、构造 HTTP + Socket 服务,但不立即监听端口;调用 runtime.start() 才会监听。
装配顺序
- 创建默认内存仓储(
InMemoryRoomRepository/InMemoryGameSessionRepository/InMemoryPlayerSessionRepository)。 - 创建
PluginRegistry(createDefaultPluginRegistry,注册 UNO 与 Doudizhu)。 - 创建
SerializerRegistry(createDefaultSerializerRegistry,注册 UNO 与 Doudizhu 序列化器)。 - 创建
GameEventSerializer(createDefaultGameEventSerializer)。 - 创建
BroadcastHub作为占位BroadcastPort。 - 创建 Application 服务:
PlayerSessionService→GameSessionService→GameService→RoomService→GameQueryService。 - 创建 HTTP
app(createApp)+httpServer。 - 创建 WebSocket 网关:
RoomGateway/GameGateway。 - 创建
SocketServer(依赖 httpServer + gateways)。 broadcastHub.setTarget(socketServer)把转发目标指向真正的 SocketServer。
ServerRuntime 接口
interface ServerRuntime {
app: ReturnType<typeof createApp>
httpServer: HttpServer
socketServer: SocketServer
roomService: RoomService
gameService: GameService
sessionService: GameSessionService
queryService: GameQueryService
playerSessionService: PlayerSessionService
broadcastHub: BroadcastHub
start(): Promise<{ port: number; host: string }>
close(): Promise<void>
readonly port: number
}模块文档索引
| 主题 | 文档 |
|---|---|
| GameService(创建 / 查询 / 分发 Action) | game-service.md |
| RoomService(房间业务) | room-service.md |
| GameSession / PlayerSession / Room / GameActionQueue | session.md |
| PluginRegistry(游戏类型注册表) | plugin-registry.md |
| SocketServer / RoomGateway / GameGateway / GameEventPublisher | websocket.md |
| Serializer / SerializerRegistry / GameEventSerializer | serialization.md |
快速上手
import { createServer } from 'decklet/server'
const runtime = createServer({
port: 3000,
host: '0.0.0.0',
logLevel: 'INFO',
disconnectGraceMs: 60_000,
corsOrigin: '*'
})
await runtime.start()
console.log(`HTTP: http://localhost:${runtime.port}`)
console.log(`WebSocket: ws://localhost:${runtime.port}`)
// 优雅关闭
// await runtime.close()注意事项
createServer不立即监听端口——这样设计便于测试:可以先创建 runtime、做断言、再启动监听 / 直接用createApp注入测试。- HTTP Controller / Route 仅是 Server 的边缘入口;核心架构是 Application Service 与 GameSession 的协作。
ServerRuntime.close()实现优雅关闭:停止接受新连接 → drain 所有 GameSession 的 ActionQueue → 关闭 Socket → 销毁 GameSession(destroy engine)。- 默认内存仓储在进程重启后丢失——生产环境需替换为持久化实现(接口已就位)。