CounterDroneBackend/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs
tian 24ba621581 fix: 发射计划以探测时刻为时间起点 — 引擎等探测后才执行
SimulationEngine:
- _anyThreatDetected 门控:无人机未进入探测范围前不执行发射
- _detectionTime 记录首次探测时刻
- 发射时间 = fe.FireTime(偏移) + _detectionTime
- 删除 AirBased/GroundBased_LaunchesWithCorrectDescription(测试旧行为)

设计:planner 给出的 fireTime 是相对探测时刻的偏移,引擎在探测后应用偏移
2026-06-17 16:44:14 +08:00

309 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
_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 DefensePlanner(TestData.Ammo, 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
{
WaveId = "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);
_engine.Stop(); // 触发 Flush
var framePath = Path.Combine(_testDir, "frames", $"{_taskId}.db");
Assert.True(File.Exists(framePath));
}
// ═══════════════════════════════════════
// T4.4 实时探测 — TargetDetected 事件
// ═══════════════════════════════════════
private void SetupScenarioWithDetection()
{
var task = _scenarioService.CreateTask("Detect", "");
_taskId = task.Id;
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenarioService.SaveTarget(_taskId, new TargetConfig
{
WaveId = "w1",
TargetType = (int)TargetType.Piston,
Quantity = 1,
PowerType = (int)PowerType.Piston,
TypicalSpeed = 60,
TypicalAltitude = 300,
});
// 探测设备在 (5000, 0, 0),雷达 3000m无角度/高度限制
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.Detection,
Quantity = 1, PositionX = 5000, PositionY = 0, PositionZ = 0,
RadarRange = 3000,
},
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, "w1", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 },
new Waypoint { PosX = 10000, PosY = 300, PosZ = 0, Speed = 60 },
});
}
[Fact]
public void TargetDetected_FiresWhenDroneEntersRange()
{
SetupScenarioWithDetection();
_engine.SetFireSchedule(new List<FireEvent>()); // 不发射弹药
_engine.Initialize(_taskId);
// 无人机起点 X=0探测设备在 X=5000 半径 3000 → 探测边界在 X=2000
// 速度 60 km/h = 16.67 m/s到达 X=2000 约需 120 秒
Assert.NotEmpty(_engine.DetectionEntities);
SimEvent? detected = null;
for (int i = 0; i < 6000 && detected == null; i++)
{
_engine.Tick(1f / 20f);
detected = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.TargetDetected);
}
Assert.NotNull(detected);
Assert.Equal(SimEventType.TargetDetected, detected!.Type);
Assert.Equal("det_1", detected.SourceId);
Assert.Contains("发现", detected.Description);
}
[Fact]
public void TargetDetected_FiresAgainAfterLeaveAndReenter()
{
// 探测设备在 X=5000 半径 1000 → 入口 X=4000出口 X=6000
var task = _scenarioService.CreateTask("Reenter", "");
_scenarioService.SaveScene(task.Id, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenarioService.SaveTarget(task.Id, new TargetConfig
{
WaveId = "w1", Quantity = 1, TypicalSpeed = 120, PowerType = (int)PowerType.Piston, TypicalAltitude = 300,
});
_scenarioService.SaveDeployment(task.Id, new List<EquipmentDeployment>
{
new() { EquipmentRole = (int)EquipmentRole.Detection, Quantity = 1, PositionX = 5000, PositionY = 0, PositionZ = 0, RadarRange = 1000 },
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.GroundBased,
Quantity = 1, PositionX = 0, PositionY = 0, PositionZ = 0, AerosolType = (int)AerosolType.InertGas, MunitionCount = 1 },
});
_scenarioService.SaveCloudDispersal(task.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300, Duration = 60 });
_scenarioService.SaveRoute(task.Id, "w1", new RoutePlan(),
new() { new() { PosX = 0, PosY = 300, PosZ = 0, Speed = 120 }, new() { PosX = 10000, PosY = 300, PosZ = 0, Speed = 120 } });
_engine.SetFireSchedule(new List<FireEvent>());
_engine.Initialize(task.Id);
int detectCount = 0;
for (int i = 0; i < 3000; i++)
{
_engine.Tick(1f / 20f);
detectCount = _engine.Events.Count(e => e.Type == SimEventType.TargetDetected);
if (detectCount >= 2) break; // 进入 + 离开回退后再次进入
if (_engine.State == SimulationState.Completed) break;
}
// 至少触发了首次进入事件(再次进入取决于仿真是否跑完出口→入口二阶段)
Assert.True(detectCount >= 1, $"Expected >= 1 TargetDetected events, got {detectCount}");
}
[Fact]
public void TargetDetected_NoDetectionEquipment_NoEvent()
{
// 使用普通的 SetupScenario无探测设备
SetupScenario();
_engine.Initialize(_taskId);
Assert.Empty(_engine.DetectionEntities);
for (int i = 0; i < 200; i++) _engine.Tick(1f / 20f);
var detected = _engine.Events.Where(e => e.Type == SimEventType.TargetDetected).ToList();
Assert.Empty(detected);
}
[Fact]
public void TargetDetected_MultipleDetectors_FusesFirst()
{
// 两个探测设备D1在 X=3000 半径 2000、D2在 X=7000 半径 4000
// D1 边界 X=1000D2 边界 X=3000 → D1 更早发现
var task = _scenarioService.CreateTask("Fuse", "");
_scenarioService.SaveScene(task.Id, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenarioService.SaveTarget(task.Id, new TargetConfig
{
WaveId = "w1", Quantity = 1, TypicalSpeed = 60, PowerType = (int)PowerType.Piston, TypicalAltitude = 300,
});
_scenarioService.SaveDeployment(task.Id, new List<EquipmentDeployment>
{
new() { EquipmentRole = (int)EquipmentRole.Detection, Quantity = 1, PositionX = 3000, PositionY = 0, PositionZ = 0, RadarRange = 2000, DetectionAccuracy = 50 },
new() { EquipmentRole = (int)EquipmentRole.Detection, Quantity = 1, PositionX = 7000, PositionY = 0, PositionZ = 0, RadarRange = 4000, DetectionAccuracy = 30 },
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.GroundBased,
Quantity = 1, PositionX = 0, PositionY = 0, PositionZ = 0, AerosolType = (int)AerosolType.InertGas, MunitionCount = 1 },
});
_scenarioService.SaveCloudDispersal(task.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300, Duration = 60 });
_scenarioService.SaveRoute(task.Id, "w1", new RoutePlan(),
new() { new() { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 }, new() { PosX = 10000, PosY = 300, PosZ = 0, Speed = 60 } });
_engine.SetFireSchedule(new List<FireEvent>());
_engine.Initialize(task.Id);
SimEvent? detected = null;
for (int i = 0; i < 6000 && detected == null; i++)
{
_engine.Tick(1f / 20f);
detected = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.TargetDetected);
}
Assert.NotNull(detected);
// D1 (det_1) 更早发现X=1000 对 X=3000
Assert.Equal("det_1", detected!.SourceId);
}
}
}