Planner no longer writes local physics formulas. All kinematics/geometry/damage go through shared utility classes (Kinematics/RouteGeometry/CloudExpansionModel/DamageAssessment). Core: RouteGeometry (route geometry), PlannerConfig + planner_config.json (config externalization). Planner route-aware layout (offset along tangent), cloud overlap 20pct. DroneEntity arc-length driven. Fixes: PathInSphere cloud-frame correction (root cause), GaussianPuffDispersion hardcoded Sunny, ComputeEffectiveRadius cloud age, planner wind offset, remove drone wind bias. Tests 167 to 191, 41s. Windy scenarios pass.
192 lines
7.7 KiB
C#
192 lines
7.7 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using CounterDrone.Core;
|
||
using CounterDrone.Core.Algorithms;
|
||
using CounterDrone.Core.Models;
|
||
using CounterDrone.Core.Repository;
|
||
using CounterDrone.Core.Services;
|
||
using CounterDrone.Core.Simulation;
|
||
using Xunit;
|
||
|
||
namespace CounterDrone.Core.Tests
|
||
{
|
||
/// <summary>仿真引擎基础单元测试(不含集成场景,集成场景见 FullPipelineTests)</summary>
|
||
public class SimulationEngineTests : IDisposable
|
||
{
|
||
private readonly string _testDir;
|
||
private readonly SimulationEngine _engine;
|
||
private readonly IScenarioService _scenarioService;
|
||
private readonly SQLite.SQLiteConnection _mainDb;
|
||
private string _taskId = string.Empty;
|
||
|
||
public SimulationEngineTests()
|
||
{
|
||
_testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}");
|
||
var paths = new TestPathProvider(_testDir);
|
||
var dbManager = new DatabaseManager(paths);
|
||
_mainDb = dbManager.OpenMainDb();
|
||
foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a);
|
||
|
||
_scenarioService = new ScenarioService(
|
||
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
|
||
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
|
||
new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb),
|
||
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
|
||
|
||
_engine = new SimulationEngine(_scenarioService, new FrameDataStore(paths),
|
||
new DamageModelRouter(), paths, new DefaultDefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance));
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_engine.Stop();
|
||
_mainDb?.Close();
|
||
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
|
||
}
|
||
|
||
private void SetupScenario()
|
||
{
|
||
var task = _scenarioService.CreateTask("Test", "");
|
||
_taskId = task.Id;
|
||
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenarioService.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
GroupId = "default",
|
||
TargetType = (int)TargetType.Piston,
|
||
Quantity = 1,
|
||
PowerType = (int)PowerType.Piston,
|
||
TypicalSpeed = 60,
|
||
});
|
||
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
new EquipmentDeployment
|
||
{
|
||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||
PlatformType = (int)PlatformType.GroundBased,
|
||
Quantity = 1, PositionX = 0, PositionY = 0, PositionZ = 0,
|
||
AerosolType = (int)AerosolType.InertGas, MunitionCount = 1, Cooldown = 5,
|
||
},
|
||
});
|
||
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal
|
||
{
|
||
AerosolType = (int)AerosolType.InertGas,
|
||
DisperseHeight = 300,
|
||
Duration = 60,
|
||
RecommendedTiming = 10,
|
||
PositionX = 1000,
|
||
PositionY = 300,
|
||
PositionZ = 0,
|
||
PositionMode = (int)PositionMode.AlgorithmRecommended,
|
||
});
|
||
_scenarioService.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
|
||
new Waypoint { PosX = 10000, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
|
||
});
|
||
}
|
||
|
||
[Fact]
|
||
public void Initialize_LoadsEntities()
|
||
{
|
||
SetupScenario();
|
||
_engine.Initialize(_taskId);
|
||
Assert.Single(_engine.Drones);
|
||
Assert.Equal(SimulationState.Running, _engine.State);
|
||
}
|
||
|
||
[Fact]
|
||
public void Tick_DroneMoves()
|
||
{
|
||
SetupScenario();
|
||
_engine.Initialize(_taskId);
|
||
_engine.Tick(1f / 20f);
|
||
Assert.True(_engine.Drones[0].PosX > 0);
|
||
}
|
||
|
||
[Fact]
|
||
public void PauseAndResume()
|
||
{
|
||
SetupScenario();
|
||
_engine.Initialize(_taskId);
|
||
_engine.Pause();
|
||
Assert.Equal(SimulationState.Paused, _engine.State);
|
||
|
||
var result = _engine.Tick(1f);
|
||
Assert.Equal(SimulationState.Paused, result.State);
|
||
|
||
_engine.Resume();
|
||
Assert.Equal(SimulationState.Running, _engine.State);
|
||
}
|
||
|
||
[Fact]
|
||
public void Stop_ClosesGracefully()
|
||
{
|
||
SetupScenario();
|
||
_engine.Initialize(_taskId);
|
||
_engine.Tick(1f / 20f);
|
||
_engine.Stop();
|
||
Assert.Equal(SimulationState.Stopped, _engine.State);
|
||
}
|
||
|
||
[Fact]
|
||
public void FrameData_IsWritten()
|
||
{
|
||
SetupScenario();
|
||
_engine.Initialize(_taskId);
|
||
for (int i = 0; i < 10; i++) _engine.Tick(1f / 20f);
|
||
|
||
var framePath = Path.Combine(_testDir, "frames", $"{_taskId}.db");
|
||
Assert.True(File.Exists(framePath));
|
||
}
|
||
|
||
[Fact]
|
||
public void AirBased_LaunchesWithCorrectDescription()
|
||
{
|
||
var task = _scenarioService.CreateTask("t", "");
|
||
_scenarioService.SaveScene(task.Id, new CombatScene());
|
||
_scenarioService.SaveTarget(task.Id, new TargetConfig { GroupId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 });
|
||
_scenarioService.SaveDeployment(task.Id, new List<EquipmentDeployment>
|
||
{
|
||
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.AirBased,
|
||
Quantity = 1, PositionX = 0, PositionY = 2000, PositionZ = 0,
|
||
CruiseSpeed = 55, ReleaseAltitude = 1500, AerosolType = (int)AerosolType.InertGas, MunitionCount = 1 },
|
||
});
|
||
_scenarioService.SaveCloudDispersal(task.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300 });
|
||
_scenarioService.SaveRoute(task.Id, "d", new RoutePlan(), new() { new() { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 }, new() { PosX = 5000, PosY = 300, PosZ = 0, Speed = 60 } });
|
||
|
||
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0 } });
|
||
_engine.Initialize(task.Id);
|
||
_engine.TimeScale = 10f;
|
||
|
||
SimEvent launch = null;
|
||
for (int i = 0; i < 5000; i++)
|
||
{
|
||
_engine.Tick(1f / 20f);
|
||
launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched);
|
||
if (launch != null) break;
|
||
if (_engine.State == SimulationState.Completed) break;
|
||
}
|
||
|
||
Assert.NotNull(launch);
|
||
Assert.Contains("空基", launch.Description);
|
||
}
|
||
|
||
[Fact]
|
||
public void GroundBased_LaunchesWithCorrectDescription()
|
||
{
|
||
SetupScenario();
|
||
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 1000, TargetY = 300, TargetZ = 0, MuzzleVelocity = 800 } });
|
||
_engine.Initialize(_taskId);
|
||
|
||
SimEvent launch = null;
|
||
for (int i = 0; i < 50; i++) { _engine.Tick(1f / 20f); launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched); if (launch != null) break; }
|
||
|
||
Assert.NotNull(launch);
|
||
Assert.Contains("计划发射", launch.Description);
|
||
}
|
||
}
|
||
}
|