Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user