Refine ship orders and viewer controls
This commit is contained in:
153
tests/backend/ShipAiServiceExecutionTests.cs
Normal file
153
tests/backend/ShipAiServiceExecutionTests.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using FluentAssertions;
|
||||
using SpaceGame.Api.Definitions;
|
||||
using SpaceGame.Api.Industry.Planning;
|
||||
using SpaceGame.Api.Shared.Runtime;
|
||||
using SpaceGame.Api.Ships.AI;
|
||||
using SpaceGame.Api.Ships.Runtime;
|
||||
using SpaceGame.Api.Universe.Contracts;
|
||||
using SpaceGame.Api.Universe.Runtime;
|
||||
using SpaceGame.Api.Universe.Simulation;
|
||||
using Xunit;
|
||||
|
||||
namespace SpaceGame.Api.Tests;
|
||||
|
||||
public sealed class ShipAiServiceExecutionTests
|
||||
{
|
||||
[Fact]
|
||||
public void MoveOrder_CompletesAndIsRemovedAfterArrival()
|
||||
{
|
||||
var ship = new ShipRuntime
|
||||
{
|
||||
Id = "ship-1",
|
||||
SystemId = "sys-1",
|
||||
Definition = CreateShipDefinition(),
|
||||
FactionId = "player",
|
||||
Position = Vector3.Zero,
|
||||
TargetPosition = Vector3.Zero,
|
||||
SpatialState = new ShipSpatialStateRuntime
|
||||
{
|
||||
CurrentSystemId = "sys-1",
|
||||
CurrentAnchorId = null,
|
||||
LocalPosition = Vector3.Zero,
|
||||
SystemPosition = Vector3.Zero,
|
||||
},
|
||||
DefaultBehavior = new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = ShipBehaviorKinds.Idle,
|
||||
},
|
||||
Skills = new ShipSkillProfileRuntime
|
||||
{
|
||||
Navigation = 3,
|
||||
Trade = 3,
|
||||
Mining = 3,
|
||||
Combat = 3,
|
||||
Construction = 3,
|
||||
},
|
||||
Health = 100f,
|
||||
};
|
||||
|
||||
ship.OrderQueue.EnqueuePlayerOrder(new ShipOrderRuntime
|
||||
{
|
||||
Id = "move-1",
|
||||
Kind = ShipOrderKinds.Move,
|
||||
SourceKind = ShipOrderSourceKind.Player,
|
||||
SourceId = "test",
|
||||
Label = "Fly to point",
|
||||
TargetSystemId = "sys-1",
|
||||
TargetPosition = new Vector3(5f, 0f, 0f),
|
||||
});
|
||||
|
||||
var world = CreateWorld(ship);
|
||||
var service = new ShipAiService(new TestBalanceService());
|
||||
var events = new List<SimulationEventRecord>();
|
||||
|
||||
service.UpdateShip(world, ship, 1f, events);
|
||||
service.UpdateShip(world, ship, 0.01f, events);
|
||||
|
||||
ship.Position.Should().Be(new Vector3(5f, 0f, 0f));
|
||||
ship.OrderQueue.Should().BeEmpty();
|
||||
ship.ActiveOrderId.Should().BeNull();
|
||||
}
|
||||
|
||||
private static SimulationWorld CreateWorld(ShipRuntime ship)
|
||||
{
|
||||
return new SimulationWorld
|
||||
{
|
||||
Label = "test",
|
||||
Seed = 1,
|
||||
Systems =
|
||||
[
|
||||
new SystemRuntime
|
||||
{
|
||||
Definition = new SolarSystemDefinition
|
||||
{
|
||||
Id = "sys-1",
|
||||
Label = "System 1",
|
||||
Position = [0f, 0f, 0f],
|
||||
Stars = [],
|
||||
AsteroidField = new AsteroidFieldDefinition(),
|
||||
ResourceNodes = [],
|
||||
Planets = [],
|
||||
},
|
||||
Position = Vector3.Zero,
|
||||
}
|
||||
],
|
||||
Anchors = [],
|
||||
Nodes = [],
|
||||
Celestials = [],
|
||||
Wrecks = [],
|
||||
Stations = [],
|
||||
Ships = [ship],
|
||||
Factions = [],
|
||||
Commanders = [],
|
||||
Claims = [],
|
||||
ConstructionSites = [],
|
||||
MarketOrders = [],
|
||||
Policies = [],
|
||||
ShipDefinitions = new Dictionary<string, ShipDefinition>(StringComparer.Ordinal),
|
||||
ItemDefinitions = new Dictionary<string, ItemDefinition>(StringComparer.Ordinal),
|
||||
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(StringComparer.Ordinal),
|
||||
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(StringComparer.Ordinal),
|
||||
Recipes = new Dictionary<string, RecipeDefinition>(StringComparer.Ordinal),
|
||||
ProductionGraph = new ProductionGraph
|
||||
{
|
||||
Commodities = new Dictionary<string, ProductionCommodityNode>(StringComparer.Ordinal),
|
||||
Processes = new Dictionary<string, ProductionProcessNode>(StringComparer.Ordinal),
|
||||
ProcessesByOutputId = new Dictionary<string, IReadOnlyList<ProductionProcessNode>>(StringComparer.Ordinal),
|
||||
ProcessesByInputId = new Dictionary<string, IReadOnlyList<ProductionProcessNode>>(StringComparer.Ordinal),
|
||||
OutputsByModuleId = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal),
|
||||
},
|
||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
private static ShipDefinition CreateShipDefinition()
|
||||
{
|
||||
return new ShipDefinition
|
||||
{
|
||||
Id = "ship-def",
|
||||
Name = "Test Ship",
|
||||
Size = "small",
|
||||
Hull = 100f,
|
||||
Purpose = ShipPurpose.Trade,
|
||||
Type = ShipType.Courier,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestBalanceService : IBalanceService
|
||||
{
|
||||
public float SimulationSpeedMultiplier => 1f;
|
||||
public float YPlane => 0f;
|
||||
public float ArrivalThreshold => 1f;
|
||||
public float MiningRate => 10f;
|
||||
public float MiningCycleSeconds => 1f;
|
||||
public float TransferRate => 10f;
|
||||
public float DockingDuration => 0.1f;
|
||||
public float UndockingDuration => 0.1f;
|
||||
public float UndockDistance => 10f;
|
||||
|
||||
public BalanceOptions GetCurrent() => new();
|
||||
|
||||
public BalanceOptions Update(BalanceOptions candidate) => candidate;
|
||||
}
|
||||
}
|
||||
140
tests/backend/ShipOrderQueueTests.cs
Normal file
140
tests/backend/ShipOrderQueueTests.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using FluentAssertions;
|
||||
using SpaceGame.Api.Shared.Runtime;
|
||||
using SpaceGame.Api.Ships.Runtime;
|
||||
using Xunit;
|
||||
|
||||
namespace SpaceGame.Api.Tests;
|
||||
|
||||
public sealed class ShipOrderQueueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Enqueue_Throws_WhenQueueIsFull()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
foreach (var index in Enumerable.Range(0, ShipOrderQueue.MaxOrders))
|
||||
{
|
||||
queue.Enqueue(CreateOrder($"order-{index}", ShipOrderSourceKind.Player, priority: index));
|
||||
}
|
||||
|
||||
var act = () => queue.Enqueue(CreateOrder("overflow", ShipOrderSourceKind.Player));
|
||||
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Order queue is full.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentOrder_UsesQueueOrder()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
queue.EnqueuePlayerOrder(CreateOrder("player-first", ShipOrderSourceKind.Player));
|
||||
queue.EnqueueManagedOrder(CreateOrder("commander-second", ShipOrderSourceKind.Commander, priority: 100));
|
||||
queue.EnqueueManagedOrder(CreateOrder("behavior-third", ShipOrderSourceKind.Behavior, priority: 999));
|
||||
|
||||
var currentOrder = queue.GetCurrentOrder();
|
||||
|
||||
currentOrder.Should().NotBeNull();
|
||||
currentOrder!.Id.Should().Be("player-first");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCurrentOrder_IgnoresTerminalStatuses()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
queue.EnqueuePlayerOrder(CreateOrder("completed-player", ShipOrderSourceKind.Player, priority: 100, status: OrderStatus.Completed));
|
||||
queue.EnqueueManagedOrder(CreateOrder("active-commander", ShipOrderSourceKind.Commander, priority: 1, status: OrderStatus.Active));
|
||||
|
||||
var currentOrder = queue.GetCurrentOrder();
|
||||
|
||||
currentOrder.Should().NotBeNull();
|
||||
currentOrder!.Id.Should().Be("active-commander");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnqueuePlayerOrder_InsertsBeforeManagedOrders()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
queue.EnqueueManagedOrder(CreateOrder("behavior-1", ShipOrderSourceKind.Behavior));
|
||||
queue.EnqueuePlayerOrder(CreateOrder("player-1", ShipOrderSourceKind.Player));
|
||||
queue.EnqueueManagedOrder(CreateOrder("behavior-2", ShipOrderSourceKind.Commander));
|
||||
queue.EnqueuePlayerOrder(CreateOrder("player-2", ShipOrderSourceKind.Player));
|
||||
|
||||
queue.Select(order => order.Id).Should().Equal("player-1", "player-2", "behavior-1", "behavior-2");
|
||||
queue.FindLeadingOrderForSource(ShipOrderSourceKind.Player)!.Id.Should().Be("player-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOrReplaceManagedOrder_ReplacesMatchingOrderWithoutGrowingQueue()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
queue.EnqueueManagedOrder(CreateOrder("order-1", ShipOrderSourceKind.Behavior, label: "Initial label"));
|
||||
|
||||
queue.AddOrReplaceManagedOrder(CreateOrder("order-1", ShipOrderSourceKind.Behavior, label: "Updated label", priority: 7));
|
||||
|
||||
queue.Count.Should().Be(1);
|
||||
queue.FindById("order-1").Should().NotBeNull();
|
||||
queue.FindById("order-1")!.Label.Should().Be("Updated label");
|
||||
queue.FindById("order-1")!.Priority.Should().Be(7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveById_RemovesMatchingOrder()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
queue.EnqueuePlayerOrder(CreateOrder("remove-me", ShipOrderSourceKind.Player));
|
||||
queue.EnqueuePlayerOrder(CreateOrder("keep-me", ShipOrderSourceKind.Player));
|
||||
|
||||
var removed = queue.RemoveById("remove-me");
|
||||
|
||||
removed.Should().BeTrue();
|
||||
queue.Select(order => order.Id).Should().ContainSingle().Which.Should().Be("keep-me");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryMovePlayerOrder_ReordersWithinPlayerSegment()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
queue.EnqueuePlayerOrder(CreateOrder("player-1", ShipOrderSourceKind.Player));
|
||||
queue.EnqueuePlayerOrder(CreateOrder("player-2", ShipOrderSourceKind.Player));
|
||||
queue.EnqueueManagedOrder(CreateOrder("behavior-1", ShipOrderSourceKind.Behavior));
|
||||
|
||||
var moved = queue.TryMovePlayerOrder("player-2", 0);
|
||||
|
||||
moved.Should().BeTrue();
|
||||
queue.Select(order => order.Id).Should().Equal("player-2", "player-1", "behavior-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCompleteOrder_RemovesCompletedDirectOrderFromQueue()
|
||||
{
|
||||
var queue = new ShipOrderQueue();
|
||||
queue.EnqueuePlayerOrder(CreateOrder("direct-order", ShipOrderSourceKind.Player, status: OrderStatus.Active));
|
||||
queue.EnqueueManagedOrder(CreateOrder("behavior-order", ShipOrderSourceKind.Behavior));
|
||||
|
||||
var completed = queue.TryCompleteOrder("direct-order");
|
||||
|
||||
completed.Should().BeTrue();
|
||||
queue.FindById("direct-order").Should().BeNull();
|
||||
queue.GetCurrentOrder()!.Id.Should().Be("behavior-order");
|
||||
}
|
||||
|
||||
private static ShipOrderRuntime CreateOrder(
|
||||
string id,
|
||||
ShipOrderSourceKind sourceKind,
|
||||
int priority = 0,
|
||||
OrderStatus status = OrderStatus.Queued,
|
||||
string? label = null,
|
||||
DateTimeOffset? createdAtUtc = null)
|
||||
{
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = id,
|
||||
Kind = "test-order",
|
||||
SourceKind = sourceKind,
|
||||
SourceId = $"{sourceKind}-source",
|
||||
Priority = priority,
|
||||
Status = status,
|
||||
Label = label,
|
||||
CreatedAtUtc = createdAtUtc ?? new DateTimeOffset(2026, 4, 8, 12, 0, 0, TimeSpan.Zero),
|
||||
};
|
||||
}
|
||||
}
|
||||
28
tests/backend/SpaceGame.Api.Tests.csproj
Normal file
28
tests/backend/SpaceGame.Api.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.1.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../apps/backend/SpaceGame.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user