using System.Collections.Generic; using System.Text; using Xunit; using XWorld.Framework; using XWorld.Framework.Protocol; using XWorld.Server.Host; using RPS.Core; using RPS.Server; namespace XWorld.Server.Host.Tests { public class RpsServerRoomTests { // ── helpers ──────────────────────────────────────────────────────── private sealed class FakeOutput : IRoomOutput { public readonly List Broadcasts = new List(); public void Broadcast(NetMessage m) => Broadcasts.Add(m); public void SendTo(int playerId, NetMessage m) { } } // Decode the last broadcast snapshot private static RpsState LastSnapshot(FakeOutput cap) { var last = cap.Broadcasts[cap.Broadcasts.Count - 1]; return new RpsLogic().Decode(last.Payload); } // Build a NetMessage carrying a Choice byte (opcode=1) private static NetMessage ChoiceMsg(Choice c) { var w = new PacketWriter(); w.WriteByte((byte)c); return new NetMessage(RpsServerRoom.ChoiceOpcode, w.ToArray()); } // Build a RoomCtx backed by FakeOutput; captures endRoom callback. 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); } private static readonly IReadOnlyList TwoHumans = new[] { new PlayerInfo { PlayerId = 10, Name = "A", IsAI = false }, new PlayerInfo { PlayerId = 20, Name = "B", IsAI = false }, }; private static readonly IReadOnlyList HumanAndAi = new[] { new PlayerInfo { PlayerId = 10, Name = "H", IsAI = false }, new PlayerInfo { PlayerId = -1, Name = "AI", IsAI = true }, }; // ── T2-1: 2 human seats play a round and score correctly ────────── [Fact] public void TwoHumans_PlayRound_ScoresUpdateByRules() { var (ctx, output, _, _) = BuildCtx(); var room = new RpsServerRoom(); room.OnRoomStart(TwoHumans, ctx); // Both humans choose: seat0=Rock, seat1=Scissors → seat0 should win room.OnMessage(10, ChoiceMsg(Choice.Rock)); room.OnMessage(20, ChoiceMsg(Choice.Scissors)); // Advance past choosing timeout (5s) → round resolves room.OnTick(5.1f); Assert.True(output.Broadcasts.Count > 0); var snap = LastSnapshot(output); Assert.Equal(RpsPhase.Revealing, snap.Phase); Assert.Equal(1, snap.Scores[0]); Assert.Equal(0, snap.Scores[1]); } // ── T2-2: 2 human seats, reaching 60s ends the room ────────────── [Fact] public void TwoHumans_Reach60s_EndsRoom() { var (ctx, output, ended, _) = BuildCtx(); var room = new RpsServerRoom(); room.OnRoomStart(TwoHumans, ctx); // Give seat0 Rock, seat1 Scissors so seat0 leads after round 1 room.OnMessage(10, ChoiceMsg(Choice.Rock)); room.OnMessage(20, ChoiceMsg(Choice.Scissors)); // Advance past choosing → Revealing room.OnTick(5.1f); // Advance past reveal → next round choosing room.OnTick(5.1f); // One huge tick past the 60s total → game finishes room.OnTick(60f); Assert.True(ended[0], "EndRoom must be called when total elapsed >= 60s"); var snap = LastSnapshot(output); Assert.Equal(RpsPhase.Finished, snap.Phase); Assert.Equal(0, snap.Winner); // seat0 won the only scored round } // ── T2-3: 1 human + 1 AI: AI auto-chooses, game settles ───────── [Fact] public void HumanAndAi_AutoAdvances_AndSettles() { var (ctx, output, ended, _) = BuildCtx(); var room = new RpsServerRoom(); room.OnRoomStart(HumanAndAi, ctx); // Human (seat0) chooses; AI (seat1 IsAI=true) auto-picks on first OnTick room.OnMessage(10, ChoiceMsg(Choice.Rock)); // Tick past choosing (AI will have auto-chosen by now) → Revealing room.OnTick(5.1f); var snap1 = LastSnapshot(output); Assert.Equal(RpsPhase.Revealing, snap1.Phase); // AI must have picked something (not None) Assert.NotEqual(Choice.None, snap1.Choices[1]); // Tick past reveal → next round Choosing room.OnTick(5.1f); // Now push total time over 60s → game ends room.OnTick(60f); Assert.True(ended[0], "EndRoom must be called with AI seat"); var snap = LastSnapshot(output); Assert.Equal(RpsPhase.Finished, snap.Phase); // Winner is 0, 1, or 2 (draw) — just must be valid Assert.True(snap.Winner >= 0 && snap.Winner <= 2); } // ── T2-4: broadcasts start immediately on OnRoomStart ──────────── [Fact] public void OnRoomStart_BroadcastsInitialSnapshot() { var (ctx, output, _, _) = BuildCtx(); var room = new RpsServerRoom(); room.OnRoomStart(TwoHumans, ctx); Assert.True(output.Broadcasts.Count >= 1); var snap = LastSnapshot(output); Assert.Equal(1, snap.Round); Assert.Equal(RpsPhase.Choosing, snap.Phase); } // ── T2-5: second choose from same seat is ignored (lock) ───────── [Fact] public void SecondChoiceFromSameSeat_IsIgnored() { var (ctx, output, _, _) = BuildCtx(); var room = new RpsServerRoom(); room.OnRoomStart(TwoHumans, ctx); room.OnMessage(10, ChoiceMsg(Choice.Rock)); room.OnMessage(10, ChoiceMsg(Choice.Scissors)); // should be ignored // Advance past choosing timeout → resolve; seat0 chose Rock room.OnMessage(20, ChoiceMsg(Choice.Scissors)); room.OnTick(5.1f); var snap = LastSnapshot(output); Assert.Equal(Choice.Rock, snap.Choices[0]); // locked to Rock Assert.Equal(1, snap.Scores[0]); // Rock beats Scissors } // ── 结算结果:胜者 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)); } } }