Files
2026-07-10 10:24:29 +08:00

234 lines
8.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<NetMessage> Broadcasts = new List<NetMessage>();
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<PlayerInfo> TwoHumans = new[]
{
new PlayerInfo { PlayerId = 10, Name = "A", IsAI = false },
new PlayerInfo { PlayerId = 20, Name = "B", IsAI = false },
};
private static readonly IReadOnlyList<PlayerInfo> 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));
}
}
}