---
name: riffle-multiplayer
description: 实现 Riffle 游戏的服务端 backend、HTTP/WS 通信、多人状态、持久数据、实时对战、聊天、排行榜、语音视频和恢复流程。
---

# Riffle 多人开发规范

多人功能统一使用游戏自己的 `backend/`，发布时滚动更新该 Session 唯一的 Runner。Runtime 只做 launch ticket 身份验证和 `gameId → backendDeploymentId` 路由；SQL/Redis 由 Binding Broker 按环境与部署 ID 隔离。不要把 Runtime 当作房间、匹配、排行榜、游戏规则或实时模拟服务。

## 开始前必须确认

先检查：

1. `@qingzhenghust/game-sdk` 的实际版本、导出的类型和包内文档。
2. 游戏的 `gameId`、用户身份来源、需要持久化的数据、可丢失的数据和恢复快照。
3. 同步模型：服务端状态逻辑、服务端连续模拟，或者明确标注的房主协调。没有已部署的服务端模拟时，不得把客户端计算、dev echo、建连成功或收到首帧称为权威模拟。
4. HTTP 与 WebSocket 各自承担的职责、消息格式、大小预算和失败后的恢复路径。
5. 每个模块的显式状态机和状态转移表。

没有真实服务端能力时，先把 backend 方案和状态机写清楚，再实现客户端；不要为了“先跑起来”退回同设备传递、伪造玩家或客户端裁决。

## 客户端接口

客户端只能通过 SDK 公开接口访问 backend：

```ts
await sdk.ready();
const backend = sdk.backend();
const snapshot = await backend.get('/match/state?matchId=demo');
await backend.post('/match/input', { requestId, sequence, action });
const socket = await backend.connect('/match/events');
const stop = socket.onMessage(message => applyServerEvent(message));
socket.send({ type: 'subscribe', requestId, matchId: 'demo' });
```

禁止在游戏代码中手写：

- `/v1/runtime/backend` URL；
- launch ticket、Authorization 或用户凭据；
- 原生 `WebSocket`、自定义 subprotocol、重连和心跳协议；
- SQL/Redis 连接；
- 把 HTTP 200、socket open 或 transport ACK 当作业务已完成。

HTTP 用于 CRUD、命令、权限检查、恢复快照和低频控制。WebSocket 用于订阅、服务器推送和短消息。两种方式必须使用同一套用户身份、版本号、权限和业务事件定义。

## Backend 入口

服务端代码放在 `backend/`，迁移放在 `migrations/`。入口必须显式声明协议：

```ts
import { defineGameBackend } from '@qingzhenghust/game-sdk/server';

export default defineGameBackend({
  http: async (request, ctx) => {
    // validate identity, request, version and authorization
    return Response.json({ ok: true });
  },
  websocket: async (socket, ctx) => {
    socket.send({ type: 'ready', protocol: 'game.v1' });
    socket.onMessage(message => {
      // validate type, requestId/eventId, sequence and permissions
      socket.send({ type: 'ack', requestId: message.requestId });
    });
  },
});
```

协议 manifest 只能是 `http`、`websocket` 或 `http-websocket`。每条 WS 消息至少包含 `type` 和协议版本；请求型消息包含 `requestId`，业务事件包含 `eventId`，连续输入包含递增 `sequence`。服务端必须限制消息大小、字段长度、频率和允许的枚举值。

`ctx` 只提供：

- `ctx.identity.userId`、`ctx.identity.gameId`：可信身份；
- `ctx.backendDeploymentId`：当前 Session 稳定的部署身份，不随发布变化；
- `ctx.sql`：游戏隔离的 PostgreSQL 查询和事务；
- `ctx.redis`：游戏隔离的短期缓存、幂等收据、租约、限流和在线协调；
- `ctx.version`：当前不可变 artifact digest，可用于诊断和快照兼容性判断。

SQL 保存持久事实、最终结果和恢复快照；Redis 保存短期数据。高频实时帧、媒体帧和无限历史不能写入 SQL/Redis。

## 部署与数据隔离契约

- 所有游戏 Runner 运行在统一的 `game-backend` namespace；Runner 不按 dev/test/prod 复制命名空间。同一个 backend deployment 可以被多个 Runtime 环境引用。
- 开发部署时还没有正式 `gameId`。平台在 Vibe Session 创建时分配稳定的 `backendDeploymentId`；同一 Session 的后续发布更新该 ID 对应的唯一 Runner Deployment，不能按内容版本、Git commit 或 artifact digest 新建 Runner。
- 正式发布游戏时，平台才建立 `gameId → backendDeploymentId` 映射。数据绑定按 `environment/backendDeploymentId` 解析；dev、test、prod 以及不同 deployment 之间不能共享数据库角色、Redis ACL、key 前缀或幂等记录。
- backend 代码只接收 `ctx.sql` 和 `ctx.redis`，不得读取、请求、记录或自行注入 PostgreSQL/Redis host、用户名、密码、连接串和 Kubernetes Secret。Runner 也不得持有这些凭据。
- migration 使用独立 owner 身份由发布平台执行；运行时 binding 只有所需 DML 和 sequence 权限，不得执行 DDL、创建角色、创建数据库或访问其他游戏数据。
- Redis key 使用业务相对 key；`backend:<environment>:<backendDeploymentId>:` 前缀和命令白名单由平台强制添加。禁止自行拼接其他环境或 deployment 的完整 key。
- backend 不得调用 Kubernetes API、探测集群服务、访问业务数据库或业务 Redis。需要的新能力通过 SDK backend contract 提供，不能靠读取环境变量或绕过 Broker 实现。

backend 源码入口放在 `backend/index.ts`，协议放在 `backend/manifest.json`，迁移放在 `backend/migrations/NNNNNN_name.sql`。只要存在 `backend/`，根目录 `./deploy.sh` 会执行 `pnpm build:backend`，将入口打成单个 `backend/dist/index.mjs` 后连同有序 migration 提交给平台。不要手写 Runner、Kubernetes 清单、数据库连接或部署脚本。

## 必须显式定义的状态机

每个多人模块至少定义以下四层，不得用一个 `connected` 布尔值代替：

### 连接状态

```text
idle → connecting → connected → closing → closed
                         └────→ recovering → connecting
connecting/connected ───────────────→ failed
```

### 业务状态

按玩法定义，例如：

```text
idle → joining → active → recovering → active
active → finishing → finished
joining → rejected/failed
```

socket connected 只代表传输层可用，不代表玩家已入场、状态已同步或比赛已开始。

### 消息状态

```text
created → sent → acknowledged → applied
                   ├──────────→ unknown
                   └──────────→ retryable
```

ACK 必须说明它确认的是接收、持久化、执行还是去重。网络超时后结果可能是 `unknown`，不能直接当作未执行；重试必须复用同一个 `requestId`/幂等键。

### 版本与顺序

- 写操作使用 `requestId` 或 `idempotencyKey`。
- 连续输入使用递增 `sequence`，服务端拒绝或幂等处理重复、乱序和跨用户序号。
- 状态写入使用 `expectedVersion`、事务或 CAS。
- 服务器返回 `stateVersion`、`lastAppliedSequence` 或等价进度。
- 旧版本客户端、旧房间、旧连接的回调不得覆盖新一代状态。

## 可靠性规则

- 普通输入和实时帧不等待业务 ACK、不暂停模拟、不无限重试；只保留最新帧并丢弃过期帧。
- 开局、进球、结算、准备、权限变化和最终写入必须有事件号、去重、有限重试、超时和快照恢复。
- 断线、切后台、网络切换、Runner 重启和 WS close 都进入 `recovering`；恢复时重新取完整快照、恢复订阅和用户意图。
- 优先发送紧凑的完整当前状态。只有恢复快照确实超限时才分片；实时帧不得等待分片重组。
- 服务端错误返回结构化 `code`、可读 `message` 和是否可重试的标志；不能吞掉发送失败。

## 常见玩法边界

- 双人 PVP：服务端校验身份、动作、序号、幂等、状态版本、碰撞/结算和最终结果。客户端预测只改善手感，不能改写裁决。
- 留言板、排行榜、异步协作：直接使用 SQL/Redis 和 HTTP/WS 订阅，不为了持久数据创建虚构房间。
- 排队和匹配：如果产品需要，作为 backend 的独立状态机实现；`queued`、`matched`、`canceling`、`expired`、`failed` 必须区分，匹配结果要有唯一 assignment 和恢复查询。
- 语音/视频：使用 SDK 已有公开媒体能力；backend 只负责权限、会话配置和业务元数据，媒体帧不经过 SQL/Redis。
- 房主协调：必须明确房主、输入转发、应用 ACK、房主离开和权威快照恢复；不能把房主结果包装成服务端模拟。

## 发布和验证

每个 backend 版本必须经过：

1. 本地 backend 单元测试和状态机转移测试；
2. migration 执行与 schema 检查；
3. Runner 健康检查；
4. 平台以同一个 `backendDeploymentId` 滚动替换当前 Runner，并通过健康检查；
5. dev 使用真实 launch ticket 的 HTTP 和 WS E2E；
6. 双端或多端验证重复、乱序、丢响应、延迟、断线、重连、切后台、旧 artifact 回滚和恢复快照，并确认重复部署没有新增 Runner Deployment。

Node 模拟只证明代码契约，不代表真机、音视频、宿主 Bridge 或真实蜂窝网络已经验收。异步测试必须设置超时、关闭连接并检查 terminal state。任何未验证的能力都要在交付说明中明确写出。
