using HSchool.Protocol; namespace HSchool.Simulation.Tests; public class GameWorldTests { private static SimulationOptions Options() => new() { TickRate = 20, WorldWidth = 1000f, WorldHeight = 1000f, PlayerSpeed = 200f, PlayerRadius = 10f, }; private static EntitySnapshot Entity(GameWorld world, uint networkId) { var buffer = new List(); world.CaptureSnapshot(buffer); return buffer.Single(entity => entity.Id == networkId); } [Fact] public void Tick_AdvancesTheTickCounter() { using var world = new GameWorld(Options()); world.Tick(); world.Tick(); Assert.Equal(2u, world.CurrentTick); } [Fact] public void SpawnPlayer_AddsAPlayerEntityToSnapshots() { using var world = new GameWorld(Options()); var networkId = world.SpawnPlayer(playerId: 1); Assert.Equal(1, world.PlayerCount); Assert.Equal(EntityKind.Player, Entity(world, networkId).Kind); } [Fact] public void SpawnPlayer_Twice_Throws() { using var world = new GameWorld(Options()); world.SpawnPlayer(playerId: 1); Assert.Throws(() => world.SpawnPlayer(playerId: 1)); } [Fact] public void Input_MovesThePlayerAtExactlySpeedTimesDelta() { var options = Options(); using var world = new GameWorld(options); var networkId = world.SpawnPlayer(playerId: 1); var startX = Entity(world, networkId).X; world.ApplyInput(playerId: 1, InputButtons.Right, sequence: 1); world.Tick(); var expected = startX + (options.PlayerSpeed * options.FixedDeltaTime); Assert.Equal(expected, Entity(world, networkId).X, tolerance: 0.001f); } [Fact] public void DiagonalInput_IsNotFasterThanCardinal() { var options = Options(); using var world = new GameWorld(options); var straight = world.SpawnPlayer(playerId: 1); var diagonal = world.SpawnPlayer(playerId: 2); world.ApplyInput(playerId: 1, InputButtons.Right, sequence: 1); world.ApplyInput(playerId: 2, InputButtons.Right | InputButtons.Down, sequence: 1); world.Tick(); var straightBefore = Entity(world, straight); var diagonalBefore = Entity(world, diagonal); world.Tick(); var straightStep = Distance(straightBefore, Entity(world, straight)); var diagonalStep = Distance(diagonalBefore, Entity(world, diagonal)); Assert.Equal(straightStep, diagonalStep, tolerance: 0.01f); } [Fact] public void Player_StopsAtTheWorldBounds() { var options = Options(); using var world = new GameWorld(options); var networkId = world.SpawnPlayer(playerId: 1); world.ApplyInput(playerId: 1, InputButtons.Left, sequence: 1); for (var i = 0; i < 200; i++) { world.Tick(); } Assert.Equal(options.PlayerRadius, Entity(world, networkId).X, tolerance: 0.001f); } [Fact] public void StaleInput_IsIgnored() { using var world = new GameWorld(Options()); var networkId = world.SpawnPlayer(playerId: 1); var startX = Entity(world, networkId).X; world.ApplyInput(playerId: 1, InputButtons.None, sequence: 10); world.ApplyInput(playerId: 1, InputButtons.Right, sequence: 2); world.Tick(); Assert.Equal(startX, Entity(world, networkId).X, tolerance: 0.001f); } [Fact] public void InputForAnUnknownPlayer_IsIgnored() { using var world = new GameWorld(Options()); world.ApplyInput(playerId: 999, InputButtons.Right, sequence: 1); world.Tick(); Assert.Equal(0, world.PlayerCount); } [Fact] public void DespawnPlayer_RemovesItFromSnapshots() { using var world = new GameWorld(Options()); var networkId = world.SpawnPlayer(playerId: 1); world.DespawnPlayer(playerId: 1); var buffer = new List(); world.CaptureSnapshot(buffer); Assert.Equal(0, world.PlayerCount); Assert.DoesNotContain(buffer, entity => entity.Id == networkId); Assert.Null(world.GetNetworkId(playerId: 1)); } [Fact] public void DespawnPlayer_Twice_IsHarmless() { using var world = new GameWorld(Options()); world.SpawnPlayer(playerId: 1); world.DespawnPlayer(playerId: 1); world.DespawnPlayer(playerId: 1); Assert.Equal(0, world.PlayerCount); } [Fact] public void NetworkIds_AreNotRecycledAfterDespawn() { using var world = new GameWorld(Options()); var first = world.SpawnPlayer(playerId: 1); world.DespawnPlayer(playerId: 1); var second = world.SpawnPlayer(playerId: 1); Assert.NotEqual(first, second); } [Fact] public void EmptyWorld_StillContainsTheStaticObstacles() { using var world = new GameWorld(Options()); var buffer = new List(); world.CaptureSnapshot(buffer); Assert.NotEmpty(buffer); Assert.All(buffer, entity => Assert.Equal(EntityKind.Obstacle, entity.Kind)); } [Fact] public void Simulation_IsDeterministicForTheSameInputs() { var first = Run(); var second = Run(); Assert.Equal(first, second); static (float X, float Y) Run() { using var world = new GameWorld(Options()); var networkId = world.SpawnPlayer(playerId: 3); for (var i = 0; i < 25; i++) { world.ApplyInput(playerId: 3, i % 2 == 0 ? InputButtons.Right : InputButtons.Down, (uint)i + 1); world.Tick(); } var entity = Entity(world, networkId); return (entity.X, entity.Y); } } [Fact] public void UsingADisposedWorld_Throws() { var world = new GameWorld(Options()); world.Dispose(); Assert.Throws(world.Tick); } private static float Distance(EntitySnapshot from, EntitySnapshot to) { var dx = to.X - from.X; var dy = to.Y - from.Y; return MathF.Sqrt((dx * dx) + (dy * dy)); } }