225 lines
6.2 KiB
C#
225 lines
6.2 KiB
C#
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<EntitySnapshot>();
|
|
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<InvalidOperationException>(() => 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<EntitySnapshot>();
|
|
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<EntitySnapshot>();
|
|
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<ObjectDisposedException>(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));
|
|
}
|
|
}
|