Skip to content

GameService ​

游戏业务入口,协调会话生命周期、状态序列化、事件广播。

概述 ​

GameService 是 Server Application 层的核心服务,负责:

  • 开始游戏(从 Room 创建 GameSession、绑定事件 publisher、调用 engine.start)
  • 执行玩家 Action(串行化、幂等、广播 per-player state)
  • 查询玩家视角的 PublicGameState
  • 结束 / 销毁游戏

不负责:

  • 游戏规则(由 Plugin 封装)
  • HTTP response / Socket frame(由 Controller / Gateway 处理)

GameServiceDeps ​

ts
interface GameServiceDeps {
  sessionService: GameSessionService
  roomRepository: RoomRepository
  serializerRegistry: SerializerRegistry
  eventSerializer: GameEventSerializer
  broadcaster: BroadcastPort
}
字段类型说明
sessionServiceGameSessionService会话生命周期管理
roomRepositoryRoomRepository关联房间状态
serializerRegistrySerializerRegistryPublicGameState 转换
eventSerializerGameEventSerializer事件脱敏
broadcasterBroadcastPortsocket 推送端口

类 ​

GameService ​

ts
class GameService {
  constructor(deps: GameServiceDeps)
}

方法 ​

startGame() ​

ts
async startGame(room: Room): Promise<GameSession>

开始游戏:从 Room 创建 GameSession、绑定事件 publisher、调用 engine.start。

参数 ​

名称类型说明
roomRoom待开始的房间(需满足 room.canStart)

返回值 ​

Promise<GameSession>:创建并启动的会话。

行为 ​

  1. 校验 room.canStart,不满足则抛 ServerError('GAME_NOT_STARTED', ..., 400)。
  2. sessionService.createSession(room):构造 engine 并 engine.createGame()。
  3. 创建 GameEventPublisher 并 attach(session.engine) 订阅引擎事件。
  4. room.setGameSession(session.id) + room.status = 'PLAYING' + roomRepository.save(room)。
  5. session.engine.start():触发 GAME_STARTED 事件链(publisher 实时广播)。
  6. session.status = 'RUNNING'。
  7. broadcaster.broadcastToRoom(room.id, 'game:started', { gameId, roomId, gameType, playerIds, stateVersion })。
  8. broadcastSyncToPlayers(session):向每位玩家发送初始 game:sync。

executeAction() ​

ts
async executeAction(
  gameId: string,
  playerId: string,
  actionId: string,
  actionSpec: { type: string; payload?: unknown }
): Promise<ActionResult>

执行玩家 Action。

参数 ​

名称类型说明
gameIdstring对局 ID
playerIdstring玩家 ID(由服务器注入,不可来自客户端 payload)
actionIdstring客户端 Action 幂等键
actionSpec{ type: string; payload?: unknown }Action 类型与负载

返回值 ​

Promise<ActionResult>:成功形态含 stateVersion;失败形态含 error.code / error.message。

行为 ​

关键点:

  • playerId 由服务器注入:来自调用方(Gateway 从 socket.data.playerId 取),客户端 payload 中即使带 playerId 也会被丢弃。
  • 同会话内 Action 严格 FIFO 串行:通过 session.enqueue 进入会话级 ActionQueue。
  • actionId 幂等:重复请求直接返回首次结果(队列外预检 + 队列内再次检查,防并发同 actionId)。
  • 失败结果同样记入幂等缓存:重复的失败请求也直接返回首次错误。
  • 成功后向每位玩家发送 game:state(per-player view)。

流程:

  1. sessionService.getSessionOrThrow(gameId) 取出会话;若 session.isClosed() 抛 GAME_NOT_FOUND。
  2. 校验 session.hasPlayer(playerId),否则抛 PLAYER_NOT_IN_GAME。
  3. 幂等预检(enqueue 之外,避免无谓排队):session.getProcessedResult(actionId) 命中则直接返回。
  4. session.enqueue(async () => { ... }):
    • 队列内再次检查幂等。
    • 构造 Action,强制注入服务器侧 playerId。
    • session.dispatch(action) → 引擎同步执行(RuleEngine → ActionHandler → Effect → EventBus)。
    • 成功:构造 ActionResultSuccess,若 engineStatus === 'finished' 则 session.markFinished()。
    • 失败(dispatchResult.ok === false):构造 ActionResultFailure,error.code = denyCode ?? 'INVALID_ACTION'。
    • 异常:构造 ActionResultFailure,error.code = 'INTERNAL_ERROR'。
    • session.recordProcessed(actionId, result)。
    • 成功时 broadcastSyncToPlayers(session)。

getGameState() ​

ts
async getGameState(gameId: string, playerId: string): Promise<PublicGameState>

取玩家视角的 PublicGameState。

参数 ​

名称类型说明
gameIdstring对局 ID
playerIdstring玩家 ID

返回值 ​

Promise<PublicGameState>:

  • 在游戏内的玩家:返回 serializeForPlayer(state, playerId)(含 myHand)。
  • 其他玩家(观战 / 未加入):返回 serializePublic(state)(无 myHand)。

行为 ​

通过 serializerRegistry.get(session.gameType) 取出该游戏类型的专属序列化器,调用对应方法。

sync() ​

ts
async sync(gameId: string, playerId: string): Promise<GameSyncResponse>

game:sync 响应:返回玩家视角的 PublicGameState + stateVersion。

参数 ​

名称类型说明
gameIdstring对局 ID
playerIdstring玩家 ID

返回值 ​

Promise<GameSyncResponse>:{ gameId, stateVersion, state }。

行为 ​

内部调用 getGameState 并包装为 GameSyncResponse。

getGameInfo() ​

ts
async getGameInfo(gameId: string): Promise<{
  gameId: string
  roomId: string
  gameType: string
  playerIds: string[]
  status: string
  stateVersion: number
  createdAt: number
  updatedAt: number
}>

取对局基础信息(不含敏感 state)。

参数 ​

名称类型说明
gameIdstring对局 ID

返回值 ​

Promise<{ gameId, roomId, gameType, playerIds, status, stateVersion, createdAt, updatedAt }>:对局元信息。

finishGame() ​

ts
async finishGame(gameId: string): Promise<void>

结束游戏:标记 GameSession 为 FINISHED,更新 Room 状态。

参数 ​

名称类型说明
gameIdstring对局 ID

返回值 ​

Promise<void>

行为 ​

引擎内部已经因 Rule 判定进入 finished;本方法同步外部状态:

  1. session.markFinished()。
  2. 找到关联 Room,room.status = 'FINISHED' + roomRepository.save(room)。
  3. broadcaster.broadcastToGame(gameId, 'game:finished', { gameId, roomId, stateVersion, winnerIds })。

removeGame() ​

ts
async removeGame(gameId: string): Promise<void>

销毁会话:清理 publisher 订阅 + 委托 sessionService.removeSession。

参数 ​

名称类型说明
gameIdstring对局 ID

返回值 ​

Promise<void>

行为 ​

  1. 取出 publisher 的 unsubscribe 函数并调用(停止事件转发)。
  2. sessionService.removeSession(gameId)(内部 session.close() drain 队列 + destroy engine + 从仓储删除)。

broadcastSyncToPlayers()(私有) ​

ts
private async broadcastSyncToPlayers(session: GameSession): Promise<void>

向每位玩家发送 game:state(per-player view)。断线玩家不发送(其重连时通过 game:sync 恢复)。

行为 ​

  • 取出 serializer = serializerRegistry.get(session.gameType)。
  • 遍历 session.playerIds:
    • 若 broadcaster.isPlayerConnected(playerId) 为 false 则跳过。
    • 否则 view = serializer.serializeForPlayer(state, playerId)。
    • broadcaster.sendToPlayer(playerId, 'game:state', { gameId, stateVersion, state: view })。

关键流程图 ​

executeAction(gameId, playerId, actionId, actionSpec)
   │
   ▼
sessionService.getSessionOrThrow(gameId)
   │
   ├─ session.isClosed() → throw GAME_NOT_FOUND
   ├─ !session.hasPlayer(playerId) → throw PLAYER_NOT_IN_GAME
   │
   ▼
session.getProcessedResult(actionId)  // 幂等预检
   │
   ├─ hit → 直接返回 cached
   │
   ▼
session.enqueue(async () => {
   session.getProcessedResult(actionId)  // 队列内再检
   │
   ▼
   session.dispatch(action)              // engine.dispatch 同步执行
   │
   ├─ RuleEngine → 拒绝?
   │   └─ result = { success: false, error: { code, message } }
   │
   ├─ ActionHandler → Effect → EventBus → GameEventPublisher
   │   │
   │   └─ broadcaster.broadcastToGame('game:event', ...)
   │
   └─ result = { success: true, stateVersion }
   │
   ▼
   session.recordProcessed(actionId, result)
   │
   ▼
   result.success? broadcastSyncToPlayers(session)
                    │
                    └─ sendToPlayer('game:state', per-player view)
})

示例 ​

ts
import { createServer } from 'decklet/server'

const runtime = createServer()
await runtime.start()

const { gameService, roomService } = runtime

// 1) 创建房间并加入 3 名玩家
const owner = await roomService.createRoom(
  { gameType: 'uno', maxPlayers: 4 },
  'p1'
)
await roomService.joinRoom(owner.id, 'p2')
await roomService.joinRoom(owner.id, 'p3')

// 2) 开始游戏
const { room, gameId } = await roomService.startRoom(owner.id)

// 3) 执行玩家 Action
const result = await gameService.executeAction(
  gameId,
  'p1',                                  // 由服务器注入的 playerId
  'client_abc123',                       // 客户端生成的 actionId
  { type: 'PLAY_CARD', payload: { cardIds: ['<cardId>'] } }
)
console.log(result.success, result.stateVersion)

// 4) 查询玩家视角
const state = await gameService.getGameState(gameId, 'p1')
console.log(state.myHand)             // 该玩家手牌
console.log(state.players)            // 所有玩家(仅 cardCount)

// 5) 结束游戏(通常由 Rule 判定自动触发,此处为手动示例)
// await gameService.finishGame(gameId)

await runtime.close()

注意事项 ​

  • playerId 必须由服务器注入——GameGateway 从 socket.data.playerId 取,GameController 从 playerIdentityMiddleware 注入的 ctx.state.playerId 取。客户端 payload 中即使带 playerId 也会被丢弃。
  • executeAction 是异步串行的——同一会话内 Action 严格 FIFO,不同会话间互不阻塞。
  • 幂等缓存有上限(默认 500 个 actionId,FIFO 驱逐);超长会话需注意可能的重复执行。
  • broadcastSyncToPlayers 跳过断线玩家——其重连时通过 game:sync 恢复。
  • engine.start() 期间派发的初始事件(GAME_STARTED 等)会被 publisher 广播到 game:${gameId} 房间,但此时玩家 socket 尚未加入该房间——这些初始事件会丢失。客户端通过 game:started + game:state 重建初始视图。