# 结算结果传递(RoomEndResult)实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 把小游戏的结算结果(胜者 pid + 比分文本)从 `RpsServerRoom` 经框架管道传到客户端结算弹框,替掉网关硬编码的 `WinnerPlayerId=0, ResultBlob=empty`。 **Architecture:** `IRoomCtx.EndRoom(RoomEndResult)` 新重载 → `Room.EndResult` 捕获 → `RoomHost.TickAll` 返回 `EndedRoom{RoomId,Result}` → `ServerLoop.OnRoomEnded` 填入既有 `RoomEndMsg` → 客户端 `MiniGameHost.ShowResultDialog` 按 blob 文本显示。零协议改动。spec:`docs/superpowers/specs/2026-07-09-room-end-result-design.md`。 **Tech Stack:** netstandard2.1 C#9(Framework.Shared/RPS)、net10.0 + xUnit(服务端与测试)、Unity 2022.3 UGUI(客户端,本环境仅 dotnet 编译验证)。 **重要约定:本仓库 `.git` 为空目录(非可用 git 仓库),用户已定"不用 git,只改代码跑测试"。所有"Commit"步骤替换为"跑测试确认绿"。** **关键背景(给零上下文工程师):** - 一份共享源码 `Client/Assets/Framework/Shared/` 同时被客户端(Unity)与服务端(`Server/Framework.Shared.csproj` 链接编译)使用。 - 游戏 DLL 只**消费**不**实现** `IRoomCtx`(全仓库唯一实现是 `Server/Server.Host/RoomCtx.cs`),给接口加成员不破坏已发布模块。 - 测试夹具 `Server/Server.Host.Tests/TestGames/rps/{1,2}/` 由 `Server/build-test-games.sh` 从 RPS 源码构建,RPS.Server 改动后必须重跑该脚本,否则 RoomHost/Gateway 相关测试跑的还是旧 DLL。 - 所有 dotnet 命令在仓库根 `D:/UD/AI/AIC#Project` 执行(路径含 `#`,bash 中加引号)。 --- ### Task 1: 共享接口 + RoomCtx/Room 结果管道(Server.Host 层) **Files:** - Modify: `Client/Assets/Framework/Shared/IGameServerRoom.cs` - Modify: `Server/Server.Host/RoomCtx.cs` - Modify: `Server/Server.Host/Room.cs`(`RequestEnd`) - Modify: `Server/Server.Host/RoomHost.cs:41`(仅回调接线一行) - Test: `Server/Server.Host.Tests/RoomCtxTests.cs`、`Server/Server.Host.Tests/RoomTests.cs` - Modify(编译修复): `Server/Server.Host.Tests/RpsServerRoomTests.cs:49`(lambda 形参) - [ ] **Step 1: 写失败测试** `RoomCtxTests.cs` 追加两个测试(类内任意位置);同时把现有 `EndRoom_InvokesCallback`(第 39 行)和 `Broadcast_And_SendTo_RouteToOutput`(第 23 行)的 endRoom lambda 从 `() => ...` 改为 `_ => ...`(形参类型将变为 `Action`): ```csharp [Fact] public void EndRoom_WithResult_PassesResultToCallback() { RoomEndResult seen = null; var ctx = new RoomCtx(new DeterministicRandom(1), new ManualClock(), new ConsoleLogger(), new InMemoryStorage(), new FakeOutput(), r => seen = r); var result = new RoomEndResult { WinnerPlayerId = 42, ResultBlob = new byte[] { 1 } }; ctx.EndRoom(result); Assert.Same(result, seen); } [Fact] public void EndRoom_Parameterless_PassesNull() { var seen = new RoomEndResult(); // 先放非 null,验证回调真的收到 null var ctx = new RoomCtx(new DeterministicRandom(1), new ManualClock(), new ConsoleLogger(), new InMemoryStorage(), new FakeOutput(), r => seen = r); ctx.EndRoom(); Assert.Null(seen); } ``` `RoomTests.cs` 追加(类内任意位置)一个 fake 房间与两个测试;同时把 `Build` helper(第 50 行)的 `() => room.RequestEnd()` 改为 `r => room.RequestEnd(r)`: ```csharp // 结束时带结算结果的房间 private sealed class ResultRoom : IGameServerRoom { public RoomEndResult Result; private IRoomCtx _ctx; public void OnRoomStart(IReadOnlyList players, IRoomCtx ctx) => _ctx = ctx; public void OnMessage(int playerId, NetMessage message) { } public void OnTick(float dt) => _ctx.EndRoom(Result); public void OnRoomEnd() { } } [Fact] public void EndRoomWithResult_IsCapturedOnRoom() { var result = new RoomEndResult { WinnerPlayerId = 7, ResultBlob = new byte[] { 9 } }; var (room, _, _) = Build("rr", new ResultRoom { Result = result }); room.Start(Players); room.Tick(1f); Assert.False(room.IsActive); Assert.Same(result, room.EndResult); } [Fact] public void EndRoomWithoutResult_EndResultIsNull() { var (room, _, _) = Build("rn", new CountingRoom()); // CountingRoom 调无参 EndRoom room.Start(Players); room.Tick(1f); room.Tick(1f); // 第 2 tick 触发 EndRoom() Assert.False(room.IsActive); Assert.Null(room.EndResult); } ``` `RpsServerRoomTests.cs` 第 49 行 `() => ended[0] = true` 改为 `_ => ended[0] = true`(仅编译修复,该文件的功能改动在 Task 3)。 - [ ] **Step 2: 跑测试确认失败(RED)** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.Host.Tests --filter "EndRoomWithResult|EndRoom_WithResult" 2>&1 | tail -15` Expected: **编译错误**(`RoomEndResult` 不存在、`Room.EndResult` 不存在)——接口未实现前编译失败即 RED。 - [ ] **Step 3: 实现** `Client/Assets/Framework/Shared/IGameServerRoom.cs` 全文替换为: ```csharp using System.Collections.Generic; namespace XWorld.Framework { // 房间结算结果:由小游戏逻辑在 EndRoom 时给出,经网关放入 RoomEndMsg 下发客户端。 public sealed class RoomEndResult { public int WinnerPlayerId; // 0 = 平局/无胜者(与 RoomEndMsg 语义一致) public byte[] ResultBlob; // 小游戏自定义结算数据;放 UTF-8 文本时框架结算弹框直接显示 } public interface IRoomCtx { IRandom Random { get; } // 权威随机 ITimer Timer { get; } ILogger Logger { get; } IStorage Storage { get; } void Broadcast(NetMessage message); void SendTo(int playerId, NetMessage message); void EndRoom(); // 由小游戏逻辑请求结束本房间(无结算结果) void EndRoom(RoomEndResult result); // 带结算结果结束;result 可为 null,等价无参 } public interface IGameServerRoom { void OnRoomStart(IReadOnlyList players, IRoomCtx ctx); void OnMessage(int playerId, NetMessage message); void OnTick(float deltaTime); // 10-20Hz,内部调用 Core.Step + 广播快照 void OnRoomEnd(); } } ``` `Server/Server.Host/RoomCtx.cs`:字段与构造参数 `Action _endRoom` → `Action _endRoom`;末尾两方法: ```csharp public void EndRoom() => _endRoom(null); public void EndRoom(RoomEndResult result) => _endRoom(result); ``` `Server/Server.Host/Room.cs`:`RequestEnd` 处(第 24-26 行)改为: ```csharp public RoomEndResult EndResult { get; private set; } // 由 RoomCtx.EndRoom 回调(或宿主)触发;下一次 tick 末尾或 Start 后据此结束。 // 多次调用以最后一次的 result 为准。 public void RequestEnd() => RequestEnd(null); public void RequestEnd(RoomEndResult result) { _endRequested = true; EndResult = result; } ``` `Server/Server.Host/RoomHost.cs` 第 41 行:`output, () => room.RequestEnd());` → `output, r => room.RequestEnd(r));` - [ ] **Step 4: 跑测试确认通过(GREEN)** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.Host.Tests 2>&1 | tail -3` Expected: 全部通过(57 旧 + 4 新 = 61 附近,0 失败)。 - [ ] **Step 5: 全 sln 回归** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.sln 2>&1 | tail -5` Expected: 全绿(Gateway 此时尚未用到新返回值,不受影响;若 Gateway 编译错说明改了不该改的东西)。 --- ### Task 2: RoomHost.TickAll 带结果返回 + 网关接线 **Files:** - Modify: `Server/Server.Host/RoomHost.cs`(`TickAll` + 新 `EndedRoom` struct) - Modify: `Server/Gateway/ServerLoop.cs:79-80, 405, 408-421` - Test: `Server/Server.Host.Tests/RoomHostTests.cs:77-90`(适配新返回类型) 说明:`TickAll` 返回类型变更会同时破坏 `ServerLoop` 与 `RoomHostTests` 编译,故三处一个任务内完成。结果真值(非 null)的断言在 Task 3 夹具重建后补,本任务先保证管道形状与零回归。 - [ ] **Step 1: 适配测试(先改测试即 RED——编译失败)** `RoomHostTests.cs` 的 `TickAll_ReturnsEndedRoomIds`(第 77-90 行)改为: ```csharp [Fact] public void TickAll_ReturnsEndedRoomIds() { var host = NewHost(); host.CreateRoom("E", "rps", 1, Cfg(), Players, new Capture()); var first = host.TickAll(10f); // 第一大 tick:TotalElapsed=10 < 60,不结束 Assert.Empty(first); var ended = new List(); for (int i = 0; i < 10 && !ended.Exists(e => e.RoomId == "E"); i++) ended.AddRange(host.TickAll(10f)); Assert.Contains(ended, e => e.RoomId == "E"); // 累计超过 60s 后该 tick 返回结束房间 } ``` - [ ] **Step 2: 跑测试确认失败(RED)** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.Host.Tests --filter "TickAll_ReturnsEndedRoomIds" 2>&1 | tail -8` Expected: 编译错误(`EndedRoom` 不存在)。 - [ ] **Step 3: 实现 RoomHost** `Server/Server.Host/RoomHost.cs`:namespace 内(RoomHost 类之前)加: ```csharp // TickAll 的结束房间条目:携带小游戏经 EndRoom(result) 给出的结算结果(可为 null) public readonly struct EndedRoom { public readonly string RoomId; public readonly RoomEndResult Result; public EndedRoom(string roomId, RoomEndResult result) { RoomId = roomId; Result = result; } } ``` `TickAll`(第 58-71 行)替换为: ```csharp public IReadOnlyList TickAll(float dt) { List ended = null; foreach (var kv in _rooms) { var room = kv.Value.Room; if (!room.IsActive) { (ended ??= new List()).Add(new EndedRoom(kv.Key, room.EndResult)); continue; } room.Tick(dt); if (!room.IsActive) (ended ??= new List()).Add(new EndedRoom(kv.Key, room.EndResult)); } if (ended != null) foreach (var e in ended) ReleaseRoom(e.RoomId); return ended ?? (IReadOnlyList)System.Array.Empty(); } ``` - [ ] **Step 4: 实现 ServerLoop 接线** `Server/Gateway/ServerLoop.cs` 三处: 第 79-80 行: ```csharp var ended = _host.TickAll(dt); for (int i = 0; i < ended.Count; i++) OnRoomEnded(ended[i].RoomId, ended[i].Result); ``` 第 405 行("Start 即结束"路径,`CreateRoomForSeats` 持有 `Room` 引用): ```csharp if (!room.IsActive) OnRoomEnded(roomId, room.EndResult); // 极端:Start 即结束 ``` `OnRoomEnded`(第 408-421 行)替换为: ```csharp private void OnRoomEnded(string roomId, RoomEndResult result) { if (!_roomPlayers.TryGetValue(roomId, out var players)) return; _roomPlayers.Remove(roomId); var msg = new RoomEndMsg { WinnerPlayerId = result?.WinnerPlayerId ?? 0, ResultBlob = result?.ResultBlob ?? Array.Empty() }; byte[] payload = msg.Encode(); for (int i = 0; i < players.Count; i++) { int pid = players[i]; SendFramework(pid, FrameworkOpcode.RoomEnd, payload); var s = _sessions.Get(pid); if (s != null) s.RoomId = null; } } ``` - [ ] **Step 5: 跑测试确认通过(GREEN)+ 全 sln 回归** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.sln 2>&1 | tail -5` Expected: 全绿(此时结果恒为 null——RPS 夹具还是旧 DLL,调的无参 EndRoom;行为与改动前完全一致)。 --- ### Task 3: RPS 服务端产出结果 + 夹具重建 + 端到端断言 **Files:** - Modify: `Server/games-src/RPS/RPS.Server/RpsServerRoom.cs` - Test: `Server/Server.Host.Tests/RpsServerRoomTests.cs`(BuildCtx 捕获结果 + 2 新测试) - Test: `Server/Server.Host.Tests/RoomHostTests.cs`(1 新测试,夹具重建后结果非 null) - Test: `Server/Gateway.Tests/RpsEndToEndTests.cs`(RoomEnd 携带结算断言) - Run: `Server/build-test-games.sh`(重建 TestGames/rps 夹具) - [ ] **Step 1: 写失败测试(单元层)** `RpsServerRoomTests.cs`:`BuildCtx`(第 38-51 行)改为四元组、捕获结果: ```csharp private static (RoomCtx ctx, FakeOutput output, bool[] ended, RoomEndResult[] result) BuildCtx() { var output = new FakeOutput(); var ended = new bool[1]; var result = new RoomEndResult[1]; var ctx = new RoomCtx( new DeterministicRandom(42), new ManualClock(), new ConsoleLogger(), new InMemoryStorage(), output, r => { ended[0] = true; result[0] = r; }); return (ctx, output, ended, result); } ``` 现有 5 个测试的解构改四元组(多出的位置用 `_`): - `TwoHumans_PlayRound_ScoresUpdateByRules`:`var (ctx, output, _, _) = BuildCtx();` - `TwoHumans_Reach60s_EndsRoom`:`var (ctx, output, ended, _) = BuildCtx();` - `HumanAndAi_AutoAdvances_AndSettles`:`var (ctx, output, ended, _) = BuildCtx();` - `OnRoomStart_BroadcastsInitialSnapshot`:`var (ctx, output, _, _) = BuildCtx();` - `SecondChoiceFromSameSeat_IsIgnored`:`var (ctx, output, _, _) = BuildCtx();` 文件头部加 `using System.Text;`,追加两个新测试: ```csharp // ── 结算结果:胜者 pid + 比分文本经 EndRoom(result) 上报 ───────── [Fact] public void Finish_ReportsWinnerPidAndScoreText() { var (ctx, _, ended, result) = BuildCtx(); var room = new RpsServerRoom(); room.OnRoomStart(TwoHumans, ctx); // 每回合双方都出招(Rock vs Scissors → seat0 全胜),避免超时随机补招引入不确定性。 // 每回合 10.2s,第 6 回合 reveal 后 TotalElapsed=61.2 ≥ 60 → Finished for (int round = 0; round < 6; round++) { room.OnMessage(10, ChoiceMsg(Choice.Rock)); room.OnMessage(20, ChoiceMsg(Choice.Scissors)); room.OnTick(5.1f); // 结算本回合 → Revealing room.OnTick(5.1f); // → 下一回合 Choosing(末回合触发 Finished) } Assert.True(ended[0]); Assert.NotNull(result[0]); Assert.Equal(10, result[0].WinnerPlayerId); // seat0 的 pid Assert.Equal("最终比分 6 : 0", Encoding.UTF8.GetString(result[0].ResultBlob)); } [Fact] public void Finish_Draw_ReportsPidZeroAndDrawText() { var (ctx, _, ended, result) = BuildCtx(); var room = new RpsServerRoom(); room.OnRoomStart(TwoHumans, ctx); // 双方每回合同出 Rock → 每回合平 → 总分 0:0 → Winner=2(平局) for (int round = 0; round < 6; round++) { room.OnMessage(10, ChoiceMsg(Choice.Rock)); room.OnMessage(20, ChoiceMsg(Choice.Rock)); room.OnTick(5.1f); room.OnTick(5.1f); } Assert.True(ended[0]); Assert.NotNull(result[0]); Assert.Equal(0, result[0].WinnerPlayerId); // 平局 → 0 Assert.Equal("平局 0 : 0", Encoding.UTF8.GetString(result[0].ResultBlob)); } ``` - [ ] **Step 2: 跑测试确认失败(RED)** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.Host.Tests --filter "Finish_Reports|Finish_Draw" 2>&1 | tail -10` Expected: FAIL——`result[0]` 为 null(RpsServerRoom 还在调无参 EndRoom)。注意:Server.Host.Tests 直接 ProjectReference RPS.Server 源码工程,不走夹具,所以这里能先红。 - [ ] **Step 3: 实现 RpsServerRoom** `Server/games-src/RPS/RPS.Server/RpsServerRoom.cs`: 头部加 `using System.Text;`。 字段区(第 16 行 `_seatOf` 之后)加: ```csharp private readonly int[] _pidOfSeat = new int[2]; // seat->playerId(结算用;AI 也有 pid) ``` `OnRoomStart` 循环体(第 23-27 行)改为: ```csharp for (int i = 0; i < players.Count && i < 2; i++) { _seatOf[players[i].PlayerId] = i; _pidOfSeat[i] = players[i].PlayerId; _state.IsAi[i] = players[i].IsAI; } ``` `OnTick` 的结束分支(第 56-61 行)改为: ```csharp if (_state.Phase == RpsPhase.Finished) { _ended = true; int s0 = _state.Scores[0], s1 = _state.Scores[1]; bool draw = _state.Winner == 2; _ctx.Logger.Info($"rps finished, winner seat={_state.Winner}, scores {s0}:{s1}"); _ctx.EndRoom(new RoomEndResult { WinnerPlayerId = draw ? 0 : _pidOfSeat[_state.Winner], ResultBlob = Encoding.UTF8.GetBytes(draw ? $"平局 {s0} : {s1}" : $"最终比分 {s0} : {s1}") }); } ``` - [ ] **Step 4: 跑测试确认通过(GREEN)** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.Host.Tests 2>&1 | tail -3` Expected: 全绿。 - [ ] **Step 5: 重建测试夹具** Run: `cd "D:/UD/AI/AIC#Project/Server" && bash build-test-games.sh` Expected: `built rps v1 -> .../TestGames/rps/1`、`built rps v2 -> ...`、`TestGames ready.`(脚本内含 Framework.Shared.dll 不泄漏断言,失败会 exit 1)。 - [ ] **Step 6: 夹具层结果断言(RoomHostTests)** `RoomHostTests.cs` 追加(1 真人 + 1 AI 随机对局,可能平局,故只断言 blob 非空): ```csharp [Fact] public void TickAll_EndedRoom_CarriesResult() { var host = NewHost(); host.CreateRoom("R", "rps", 1, Cfg(), Players, new Capture()); RoomEndResult result = null; bool found = false; for (int i = 0; i < 10 && !found; i++) foreach (var e in host.TickAll(10f)) if (e.RoomId == "R") { found = true; result = e.Result; } Assert.True(found); Assert.NotNull(result); // 夹具已重建,RPS 走 EndRoom(result) Assert.NotNull(result.ResultBlob); Assert.NotEmpty(result.ResultBlob); // "最终比分 x : y" 或 "平局 x : y" } ``` Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.Host.Tests --filter "TickAll_EndedRoom_CarriesResult" 2>&1 | tail -5` Expected: PASS(若 FAIL 且 result 为 null,说明 Step 5 夹具没重建成功)。 - [ ] **Step 7: 端到端断言(Gateway.Tests)** `RpsEndToEndTests.cs` 两个测试各加 RoomEnd 内容断言: `TwoHumans_MatchAndPlayToRoomEnd`:第 53 行附近加变量 `RoomEndMsg c1End = null, c2End = null;`;t1 的 RoomEnd 分支(第 78-81 行)改: ```csharp else if (f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.RoomEnd) { c1End = RoomEndMsg.Decode(f.Payload); c1RoomEnd = true; } ``` t2 同样改(`c2End = RoomEndMsg.Decode(f.Payload);`)。末尾断言区(第 118-119 行后)加: ```csharp // 结算结果:blob 非占位(RPS 填入 UTF-8 比分文本),双端一致 Assert.NotNull(c1End); Assert.NotNull(c2End); Assert.True(c1End.ResultBlob != null && c1End.ResultBlob.Length > 0, "RoomEnd 应携带结算 blob"); Assert.Equal(c1End.WinnerPlayerId, c2End.WinnerPlayerId); Assert.Contains(":", System.Text.Encoding.UTF8.GetString(c1End.ResultBlob)); // 比分/平局文本 ``` (不断言具体胜者:双方每快照重发出招,回合是否踩到 Choosing 窗口受 tick 时序影响,可能随机补招/平局。确定性胜负已在 `RpsServerRoomTests` 用 ManualClock 断言。) `SingleClient_AiFill_MatchAndPlayToRoomEnd`:加变量 `RoomEndMsg end = null;`,RoomEnd 分支(第 170-173 行)改 `end = RoomEndMsg.Decode(f.Payload); roomEnd = true;`,末尾断言(第 178 行后)加: ```csharp Assert.NotNull(end); Assert.True(end.ResultBlob != null && end.ResultBlob.Length > 0, "AI 对局的 RoomEnd 也应携带结算 blob"); ``` - [ ] **Step 8: 跑 Gateway 测试确认通过 + 全 sln 回归** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Gateway.Tests 2>&1 | tail -3` Expected: 全绿(注意 Gateway.Tests 的 TestGames 由构建从 Server.Host.Tests/TestGames 拷贝,Step 5 已更新源头)。 Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.sln 2>&1 | tail -5` Expected: 全绿。 --- ### Task 4: 客户端结算弹框显示结果(MiniGameHost) **Files:** - Modify: `Client/Assets/Script/xmain/MiniGame/MiniGameHost.cs`(`ShowResultDialog`/`ResultTitle`/`ResultMessage`) - Verify: `dotnet build Client/HotUpdateGames/MiniGameRuntime.Verify`(编译验证壳,整目录 `` MiniGame 源码;csproj 内 UnityEditorPath 默认值即本机 2022.3.62f3) 说明:本环境无 Unity,无法运行时验证;用编译验证壳保证 0 错,行为由用户 Unity Play 人工核对。客户端只消费 `RoomEndMsg`(既有类型),不引用 `RoomEndResult`,故对 `Library/ScriptAssemblies` 里的旧 Framework.Shared.dll 无依赖。 - [ ] **Step 1: 修改 ShowResultDialog(结算文本只解一次,标题/正文共用)** `MiniGameHost.cs` `ShowResultDialog` 中,在 `_resultDialog = new GameObject(...)` 之前加一行: ```csharp string resultText = DecodeResultBlob(roomEnd.ResultBlob); ``` Title/Message 两处 CreateText 调用改为传入 `resultText`: ```csharp Text title = CreateText("Title", panel.transform, ResultTitle(roomEnd, resultText), 34, FontStyle.Bold, TextAnchor.MiddleCenter); ``` ```csharp Text message = CreateText("Message", panel.transform, ResultMessage(roomEnd, resultText), 22, FontStyle.Normal, TextAnchor.MiddleCenter); ``` - [ ] **Step 2: 替换 ResultTitle / ResultMessage** ```csharp private string ResultTitle(RoomEndMsg roomEnd, string resultText) { if (roomEnd.WinnerPlayerId == 0) { // 有结算文本 = 游戏明确上报了"无胜者"→ 平局;无文本 = 旧模块/异常结束,保持中性标题 return string.IsNullOrEmpty(resultText) ? "游戏结束" : "平局"; } if (_selfPlayerId != 0 && roomEnd.WinnerPlayerId == _selfPlayerId) { return "胜利"; } return "失败"; } private string ResultMessage(RoomEndMsg roomEnd, string resultText) { // 游戏上报的结算文本(如"最终比分 2 : 1")优先;裸 pid 对玩家无意义,不再显示 if (!string.IsNullOrEmpty(resultText)) { return resultText; } return string.IsNullOrEmpty(_roomId) ? $"{_gameId}@{_version}" : $"房间 {_roomId}"; } ``` (原 `ResultMessage` 里 `"\n胜利玩家:" + roomEnd.WinnerPlayerId` 的拼接随之删除;`DecodeResultBlob` 保持不变。) - [ ] **Step 3: 编译验证** Run: `cd "D:/UD/AI/AIC#Project/Client/HotUpdateGames/MiniGameRuntime.Verify" && dotnet build 2>&1 | tail -5` Expected: `0 个错误`(0 Errors)。若报 UnityEditorPath 相关缺 DLL,传 `-p:UnityEditorPath="<本机 Unity 2022.3.62f3 Editor 目录>"` 重试。 --- ### Task 5: 全量回归 + 收尾 - [ ] **Step 1: 服务端全量测试** Run: `cd "D:/UD/AI/AIC#Project/Server" && dotnet test Server.sln 2>&1 | tail -6` Expected: 全绿,总数 ≈ 旧基线 + 新增 8(RoomCtx 2、Room 2、RpsServerRoom 2、RoomHost 1、e2e 断言在既有测试内不增数)。 - [ ] **Step 2: 客户端验证壳复核** Run: `cd "D:/UD/AI/AIC#Project/Client/HotUpdateGames/MiniGameRuntime.Verify" && dotnet build 2>&1 | tail -3` Expected: 0 错误。 - [ ] **Step 3: 告知用户的人工步骤(计划执行者原样转达,不代做)** 1. Unity Editor 打开工程让其重新编译(Framework.Shared 源码变了)。 2. 菜单「XWorld/生成小游戏更新」重发 RPS 服务器模块到 `deploy/minigame/rps//`(旧包不崩,但弹框无结算内容)。 3. 客户端 `XWorld.Framework.Shared` + `XWorld.Link` 热更 DLL 重出(真机);Editor Play 本地即可验。 4. Play 一局 RPS 核对:胜→「胜利」、负→「失败」、平→「平局」,正文比分文本,点「确定」正常退出。 --- ## Self-Review 记录 - **Spec 覆盖**:§1 共享接口=Task 1;§2 宿主=Task 1+2;§3 网关=Task 2;§4 RPS=Task 3;§5 客户端=Task 4;测试章=各任务 TDD 步骤+Task 3 Step 6/7;部署影响=Task 5 Step 3。无缺口。 - **占位扫描**:无 TBD/TODO;所有代码步骤含完整代码。 - **类型一致性**:`RoomEndResult{WinnerPlayerId,ResultBlob}`、`Room.EndResult`、`RequestEnd(RoomEndResult)`、`EndedRoom{RoomId,Result}`、`OnRoomEnded(string,RoomEndResult)`、`ResultTitle/ResultMessage(RoomEndMsg,string)` 各任务间引用一致。 - **确定性核对**:`Finish_ReportsWinnerPidAndScoreText` 每回合双方显式出招,无随机补招;6 回合 ×10.2s=61.2s≥60 触发 Finish,比分 6:0 确定。