Add networked Fighter3D minigame
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
using Fighter3D.Core;
|
||||
using XWorld.Framework;
|
||||
|
||||
namespace XWorld.Server.Host.Tests
|
||||
{
|
||||
public class Fighter3DLogicTests
|
||||
{
|
||||
private static FighterState NewState()
|
||||
{
|
||||
return new FighterLogic().CreateInitial(
|
||||
new RoomConfig { GameId = "fighter3d", Version = 1, Seed = 1, PlayerCount = 2 },
|
||||
new DeterministicRandom(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialState_StartsTwoFightersMirrored()
|
||||
{
|
||||
var state = NewState();
|
||||
|
||||
Assert.Equal(FighterPhase.Running, state.Phase);
|
||||
Assert.Equal(1000, state.Fighters[0].Hp);
|
||||
Assert.Equal(1000, state.Fighters[1].Hp);
|
||||
Assert.True(state.Fighters[0].X < state.Fighters[1].X);
|
||||
Assert.Equal(1, state.Fighters[0].Facing);
|
||||
Assert.Equal(-1, state.Fighters[1].Facing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Movement_ClampsToStageBounds()
|
||||
{
|
||||
var logic = new FighterLogic();
|
||||
var state = NewState();
|
||||
|
||||
state = logic.Step(state, FighterInput.Controls(0, -1, FighterButtons.None), null).State;
|
||||
for (int i = 0; i < 600; i++)
|
||||
state = logic.Step(state, FighterInput.Tick(1f / FighterLogic.TickRate), null).State;
|
||||
|
||||
Assert.Equal(-FighterLogic.StageHalfWidth, state.Fighters[0].X, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LightAttack_HitsOpponentAndAppliesStun()
|
||||
{
|
||||
var logic = new FighterLogic();
|
||||
var state = NewState();
|
||||
state.Fighters[0].X = -0.4f;
|
||||
state.Fighters[1].X = 0.4f;
|
||||
|
||||
state = logic.Step(state, FighterInput.Controls(0, 0, FighterButtons.Light), null).State;
|
||||
for (int i = 0; i < 10; i++)
|
||||
state = logic.Step(state, FighterInput.Tick(1f / FighterLogic.TickRate), null).State;
|
||||
|
||||
Assert.True(state.Fighters[1].Hp < 1000);
|
||||
Assert.Equal(FighterAction.HitStun, state.Fighters[1].Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HoldingAway_BlocksIncomingAttack()
|
||||
{
|
||||
var logic = new FighterLogic();
|
||||
var state = NewState();
|
||||
state.Fighters[0].X = -0.4f;
|
||||
state.Fighters[1].X = 0.4f;
|
||||
|
||||
state = logic.Step(state, FighterInput.Controls(1, 1, FighterButtons.None), null).State;
|
||||
state = logic.Step(state, FighterInput.Controls(0, 0, FighterButtons.Light), null).State;
|
||||
for (int i = 0; i < 10; i++)
|
||||
state = logic.Step(state, FighterInput.Tick(1f / FighterLogic.TickRate), null).State;
|
||||
|
||||
Assert.Equal(1000, state.Fighters[1].Hp);
|
||||
Assert.Equal(FighterAction.BlockStun, state.Fighters[1].Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Timeout_WinnerIsHigherHealthSeat()
|
||||
{
|
||||
var logic = new FighterLogic();
|
||||
var state = NewState();
|
||||
state.Fighters[1].Hp = 900;
|
||||
|
||||
state = logic.Step(state, FighterInput.Tick(FighterLogic.RoundSeconds), null).State;
|
||||
|
||||
Assert.Equal(FighterPhase.Finished, state.Phase);
|
||||
Assert.Equal(0, state.WinnerSeat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeDecode_RoundTripsSnapshot()
|
||||
{
|
||||
var logic = new FighterLogic();
|
||||
var state = NewState();
|
||||
state.Tick = 123;
|
||||
state.ElapsedSeconds = 2.5f;
|
||||
state.Fighters[0].Hp = 777;
|
||||
state.Fighters[1].Action = FighterAction.BlockStun;
|
||||
state.Fighters[1].ActionFrame = 6;
|
||||
state.WinnerSeat = 1;
|
||||
|
||||
var back = logic.Decode(logic.Encode(state));
|
||||
|
||||
Assert.Equal(123, back.Tick);
|
||||
Assert.Equal(2.5f, back.ElapsedSeconds, 3);
|
||||
Assert.Equal(777, back.Fighters[0].Hp);
|
||||
Assert.Equal(FighterAction.BlockStun, back.Fighters[1].Action);
|
||||
Assert.Equal(6, back.Fighters[1].ActionFrame);
|
||||
Assert.Equal(1, back.WinnerSeat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Fighter3D.Core;
|
||||
using Fighter3D.Server;
|
||||
using XWorld.Framework;
|
||||
using XWorld.Framework.Protocol;
|
||||
using XWorld.Server.Host;
|
||||
|
||||
namespace XWorld.Server.Host.Tests
|
||||
{
|
||||
public class Fighter3DServerRoomTests
|
||||
{
|
||||
private sealed class FakeOutput : IRoomOutput
|
||||
{
|
||||
public readonly List<NetMessage> Broadcasts = new List<NetMessage>();
|
||||
public void Broadcast(NetMessage message) => Broadcasts.Add(message);
|
||||
public void SendTo(int playerId, NetMessage message) { }
|
||||
}
|
||||
|
||||
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 (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 NetMessage ControlsMsg(int axis, FighterButtons buttons)
|
||||
{
|
||||
var writer = new PacketWriter();
|
||||
writer.WriteVarInt(axis);
|
||||
writer.WriteByte((byte)buttons);
|
||||
return new NetMessage(FighterServerRoom.InputOpcode, writer.ToArray());
|
||||
}
|
||||
|
||||
private static FighterState LastSnapshot(FakeOutput output)
|
||||
{
|
||||
var last = output.Broadcasts[output.Broadcasts.Count - 1];
|
||||
Assert.Equal(FighterServerRoom.SnapshotOpcode, last.Opcode);
|
||||
return new FighterLogic().Decode(last.Payload);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnRoomStart_BroadcastsInitialSnapshot()
|
||||
{
|
||||
var (ctx, output, _, _) = BuildCtx();
|
||||
var room = new FighterServerRoom();
|
||||
|
||||
room.OnRoomStart(TwoHumans, ctx);
|
||||
|
||||
Assert.NotEmpty(output.Broadcasts);
|
||||
var snapshot = LastSnapshot(output);
|
||||
Assert.Equal(FighterPhase.Running, snapshot.Phase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedHeavyAttacks_CanKoAndEndRoomWithWinnerPid()
|
||||
{
|
||||
var (ctx, output, ended, result) = BuildCtx();
|
||||
var room = new FighterServerRoom();
|
||||
room.OnRoomStart(TwoHumans, ctx);
|
||||
|
||||
room.OnMessage(10, ControlsMsg(0, FighterButtons.Heavy));
|
||||
for (int i = 0; i < 800 && !ended[0]; i++)
|
||||
room.OnTick(1f / 20f);
|
||||
|
||||
Assert.True(ended[0]);
|
||||
Assert.NotNull(result[0]);
|
||||
Assert.Equal(10, result[0].WinnerPlayerId);
|
||||
Assert.Contains("Fighter3D", Encoding.UTF8.GetString(result[0].ResultBlob));
|
||||
|
||||
var snapshot = LastSnapshot(output);
|
||||
Assert.Equal(FighterPhase.Finished, snapshot.Phase);
|
||||
Assert.Equal(0, snapshot.WinnerSeat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@
|
||||
<ProjectReference Include="..\PublishTool\PublishTool.csproj" />
|
||||
<ProjectReference Include="..\games-src\RPS\RPS.Core\RPS.Core.csproj" />
|
||||
<ProjectReference Include="..\games-src\RPS\RPS.Server\RPS.Server.csproj" />
|
||||
<ProjectReference Include="..\games-src\Fighter3D\Fighter3D.Core\Fighter3D.Core.csproj" />
|
||||
<ProjectReference Include="..\games-src\Fighter3D\Fighter3D.Server\Fighter3D.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -33,4 +35,4 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user