diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleInputRedWeaponElementDTO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleInputRedWeaponElementDTO.java index 462f7ed..f7c7f51 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleInputRedWeaponElementDTO.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleInputRedWeaponElementDTO.java @@ -30,6 +30,8 @@ public class FireRuleInputRedWeaponElementDTO { private Boolean isInterferenceTarget; private Boolean isDefendImportantPlace; + //命中率 + private Double successTargetRad; private String groupType; diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleTaskInputDTO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleTaskInputDTO.java index 965faa5..eeb8764 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleTaskInputDTO.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/dto/FireRuleTaskInputDTO.java @@ -32,6 +32,11 @@ public class FireRuleTaskInputDTO { */ private String side; + /** + * 蓝方任务装备命中率 + */ + private Double successTargetRad; + /** * 航迹所属实体或阵营标识 */ diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java index 47c4548..77269ac 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/ultimately/vo/FireRuleRedWeaponEquipmentVO.java @@ -29,6 +29,8 @@ public class FireRuleRedWeaponEquipmentVO { private Boolean isInterferenceTarget; private Boolean isDefendImportantPlace; + //命中率 + private Double successTargetRad; private String groupType; 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 index a065aa4..7c8776e 100644 --- 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 @@ -3,19 +3,32 @@ 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.dto.FireRuleCoordinateDTO; +import com.solution.rule.domain.ultimately.dto.FireRuleTrackPointDTO; +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.dto.FireRuleComponentParamDTO; import com.solution.rule.domain.ultimately.vo.FireRuleLauncherConfigurationVO; +import com.solution.rule.domain.ultimately.vo.FireRuleExecuteBlockVO; +import com.solution.rule.domain.ultimately.vo.FireRuleExecuteTargetItemVO; 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.FireRuleRedPlatformSlotVO; 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.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * 将入参中的红方装备 {@code SubComponents}(DTO)转为输出 VO,保持与原始 JSON 结构一致。 @@ -95,6 +108,325 @@ public final class FireRuleRedWeaponOutputFillHelper { return list; } + /** + * 阵位填充:写入每个 redWeapon 的 SubComponents.platform[].positions = [lon,lat,height]。 + * 混合模式:蓝方武器坐标中心为锚点 + 航迹方位偏移 + 编队偏移 + 作战区约束。 + */ + @SuppressWarnings("rawtypes") + public static void fillPlatformPositions(List redWeapons, FireRuleTaskInputDTO blueTask, Map params) { + if (redWeapons == null || redWeapons.isEmpty() || blueTask == null) { + return; + } + Coord anchor = computeBlueAnchor(blueTask); + if (anchor == null) { + return; + } + double bearingDeg = computeTrackBearingDeg(blueTask, str(params, "trackPointDirectionMode", "head2next"), + readDouble(params, "fallbackBearingDeg", 0d)); + + double minKm = readDouble(params, "deployDistanceKmMin", 8d); + double maxKm = readDouble(params, "deployDistanceKmMax", 30d); + double defKm = readDouble(params, "deployDistanceKmDefault", 15d); + String formation = str(params, "formationType", "line"); + double spacingM = readDouble(params, "formationSpacingMeters", 300d); + double headingOffset = readDouble(params, "formationHeadingOffsetDeg", 15d); + double minInterM = readDouble(params, "minInterPlatformDistanceMeters", 80d); + if (spacingM < minInterM) { + spacingM = minInterM; + } + double defaultHeight = readDouble(params, "defaultDeployHeight", 30d); + double followRatio = readDouble(params, "heightFollowBlueRatio", 0d); + boolean clamp = Boolean.parseBoolean(String.valueOf(params.getOrDefault("enableWarZoneClamp", true))); + List warZone = toWarZonePolygon(blueTask.getWarZoneLocation()); + Map distanceByPlatform = parseDistanceByPlatformCsv(str(params, "distanceByPlatformCsv", "")); + + for (FireRuleRedWeaponEquipmentVO red : redWeapons) { + if (red == null || red.getSubComponents() == null || red.getSubComponents().getPlatform() == null) { + continue; + } + double distKm = resolveDeployDistanceKm(red, distanceByPlatform, defKm); + distKm = clampDouble(distKm, minKm, maxKm); + + List platforms = red.getSubComponents().getPlatform(); + for (int i = 0; i < platforms.size(); i++) { + FireRuleRedPlatformSlotVO slot = platforms.get(i); + if (slot == null) { + continue; + } + double extraBearing = formationBearingOffsetDeg(formation, i, headingOffset); + double extraMeters = formationOffsetMeters(formation, i, spacingM); + Coord c = moveByMeters(anchor, distKm * 1000d + extraMeters, bearingDeg + extraBearing); + + if (clamp && warZone.size() >= 3 && !isPointInPolygon(c, warZone)) { + c = projectInsidePolygon(c, anchor, warZone); + } + + double h = defaultHeight; + if (anchor.height != null) { + h = defaultHeight + anchor.height * followRatio; + } + slot.setPositions(asPosition(c.lon, c.lat, h)); + } + } + } + + /** + * 目标分配:为每个红方 Tasks 节点写入 task.execute[0].targetList[*].targetId。\n + * targetId 来源:当前蓝方任务 {@link FireRuleTaskInputDTO#getTaskWeapons()} 下每条武器的 equipmentId。\n + * 允许多个红方装备指向同一个蓝方目标(不做去重占用)。\n + * 参数来自 rule.drl buildParam:\n + * - executeTypeDefault\n + * - targetPickMode: roundRobin/random\n + * - minTargetsPerRed / maxTargetsPerRedCap\n + * - radToTargetsCsv\n + * - rangeParseRegex / rangeUnit / minRangeToAllowAssignKm\n + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static void assignTargetsToTasks(List tasks, + List redWeapons, + FireRuleTaskInputDTO blueTask, + Map params) { + if (tasks == null || tasks.isEmpty() || blueTask == null) { + return; + } + List candidates = extractBlueTargetEquipmentIds(blueTask, params); + if (candidates.isEmpty()) { + return; + } + + String executeType = str(params, "executeTypeDefault", "assault"); + String pickMode = str(params, "targetPickMode", "roundRobin"); + + for (int i = 0; i < tasks.size(); i++) { + FireRuleSceneTaskNodeVO node = tasks.get(i); + if (node == null) { + continue; + } + if (node.getTask() == null) { + node.setTask(new FireRuleSceneTaskPayloadVO()); + } + FireRuleSceneTaskPayloadVO payload = node.getTask(); + Double redHitRate = resolveRedWeaponHitRate(payload.getWeaponId(), redWeapons); + int targetsPerRed = resolveTargetsPerRed(redHitRate, params); + + FireRuleExecuteBlockVO block = new FireRuleExecuteBlockVO(); + block.setType(executeType); + + List targetList = new ArrayList<>(); + for (int k = 0; k < targetsPerRed; k++) { + String blueEquipmentId = pickBlueTargetId(candidates, pickMode, i, k, node, blueTask); + if (isBlank(blueEquipmentId)) { + continue; + } + FireRuleExecuteTargetItemVO item = new FireRuleExecuteTargetItemVO(); + item.setId(UUID.randomUUID().toString()); + item.setTargetId(blueEquipmentId); + // weaponId:可填红方装备的 equipmentId(若无则留空) + item.setWeaponId(payload.getWeaponId()); + targetList.add(item); + } + block.setTargetList(targetList.isEmpty() ? null : targetList); + + List execute = new ArrayList<>(); + execute.add(block); + payload.setExecute(execute); + } + } + + private static Double resolveRedWeaponHitRate(String weaponId, List redWeapons) { + if (isBlank(weaponId) || redWeapons == null || redWeapons.isEmpty()) { + return null; + } + for (FireRuleRedWeaponEquipmentVO w : redWeapons) { + if (w != null && weaponId.equals(w.getEquipmentId())) { + return w.getSuccessTargetRad(); + } + } + return null; + } + + private static String pickBlueTargetId(List candidates, String pickMode, int taskIndex, int innerIndex, + FireRuleSceneTaskNodeVO node, FireRuleTaskInputDTO blueTask) { + if (candidates == null || candidates.isEmpty()) { + return null; + } + if ("random".equalsIgnoreCase(pickMode)) { + // 伪随机但稳定:基于 redTask.id + blueTask.id + innerIndex 做 hash + String seed = nz(node != null ? node.getId() : "") + "|" + nz(blueTask.getId()) + "|" + innerIndex; + int h = Math.abs(seed.hashCode()); + return candidates.get(h % candidates.size()); + } + // 默认 roundRobin:按 Tasks 序号与目标序号轮询 + int idx = Math.abs(taskIndex + innerIndex) % candidates.size(); + return candidates.get(idx); + } + + private static int resolveTargetsPerRed(Double successTargetRad, Map params) { + int min = readInt(params, "minTargetsPerRed", 1); + int max = readInt(params, "maxTargetsPerRedCap", 3); + if (max <= 0) { + max = 1; + } + if (min < 0) { + min = 0; + } + if (min > max) { + min = max; + } + + int byRad = parseRadToTargets(successTargetRad, str(params, "radToTargetsCsv", "")); + int v = byRad > 0 ? byRad : min; + if (v < min) { + v = min; + } + if (v > max) { + v = max; + } + return v; + } + + private static int parseRadToTargets(Double rad, String csv) { + if (rad == null || csv == null || csv.trim().isEmpty()) { + return 0; + } + double r = rad.doubleValue(); + // 允许无序输入:找所有 threshold<=r 的最大 threshold 对应的 targets + double bestTh = -1; + int bestTargets = 0; + for (String part : csv.split(",")) { + String p = part.trim(); + if (p.isEmpty() || !p.contains(":")) { + continue; + } + String[] kv = p.split(":"); + if (kv.length != 2) { + continue; + } + try { + double th = Double.parseDouble(kv[0].trim()); + int t = Integer.parseInt(kv[1].trim()); + if (r >= th && th > bestTh) { + bestTh = th; + bestTargets = t; + } + } catch (Exception ignore) { + } + } + return bestTargets; + } + + private static List extractBlueTargetEquipmentIds(FireRuleTaskInputDTO blueTask, Map params) { + List all = new ArrayList<>(); + if (blueTask == null || blueTask.getTaskWeapons() == null) { + return all; + } + + // 射程过滤参数 + String regex = str(params, "rangeParseRegex", "(\\d+(?:\\.\\d+)?)"); + String unit = str(params, "rangeUnit", "km"); + double minKm = readDouble(params, "minRangeToAllowAssignKm", 0d); + Pattern pattern = safePattern(regex); + + for (FireRuleTaskWeaponDTO w : blueTask.getTaskWeapons()) { + if (w == null || isBlank(w.getEquipmentId())) { + continue; + } + Double km = tryParseRangeKm(w, pattern, unit); + if (km != null && km.doubleValue() < minKm) { + continue; + } + all.add(w.getEquipmentId()); + } + return all; + } + + private static Double tryParseRangeKm(FireRuleTaskWeaponDTO w, Pattern pattern, String unit) { + if (w == null || w.getComponents() == null || w.getComponents().isEmpty() || pattern == null) { + return null; + } + for (FireRuleWeaponComponentDTO c : w.getComponents()) { + if (c == null || c.getComponentParams() == null) { + continue; + } + for (FireRuleComponentParamDTO p : c.getComponentParams()) { + if (p == null) { + continue; + } + Double km = parseNumberToKm(p.getAttDefaultValue(), pattern, unit); + if (km != null) { + return km; + } + km = parseNumberToKm(p.getAttExplain(), pattern, unit); + if (km != null) { + return km; + } + } + } + return null; + } + + private static Double parseNumberToKm(String text, Pattern pattern, String unit) { + if (text == null || text.trim().isEmpty() || pattern == null) { + return null; + } + Matcher m = pattern.matcher(text); + if (!m.find()) { + return null; + } + try { + double v = Double.parseDouble(m.group(1)); + if ("m".equalsIgnoreCase(unit)) { + return v / 1000.0d; + } + return v; + } catch (Exception e) { + return null; + } + } + + private static Pattern safePattern(String regex) { + try { + return Pattern.compile(regex); + } catch (Exception e) { + return null; + } + } + + private static double readDouble(Map params, String key, double def) { + Object v = params != null ? params.get(key) : null; + if (v == null) { + return def; + } + if (v instanceof Number) { + return ((Number) v).doubleValue(); + } + try { + return Double.parseDouble(String.valueOf(v).trim()); + } catch (Exception e) { + return def; + } + } + + private static int readInt(Map params, String key, int def) { + Object v = params != null ? params.get(key) : null; + if (v == null) { + return def; + } + if (v instanceof Number) { + return ((Number) v).intValue(); + } + try { + return Integer.parseInt(String.valueOf(v).trim()); + } catch (Exception e) { + return def; + } + } + + private static String str(Map params, String key, String def) { + Object v = params != null ? params.get(key) : null; + return v == null ? def : String.valueOf(v); + } + private static List buildMissionList(FireRuleRedWeaponEquipmentVO redWeapon) { if (redWeapon == null || redWeapon.getSubComponents() == null || redWeapon.getSubComponents().getWeapon() == null) { return null; @@ -149,4 +481,210 @@ public final class FireRuleRedWeaponOutputFillHelper { private static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } + + // ------------------ 阵位几何辅助 ------------------ + private static class Coord { + final double lon; + final double lat; + final Double height; + Coord(double lon, double lat, Double height) { + this.lon = lon; + this.lat = lat; + this.height = height; + } + } + + private static Coord computeBlueAnchor(FireRuleTaskInputDTO blueTask) { + if (blueTask == null || blueTask.getTaskWeapons() == null || blueTask.getTaskWeapons().isEmpty()) { + return null; + } + double sumLon = 0d, sumLat = 0d, sumH = 0d; + int count = 0, hc = 0; + for (FireRuleTaskWeaponDTO w : blueTask.getTaskWeapons()) { + if (w == null || w.getCoordinate() == null || w.getCoordinate().getLongitude() == null || w.getCoordinate().getLatitude() == null) { + continue; + } + sumLon += w.getCoordinate().getLongitude(); + sumLat += w.getCoordinate().getLatitude(); + count++; + if (w.getCoordinate().getHeight() != null) { + sumH += w.getCoordinate().getHeight(); + hc++; + } + } + if (count == 0) { + return null; + } + return new Coord(sumLon / count, sumLat / count, hc > 0 ? sumH / hc : null); + } + + private static double computeTrackBearingDeg(FireRuleTaskInputDTO blueTask, String mode, double fallbackDeg) { + if (blueTask == null || blueTask.getTrackPoints() == null || blueTask.getTrackPoints().size() < 2) { + return fallbackDeg; + } + FireRuleTrackPointDTO a; + FireRuleTrackPointDTO b; + if ("tail2prev".equalsIgnoreCase(mode)) { + int n = blueTask.getTrackPoints().size(); + a = blueTask.getTrackPoints().get(n - 2); + b = blueTask.getTrackPoints().get(n - 1); + } else { + a = blueTask.getTrackPoints().get(0); + b = blueTask.getTrackPoints().get(1); + } + if (a == null || b == null || a.getLongitude() == null || a.getLatitude() == null || b.getLongitude() == null || b.getLatitude() == null) { + return fallbackDeg; + } + return bearingDeg(a.getLongitude(), a.getLatitude(), b.getLongitude(), b.getLatitude()); + } + + private static List toWarZonePolygon(List warZone) { + List list = new ArrayList<>(); + if (warZone == null) { + return list; + } + for (FireRuleCoordinateDTO c : warZone) { + if (c != null && c.getLongitude() != null && c.getLatitude() != null) { + list.add(new Coord(c.getLongitude(), c.getLatitude(), c.getHeight())); + } + } + return list; + } + + private static Map parseDistanceByPlatformCsv(String csv) { + Map map = new HashMap<>(); + if (isBlank(csv)) { + return map; + } + for (String part : csv.split(",")) { + String p = part.trim(); + if (!p.contains(":")) { + continue; + } + String[] kv = p.split(":"); + if (kv.length != 2) { + continue; + } + try { + map.put(kv[0].trim(), Double.parseDouble(kv[1].trim())); + } catch (Exception ignore) { + } + } + return map; + } + + private static double resolveDeployDistanceKm(FireRuleRedWeaponEquipmentVO red, Map byPlatform, double defKm) { + String name = red != null ? nz(red.getName()) : ""; + String platform = red != null ? nz(red.getPlatformType()) : ""; + for (Map.Entry e : byPlatform.entrySet()) { + String k = e.getKey(); + if (!isBlank(k) && (name.contains(k) || platform.contains(k))) { + return e.getValue(); + } + } + return defKm; + } + + private static double formationBearingOffsetDeg(String formation, int index, double headingOffset) { + if ("line".equalsIgnoreCase(formation)) { + return 90d; + } + if ("wedge".equalsIgnoreCase(formation)) { + return (index % 2 == 0 ? 1 : -1) * headingOffset; + } + if ("circle".equalsIgnoreCase(formation)) { + return index * 45d; + } + return 0d; + } + + private static double formationOffsetMeters(String formation, int index, double spacingM) { + if ("line".equalsIgnoreCase(formation)) { + return (index - 0.5d) * spacingM; + } + if ("wedge".equalsIgnoreCase(formation)) { + return (index + 1) * spacingM * 0.5d; + } + if ("circle".equalsIgnoreCase(formation)) { + return spacingM; + } + return index * spacingM; + } + + private static Coord moveByMeters(Coord anchor, double meters, double bearingDeg) { + double R = 6378137.0d; + double brng = Math.toRadians(bearingDeg); + double lat1 = Math.toRadians(anchor.lat); + double lon1 = Math.toRadians(anchor.lon); + double dR = meters / R; + double lat2 = Math.asin(Math.sin(lat1) * Math.cos(dR) + Math.cos(lat1) * Math.sin(dR) * Math.cos(brng)); + double lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(dR) * Math.cos(lat1), + Math.cos(dR) - Math.sin(lat1) * Math.sin(lat2)); + return new Coord(Math.toDegrees(lon2), Math.toDegrees(lat2), anchor.height); + } + + private static double bearingDeg(double lon1, double lat1, double lon2, double lat2) { + double phi1 = Math.toRadians(lat1); + double phi2 = Math.toRadians(lat2); + double dLon = Math.toRadians(lon2 - lon1); + double y = Math.sin(dLon) * Math.cos(phi2); + double x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(dLon); + double brng = Math.toDegrees(Math.atan2(y, x)); + return (brng + 360d) % 360d; + } + + private static boolean isPointInPolygon(Coord p, List poly) { + boolean inside = false; + for (int i = 0, j = poly.size() - 1; i < poly.size(); j = i++) { + double xi = poly.get(i).lon, yi = poly.get(i).lat; + double xj = poly.get(j).lon, yj = poly.get(j).lat; + boolean intersect = ((yi > p.lat) != (yj > p.lat)) + && (p.lon < (xj - xi) * (p.lat - yi) / (yj - yi + 1e-12) + xi); + if (intersect) { + inside = !inside; + } + } + return inside; + } + + private static Coord projectInsidePolygon(Coord out, Coord anchor, List poly) { + if (isPointInPolygon(out, poly)) { + return out; + } + // 二分逼近:沿 anchor -> out 连线回退到多边形内部 + Coord in = anchor; + if (!isPointInPolygon(in, poly)) { + // anchor 不在区内时,退化为原点不处理 + return out; + } + Coord lo = in; + Coord hi = out; + for (int i = 0; i < 24; i++) { + Coord mid = new Coord((lo.lon + hi.lon) / 2d, (lo.lat + hi.lat) / 2d, out.height); + if (isPointInPolygon(mid, poly)) { + lo = mid; + } else { + hi = mid; + } + } + return lo; + } + + private static List asPosition(double lon, double lat, double height) { + List p = new ArrayList<>(3); + p.add(lon); + p.add(lat); + p.add(height); + return p; + } + + private static double clampDouble(double v, double min, double max) { + if (v < min) { + return min; + } + if (v > max) { + return max; + } + return v; + } } 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 9c03fe7..90cb5c8 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 @@ -11,8 +11,10 @@ import com.solution.rule.domain.ultimately.vo.FireRuleTaskInputVO; import com.solution.rule.domain.ultimately.vo.FireRuleTaskWeaponVO; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Drools 规则调用的装备匹配逻辑。所有业务词均来自 globalParams(由 rule.drl 的 buildParam 注入),本类不写死中文业务词。 @@ -103,13 +105,126 @@ public final class RuleFunction { return; } - FireRuleInputRedWeaponElementDTO chosen = pool.remove(bestIndex); + pool.remove(bestIndex); out.setRedWeapons(convertPoolToEquipmentVoList(pool)); // Tasks 由最终输出 redWeapons 一一生成(一个装备 -> 一个任务) out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons())); } + /** + * 目标规则:显式为 Tasks 填 targetId,并按红方命中率不足时补拿装备(出池,带上限)。 + */ + @SuppressWarnings("rawtypes") + public static void target(DroolsFact fact, Map globalParams){ + if (fact == null || fact.getTask() == null || fact.getFireRuleOutputVO() == null) { + return; + } + Map p = castParams(globalParams); + FireRuleTaskInputDTO blue = fact.getTask(); + FireRuleOutputVO out = fact.getFireRuleOutputVO(); + List pool = fact.getRedWeapons(); + if (pool == null) { + pool = new ArrayList<>(); + fact.setRedWeapons(pool); + } + if (out.getRedWeapons() == null) { + out.setRedWeapons(convertPoolToEquipmentVoList(pool)); + } + if (out.getTasks() == null || out.getTasks().size() != out.getRedWeapons().size()) { + out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons())); + } + + double threshold = readDouble(p, "redHitRateThreshold", 0.6d); + int maxExtra = readInt(p, "maxExtraWeaponsPerTask", 2); + int maxRounds = readInt(p, "maxSupplementRounds", 5); + int extraMinScore = readInt(p, "extraPickMinScore", 1); + int weight = readInt(p, "weight", 1); + String blueBlob = buildBlueTextBlob(blue); + + if (maxExtra > 0 && maxRounds > 0 && !pool.isEmpty()) { + Set selectedIds = new HashSet<>(); + for (FireRuleRedWeaponEquipmentVO w : out.getRedWeapons()) { + if (w != null && !isBlank(w.getEquipmentId())) { + selectedIds.add(w.getEquipmentId()); + } + } + + int extraCount = 0; + int rounds = 0; + while (rounds < maxRounds && extraCount < maxExtra && !pool.isEmpty()) { + rounds++; + boolean needExtra = false; + for (FireRuleRedWeaponEquipmentVO w : out.getRedWeapons()) { + Double rad = w != null ? w.getSuccessTargetRad() : null; + if (rad == null || rad.doubleValue() < threshold) { + needExtra = true; + break; + } + } + if (!needExtra) { + break; + } + + int bestIdx = -1; + int bestScore = Integer.MIN_VALUE; + for (int i = 0; i < pool.size(); i++) { + FireRuleInputRedWeaponElementDTO red = pool.get(i); + if (red == null) { + continue; + } + if (!isBlank(red.getEquipmentId()) && selectedIds.contains(red.getEquipmentId())) { + continue; + } + String redBlob = buildRedTextBlob(red); + int s = scoreRuleSlots(blueBlob, redBlob, p, weight) + + scoreLegacyLayer(blueBlob, redBlob, p, weight); + if (s > bestScore) { + bestScore = s; + bestIdx = i; + } else if (s == bestScore && bestIdx >= 0) { + if (compareRedForTieBreak(pool.get(bestIdx), red, p) > 0) { + bestIdx = i; + } + } + } + if (bestIdx < 0 || bestScore < extraMinScore) { + break; + } + FireRuleInputRedWeaponElementDTO extra = pool.remove(bestIdx); + FireRuleRedWeaponEquipmentVO vo = toRedEquipmentVo(extra); + out.getRedWeapons().add(vo); + if (!isBlank(vo.getEquipmentId())) { + selectedIds.add(vo.getEquipmentId()); + } + extraCount++; + } + } + + out.setTasks(FireRuleRedWeaponOutputFillHelper.toTaskNodes(out.getRedWeapons())); + FireRuleRedWeaponOutputFillHelper.assignTargetsToTasks(out.getTasks(), out.getRedWeapons(), blue, p); + } + + /** + * 阵位规则:根据蓝方武器 coordinate + trackPoints + warZone 计算红方平台 positions。 + */ + @SuppressWarnings("rawtypes") + public static void position(DroolsFact fact, Map globalParams) { + if (fact == null || fact.getTask() == null || fact.getFireRuleOutputVO() == null) { + return; + } + Map p = castParams(globalParams); + boolean enabled = Boolean.parseBoolean(String.valueOf(p.getOrDefault("positionRuleEnabled", true))); + if (!enabled) { + return; + } + FireRuleOutputVO out = fact.getFireRuleOutputVO(); + if (out.getRedWeapons() == null || out.getRedWeapons().isEmpty()) { + return; + } + FireRuleRedWeaponOutputFillHelper.fillPlatformPositions(out.getRedWeapons(), fact.getTask(), p); + } + @SuppressWarnings("unchecked") private static Map castParams(Map raw) { return raw == null ? new java.util.HashMap<>() : (Map) raw; @@ -277,10 +392,29 @@ public final class RuleFunction { } } + private static double readDouble(Map p, String key, double def) { + Object v = p.get(key); + if (v == null) { + return def; + } + if (v instanceof Number) { + return ((Number) v).doubleValue(); + } + try { + return Double.parseDouble(String.valueOf(v).trim()); + } catch (NumberFormatException e) { + return def; + } + } + private static String nz(String s) { return s == null ? "" : s; } + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } + private static FireRuleTaskWeaponVO toTaskWeaponVo(FireRuleInputRedWeaponElementDTO r) { FireRuleTaskWeaponVO w = new FireRuleTaskWeaponVO(); if (r != null) { @@ -315,6 +449,7 @@ public final class RuleFunction { vo.setIsReconTarget(src.getIsReconTarget()); vo.setIsInterferenceTarget(src.getIsInterferenceTarget()); vo.setIsDefendImportantPlace(src.getIsDefendImportantPlace()); + vo.setSuccessTargetRad(src.getSuccessTargetRad()); vo.setGroupType(src.getGroupType()); vo.setEquipmentId(src.getEquipmentId()); vo.setName(src.getName()); @@ -323,4 +458,5 @@ public final class RuleFunction { vo.setSubComponents(FireRuleRedWeaponOutputFillHelper.toOutputSubComponents(src.getSubComponents())); return vo; } + } diff --git a/auto-solution-rule/src/main/resources/rules/rule.drl b/auto-solution-rule/src/main/resources/rules/rule.drl index 17619c5..0ac479b 100644 --- a/auto-solution-rule/src/main/resources/rules/rule.drl +++ b/auto-solution-rule/src/main/resources/rules/rule.drl @@ -4,6 +4,8 @@ import com.solution.rule.domain.ultimately.fact.DroolsFact; import java.util.Map; import static com.solution.rule.utils.RuleFunction.equipmentRule; +import static com.solution.rule.utils.RuleFunction.target; +import static com.solution.rule.utils.RuleFunction.position; global java.util.Map globalParams; @@ -74,6 +76,77 @@ function Map buildParam(){ param.put("redMatchKeywords_missile", "防空,导弹,导弹发射"); param.put("missileScore", 1); + // ===================== 目标分配参数(写入 Tasks.task.execute) ===================== + // executeTypeDefault:生成 execute[0] 的类型字段 + param.put("executeTypeDefault", "assault"); + // targetPickMode:roundRobin(稳定轮询) / random(伪随机但同输入稳定) + param.put("targetPickMode", "roundRobin"); + // minTargetsPerRed / maxTargetsPerRedCap:每个红方任务最少/最多分配的目标数 + param.put("minTargetsPerRed", 1); + param.put("maxTargetsPerRedCap", 3); + // radToTargetsCsv:successTargetRad(命中率) -> 每红装目标数 的映射(阈值:目标数),按阈值从大到小匹配 + // 例:0.8:1,0.5:2,0.2:3 表示 successTargetRad>=0.8 分1个;>=0.5 分2个;>=0.2 分3个 + param.put("radToTargetsCsv", "0.8:1,0.5:2,0.2:3"); + // rangeParseRegex:从 attDefaultValue/attExplain 中提取射程数值的正则(取第1个数字) + param.put("rangeParseRegex", "(\\\\d+(?:\\\\.\\\\d+)?)"); + // rangeUnit:提取数值的单位,km/m(二选一) + param.put("rangeUnit", "km"); + // minRangeToAllowAssignKm:若解析到的蓝方射程小于该值,则该蓝方装备不参与被分配(无法解析则忽略此过滤) + param.put("minRangeToAllowAssignKm", 0); + + // ===================== 低命中率补拿装备参数 ===================== + // redHitRateThreshold:红方装备命中率阈值(低于该值时触发补拿) + param.put("redHitRateThreshold", 0.6); + // maxExtraWeaponsPerTask:每条蓝方任务最多补拿几件红装 + param.put("maxExtraWeaponsPerTask", 2); + // maxSupplementRounds:补拿循环最大轮次(防死循环) + param.put("maxSupplementRounds", 2); + // extraPickMinScore:补拿时红装最低匹配分 + param.put("extraPickMinScore", 1); + + // ===================== 阵位规则参数(写入 SubComponents.platform[].positions) ===================== + // positionRuleEnabled:是否启用阵位规则。true=执行阵位生成;false=跳过,不改 platform.positions。 + param.put("positionRuleEnabled", true); + // positionAnchorMode:锚点模式。当前实现使用 hybrid(蓝方 taskWeapons.coordinate 的中心点作为主锚点)。 + param.put("positionAnchorMode", "hybrid"); + // trackPointDirectionMode:航向计算模式。 + // - head2next:取 trackPoints[0] -> trackPoints[1] 作为方向(默认) + // - tail2prev:取倒数第二个 -> 最后一个点作为方向 + param.put("trackPointDirectionMode", "head2next"); + // fallbackBearingDeg:当 trackPoints 缺失或无法计算方位时,使用该默认方位角(度,0-360)。 + param.put("fallbackBearingDeg", 0); + // deployDistanceKmMin:部署距离下限(km)。最终距离不会小于该值。 + param.put("deployDistanceKmMin", 8); + // deployDistanceKmMax:部署距离上限(km)。最终距离不会大于该值。 + param.put("deployDistanceKmMax", 30); + // deployDistanceKmDefault:默认部署距离(km)。 + // 当 distanceByPlatformCsv 未命中任何关键词时,使用该值。 + param.put("deployDistanceKmDefault", 15); + // distanceByPlatformCsv:按“关键词”覆盖部署距离(km),不写死具体类型,完全由业务配置。 + // 格式:关键词:距离,关键词:距离(示例:防空:18,反坦克:10,迫击炮:8) + // 匹配范围:红方装备 Name / Platform_type 文本包含关键词即命中。 + // 优先级:命中后覆盖 deployDistanceKmDefault;但最终仍受 deployDistanceKmMin 与 deployDistanceKmMax 约束。 + param.put("distanceByPlatformCsv", ""); + // formationType:编队样式,可选 line / wedge / circle。 + param.put("formationType", "line"); + // formationSpacingMeters:编队间距(米),影响同一红装下 platform[] 点位离散程度。 + // 说明:Java 侧会与 minInterPlatformDistanceMeters 比较,取更大值,避免平台重叠过近。 + param.put("formationSpacingMeters", 300); + // formationHeadingOffsetDeg:编队相对主航向的偏转角(度),主要用于 wedge/circle 的分散方向。 + param.put("formationHeadingOffsetDeg", 15); + // defaultDeployHeight:默认部署高度(米),用于 positions 第3位高度值基线。 + param.put("defaultDeployHeight", 30); + // heightFollowBlueRatio:高度跟随蓝方比例(>=0)。 + // 计算方式:高度 = defaultDeployHeight + 蓝方锚点平均高度 * heightFollowBlueRatio。 + // 0 表示不跟随蓝方高度,仅使用默认高度。 + param.put("heightFollowBlueRatio", 0.0); + // enableWarZoneClamp:是否启用作战区约束。true=超出 warZoneLocation 时回拉到区内。 + param.put("enableWarZoneClamp", true); + // warZoneClampMode:作战区约束模式。当前实现使用 nearestInside(沿锚点到目标点方向二分回拉到区内)。 + param.put("warZoneClampMode", "nearestInside"); + // minInterPlatformDistanceMeters:平台最小间距(米)下限,用于抑制平台点位过度重叠。 + param.put("minInterPlatformDistanceMeters", 80); + return param; } @@ -86,3 +159,23 @@ then globalParams.putAll(buildParam()); equipmentRule($fact, globalParams); end + +rule "目标匹配" +salience 90 +when + $fact : DroolsFact(task != null) +then + // 显式目标分配规则:填充 Tasks.task.execute.targetList[*].targetId + globalParams.putAll(buildParam()); + target($fact, globalParams); +end + +rule "阵位匹配" +salience 80 +when + $fact : DroolsFact(task != null) +then + // 显式阵位规则:填充 redWeapons.SubComponents.platform[].positions + globalParams.putAll(buildParam()); + position($fact, globalParams); +end