@@ -149,6 +149,18 @@ function Map buildBusinessConfig() {
cfg.put("blueMissileRangeDefault", 220); // 蓝方导弹范围默认值
cfg.put("minBlueMissileCountForLinkage", 1); // 联动触发门槛
// ---------- 命中率与动态火力(可改) ----------
cfg.put("hitRateCsv", "防空导弹武器=0.72,火力打击无人机=0.62,武装直升机=0.68,反坦克火箭=0.55,反坦克导弹系统=0.78,迫榴炮=0.45,车载迫击炮=0.43,导弹发射车=0.82");
cfg.put("defaultHitRateFallback", "0.6"); // 兜底命中率
cfg.put("desiredKillProbability", "0.9"); // 期望毁伤置信度
cfg.put("offsetCsvByWeapon", ""); // 显式offset( 武器=offset) 优先, 例: 反坦克火箭=1,武装直升机=2
cfg.put("enableDynamicMultiRedPerBlue", Boolean.TRUE); // 按命中率动态决定每个蓝目标需要几个红武器
cfg.put("minRedWeaponsPerBlueTarget", 1); // 每个蓝目标最少分配红武器数量
cfg.put("maxRedWeaponsPerBlueTargetCap", 3); // 每个蓝目标最多分配红武器数量上限
cfg.put("enableRangeSanityCheck", Boolean.TRUE); // 距离-射程合理性校验开关
cfg.put("enableAutoRangeRecommend", Boolean.TRUE); // 射程不足时自动调参
cfg.put("rangeSafetyMarginMeters", 50); // 射程安全裕量
// ---------- 全组件数量匹配(可改) ----------
// 逻辑:按红方武器 targetId 找到对应蓝方装备( equipmentId) , 然后覆盖所有“非导弹组件”的数量
cfg.put("enableComponentQuantityMatch", Boolean.TRUE);
@@ -239,6 +251,17 @@ then
applyAllComponentQuantities($fact, cfg);
end
//-------------------------------------------------------------------------------
rule "命中率规则-动态数量与offset"
agenda-group "打击任务"
salience 52
when
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
then
Map cfg = buildBusinessConfig();
applyHitRateDrivenOffsets($fact, cfg);
end
//-------------------------------------------------------------------------------
rule "任务自动匹配规则"
agenda-group "打击任务"
@@ -274,9 +297,20 @@ then
end
//-------------------------------------------------------------------------------
rule "航迹规则-生成红方航迹 "
rule "射程合理性校验规则 "
agenda-group "打击任务"
salience 47
when
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
then
Map cfg = buildBusinessConfig();
applyRangeSanityAndRecommend($fact, cfg);
end
//-------------------------------------------------------------------------------
rule "航迹规则-生成红方航迹"
agenda-group "打击任务"
salience 46
when
// 根据蓝方 trackPoints 生成红方 trackPoints, 点数保持一致
$fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方")
@@ -391,6 +425,9 @@ function void configureRedWeaponsByBlue(
limitRedArtilleryToShellOnly(redWeapons, (String) cfg.get("shellRangeDefault"));
}
// 写入红方武器默认命中率(若入参已给 hitRate, 则不覆盖)
applyDefaultHitRateIfAbsent(redWeapons, cfg);
// 自动绑定红方武器 targetId( 来源: 蓝方 equipmentId)
bindTargetIdsForRedWeapons(redWeapons, blueWeapons, cfg);
}
@@ -542,25 +579,72 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
}
Map pools = extractBlueTargetPools(blueWeapons);
Map cursor = new java.util.HashMap( );
Map blueById = indexBlueWeaponsById(blueWeapons );
Map assignedCountByBlueId = new java.util.HashMap();
Map survivalByBlueId = initBlueSurvivalMap(blueById);
double desiredKill = normalizeProbability(readDoubleCfg(cfg, "desiredKillProbability", 0.9d), 0.9d);
int cap = readIntCfg(cfg, "maxRedWeaponsPerBlueTargetCap", 3);
int minPerBlue = readIntCfg(cfg, "minRedWeaponsPerBlueTarget", 1);
if (cap <= 0) {
cap = 1;
}
if (minPerBlue < 0) {
minPerBlue = 0;
}
if (minPerBlue > cap) {
minPerBlue = cap;
}
int total = redWeapons.size();
int bound = 0;
// 第一轮:按武器类别优先匹配
// 第一轮:先满足每个蓝目标的最小分配( 在cap内)
if (minPerBlue > 0) {
List allBlueIds = (List) pools.get("all");
if (allBlueIds != null && !allBlueIds.isEmpty()) {
for (Object idObj : allBlueIds) {
String blueId = String.valueOf(idObj);
int need = minPerBlue;
while (need > 0) {
Weapon candidate = pickUnboundRedWeaponForBlue(blueId, redWeapons);
if (candidate == null) {
break;
}
candidate.setTargetId(blueId);
bound++;
incrementAssignedCount(assignedCountByBlueId, blueId);
double p = resolveWeaponHitRate(candidate, cfg);
updateBlueSurvival(survivalByBlueId, blueId, p);
need--;
}
}
}
}
// 第二轮:按命中率动态分配,优先填补“尚未达到期望毁伤概率”的蓝目标
for (Object obj : redWeapons) {
Weapon redWeapon = (Weapon) obj;
if (redWeapon == null) {
continue;
}
if (!isBlank(redWeapon.getTargetId())) {
bound++;
continue;
}
String poolKey = inferBluePoolKeyForRedWeapon(redWeapon);
String targetId = pickTargetIdFromPools(pools, cursor, poolKey);
String targetId = pickBlue TargetByNeed(
pools,
poolKey,
assignedCountByBlueId,
survivalByBlueId,
desiredKill,
cap
);
if (!isBlank(targetId)) {
redWeapon.setTargetId(targetId);
bound++;
incrementAssignedCount(assignedCountByBlueId, targetId);
double p = resolveWeaponHitRate(redWeapon, cfg);
updateBlueSurvival(survivalByBlueId, targetId, p);
}
}
@@ -571,7 +655,7 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
return;
}
// 第二 轮:若绑定率不足,回退到 全目标池尽量 补齐(仍允许复用目标 )
// 第三 轮:若绑定率不足,回退全目标池补齐(仍受cap约束 )
for (Object obj : redWeapons) {
if (total > 0 && ((double) bound / (double) total) >= minRatio) {
break;
@@ -580,28 +664,418 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
continue;
}
String targetId = pickTargetIdFromPools(pools, cursor, "all");
String targetId = pickBlue TargetByNeed(
pools,
"all",
assignedCountByBlueId,
survivalByBlueId,
desiredKill,
cap
);
if (!isBlank(targetId)) {
redWeapon.setTargetId(targetId);
bound++;
incrementAssignedCount(assignedCountByBlueId, targetId);
double p = resolveWeaponHitRate(redWeapon, cfg);
updateBlueSurvival(survivalByBlueId, targetId, p);
}
}
// 第三 轮:若不允许空 targetId, 最后强制从 all 池补齐(尽力而为)
// 第四 轮:若不允许空 targetId, 最后尽力补齐
if (!allowReserveWithoutTarget) {
for (Object obj : redWeapons) {
Weapon redWeapon = (Weapon) obj;
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
continue;
}
String targetId = pickTargetIdFromPools(pools, cursor, "all");
String targetId = pickBlue TargetByNeed(
pools,
"all",
assignedCountByBlueId,
survivalByBlueId,
desiredKill,
cap
);
if (!isBlank(targetId)) {
redWeapon.setTargetId(targetId);
incrementAssignedCount(assignedCountByBlueId, targetId);
double p = resolveWeaponHitRate(redWeapon, cfg);
updateBlueSurvival(survivalByBlueId, targetId, p);
}
}
}
}
function void applyHitRateDrivenOffsets(FactTask fact, Map cfg) {
if (fact == null || fact.getRedTask() == null) {
return;
}
List redWeapons = fact.getRedTask().getTaskWeapons();
if (redWeapons == null || redWeapons.isEmpty()) {
return;
}
applyDefaultHitRateIfAbsent(redWeapons, cfg);
Map explicitOffset = parseNameIntCsv((String) cfg.get("offsetCsvByWeapon"));
double desiredKill = normalizeProbability(readDoubleCfg(cfg, "desiredKillProbability", 0.9d), 0.9d);
for (Object obj : redWeapons) {
Weapon redWeapon = (Weapon) obj;
if (redWeapon == null) {
continue;
}
int base = redWeapon.getNumber() == null || redWeapon.getNumber() <= 0 ? 1 : redWeapon.getNumber().intValue();
Integer explicit = (Integer) explicitOffset.get(redWeapon.getName());
int offset = 0;
if (explicit != null) {
offset = explicit.intValue();
} else {
double pHit = resolveWeaponHitRate(redWeapon, cfg);
int required = computeRequiredShots(pHit, desiredKill, base);
offset = required - base;
}
if (offset < 0) {
offset = 0;
}
redWeapon.setNumber(base + offset);
}
}
function void applyRangeSanityAndRecommend(FactTask fact, Map cfg) {
if (!readBooleanCfg(cfg, "enableRangeSanityCheck", true)) {
return;
}
if (fact == null || fact.getBlueTask() == null || fact.getRedTask() == null) {
return;
}
List blueWeapons = fact.getBlueTask().getTaskWeapons();
List redWeapons = fact.getRedTask().getTaskWeapons();
if (blueWeapons == null || blueWeapons.isEmpty() || redWeapons == null || redWeapons.isEmpty()) {
return;
}
Map blueById = indexBlueWeaponsById(blueWeapons);
int margin = readIntCfg(cfg, "rangeSafetyMarginMeters", 50);
boolean autoAdjust = readBooleanCfg(cfg, "enableAutoRangeRecommend", true);
for (Object obj : redWeapons) {
Weapon redWeapon = (Weapon) obj;
if (redWeapon == null || isBlank(redWeapon.getTargetId())) {
continue;
}
Weapon blueWeapon = (Weapon) blueById.get(redWeapon.getTargetId());
if (blueWeapon == null) {
continue;
}
Coordinate rc = redWeapon.getCoordinate();
Coordinate bc = blueWeapon.getCoordinate();
if (!isCoordinateUsable(rc) || !isCoordinateUsable(bc)) {
continue;
}
int currentRange = readRangeMetersFromWeapon(redWeapon);
if (currentRange <= 0) {
continue;
}
double d = approxDistanceMeters(
rc.getLongitude().doubleValue(),
rc.getLatitude().doubleValue(),
bc.getLongitude().doubleValue(),
bc.getLatitude().doubleValue()
);
int minRequired = (int) Math.ceil(d) + margin;
if (currentRange >= minRequired) {
continue;
}
if (autoAdjust) {
setWeaponFirstRangeParamAtLeast(redWeapon, minRequired);
}
}
}
function void applyDefaultHitRateIfAbsent(List redWeapons, Map cfg) {
if (redWeapons == null || redWeapons.isEmpty()) {
return;
}
Map hitRateMap = parseNameDoubleCsv((String) cfg.get("hitRateCsv"));
double fallback = normalizeProbability(readDoubleCfg(cfg, "defaultHitRateFallback", 0.6d), 0.6d);
for (Object obj : redWeapons) {
Weapon redWeapon = (Weapon) obj;
if (redWeapon == null) {
continue;
}
if (redWeapon.getHitRate() != null && redWeapon.getHitRate().doubleValue() > 0.0d) {
continue;
}
Double p = (Double) hitRateMap.get(redWeapon.getName());
if (p == null) {
p = fallback;
}
redWeapon.setHitRate(normalizeProbability(p.doubleValue(), fallback));
}
}
function double resolveWeaponHitRate(Weapon redWeapon, Map cfg) {
if (redWeapon != null && redWeapon.getHitRate() != null) {
double p = redWeapon.getHitRate().doubleValue();
return normalizeProbability(p, 0.6d);
}
Map hitRateMap = parseNameDoubleCsv((String) cfg.get("hitRateCsv"));
Double fromCfg = redWeapon == null ? null : (Double) hitRateMap.get(redWeapon.getName());
if (fromCfg != null) {
return normalizeProbability(fromCfg.doubleValue(), 0.6d);
}
return normalizeProbability(readDoubleCfg(cfg, "defaultHitRateFallback", 0.6d), 0.6d);
}
function int computeRequiredShots(double pHit, double pKill, int fallback) {
double p = normalizeProbability(pHit, 0.6d);
double pk = normalizeProbability(pKill, 0.9d);
if (p >= 0.999d) {
return 1;
}
if (p <= 0.0001d) {
return fallback <= 0 ? 1 : fallback;
}
double up = Math.log(1.0d - pk);
double down = Math.log(1.0d - p);
if (down == 0.0d) {
return fallback <= 0 ? 1 : fallback;
}
int n = (int) Math.ceil(up / down);
if (n <= 0) {
n = 1;
}
return n;
}
function Map parseNameDoubleCsv(String csv) {
Map result = new java.util.HashMap();
if (csv == null || csv.trim().equals("")) {
return result;
}
String[] items = csv.split(",");
for (int i = 0; i < items.length; i++) {
String one = items[i];
if (one == null) {
continue;
}
String text = one.trim();
if (text.equals("")) {
continue;
}
int idx = text.indexOf("=");
if (idx <= 0 || idx >= text.length() - 1) {
continue;
}
String key = text.substring(0, idx).trim();
String val = text.substring(idx + 1).trim();
if (key.equals("") || val.equals("")) {
continue;
}
result.put(key, Double.valueOf(parseDoubleSafe(val, 0.0d)));
}
return result;
}
function Map parseNameIntCsv(String csv) {
Map result = new java.util.HashMap();
if (csv == null || csv.trim().equals("")) {
return result;
}
String[] items = csv.split(",");
for (int i = 0; i < items.length; i++) {
String one = items[i];
if (one == null) {
continue;
}
String text = one.trim();
if (text.equals("")) {
continue;
}
int idx = text.indexOf("=");
if (idx <= 0 || idx >= text.length() - 1) {
continue;
}
String key = text.substring(0, idx).trim();
String val = text.substring(idx + 1).trim();
if (key.equals("") || val.equals("")) {
continue;
}
result.put(key, Integer.valueOf(parseIntSafe(val, 0)));
}
return result;
}
function double normalizeProbability(double p, double fallback) {
if (Double.isNaN(p) || Double.isInfinite(p)) {
p = fallback;
}
if (p <= 0.0d) {
p = fallback;
}
if (p <= 0.0d) {
p = 0.0001d;
}
if (p >= 1.0d) {
p = 0.999d;
}
return p;
}
function Map indexBlueWeaponsById(List blueWeapons) {
Map result = new java.util.HashMap();
if (blueWeapons == null) {
return result;
}
for (Object obj : blueWeapons) {
Weapon w = (Weapon) obj;
if (w == null || isBlank(w.getEquipmentId())) {
continue;
}
result.put(w.getEquipmentId(), w);
}
return result;
}
function Map initBlueSurvivalMap(Map blueById) {
Map result = new java.util.HashMap();
if (blueById == null) {
return result;
}
for (Object key : blueById.keySet()) {
result.put(key, Double.valueOf(1.0d));
}
return result;
}
function void updateBlueSurvival(Map survivalByBlueId, String blueId, double pHit) {
if (survivalByBlueId == null || isBlank(blueId)) {
return;
}
Double survival = (Double) survivalByBlueId.get(blueId);
if (survival == null) {
survival = Double.valueOf(1.0d);
}
double s = survival.doubleValue();
double p = normalizeProbability(pHit, 0.6d);
s = s * (1.0d - p);
if (s < 0.0d) {
s = 0.0d;
}
survivalByBlueId.put(blueId, Double.valueOf(s));
}
function void incrementAssignedCount(Map assignedCountByBlueId, String blueId) {
Integer old = (Integer) assignedCountByBlueId.get(blueId);
int now = old == null ? 1 : old.intValue() + 1;
assignedCountByBlueId.put(blueId, Integer.valueOf(now));
}
function Weapon pickUnboundRedWeaponForBlue(String blueId, List redWeapons) {
if (redWeapons == null || redWeapons.isEmpty()) {
return null;
}
for (Object obj : redWeapons) {
Weapon w = (Weapon) obj;
if (w == null || !isBlank(w.getTargetId())) {
continue;
}
return w;
}
return null;
}
function String pickBlueTargetByNeed(Map pools, String preferredKey, Map assignedCountByBlueId, Map survivalByBlueId, double desiredKill, int cap) {
String best = pickBestBlueIdFromPool((List) pools.get(preferredKey), assignedCountByBlueId, survivalByBlueId, desiredKill, cap);
if (!isBlank(best)) {
return best;
}
if (!"ground".equals(preferredKey)) {
String fromGround = pickBestBlueIdFromPool((List) pools.get("ground"), assignedCountByBlueId, survivalByBlueId, desiredKill, cap);
if (!isBlank(fromGround)) {
return fromGround;
}
}
return pickBestBlueIdFromPool((List) pools.get("all"), assignedCountByBlueId, survivalByBlueId, desiredKill, cap);
}
function String pickBestBlueIdFromPool(List ids, Map assignedCountByBlueId, Map survivalByBlueId, double desiredKill, int cap) {
if (ids == null || ids.isEmpty()) {
return null;
}
String bestId = null;
double bestNeed = -9999.0d;
int bestAssigned = Integer.MAX_VALUE;
for (Object obj : ids) {
String id = String.valueOf(obj);
if (isBlank(id)) {
continue;
}
Integer assignedObj = (Integer) assignedCountByBlueId.get(id);
int assigned = assignedObj == null ? 0 : assignedObj.intValue();
if (assigned >= cap) {
continue;
}
Double survivalObj = (Double) survivalByBlueId.get(id);
double survival = survivalObj == null ? 1.0d : survivalObj.doubleValue();
double achieved = 1.0d - survival;
double need = desiredKill - achieved;
if (need > bestNeed || (Math.abs(need - bestNeed) < 1e-9 && assigned < bestAssigned)) {
bestNeed = need;
bestAssigned = assigned;
bestId = id;
}
}
return bestId;
}
function boolean isCoordinateUsable(Coordinate c) {
return c != null && c.getLongitude() != null && c.getLatitude() != null;
}
function int readRangeMetersFromWeapon(Weapon weapon) {
if (weapon == null || weapon.getComponents() == null) {
return -1;
}
for (Object obj : weapon.getComponents()) {
SubComponents comp = (SubComponents) obj;
if (comp == null || comp.getComponentParams() == null || comp.getComponentParams().isEmpty()) {
continue;
}
ComponentParam first = (ComponentParam) comp.getComponentParams().get(0);
if (first == null || first.getAttExplain() == null) {
continue;
}
String explain = first.getAttExplain();
if (!(explain.contains("范围") || explain.contains("射程"))) {
continue;
}
return parseIntSafe(first.getAttDefaultValue(), -1);
}
return -1;
}
function void setWeaponFirstRangeParamAtLeast(Weapon weapon, int minRange) {
if (weapon == null || weapon.getComponents() == null || minRange <= 0) {
return;
}
for (Object obj : weapon.getComponents()) {
SubComponents comp = (SubComponents) obj;
if (comp == null || comp.getComponentParams() == null || comp.getComponentParams().isEmpty()) {
continue;
}
ComponentParam first = (ComponentParam) comp.getComponentParams().get(0);
if (first == null || first.getAttExplain() == null) {
continue;
}
String explain = first.getAttExplain();
if (!(explain.contains("范围") || explain.contains("射程"))) {
continue;
}
int current = parseIntSafe(first.getAttDefaultValue(), 0);
if (current < minRange) {
first.setAttDefaultValue(String.valueOf(minRange));
}
return;
}
}
//-------------------------------------------------------------------------------
// component 映射解析 + 全组件数量覆盖
function Map parseDeviceNameMapping(String csv) {
@@ -1654,6 +2128,17 @@ function int parseIntSafe(String text, int fallback) {
}
}
function double parseDoubleSafe(String text, double fallback) {
if (text == null || text.equals("")) {
return fallback;
}
try {
return Double.parseDouble(text.trim());
} catch (Exception ex) {
return fallback;
}
}
function int readIntCfg(Map cfg, String key, int fallback) {
if (cfg == null || key == null) {
return fallback;