Update .gitignore to exclude TypeScript build info and add dist directory. Expand README with project overview, technology stack, prerequisites, and instructions for running and testing the application.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user