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

201 lines
9.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
using XWorld.Framework;
using XWorld.Framework.Protocol;
using XWorld.Server.Gateway;
namespace XWorld.Server.Gateway.Tests
{
public class RpsEndToEndTests
{
// RPS ChoiceOpcode = 1 (same as SnapshotOpcode from RpsServerRoom)
private const ushort RpsChoiceOpcode = 1;
private static Frame RpsJoinFrame()
=> new Frame(Channel.Framework, (ushort)FrameworkOpcode.MatchRequest,
new MatchRequestMsg { GameId = "rps", Version = 1 }.Encode());
private static Frame RpsChoiceFrame(byte choice)
{
var w = new PacketWriter();
w.WriteByte(choice);
return new Frame(Channel.Game, RpsChoiceOpcode, w.ToArray());
}
/// <summary>
/// Two real clients match → both receive MatchFound → play choices → both receive RoomEnd
/// Uses large logicalDt so 60s RPS finishes in ~6 ticks (≈120ms wall-clock with 20ms interval)
/// </summary>
[Fact]
public async Task TwoHumans_MatchAndPlayToRoomEnd()
{
var host = new GameServerHost();
// matchTimeoutTicks=200 (generous, will fill by count=2)
// logicalDt=11f: each tick advances 11 logical seconds → ~6 ticks > 60s
await host.StartAsync(TestGames.Root, tickIntervalMs: 20,
reconnectWindowTicks: 100, matchTimeoutTicks: 200, logicalDt: 11f);
try
{
using var client1 = new WsTestClient();
using var client2 = new WsTestClient();
await client1.ConnectAsync(host.WsBaseUrl, pid: 1);
await client2.ConnectAsync(host.WsBaseUrl, pid: 2);
await client1.SendFrameAsync(RpsJoinFrame());
await client2.SendFrameAsync(RpsJoinFrame());
// Both clients need to collect frames until MatchFound, then send choices, then wait for RoomEnd
bool c1MatchFound = false, c2MatchFound = false;
bool c1RoomEnd = false, c2RoomEnd = false;
string c1RoomId = null, c2RoomId = null;
RoomEndMsg c1End = null, c2End = null;
// Receive frames from both clients concurrently
var t1 = Task.Run(async () =>
{
for (int i = 0; i < 60 && !c1RoomEnd; i++)
{
Frame f;
try { f = await client1.ReceiveFrameAsync(3000); }
catch (TimeoutException) { break; }
catch (Exception) { break; }
if (f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchFound)
{
c1MatchFound = true;
var mf = MatchFoundMsg.Decode(f.Payload);
c1RoomId = mf.RoomId;
// Send Rock choice after matched
await client1.SendFrameAsync(RpsChoiceFrame(1));
}
else if (f.Channel == Channel.Game)
{
// Got a snapshot; keep sending Rock in case we're in Choosing phase
await client1.SendFrameAsync(RpsChoiceFrame(1));
}
else if (f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.RoomEnd)
{
c1End = RoomEndMsg.Decode(f.Payload);
c1RoomEnd = true;
}
}
});
var t2 = Task.Run(async () =>
{
for (int i = 0; i < 60 && !c2RoomEnd; i++)
{
Frame f;
try { f = await client2.ReceiveFrameAsync(3000); }
catch (TimeoutException) { break; }
catch (Exception) { break; }
if (f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchFound)
{
c2MatchFound = true;
var mf = MatchFoundMsg.Decode(f.Payload);
c2RoomId = mf.RoomId;
await client2.SendFrameAsync(RpsChoiceFrame(2)); // Paper
}
else if (f.Channel == Channel.Game)
{
await client2.SendFrameAsync(RpsChoiceFrame(2)); // Paper
}
else if (f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.RoomEnd)
{
c2End = RoomEndMsg.Decode(f.Payload);
c2RoomEnd = true;
}
}
});
await Task.WhenAll(t1, t2);
Assert.True(c1MatchFound, "client1 should receive MatchFound");
Assert.True(c2MatchFound, "client2 should receive MatchFound");
Assert.NotNull(c1RoomId);
Assert.Equal(c1RoomId, c2RoomId); // Both in same room
Assert.True(c1RoomEnd, "client1 should receive RoomEnd");
Assert.True(c2RoomEnd, "client2 should receive RoomEnd");
// 结算结果: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.Equal(c1End.ResultBlob, c2End.ResultBlob); // 网关按房间统一下发同一结算
Assert.Contains(":", System.Text.Encoding.UTF8.GetString(c1End.ResultBlob)); // 比分/平局文本
await client1.CloseAsync();
await client2.CloseAsync();
}
finally { await host.StopAsync(); }
}
/// <summary>
/// Single client + small matchTimeoutTicks → AI fill → client receives MatchFound with AI player → RoomEnd
/// </summary>
[Fact]
public async Task SingleClient_AiFill_MatchAndPlayToRoomEnd()
{
var host = new GameServerHost();
// matchTimeoutTicks=3: after ~3 ticks (60ms at 20ms interval), AI fills in
// logicalDt=11f: large logical time step to finish RPS in ~6 ticks
await host.StartAsync(TestGames.Root, tickIntervalMs: 20,
reconnectWindowTicks: 100, matchTimeoutTicks: 3, logicalDt: 11f);
try
{
using var client = new WsTestClient();
await client.ConnectAsync(host.WsBaseUrl, pid: 7);
await client.SendFrameAsync(RpsJoinFrame());
bool matchFound = false;
bool hasAiPlayer = false;
bool roomEnd = false;
RoomEndMsg end = null;
for (int i = 0; i < 60 && !roomEnd; i++)
{
Frame f;
try { f = await client.ReceiveFrameAsync(3000); }
catch (TimeoutException) { break; }
catch (Exception) { break; }
if (f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.MatchFound)
{
matchFound = true;
var mf = MatchFoundMsg.Decode(f.Payload);
Assert.Equal(2, mf.Players.Count);
foreach (var p in mf.Players)
if (p.IsAI) { hasAiPlayer = true; Assert.True(p.PlayerId < 0); }
// Send Rock choice after matched
await client.SendFrameAsync(RpsChoiceFrame(1));
}
else if (f.Channel == Channel.Game)
{
// Keep sending Rock on each snapshot
await client.SendFrameAsync(RpsChoiceFrame(1));
}
else if (f.Channel == Channel.Framework && f.Opcode == (ushort)FrameworkOpcode.RoomEnd)
{
end = RoomEndMsg.Decode(f.Payload);
roomEnd = true;
}
}
Assert.True(matchFound, "Should receive MatchFound (with AI fill)");
Assert.True(hasAiPlayer, "MatchFound should include an AI player");
Assert.True(roomEnd, "Should receive RoomEnd after AI-fill game settles");
Assert.NotNull(end);
Assert.True(end.ResultBlob != null && end.ResultBlob.Length > 0, "AI 对局的 RoomEnd 也应携带结算 blob");
await client.CloseAsync();
}
finally { await host.StopAsync(); }
}
}
}