火力规则:添加武器命中率因素

This commit is contained in:
MHW
2026-03-31 15:31:50 +08:00
parent a7c19fb7e7
commit e52aaa1680
3 changed files with 515 additions and 12 deletions

View File

@@ -25,6 +25,9 @@ public class Weapon {
//武器数量
private Integer number;
//武器命中率
private Double hitRate;
//目标id
private String targetId;
}

View File

@@ -86,7 +86,20 @@
- 导弹发射车优先绑定蓝方导弹能力目标
- 当优先池不足时自动回退到地面池/全目标池,保证大部分武器有目标。
### 2.7 阵位规则参数(新增)
### 2.7 命中率与动态火力参数(新增)
- `hitRateCsv`:业务可配置红方武器命中率,格式 `武器名=0.72,武器名2=0.55`
- `defaultHitRateFallback`:未命中 `hitRateCsv` 且武器未携带 `hitRate` 时的兜底命中率。
- `desiredKillProbability`:目标毁伤置信度(例如 `0.9`)。
- `offsetCsvByWeapon`:显式 offset最高优先级格式 `武器名=1,武器名2=2`
- `enableDynamicMultiRedPerBlue`:是否按命中率动态决定“一个蓝方目标需要几把红方武器”。
- `minRedWeaponsPerBlueTarget` / `maxRedWeaponsPerBlueTargetCap`:每个蓝方目标的最小/最大红方分配数。
优先级说明(重要):
-`offsetCsvByWeapon` 命中某武器,则直接使用显式 offset不再使用命中率推导 offset。
- 未配置显式 offset 时,规则按命中率与 `desiredKillProbability` 自动推导所需火力数量。
- 同一蓝方 `equipmentId` 可能被多个红方武器绑定(不是固定 2 把),数量由命中率动态计算并受 cap 限制。
### 2.8 阵位规则参数(新增)
- `enablePositionRules`:阵位规则总开关。
- 阵位输入来源:`blueTask.warZoneLocation``blueTask.defZoneLocation`(各 4 个经纬点)。
- `fireUnitSpacingMeters`:防区/作战区点位间距(米),例如 `100` 代表约每 100 米一个火力单元。
@@ -120,7 +133,7 @@
}
```
### 2.8 航迹规则参数(新增)
### 2.9 航迹规则参数(新增)
- `enableTrajectoryRules`:航迹规则总开关。
- `strategyMode``auto/shortest/flank/interfere`
- `auto`:智能选择策略。
@@ -149,10 +162,12 @@
- 主决策在 `红方武器自适应装配规则`调用 `configureRedWeaponsByBlue(...)`映射配置添加武器
- 导弹增强在 `导弹联动增强规则`调用 `applyMissileLinkage(...)`受开关和阈值控制
- 全组件数量匹配在 `全组件数量匹配规则`按红方 `targetId` 绑定蓝方装备覆盖非导弹组件 `componentParams[0].number`找不到组件/targetId 允许跳过
- 命中率驱动数量在 `命中率规则-动态数量与offset` `hitRate` 与目标毁伤概率推导火力数量显式 offset 配置优先
- 任务命名在 `任务自动匹配规则`调用 `assignTaskNameByRedWeapons(...)`按红方最终武器自动生成任务名和 `dataType`
- 炮类约束命中炮类条件时炮类武器只保留 `炮弹` 组件单位 `范围米`
- `targetId` 绑定在装配后自动执行尽量为红方武器绑定蓝方 `equipmentId`允许少量空值冗余
- `targetId` 绑定在装配后自动执行按命中率动态给蓝目标分配多个红方武器受上下限约束允许少量空值冗余
- 阵位部署按多边形区域和武器类型自动赋位保证防区火力覆盖
- 射程合理性在 `射程合理性校验规则`基于蓝/红武器坐标计算距离自动避免射程不足却打击的不合理情况可自动调参)。
- 航迹生成根据蓝方 `trackPoints` 生成红方 `trackPoints`点数与蓝方一致支持三套策略和智能选择
## 3.1 任务名称自动匹配(新增)

View File

@@ -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);
@@ -238,6 +250,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 "打击任务"
@@ -273,9 +296,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 == "红方")
@@ -390,6 +424,9 @@ function void configureRedWeaponsByBlue(
limitRedArtilleryToShellOnly(redWeapons, (String) cfg.get("shellRangeDefault"));
}
// 写入红方武器默认命中率(若入参已给 hitRate则不覆盖
applyDefaultHitRateIfAbsent(redWeapons, cfg);
// 自动绑定红方武器 targetId来源蓝方 equipmentId
bindTargetIdsForRedWeapons(redWeapons, blueWeapons, cfg);
}
@@ -541,25 +578,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 = pickBlueTargetByNeed(
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);
}
}
@@ -570,7 +654,7 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
return;
}
// 第轮:若绑定率不足,回退全目标池尽量补齐(仍允许复用目标
// 第轮:若绑定率不足,回退全目标池补齐(仍受cap约束
for (Object obj : redWeapons) {
if (total > 0 && ((double) bound / (double) total) >= minRatio) {
break;
@@ -579,28 +663,418 @@ function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map
if (redWeapon == null || !isBlank(redWeapon.getTargetId())) {
continue;
}
String targetId = pickTargetIdFromPools(pools, cursor, "all");
String targetId = pickBlueTargetByNeed(
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 = pickBlueTargetByNeed(
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) {
@@ -1653,6 +2127,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;