Files
AIC-Project/docs/superpowers/specs/2026-07-09-room-end-result-design.md
T
2026-07-10 10:24:29 +08:00

110 lines
4.9 KiB
Markdown

# 结算结果传递(RoomEndResult)设计
日期:2026-07-09
状态:已与用户确认(方案 A:`IRoomCtx.EndRoom` 加带结果重载)
## 背景 / 问题
对局结束后框架结算弹框只能显示"游戏结束 + 房间号":网关 `ServerLoop.OnRoomEnded` 硬编码
`RoomEndMsg { WinnerPlayerId = 0, ResultBlob = empty }`(ServerLoop.cs:412),而游戏逻辑
(`RpsServerRoom`)的胜负与比分只写了日志,框架 `IRoomCtx.EndRoom()` 没有传结果的通道。
这是既有占位欠账(见 minigame 进度备忘"`RoomEndMsg.WinnerPlayerId=0` 是占位")。
协议层 `RoomEndMsg`(FrameworkMessages.cs:219)早已有 `WinnerPlayerId` + `ResultBlob`
两字段,客户端 `MiniGameHost.ShowResultDialog` 也已按这两字段渲染——缺的只是服务端从
游戏逻辑到网关的管道。**零协议改动。**
用户确认的结算内容:**胜负 + 比分**(标题胜利/失败/平局,正文最终比分文本)。
## 设计
### 1. 共享接口(`Client/Assets/Framework/Shared/`)
(实施修订:`RoomEndResult` 按仓库 DTO 惯例落在 `GameTypes.cs`,接口重载在 `IGameServerRoom.cs`。)
```csharp
public sealed class RoomEndResult
{
public int WinnerPlayerId; // 0 = 平局/无胜者(与 RoomEndMsg 语义一致)
public byte[] ResultBlob; // 小游戏自定义结算数据;约定:放 UTF-8 文本时框架弹框直接显示
}
public interface IRoomCtx
{
// ...现有成员不变...
void EndRoom(); // 保留:等价 EndRoom(null)
void EndRoom(RoomEndResult result); // 新增
}
```
兼容性:游戏 DLL 只**消费**不**实现** `IRoomCtx`,给接口加成员不破坏已发布旧模块
(旧模块调无参版 → 结果为 null → 弹框行为同现状)。实现方仅框架内 `RoomCtx` 与测试
fake,一起改。不使用默认接口方法(避开 HybridCLR/mono DIM 运行时风险)。
### 2. 服务端宿主(`Server/Server.Host/`)
- `RoomCtx`:构造参数 `Action endRoom``Action<RoomEndResult>`;
`EndRoom()` → 回调(null);`EndRoom(result)` → 回调(result)。
- `Room`:`RequestEnd(RoomEndResult result)` 存入 `public RoomEndResult EndResult`
属性(多次调用以最后一次为准);保留无参 `RequestEnd()` = 传 null。
- `RoomHost.TickAll` 返回 `IReadOnlyList<string>``IReadOnlyList<EndedRoom>`:
`public readonly struct EndedRoom { public string RoomId; public RoomEndResult Result; }`,
`ReleaseRoom` 前捕获 `room.EndResult`。现有使用 TickAll 的调用点与测试同步更新。
- "Start 即结束"路径:`ServerLoop.CreateRoomForSeats` 持有 `Room` 引用,直接读
`room.EndResult`
### 3. 网关(`Server/Gateway/ServerLoop.cs`)
`OnRoomEnded(string roomId, RoomEndResult result)`:
```csharp
var msg = new RoomEndMsg {
WinnerPlayerId = result?.WinnerPlayerId ?? 0,
ResultBlob = result?.ResultBlob ?? Array.Empty<byte>()
};
```
### 4. RPS 服务端(`Server/games-src/RPS/RPS.Server/RpsServerRoom.cs`)
- `OnRoomStart` 记录 `_pidOfSeat[2]`(座位→pid;AI 玩家也有 pid)。
- 结束时(`Phase == Finished`):
- `Winner == 2`(平局)→ `WinnerPlayerId = 0`,文本 `"平局 {s0} : {s1}"`;
- 否则 → `WinnerPlayerId = _pidOfSeat[winner]`,文本 `"最终比分 {s0} : {s1}"`
- 文本按座位序,双端同文本(广播);UTF-8 编码进 `ResultBlob`,调 `EndRoom(result)`
### 5. 客户端弹框(`Client/Assets/Script/xmain/MiniGame/MiniGameHost.cs`,框架层)
RPS 客户端零改动。
- `ResultTitle`:
- `winner == _selfPlayerId`(非 0)→ "胜利";
- `winner != 0` 且非自己 → "失败";
- `winner == 0`:blob 有文本 → "平局";blob 空 → "游戏结束"
(旧模块/异常结束无结果时保持中性标题,不误报平局)。
- `ResultMessage`:blob 解出非空文本 → 只显示 blob 文本(去掉裸 pid 行);
空 → 维持现状回退(房间号)。
## 测试(TDD)
- **Server.Host.Tests**:①`RoomCtx.EndRoom(result)``Room.EndResult` 捕获;
`TickAll` 返回的 `EndedRoom` 携带结果;③未设结果时 `Result == null`;
`RpsServerRoomTests`:对局打完,以正确胜者 pid 与比分文本调了 `EndRoom`
(含平局→pid 0 分支)。
- **Gateway.Tests**:RPS 端到端断言收到的 RoomEnd 帧 `WinnerPlayerId`/`ResultBlob`
非占位;`build-test-games.sh` 重建 rps 测试夹具(RPS.Server 源码变更)。
- **客户端**(本环境无 Unity):IDE 诊断/csc 0 错;用户 Unity Play 一局人工核对弹框
(胜利/失败/平局标题 + 比分文本,点确定正常退出)。
## 部署影响
- 服务端:网关 + Server.Host 一体重编部署即生效。
- RPS 服务器模块:须经发布流水线重发 `deploy/minigame/rps/<ver>/`
(旧包兼容不崩,只是弹框无结算内容)。
- 客户端:`XWorld.Link``XWorld.Framework.Shared` 热更 DLL 重出。
## 不做(YAGNI)
- `SetResult()` 两步接口(无场景);
- 结构化结算(每回合出招记录等,用户选了胜负+比分);
- 宿主强制结束时的结果通道(结果恒 null → 弹框中性"游戏结束",可接受)。