diff --git a/auto-solution-admin/src/main/java/com/solution/web/controller/rule/FireRuleController.java b/auto-solution-admin/src/main/java/com/solution/web/controller/rule/FireRuleController.java index 4abef30..56ba729 100644 --- a/auto-solution-admin/src/main/java/com/solution/web/controller/rule/FireRuleController.java +++ b/auto-solution-admin/src/main/java/com/solution/web/controller/rule/FireRuleController.java @@ -86,16 +86,12 @@ public class FireRuleController extends BaseController { return success(ruleService.getComponents(platformId)); } - /** - * 开始执行规则匹配 - * @param task 敌方参数 - * @return - */ - @PostMapping("/rule") + + /* @PostMapping("/rule") @ApiOperation("开始执行规则匹配") public AjaxResult execute(@RequestBody Task task){ return success(ruleService.executeTask(task)); - } + }*/ /** diff --git a/auto-solution-rule/src/main/java/com/solution/rule/config/FireRuleMatchDefaultParams.java b/auto-solution-rule/src/main/java/com/solution/rule/config/FireRuleMatchDefaultParams.java new file mode 100644 index 0000000..f4c46c1 --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/config/FireRuleMatchDefaultParams.java @@ -0,0 +1,33 @@ +package com.solution.rule.config; + +import java.util.HashMap; +import java.util.Map; + +/** + * 装备匹配规则默认参数,与 {@code resources/rules/rule.drl} 中 {@code buildParam} 键保持一致。 + */ +public final class FireRuleMatchDefaultParams { + + private FireRuleMatchDefaultParams() { + } + + public static Map defaults() { + Map param = new HashMap<>(); + param.put("weight", 1); + param.put("minSelectedScore", 1); + param.put("tankScore", 1); + param.put("airScore", 2); + param.put("groundScore", 1); + param.put("missileScore", 1); + param.put("airTaskScore", 10); + param.put("bluePlatformKeywords_air", "F-16,J-10,F-35"); + param.put("redPreferredWhenBlueAir", "防空,导弹,无人机,直升机,空空"); + param.put("redPreferredWhenGround", "远火,榴弹,炮,火箭"); + param.put("airTaskKeywords", "空中,制空,拦截,空战"); + param.put("groundTaskKeywords", "地面,突击,登陆"); + param.put("tankKeywords", "坦克,装甲"); + param.put("missileKeywords", "导弹,火箭弹,巡航"); + param.put("tieBreak", "equipmentId"); + return param; + } +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/service/impl/FireRuleServiceImpl.java b/auto-solution-rule/src/main/java/com/solution/rule/service/impl/FireRuleServiceImpl.java index 1d93c65..313aaca 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/service/impl/FireRuleServiceImpl.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/service/impl/FireRuleServiceImpl.java @@ -12,6 +12,7 @@ import com.solution.rule.domain.ultimately.dto.FireRuleInputDTO; import com.solution.rule.domain.ultimately.dto.FireRuleInputForceSideDTO; import com.solution.rule.domain.ultimately.dto.FireRuleInputRedWeaponElementDTO; import com.solution.rule.domain.ultimately.dto.FireRuleTaskInputDTO; +import com.solution.rule.config.FireRuleMatchDefaultParams; import com.solution.rule.domain.ultimately.fact.DroolsFact; import com.solution.rule.domain.ultimately.vo.FireRuleOutputVO; import com.solution.rule.domain.vo.ComponentCountVO; @@ -27,6 +28,7 @@ import com.solution.rule.strategy.SceneStrategy; import com.solution.rule.strategy.SceneStrategyFactory; import org.kie.api.KieBase; import org.kie.api.runtime.KieSession; +import org.kie.api.runtime.rule.FactHandle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -197,7 +199,7 @@ public class FireRuleServiceImpl implements FireRuleService { * @return */ @Override - public FireRuleOutputVO rule(FireRuleInputDTO task) { + public FireRuleOutputVO rule(FireRuleInputDTO task) { if(ObjectUtil.isEmpty(task)){ throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION); } @@ -207,10 +209,9 @@ public class FireRuleServiceImpl implements FireRuleService { } //创建KieSession KieSession kieSession = kieBase.newKieSession(); - //设置Drools全局变量 Map globalParams = new HashMap<>(); +// globalParams.putAll(FireRuleMatchDefaultParams.defaults()); kieSession.setGlobal("globalParams", globalParams); - kieSession.insert(globalParams); //获取红方阵营id String redObjectHandleId = getObjectHandle(task); if(ObjectUtil.isEmpty(redObjectHandleId)){ @@ -229,16 +230,25 @@ public class FireRuleServiceImpl implements FireRuleService { fireRuleOutputVO.setSourceFile(task.getSourceFile()); DroolsFact droolsFact = new DroolsFact(); - droolsFact.setRedWeapons(redWeapons); - -// droolsFact.getFireRuleOutputVO().setRedWeapons(redWeapons); + droolsFact.setRedWeapons(new ArrayList<>(redWeapons)); droolsFact.setFireRuleOutputVO(fireRuleOutputVO); - for (FireRuleTaskInputDTO fireRuleTaskInputDTO : tasks) { - droolsFact.setTask(fireRuleTaskInputDTO); - kieSession.insert(droolsFact); + + FactHandle droolsFactHandle = null; + try { + for (FireRuleTaskInputDTO fireRuleTaskInputDTO : tasks) { + droolsFact.setTask(fireRuleTaskInputDTO); + if (droolsFactHandle == null) { + droolsFactHandle = kieSession.insert(droolsFact); + } else { + kieSession.update(droolsFactHandle, droolsFact); + } + kieSession.fireAllRules(); + } + } finally { + kieSession.dispose(); } - return droolsFact.getFireRuleOutputVO(); + return fireRuleOutputVO; } /** diff --git a/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java b/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java new file mode 100644 index 0000000..a065aa4 --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/utils/FireRuleRedWeaponOutputFillHelper.java @@ -0,0 +1,152 @@ +package com.solution.rule.utils; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.solution.rule.domain.ultimately.dto.FireRuleInputRedSubComponentsDTO; +import com.solution.rule.domain.ultimately.vo.FireRuleLauncherConfigurationVO; +import com.solution.rule.domain.ultimately.vo.FireRuleMissionListItemVO; +import com.solution.rule.domain.ultimately.vo.FireRuleMountedWeaponRefVO; +import com.solution.rule.domain.ultimately.vo.FireRuleRedSubComponentsVO; +import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponEquipmentVO; +import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponSlotVO; +import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskNodeVO; +import com.solution.rule.domain.ultimately.vo.FireRuleSceneTaskPayloadVO; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 将入参中的红方装备 {@code SubComponents}(DTO)转为输出 VO,保持与原始 JSON 结构一致。 + */ +public final class FireRuleRedWeaponOutputFillHelper { + + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private FireRuleRedWeaponOutputFillHelper() { + } + + /** + * DTO 与 VO 字段/JsonProperty 对齐,通过 Jackson 树转换实现原样输出。 + */ + public static FireRuleRedSubComponentsVO toOutputSubComponents(FireRuleInputRedSubComponentsDTO src) { + if (src == null) { + return null; + } + return MAPPER.convertValue(src, FireRuleRedSubComponentsVO.class); + } + + /** + * 将输出 redWeapons 单项映射为 Tasks 节点(一个装备 -> 一个任务节点)。 + * + *

字段映射约定:

+ *
    + *
  • drawName = Name + \"打击任务\"
  • + *
  • side = OwnerForceSide(按你的要求直接复制)
  • + *
  • id 优先 EquipmentID,其次 PlatID,最后用 index 兜底
  • + *
  • task.payload.weaponId = EquipmentID
  • + *
  • task.payload.sideId = OwnerForceSide
  • + *
  • missionList:尽量从 SubComponents.weapon[*] 中提取 mountedWeapon/name/number/launcherType
  • + *
+ */ + public static FireRuleSceneTaskNodeVO toTaskNode(FireRuleRedWeaponEquipmentVO redWeapon, int index) { + FireRuleSceneTaskNodeVO node = new FireRuleSceneTaskNodeVO(); + if (redWeapon == null) { + node.setId("redWeaponTask_" + index); + node.setDrawName("打击任务"); + return node; + } + + String name = nz(redWeapon.getName()); + node.setName(name); + node.setDrawName(name + "打击任务"); + node.setId(buildNodeId(redWeapon, index)); + node.setSide(redWeapon.getOwnerForceSide()); + // 按你的要求写死 Tasks 节点的展示字段 + node.setColor("rgb(220, 39, 39)"); + node.setDataType("taskPlane"); + node.setGroupType("tasks"); + node.setShow(true); + node.setIsSelected(false); + node.setSort((long) index); + + FireRuleSceneTaskPayloadVO payload = new FireRuleSceneTaskPayloadVO(); + payload.setName(name); + payload.setWeaponId(redWeapon.getEquipmentId()); + payload.setSideId(redWeapon.getOwnerForceSide()); + payload.setMissionList(buildMissionList(redWeapon)); + node.setTask(payload); + return node; + } + + /** + * 批量转换:redWeapons 有几个就生成几个 Tasks。 + */ + public static List toTaskNodes(List redWeapons) { + if (redWeapons == null || redWeapons.isEmpty()) { + return Collections.emptyList(); + } + List list = new ArrayList<>(redWeapons.size()); + for (int i = 0; i < redWeapons.size(); i++) { + list.add(toTaskNode(redWeapons.get(i), i)); + } + return list; + } + + private static List buildMissionList(FireRuleRedWeaponEquipmentVO redWeapon) { + if (redWeapon == null || redWeapon.getSubComponents() == null || redWeapon.getSubComponents().getWeapon() == null) { + return null; + } + List result = new ArrayList<>(); + for (FireRuleRedWeaponSlotVO slot : redWeapon.getSubComponents().getWeapon()) { + if (slot == null) { + continue; + } + FireRuleLauncherConfigurationVO cfg = slot.getConfiguration(); + FireRuleMountedWeaponRefVO mounted = cfg != null ? cfg.getMountedWeapon() : null; + + FireRuleMissionListItemVO item = new FireRuleMissionListItemVO(); + // label:优先挂载武器名,其次 deviceName + String label = mounted != null ? mounted.getName() : null; + if (isBlank(label)) { + label = slot.getDeviceName(); + } + item.setLabel(label); + item.setNumber(cfg != null ? cfg.getNumber() : null); + item.setValue(mounted != null ? mounted.getName() : null); + item.setLauncherType(extractLauncherType(slot.getTwiceModified())); + result.add(item); + } + return result.isEmpty() ? null : result; + } + + private static String extractLauncherType(Map twiceModified) { + if (twiceModified == null || twiceModified.isEmpty()) { + return null; + } + Object v = twiceModified.get("launcherType"); + return v == null ? null : String.valueOf(v); + } + + private static String buildNodeId(FireRuleRedWeaponEquipmentVO redWeapon, int index) { + String id = nz(redWeapon.getEquipmentId()); + if (!id.isEmpty()) { + return id; + } + id = nz(redWeapon.getPlatId()); + if (!id.isEmpty()) { + return id; + } + return "redWeaponTask_" + index; + } + + private static String nz(String s) { + return s == null ? "" : s; + } + + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java b/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java index f642ef4..9c03fe7 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/utils/RuleFunction.java @@ -1,34 +1,326 @@ package com.solution.rule.utils; +import com.solution.rule.domain.ultimately.dto.FireRuleInputRedWeaponElementDTO; import com.solution.rule.domain.ultimately.dto.FireRuleTaskInputDTO; +import com.solution.rule.domain.ultimately.dto.FireRuleTaskWeaponDTO; +import com.solution.rule.domain.ultimately.dto.FireRuleWeaponComponentDTO; import com.solution.rule.domain.ultimately.fact.DroolsFact; +import com.solution.rule.domain.ultimately.vo.FireRuleOutputVO; +import com.solution.rule.domain.ultimately.vo.FireRuleRedWeaponEquipmentVO; +import com.solution.rule.domain.ultimately.vo.FireRuleTaskInputVO; +import com.solution.rule.domain.ultimately.vo.FireRuleTaskWeaponVO; +import java.util.ArrayList; +import java.util.List; import java.util.Map; /** - * 规则函数方法 + * Drools 规则调用的装备匹配逻辑。所有业务词均来自 globalParams(由 rule.drl 的 buildParam 注入),本类不写死中文业务词。 + *

+ * 总分公式(每件候选红装) + *

+ *   score(red) = scoreRuleSlots(...) + scoreLegacyLayer(...)
+ *
+ *   scoreRuleSlots = Σ(i=1..ruleSlotCount)  [ containsAny(blueBlob, blueRuleKeywords_i)
+ *                                         ∧ containsAny(redBlob, redRuleKeywords_i)
+ *                                         ? ruleScore_i * weight : 0 ]
+ *
+ *   scoreLegacyLayer = ①空中平台 + ②空中任务 + ③地面任务 + ④坦克 + ⑤导弹 五段条件之和,每段若满足均为「对应 *Score * weight」
+ *   (五段含义见 scoreLegacyLayer 方法注释与 rule.drl 中各键说明)。
+ * 
+ *

+ * 文本串定义 + *

    + *
  • blueBlob:{@link #buildBlueTextBlob} — 蓝方任务 drawName、dataType、taskWeapons 及组件名拼接成一大串,用于关键词包含判断。
  • + *
  • redBlob:{@link #buildRedTextBlob} — 红装 name、platformType、supportType 拼接。
  • + *
+ *

+ * 选优与输出 + *

    + *
  • 在 redWeapons 池中对每件算 score,取最大值;若并列,{@link #compareRedForTieBreak}(由 tieBreak 参数控制,默认 equipmentId 小者优先)。
  • + *
  • 若 maxScore < minSelectedScore:不追加 fireRuleInputs;仍将当前池映射到 FireRuleOutputVO.redWeapons。
  • + *
  • 否则:池中 remove 选中项;fireRuleInputs 追加一行,drawName += outputDrawNameSuffix,taskWeapons 一条映射自选中红装;redWeapons 为剩余池。
  • + *
*/ -public class RuleFunction { +public final class RuleFunction { + private RuleFunction() { + } /** - * 装备匹配 + * 单轮规则执行:对当前 DroolsFact 的一条蓝方任务,从 redWeapons 中选一件装备并写回输出。 + *

+ * 参数 p(globalParams)中各键含义与运算见 {@link RuleFunction} 类注释及 resources/rules/rule.drl 内 buildParam 行内注释。 */ - public static void equipmentRule(DroolsFact task, Map globalParams){ - //空中总分数 - Integer airScore = 0; - //反坦克总分数 - Integer antitankScore = 0; - //远程打击总分数 - Integer remoteScore = 0; + @SuppressWarnings("rawtypes") + public static void equipmentRule(DroolsFact fact, Map globalParams) { + if (fact == null || fact.getTask() == null) { + return; + } + Map p = castParams(globalParams); + FireRuleTaskInputDTO blue = fact.getTask(); + List pool = fact.getRedWeapons(); + if (pool == null) { + pool = new ArrayList<>(); + fact.setRedWeapons(pool); + } - FireRuleTaskInputDTO blueTask = task.getTask(); + // weight、minSelectedScore:见 rule.drl 注释 + String blueBlob = buildBlueTextBlob(blue); + int weight = readInt(p, "weight", 1); + int minScore = readInt(p, "minSelectedScore", 1); - //权重因子 - Integer weight = (Integer) globalParams.get("weight"); - String drawName = blueTask.getDrawName(); - if("".contains(drawName)){ + int bestIndex = -1; + int bestScore = Integer.MIN_VALUE; + for (int i = 0; i < pool.size(); i++) { + FireRuleInputRedWeaponElementDTO red = pool.get(i); + String redBlob = buildRedTextBlob(red); + int score = scoreRuleSlots(blueBlob, redBlob, p, weight) + + scoreLegacyLayer(blueBlob, redBlob, p, weight); + if (score > bestScore) { + bestScore = score; + bestIndex = i; + } else if (score == bestScore && bestIndex >= 0) { + // 并列:compareRedForTieBreak(a,b)>0 表示当前 best 的 equipmentId 比候选 red 大,应换成更小的 id + if (compareRedForTieBreak(pool.get(bestIndex), red, p) > 0) { + bestIndex = i; + } + } + } + + FireRuleOutputVO out = fact.getFireRuleOutputVO(); + if (out == null) { + out = new FireRuleOutputVO(); + fact.setFireRuleOutputVO(out); + } + + // 未达门槛或池空:不写 fireRuleInputs,仅同步「剩余池」到输出 redWeapons + if (bestIndex < 0 || pool.isEmpty() || bestScore < minScore) { + out.setRedWeapons(convertPoolToEquipmentVoList(pool)); + // Tasks 由最终输出 redWeapons 一一生成(一个装备 -> 一个任务) + out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons())); + return; + } + + FireRuleInputRedWeaponElementDTO chosen = pool.remove(bestIndex); + + out.setRedWeapons(convertPoolToEquipmentVoList(pool)); + // Tasks 由最终输出 redWeapons 一一生成(一个装备 -> 一个任务) + out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons())); + } + + @SuppressWarnings("unchecked") + private static Map castParams(Map raw) { + return raw == null ? new java.util.HashMap<>() : (Map) raw; + } + + /** + * 蓝方侧用于「关键词包含」判断的合并文本(空格分隔各字段)。 + */ + private static String buildBlueTextBlob(FireRuleTaskInputDTO task) { + StringBuilder sb = new StringBuilder(); + append(sb, task.getDrawName()); + append(sb, task.getDataType()); + if (task.getTaskWeapons() != null) { + for (FireRuleTaskWeaponDTO w : task.getTaskWeapons()) { + if (w == null) { + continue; + } + append(sb, w.getName()); + append(sb, w.getSupportType()); + append(sb, w.getEquipmentId()); + if (w.getComponents() != null) { + for (FireRuleWeaponComponentDTO c : w.getComponents()) { + if (c == null) { + continue; + } + append(sb, c.getDeviceName()); + } + } + } + } + return sb.toString(); + } + + /** + * 红方侧用于「关键词包含」判断的合并文本。 + */ + private static String buildRedTextBlob(FireRuleInputRedWeaponElementDTO red) { + if (red == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + append(sb, red.getName()); + append(sb, red.getPlatformType()); + append(sb, red.getSupportType()); + return sb.toString(); + } + + private static void append(StringBuilder sb, String s) { + if (s != null && !s.isEmpty()) { + sb.append(s).append(' '); } } + + /** + * 规则槽得分:对 i=1..ruleSlotCount,若 blueBlob 命中 blueRuleKeywords_i 且 redBlob 命中 redRuleKeywords_i, + * 则累加 ruleScore_i * weight。关键词为英文逗号分隔,任一词作为子串出现在文本中即命中。 + */ + private static int scoreRuleSlots(String blueBlob, String redBlob, Map p, int weight) { + int n = readInt(p, "ruleSlotCount", 0); + int sum = 0; + for (int i = 1; i <= n; i++) { + String bk = str(p, "blueRuleKeywords_" + i, ""); + String rk = str(p, "redRuleKeywords_" + i, ""); + if (bk.isEmpty() || rk.isEmpty()) { + continue; + } + if (containsAny(blueBlob, bk) && containsAny(redBlob, rk)) { + sum += readInt(p, "ruleScore_" + i, 0) * weight; + } + } + return sum; + } + + /** + * 兼容层得分:五段独立条件,可叠加。每段均为「蓝关键词命中 ∧ 红关键词命中 → 加 对应分数 * weight」。 + *

    + *
  • ① bluePlatformKeywords_air + redPreferredWhenBlueAir → airScore
  • + *
  • ② airTaskKeywords + redPreferredWhenBlueAir → airTaskScore
  • + *
  • ③ groundTaskKeywords + redPreferredWhenGround → groundScore
  • + *
  • ④ tankKeywords + redMatchKeywords_tank → tankScore
  • + *
  • ⑤ missileKeywords + redMatchKeywords_missile → missileScore
  • + *
+ * 键名与 rule.drl 中 param.put 一致。 + */ + private static int scoreLegacyLayer(String blueBlob, String redBlob, Map p, int weight) { + int s = 0; + if (containsAny(blueBlob, str(p, "bluePlatformKeywords_air", "")) + && containsAny(redBlob, str(p, "redPreferredWhenBlueAir", ""))) { + s += readInt(p, "airScore", 0) * weight; + } + if (containsAny(blueBlob, str(p, "airTaskKeywords", "")) + && containsAny(redBlob, str(p, "redPreferredWhenBlueAir", ""))) { + s += readInt(p, "airTaskScore", 0) * weight; + } + if (containsAny(blueBlob, str(p, "groundTaskKeywords", "")) + && containsAny(redBlob, str(p, "redPreferredWhenGround", ""))) { + s += readInt(p, "groundScore", 0) * weight; + } + if (containsAny(blueBlob, str(p, "tankKeywords", "")) + && containsAny(redBlob, str(p, "redMatchKeywords_tank", ""))) { + s += readInt(p, "tankScore", 0) * weight; + } + if (containsAny(blueBlob, str(p, "missileKeywords", "")) + && containsAny(redBlob, str(p, "redMatchKeywords_missile", ""))) { + s += readInt(p, "missileScore", 0) * weight; + } + return s; + } + + /** + * 并列时比较两件红装:tieBreak=equipmentId 时返回 id 字典序比较结果(>0 表示 a 的 id 大于 b,应选 b)。 + */ + private static int compareRedForTieBreak( + FireRuleInputRedWeaponElementDTO a, + FireRuleInputRedWeaponElementDTO b, + Map p) { + if (a == null) { + return b == null ? 0 : 1; + } + if (b == null) { + return -1; + } + String mode = str(p, "tieBreak", "equipmentId"); + if ("equipmentId".equals(mode)) { + String ida = nz(a.getEquipmentId()); + String idb = nz(b.getEquipmentId()); + return ida.compareTo(idb); + } + return 0; + } + + /** + * commaKeywords:英文逗号分隔;若 text 包含其中任一词(trim 后非空)则命中。 + */ + private static boolean containsAny(String text, String commaKeywords) { + if (text == null || text.isEmpty() || commaKeywords == null || commaKeywords.isEmpty()) { + return false; + } + for (String part : commaKeywords.split(",")) { + String k = part.trim(); + if (!k.isEmpty() && text.contains(k)) { + return true; + } + } + return false; + } + + private static String str(Map p, String key, String def) { + Object v = p.get(key); + return v == null ? def : String.valueOf(v); + } + + private static int readInt(Map p, String key, int def) { + Object v = p.get(key); + if (v == null) { + return def; + } + if (v instanceof Number) { + return ((Number) v).intValue(); + } + try { + return Integer.parseInt(String.valueOf(v).trim()); + } catch (NumberFormatException e) { + return def; + } + } + + private static String nz(String s) { + return s == null ? "" : s; + } + + private static FireRuleTaskWeaponVO toTaskWeaponVo(FireRuleInputRedWeaponElementDTO r) { + FireRuleTaskWeaponVO w = new FireRuleTaskWeaponVO(); + if (r != null) { + w.setEquipmentId(r.getEquipmentId()); + w.setName(r.getName()); + w.setSupportType(r.getSupportType()); + } + return w; + } + + private static List convertPoolToEquipmentVoList( + List pool) { + List list = new ArrayList<>(); + if (pool == null) { + return list; + } + for (FireRuleInputRedWeaponElementDTO e : pool) { + list.add(toRedEquipmentVo(e)); + } + return list; + } + + private static FireRuleRedWeaponEquipmentVO toRedEquipmentVo(FireRuleInputRedWeaponElementDTO src) { + if (src == null) { + return null; + } + FireRuleRedWeaponEquipmentVO vo = new FireRuleRedWeaponEquipmentVO(); + vo.setSupportType(src.getSupportType()); + vo.setTroopsDetail(src.getTroopsDetail()); + vo.setPlatformType(src.getPlatformType()); + vo.setIsStrikeTarget(src.getIsStrikeTarget()); + vo.setIsReconTarget(src.getIsReconTarget()); + vo.setIsInterferenceTarget(src.getIsInterferenceTarget()); + vo.setIsDefendImportantPlace(src.getIsDefendImportantPlace()); + vo.setGroupType(src.getGroupType()); + vo.setEquipmentId(src.getEquipmentId()); + vo.setName(src.getName()); + vo.setOwnerForceSide(src.getOwnerForceSide()); + vo.setPlatId(src.getPlatId()); + vo.setSubComponents(FireRuleRedWeaponOutputFillHelper.toOutputSubComponents(src.getSubComponents())); + return vo; + } } diff --git a/auto-solution-rule/src/main/resources/rules/fire-rule.drl b/auto-solution-rule/src/main/resources/rules/fire-rule.drl deleted file mode 100644 index af8804a..0000000 --- a/auto-solution-rule/src/main/resources/rules/fire-rule.drl +++ /dev/null @@ -1,3500 +0,0 @@ -package rules; - -import com.solution.rule.domain.simplerulepojo.fact.FactTask; -import com.solution.rule.domain.simplerulepojo.Task; -import com.solution.rule.domain.simplerulepojo.Weapon; -import com.solution.rule.domain.simplerulepojo.SubComponents; -import com.solution.rule.domain.simplerulepojo.ComponentParam; -import com.solution.rule.domain.simplerulepojo.Coordinate; -import com.solution.rule.domain.simplerulepojo.TrackPoints; - -import java.util.List; -import java.util.Map; -import java.util.ArrayList; - -global java.util.Map globalParams; - -//------------------------------------------------------------------------------- -rule "任务匹配1" -agenda-group "打击任务" -salience 100 -when -then - // legacy 占位:旧的固定字符串匹配已停用,改由“任务自动匹配规则”统一处理。 -end -//------------------------------------------------------------------------------- -rule "任务匹配2" -agenda-group "打击任务" -salience 100 -when -then - // legacy 占位:旧的固定字符串匹配已停用,改由“任务自动匹配规则”统一处理。 -end -//------------------------------------------------------------------------------- -rule "威胁等级规则" -agenda-group "打击任务" -salience 90 -when - //如果蓝方威胁等级大于等于3,则全局武器数量为3,添加插入导弹发射车辆和防空导弹武器 - $task : FactTask(blueTask.side == "蓝方",blueTask.threatLevel >= "3") - $redTask : FactTask(redTask.side == "红方") -then - //设置平台下组件的数量 - globalParams.put("platNum",3); - //威胁等级大于等于3固定插入导弹发射车辆 - // ========== 调用函数 ========== - threatLevels($redTask, globalParams); -end -//------------------------------------------------------------------------------- -rule "地面类型匹配规则" -agenda-group "打击任务" -salience 80 -when - //如果蓝方武器为地面类型且高度不超过500米,则使用插入空中力量打击 - $task : FactTask(blueTask.side == "蓝方") - $weapons : List() from $task.blueTask.taskWeapons - $weapon : Weapon(supportType == "ground") from $weapons -then - Task redTask = $task.getRedTask(); - List taskWeapons = redTask.getTaskWeapons(); - Weapon weapon = new Weapon(); - weapon.setName("F-16"); - weapon.setNumber((Integer) globalParams.get("platNum")); - weapon.setSupportType("plane"); - taskWeapons.add(weapon); -end -//------------------------------------------------------------------------------- -rule "空中类型匹配规则" -agenda-group "打击任务" -salience 80 -when - //如果蓝方武器为地面类型且高度不超过500米,则使用插入空中力量打击 - $task : FactTask(blueTask.side == "蓝方") - $weapons : List() from $task.blueTask.taskWeapons - $weapon : Weapon(supportType == "overhead") from $weapons -then - Task redTask = $task.getRedTask(); - List taskWeapons = redTask.getTaskWeapons(); - Weapon weapon = new Weapon(); - weapon.setName("F-16"); - weapon.setNumber((Integer) globalParams.get("platNum")); - weapon.setSupportType("plane"); - taskWeapons.add(weapon); -end -//------------------------------------------------------------------------------- -rule "装备组件匹配" -agenda-group "打击任务" -salience 70 -when -then - // legacy 规则已停用:完整武器库逻辑已在 //TODO 下接管。 - // 保留该规则名称便于回滚和历史追踪,不再执行任何动作。 -end -//------------------------------------------------------------------------------- -rule "组件参数匹配" -agenda-group "打击任务" -salience 60 -when -then - // legacy 占位规则:参数处理已并入“红方武器自适应装配规则/导弹联动增强规则”。 -end -//------------------------------------------------------------------------------- -//TODO -//------------------------------------------------------------------------------- -// ========================= 业务可改区(只改这里) ========================= -// 说明: -// 1) 业务人员只改 buildBusinessConfig() 里的值,其他函数不要改。 -// 2) 规则是“严格白名单”,未命中条件时允许不匹配(不新增红方武器)。 - // 3) 可通过开关控制是否启用空中/地面/装甲/导弹联动策略。 - // 4) 业务可直接配置“蓝方类型 -> 红方方案(多选)”,例如坦克可选火箭或导弹系统。 - -function Map buildBusinessConfig() { - Map cfg = new java.util.HashMap(); - - // ---------- 红方完整武器库名称映射(可改) ---------- - cfg.put("redStrikeDroneName", "火力打击无人机"); - cfg.put("redArmedHelicopterName", "武装直升机"); - cfg.put("redHowitzerName", "迫榴炮"); - cfg.put("redVehicleMortarName", "车载迫击炮"); - cfg.put("redAaWeaponName", "防空导弹武器"); - cfg.put("redAtRocketName", "反坦克火箭"); - cfg.put("redAtMissileSystemName", "反坦克导弹系统"); - cfg.put("redMissileVehicleName", "导弹发射车"); - - // ---------- 白名单开关(可改) ---------- - cfg.put("enableAirRule", Boolean.TRUE); // 蓝方空中 -> 红方空中反制组 - cfg.put("enableGroundRule", Boolean.TRUE); // 蓝方地面 -> 红方炮类反制组 - cfg.put("enableArmorRule", Boolean.TRUE); // 蓝方装甲 -> 红方反坦克组 - cfg.put("enableMissileVehicleRule", Boolean.FALSE); // 蓝方导弹 -> 红方导弹发射车(默认关) - cfg.put("enableMissileLinkage", Boolean.TRUE); // 导弹参数联动开关 - cfg.put("allowMultiGroup", Boolean.TRUE); // true=允许多组叠加,false=命中首组即停止 - cfg.put("enableArmedHelicopterOnAir", Boolean.TRUE); - - // ---------- 过程驱动整数加分引擎(可改) ---------- - cfg.put("enableRuleScoring", Boolean.TRUE); // true=过程驱动整数加分;false=回退旧白名单 - cfg.put("minScoreToAssign", 1); // 最低入选分(整数) - cfg.put("nearDefenseDistanceMeters", 500); // 近防区距离阈值 - cfg.put("highThreatLevel", 3); // 高威胁阈值 - cfg.put("highTargetCount", 2); // 蓝方数量阈值 - cfg.put("score_type_antiArmor", 1); // 蓝方装甲 -> 反坦克类加分 - cfg.put("score_nearDefense_artillery", 1); // 近防区 -> 迫榴炮类加分 - cfg.put("score_highThreat_missile", 1); // 高威胁 -> 导弹/防空类加分 - cfg.put("score_highCount_artillery", 1); // 蓝方数量高 -> 炮类加分 - cfg.put("score_hasMissile_airDefence", 1); // 蓝方有导弹 -> 防空类加分 - - // ---------- 数量与参数(可改) ---------- - cfg.put("defaultAirNum", 1); - cfg.put("defaultGroundNum", 1); - cfg.put("defaultMissileVehicleNum", 1); - cfg.put("shellRangeDefault", "1500"); // 炮类单位固定:范围米 - cfg.put("missileCountOffset", 1); // 红方导弹数量 = 蓝方 + offset - cfg.put("missileRangeOffset", 80); // 红方导弹范围增量 - 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("hitRateThreshold", "0.7"); // 命中率阈值:低于该值触发阶梯增量 - cfg.put("hitRateGapStep1Max", "0.5"); // gap第一档上界(gap=threshold-hitRate) - cfg.put("hitRateGapStep2Max", "1.0"); // gap第二档上界 - cfg.put("hitRateStep1Offset", 1); // 第一档固定增量 - cfg.put("hitRateStep2Offset", 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); - // 格式示例:蓝穿甲弹->红穿甲弹,蓝火控雷达->红火控雷达;为空则默认 deviceName 一致匹配 - cfg.put("componentDeviceNameMappingCsv", ""); - // 仅匹配:蓝方组件 deviceName 包含该关键词时才匹配 - cfg.put("missileComponentNameContains", "导弹"); - - // ---------- 任务自动命名模板(可改) ---------- - // 任务分类优先级:导弹突击 > 防空压制 > 反装甲打击 > 炮火压制 > 通用打击 - cfg.put("taskName_missile_strike", "导弹突击打击任务"); - cfg.put("taskName_air_defence", "防空压制打击任务"); - cfg.put("taskName_anti_armor", "反装甲打击任务"); - cfg.put("taskName_artillery", "炮火压制打击任务"); - cfg.put("taskName_general", "通用打击任务"); - cfg.put("taskDataType_missile_strike", "missile-strike"); - cfg.put("taskDataType_air_defence", "air-defence"); - cfg.put("taskDataType_anti_armor", "anti-armor"); - cfg.put("taskDataType_artillery", "artillery"); - cfg.put("taskDataType_general", "strike"); - - // ---------- targetId 自动绑定(可改) ---------- - cfg.put("enableTargetAutoBind", Boolean.TRUE); // 是否自动给红方武器绑定蓝方目标 - cfg.put("minTargetBindRatio", "0.7"); // 最低绑定比例(大部分有目标) - cfg.put("allowReserveWithoutTarget", Boolean.TRUE); // 允许少量武器 targetId 为空(火力冗余) - cfg.put("w_target_type", "0.30"); // target分配:类型匹配权重 w_target_type 越大,规则越“看重类型对口”。 - cfg.put("w_target_dist", "0.25"); // target分配:距离权重 w_target_dist 越大,系统越偏向“更近/更容易打到”的目标 射程不足会被直接淘汰,不参与加权计算 - cfg.put("w_target_height", "0.10"); // target分配:高度差权重 作用:w_target_height 越大,系统越偏向“高度层更匹配”的目标(例如空中平台对空中目标、地面平台对地面目标更自然) - cfg.put("w_target_hit", "0.20"); // target分配:命中率权重 w_target_hit 越大,高命中率武器越容易拿到更高分,从而优先绑定目标。 - cfg.put("w_target_threat", "0.15"); // target分配:蓝目标威胁权重 作用:w_target_threat 越大,红武器会更倾向优先打“高威胁蓝目标”,即使距离稍远或类型略次优也可能被拉高排名 - cfg.put("maxEffectiveDistance", 3000); // 超过该距离候选失效 - cfg.put("maxHeightGap", 800); // 超过该高度差候选失效 - cfg.put("targetDecayAlpha", "0.35"); // 边际收益递减系数 - cfg.put("fallbackToNearestTarget", Boolean.TRUE); // 候选失效时回退最近目标 - cfg.put("allowUnassignedRedWeapon", Boolean.TRUE); // 无有效候选时允许红方targetId为空 - //权重怎么用的算法要研究 - - // ---------- 阵位规则参数(可改) ---------- - cfg.put("enablePositionRules", Boolean.TRUE); // 阵位规则总开关 - // 区域来源已切换到 Task 实体字段:warZoneLocation / defZoneLocation(4点经纬度) - cfg.put("fireUnitSpacingMeters", 100); // 火力单元间距(米) - cfg.put("airDeployZonePreference", "combat"); // 飞机优先部署区:combat/defense - cfg.put("defensePriorityWeapons", "反坦克导弹系统,反坦克火箭,车载迫击炮,迫榴炮"); // 优先部署防区武器 - cfg.put("groundDeployHeight", 20); // 地面武器部署高度 - cfg.put("airDeployHeight", 300); // 空中武器部署高度(自动高度关闭或兜底时使用) - cfg.put("enableAutoAirDeployHeight", Boolean.TRUE); // 空中部署高度自动生成开关 - cfg.put("airHeightMin", 50); // 自动高度下限 - cfg.put("airHeightMax", 20000); // 自动高度上限 - cfg.put("airHeightFallback", 300); // 无航迹/无蓝方武器高度时的兜底高度 - cfg.put("airHeightSpeedThreshold", 180); // 速度修正:平均速度≥此值视为“快” - cfg.put("airHeightAdjustFast", 40); // 速度快时额外抬高(米) - cfg.put("airHeightNearDefenseDistance", 800); // 距防区“近”阈值(米) - cfg.put("airHeightFarDefenseDistance", 2500); // 距防区“远”阈值(米) - cfg.put("airHeightAdjustNear", -30); // 近防区高度修正(通常为负,压低) - cfg.put("airHeightAdjustFar", 40); // 远防区高度修正(抬高) - cfg.put("airHeightAdjustMain", 20); // 主机相对基准额外高度 - cfg.put("airHeightAdjustWing", -10); // 僚机相对基准额外高度 - cfg.put("formationDefaultType", "TRIANGLE"); // 默认阵型:TRIANGLE/DIAMOND/LINE/COLUMN/WEDGE - cfg.put("enableAutoFormationSelect", Boolean.TRUE); // 阵型自动选择开关 - cfg.put("formationFastSpeedThreshold", 180); // 自动选阵型:高速阈值 - cfg.put("formationNearDefenseDistance", 800); // 自动选阵型:近防区阈值 - cfg.put("formationFarDefenseDistance", 2500); // 自动选阵型:远防区阈值 - cfg.put("formationHighThreatLevel", 3); // 自动选阵型:高威胁阈值 - cfg.put("formationLargeGroupCount", 6); // 自动选阵型:大编队阈值 - cfg.put("formationRule_near_fast", "WEDGE"); // 近快态势阵型 - cfg.put("formationRule_far_fast", "LINE"); // 远快态势阵型 - cfg.put("formationRule_near_slow", "DIAMOND"); // 近慢态势阵型 - cfg.put("formationRule_high_threat", "DIAMOND"); // 高威胁优先阵型 - cfg.put("formationRule_large_group", "COLUMN"); // 大编队优先阵型 - cfg.put("formationRule_air_majority", "LINE"); // 空中占比高阵型 - cfg.put("formationRule_default", "TRIANGLE"); // 自动选择默认阵型 - cfg.put("formationDefaultSpacingMeters", 120); // 阵型间距兜底(自动计算失败时) - cfg.put("mainWingDistanceDefaultMeters", 100); // 主僚距离兜底(自动计算失败时) - cfg.put("formationHeadingDefaultDeg", 0); // 默认阵型朝向 - cfg.put("formationSpacingMinMeters", 60); // 自动间距下限 - cfg.put("formationSpacingMaxMeters", 220); // 自动间距上限 - cfg.put("defenseScaleMinMeters", 300); // 防区尺度映射下限 - cfg.put("defenseScaleMaxMeters", 3000); // 防区尺度映射上限 - cfg.put("mainWingDistanceMinMeters", 60); // 自动主僚距离下限 - cfg.put("mainWingDistanceMaxMeters", 260); // 自动主僚距离上限 - cfg.put("mainWingDistanceModeFactor_near_fast", "0.90"); // 近快:更紧凑 - cfg.put("mainWingDistanceModeFactor_far_fast", "1.15"); // 远快:更疏开 - cfg.put("mainWingDistanceModeFactor_near_slow", "0.95"); // 近慢:略紧凑 - cfg.put("mainWingDistanceModeFactor_default", "1.00"); // 默认系数 - cfg.put("speedFastThreshold", 180); // 蓝方高速阈值 - cfg.put("distanceNearDefenseThresholdMeters", 800); // 近防区阈值 - cfg.put("distanceFarDefenseThresholdMeters", 2500); // 远防区阈值 - - // ---------- 航迹规则参数(可改) ---------- - cfg.put("enableTrajectoryRules", Boolean.TRUE); // 航迹规则总开关 - // 智能 最短的 侧面 干扰 - cfg.put("strategyMode", "shortest"); // auto/shortest/flank/interfere - cfg.put("enableShortest", Boolean.TRUE); - cfg.put("enableFlank", Boolean.TRUE); - cfg.put("enableInterfere", Boolean.TRUE); - cfg.put("nearDefDistanceMeters", 800); // 近防区阈值 - cfg.put("farDefDistanceMeters", 2500); // 远防区阈值 - cfg.put("fastSpeedThreshold", 180); // 快速阈值 - cfg.put("flankOffsetMeters", 150); // 绕后偏移 - cfg.put("interfereOffsetMeters", 120); // 干扰基础偏移 - cfg.put("interfereZigzagAmplitude", 90); // 干扰锯齿幅度 - cfg.put("keepBlueHeight", Boolean.TRUE); // true=沿用蓝方高度 - cfg.put("redTrackHeightOverride", 200); // keepBlueHeight=false 时生效 - - return cfg; -} - -//------------------------------------------------------------------------------- -rule "红方武器自适应装配规则" -agenda-group "打击任务" -salience 55 -when - // 蓝方与红方任务都存在时触发,做“武器+组件”的基础装配 - $fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方") -then - Map cfg = buildBusinessConfig(); - configureRedWeaponsByBlue($fact, cfg); -end - -//------------------------------------------------------------------------------- -rule "导弹联动增强规则" -agenda-group "打击任务" -salience 54 -when - // 在基础装配后执行:若蓝方挂载导弹,红方空中武器自动增强导弹能力 - $fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方") -then - Map cfg = buildBusinessConfig(); - applyMissileLinkage($fact, cfg); -end - -//------------------------------------------------------------------------------- -// 仅导弹组件匹配:按 redWeapon.targetId 对应蓝方装备,仅覆盖导弹组件的数量与首参数 -rule "导弹组件数量匹配规则" -agenda-group "打击任务" -salience 53 -when - $fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方") -then - Map cfg = buildBusinessConfig(); - 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 "打击任务" -salience 50 -when - // 以红方最终武器为主自动生成任务名,保证任务名与武器一致 - $fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方") -then - Map cfg = buildBusinessConfig(); - assignTaskNameByRedWeapons($fact, cfg); -end - -//------------------------------------------------------------------------------- -rule "阵位规则-区域解析与点位生成" -agenda-group "打击任务" -salience 49 -when - $fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方") -then - Map cfg = buildBusinessConfig(); - prepareDeploymentPools($fact, cfg, globalParams); -end - -//------------------------------------------------------------------------------- -rule "阵位规则-武器部署赋位" -agenda-group "打击任务" -salience 48 -when - $fact : FactTask(blueTask.side == "蓝方", redTask.side == "红方") -then - Map cfg = buildBusinessConfig(); - applyWeaponDeployment($fact, cfg, globalParams); -end - -//------------------------------------------------------------------------------- -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 == "红方") -then - Map cfg = buildBusinessConfig(); - applyTrajectoryGeneration($fact, cfg); -end - -//------------------------------------------------------------------------------- -// 说明:以下函数全部是 DRL function(不是 Java 类方法) -// 目标:让不懂代码的业务同事只改“可调整常量区”即可完成策略调整 - -// 根据蓝方武器结构,按业务映射装配红方武器并写入基础组件 -function void configureRedWeaponsByBlue( - FactTask fact, - Map cfg -) { - if (fact == null || fact.getBlueTask() == null || fact.getRedTask() == null) { - return; - } - List blueWeapons = fact.getBlueTask().getTaskWeapons(); - if (blueWeapons == null || blueWeapons.isEmpty()) { - return; - } - Task redTask = fact.getRedTask(); - List redWeapons = redTask.getTaskWeapons(); - if (redWeapons == null) { - redWeapons = new ArrayList<>(); - redTask.setTaskWeapons(redWeapons); - } - - // 蓝方组件模板(用于测试和参数联动):若蓝方没填组件,给一个合理默认值 - buildBlueTestComponents(blueWeapons); - - // 过程驱动模式:根据蓝方类型/数量/位置等态势特征评分选武器 - if (readBooleanCfg(cfg, "enableRuleScoring", true)) { - runProcessDrivenSelection(fact, cfg); - return; - } - - boolean hasBlueAir = false; - boolean hasBlueGround = false; - boolean hasBlueArtillery = false; - boolean hasBlueArmor = false; - boolean hasBlueMissile = false; - for (Object obj : blueWeapons) { - Weapon blueWeapon = (Weapon) obj; - if (blueWeapon == null) { - continue; - } - if (isAirWeapon(blueWeapon)) { - hasBlueAir = true; - } - if (isGroundWeapon(blueWeapon)) { - hasBlueGround = true; - } - if (isArtilleryWeapon(blueWeapon)) { - hasBlueArtillery = true; - } - if (isArmorWeapon(blueWeapon)) { - hasBlueArmor = true; - } - if (hasMissileComponent(blueWeapon)) { - hasBlueMissile = true; - } - } - - boolean allowMultiGroup = readBooleanCfg(cfg, "allowMultiGroup", true); - boolean matchedAny = false; - - // 严格白名单-1:空中目标反制组(由 map_air_targets 控制) - if (readBooleanCfg(cfg, "enableAirRule", true) && hasBlueAir && (!matchedAny || allowMultiGroup)) { - int before = redWeapons.size(); - applyMappedWeapons(redWeapons, cfg, "map_air_targets", readIntCfg(cfg, "defaultAirNum", 1), readBooleanCfg(cfg, "enableArmedHelicopterOnAir", true)); - if (redWeapons.size() > before) { - matchedAny = true; - } - } - - // 严格白名单-2:地面目标炮类组(由 map_ground_targets 控制) - if (readBooleanCfg(cfg, "enableGroundRule", true) && hasBlueGround && (!matchedAny || allowMultiGroup)) { - int before = redWeapons.size(); - applyMappedWeapons(redWeapons, cfg, "map_ground_targets", readIntCfg(cfg, "defaultGroundNum", 1), true); - if (redWeapons.size() > before) { - matchedAny = true; - } - } - - // 严格白名单-3:装甲目标反坦克组(由 map_armor_targets 控制) - if (readBooleanCfg(cfg, "enableArmorRule", true) && hasBlueArmor && (!matchedAny || allowMultiGroup)) { - int before = redWeapons.size(); - applyMappedWeapons(redWeapons, cfg, "map_armor_targets", readIntCfg(cfg, "defaultGroundNum", 1), true); - if (redWeapons.size() > before) { - matchedAny = true; - } - } - - // 严格白名单-4:炮类目标反制组(由 map_artillery_targets 控制) - if (hasBlueArtillery && (!matchedAny || allowMultiGroup)) { - int before = redWeapons.size(); - applyMappedWeapons(redWeapons, cfg, "map_artillery_targets", readIntCfg(cfg, "defaultGroundNum", 1), true); - if (redWeapons.size() > before) { - matchedAny = true; - } - } - - // 严格白名单-5:导弹补充组(默认关闭,由 map_missile_targets 控制) - if (readBooleanCfg(cfg, "enableMissileVehicleRule", false) && hasBlueMissile && (!matchedAny || allowMultiGroup)) { - int before = redWeapons.size(); - applyMappedWeapons(redWeapons, cfg, "map_missile_targets", readIntCfg(cfg, "defaultMissileVehicleNum", 1), true); - if (redWeapons.size() > before) { - matchedAny = true; - } - } - - // 炮类限制仅对已匹配策略生效;若本轮未命中白名单,不做任何新增/补齐 - if (matchedAny && hasBlueArtillery) { - limitRedArtilleryToShellOnly(redWeapons, (String) cfg.get("shellRangeDefault")); - } - - // 写入红方武器默认命中率(若入参已给 hitRate,则不覆盖) - applyDefaultHitRateIfAbsent(redWeapons, cfg); - - // 自动绑定红方武器 targetId(来源:蓝方 equipmentId) - bindTargetIdsForRedWeapons(redWeapons, blueWeapons, cfg); -} - -//------------------------------------------------------------------------------- -// 过程驱动主流程:特征提取 -> 评分选武器 -> 动态分配 -function void runProcessDrivenSelection(FactTask fact, Map cfg) { - if (fact == null || fact.getBlueTask() == null || fact.getRedTask() == null) { - return; - } - Task blueTask = fact.getBlueTask(); - Task redTask = fact.getRedTask(); - List blueWeapons = blueTask.getTaskWeapons(); - if (blueWeapons == null || blueWeapons.isEmpty()) { - return; - } - List redWeapons = redTask.getTaskWeapons(); - if (redWeapons == null) { - redWeapons = new ArrayList(); - redTask.setTaskWeapons(redWeapons); - } else { - redWeapons.clear(); - } - - List blueFeatures = extractBlueTargetFeatures(blueTask, cfg); - if (blueFeatures == null || blueFeatures.isEmpty()) { - return; - } - List redPool = getRedWeaponPoolByConfig(cfg); - if (redPool == null || redPool.isEmpty()) { - return; - } - - double desiredKill = normalizeProbability(readDoubleCfg(cfg, "desiredKillProbability", 0.9d), 0.9d); - int minAssign = readIntCfg(cfg, "minRedWeaponsPerBlueTarget", 1); - int maxAssign = readIntCfg(cfg, "maxRedWeaponsPerBlueTargetCap", 3); - if (maxAssign <= 0) { - maxAssign = 1; - } - if (minAssign < 0) { - minAssign = 0; - } - if (minAssign > maxAssign) { - minAssign = maxAssign; - } - int minScore = readIntCfg(cfg, "minScoreToAssign", 1); - - for (Object featObj : blueFeatures) { - Map feat = (Map) featObj; - String blueId = String.valueOf(feat.get("blueId")); - Weapon blueWeapon = (Weapon) feat.get("blueWeapon"); - if (blueWeapon == null || isBlank(blueId)) { - continue; - } - - List topNames = pickTopScoredWeapons(feat, redPool, cfg, minScore); - if (topNames == null || topNames.isEmpty()) { - continue; - } - int needCount = computeNeededRedCountFromFeature(feat, cfg, desiredKill); - allocateRedWeaponsForBlueTarget(topNames, needCount, minAssign, maxAssign, blueId, redWeapons, cfg); - } - - if (!redWeapons.isEmpty()) { - applyDefaultHitRateIfAbsent(redWeapons, cfg); - if (containsBlueArtillery(blueWeapons)) { - limitRedArtilleryToShellOnly(redWeapons, (String) cfg.get("shellRangeDefault")); - } - } -} - -function List extractBlueTargetFeatures(Task blueTask, Map cfg) { - List result = new ArrayList(); - if (blueTask == null || blueTask.getTaskWeapons() == null) { - return result; - } - int threat = parseIntSafe(blueTask.getThreatLevel(), 1); - for (Object obj : blueTask.getTaskWeapons()) { - Weapon blue = (Weapon) obj; - if (blue == null || isBlank(blue.getEquipmentId())) { - continue; - } - Map feature = new java.util.HashMap(); - feature.put("blueId", blue.getEquipmentId()); - feature.put("blueWeapon", blue); - feature.put("isAir", Boolean.valueOf(isAirWeapon(blue))); - feature.put("isArmor", Boolean.valueOf(isArmorWeapon(blue))); - feature.put("isArtillery", Boolean.valueOf(isArtilleryWeapon(blue))); - feature.put("isGround", Boolean.valueOf(isGroundWeapon(blue))); - feature.put("hasMissile", Boolean.valueOf(hasMissileComponent(blue))); - int blueNum = blue.getNumber() == null || blue.getNumber() <= 0 ? 1 : blue.getNumber().intValue(); - feature.put("blueNum", Integer.valueOf(blueNum)); - feature.put("threat", Integer.valueOf(threat)); - feature.put("distance", Double.valueOf(computeBlueDistanceToDefenseMeters(blue, blueTask))); - result.add(feature); - } - return result; -} - -function List pickTopScoredWeapons(Map feature, List redPool, Map cfg, int minScoreToAssign) { - List result = new ArrayList(); - if (feature == null || redPool == null || redPool.isEmpty()) { - return result; - } - int maxScore = Integer.MIN_VALUE; - for (Object obj : redPool) { - String redName = String.valueOf(obj); - if (isBlank(redName)) { - continue; - } - int s = scoreRedWeaponByRules(feature, redName, cfg); - if (s > maxScore) { - maxScore = s; - } - } - if (maxScore < minScoreToAssign) { - return result; - } - for (Object obj : redPool) { - String redName = String.valueOf(obj); - if (isBlank(redName)) { - continue; - } - int s = scoreRedWeaponByRules(feature, redName, cfg); - if (s == maxScore) { - addUnique(result, redName); - } - } - return result; -} - -function int scoreRedWeaponByRules(Map feature, String redName, Map cfg) { - if (feature == null || redName == null) { - return 0; - } - int score = 0; - boolean isAir = ((Boolean) feature.get("isAir")).booleanValue(); - boolean isArmor = ((Boolean) feature.get("isArmor")).booleanValue(); - boolean isArtillery = ((Boolean) feature.get("isArtillery")).booleanValue(); - boolean hasMissile = ((Boolean) feature.get("hasMissile")).booleanValue(); - int blueNum = ((Integer) feature.get("blueNum")).intValue(); - int threat = ((Integer) feature.get("threat")).intValue(); - double distance = ((Double) feature.get("distance")).doubleValue(); - - if (isArmor && redName.contains("反坦克")) { - score += readIntCfg(cfg, "score_type_antiArmor", 1); - } - if (isAir && (redName.contains("防空导弹") || redName.contains("无人机") || redName.contains("直升机"))) { - score += readIntCfg(cfg, "score_highThreat_missile", 1); - } - if (isArtillery && (redName.contains("迫榴炮") || redName.contains("迫击炮"))) { - score += 1; - } - if (distance <= readIntCfg(cfg, "nearDefenseDistanceMeters", 500) && redName.contains("迫榴炮")) { - score += readIntCfg(cfg, "score_nearDefense_artillery", 1); - } - if (threat >= readIntCfg(cfg, "highThreatLevel", 3) && (redName.contains("导弹") || redName.contains("防空"))) { - score += readIntCfg(cfg, "score_highThreat_missile", 1); - } - if (blueNum >= readIntCfg(cfg, "highTargetCount", 2) && (redName.contains("迫榴炮") || redName.contains("迫击炮"))) { - score += readIntCfg(cfg, "score_highCount_artillery", 1); - } - if (hasMissile && (redName.contains("防空导弹") || redName.contains("导弹发射车"))) { - score += readIntCfg(cfg, "score_hasMissile_airDefence", 1); - } - - int estRange = estimateRangeByRedWeaponName(redName); - int margin = readIntCfg(cfg, "rangeSafetyMarginMeters", 50); - if (distance < Double.MAX_VALUE / 2.0d && estRange >= (int) Math.ceil(distance) + margin) { - score += 1; - } - return score; -} - -function void allocateRedWeaponsForBlueTarget(List topNames, int needCount, int minAssign, int maxAssign, String blueId, List redWeapons, Map cfg) { - if (topNames == null || topNames.isEmpty() || isBlank(blueId) || redWeapons == null) { - return; - } - int finalNeed = needCount; - if (finalNeed < minAssign) { - finalNeed = minAssign; - } - if (finalNeed > maxAssign) { - finalNeed = maxAssign; - } - int assigned = 0; - while (assigned < finalNeed) { - for (Object obj : topNames) { - if (assigned >= finalNeed) { - break; - } - String redName = String.valueOf(obj); - Weapon red = createRedWeaponInstance(redName, inferSupportTypeByWeaponName(redName), 1); - red.setTargetId(blueId); - ensureBasicRedComponents(red); - red.setHitRate(Double.valueOf(resolveHitRateByName(redName, cfg))); - redWeapons.add(red); - assigned++; - } - } -} - -function int computeNeededRedCountFromFeature(Map feature, Map cfg, double desiredKill) { - if (feature == null) { - return 1; - } - int blueNum = ((Integer) feature.get("blueNum")).intValue(); - int threat = ((Integer) feature.get("threat")).intValue(); - double distance = ((Double) feature.get("distance")).doubleValue(); - double pBase = normalizeProbability(readDoubleCfg(cfg, "defaultHitRateFallback", 0.6d), 0.6d); - if (((Boolean) feature.get("isArmor")).booleanValue()) { - pBase = pBase + 0.05d; - } - if (((Boolean) feature.get("isAir")).booleanValue()) { - pBase = pBase - 0.05d; - } - if (distance > 1500.0d) { - pBase = pBase - 0.06d; - } - pBase = normalizeProbability(pBase, 0.6d); - int oneTargetNeed = computeRequiredShots(pBase, desiredKill, 1); - int count = oneTargetNeed + Math.max(0, blueNum - 1); - if (threat >= 3) { - count++; - } - return Math.max(1, count); -} - -function double computeBlueDistanceToDefenseMeters(Weapon blueWeapon, Task blueTask) { - if (blueWeapon == null || blueWeapon.getCoordinate() == null || blueTask == null || blueTask.getDefZoneLocation() == null) { - return Double.MAX_VALUE; - } - Coordinate b = blueWeapon.getCoordinate(); - if (b.getLongitude() == null || b.getLatitude() == null) { - return Double.MAX_VALUE; - } - double best = Double.MAX_VALUE; - for (Object obj : blueTask.getDefZoneLocation()) { - Coordinate c = (Coordinate) obj; - if (c == null || c.getLongitude() == null || c.getLatitude() == null) { - continue; - } - double d = approxDistanceMeters( - b.getLongitude().doubleValue(), - b.getLatitude().doubleValue(), - c.getLongitude().doubleValue(), - c.getLatitude().doubleValue() - ); - if (d < best) { - best = d; - } - } - return best; -} - -function int estimateRangeByRedWeaponName(String redName) { - if (redName == null) { - return 800; - } - if (redName.contains("反坦克导弹系统")) { - return 1800; - } - if (redName.contains("导弹发射车")) { - return 2500; - } - if (redName.contains("防空导弹")) { - return 2200; - } - if (redName.contains("武装直升机")) { - return 1500; - } - if (redName.contains("无人机")) { - return 1200; - } - if (redName.contains("迫榴炮") || redName.contains("迫击炮")) { - return 1600; - } - if (redName.contains("反坦克火箭")) { - return 1000; - } - return 900; -} - -function double resolveHitRateByName(String redName, Map cfg) { - Map hitRateMap = parseNameDoubleCsv((String) cfg.get("hitRateCsv")); - Double p = (Double) hitRateMap.get(redName); - if (p == null) { - p = Double.valueOf(readDoubleCfg(cfg, "defaultHitRateFallback", 0.6d)); - } - return normalizeProbability(p.doubleValue(), 0.6d); -} - -function List getRedWeaponPoolByConfig(Map cfg) { - List pool = new ArrayList(); - addUnique(pool, (String) cfg.get("redStrikeDroneName")); - addUnique(pool, (String) cfg.get("redArmedHelicopterName")); - addUnique(pool, (String) cfg.get("redHowitzerName")); - addUnique(pool, (String) cfg.get("redVehicleMortarName")); - addUnique(pool, (String) cfg.get("redAaWeaponName")); - addUnique(pool, (String) cfg.get("redAtRocketName")); - addUnique(pool, (String) cfg.get("redAtMissileSystemName")); - addUnique(pool, (String) cfg.get("redMissileVehicleName")); - return pool; -} - -function Weapon createRedWeaponInstance(String name, String supportType, int number) { - Weapon w = new Weapon(); - w.setName(name); - w.setSupportType(supportType); - w.setNumber(number); - w.setComponents(new ArrayList()); - return w; -} - -function boolean containsBlueArtillery(List blueWeapons) { - if (blueWeapons == null) { - return false; - } - for (Object obj : blueWeapons) { - Weapon w = (Weapon) obj; - if (w != null && isArtilleryWeapon(w)) { - return true; - } - } - return false; -} - -function void applyMappedWeapons(List redWeapons, Map cfg, String mapKey, int defaultNum, boolean allowArmedHelicopter) { - List mappedNames = parseMappedWeaponNames(cfg, mapKey); - if (mappedNames == null || mappedNames.isEmpty()) { - return; - } - for (Object obj : mappedNames) { - String weaponName = (String) obj; - if (!allowArmedHelicopter && weaponName != null && weaponName.equals((String) cfg.get("redArmedHelicopterName"))) { - continue; - } - String supportType = inferSupportTypeByWeaponName(weaponName); - Weapon redWeapon = ensureRedWeapon(redWeapons, weaponName, supportType, defaultNum); - ensureBasicRedComponents(redWeapon); - } -} - -function List parseMappedWeaponNames(Map cfg, String mapKey) { - List result = new ArrayList(); - if (cfg == null || mapKey == null) { - return result; - } - Object raw = cfg.get(mapKey); - if (raw == null) { - return result; - } - String text = String.valueOf(raw); - if (text == null || text.trim().equals("")) { - return result; - } - String[] parts = text.split(","); - for (int i = 0; i < parts.length; i++) { - String one = parts[i]; - if (one == null) { - continue; - } - String name = one.trim(); - if (name.equals("")) { - continue; - } - if (isValidRedWeaponNameByConfig(cfg, name) && !containsString(result, name)) { - result.add(name); - } - } - return result; -} - -function List parseCsvList(String text) { - List result = new ArrayList(); - if (text == null || text.trim().equals("")) { - return result; - } - String[] parts = text.split(","); - for (int i = 0; i < parts.length; i++) { - String one = parts[i]; - if (one == null) { - continue; - } - String item = one.trim(); - if (!item.equals("") && !result.contains(item)) { - result.add(item); - } - } - return result; -} - -function boolean isValidRedWeaponNameByConfig(Map cfg, String weaponName) { - if (cfg == null || weaponName == null || weaponName.equals("")) { - return false; - } - return weaponName.equals((String) cfg.get("redStrikeDroneName")) - || weaponName.equals((String) cfg.get("redArmedHelicopterName")) - || weaponName.equals((String) cfg.get("redHowitzerName")) - || weaponName.equals((String) cfg.get("redVehicleMortarName")) - || weaponName.equals((String) cfg.get("redAaWeaponName")) - || weaponName.equals((String) cfg.get("redAtRocketName")) - || weaponName.equals((String) cfg.get("redAtMissileSystemName")) - || weaponName.equals((String) cfg.get("redMissileVehicleName")); -} - -function boolean containsString(List values, String target) { - if (values == null || target == null) { - return false; - } - for (Object obj : values) { - if (obj != null && target.equals(String.valueOf(obj))) { - return true; - } - } - return false; -} - -function String inferSupportTypeByWeaponName(String weaponName) { - if (weaponName == null) { - return "ground"; - } - if (weaponName.contains("无人机") || weaponName.contains("直升机")) { - return "overhead"; - } - if (weaponName.contains("防空导弹")) { - return "antiaircraft"; - } - return "ground"; -} - -// 蓝方若有导弹,红方空中武器补导弹:数量 = 蓝方 + offset,范围略高 -function void applyMissileLinkage( - FactTask fact, - Map cfg -) { - 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; - } - - if (!readBooleanCfg(cfg, "enableMissileLinkage", true)) { - return; - } - int blueMissileCount = countBlueMissileNumber(blueWeapons); - if (blueMissileCount < readIntCfg(cfg, "minBlueMissileCountForLinkage", 1)) { - return; - } - int blueMissileRange = readBlueMissileRange(blueWeapons, readIntCfg(cfg, "blueMissileRangeDefault", 220)); - int redMissileTarget = blueMissileCount + readIntCfg(cfg, "missileCountOffset", 1); - int redRangeTarget = blueMissileRange + readIntCfg(cfg, "missileRangeOffset", 80); - - for (Object obj : redWeapons) { - Weapon redWeapon = (Weapon) obj; - if (redWeapon == null || !isRedAirWeapon(redWeapon)) { - continue; - } - ensureMissileComponentForRedAirWeapon(redWeapon, redMissileTarget, redRangeTarget); - } -} - -function void bindTargetIdsForRedWeapons(List redWeapons, List blueWeapons, Map cfg) { - if (!readBooleanCfg(cfg, "enableTargetAutoBind", true)) { - return; - } - if (redWeapons == null || redWeapons.isEmpty() || blueWeapons == null || blueWeapons.isEmpty()) { - return; - } - Map blueById = indexBlueWeaponsById(blueWeapons); - Map assignedCountByBlueId = new java.util.HashMap(); - double decayAlpha = readDoubleCfg(cfg, "targetDecayAlpha", 0.35d); - boolean fallbackToNearest = readBooleanCfg(cfg, "fallbackToNearestTarget", true); - boolean allowUnassigned = readBooleanCfg(cfg, "allowUnassignedRedWeapon", true); - int total = redWeapons.size(); - int bound = 0; - - // 核心:每把红武器选择“当前边际收益最高”的蓝目标 - for (Object objR : redWeapons) { - Weapon redWeapon = (Weapon) objR; - if (redWeapon == null) { - continue; - } - String bestBlueId = null; - double bestMarginal = -1.0d; - for (Object objB : blueWeapons) { - Weapon blueWeapon = (Weapon) objB; - if (blueWeapon == null || isBlank(blueWeapon.getEquipmentId())) { - continue; - } - if (!isPairFeasible(redWeapon, blueWeapon, cfg)) { - continue; - } - double baseScore = computePairScore(redWeapon, blueWeapon, cfg); - Integer countObj = (Integer) assignedCountByBlueId.get(blueWeapon.getEquipmentId()); - int k = countObj == null ? 0 : countObj.intValue(); - double decay = 1.0d / (1.0d + decayAlpha * k); - double marginal = baseScore * decay; - if (marginal > bestMarginal) { - bestMarginal = marginal; - bestBlueId = blueWeapon.getEquipmentId(); - } - } - if (isBlank(bestBlueId) && fallbackToNearest) { - bestBlueId = pickNearestBlueId(redWeapon, blueWeapons); - } - if (isBlank(bestBlueId)) { - if (!allowUnassigned) { - bestBlueId = pickAnyBlueId(blueWeapons); - } - } - if (!isBlank(bestBlueId)) { - redWeapon.setTargetId(bestBlueId); - bound++; - incrementAssignedCount(assignedCountByBlueId, bestBlueId); - } - } - - double minRatio = readDoubleCfg(cfg, "minTargetBindRatio", 0.7d); - boolean allowReserveWithoutTarget = readBooleanCfg(cfg, "allowReserveWithoutTarget", true); - if (!allowReserveWithoutTarget) { - for (Object obj : redWeapons) { - Weapon redWeapon = (Weapon) obj; - if (redWeapon == null || !isBlank(redWeapon.getTargetId())) { - continue; - } - String any = pickAnyBlueId(blueWeapons); - if (!isBlank(any)) { - redWeapon.setTargetId(any); - bound++; - } - } - return; - } - // 允许冗余时,只保证最低绑定比例 - if (total > 0 && ((double) bound / (double) total) < minRatio) { - for (Object obj : redWeapons) { - Weapon redWeapon = (Weapon) obj; - if (redWeapon == null || !isBlank(redWeapon.getTargetId())) { - continue; - } - String any = pickAnyBlueId(blueWeapons); - if (isBlank(any)) { - continue; - } - redWeapon.setTargetId(any); - bound++; - if (((double) bound / (double) total) >= minRatio) { - break; - } - } - } -} - -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 threshold = normalizeProbability(readDoubleCfg(cfg, "hitRateThreshold", 0.7d), 0.7d); - double step1Max = readDoubleCfg(cfg, "hitRateGapStep1Max", 0.5d); - double step2Max = readDoubleCfg(cfg, "hitRateGapStep2Max", 1.0d); - int step1Offset = readIntCfg(cfg, "hitRateStep1Offset", 1); - int step2Offset = readIntCfg(cfg, "hitRateStep2Offset", 2); - if (step1Max < 0.0d) { - step1Max = 0.0d; - } - if (step2Max < step1Max) { - step2Max = step1Max; - } - if (step1Offset < 0) { - step1Offset = 0; - } - if (step2Offset < step1Offset) { - step2Offset = step1Offset; - } - 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); - double gap = threshold - pHit; - if (gap <= 0.0d) { - offset = 0; - } else if (gap <= step1Max) { - offset = step1Offset; - } else if (gap <= step2Max) { - offset = step2Offset; - } else { - // 超出第二档时按第二档处理,避免火力数量无上限膨胀 - offset = step2Offset; - } - } - 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 (tryReplaceWithBetterRangeWeapon(redWeapon, minRequired, cfg)) { - continue; - } - if (autoAdjust) { - setWeaponFirstRangeParamAtLeast(redWeapon, minRequired); - } - } -} - -function boolean tryReplaceWithBetterRangeWeapon(Weapon redWeapon, int minRequired, Map cfg) { - if (redWeapon == null || redWeapon.getName() == null) { - return false; - } - String current = redWeapon.getName(); - String replacement = null; - if (current.contains("反坦克火箭")) { - replacement = "反坦克导弹系统"; - } else if (current.contains("迫击炮")) { - replacement = "迫榴炮"; - } else if (current.contains("无人机")) { - replacement = "武装直升机"; - } - if (isBlank(replacement)) { - return false; - } - if (estimateRangeByRedWeaponName(replacement) < minRequired) { - return false; - } - redWeapon.setName(replacement); - redWeapon.setSupportType(inferSupportTypeByWeaponName(replacement)); - redWeapon.setComponents(new ArrayList()); - ensureBasicRedComponents(redWeapon); - redWeapon.setHitRate(Double.valueOf(resolveHitRateByName(replacement, cfg))); - return true; -} - -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 boolean isPairFeasible(Weapon redWeapon, Weapon blueWeapon, Map cfg) { - if (redWeapon == null || blueWeapon == null || isBlank(blueWeapon.getEquipmentId())) { - return false; - } - // 类型禁配(最基础约束) - String pool = inferBluePoolKeyForRedWeapon(redWeapon); - if ("air".equals(pool) && !isAirWeapon(blueWeapon)) { - return false; - } - if ("armor".equals(pool) && !isArmorWeapon(blueWeapon)) { - return false; - } - if ("artillery".equals(pool) && !isArtilleryWeapon(blueWeapon) && !isGroundWeapon(blueWeapon)) { - return false; - } - // 距离/高度约束(有坐标时启用) - Coordinate rc = redWeapon.getCoordinate(); - Coordinate bc = blueWeapon.getCoordinate(); - if (isCoordinateUsable(rc) && isCoordinateUsable(bc)) { - double d = approxDistanceMeters( - rc.getLongitude().doubleValue(), - rc.getLatitude().doubleValue(), - bc.getLongitude().doubleValue(), - bc.getLatitude().doubleValue() - ); - int maxDist = readIntCfg(cfg, "maxEffectiveDistance", 3000); - if (d > maxDist) { - return false; - } - int rh = rc.getHeight() == null ? 0 : rc.getHeight().intValue(); - int bh = bc.getHeight() == null ? 0 : bc.getHeight().intValue(); - int maxGap = readIntCfg(cfg, "maxHeightGap", 800); - if (Math.abs(rh - bh) > maxGap) { - return false; - } - int estRange = estimateRangeByRedWeaponName(redWeapon.getName()); - int margin = readIntCfg(cfg, "rangeSafetyMarginMeters", 50); - if (estRange < (int) Math.ceil(d) + margin) { - return false; - } - } - return true; -} - -function double computePairScore(Weapon redWeapon, Weapon blueWeapon, Map cfg) { - double wType = readDoubleCfg(cfg, "w_target_type", 0.30d); - double wDist = readDoubleCfg(cfg, "w_target_dist", 0.25d); - double wHeight = readDoubleCfg(cfg, "w_target_height", 0.10d); - double wHit = readDoubleCfg(cfg, "w_target_hit", 0.20d); - double wThreat = readDoubleCfg(cfg, "w_target_threat", 0.15d); - double typeFit = computeTypeFit(redWeapon, blueWeapon); - double distanceFit = computeDistanceFit(redWeapon, blueWeapon, cfg); - double heightFit = computeHeightFit(redWeapon, blueWeapon, cfg); - double hitContribution = resolveWeaponHitRate(redWeapon, cfg); - double threatWeight = computeBlueThreatWeight(blueWeapon); - return (wType * typeFit) + (wDist * distanceFit) + (wHeight * heightFit) + (wHit * hitContribution) + (wThreat * threatWeight); -} - -function double computeTypeFit(Weapon redWeapon, Weapon blueWeapon) { - String pool = inferBluePoolKeyForRedWeapon(redWeapon); - if ("air".equals(pool) && isAirWeapon(blueWeapon)) { - return 1.0d; - } - if ("armor".equals(pool) && isArmorWeapon(blueWeapon)) { - return 1.0d; - } - if ("artillery".equals(pool) && isArtilleryWeapon(blueWeapon)) { - return 0.9d; - } - if ("missile".equals(pool) && hasMissileComponent(blueWeapon)) { - return 0.9d; - } - if ("ground".equals(pool) && isGroundWeapon(blueWeapon)) { - return 0.7d; - } - return 0.2d; -} - -function double computeDistanceFit(Weapon redWeapon, Weapon blueWeapon, Map cfg) { - Coordinate rc = redWeapon == null ? null : redWeapon.getCoordinate(); - Coordinate bc = blueWeapon == null ? null : blueWeapon.getCoordinate(); - if (!isCoordinateUsable(rc) || !isCoordinateUsable(bc)) { - return 0.5d; - } - double d = approxDistanceMeters( - rc.getLongitude().doubleValue(), - rc.getLatitude().doubleValue(), - bc.getLongitude().doubleValue(), - bc.getLatitude().doubleValue() - ); - int maxDist = readIntCfg(cfg, "maxEffectiveDistance", 3000); - if (maxDist <= 0) { - maxDist = 3000; - } - double ratio = d / (double) maxDist; - if (ratio <= 0.3d) { - return 1.0d; - } - if (ratio <= 0.6d) { - return 0.8d; - } - if (ratio <= 1.0d) { - return 0.5d; - } - return 0.0d; -} - -function double computeHeightFit(Weapon redWeapon, Weapon blueWeapon, Map cfg) { - Coordinate rc = redWeapon == null ? null : redWeapon.getCoordinate(); - Coordinate bc = blueWeapon == null ? null : blueWeapon.getCoordinate(); - if (rc == null || bc == null) { - return 0.6d; - } - int rh = rc.getHeight() == null ? 0 : rc.getHeight().intValue(); - int bh = bc.getHeight() == null ? 0 : bc.getHeight().intValue(); - int gap = Math.abs(rh - bh); - int maxGap = readIntCfg(cfg, "maxHeightGap", 800); - if (maxGap <= 0) { - maxGap = 800; - } - if (gap >= maxGap) { - return 0.0d; - } - double ratio = (double) gap / (double) maxGap; - return 1.0d - ratio; -} - -function double computeBlueThreatWeight(Weapon blueWeapon) { - if (blueWeapon == null) { - return 0.5d; - } - double t = 0.4d; - if (isAirWeapon(blueWeapon)) { - t += 0.3d; - } - if (isArmorWeapon(blueWeapon)) { - t += 0.2d; - } - if (hasMissileComponent(blueWeapon)) { - t += 0.2d; - } - if (blueWeapon.getNumber() != null && blueWeapon.getNumber().intValue() >= 2) { - t += 0.1d; - } - if (t > 1.0d) { - t = 1.0d; - } - return t; -} - -function String pickNearestBlueId(Weapon redWeapon, List blueWeapons) { - if (redWeapon == null || redWeapon.getCoordinate() == null || !isCoordinateUsable(redWeapon.getCoordinate()) || blueWeapons == null) { - return null; - } - String bestId = null; - double best = Double.MAX_VALUE; - for (Object obj : blueWeapons) { - Weapon b = (Weapon) obj; - if (b == null || isBlank(b.getEquipmentId()) || !isCoordinateUsable(b.getCoordinate())) { - continue; - } - double d = approxDistanceMeters( - redWeapon.getCoordinate().getLongitude().doubleValue(), - redWeapon.getCoordinate().getLatitude().doubleValue(), - b.getCoordinate().getLongitude().doubleValue(), - b.getCoordinate().getLatitude().doubleValue() - ); - if (d < best) { - best = d; - bestId = b.getEquipmentId(); - } - } - return bestId; -} - -function String pickAnyBlueId(List blueWeapons) { - if (blueWeapons == null || blueWeapons.isEmpty()) { - return null; - } - for (Object obj : blueWeapons) { - Weapon b = (Weapon) obj; - if (b != null && !isBlank(b.getEquipmentId())) { - return b.getEquipmentId(); - } - } - return null; -} - -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) { - Map result = new java.util.HashMap(); - if (csv == null) { - return result; - } - String text = csv.trim(); - if (text.equals("")) { - return result; - } - String[] parts = text.split(","); - for (int i = 0; i < parts.length; i++) { - String one = parts[i]; - if (one == null) { - continue; - } - String item = one.trim(); - if (item.equals("")) { - continue; - } - int idx = item.indexOf("->"); - if (idx <= 0 || idx >= item.length() - 2) { - continue; - } - String left = item.substring(0, idx).trim(); - String right = item.substring(idx + 2).trim(); - if (left.equals("") || right.equals("")) { - continue; - } - result.put(left, right); - } - return result; -} - -function void applyAllComponentQuantities(FactTask fact, Map cfg) { - if (fact == null || fact.getBlueTask() == null || fact.getRedTask() == null) { - return; - } - if (!readBooleanCfg(cfg, "enableComponentQuantityMatch", true)) { - return; - } - Task blueTask = fact.getBlueTask(); - Task redTask = fact.getRedTask(); - List blueWeapons = blueTask.getTaskWeapons(); - List redWeapons = redTask.getTaskWeapons(); - if (blueWeapons == null || blueWeapons.isEmpty() || redWeapons == null || redWeapons.isEmpty()) { - return; - } - - String mappingCsv = cfg == null ? null : (String) cfg.get("componentDeviceNameMappingCsv"); - Map deviceNameMapping = parseDeviceNameMapping(mappingCsv); - - String missileKeyword = cfg == null ? null : (String) cfg.get("missileComponentNameContains"); - if (missileKeyword == null || missileKeyword.trim().equals("")) { - missileKeyword = "导弹"; - } else { - missileKeyword = missileKeyword.trim(); - } - - // 遍历红方每个武器:用 targetId 找蓝方装备(equipmentId) - for (Object objR : redWeapons) { - Weapon redWeapon = (Weapon) objR; - if (redWeapon == null) { - continue; - } - String targetId = redWeapon.getTargetId(); - if (isBlank(targetId)) { - // 允许不匹配:targetId 为空直接跳过 - continue; - } - - Weapon blueWeapon = null; - for (Object objB : blueWeapons) { - Weapon w = (Weapon) objB; - if (w == null) { - continue; - } - String eqId = w.getEquipmentId(); - if (eqId != null && eqId.equals(targetId)) { - blueWeapon = w; - break; - } - } - if (blueWeapon == null) { - continue; - } - - List blueComps = blueWeapon.getComponents(); - List redComps = redWeapon.getComponents(); - if (blueComps == null || blueComps.isEmpty() || redComps == null || redComps.isEmpty()) { - continue; - } - - // 用蓝方组件驱动覆盖红方组件数量 - for (Object objBC : blueComps) { - SubComponents blueComp = (SubComponents) objBC; - if (blueComp == null) { - continue; - } - String blueCompName = blueComp.getDeviceName(); - if (isBlank(blueCompName)) { - continue; - } - - // 仅导弹组件参与匹配,非导弹组件保持红方原值 - if (!blueCompName.contains(missileKeyword)) { - continue; - } - - String redCompName = (String) deviceNameMapping.get(blueCompName); - if (redCompName == null || redCompName.trim().equals("")) { - redCompName = blueCompName; - } - - SubComponents redComp = null; - for (Object objRC : redComps) { - SubComponents rc = (SubComponents) objRC; - if (rc == null) { - continue; - } - if (redCompName.equals(rc.getDeviceName())) { - redComp = rc; - break; - } - } - if (redComp == null) { - // 允许不匹配:红方下没有该组件则跳过 - continue; - } - - List blueParams = blueComp.getComponentParams(); - List redParams = redComp.getComponentParams(); - if (blueParams == null || blueParams.isEmpty() || redParams == null || redParams.isEmpty()) { - continue; - } - - ComponentParam blueFirst = (ComponentParam) blueParams.get(0); - ComponentParam redFirst = (ComponentParam) redParams.get(0); - if (blueFirst == null || redFirst == null) { - continue; - } - if (blueFirst.getNumber() == null) { - continue; - } - - // 覆盖导弹组件数量(componentParams[0].number) - redFirst.setNumber(blueFirst.getNumber()); - // 覆盖导弹组件首参数默认值(如范围米/破坏范围米) - if (!isBlank(blueFirst.getAttDefaultValue())) { - redFirst.setAttDefaultValue(blueFirst.getAttDefaultValue()); - } - } - } -} - -function void prepareDeploymentPools(FactTask fact, Map cfg, Map runtime) { - if (!readBooleanCfg(cfg, "enablePositionRules", true)) { - return; - } - if (fact == null || runtime == null) { - return; - } - List combatPolygon = extractZonePolygonFromTask(fact, true); - List defensePolygon = extractZonePolygonFromTask(fact, false); - int spacing = readIntCfg(cfg, "fireUnitSpacingMeters", 100); - List combatPoints = buildGridPointsInPolygon(combatPolygon, spacing, readIntCfg(cfg, "groundDeployHeight", 20)); - List defensePoints = buildGridPointsInPolygon(defensePolygon, spacing, readIntCfg(cfg, "groundDeployHeight", 20)); - runtime.put("deploymentCombatPoints", combatPoints); - runtime.put("deploymentDefensePoints", defensePoints); -} - -function void applyWeaponDeployment(FactTask fact, Map cfg, Map runtime) { - if (!readBooleanCfg(cfg, "enablePositionRules", true)) { - return; - } - if (fact == null || fact.getRedTask() == null || fact.getRedTask().getTaskWeapons() == null || runtime == null) { - return; - } - List combatPoints = (List) runtime.get("deploymentCombatPoints"); - List defensePoints = (List) runtime.get("deploymentDefensePoints"); - if ((combatPoints == null || combatPoints.isEmpty()) && (defensePoints == null || defensePoints.isEmpty())) { - return; - } - Map cursor = new java.util.HashMap(); - List defensePriorityWeapons = parseCsvList((String) cfg.get("defensePriorityWeapons")); - String airPref = String.valueOf(cfg.get("airDeployZonePreference")); - int groundH = readIntCfg(cfg, "groundDeployHeight", 20); - int airH = readIntCfg(cfg, "airDeployHeight", 300); - Task blueTask = fact.getBlueTask(); - Task redTask = fact.getRedTask(); - String deployMode = resolveDeployModeByBlueState(blueTask, cfg); - Coordinate groundAnchor = pickAnchorByMode(deployMode, combatPoints, defensePoints, cursor); - Coordinate airAnchor = pickAnchorByMode("defense".equalsIgnoreCase(airPref) ? "near_slow" : deployMode, combatPoints, defensePoints, cursor); - String formationType = resolveFormationType(redTask, blueTask, cfg, deployMode); - int spacing = resolveFormationSpacing(redTask, blueTask, cfg); - int headingDeg = resolveFormationHeading(redTask, blueTask, cfg); - int mainWingDistance = resolveMainWingDistance(redTask, blueTask, cfg, deployMode); - - List groundWeapons = new ArrayList(); - List airWeapons = new ArrayList(); - for (Object obj : redTask.getTaskWeapons()) { - Weapon redWeapon = (Weapon) obj; - if (redWeapon == null) { - continue; - } - if (isRedAirWeapon(redWeapon)) { - airWeapons.add(redWeapon); - } else { - groundWeapons.add(redWeapon); - } - } - - applyFormationForWeaponGroup(groundWeapons, groundAnchor, formationType, spacing, headingDeg, mainWingDistance, groundH, defensePriorityWeapons, combatPoints, defensePoints, cursor, null, null, null, false); - applyFormationForWeaponGroup(airWeapons, airAnchor, formationType, spacing, headingDeg, mainWingDistance, airH, defensePriorityWeapons, combatPoints, defensePoints, cursor, blueTask, cfg, deployMode, true); -} - -function void applyFormationForWeaponGroup(List weapons, Coordinate anchor, String formationType, int spacing, int headingDeg, int mainWingDistance, int defaultHeight, List defensePriorityWeapons, List combatPoints, List defensePoints, Map cursor, Task blueTask, Map cfg, String deployMode, boolean isAirGroup) { - if (weapons == null || weapons.isEmpty()) { - return; - } - List sortedWeapons = sortWeaponsByFormationRole(weapons); - if (anchor == null) { - for (int i = 0; i < sortedWeapons.size(); i++) { - Weapon w = (Weapon) sortedWeapons.get(i); - if (w == null) { - continue; - } - int h = resolveWeaponDeployHeight(w, blueTask, cfg, deployMode, i, defaultHeight, isAirGroup); - Coordinate fallback = null; - if (defensePriorityWeapons != null && defensePriorityWeapons.contains(w.getName())) { - fallback = pickCoordinateByPreference(defensePoints, combatPoints, cursor, "defense"); - } else { - fallback = pickCoordinateByPreference(combatPoints, defensePoints, cursor, "combat"); - } - if (fallback != null) { - w.setCoordinate(cloneCoordinateWithHeight(fallback, h)); - } - } - return; - } - List offsets = buildFormationOffsets(formationType, sortedWeapons.size(), spacing); - for (int i = 0; i < sortedWeapons.size(); i++) { - Weapon w = (Weapon) sortedWeapons.get(i); - if (w == null) { - continue; - } - int h = resolveWeaponDeployHeight(w, blueTask, cfg, deployMode, i, defaultHeight, isAirGroup); - Map off = i < offsets.size() ? (Map) offsets.get(i) : null; - double dx = off == null || off.get("dx") == null ? 0.0d : ((Double) off.get("dx")).doubleValue(); - double dy = off == null || off.get("dy") == null ? 0.0d : ((Double) off.get("dy")).doubleValue(); - if (w.getFormationRole() != null && "WING".equalsIgnoreCase(w.getFormationRole()) && w.getWingRelativeDistanceMeters() != null && w.getWingRelativeDistanceMeters().intValue() > 0) { - int dist = w.getWingRelativeDistanceMeters().intValue(); - int bearing = w.getWingRelativeBearingDeg() == null ? (i % 2 == 0 ? 45 : -45) : w.getWingRelativeBearingDeg().intValue(); - dx = dist * Math.cos(Math.toRadians(bearing)); - dy = dist * Math.sin(Math.toRadians(bearing)); - } else if (w.getFormationRole() != null && "WING".equalsIgnoreCase(w.getFormationRole())) { - int dist = mainWingDistance; - int bearing = w.getWingRelativeBearingDeg() == null ? (i % 2 == 0 ? 45 : -45) : w.getWingRelativeBearingDeg().intValue(); - dx = dist * Math.cos(Math.toRadians(bearing)); - dy = dist * Math.sin(Math.toRadians(bearing)); - } - Coordinate c = moveCoordinateByMeters(anchor, dx, dy, headingDeg, h); - if (c != null) { - w.setCoordinate(c); - if (w.getFormationRole() == null || w.getFormationRole().trim().equals("")) { - w.setFormationRole(i == 0 ? "MAIN" : "WING"); - } - } - } -} - -function String resolveDeployModeByBlueState(Task blueTask, Map cfg) { - double d = computeDefMinDistanceMeters(blueTask); - int avgSpeed = computeAverageSpeed(blueTask == null ? null : blueTask.getTrackPoints()); - int fast = readIntCfg(cfg, "speedFastThreshold", 180); - int near = readIntCfg(cfg, "distanceNearDefenseThresholdMeters", 800); - int far = readIntCfg(cfg, "distanceFarDefenseThresholdMeters", 2500); - if (avgSpeed >= fast && d <= near) { - return "near_fast"; - } - if (avgSpeed >= fast && d >= far) { - return "far_fast"; - } - if (avgSpeed < fast && d <= near) { - return "near_slow"; - } - return "default"; -} - -function Coordinate pickAnchorByMode(String mode, List combatPoints, List defensePoints, Map cursor) { - if ("near_fast".equals(mode)) { - return pickCoordinateByPreference(defensePoints, combatPoints, cursor, "near_fast"); - } - if ("far_fast".equals(mode)) { - return pickCoordinateByPreference(combatPoints, defensePoints, cursor, "far_fast"); - } - if ("near_slow".equals(mode)) { - return pickCoordinateByPreference(defensePoints, combatPoints, cursor, "near_slow"); - } - return pickCoordinateByPreference(combatPoints, defensePoints, cursor, "default"); -} - -function String resolveFormationType(Task redTask, Task blueTask, Map cfg, String deployMode) { - String fromTask = redTask == null ? null : redTask.getFormationType(); - if (!isBlank(fromTask)) { - return normalizeFormationType(fromTask, cfg); - } - if (readBooleanCfg(cfg, "enableAutoFormationSelect", true)) { - String autoType = autoSelectFormationType(redTask, blueTask, cfg, deployMode); - if (!isBlank(autoType)) { - return normalizeFormationType(autoType, cfg); - } - } - String fromCfg = cfg == null ? null : String.valueOf(cfg.get("formationDefaultType")); - if (isBlank(fromCfg)) { - return "TRIANGLE"; - } - return normalizeFormationType(fromCfg, cfg); -} - -function String autoSelectFormationType(Task redTask, Task blueTask, Map cfg, String deployMode) { - int threat = parseIntSafe(blueTask == null ? null : blueTask.getThreatLevel(), 1); - int highThreat = readIntCfg(cfg, "formationHighThreatLevel", 3); - if (threat >= highThreat) { - return String.valueOf(cfg.get("formationRule_high_threat")); - } - if ("near_fast".equals(deployMode)) { - return String.valueOf(cfg.get("formationRule_near_fast")); - } - if ("far_fast".equals(deployMode)) { - return String.valueOf(cfg.get("formationRule_far_fast")); - } - if ("near_slow".equals(deployMode)) { - return String.valueOf(cfg.get("formationRule_near_slow")); - } - int largeGroupCount = readIntCfg(cfg, "formationLargeGroupCount", 6); - int weaponCount = redTask == null || redTask.getTaskWeapons() == null ? 0 : redTask.getTaskWeapons().size(); - if (weaponCount >= largeGroupCount) { - return String.valueOf(cfg.get("formationRule_large_group")); - } - double airRatio = computeRedAirRatio(redTask); - if (airRatio >= 0.5d) { - return String.valueOf(cfg.get("formationRule_air_majority")); - } - return String.valueOf(cfg.get("formationRule_default")); -} - -function double computeRedAirRatio(Task redTask) { - if (redTask == null || redTask.getTaskWeapons() == null || redTask.getTaskWeapons().isEmpty()) { - return 0.0d; - } - int total = 0; - int air = 0; - for (Object obj : redTask.getTaskWeapons()) { - Weapon w = (Weapon) obj; - if (w == null) { - continue; - } - total++; - if (isRedAirWeapon(w)) { - air++; - } - } - if (total <= 0) { - return 0.0d; - } - return (double) air / (double) total; -} - -function String normalizeFormationType(String input, Map cfg) { - String defaultType = cfg == null ? "TRIANGLE" : String.valueOf(cfg.get("formationDefaultType")); - if (isBlank(defaultType)) { - defaultType = "TRIANGLE"; - } - if (isBlank(input)) { - return defaultType.trim().toUpperCase(); - } - String t = input.trim().toUpperCase(); - if ("TRIANGLE".equals(t) || "DIAMOND".equals(t) || "LINE".equals(t) || "COLUMN".equals(t) || "WEDGE".equals(t)) { - return t; - } - return defaultType.trim().toUpperCase(); -} - -function int resolveFormationSpacing(Task redTask, Task blueTask, Map cfg) { - if (redTask != null && redTask.getFormationSpacingMeters() != null && redTask.getFormationSpacingMeters().intValue() > 0) { - return redTask.getFormationSpacingMeters().intValue(); - } - int fallback = readIntCfg(cfg, "formationDefaultSpacingMeters", 120); - double scale = computeDefenseZoneScaleMeters(blueTask); - if (scale <= 0.0d) { - return fallback; - } - int minSpacing = readIntCfg(cfg, "formationSpacingMinMeters", 60); - int maxSpacing = readIntCfg(cfg, "formationSpacingMaxMeters", 220); - int scaleMin = readIntCfg(cfg, "defenseScaleMinMeters", 300); - int scaleMax = readIntCfg(cfg, "defenseScaleMaxMeters", 3000); - double t = normalizeByRange(scale, (double) scaleMin, (double) scaleMax); - return clampInt((int) Math.round(minSpacing + (maxSpacing - minSpacing) * t), minSpacing, maxSpacing); -} - -function int resolveFormationHeading(Task redTask, Task blueTask, Map cfg) { - if (redTask != null && redTask.getFormationHeadingDeg() != null) { - return redTask.getFormationHeadingDeg().intValue(); - } - Integer headingFromTrack = computeHeadingFromBlueTrack(blueTask); - if (headingFromTrack != null) { - return headingFromTrack.intValue(); - } - return readIntCfg(cfg, "formationHeadingDefaultDeg", 0); -} - -function int resolveMainWingDistance(Task redTask, Task blueTask, Map cfg, String deployMode) { - if (redTask != null && redTask.getMainWingDistanceMeters() != null && redTask.getMainWingDistanceMeters().intValue() > 0) { - return redTask.getMainWingDistanceMeters().intValue(); - } - int fallback = readIntCfg(cfg, "mainWingDistanceDefaultMeters", 100); - double scale = computeDefenseZoneScaleMeters(blueTask); - if (scale <= 0.0d) { - return fallback; - } - int minD = readIntCfg(cfg, "mainWingDistanceMinMeters", 60); - int maxD = readIntCfg(cfg, "mainWingDistanceMaxMeters", 260); - int scaleMin = readIntCfg(cfg, "defenseScaleMinMeters", 300); - int scaleMax = readIntCfg(cfg, "defenseScaleMaxMeters", 3000); - double t = normalizeByRange(scale, (double) scaleMin, (double) scaleMax); - double base = minD + (maxD - minD) * t; - String key = "mainWingDistanceModeFactor_default"; - if ("near_fast".equals(deployMode)) { - key = "mainWingDistanceModeFactor_near_fast"; - } else if ("far_fast".equals(deployMode)) { - key = "mainWingDistanceModeFactor_far_fast"; - } else if ("near_slow".equals(deployMode)) { - key = "mainWingDistanceModeFactor_near_slow"; - } - double factor = readDoubleCfg(cfg, key, 1.0d); - int finalDist = (int) Math.round(base * factor); - return clampInt(finalDist, minD, maxD); -} - -function double computeDefenseZoneScaleMeters(Task blueTask) { - if (blueTask == null || blueTask.getDefZoneLocation() == null || blueTask.getDefZoneLocation().size() < 3) { - return -1.0d; - } - List points = blueTask.getDefZoneLocation(); - double maxD = -1.0d; - for (int i = 0; i < points.size(); i++) { - Coordinate a = (Coordinate) points.get(i); - if (a == null || a.getLongitude() == null || a.getLatitude() == null) { - continue; - } - for (int j = i + 1; j < points.size(); j++) { - Coordinate b = (Coordinate) points.get(j); - if (b == null || b.getLongitude() == null || b.getLatitude() == null) { - continue; - } - double d = approxDistanceMeters( - a.getLongitude().doubleValue(), - a.getLatitude().doubleValue(), - b.getLongitude().doubleValue(), - b.getLatitude().doubleValue() - ); - if (d > maxD) { - maxD = d; - } - } - } - return maxD; -} - -function double normalizeByRange(double value, double minV, double maxV) { - if (maxV <= minV) { - return 0.0d; - } - if (value <= minV) { - return 0.0d; - } - if (value >= maxV) { - return 1.0d; - } - return (value - minV) / (maxV - minV); -} - -function int clampInt(int value, int minV, int maxV) { - if (value < minV) { - return minV; - } - if (value > maxV) { - return maxV; - } - return value; -} - -/** 入参武器坐标高度>0 视为已指定部署高度 */ -function int readWeaponHeightIfValid(Weapon w) { - if (w == null || w.getCoordinate() == null || w.getCoordinate().getHeight() == null) { - return -1; - } - int h = w.getCoordinate().getHeight().intValue(); - return h > 0 ? h : -1; -} - -function int resolveBlueBaseHeight(Task blueTask, Map cfg) { - int fallback = readIntCfg(cfg, "airHeightFallback", readIntCfg(cfg, "airDeployHeight", 300)); - if (blueTask != null && blueTask.getTrackPoints() != null && !blueTask.getTrackPoints().isEmpty()) { - TrackPoints tail = (TrackPoints) blueTask.getTrackPoints().get(blueTask.getTrackPoints().size() - 1); - if (tail != null && tail.getHeight() != null && tail.getHeight().intValue() > 0) { - return tail.getHeight().intValue(); - } - } - if (blueTask != null && blueTask.getTaskWeapons() != null) { - int sum = 0; - int cnt = 0; - for (Object o : blueTask.getTaskWeapons()) { - Weapon bw = (Weapon) o; - if (bw == null || bw.getCoordinate() == null || bw.getCoordinate().getHeight() == null) { - continue; - } - int hh = bw.getCoordinate().getHeight().intValue(); - if (hh > 0) { - sum += hh; - cnt++; - } - } - if (cnt > 0) { - return sum / cnt; - } - } - return fallback; -} - -function int resolveRoleHeightAdjust(Weapon w, int indexInFormation, Map cfg) { - int mainAdj = readIntCfg(cfg, "airHeightAdjustMain", 20); - int wingAdj = readIntCfg(cfg, "airHeightAdjustWing", -10); - if (w != null && w.getFormationRole() != null && !w.getFormationRole().trim().equals("")) { - if ("MAIN".equalsIgnoreCase(w.getFormationRole())) { - return mainAdj; - } - if ("WING".equalsIgnoreCase(w.getFormationRole())) { - return wingAdj; - } - } - return indexInFormation == 0 ? mainAdj : wingAdj; -} - -function int computeAutoAirHeight(Weapon w, Task blueTask, Map cfg, int indexInFormation) { - int minH = readIntCfg(cfg, "airHeightMin", 50); - int maxH = readIntCfg(cfg, "airHeightMax", 20000); - int base = resolveBlueBaseHeight(blueTask, cfg); - int speedTh = readIntCfg(cfg, "airHeightSpeedThreshold", 180); - int fastAdj = readIntCfg(cfg, "airHeightAdjustFast", 40); - int avgSpeed = computeAverageSpeed(blueTask == null ? null : blueTask.getTrackPoints()); - int speedAdj = (avgSpeed >= speedTh) ? fastAdj : 0; - double d = computeDefMinDistanceMeters(blueTask); - int nearD = readIntCfg(cfg, "airHeightNearDefenseDistance", 800); - int farD = readIntCfg(cfg, "airHeightFarDefenseDistance", 2500); - int nearAdj = readIntCfg(cfg, "airHeightAdjustNear", -30); - int farAdj = readIntCfg(cfg, "airHeightAdjustFar", 40); - int distAdj = 0; - if (d < Double.MAX_VALUE / 4.0d) { - if (d <= (double) nearD) { - distAdj = nearAdj; - } else if (d >= (double) farD) { - distAdj = farAdj; - } - } - int roleAdj = resolveRoleHeightAdjust(w, indexInFormation, cfg); - int sum = base + speedAdj + distAdj + roleAdj; - return clampInt(sum, minH, maxH); -} - -/** - * 空中武器部署高度:1) 入参已给高度优先 2) 开启自动则混合计算 3) 否则固定 airDeployHeight - * 地面武器:始终使用 defaultHeight(isAirGroup=false) - */ -function int resolveWeaponDeployHeight(Weapon w, Task blueTask, Map cfg, String deployMode, int indexInFormation, int defaultHeight, boolean isAirGroup) { - if (!isAirGroup) { - return defaultHeight; - } - int given = readWeaponHeightIfValid(w); - if (given > 0) { - return given; - } - if (cfg == null || !readBooleanCfg(cfg, "enableAutoAirDeployHeight", true)) { - return defaultHeight; - } - return computeAutoAirHeight(w, blueTask, cfg, indexInFormation); -} - -function Integer computeHeadingFromBlueTrack(Task blueTask) { - if (blueTask == null || blueTask.getTrackPoints() == null || blueTask.getTrackPoints().size() < 2) { - return null; - } - List points = blueTask.getTrackPoints(); - TrackPoints p2 = (TrackPoints) points.get(points.size() - 1); - TrackPoints p1 = (TrackPoints) points.get(points.size() - 2); - if (p1 == null || p2 == null || p1.getLongitude() == null || p1.getLatitude() == null || p2.getLongitude() == null || p2.getLatitude() == null) { - return null; - } - double dLon = p2.getLongitude().doubleValue() - p1.getLongitude().doubleValue(); - double dLat = p2.getLatitude().doubleValue() - p1.getLatitude().doubleValue(); - if (Math.abs(dLon) < 1e-12 && Math.abs(dLat) < 1e-12) { - return null; - } - double deg = Math.toDegrees(Math.atan2(dLon, dLat)); - int heading = (int) Math.round(deg); - if (heading < 0) { - heading += 360; - } - if (heading >= 360) { - heading = heading % 360; - } - return Integer.valueOf(heading); -} - -function List sortWeaponsByFormationRole(List weapons) { - List mainList = new ArrayList(); - List wingList = new ArrayList(); - List unknownList = new ArrayList(); - for (Object obj : weapons) { - Weapon w = (Weapon) obj; - if (w == null) { - continue; - } - String role = w.getFormationRole(); - if (role != null && "MAIN".equalsIgnoreCase(role)) { - mainList.add(w); - } else if (role != null && "WING".equalsIgnoreCase(role)) { - wingList.add(w); - } else { - unknownList.add(w); - } - } - List result = new ArrayList(); - if (!mainList.isEmpty()) { - result.addAll(mainList); - result.addAll(wingList); - result.addAll(unknownList); - return result; - } - if (!unknownList.isEmpty()) { - result.add(unknownList.get(0)); - for (int i = 1; i < unknownList.size(); i++) { - result.add(unknownList.get(i)); - } - result.addAll(wingList); - return result; - } - result.addAll(wingList); - return result; -} - -function List buildFormationOffsets(String formationType, int size, int spacing) { - List result = new ArrayList(); - if (size <= 0) { - return result; - } - String type = formationType == null ? "TRIANGLE" : formationType.trim().toUpperCase(); - result.add(buildOffset(0.0d, 0.0d)); - for (int i = 1; i < size; i++) { - if ("DIAMOND".equals(type)) { - if (i == 1) { - result.add(buildOffset(-spacing, -spacing)); - } else if (i == 2) { - result.add(buildOffset(spacing, -spacing)); - } else if (i == 3) { - result.add(buildOffset(0.0d, -2.0d * spacing)); - } else { - int row = (i - 4) / 2 + 1; - int sign = (i % 2 == 0) ? 1 : -1; - result.add(buildOffset(sign * spacing * (row + 1), -spacing * (row + 2))); - } - } else if ("LINE".equals(type)) { - int sign = (i % 2 == 0) ? 1 : -1; - int k = (i + 1) / 2; - result.add(buildOffset(sign * k * spacing, 0.0d)); - } else if ("COLUMN".equals(type)) { - result.add(buildOffset(0.0d, -i * spacing)); - } else if ("WEDGE".equals(type)) { - int layer = (i + 1) / 2; - int sign = (i % 2 == 0) ? 1 : -1; - result.add(buildOffset(sign * layer * spacing, -layer * spacing)); - } else { // TRIANGLE/default - int layer = (i + 1) / 2; - int sign = (i % 2 == 0) ? 1 : -1; - result.add(buildOffset(sign * layer * spacing, -layer * spacing)); - } - } - return result; -} - -function Map buildOffset(double dx, double dy) { - Map m = new java.util.HashMap(); - m.put("dx", Double.valueOf(dx)); - m.put("dy", Double.valueOf(dy)); - return m; -} - -function Coordinate moveCoordinateByMeters(Coordinate anchor, double dxMeters, double dyMeters, int headingDeg, int height) { - if (anchor == null || anchor.getLongitude() == null || anchor.getLatitude() == null) { - return null; - } - double rad = Math.toRadians((double) headingDeg); - double rx = (dxMeters * Math.cos(rad)) - (dyMeters * Math.sin(rad)); - double ry = (dxMeters * Math.sin(rad)) + (dyMeters * Math.cos(rad)); - double lat = anchor.getLatitude().doubleValue() + metersToLatDeg(ry); - double lon = anchor.getLongitude().doubleValue() + metersToLonDeg(rx, anchor.getLatitude().doubleValue()); - Coordinate c = new Coordinate(); - c.setLongitude(new java.math.BigDecimal(String.valueOf(lon))); - c.setLatitude(new java.math.BigDecimal(String.valueOf(lat))); - c.setHeight(height); - return c; -} - -function void applyTrajectoryGeneration(FactTask fact, Map cfg) { - if (!readBooleanCfg(cfg, "enableTrajectoryRules", true)) { - return; - } - if (fact == null || fact.getBlueTask() == null || fact.getRedTask() == null) { - return; - } - Task blueTask = fact.getBlueTask(); - Task redTask = fact.getRedTask(); - List blueTrack = blueTask.getTrackPoints(); - if (blueTrack == null || blueTrack.isEmpty()) { - return; - } - List defZone = blueTask.getDefZoneLocation(); - if (defZone == null || defZone.size() < 3) { - return; - } - String strategy = chooseTrajectoryStrategy(blueTask, cfg); - Coordinate endPoint = findNearestDefPointToBlueTail(blueTask); - List redTrack = generateRedTrackPoints(blueTrack, strategy, cfg, endPoint); - if (redTrack != null && !redTrack.isEmpty()) { - redTask.setTrackPoints(redTrack); - } -} - -function String chooseTrajectoryStrategy(Task blueTask, Map cfg) { - String mode = String.valueOf(cfg.get("strategyMode")); - if (mode == null || mode.trim().equals("")) { - mode = "auto"; - } - mode = mode.trim().toLowerCase(); - if (!mode.equals("auto")) { - return fallbackToEnabledStrategy(mode, cfg); - } - double defMinDistance = computeDefMinDistanceMeters(blueTask); - int avgSpeed = computeAverageSpeed(blueTask.getTrackPoints()); - int near = readIntCfg(cfg, "nearDefDistanceMeters", 800); - int far = readIntCfg(cfg, "farDefDistanceMeters", 2500); - int fast = readIntCfg(cfg, "fastSpeedThreshold", 180); - String selected = "flank"; - if (avgSpeed >= fast && defMinDistance <= near) { - selected = "shortest"; - } else if (avgSpeed >= fast && defMinDistance >= far) { - selected = "interfere"; - } - return fallbackToEnabledStrategy(selected, cfg); -} - -function String fallbackToEnabledStrategy(String preferred, Map cfg) { - if (isStrategyEnabled(preferred, cfg)) { - return preferred; - } - if (isStrategyEnabled("shortest", cfg)) { - return "shortest"; - } - if (isStrategyEnabled("flank", cfg)) { - return "flank"; - } - if (isStrategyEnabled("interfere", cfg)) { - return "interfere"; - } - return "shortest"; -} - -function boolean isStrategyEnabled(String strategy, Map cfg) { - if (strategy == null) { - return false; - } - if (strategy.equals("shortest")) { - return readBooleanCfg(cfg, "enableShortest", true); - } - if (strategy.equals("flank")) { - return readBooleanCfg(cfg, "enableFlank", true); - } - if (strategy.equals("interfere")) { - return readBooleanCfg(cfg, "enableInterfere", true); - } - return false; -} - -function Coordinate findNearestDefPointToBlueTail(Task blueTask) { - if (blueTask == null || blueTask.getTrackPoints() == null || blueTask.getTrackPoints().isEmpty() || blueTask.getDefZoneLocation() == null || blueTask.getDefZoneLocation().isEmpty()) { - return null; - } - TrackPoints tail = (TrackPoints) blueTask.getTrackPoints().get(blueTask.getTrackPoints().size() - 1); - if (tail == null || tail.getLongitude() == null || tail.getLatitude() == null) { - return null; - } - Coordinate nearest = null; - double best = Double.MAX_VALUE; - for (Object obj : blueTask.getDefZoneLocation()) { - Coordinate c = (Coordinate) obj; - if (c == null || c.getLongitude() == null || c.getLatitude() == null) { - continue; - } - double d = approxDistanceMeters(tail.getLongitude().doubleValue(), tail.getLatitude().doubleValue(), c.getLongitude().doubleValue(), c.getLatitude().doubleValue()); - if (d < best) { - best = d; - nearest = c; - } - } - return nearest; -} - -function double computeDefMinDistanceMeters(Task blueTask) { - Coordinate nearest = findNearestDefPointToBlueTail(blueTask); - if (nearest == null || blueTask == null || blueTask.getTrackPoints() == null || blueTask.getTrackPoints().isEmpty()) { - return Double.MAX_VALUE; - } - TrackPoints tail = (TrackPoints) blueTask.getTrackPoints().get(blueTask.getTrackPoints().size() - 1); - return approxDistanceMeters( - tail.getLongitude().doubleValue(), - tail.getLatitude().doubleValue(), - nearest.getLongitude().doubleValue(), - nearest.getLatitude().doubleValue() - ); -} - -function int computeAverageSpeed(List trackPoints) { - if (trackPoints == null || trackPoints.isEmpty()) { - return 0; - } - int total = 0; - int count = 0; - for (Object obj : trackPoints) { - TrackPoints p = (TrackPoints) obj; - if (p == null || p.getSpeed() == null) { - continue; - } - total += p.getSpeed(); - count++; - } - if (count <= 0) { - return 0; - } - return total / count; -} - -function List generateRedTrackPoints(List blueTrackPoints, String strategy, Map cfg, Coordinate endPoint) { - List result = new ArrayList(); - if (blueTrackPoints == null || blueTrackPoints.isEmpty()) { - return result; - } - TrackPoints start = (TrackPoints) blueTrackPoints.get(0); - TrackPoints tail = (TrackPoints) blueTrackPoints.get(blueTrackPoints.size() - 1); - if (start == null || start.getLongitude() == null || start.getLatitude() == null) { - return result; - } - double sLon = start.getLongitude().doubleValue(); - double sLat = start.getLatitude().doubleValue(); - double eLon = (endPoint != null && endPoint.getLongitude() != null) ? endPoint.getLongitude().doubleValue() : (tail == null || tail.getLongitude() == null ? sLon : tail.getLongitude().doubleValue()); - double eLat = (endPoint != null && endPoint.getLatitude() != null) ? endPoint.getLatitude().doubleValue() : (tail == null || tail.getLatitude() == null ? sLat : tail.getLatitude().doubleValue()); - double dx = eLon - sLon; - double dy = eLat - sLat; - int n = blueTrackPoints.size(); - int flankOffset = readIntCfg(cfg, "flankOffsetMeters", 150); - int intBase = readIntCfg(cfg, "interfereOffsetMeters", 120); - int intAmp = readIntCfg(cfg, "interfereZigzagAmplitude", 90); - boolean keepBlueHeight = readBooleanCfg(cfg, "keepBlueHeight", true); - int redH = readIntCfg(cfg, "redTrackHeightOverride", 200); - - for (int i = 0; i < n; i++) { - double t = (n <= 1) ? 1.0d : ((double) i / (double) (n - 1)); - double baseLon = sLon + dx * t; - double baseLat = sLat + dy * t; - double offMeters = 0.0d; - if ("flank".equals(strategy)) { - offMeters = flankOffset * Math.sin(Math.PI * t); - } else if ("interfere".equals(strategy)) { - double zig = (i % 2 == 0 ? 1.0d : -1.0d) * intAmp; - offMeters = intBase * Math.sin(2.0d * Math.PI * t) + zig; - } - double latDeg = metersToLatDeg(offMeters); - double lonDeg = metersToLonDeg(offMeters, baseLat); - double norm = Math.sqrt(dx * dx + dy * dy); - if (norm < 1e-10) { - norm = 1e-10; - } - double nx = -dy / norm; - double ny = dx / norm; - double finalLon = baseLon + nx * lonDeg; - double finalLat = baseLat + ny * latDeg; - - TrackPoints blueP = (TrackPoints) blueTrackPoints.get(i); - TrackPoints redP = new TrackPoints(); - redP.setIndex(i); - redP.setLongitude(new java.math.BigDecimal(String.valueOf(finalLon))); - redP.setLatitude(new java.math.BigDecimal(String.valueOf(finalLat))); - redP.setSpeed(blueP == null || blueP.getSpeed() == null ? 0 : blueP.getSpeed()); - if (keepBlueHeight) { - redP.setHeight(blueP == null || blueP.getHeight() == null ? redH : blueP.getHeight()); - } else { - redP.setHeight(redH); - } - result.add(redP); - } - return result; -} - -function double metersToLatDeg(double meters) { - return meters / 111000.0d; -} - -function double metersToLonDeg(double meters, double latitudeDeg) { - double cos = Math.cos(Math.toRadians(latitudeDeg)); - if (Math.abs(cos) < 1e-6) { - cos = 1e-6; - } - return meters / (111000.0d * cos); -} - -function double approxDistanceMeters(double lon1, double lat1, double lon2, double lat2) { - double dx = (lon2 - lon1) * 111000.0d * Math.cos(Math.toRadians((lat1 + lat2) / 2.0d)); - double dy = (lat2 - lat1) * 111000.0d; - return Math.sqrt(dx * dx + dy * dy); -} - -function List extractZonePolygonFromTask(FactTask fact, boolean isCombat) { - // 输入约定:Task.warZoneLocation / defZoneLocation 传入 4 个经纬点(高度可空) - List result = new ArrayList(); - Task blueTask = fact == null ? null : fact.getBlueTask(); - if (blueTask == null) { - return result; - } - List source = isCombat ? blueTask.getWarZoneLocation() : blueTask.getDefZoneLocation(); - if (source == null || source.isEmpty()) { - return result; - } - for (Object oneObj : source) { - Coordinate one = (Coordinate) oneObj; - if (one == null || one.getLongitude() == null || one.getLatitude() == null) { - continue; - } - Coordinate c = new Coordinate(); - c.setLongitude(one.getLongitude()); - c.setLatitude(one.getLatitude()); - c.setHeight(one.getHeight()); - result.add(c); - } - return result; -} - -function List normalizeToCoordinateList(Object raw) { - List result = new ArrayList(); - if (raw == null) { - return result; - } - if (!(raw instanceof List)) { - return result; - } - List values = (List) raw; - for (Object obj : values) { - Coordinate c = toCoordinate(obj); - if (c != null) { - result.add(c); - } - } - return result; -} - -function Coordinate toCoordinate(Object obj) { - if (obj == null) { - return null; - } - if (obj instanceof Coordinate) { - return (Coordinate) obj; - } - if (obj instanceof Map) { - Map m = (Map) obj; - Object lon = m.get("longitude"); - Object lat = m.get("latitude"); - Object h = m.get("height"); - if (lon == null || lat == null) { - return null; - } - Coordinate c = new Coordinate(); - try { - c.setLongitude(new java.math.BigDecimal(String.valueOf(lon))); - c.setLatitude(new java.math.BigDecimal(String.valueOf(lat))); - c.setHeight(h == null ? 0 : parseIntSafe(String.valueOf(h), 0)); - return c; - } catch (Exception ex) { - return null; - } - } - return null; -} - -function List buildGridPointsInPolygon(List polygon, int spacingMeters, int defaultHeight) { - List points = new ArrayList(); - if (polygon == null || polygon.size() < 3) { - return points; - } - double step = ((double) spacingMeters) / 111000.0d; - if (step <= 0) { - step = 0.0009d; - } - double minLon = 180.0d; - double maxLon = -180.0d; - double minLat = 90.0d; - double maxLat = -90.0d; - for (Object cObj : polygon) { - Coordinate c = (Coordinate) cObj; - if (c == null || c.getLongitude() == null || c.getLatitude() == null) { - continue; - } - double lon = c.getLongitude().doubleValue(); - double lat = c.getLatitude().doubleValue(); - if (lon < minLon) minLon = lon; - if (lon > maxLon) maxLon = lon; - if (lat < minLat) minLat = lat; - if (lat > maxLat) maxLat = lat; - } - for (double lon = minLon; lon <= maxLon; lon += step) { - for (double lat = minLat; lat <= maxLat; lat += step) { - if (isPointInsidePolygon(lon, lat, polygon)) { - Coordinate c = new Coordinate(); - c.setLongitude(new java.math.BigDecimal(String.valueOf(lon))); - c.setLatitude(new java.math.BigDecimal(String.valueOf(lat))); - c.setHeight(defaultHeight); - points.add(c); - } - } - } - return points; -} - -function boolean isPointInsidePolygon(double x, double y, List polygon) { - if (polygon == null || polygon.size() < 3) { - return false; - } - boolean inside = false; - int n = polygon.size(); - int j = n - 1; - for (int i = 0; i < n; i++) { - Coordinate pi = (Coordinate) polygon.get(i); - Coordinate pj = (Coordinate) polygon.get(j); - if (pi == null || pj == null || pi.getLongitude() == null || pi.getLatitude() == null || pj.getLongitude() == null || pj.getLatitude() == null) { - j = i; - continue; - } - double xi = pi.getLongitude().doubleValue(); - double yi = pi.getLatitude().doubleValue(); - double xj = pj.getLongitude().doubleValue(); - double yj = pj.getLatitude().doubleValue(); - boolean intersect = ((yi > y) != (yj > y)) - && (x < (xj - xi) * (y - yi) / ((yj - yi) == 0 ? 1e-12 : (yj - yi)) + xi); - if (intersect) { - inside = !inside; - } - j = i; - } - return inside; -} - -function Coordinate pickCoordinateByPreference(List first, List second, Map cursor, String key) { - Coordinate c1 = pickCoordinateRoundRobin(first, cursor, "first_" + key); - if (c1 != null) { - return c1; - } - return pickCoordinateRoundRobin(second, cursor, "second_" + key); -} - -function Coordinate pickCoordinateRoundRobin(List values, Map cursor, String key) { - if (values == null || values.isEmpty()) { - return null; - } - Integer idxObj = (Integer) cursor.get(key); - int idx = idxObj == null ? 0 : idxObj.intValue(); - Coordinate value = (Coordinate) values.get(idx % values.size()); - cursor.put(key, idx + 1); - return value; -} - -function Coordinate cloneCoordinateWithHeight(Coordinate source, int height) { - if (source == null) { - return null; - } - Coordinate c = new Coordinate(); - c.setLongitude(source.getLongitude()); - c.setLatitude(source.getLatitude()); - c.setHeight(height); - return c; -} - -function Map extractBlueTargetPools(List blueWeapons) { - Map pools = new java.util.HashMap(); - pools.put("air", new ArrayList()); - pools.put("armor", new ArrayList()); - pools.put("artillery", new ArrayList()); - pools.put("ground", new ArrayList()); - pools.put("missile", new ArrayList()); - pools.put("all", new ArrayList()); - - for (Object obj : blueWeapons) { - Weapon blueWeapon = (Weapon) obj; - if (blueWeapon == null) { - continue; - } - String id = blueWeapon.getEquipmentId(); - if (isBlank(id)) { - continue; - } - addUnique((List) pools.get("all"), id); - if (isAirWeapon(blueWeapon)) { - addUnique((List) pools.get("air"), id); - } - if (isArmorWeapon(blueWeapon)) { - addUnique((List) pools.get("armor"), id); - } - if (isArtilleryWeapon(blueWeapon)) { - addUnique((List) pools.get("artillery"), id); - } - if (isGroundWeapon(blueWeapon)) { - addUnique((List) pools.get("ground"), id); - } - if (hasMissileComponent(blueWeapon)) { - addUnique((List) pools.get("missile"), id); - } - } - return pools; -} - -function String inferBluePoolKeyForRedWeapon(Weapon redWeapon) { - if (redWeapon == null || redWeapon.getName() == null) { - return "ground"; - } - String name = redWeapon.getName(); - if (name.contains("反坦克")) { - return "armor"; - } - if (name.contains("防空导弹") || name.contains("无人机") || name.contains("直升机")) { - return "air"; - } - if (name.contains("迫榴炮") || name.contains("迫击炮")) { - return "artillery"; - } - if (name.contains("导弹发射车")) { - return "missile"; - } - return "ground"; -} - -function String pickTargetIdFromPools(Map pools, Map cursor, String preferredKey) { - String fromPreferred = pickFromSinglePool(pools, cursor, preferredKey); - if (!isBlank(fromPreferred)) { - return fromPreferred; - } - if (!"ground".equals(preferredKey)) { - String fromGround = pickFromSinglePool(pools, cursor, "ground"); - if (!isBlank(fromGround)) { - return fromGround; - } - } - return pickFromSinglePool(pools, cursor, "all"); -} - -function String pickFromSinglePool(Map pools, Map cursor, String poolKey) { - if (pools == null || cursor == null || poolKey == null) { - return null; - } - List ids = (List) pools.get(poolKey); - if (ids == null || ids.isEmpty()) { - return null; - } - Integer idxObj = (Integer) cursor.get(poolKey); - int idx = idxObj == null ? 0 : idxObj.intValue(); - String id = (String) ids.get(idx % ids.size()); - cursor.put(poolKey, idx + 1); - return id; -} - -function void addUnique(List values, String value) { - if (values == null || isBlank(value)) { - return; - } - if (!containsString(values, value)) { - values.add(value); - } -} - -function boolean isBlank(String text) { - return text == null || text.trim().equals(""); -} - -function void assignTaskNameByRedWeapons(FactTask fact, Map cfg) { - if (fact == null || fact.getRedTask() == null) { - return; - } - Task redTask = fact.getRedTask(); - List redWeapons = redTask.getTaskWeapons(); - String category = classifyTaskByRedWeapons(redWeapons); - - // 一致性校验:分类与武器不一致则回落通用打击 - if (!isTaskCategoryConsistent(category, redWeapons)) { - category = "general"; - } - - redTask.setDrawName(resolveTaskNameByCategory(cfg, category)); - redTask.setDataType(resolveTaskDataTypeByCategory(cfg, category)); -} - -function String classifyTaskByRedWeapons(List redWeapons) { - if (redWeapons == null || redWeapons.isEmpty()) { - return "general"; - } - // 符合实际的优先级:导弹突击 > 防空压制 > 反装甲 > 炮火压制 > 通用 - if (hasRedWeaponName(redWeapons, "导弹发射车")) { - return "missile_strike"; - } - if (hasAnyRedWeaponName(redWeapons, "防空导弹武器,火力打击无人机,武装直升机")) { - return "air_defence"; - } - if (hasAnyRedWeaponName(redWeapons, "反坦克火箭,反坦克导弹系统")) { - return "anti_armor"; - } - if (hasAnyRedWeaponName(redWeapons, "迫榴炮,车载迫击炮")) { - return "artillery"; - } - return "general"; -} - -function boolean isTaskCategoryConsistent(String category, List redWeapons) { - if (category == null) { - return false; - } - if (category.equals("missile_strike")) { - return hasRedWeaponName(redWeapons, "导弹发射车"); - } - if (category.equals("air_defence")) { - return hasAnyRedWeaponName(redWeapons, "防空导弹武器,火力打击无人机,武装直升机"); - } - if (category.equals("anti_armor")) { - return hasAnyRedWeaponName(redWeapons, "反坦克火箭,反坦克导弹系统"); - } - if (category.equals("artillery")) { - return hasAnyRedWeaponName(redWeapons, "迫榴炮,车载迫击炮"); - } - return true; -} - -function String resolveTaskNameByCategory(Map cfg, String category) { - if (cfg == null || category == null) { - return "通用打击任务"; - } - if (category.equals("missile_strike")) { - return String.valueOf(cfg.get("taskName_missile_strike")); - } - if (category.equals("air_defence")) { - return String.valueOf(cfg.get("taskName_air_defence")); - } - if (category.equals("anti_armor")) { - return String.valueOf(cfg.get("taskName_anti_armor")); - } - if (category.equals("artillery")) { - return String.valueOf(cfg.get("taskName_artillery")); - } - return String.valueOf(cfg.get("taskName_general")); -} - -function String resolveTaskDataTypeByCategory(Map cfg, String category) { - if (cfg == null || category == null) { - return "strike"; - } - if (category.equals("missile_strike")) { - return String.valueOf(cfg.get("taskDataType_missile_strike")); - } - if (category.equals("air_defence")) { - return String.valueOf(cfg.get("taskDataType_air_defence")); - } - if (category.equals("anti_armor")) { - return String.valueOf(cfg.get("taskDataType_anti_armor")); - } - if (category.equals("artillery")) { - return String.valueOf(cfg.get("taskDataType_artillery")); - } - return String.valueOf(cfg.get("taskDataType_general")); -} - -function boolean hasAnyRedWeaponName(List redWeapons, String commaNames) { - if (redWeapons == null || redWeapons.isEmpty() || commaNames == null || commaNames.equals("")) { - return false; - } - String[] names = commaNames.split(","); - for (int i = 0; i < names.length; i++) { - String one = names[i]; - if (one == null) { - continue; - } - if (hasRedWeaponName(redWeapons, one.trim())) { - return true; - } - } - return false; -} - -function boolean hasRedWeaponName(List redWeapons, String weaponName) { - if (redWeapons == null || redWeapons.isEmpty() || weaponName == null || weaponName.equals("")) { - return false; - } - for (Object obj : redWeapons) { - Weapon w = (Weapon) obj; - if (w != null && w.getName() != null && w.getName().equals(weaponName)) { - return true; - } - } - return false; -} - -// 蓝方组件模板:仅在组件缺失时补齐,作为规则联动测试用 -function void buildBlueTestComponents(List weapons) { - if (weapons == null || weapons.isEmpty()) { - return; - } - for (Object obj : weapons) { - Weapon weapon = (Weapon) obj; - if (weapon == null) { - continue; - } - List components = weapon.getComponents(); - if (components == null) { - components = new ArrayList<>(); - weapon.setComponents(components); - } - if (!components.isEmpty()) { - continue; - } - - // 蓝方主要用于触发规则,模板尽量简洁 - if (isAirWeapon(weapon)) { - components.add(buildComponent("火控雷达", "220", "探测范围米", 1)); - components.add(buildComponent("空空导弹", "220", "破坏范围米", 1)); - } else if (isArtilleryWeapon(weapon)) { - components.add(buildComponent("炮弹", "1200", "范围米", 6)); - } else if (isGroundWeapon(weapon)) { - components.add(buildComponent("机枪", "600", "射程米", 1)); - } - } -} - -// 红方基础组件模板:便于业务人员看懂武器都带了哪些能力 -function void ensureBasicRedComponents(Weapon weapon) { - if (weapon == null) { - return; - } - String name = weapon.getName(); - if (name == null) { - name = ""; - } - if (name.contains("防空导弹")) { - ensureComponent(weapon, "搜索雷达", "260", "探测范围米", 1); - ensureComponent(weapon, "防空导弹", "300", "破坏范围米", 1); - } else if (name.contains("无人机")) { - ensureComponent(weapon, "光电吊舱", "180", "识别范围米", 1); - ensureComponent(weapon, "空地导弹", "260", "破坏范围米", 1); - } else if (name.contains("武装直升机")) { - ensureComponent(weapon, "火控雷达", "220", "探测范围米", 1); - ensureComponent(weapon, "机载导弹", "280", "破坏范围米", 2); - } else if (name.contains("反坦克火箭")) { - ensureComponent(weapon, "火箭弹", "200", "破坏范围米", 4); - } else if (name.contains("反坦克导弹系统")) { - ensureComponent(weapon, "反坦克导弹", "320", "破坏范围米", 2); - ensureComponent(weapon, "激光测距", "180", "测距米", 1); - } else if (name.contains("迫榴炮") || name.contains("迫击炮")) { - ensureComponent(weapon, "炮弹", "1500", "范围米", 8); - } else if (name.contains("导弹发射车")) { - ensureComponent(weapon, "导弹发射架", "260", "破坏范围米", 1); - ensureComponent(weapon, "制导雷达", "240", "探测范围米", 1); - } else { - // 兜底组件,避免出现完全无组件的武器 - ensureComponent(weapon, "火控系统", "100", "作用范围米", 1); - } -} - -// 炮类限制:武器组件只能保留“炮弹”,并固定参数单位“范围米” -function void limitRedArtilleryToShellOnly(List redWeapons, String shellRangeDefault) { - if (redWeapons == null || redWeapons.isEmpty()) { - return; - } - for (Object obj : redWeapons) { - Weapon redWeapon = (Weapon) obj; - if (redWeapon == null || !isArtilleryWeapon(redWeapon)) { - continue; - } - List onlyShell = new ArrayList<>(); - onlyShell.add(buildComponent("炮弹", shellRangeDefault, "范围米", 8)); - redWeapon.setComponents(onlyShell); - } -} - -function Weapon ensureRedWeapon(List redWeapons, String name, String supportType, int number) { - for (Object obj : redWeapons) { - Weapon w = (Weapon) obj; - if (w != null && w.getName() != null && w.getName().equals(name)) { - if (w.getSupportType() == null || w.getSupportType().equals("")) { - w.setSupportType(supportType); - } - if (w.getNumber() == null || w.getNumber() <= 0) { - w.setNumber(number); - } - if (w.getComponents() == null) { - w.setComponents(new ArrayList<>()); - } - return w; - } - } - Weapon w = new Weapon(); - w.setName(name); - w.setSupportType(supportType); - w.setNumber(number); - w.setComponents(new ArrayList<>()); - redWeapons.add(w); - return w; -} - -function void ensureMissileComponentForRedAirWeapon(Weapon redWeapon, int missileNumber, int missileRange) { - List components = redWeapon.getComponents(); - if (components == null) { - components = new ArrayList<>(); - redWeapon.setComponents(components); - } - for (SubComponents c : components) { - if (c != null && c.getDeviceName() != null && c.getDeviceName().contains("导弹")) { - ensureOrUpdateParam(c, String.valueOf(missileRange), "破坏范围米", missileNumber); - return; - } - } - components.add(buildComponent("联动导弹", String.valueOf(missileRange), "破坏范围米", missileNumber)); -} - -function void ensureComponent(Weapon weapon, String deviceName, String value, String unit, int number) { - List components = weapon.getComponents(); - if (components == null) { - components = new ArrayList<>(); - weapon.setComponents(components); - } - for (SubComponents c : components) { - if (c != null && c.getDeviceName() != null && c.getDeviceName().equals(deviceName)) { - ensureOrUpdateParam(c, value, unit, number); - return; - } - } - components.add(buildComponent(deviceName, value, unit, number)); -} - -function SubComponents buildComponent(String deviceName, String value, String unit, int number) { - SubComponents component = new SubComponents(); - component.setDeviceName(deviceName); - List params = new ArrayList<>(); - ComponentParam param = new ComponentParam(); - param.setAttDefaultValue(value); - param.setAttExplain(unit); - param.setNumber(number); - params.add(param); - component.setComponentParams(params); - return component; -} - -function void ensureOrUpdateParam(SubComponents component, String value, String unit, int number) { - List params = component.getComponentParams(); - if (params == null) { - params = new ArrayList<>(); - component.setComponentParams(params); - } - if (params.isEmpty()) { - ComponentParam param = new ComponentParam(); - param.setAttDefaultValue(value); - param.setAttExplain(unit); - param.setNumber(number); - params.add(param); - return; - } - ComponentParam first = params.get(0); - first.setAttDefaultValue(value); - first.setAttExplain(unit); - first.setNumber(number); -} - -function int countBlueMissileNumber(List weapons) { - int total = 0; - for (Object obj : weapons) { - Weapon w = (Weapon) obj; - if (w == null || w.getComponents() == null) { - continue; - } - for (SubComponents c : w.getComponents()) { - if (c == null || c.getDeviceName() == null || !c.getDeviceName().contains("导弹")) { - continue; - } - int n = 1; - if (c.getComponentParams() != null && !c.getComponentParams().isEmpty() && c.getComponentParams().get(0) != null && c.getComponentParams().get(0).getNumber() != null) { - n = c.getComponentParams().get(0).getNumber(); - } - total = total + n; - } - } - return total; -} - -function int readBlueMissileRange(List weapons, int fallback) { - int best = 0; - for (Object obj : weapons) { - Weapon w = (Weapon) obj; - if (w == null || w.getComponents() == null) { - continue; - } - for (SubComponents c : w.getComponents()) { - if (c == null || c.getDeviceName() == null || !c.getDeviceName().contains("导弹")) { - continue; - } - if (c.getComponentParams() == null || c.getComponentParams().isEmpty() || c.getComponentParams().get(0) == null) { - continue; - } - String value = c.getComponentParams().get(0).getAttDefaultValue(); - int parsed = parseIntSafe(value, fallback); - if (parsed > best) { - best = parsed; - } - } - } - if (best <= 0) { - return fallback; - } - return best; -} - -function int parseIntSafe(String text, int fallback) { - if (text == null || text.equals("")) { - return fallback; - } - try { - return Integer.parseInt(text.trim()); - } catch (Exception ex) { - return 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; - } - Object value = cfg.get(key); - if (value == null) { - return fallback; - } - if (value instanceof Integer) { - return ((Integer) value).intValue(); - } - return parseIntSafe(String.valueOf(value), fallback); -} - -function boolean readBooleanCfg(Map cfg, String key, boolean fallback) { - if (cfg == null || key == null) { - return fallback; - } - Object value = cfg.get(key); - if (value == null) { - return fallback; - } - if (value instanceof Boolean) { - return ((Boolean) value).booleanValue(); - } - String text = String.valueOf(value); - if (text == null) { - return fallback; - } - return "true".equalsIgnoreCase(text.trim()); -} - -function double readDoubleCfg(Map cfg, String key, double fallback) { - if (cfg == null || key == null) { - return fallback; - } - Object value = cfg.get(key); - if (value == null) { - return fallback; - } - try { - return Double.parseDouble(String.valueOf(value).trim()); - } catch (Exception ex) { - return fallback; - } -} - -function boolean isRedAirWeapon(Weapon weapon) { - if (weapon == null) { - return false; - } - String supportType = weapon.getSupportType(); - String name = weapon.getName(); - return (supportType != null && (supportType.equals("overhead") || supportType.equals("plane"))) - || (name != null && (name.contains("无人机") || name.contains("直升机"))); -} - -function boolean isAirWeapon(Weapon weapon) { - if (weapon == null) { - return false; - } - String supportType = weapon.getSupportType(); - String name = weapon.getName(); - return (supportType != null && (supportType.equals("overhead") || supportType.equals("plane"))) - || (name != null && ( - name.contains("直升机") - || name.contains("地空导弹") - || name.contains("单兵防空导弹") - || name.contains("制导导弹") - || name.contains("无人机") - )); -} - -function boolean isGroundWeapon(Weapon weapon) { - if (weapon == null) { - return false; - } - String supportType = weapon.getSupportType(); - String name = weapon.getName(); - return (supportType != null && supportType.equals("ground")) - || (name != null && ( - name.contains("坦克") - || name.contains("装甲车") - || name.contains("迫击炮") - || name.contains("迫榴炮") - || name.contains("车载迫击炮") - || name.contains("导弹发射车") - || name.contains("反坦克") - )); -} - -function boolean isArtilleryWeapon(Weapon weapon) { - if (weapon == null || weapon.getName() == null) { - return false; - } - String name = weapon.getName(); - return name.contains("迫榴炮") - || name.contains("迫击炮") - || name.contains("车载迫击炮") - || name.contains("120mm"); -} - -function boolean isArmorWeapon(Weapon weapon) { - if (weapon == null || weapon.getName() == null) { - return false; - } - String name = weapon.getName(); - return name.contains("主战坦克") - || name.contains("坦克") - || name.contains("装甲车"); -} - -function boolean hasMissileComponent(Weapon weapon) { - if (weapon == null || weapon.getComponents() == null) { - return false; - } - for (SubComponents c : weapon.getComponents()) { - if (c != null && c.getDeviceName() != null && c.getDeviceName().contains("导弹")) { - return true; - } - } - return false; -} - -// ========== legacy 函数区(保留仅供回滚,不参与当前业务规则) ========== -function void matchLauncherComponents( - FactTask blueFact, - FactTask redFact, - String launcherName, - String redPlaneSupportType, - String redMissileVehicleKeyword, - String redMissileVehicleEnKeyword, - int redMoreThanBlueOffset, - int triggerBlueLauncherCount -) { - Task blueTask = blueFact.getBlueTask(); - Task redTask = redFact.getRedTask(); - if (blueTask == null || redTask == null) { - return; - } - - List blueWeapons = blueTask.getTaskWeapons(); - List redWeapons = redTask.getTaskWeapons(); - if (blueWeapons == null || redWeapons == null || redWeapons.isEmpty()) { - return; - } - - int blueLauncherCount = countLauncherComponents(blueWeapons, launcherName); - if (blueLauncherCount <= 0) { - return; - } - - // 规则1:红方若存在 plane 或导弹发射车,则这些武器都需要具备发射架 - List candidateRedWeapons = new ArrayList<>(); - for (Weapon redWeapon : redWeapons) { - if (isRedWeaponNeedLauncher(redWeapon, redPlaneSupportType, redMissileVehicleKeyword, redMissileVehicleEnKeyword)) { - candidateRedWeapons.add(redWeapon); - ensureWeaponHasLauncher(redWeapon, launcherName); - } - } - if (candidateRedWeapons.isEmpty()) { - return; - } - - // 规则2:当蓝方发射架数量达到触发值时,红方发射架数量 = 蓝方 + 可配置偏移量 - if (blueLauncherCount == triggerBlueLauncherCount) { - int targetRedLauncherCount = blueLauncherCount + redMoreThanBlueOffset; - int currentRedLauncherCount = countLauncherComponents(redWeapons, launcherName); - int needAdd = targetRedLauncherCount - currentRedLauncherCount; - if (needAdd > 0) { - Weapon fallbackWeapon = candidateRedWeapons.get(0); - for (int i = 0; i < needAdd; i++) { - addLauncherToWeapon(fallbackWeapon, launcherName); - } - } - } -} - -function boolean isRedWeaponNeedLauncher( - Weapon weapon, - String redPlaneSupportType, - String redMissileVehicleKeyword, - String redMissileVehicleEnKeyword -) { - if (weapon == null) { - return false; - } - String supportType = weapon.getSupportType(); - String weaponName = weapon.getName(); - return (supportType != null && supportType.equals(redPlaneSupportType)) - || (weaponName != null && (weaponName.contains(redMissileVehicleKeyword) || weaponName.contains(redMissileVehicleEnKeyword))); -} - -function void ensureWeaponHasLauncher(Weapon weapon, String launcherName) { - if (weapon == null) { - return; - } - List components = weapon.getComponents(); - if (components == null) { - components = new ArrayList<>(); - weapon.setComponents(components); - } - for (SubComponents component : components) { - if (component != null && component.getDeviceName() != null && component.getDeviceName().contains(launcherName)) { - return; - } - } - addLauncherToWeapon(weapon, launcherName); -} - -function void addLauncherToWeapon(Weapon weapon, String launcherName) { - List components = weapon.getComponents(); - if (components == null) { - components = new ArrayList<>(); - weapon.setComponents(components); - } - SubComponents launcher = new SubComponents(); - launcher.setDeviceName(launcherName); - components.add(launcher); -} - -function int countLauncherComponents(List weapons, String launcherName) { - if (weapons == null || weapons.isEmpty()) { - return 0; - } - int count = 0; - for (Object weaponObj : weapons) { - Weapon weapon = (Weapon) weaponObj; - if (weapon == null || weapon.getComponents() == null) { - continue; - } - for (SubComponents component : weapon.getComponents()) { - if (component != null && component.getDeviceName() != null && component.getDeviceName().contains(launcherName)) { - count++; - } - } - } - return count; -} - -//威胁等级添加武器函数 -function void threatLevels(FactTask redFact, Map params) { - // 创建武器列表 - List weapons = new ArrayList<>(); - - // 创建导弹发射车 - Weapon weapon1 = new Weapon(); - weapon1.setNumber((Integer) params.get("platNum")); - weapon1.setSupportType("ground"); - weapon1.setEquipmentId("1"); - weapon1.setName("missile-launching-vehicle"); - weapon1.setComponents(new ArrayList<>()); - - // 创建防空导弹武器 - Weapon weapon2 = new Weapon(); - weapon2.setNumber((Integer) params.get("platNum")); - weapon2.setSupportType("antiaircraft"); - weapon2.setEquipmentId("2"); - weapon2.setName("Anti-aircraft-missile-weapon"); - weapon2.setComponents(new ArrayList<>()); - - // 添加到列表 - weapons.add(weapon1); - weapons.add(weapon2); - - // 设置到红方任务 - redFact.getRedTask().setTaskWeapons(weapons); -} diff --git a/auto-solution-rule/src/main/resources/rules/rule.drl b/auto-solution-rule/src/main/resources/rules/rule.drl index 6e911f5..17619c5 100644 --- a/auto-solution-rule/src/main/resources/rules/rule.drl +++ b/auto-solution-rule/src/main/resources/rules/rule.drl @@ -3,35 +3,86 @@ package rules; import com.solution.rule.domain.ultimately.fact.DroolsFact; import java.util.Map; -import com.solution.rule.utils.RuleFunction.equipmentRule; +import static com.solution.rule.utils.RuleFunction.equipmentRule; global java.util.Map globalParams; +/** + * 构建装备匹配所需的全部可调参数(会 merge 进 globalParams,覆盖 Java 侧同名默认值)。 + * + * ========== 总体运算逻辑(与 RuleFunction.equipmentRule 一致)========== + * 1)先拼「蓝方文本串」blueBlob:任务 drawName、dataType、taskWeapons 下各武器的 name/supportType/equipmentId、组件 deviceName。 + * 2)对每个红方装备拼「红方文本串」redBlob:name、platform_type、SupportType。 + * 3)每件红装得分 score = 规则槽得分(scoreRuleSlots) + 兼容层得分(scoreLegacyLayer),均为整数。 + * - 规则槽:对 i=1..ruleSlotCount,若 blueBlob 命中 blueRuleKeywords_i 且 redBlob 命中 redRuleKeywords_i, + * 则加上 ruleScore_i * weight。 + * - 兼容层:多组「蓝关键词 + 红关键词 + 对应分数」,见下方各键说明;每组条件同时满足则加上 对应Score * weight。 + * 4)在池中取 score 最大者;若多人并列,由 tieBreak 决定(见 tieBreak)。 + * 5)若 maxScore < minSelectedScore,视为未匹配:不往 fireRuleInputs 追加行,redWeapons 输出仍为当前池。 + * 6)若匹配成功:从池中 remove 该件;fireRuleInputs 追加一行,drawName 后接 outputDrawNameSuffix;taskWeapons 填选中红装映射。 + * + * 关键词格式:英文逗号分隔,子串包含即算命中(contains),不要求整词匹配。 + */ function Map buildParam(){ Map param = new java.util.HashMap(); - //权重因子 - param.put("weight", 1); - //最低入选分数 - param.put("minSelectedScore",1); - //蓝方坦克类 -> 红方反坦克加分 - param.put("tankScore", 1); - //蓝方空中类 -> 红方反空中加分 - param.put("airScore", 2); - //蓝方地面类 -> 红方远程打击加分 - param.put("groundScore", 1); - //蓝方有导弹 -> 红方防空加分 - param.put("missileScore", 1); - //蓝方是空中任务 -> 红方防空加分 - param.put("airTaskScore", 10); + // ---------- 全局倍率与门槛 ---------- + // weight:对上述所有「基础分数」的统一乘数(规则槽的 ruleScore_i、兼容层的 *Score 都会乘 weight)。 + param.put("weight", 1); + // minSelectedScore:单件红装总分达到该值及以上才会被选中并写入 fireRuleInputs;否则本任务视为未匹配到装备。 + param.put("minSelectedScore", 1); + // tieBreak:并列最高分时的决胜方式。当前实现仅支持 "equipmentId":装备 ID 字典序更小的优先。 + param.put("tieBreak", "equipmentId"); + // outputDrawNameSuffix:匹配成功写入 fireRuleInputs 时,在蓝方原 drawName 后面拼接的后缀(如「打击任务」)。 + param.put("outputDrawNameSuffix", "打击任务"); + + // ---------- 规则槽(可配置条数,便于只改本文件而不改 Java)---------- + // ruleSlotCount:启用几条槽规则;第 i 条使用 blueRuleKeywords_i、redRuleKeywords_i、ruleScore_i。 + param.put("ruleSlotCount", 3); + // 槽1:蓝方文本中出现任一子串 且 红方文本中出现任一子串 → 加 ruleScore_1 * weight。 + param.put("blueRuleKeywords_1", "F-16,F-35"); + param.put("redRuleKeywords_1", "防空,导弹,无人机"); + param.put("ruleScore_1", 5); + param.put("blueRuleKeywords_2", "坦克,装甲"); + param.put("redRuleKeywords_2", "反坦克"); + param.put("ruleScore_2", 4); + param.put("blueRuleKeywords_3", "地面,突击"); + param.put("redRuleKeywords_3", "远火,榴弹,炮"); + param.put("ruleScore_3", 2); + + // ---------- 兼容层:按「场景关键词」配对加分(与 RuleFunction.scoreLegacyLayer 一一对应)---------- + // 以下每组均为:蓝方文本命中第一列关键词 且 红方文本命中第二列关键词 → 加 第三列分数 * weight。 + // + // ① 空中平台类:蓝方像「对空/机型」且红方像对空装备 → + airScore * weight + param.put("bluePlatformKeywords_air", "F-16,J-10,F-35"); + param.put("redPreferredWhenBlueAir", "防空,导弹,无人机,直升机,空空"); + param.put("airScore", 2); + // ② 任务文案像空中任务 且 红方偏好对空 → + airTaskScore * weight(可与①叠加) + param.put("airTaskKeywords", "空中,制空,拦截,空战"); + param.put("airTaskScore", 10); + // ③ 任务文案像地面任务 且 红方偏好地面火力 → + groundScore * weight + param.put("groundTaskKeywords", "地面,突击,登陆"); + param.put("redPreferredWhenGround", "远火,榴弹,炮,火箭"); + param.put("groundScore", 1); + // ④ 蓝方像坦克/装甲目标 且 红方文本命中 redMatchKeywords_tank → + tankScore * weight + param.put("tankKeywords", "坦克,装甲"); + param.put("redMatchKeywords_tank", "反坦克"); + param.put("tankScore", 1); + // ⑤ 蓝方像导弹类 且 红方文本命中 redMatchKeywords_missile → + missileScore * weight + param.put("missileKeywords", "导弹,火箭弹,巡航"); + param.put("redMatchKeywords_missile", "防空,导弹,导弹发射"); + param.put("missileScore", 1); + + return param; } rule "装备匹配" salience 100 when - $fact : DroolsFact(task.side != "") + $fact : DroolsFact(task != null) then - //如何引入Java静态方法? + // 以本文件 buildParam 为真源覆盖同名键,再执行 Java 侧匹配逻辑 + globalParams.putAll(buildParam()); equipmentRule($fact, globalParams); end