新增 GetDefenseRecommendation 防御推荐方案 + 去掉 Critical
- IScenarioService 新增 GetDefenseRecommendation(scenarioId) - ScenarioService 构造函数加可选 IPathProvider + IDefensePlanner - DefenseRecommendation / RecommendOption 返回类型 - 前端调用获取最佳抛撒参数 → 用户一键应用写入 CloudDispersal - 去掉 Critical(省弹药到 50% 概率,无决策价值) - Unity ScenarioManager 桥接 + 构造注入 planner - 对接文档加推荐用法示例 - 265 测试通过(+3 推荐),Unity 编译通过
This commit is contained in:
parent
0002e7efbe
commit
9d81ba0b28
@ -324,8 +324,37 @@ public void DeleteWaypoint(string id);
|
||||
// Misc
|
||||
public void UpdateStep(string scenarioId, int step);
|
||||
public EnumMetadata GetEnums(); // 枚举中英文对照(下拉框数据源)
|
||||
|
||||
// 防御推荐(配置阶段调用,仿真前生成最优抛撒参数)
|
||||
public DefenseRecommendation GetDefenseRecommendation(string scenarioId);
|
||||
```
|
||||
|
||||
**防御推荐用法**("一键应用"功能):
|
||||
|
||||
```csharp
|
||||
var rec = scenario.GetDefenseRecommendation(scenarioId);
|
||||
if (rec.Success)
|
||||
{
|
||||
// rec.Best.PosX/Y/Z — 推荐抛撒位置
|
||||
// rec.Best.Timing — 推荐抛撒时机(仿真秒)
|
||||
// rec.Best.SalvoRounds — 推荐弹药数
|
||||
// rec.Best.EstimatedProbability — 估计拦截概率(0~1)
|
||||
// rec.Best.ThreatsEngaged / ThreatsUnengaged — 已拦截/未拦截威胁数
|
||||
|
||||
// 用户点"一键应用" → 写入 CloudDispersal
|
||||
var cloud = scenario.GetCloud(scenarioId);
|
||||
cloud.PositionX = rec.Best.PosX;
|
||||
cloud.PositionY = rec.Best.PosY;
|
||||
cloud.PositionZ = rec.Best.PosZ;
|
||||
cloud.Source = "Algorithm";
|
||||
cloud.PositionMode = (int)PositionMode.AlgorithmRecommended;
|
||||
scenario.SaveCloud(scenarioId, cloud);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"推荐失败: {rec.Summary}");
|
||||
}
|
||||
|
||||
### IDataService(通过 `ScenarioManager.DataService` 访问)
|
||||
|
||||
7 类基础数据,每类 4 个方法(`GetAll*` / `Get*(id)` / `Save*(spec)` / `Delete*(id)`):
|
||||
@ -589,6 +618,26 @@ public class EnumMetadata
|
||||
| `Content` | string | Markdown 报告文本(快速展示用) |
|
||||
| `ReportDataJson` | string? | 结构化数据 JSON(PDF 导出用) |
|
||||
|
||||
### DefenseRecommendation(防御推荐方案)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `Success` | bool | 是否规划成功 |
|
||||
| `Summary` | string | 方案摘要(失败时含原因) |
|
||||
| `Best` | RecommendOption | 最佳推荐参数 |
|
||||
|
||||
### RecommendOption(推荐参数)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `PosX/Y/Z` | double | 推荐抛撒位置(Y=高度,米) |
|
||||
| `Timing` | double | 推荐抛撒时机(仿真秒) |
|
||||
| `SalvoRounds` | int | 推荐弹药数(齐射发数) |
|
||||
| `SalvoSpacing` | double | 云团展开间距(米) |
|
||||
| `EstimatedProbability` | double | 估计拦截概率(0~1) |
|
||||
| `ThreatsEngaged` | int | 已拦截威胁数 |
|
||||
| `ThreatsUnengaged` | int | 未拦截威胁数 |
|
||||
|
||||
---
|
||||
|
||||
## 八、运行时实体属性
|
||||
|
||||
46
src/CounterDrone.Core/Services/DefenseRecommendation.cs
Normal file
46
src/CounterDrone.Core/Services/DefenseRecommendation.cs
Normal file
@ -0,0 +1,46 @@
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
/// <summary>防御推荐方案 — 配置阶段调用 planner 生成,供前端"一键应用"</summary>
|
||||
public class DefenseRecommendation
|
||||
{
|
||||
/// <summary>最佳推荐参数(拦截概率最高的抛撒参数)</summary>
|
||||
public RecommendOption Best { get; set; } = new();
|
||||
|
||||
/// <summary>方案摘要(含失败原因,如有)</summary>
|
||||
public string Summary { get; set; } = "";
|
||||
|
||||
/// <summary>是否规划成功</summary>
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>推荐参数档位</summary>
|
||||
public class RecommendOption
|
||||
{
|
||||
/// <summary>推荐抛撒位置 X(米)</summary>
|
||||
public double PosX { get; set; }
|
||||
|
||||
/// <summary>推荐抛撒位置 Y(米,高度)</summary>
|
||||
public double PosY { get; set; }
|
||||
|
||||
/// <summary>推荐抛撒位置 Z(米)</summary>
|
||||
public double PosZ { get; set; }
|
||||
|
||||
/// <summary>推荐抛撒时机(仿真秒)</summary>
|
||||
public double Timing { get; set; }
|
||||
|
||||
/// <summary>推荐弹药数(齐射发数)</summary>
|
||||
public int SalvoRounds { get; set; }
|
||||
|
||||
/// <summary>云团展开间距(米)</summary>
|
||||
public double SalvoSpacing { get; set; }
|
||||
|
||||
/// <summary>估计拦截概率(0~1)</summary>
|
||||
public double EstimatedProbability { get; set; }
|
||||
|
||||
/// <summary>已拦截威胁数</summary>
|
||||
public int ThreatsEngaged { get; set; }
|
||||
|
||||
/// <summary>未拦截威胁数</summary>
|
||||
public int ThreatsUnengaged { get; set; }
|
||||
}
|
||||
}
|
||||
@ -59,5 +59,8 @@ namespace CounterDrone.Core.Services
|
||||
|
||||
/// <summary>返回所有枚举定义(供前端下拉框使用)</summary>
|
||||
EnumMetadata GetEnums();
|
||||
|
||||
/// <summary>生成防御推荐方案(配置阶段调用,仿真前基于已配置参数计算最佳/最危险抛撒参数)</summary>
|
||||
DefenseRecommendation GetDefenseRecommendation(string scenarioId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
@ -17,6 +19,8 @@ namespace CounterDrone.Core.Services
|
||||
private readonly CloudDispersalRepository _cloudRepo;
|
||||
private readonly RoutePlanRepository _routeRepo;
|
||||
private readonly WaypointRepository _waypointRepo;
|
||||
private readonly IPathProvider? _paths;
|
||||
private readonly IDefensePlanner? _planner;
|
||||
|
||||
public ScenarioService(
|
||||
ScenarioRepository scenarioRepo,
|
||||
@ -26,7 +30,9 @@ namespace CounterDrone.Core.Services
|
||||
ScenarioUnitRepository equipRepo,
|
||||
CloudDispersalRepository cloudRepo,
|
||||
RoutePlanRepository routeRepo,
|
||||
WaypointRepository waypointRepo)
|
||||
WaypointRepository waypointRepo,
|
||||
IPathProvider? paths = null,
|
||||
IDefensePlanner? planner = null)
|
||||
{
|
||||
_scenarioRepo = scenarioRepo;
|
||||
_sceneRepo = sceneRepo;
|
||||
@ -36,6 +42,8 @@ namespace CounterDrone.Core.Services
|
||||
_cloudRepo = cloudRepo;
|
||||
_routeRepo = routeRepo;
|
||||
_waypointRepo = waypointRepo;
|
||||
_paths = paths;
|
||||
_planner = planner;
|
||||
}
|
||||
|
||||
// ========== scenario CRUD ==========
|
||||
@ -479,6 +487,157 @@ namespace CounterDrone.Core.Services
|
||||
|
||||
private static int _counter;
|
||||
|
||||
// ========== Defense Recommendation ==========
|
||||
|
||||
public DefenseRecommendation GetDefenseRecommendation(string scenarioId)
|
||||
{
|
||||
if (_paths == null || _planner == null)
|
||||
throw new InvalidOperationException("ScenarioService 未配置 IPathProvider/IDefensePlanner,无法生成推荐方案");
|
||||
|
||||
var config = GetScenarioDetail(scenarioId);
|
||||
if (config == null)
|
||||
throw new ArgumentException($"想定不存在: {scenarioId}");
|
||||
|
||||
// 加载 spec 数据
|
||||
Dictionary<string, DroneSpec> droneSpecs;
|
||||
Dictionary<string, LaunchPlatformSpec> platformSpecs;
|
||||
Dictionary<string, SensorSpec> sensorSpecs;
|
||||
List<AmmunitionSpec> ammoCatalog;
|
||||
using (var db = new SQLiteConnection(_paths.GetMainDbPath()))
|
||||
{
|
||||
droneSpecs = db.Table<DroneSpec>().ToDictionary(d => d.Id);
|
||||
platformSpecs = db.Table<LaunchPlatformSpec>().ToDictionary(f => f.Id);
|
||||
sensorSpecs = db.Table<SensorSpec>().ToDictionary(s => s.Id);
|
||||
ammoCatalog = db.Table<AmmunitionSpec>().ToList();
|
||||
}
|
||||
|
||||
var fireUnits = BuildFireUnits(config, platformSpecs);
|
||||
var threats = BuildDroneWaves(config, droneSpecs);
|
||||
var detectionSources = BuildDetectionSources(config, sensorSpecs);
|
||||
|
||||
if (fireUnits.Count == 0 || threats.Count == 0)
|
||||
return new DefenseRecommendation
|
||||
{
|
||||
Success = false,
|
||||
Summary = "缺少火力单元或威胁目标,无法生成推荐方案",
|
||||
};
|
||||
|
||||
var result = _planner.Plan(fireUnits, threats, config.Scene, detectionSources);
|
||||
|
||||
if (result.Best.MergedSchedule.Count == 0)
|
||||
return new DefenseRecommendation
|
||||
{
|
||||
Success = false,
|
||||
Summary = result.Best.Summary,
|
||||
};
|
||||
|
||||
return new DefenseRecommendation
|
||||
{
|
||||
Success = true,
|
||||
Summary = result.Best.Summary,
|
||||
Best = PlanToOption(result.Best),
|
||||
};
|
||||
}
|
||||
|
||||
private static RecommendOption PlanToOption(DefensePlan plan)
|
||||
{
|
||||
if (plan.MergedSchedule.Count == 0)
|
||||
return new RecommendOption();
|
||||
|
||||
// 取首发作为代表抛撒点
|
||||
var first = plan.MergedSchedule.OrderBy(f => f.FireTime).First();
|
||||
return new RecommendOption
|
||||
{
|
||||
PosX = first.TargetX,
|
||||
PosY = first.TargetY,
|
||||
PosZ = first.TargetZ,
|
||||
Timing = first.FireTime,
|
||||
SalvoRounds = plan.MergedSchedule.Count,
|
||||
SalvoSpacing = 0, // spacing 在 planner 内部计算,不暴露到 DefensePlan
|
||||
EstimatedProbability = plan.OverallProbability,
|
||||
ThreatsEngaged = plan.ThreatsEngaged,
|
||||
ThreatsUnengaged = plan.ThreatsUnengaged,
|
||||
};
|
||||
}
|
||||
|
||||
private static List<FireUnit> BuildFireUnits(ScenarioConfig config, Dictionary<string, LaunchPlatformSpec> specs)
|
||||
{
|
||||
var units = new List<FireUnit>();
|
||||
foreach (var eq in config.Units.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
|
||||
{
|
||||
if (!specs.TryGetValue(eq.LaunchPlatformSpecId, out var spec)) continue;
|
||||
int qty = Math.Max(1, eq.Quantity);
|
||||
for (int i = 0; i < qty; i++)
|
||||
{
|
||||
units.Add(new FireUnit
|
||||
{
|
||||
Id = $"fu{units.Count}",
|
||||
Type = (PlatformType)spec.PlatformType,
|
||||
Position = new Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
|
||||
GunCount = spec.GunCount,
|
||||
ChannelsPerGun = spec.ChannelsPerGun,
|
||||
ChannelInterval = (float)spec.ChannelInterval,
|
||||
CruiseSpeed = (float)spec.CruiseSpeed,
|
||||
ReleaseAltitude = (float)spec.ReleaseAltitude,
|
||||
MuzzleVelocity = (float)spec.MuzzleVelocity,
|
||||
TotalMunitions = eq.MunitionCount ?? spec.GunCount * spec.ChannelsPerGun,
|
||||
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
|
||||
Cooldown = (float)spec.Cooldown,
|
||||
});
|
||||
}
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
private static List<DroneWave> BuildDroneWaves(ScenarioConfig config, Dictionary<string, DroneSpec> specs)
|
||||
{
|
||||
var waves = new List<DroneWave>();
|
||||
foreach (var t in config.Drones)
|
||||
{
|
||||
if (!specs.TryGetValue(t.DroneSpecId, out var droneSpec)) continue;
|
||||
var route = config.Routes.FirstOrDefault(r => r.WaveId == t.WaveId);
|
||||
var wps = config.WaypointGroups.GetValueOrDefault(t.WaveId, new List<Waypoint>());
|
||||
waves.Add(new DroneWave
|
||||
{
|
||||
WaveId = t.WaveId,
|
||||
Profile = droneSpec,
|
||||
Quantity = t.Quantity,
|
||||
Route = route ?? new RoutePlan(),
|
||||
Waypoints = wps,
|
||||
});
|
||||
}
|
||||
return waves;
|
||||
}
|
||||
|
||||
private static List<DetectionSource> BuildDetectionSources(ScenarioConfig config, Dictionary<string, SensorSpec> sensorSpecs)
|
||||
{
|
||||
var sources = new List<DetectionSource>();
|
||||
foreach (var eq in config.Units)
|
||||
{
|
||||
if (string.IsNullOrEmpty(eq.SensorSpecId)) continue;
|
||||
if (!sensorSpecs.TryGetValue(eq.SensorSpecId, out var sensor)) continue;
|
||||
bool hasDetection = sensor.RadarRange > 0 || sensor.EORange > 0 || sensor.IRRange > 0;
|
||||
if (!hasDetection) continue;
|
||||
int qty = Math.Max(1, eq.Quantity);
|
||||
for (int i = 0; i < qty; i++)
|
||||
{
|
||||
sources.Add(new DetectionSource
|
||||
{
|
||||
Position = new Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
|
||||
RadarRange = (float)sensor.RadarRange,
|
||||
EORange = (float)sensor.EORange,
|
||||
IRRange = (float)sensor.IRRange,
|
||||
Accuracy = (float)sensor.Accuracy,
|
||||
MinElevation = (float)(sensor.MinElevation ?? float.MaxValue),
|
||||
MaxElevation = (float)(sensor.MaxElevation ?? float.MaxValue),
|
||||
MinDetectAlt = (float)(sensor.MinDetectAlt ?? float.MaxValue),
|
||||
MaxDetectAlt = (float)(sensor.MaxDetectAlt ?? float.MaxValue),
|
||||
});
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
public static string GenerateScenarioNumber()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
|
||||
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
using CounterDrone.Core.Services;
|
||||
@ -25,11 +26,15 @@ namespace CounterDrone.Unity
|
||||
Debug.Log($"ScenarioManager.Awake: dbPath={paths.GetMainDbPath()}");
|
||||
_db = new DatabaseManager(paths).OpenMainDb();
|
||||
SqliteConnectionTracker.Track(_db);
|
||||
var planner = new DefensePlanner(
|
||||
DefaultData.Load(paths).Ammunition,
|
||||
PlannerConfig.Load(paths));
|
||||
_service = new ScenarioService(
|
||||
new ScenarioRepository(_db), new CombatSceneRepository(_db),
|
||||
new ControlZoneRepository(_db), new ScenarioDroneRepository(_db),
|
||||
new ScenarioUnitRepository(_db), new CloudDispersalRepository(_db),
|
||||
new RoutePlanRepository(_db), new WaypointRepository(_db));
|
||||
new RoutePlanRepository(_db), new WaypointRepository(_db),
|
||||
paths, planner);
|
||||
_dataService = new DataService(
|
||||
new AmmunitionSpecRepository(_db), new FireUnitSpecRepository(_db),
|
||||
new DroneSpecRepository(_db), new SensorSpecRepository(_db),
|
||||
@ -90,6 +95,10 @@ namespace CounterDrone.Unity
|
||||
|
||||
public EnumMetadata GetEnums() => _service.GetEnums();
|
||||
|
||||
/// <summary>生成防御推荐方案(配置阶段调用,返回最佳值/最危险值供前端"一键应用")</summary>
|
||||
public DefenseRecommendation GetDefenseRecommendation(string scenarioId)
|
||||
=> _service.GetDefenseRecommendation(scenarioId);
|
||||
|
||||
[ContextMenu("Verify")]
|
||||
void Verify()
|
||||
{
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
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 SQLite;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DefenseRecommendationTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
private readonly IPathProvider _paths;
|
||||
private readonly SQLiteConnection _db;
|
||||
private readonly ScenarioService _scenario;
|
||||
private string _scenarioId = string.Empty;
|
||||
|
||||
public DefenseRecommendationTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"cd_rec_{Guid.NewGuid():N}");
|
||||
_paths = new TestPathProvider(_testDir);
|
||||
var dbm = new DatabaseManager(_paths);
|
||||
_db = dbm.OpenMainDb();
|
||||
|
||||
var planner = new DefensePlanner(
|
||||
DefaultData.Load(_paths).Ammunition,
|
||||
PlannerConfig.Load(_paths));
|
||||
_scenario = new ScenarioService(
|
||||
new ScenarioRepository(_db), new CombatSceneRepository(_db),
|
||||
new ControlZoneRepository(_db), new ScenarioDroneRepository(_db),
|
||||
new ScenarioUnitRepository(_db), new CloudDispersalRepository(_db),
|
||||
new RoutePlanRepository(_db), new WaypointRepository(_db),
|
||||
_paths, planner);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Close();
|
||||
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDefenseRecommendation_PresetScenario_ReturnsBest()
|
||||
{
|
||||
// 用预设想定
|
||||
var results = _scenario.SearchScenarios("活塞", null, null, 1, 10);
|
||||
Assert.True(results.TotalCount > 0, "预设想定未找到");
|
||||
_scenarioId = results.Items[0].Id;
|
||||
|
||||
var rec = _scenario.GetDefenseRecommendation(_scenarioId);
|
||||
|
||||
Assert.True(rec.Success, $"推荐失败: {rec.Summary}");
|
||||
Assert.True(rec.Best.SalvoRounds > 0, "最佳方案弹药数应 > 0");
|
||||
Assert.True(rec.Best.EstimatedProbability > 0, "最佳方案概率应 > 0");
|
||||
Assert.NotEmpty(rec.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDefenseRecommendation_NoFireUnits_ReturnsFailure()
|
||||
{
|
||||
// 创建无火力单元的想定
|
||||
var sc = _scenario.CreateScenario("无防御测试", "");
|
||||
_scenarioId = sc.Id;
|
||||
_scenario.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
|
||||
_scenario.SaveScenarioDrone(_scenarioId, TestData.CreateScenarioDrone("default", 1));
|
||||
_scenario.SaveRoute(_scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
new List<Waypoint> {
|
||||
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_scenarioId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas });
|
||||
|
||||
var rec = _scenario.GetDefenseRecommendation(_scenarioId);
|
||||
|
||||
Assert.False(rec.Success);
|
||||
Assert.Contains("火力单元", rec.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDefenseRecommendation_NoPlanner_Throws()
|
||||
{
|
||||
// 不传 planner 的 ScenarioService
|
||||
var svcNoPlanner = new ScenarioService(
|
||||
new ScenarioRepository(_db), new CombatSceneRepository(_db),
|
||||
new ControlZoneRepository(_db), new ScenarioDroneRepository(_db),
|
||||
new ScenarioUnitRepository(_db), new CloudDispersalRepository(_db),
|
||||
new RoutePlanRepository(_db), new WaypointRepository(_db));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => svcNoPlanner.GetDefenseRecommendation("any"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user