火力规则:装备匹配规则实现

This commit is contained in:
MHW
2026-04-07 14:41:46 +08:00
parent 866fd215f2
commit 72f4caf555
7 changed files with 583 additions and 3549 deletions

View File

@@ -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<String, Object> defaults() {
Map<String, Object> 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;
}
}

View File

@@ -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<String, Object> 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;
}
/**

View File

@@ -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 节点(一个装备 -> 一个任务节点)。
*
* <p>字段映射约定:</p>
* <ul>
* <li>drawName = Name + \"打击任务\"</li>
* <li>side = OwnerForceSide按你的要求直接复制</li>
* <li>id 优先 EquipmentID其次 PlatID最后用 index 兜底</li>
* <li>task.payload.weaponId = EquipmentID</li>
* <li>task.payload.sideId = OwnerForceSide</li>
* <li>missionList尽量从 SubComponents.weapon[*] 中提取 mountedWeapon/name/number/launcherType</li>
* </ul>
*/
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<FireRuleSceneTaskNodeVO> toTaskNodes(List<FireRuleRedWeaponEquipmentVO> redWeapons) {
if (redWeapons == null || redWeapons.isEmpty()) {
return Collections.emptyList();
}
List<FireRuleSceneTaskNodeVO> 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<FireRuleMissionListItemVO> buildMissionList(FireRuleRedWeaponEquipmentVO redWeapon) {
if (redWeapon == null || redWeapon.getSubComponents() == null || redWeapon.getSubComponents().getWeapon() == null) {
return null;
}
List<FireRuleMissionListItemVO> 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<String, Object> 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();
}
}

View File

@@ -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 注入),本类不写死中文业务词。
* <p>
* <b>总分公式(每件候选红装)</b>
* <pre>
* 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 中各键说明)。
* </pre>
* <p>
* <b>文本串定义</b>
* <ul>
* <li>blueBlob{@link #buildBlueTextBlob} — 蓝方任务 drawName、dataType、taskWeapons 及组件名拼接成一大串,用于关键词包含判断。</li>
* <li>redBlob{@link #buildRedTextBlob} — 红装 name、platformType、supportType 拼接。</li>
* </ul>
* <p>
* <b>选优与输出</b>
* <ul>
* <li>在 redWeapons 池中对每件算 score取最大值若并列{@link #compareRedForTieBreak}(由 tieBreak 参数控制,默认 equipmentId 小者优先)。</li>
* <li>若 maxScore &lt; minSelectedScore不追加 fireRuleInputs仍将当前池映射到 FireRuleOutputVO.redWeapons。</li>
* <li>否则:池中 remove 选中项fireRuleInputs 追加一行drawName += outputDrawNameSuffixtaskWeapons 一条映射自选中红装redWeapons 为剩余池。</li>
* </ul>
*/
public class RuleFunction {
public final class RuleFunction {
private RuleFunction() {
}
/**
* 装备匹配
* 单轮规则执行:对当前 DroolsFact 的一条蓝方任务,从 redWeapons 中选一件装备并写回输出。
* <p>
* 参数 pglobalParams中各键含义与运算见 {@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<String, Object> p = castParams(globalParams);
FireRuleTaskInputDTO blue = fact.getTask();
List<FireRuleInputRedWeaponElementDTO> 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<String, Object> castParams(Map raw) {
return raw == null ? new java.util.HashMap<>() : (Map<String, Object>) 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<String, Object> 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」。
* <ul>
* <li>① bluePlatformKeywords_air + redPreferredWhenBlueAir → airScore</li>
* <li>② airTaskKeywords + redPreferredWhenBlueAir → airTaskScore</li>
* <li>③ groundTaskKeywords + redPreferredWhenGround → groundScore</li>
* <li>④ tankKeywords + redMatchKeywords_tank → tankScore</li>
* <li>⑤ missileKeywords + redMatchKeywords_missile → missileScore</li>
* </ul>
* 键名与 rule.drl 中 param.put 一致。
*/
private static int scoreLegacyLayer(String blueBlob, String redBlob, Map<String, Object> 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 字典序比较结果(&gt;0 表示 a 的 id 大于 b应选 b
*/
private static int compareRedForTieBreak(
FireRuleInputRedWeaponElementDTO a,
FireRuleInputRedWeaponElementDTO b,
Map<String, Object> 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<String, Object> p, String key, String def) {
Object v = p.get(key);
return v == null ? def : String.valueOf(v);
}
private static int readInt(Map<String, Object> 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<FireRuleRedWeaponEquipmentVO> convertPoolToEquipmentVoList(
List<FireRuleInputRedWeaponElementDTO> pool) {
List<FireRuleRedWeaponEquipmentVO> 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;
}
}