23 KiB
结算结果传递(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<RoomEndResult>):
[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):
// 结束时带结算结果的房间
private sealed class ResultRoom : IGameServerRoom
{
public RoomEndResult Result;
private IRoomCtx _ctx;
public void OnRoomStart(IReadOnlyList<PlayerInfo> 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 全文替换为:
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<PlayerInfo> 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<RoomEndResult> _endRoom;末尾两方法:
public void EndRoom() => _endRoom(null);
public void EndRoom(RoomEndResult result) => _endRoom(result);
Server/Server.Host/Room.cs:RequestEnd 处(第 24-26 行)改为:
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+ 新EndedRoomstruct) - 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 行)改为:
[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<EndedRoom>();
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 类之前)加:
// 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 行)替换为:
public IReadOnlyList<EndedRoom> TickAll(float dt)
{
List<EndedRoom> ended = null;
foreach (var kv in _rooms)
{
var room = kv.Value.Room;
if (!room.IsActive) { (ended ??= new List<EndedRoom>()).Add(new EndedRoom(kv.Key, room.EndResult)); continue; }
room.Tick(dt);
if (!room.IsActive) (ended ??= new List<EndedRoom>()).Add(new EndedRoom(kv.Key, room.EndResult));
}
if (ended != null)
foreach (var e in ended) ReleaseRoom(e.RoomId);
return ended ?? (IReadOnlyList<EndedRoom>)System.Array.Empty<EndedRoom>();
}
- Step 4: 实现 ServerLoop 接线
Server/Gateway/ServerLoop.cs 三处:
第 79-80 行:
var ended = _host.TickAll(dt);
for (int i = 0; i < ended.Count; i++) OnRoomEnded(ended[i].RoomId, ended[i].Result);
第 405 行("Start 即结束"路径,CreateRoomForSeats 持有 Room 引用):
if (!room.IsActive) OnRoomEnded(roomId, room.EndResult); // 极端:Start 即结束
OnRoomEnded(第 408-421 行)替换为:
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>()
};
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 行)改为四元组、捕获结果:
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;,追加两个新测试:
// ── 结算结果:胜者 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 之后)加:
private readonly int[] _pidOfSeat = new int[2]; // seat->playerId(结算用;AI 也有 pid)
OnRoomStart 循环体(第 23-27 行)改为:
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 行)改为:
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 非空):
[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 行)改:
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 行后)加:
// 结算结果: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 行后)加:
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(编译验证壳,整目录<Compile Include>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(...) 之前加一行:
string resultText = DecodeResultBlob(roomEnd.ResultBlob);
Title/Message 两处 CreateText 调用改为传入 resultText:
Text title = CreateText("Title", panel.transform, ResultTitle(roomEnd, resultText), 34, FontStyle.Bold, TextAnchor.MiddleCenter);
Text message = CreateText("Message", panel.transform, ResultMessage(roomEnd, resultText), 22, FontStyle.Normal, TextAnchor.MiddleCenter);
- Step 2: 替换 ResultTitle / ResultMessage
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: 告知用户的人工步骤(计划执行者原样转达,不代做)
- Unity Editor 打开工程让其重新编译(Framework.Shared 源码变了)。
- 菜单「XWorld/生成小游戏更新」重发 RPS 服务器模块到
deploy/minigame/rps/<ver>/(旧包不崩,但弹框无结算内容)。 - 客户端
XWorld.Framework.Shared+XWorld.Link热更 DLL 重出(真机);Editor Play 本地即可验。 - 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 确定。