Skip to content

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,可显式跳过可选依赖:

    bash
    npm install --omit=optional decklet

    此时 decklet 主入口(引擎 API)完全可用,但引用 decklet/server 会在运行时报模块缺失(如 Cannot find module 'koa')。

引用方式 ​

ts
// 整体引用 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/* 包:

bash
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)
      })

分层说明 ​

层路径职责
sharedsrc/server/shared/跨层共享的 DTO / 协议类型 / 错误 / 日志 / ID 生成 / 广播占位
domainsrc/server/domain/领域实体:Room / GameSession / PlayerSession / GameActionQueue / 枚举
applicationsrc/server/application/应用服务:RoomService / GameService / GameSessionService / PlayerSessionService / GameQueryService
registrysrc/server/registry/插件注册表:PluginRegistry
serializerssrc/server/serializers/状态 / 事件序列化器:GameStateSerializer / SerializerRegistry / GameEventSerializer
infrastructuresrc/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:统一入口 ​

ts
function createServer(options?: ServerOptions): ServerRuntime

createServer 装配全部依赖、构造 HTTP + Socket 服务,但不立即监听端口;调用 runtime.start() 才会监听。

装配顺序 ​

  1. 创建默认内存仓储(InMemoryRoomRepository / InMemoryGameSessionRepository / InMemoryPlayerSessionRepository)。
  2. 创建 PluginRegistry(createDefaultPluginRegistry,注册 UNO 与 Doudizhu)。
  3. 创建 SerializerRegistry(createDefaultSerializerRegistry,注册 UNO 与 Doudizhu 序列化器)。
  4. 创建 GameEventSerializer(createDefaultGameEventSerializer)。
  5. 创建 BroadcastHub 作为占位 BroadcastPort。
  6. 创建 Application 服务:PlayerSessionService → GameSessionService → GameService → RoomService → GameQueryService。
  7. 创建 HTTP app(createApp)+ httpServer。
  8. 创建 WebSocket 网关:RoomGateway / GameGateway。
  9. 创建 SocketServer(依赖 httpServer + gateways)。
  10. broadcastHub.setTarget(socketServer) 把转发目标指向真正的 SocketServer。

ServerRuntime 接口 ​

ts
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 / GameActionQueuesession.md
PluginRegistry(游戏类型注册表)plugin-registry.md
SocketServer / RoomGateway / GameGateway / GameEventPublisherwebsocket.md
Serializer / SerializerRegistry / GameEventSerializerserialization.md

快速上手 ​

ts
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)。
  • 默认内存仓储在进程重启后丢失——生产环境需替换为持久化实现(接口已就位)。