234 lines
8.9 KiB
C#
234 lines
8.9 KiB
C#
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));
|
||
}
|
||
}
|
||
}
|