From 82be31d04536eef620ec8344602b86b7299cdb9f Mon Sep 17 00:00:00 2001 From: Scoppio Date: Sun, 29 Dec 2024 23:29:23 -0300 Subject: [PATCH] feat: added new curves, added reflection based ui controls --- .../considerations/proto_considerations.yaml | 4 +- .../data/ai/tw/decisions/proto_decisions.yaml | 4 +- megamek/data/ai/tw/evaluators/proto_dse.yaml | 8 +- .../megamek/ai/utility/BandFilterCurve.java | 106 ++++++++++++++++ .../src/megamek/ai/utility/BandPassCurve.java | 106 ++++++++++++++++ .../src/megamek/ai/utility/Consideration.java | 51 ++++++-- megamek/src/megamek/ai/utility/Curve.java | 20 ++- .../src/megamek/ai/utility/DefaultCurve.java | 9 +- .../src/megamek/ai/utility/LinearCurve.java | 2 + .../src/megamek/ai/utility/LogisticCurve.java | 4 + .../src/megamek/ai/utility/LogitCurve.java | 4 + .../megamek/ai/utility/ParabolicCurve.java | 3 + .../ai/utility/tw/TWUtilityAIRepository.java | 76 ++++++------ .../tw/considerations/MyUnitArmor.java | 2 +- .../tw/considerations/MyUnitIsCrippled.java | 39 ++++++ .../tw/considerations/MyUnitRoleIs.java | 5 +- .../tw/considerations/TargetUnitsArmor.java | 64 ++++++++++ .../considerations/TargetUnitsHaveRole.java | 3 +- .../ui/swing/ai/editor/AiProfileEditor.java | 93 ++++++++++++-- .../ui/swing/ai/editor/ConsiderationPane.java | 65 ++++++---- .../editor/ConsiderationParametersTable.java | 115 ++++++++++++++++++ .../client/ui/swing/ai/editor/CurvePane.java | 75 ++++++++---- .../ai/editor/DecisionScoreEvaluatorPane.java | 15 +++ .../editor/DecisionScoreEvaluatorTable.java | 26 ---- .../swing/ai/editor/ParametersTableModel.java | 63 ++++------ .../ui/swing/ai/editor/SpinnerCellEditor.java | 45 +++++++ .../swing/ai/editor/TWConsiderationClass.java | 50 ++++++++ 27 files changed, 857 insertions(+), 200 deletions(-) create mode 100644 megamek/src/megamek/ai/utility/BandFilterCurve.java create mode 100644 megamek/src/megamek/ai/utility/BandPassCurve.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java create mode 100644 megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java create mode 100644 megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java diff --git a/megamek/data/ai/tw/considerations/proto_considerations.yaml b/megamek/data/ai/tw/considerations/proto_considerations.yaml index 939fb9e1bd2..7152f98239b 100644 --- a/megamek/data/ai/tw/considerations/proto_considerations.yaml +++ b/megamek/data/ai/tw/considerations/proto_considerations.yaml @@ -3,9 +3,7 @@ name: "MyUnitArmor" curve: ! m: 0.5 b: 0.3 -parameters: - minValue: 0 - maxValue: 10 +parameters: {} --- ! name: "TargetWithinOptimalRange" curve: ! diff --git a/megamek/data/ai/tw/decisions/proto_decisions.yaml b/megamek/data/ai/tw/decisions/proto_decisions.yaml index ab04fef9e2b..f6dac7b3e47 100644 --- a/megamek/data/ai/tw/decisions/proto_decisions.yaml +++ b/megamek/data/ai/tw/decisions/proto_decisions.yaml @@ -13,9 +13,7 @@ decisionScoreEvaluator: ! curve: ! m: 0.5 b: 0.3 - parameters: - minValue: 0 - maxValue: 10 + parameters: {} - ! name: "TargetWithinOptimalRange" curve: ! diff --git a/megamek/data/ai/tw/evaluators/proto_dse.yaml b/megamek/data/ai/tw/evaluators/proto_dse.yaml index 092c3027257..5ac8befb1a2 100644 --- a/megamek/data/ai/tw/evaluators/proto_dse.yaml +++ b/megamek/data/ai/tw/evaluators/proto_dse.yaml @@ -9,9 +9,7 @@ considerations: curve: ! m: 0.5 b: 0.3 - parameters: - minValue: 0 - maxValue: 10 + parameters: {} - ! name: "TargetWithinOptimalRange" curve: ! @@ -33,9 +31,7 @@ considerations: curveType: "LinearCurve" m: 0.5 b: 0.3 - parameters: - minValue: 0 - maxValue: 10 + parameters: {} - ! name: "TargetWithinOptimalRange" curve: diff --git a/megamek/src/megamek/ai/utility/BandFilterCurve.java b/megamek/src/megamek/ai/utility/BandFilterCurve.java new file mode 100644 index 00000000000..2309ed756d7 --- /dev/null +++ b/megamek/src/megamek/ai/utility/BandFilterCurve.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.StringJoiner; + +import static megamek.codeUtilities.MathUtility.clamp01; + +@JsonTypeName("BandFilterCurve") +public class BandFilterCurve implements Curve { + private double m; + private double b; + private double k; + private double c; + + @JsonCreator + public BandFilterCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b, + @JsonProperty("k") double k, + @JsonProperty("c") double c) { + this.m = m; + this.b = b; + this.k = k; + this.c = c; + } + + @Override + public BandFilterCurve copy() { + return new BandFilterCurve(m, b, k, c); + } + + public double evaluate(double x) { + var bandStart = m - b / 2; + var bandEnd = m + b / 2; + + return clamp01(x < bandStart ? 1d + c : x > bandEnd ? 1d + c : 0d + k); + } + + @Override + public double getC() { + return c; + } + + @Override + public void setC(double c) { + this.c = c; + } + + @Override + public double getM() { + return m; + } + + @Override + public double getB() { + return b; + } + + @Override + public double getK() { + return k; + } + + @Override + public void setK(double k) { + this.k = k; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public String toString() { + return new StringJoiner(", ", BandFilterCurve.class.getSimpleName() + " [", "]") + .add("m=" + m) + .add("b=" + b) + .add("k=" + k) + .add("c=" + c) + .toString(); + } +} diff --git a/megamek/src/megamek/ai/utility/BandPassCurve.java b/megamek/src/megamek/ai/utility/BandPassCurve.java new file mode 100644 index 00000000000..6dbb07b7017 --- /dev/null +++ b/megamek/src/megamek/ai/utility/BandPassCurve.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.ai.utility; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.StringJoiner; + +import static megamek.codeUtilities.MathUtility.clamp01; + +@JsonTypeName("BandPassCurve") +public class BandPassCurve implements Curve { + private double m; + private double b; + private double k; + private double c; + + @JsonCreator + public BandPassCurve( + @JsonProperty("m") double m, + @JsonProperty("b") double b, + @JsonProperty("k") double k, + @JsonProperty("c") double c) { + this.m = m; + this.b = b; + this.k = k; + this.c = c; + } + + @Override + public BandPassCurve copy() { + return new BandPassCurve(m, b, k, c); + } + + public double evaluate(double x) { + var bandStart = m - b / 2; + var bandEnd = m + b / 2; + + return clamp01(x < bandStart ? 0d + k : x > bandEnd ? 0d + k : 1d + c); + } + + @Override + public double getK() { + return k; + } + + @Override + public void setK(double k) { + this.k = k; + } + + @Override + public double getC() { + return c; + } + + @Override + public void setC(double c) { + this.c = c; + } + + @Override + public double getM() { + return m; + } + + @Override + public double getB() { + return b; + } + + @Override + public void setM(double m) { + this.m = m; + } + + @Override + public void setB(double b) { + this.b = b; + } + + @Override + public String toString() { + return new StringJoiner(", ", BandPassCurve.class.getSimpleName() + " [", "]") + .add("m=" + m) + .add("b=" + b) + .add("k=" + k) + .add("c=" + c) + .toString(); + } +} diff --git a/megamek/src/megamek/ai/utility/Consideration.java b/megamek/src/megamek/ai/utility/Consideration.java index 653afd89e4a..abffd2d5c47 100644 --- a/megamek/src/megamek/ai/utility/Consideration.java +++ b/megamek/src/megamek/ai/utility/Consideration.java @@ -47,6 +47,8 @@ public abstract class Consideration implements Named @JsonProperty("parameters") protected Map parameters = Collections.emptyMap(); + protected transient Map> parameterTypes = Collections.emptyMap(); + public Consideration() { } @@ -78,35 +80,58 @@ public Map getParameters() { return Map.copyOf(parameters); } + public Map> getParameterTypes() { + return Map.copyOf(parameterTypes); + } + + public Class getParameterType(String key) { + return parameterTypes.get(key); + } + public void setParameters(Map parameters) { + var params = new HashMap(); + for (var entry : parameters.entrySet()) { + var clazz = parameterTypes.get(entry.getKey()); + if (clazz == null) { + throw new IllegalArgumentException("Unknown parameter: " + entry.getKey()); + } + if (clazz.isAssignableFrom(entry.getValue().getClass())) { + throw new IllegalArgumentException("Invalid parameter type for " + entry.getKey() + ": " + entry.getValue().getClass()); + } + params.put(entry.getKey(), entry.getValue()); + } this.parameters = Map.copyOf(parameters); } - protected double getDoubleParameter(String key) { - return (double) parameters.get(key); + public double getDoubleParameter(String key) { + return (double) getParameter(key); + } + + public int getIntParameter(String key) { + return (int) getParameter(key); } - protected int getIntParameter(String key) { - return (int) parameters.get(key); + public boolean getBooleanParameter(String key) { + return (boolean) getParameter(key); } - protected boolean getBooleanParameter(String key) { - return (boolean) parameters.get(key); + public String getStringParameter(String key) { + return (String) getParameter(key); } - protected String getStringParameter(String key) { - return (String) parameters.get(key); + public float getFloatParameter(String key) { + return (float) getParameter(key); } - protected float getFloatParameter(String key) { - return (float) parameters.get(key); + public long getLongParameter(String key) { + return (long) getParameter(key); } - protected long getLongParameter(String key) { - return (long) parameters.get(key); + public Object getParameter(String key) { + return parameters.get(key); } - protected boolean hasParameter(String key) { + public boolean hasParameter(String key) { return parameters.containsKey(key); } diff --git a/megamek/src/megamek/ai/utility/Curve.java b/megamek/src/megamek/ai/utility/Curve.java index 3f7d6f12606..ad4c64500cf 100644 --- a/megamek/src/megamek/ai/utility/Curve.java +++ b/megamek/src/megamek/ai/utility/Curve.java @@ -31,7 +31,9 @@ @JsonSubTypes.Type(value = LinearCurve.class, name = "LinearCurve"), @JsonSubTypes.Type(value = LogisticCurve.class, name = "LogisticCurve"), @JsonSubTypes.Type(value = LogitCurve.class, name = "LogitCurve"), - @JsonSubTypes.Type(value = ParabolicCurve.class, name = "ParabolicCurve") + @JsonSubTypes.Type(value = ParabolicCurve.class, name = "ParabolicCurve"), + @JsonSubTypes.Type(value = BandPassCurve.class, name = "BandPassCurve"), + @JsonSubTypes.Type(value = BandFilterCurve.class, name = "BandFilterCurve"), }) public interface Curve { double evaluate(double x); @@ -127,4 +129,20 @@ default void setK(double k) { default void setC(double c) { // } + + default double getM() { + return 0.0; + } + + default double getB() { + return 0.0; + } + + default double getK() { + return 0.0; + } + + default double getC() { + return 0.0; + } } diff --git a/megamek/src/megamek/ai/utility/DefaultCurve.java b/megamek/src/megamek/ai/utility/DefaultCurve.java index f12b55f9444..283d5d18ba4 100644 --- a/megamek/src/megamek/ai/utility/DefaultCurve.java +++ b/megamek/src/megamek/ai/utility/DefaultCurve.java @@ -27,7 +27,10 @@ public enum DefaultCurve { LogisticDecreasing(new LogisticCurve(1.0, 0.5, -10.0, 0.0)), Logit(new LogitCurve(1.0, 0.5, -15.0, 0.0)), - LogitDecreasing(new LogitCurve(1.0, 0.5, 15.0, 0.0)); + LogitDecreasing(new LogitCurve(1.0, 0.5, 15.0, 0.0)), + + BandPass(new BandPassCurve(0.5, 0.2, 0, 0)), + BandFilter(new BandFilterCurve(0.5, 0.2, 0, 0)); private final Curve curve; @@ -44,6 +47,10 @@ public static DefaultCurve fromCurve(Curve curve) { return Logistic; } else if (curve instanceof LogitCurve) { return Logit; + } else if (curve instanceof BandPassCurve) { + return BandPass; + } else if (curve instanceof BandFilterCurve) { + return BandFilter; } // Return Linear as default return Linear; diff --git a/megamek/src/megamek/ai/utility/LinearCurve.java b/megamek/src/megamek/ai/utility/LinearCurve.java index 9a3b5eae258..744846ab37b 100644 --- a/megamek/src/megamek/ai/utility/LinearCurve.java +++ b/megamek/src/megamek/ai/utility/LinearCurve.java @@ -46,10 +46,12 @@ public double evaluate(double x) { return clamp01(m * x + b); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } diff --git a/megamek/src/megamek/ai/utility/LogisticCurve.java b/megamek/src/megamek/ai/utility/LogisticCurve.java index 71174b93acb..c963a1283d3 100644 --- a/megamek/src/megamek/ai/utility/LogisticCurve.java +++ b/megamek/src/megamek/ai/utility/LogisticCurve.java @@ -53,18 +53,22 @@ public double evaluate(double x) { return clamp01(m * (1 / (1 + Math.exp(-k * (x - b)))) + c); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } + @Override public double getK() { return k; } + @Override public double getC() { return c; } diff --git a/megamek/src/megamek/ai/utility/LogitCurve.java b/megamek/src/megamek/ai/utility/LogitCurve.java index 0b608d6dc39..b1c95abf120 100644 --- a/megamek/src/megamek/ai/utility/LogitCurve.java +++ b/megamek/src/megamek/ai/utility/LogitCurve.java @@ -58,18 +58,22 @@ public double evaluate(double x) { return clamp01(b + (1 / k) * Math.log((m - (x - c)) / (x - c))); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } + @Override public double getK() { return k; } + @Override public double getC() { return c; } diff --git a/megamek/src/megamek/ai/utility/ParabolicCurve.java b/megamek/src/megamek/ai/utility/ParabolicCurve.java index b102b2fd018..4c142d80ec4 100644 --- a/megamek/src/megamek/ai/utility/ParabolicCurve.java +++ b/megamek/src/megamek/ai/utility/ParabolicCurve.java @@ -50,14 +50,17 @@ public double evaluate(double x) { return clamp01(-m * Math.pow(x - b, 2) + k); } + @Override public double getM() { return m; } + @Override public double getB() { return b; } + @Override public double getK() { return k; } diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java index a24c9de3233..f6e1e5a0019 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/TWUtilityAIRepository.java @@ -17,7 +17,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; +import megamek.client.bot.duchess.ai.utility.tw.considerations.*; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecision; import megamek.client.bot.duchess.ai.utility.tw.decision.TWDecisionScoreEvaluator; import megamek.client.bot.duchess.ai.utility.tw.profile.TWProfile; @@ -65,25 +65,6 @@ private void initialize() { loadUserDataRepository(); } - private void loadRepository() { - loadData(Configuration.twAiDir()); - } - - private void loadUserDataRepository() { - loadData(Configuration.userDataAiTwDir()); - } - - private void loadData(File directory) { - loadConsiderations(new File(directory, CONSIDERATIONS)) - .forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration)); - loadDecisionScoreEvaluators(new File(directory, EVALUATORS)).forEach( - twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator)); - loadDecisions(new File(directory, DECISIONS)).forEach( - twDecision -> decisions.put(twDecision.getName(), twDecision)); - loadProfiles(new File(directory, PROFILES)).forEach( - twProfile -> profiles.put(twProfile.getName(), twProfile)); - } - public void reloadRepository() { decisionScoreEvaluators.clear(); considerations.clear(); @@ -92,6 +73,24 @@ public void reloadRepository() { initialize(); } + public void persistData() { + var twAiDir = Configuration.twAiDir(); + createDirectoryStructureIfMissing(twAiDir); + persistToFile(new File(twAiDir, EVALUATORS + File.separator + "decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); + persistToFile(new File(twAiDir, CONSIDERATIONS + File.separator + "considerations.yaml"), considerations.values()); + persistToFile(new File(twAiDir, DECISIONS + File.separator + "decisions.yaml"), decisions.values()); + persistToFile(new File(twAiDir, PROFILES + File.separator + "profiles.yaml"), profiles.values()); + } + + public void persistDataToUserData() { + var userDataAiTwDir = Configuration.userDataAiTwDir(); + createDirectoryStructureIfMissing(userDataAiTwDir); + persistToFile(new File(userDataAiTwDir, EVALUATORS + File.separator + "custom_decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); + persistToFile(new File(userDataAiTwDir, CONSIDERATIONS + File.separator + "custom_considerations.yaml"), considerations.values()); + persistToFile(new File(userDataAiTwDir, DECISIONS + File.separator + "custom_decisions.yaml"), decisions.values()); + persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); + } + public List getDecisions() { return List.copyOf(decisions.values()); } @@ -168,6 +167,25 @@ public void addProfile(TWProfile profile) { profiles.put(profile.getName(), profile); } + private void loadRepository() { + loadData(Configuration.twAiDir()); + } + + private void loadUserDataRepository() { + loadData(Configuration.userDataAiTwDir()); + } + + private void loadData(File directory) { + loadConsiderations(new File(directory, CONSIDERATIONS)) + .forEach(twConsideration -> considerations.put(twConsideration.getClass().getSimpleName(), twConsideration)); + loadDecisionScoreEvaluators(new File(directory, EVALUATORS)).forEach( + twDecisionScoreEvaluator -> decisionScoreEvaluators.put(twDecisionScoreEvaluator.getName(), twDecisionScoreEvaluator)); + loadDecisions(new File(directory, DECISIONS)).forEach( + twDecision -> decisions.put(twDecision.getName(), twDecision)); + loadProfiles(new File(directory, PROFILES)).forEach( + twProfile -> profiles.put(twProfile.getName(), twProfile)); + } + private List loadDecisionScoreEvaluators(File inputFile) { return loadObjects(inputFile, TWDecisionScoreEvaluator.class); } @@ -224,24 +242,6 @@ private List objectsFromFile(Class clazz, File file) { return Collections.emptyList(); } - public void persistData() { - var twAiDir = Configuration.twAiDir(); - createDirectoryStructureIfMissing(twAiDir); - persistToFile(new File(twAiDir, EVALUATORS + File.separator + "decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); - persistToFile(new File(twAiDir, CONSIDERATIONS + File.separator + "considerations.yaml"), considerations.values()); - persistToFile(new File(twAiDir, DECISIONS + File.separator + "decisions.yaml"), decisions.values()); - persistToFile(new File(twAiDir, PROFILES + File.separator + "profiles.yaml"), profiles.values()); - } - - public void persistDataToUserData() { - var userDataAiTwDir = Configuration.userDataAiTwDir(); - createDirectoryStructureIfMissing(userDataAiTwDir); - persistToFile(new File(userDataAiTwDir, EVALUATORS + File.separator + "custom_decision_score_evaluators.yaml"), decisionScoreEvaluators.values()); - persistToFile(new File(userDataAiTwDir, CONSIDERATIONS + File.separator + "custom_considerations.yaml"), considerations.values()); - persistToFile(new File(userDataAiTwDir, DECISIONS + File.separator + "custom_decisions.yaml"), decisions.values()); - persistToFile(new File(userDataAiTwDir, PROFILES + File.separator + "custom_profiles.yaml"), profiles.values()); - } - private void persistToFile(File outputFile, Collection objects) { if (objects.isEmpty()) { return; diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java index 588b20d24a8..26c806ddc53 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitArmor.java @@ -25,7 +25,7 @@ import static megamek.codeUtilities.MathUtility.clamp01; /** - * This consideration is used to determine if a target is an easy target. + * This consideration is used to determine the armor percent. */ @JsonTypeName("MyUnitArmor") public class MyUnitArmor extends TWConsideration { diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java new file mode 100644 index 00000000000..e06c3b6c901 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitIsCrippled.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine if the unit is crippled or not. + */ +@JsonTypeName("MyUnitIsCrippled") +public class MyUnitIsCrippled extends TWConsideration { + + public MyUnitIsCrippled() { + } + + @Override + public double score(DecisionContext context) { + var currentUnit = context.getCurrentUnit().orElseThrow(); + return currentUnit.isCrippled(true) ? 1d : 0d; + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java index 3564b2cfca6..2d5ba236f68 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/MyUnitRoleIs.java @@ -22,8 +22,6 @@ import java.util.Map; -import static megamek.codeUtilities.MathUtility.clamp01; - /** * This consideration is used to determine if a target is an easy target. */ @@ -31,7 +29,8 @@ public class MyUnitRoleIs extends TWConsideration { public MyUnitRoleIs() { - parameters = Map.of("role", UnitRole.AMBUSHER.name()); + parameters = Map.of("role", UnitRole.AMBUSHER); + parameterTypes = Map.of("role", UnitRole.class); } @Override diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java new file mode 100644 index 00000000000..663aa2ac1e8 --- /dev/null +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsArmor.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.bot.duchess.ai.utility.tw.considerations; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import megamek.ai.utility.DecisionContext; +import megamek.common.Entity; + +import java.util.Map; + +import static megamek.codeUtilities.MathUtility.clamp01; + +/** + * This consideration is used to determine the armor percent of enemies. + */ +@JsonTypeName("TargetUnitsArmor") +public class TargetUnitsArmor extends TWConsideration { + + public enum Aggregation { + AVERAGE, + MIN, + MAX + } + + public TargetUnitsArmor() { + parameters = Map.of("aggregation", Aggregation.AVERAGE); + parameterTypes = Map.of("aggregation", Aggregation.class); + } + + @Override + public double score(DecisionContext context) { + var targets = context.getTargets(); + if (targets.isEmpty()) { + return 0d; + } + var armorPercent = 0d; + for (var target : targets) { + armorPercent += target.getArmorRemainingPercent(); + } + if (getBooleanParameter("average")) { + return clamp01(armorPercent / targets.size()); + } else if (getBooleanParameter("min")) { + return clamp01(targets.stream().mapToDouble(Entity::getArmorRemainingPercent).min().orElse(0d)); + } else if (getBooleanParameter("max")) { + return clamp01(targets.stream().mapToDouble(Entity::getArmorRemainingPercent).max().orElse(0d)); + } + + return clamp01(armorPercent); + } + +} diff --git a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java index 1210b22893d..da7892f7339 100644 --- a/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java +++ b/megamek/src/megamek/client/bot/duchess/ai/utility/tw/considerations/TargetUnitsHaveRole.java @@ -29,7 +29,8 @@ public class TargetUnitsHaveRole extends TWConsideration { public TargetUnitsHaveRole() { - parameters = Map.of("role", UnitRole.AMBUSHER.name()); + parameters = Map.of("role", UnitRole.AMBUSHER); + parameterTypes = Map.of("role", UnitRole.class); } @Override diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java index 6a8a245de06..1e3dad06eed 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/AiProfileEditor.java @@ -156,15 +156,15 @@ public void mouseClicked(MouseEvent e) { } }); - repositoryViewer.addTreeSelectionListener(e -> { - TreePath path = e.getPath(); - if (path != null) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.isLeaf()) { - handleOpenNodeAction(node); - } - } - }); +// repositoryViewer.addTreeSelectionListener(e -> { +// TreePath path = e.getPath(); +// if (path != null) { +// DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); +// if (node.isLeaf()) { +// handleOpenNodeAction(node); +// } +// } +// }); getFrame().setJMenuBar(menuBar); menuBar.addActionListener(this); saveProfileButton.addActionListener(e -> { @@ -232,7 +232,7 @@ public void mouseClicked(MouseEvent e) { private JPopupMenu createContextMenu(DefaultMutableTreeNode node) { // Create a popup menu JPopupMenu contextMenu = new JPopupMenu(); - + var obj = node.getUserObject(); // Example menu item #1 JMenuItem menuItemAction = new JMenuItem("Open"); menuItemAction.addActionListener(evt -> { @@ -240,17 +240,79 @@ private JPopupMenu createContextMenu(DefaultMutableTreeNode node) { }); contextMenu.add(menuItemAction); + if (obj instanceof TWDecision twDecision) { + var action = new JMenuItem("Add to current Profile"); + action.addActionListener(evt -> { + var model = profileDecisionTable.getModel(); + //noinspection unchecked + ((DecisionTableModel) model).addRow(twDecision); + }); + contextMenu.add(action); + } else if (obj instanceof TWProfile) { + var action = getCopyProfileMenuItem((TWProfile) obj); + contextMenu.add(action); + } else if (obj instanceof TWDecisionScoreEvaluator twDse) { + var action = new JMenuItem("New Decision Score Evaluator"); + action.addActionListener(evt -> { + createNewDecisionScoreEvaluator(); + }); + contextMenu.add(action); + } else if (obj instanceof TWConsideration twConsideration) { + var action = new JMenuItem("New Consideration"); + action.addActionListener(evt -> { + addNewConsideration(); + }); + contextMenu.add(action); + // if the tab is a DSE, add the consideration to the DSE + if (mainEditorTabbedPane.getSelectedComponent() == dseTabPane) { + var action1 = new JMenuItem("Add to current Decision Score Evaluator"); + action1.addActionListener(evt -> { + var dse = ((DecisionScoreEvaluatorPane) dsePane).getDecisionScoreEvaluator(); + dse.addConsideration(twConsideration); + ((DecisionScoreEvaluatorPane) dsePane).setDecisionScoreEvaluator(dse); + }); + contextMenu.add(action1); + } else if (mainEditorTabbedPane.getSelectedComponent() == decisionTabPane) { + var action1 = new JMenuItem("Add to current Decision"); + action1.addActionListener(evt -> { + var dse = ((DecisionScoreEvaluatorPane)decisionTabDsePanel).getDecisionScoreEvaluator(); + dse.addConsideration(twConsideration); + ((DecisionScoreEvaluatorPane) decisionTabDsePanel).setDecisionScoreEvaluator(dse); + }); + contextMenu.add(action1); + } + } + // Example menu item #2 JMenuItem menuItemOther = new JMenuItem("Delete"); menuItemOther.addActionListener(evt -> { // Another action - handleDeleteNodeAction(node); + int deletePrompt = JOptionPane.showConfirmDialog(null, + Messages.getString("aiEditor.deleteNodePrompt"), + Messages.getString("BoardEditor.deleteNodeTitle"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + if (deletePrompt == JOptionPane.YES_OPTION) { + handleDeleteNodeAction(node); + } }); contextMenu.add(menuItemOther); return contextMenu; } + private JMenuItem getCopyProfileMenuItem(TWProfile obj) { + var action = new JMenuItem("Copy Profile"); + action.addActionListener(evt -> { + profileId = -1; + profileNameTextField.setText(obj.getName() + " (Copy)"); + descriptionTextField.setText(obj.getDescription()); + profileDecisionTable.setModel(new DecisionTableModel<>(obj.getDecisions())); + mainEditorTabbedPane.setSelectedComponent(profileTabPane); + hasProfileChanges = true; + }); + return action; + } private void handleOpenNodeAction(DefaultMutableTreeNode node) { @@ -282,6 +344,7 @@ private void handleDeleteNodeAction(DefaultMutableTreeNode node) { sharedData.removeConsideration(twConsideration); hasConsiderationChanges = true; } + ((DefaultTreeModel) repositoryViewer.getModel()).removeNodeFromParent(node); } private void openConsideration(TWConsideration twConsideration) { @@ -512,8 +575,12 @@ private void loadDataRepoViewer() { addToMutableTreeNode(root, TreeViewHelper.CONSIDERATIONS.getName(), sharedData.getConsiderations()); DefaultTreeModel treeModel = new DefaultTreeModel(root); - repositoryViewer = new JTree(treeModel); - repositoryViewer.updateUI(); + if (repositoryViewer == null) { + repositoryViewer = new JTree(treeModel); + } else { + repositoryViewer.setModel(treeModel); + repositoryViewer.updateUI(); + } } private void addToMutableTreeNode(DefaultMutableTreeNode root, String nodeName, List items) { diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java index 5513d884586..0a0e3519751 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationPane.java @@ -17,24 +17,19 @@ import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; -import com.intellij.uiDesigner.core.Spacer; import megamek.ai.utility.Consideration; import megamek.ai.utility.DefaultCurve; -import megamek.client.bot.duchess.ai.utility.tw.TWUtilityAIRepository; import megamek.client.bot.duchess.ai.utility.tw.considerations.TWConsideration; -import megamek.common.Entity; import javax.swing.*; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.lang.reflect.Method; import java.util.Collections; import java.util.ResourceBundle; public class ConsiderationPane extends JPanel { private JTextField considerationName; - private JComboBox considerationComboBox; + private JComboBox considerationComboBox; private JPanel curveContainer; private JTable parametersTable; private JPanel considerationPane; @@ -45,26 +40,46 @@ public ConsiderationPane() { add(considerationPane); considerationComboBox.addActionListener(e -> { - TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); - if (consideration != null) { - considerationName.setText(consideration.getName()); - ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); - ((CurvePane) (curveContainer)).setCurve(consideration.getCurve()); + if (considerationComboBox.getSelectedItem() == null) { + return; } + + var selectedClass = ((TWConsiderationClass) considerationComboBox.getSelectedItem()).getConsiderationClass(); + if (selectedClass == null) { + return; + } + + try { + // Create a new instance via reflection + var newInstance = (TWConsideration) selectedClass.getDeclaredConstructor().newInstance(); + + // Populate your fields (likely blank or default if no-arg constructor doesn't do much) + considerationName.setText(selectedClass.getSimpleName()); + ((ParametersTableModel) parametersTable.getModel()).setParameters(newInstance); + ((CurvePane) curveContainer).setCurve(DefaultCurve.Linear.getCurve()); + } catch (Exception ex) { + JOptionPane.showMessageDialog( + this, + "Failed to instantiate " + selectedClass.getSimpleName() + ": " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + }); } public void setConsideration(Consideration consideration) { - considerationComboBox.setSelectedItem(consideration); + considerationComboBox.setSelectedItem(TWConsiderationClass.fromClass(consideration.getClass())); considerationName.setText(consideration.getName()); - ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration.getParameters()); + ((ParametersTableModel) parametersTable.getModel()).setParameters(consideration); ((CurvePane) curveContainer).setCurve(consideration.getCurve()); } public void setEmptyConsideration() { considerationComboBox.setSelectedItem(null); considerationName.setText(""); - ((ParametersTableModel) parametersTable.getModel()).setParameters(Collections.emptyMap()); + ((ParametersTableModel) parametersTable.getModel()).setEmptyParameters(); ((CurvePane) curveContainer).setCurve(DefaultCurve.Logit.getCurve()); } @@ -73,21 +88,27 @@ public void setHoverStateModel(HoverStateModel model) { } public TWConsideration getConsideration() { - TWConsideration consideration = (TWConsideration) considerationComboBox.getSelectedItem(); - if (consideration == null) { + var selectedItem = (TWConsiderationClass) considerationComboBox.getSelectedItem(); + if (selectedItem == null) { throw new IllegalStateException("No consideration selected"); } - consideration.setName(considerationName.getText()); - consideration.setParameters(((ParametersTableModel) parametersTable.getModel()).getParameters()); - consideration.setCurve(((CurvePane) curveContainer).getCurve().copy()); - return consideration; + + try { + var consideration = (TWConsideration) selectedItem.getConsiderationClass().getDeclaredConstructor().newInstance(); + consideration.setName(considerationName.getText()); + consideration.setParameters(((ParametersTableModel) parametersTable.getModel()).getParameters()); + consideration.setCurve(((CurvePane) curveContainer).getCurve().copy()); + return consideration; + } catch (Exception ex) { + throw new IllegalStateException("Failed to instantiate " + selectedItem.getConsiderationClass().getSimpleName(), ex); + } } private void createUIComponents() { - parametersTable = new JTable(new ParametersTableModel()); + parametersTable = new ConsiderationParametersTable(new ParametersTableModel()); parametersTable.setModel(new ParametersTableModel()); - considerationComboBox = new JComboBox<>(TWUtilityAIRepository.getInstance().getConsiderations().toArray(new TWConsideration[0])); + considerationComboBox = new JComboBox<>(TWConsiderationClass.values()); considerationComboBox.setSelectedItem(null); curveContainer = new CurvePane(); diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java new file mode 100644 index 00000000000..8abacd04a07 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ConsiderationParametersTable.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import javax.swing.*; +import javax.swing.table.TableCellEditor; +import java.awt.*; + +public class ConsiderationParametersTable extends JTable { + + public ConsiderationParametersTable( + ParametersTableModel model) { + super(model); + } + + public void createUIComponents() { + // + } + + @Override + @SuppressWarnings("unchecked") + public ParametersTableModel getModel() { + return (ParametersTableModel) super.getModel(); + } + + @Override + public TableCellEditor getCellEditor(int row, int column) { + if (column == 1) { + var clazz = getModel().getParameterValueAt(row); + var value = getModel().getValueAt(row, column); + if (clazz == null) { + // Should actually throw an error here... + return super.getCellEditor(row, column); + } + if (clazz.equals(Boolean.class)) { + return new DefaultCellEditor(new JCheckBox()); + } else if (clazz.equals(Double.class)) { + return new SpinnerCellEditor( + (double) (value == null ? 0d : value), + Double.MIN_VALUE, + Double.MAX_VALUE, + 0.1d + ); + } else if (clazz.equals(Float.class)) { + return new SpinnerCellEditor( + (float) (value == null ? 0f : value), + Float.MIN_VALUE, + Float.MAX_VALUE, + 0.1f + ); + } else if (clazz.equals(Integer.class)) { + return new SpinnerCellEditor( + (int) (value == null ? 0 : value), + Integer.MIN_VALUE, + Integer.MAX_VALUE, + 1 + ); + } else if (clazz.equals(Long.class)) { + return new SpinnerCellEditor( + (long) (value == null ? 0 : value), + Long.MIN_VALUE, + Long.MAX_VALUE, + 1 + ); + } else if (clazz.equals(String.class)) { + return new DefaultCellEditor(new JTextField()); + } else if (clazz.isEnum()) { + var cb = new JComboBox<>( + clazz.getEnumConstants() + ); + return new DefaultCellEditor(cb); + } + } + return super.getCellEditor(row, column); + } + + public static class SpinnerCellEditor extends AbstractCellEditor implements TableCellEditor { + private final JSpinner spinner; + + public SpinnerCellEditor(double defaultValue, double min, double max, double step) { + spinner = new JSpinner(new SpinnerNumberModel(min, min, max, step)); + spinner.setValue(defaultValue); + JComponent editor = spinner.getEditor(); + if (editor instanceof JSpinner.DefaultEditor) { + JFormattedTextField textField = ((JSpinner.DefaultEditor) editor).getTextField(); + textField.setHorizontalAlignment(JFormattedTextField.LEFT); + } + } + + @Override + public Object getCellEditorValue() { + return spinner.getValue(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + spinner.setValue(value); + return spinner; + } + } + +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java index cbca8f85afb..cd3fbed5f6d 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/CurvePane.java @@ -15,8 +15,6 @@ package megamek.client.ui.swing.ai.editor; -import com.intellij.uiDesigner.core.GridConstraints; -import com.intellij.uiDesigner.core.GridLayoutManager; import megamek.ai.utility.*; import javax.swing.*; @@ -249,31 +247,54 @@ private void createUIComponents() { private void updateCurveDataUI() { curveGraph.repaint(); var curve = selectedCurve.get(); - if (curve instanceof LinearCurve) { - bParamSpinner.setValue(((LinearCurve) curve).getB()); - mParamSpinner.setValue(((LinearCurve) curve).getM()); - kParamSpinner.setEnabled(false); - cParamSpinner.setEnabled(false); - } else if (curve instanceof ParabolicCurve) { - bParamSpinner.setValue(((ParabolicCurve) curve).getB()); - mParamSpinner.setValue(((ParabolicCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((ParabolicCurve) curve).getK()); - cParamSpinner.setEnabled(false); - } else if (curve instanceof LogitCurve) { - bParamSpinner.setValue(((LogitCurve) curve).getB()); - mParamSpinner.setValue(((LogitCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((LogitCurve) curve).getK()); - cParamSpinner.setEnabled(true); - cParamSpinner.setValue(((LogitCurve) curve).getC()); - } else if (curve instanceof LogisticCurve) { - bParamSpinner.setValue(((LogisticCurve) curve).getB()); - mParamSpinner.setValue(((LogisticCurve) curve).getM()); - kParamSpinner.setEnabled(true); - kParamSpinner.setValue(((LogisticCurve) curve).getK()); - cParamSpinner.setEnabled(true); - cParamSpinner.setValue(((LogisticCurve) curve).getC()); + Class curveClass = curve.getClass(); + + // B + boolean hasB = isMethodOverridden(curveClass, Curve.class, "getB"); + bParamSpinner.setEnabled(hasB); + if (hasB) { + bParamSpinner.setValue(curve.getB()); + } + + // M + boolean hasM = isMethodOverridden(curveClass, Curve.class, "getM"); + mParamSpinner.setEnabled(hasM); + if (hasM) { + mParamSpinner.setValue(curve.getM()); + } + + // K + boolean hasK = isMethodOverridden(curveClass, Curve.class, "getK"); + kParamSpinner.setEnabled(hasK); + if (hasK) { + kParamSpinner.setValue(curve.getK()); + } + + // C + boolean hasC = isMethodOverridden(curveClass, Curve.class, "getC"); + cParamSpinner.setEnabled(hasC); + if (hasC) { + cParamSpinner.setValue(curve.getC()); + } + } + + public static boolean isMethodOverridden( + Class clazz, + Class interfaceClass, + String methodName, + Class... paramTypes + ) { + try { + // The method from the interface + var interfaceMethod = interfaceClass.getMethod(methodName, paramTypes); + // The method as found on the concrete class + var classMethod = clazz.getMethod(methodName, paramTypes); + + // If the declaring class is different, the method is overridden + return !classMethod.getDeclaringClass().equals(interfaceMethod.getDeclaringClass()); + } catch (NoSuchMethodException e) { + // If the class or interface does not declare the method at all + return false; } } } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java index 34af41a0a8b..11f103981d9 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorPane.java @@ -23,6 +23,8 @@ import javax.swing.*; import java.awt.*; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -40,6 +42,18 @@ public DecisionScoreEvaluatorPane() { $$$setupUI$$$(); add(decisionScoreEvaluatorPane, BorderLayout.WEST); hoverStateModel = new HoverStateModel(); + + // Add a MouseWheelListener to forward the event to the parent JScrollPane + this.addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (getParent() instanceof JViewport viewport) { + if (viewport.getParent() instanceof JScrollPane scrollPane) { + scrollPane.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, scrollPane)); + } + } + } + }); } public TWDecisionScoreEvaluator getDecisionScoreEvaluator() { @@ -96,6 +110,7 @@ public void setDecisionScoreEvaluator(DecisionScoreEvaluator dse) { considerationsPane.add(c, new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); considerationsPane.add(new JSeparator(), new GridConstraints(row++, 0, 1, 1, GridConstraints.ANCHOR_NORTHEAST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null)); } + this.updateUI(); } /** diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java index 02522ae7135..323662edb54 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/DecisionScoreEvaluatorTable.java @@ -66,30 +66,4 @@ public TableCellEditor getCellEditor(int row, int column) { return super.getCellEditor(row, column); } - - public static class SpinnerCellEditor extends AbstractCellEditor implements TableCellEditor { - private final JSpinner spinner; - - public SpinnerCellEditor(double defaultValue, double min, double max, double step) { - spinner = new JSpinner(new SpinnerNumberModel(min, min, max, step)); - spinner.setValue(defaultValue); - JComponent editor = spinner.getEditor(); - if (editor instanceof JSpinner.DefaultEditor) { - JFormattedTextField textField = ((JSpinner.DefaultEditor) editor).getTextField(); - textField.setHorizontalAlignment(JFormattedTextField.LEFT); - } - } - - @Override - public Object getCellEditorValue() { - return spinner.getValue(); - } - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - spinner.setValue(value); - return spinner; - } - } - } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java index 92e576a30c8..fdb55469276 100644 --- a/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java +++ b/megamek/src/megamek/client/ui/swing/ai/editor/ParametersTableModel.java @@ -15,6 +15,7 @@ package megamek.client.ui.swing.ai.editor; +import megamek.ai.utility.Consideration; import megamek.logging.MMLogger; import javax.swing.table.AbstractTableModel; @@ -25,52 +26,32 @@ public class ParametersTableModel extends AbstractTableModel { private static final MMLogger logger = MMLogger.create(ParametersTableModel.class); - private final Map hashRows = new HashMap<>(); private final List rowValues = new ArrayList<>(); private final String[] columnNames = { "Name", "Value" }; - private final Class[] columnClasses = { String.class, Object.class }; - private record Row(String name, Object value) {} + private final Class[] columnClasses = { String.class, String.class }; + private record Row(String name, Object value, Class clazz) {} public ParametersTableModel() { } - public ParametersTableModel(Map parameters) { - this.hashRows.putAll(parameters); - for (Map.Entry entry : parameters.entrySet()) { - rowValues.add(new Row(entry.getKey(), entry.getValue())); - } + public void setParameters(Consideration consideration) { + setParameters(consideration.getParameters(), consideration.getParameterTypes()); } - public void setParameters(Map parameters) { - hashRows.clear(); + public void setEmptyParameters() { rowValues.clear(); - for (Map.Entry entry : parameters.entrySet()) { - hashRows.put(entry.getKey(), entry.getValue()); - rowValues.add(new Row(entry.getKey(), entry.getValue())); - } fireTableDataChanged(); } - public void addRow(String parameterName, Object value) { - if (hashRows.containsKey(parameterName)) { - logger.formattedErrorDialog("Parameter already exists", - "Could not add parameter {}, another parameters with the same name is already present.", parameterName); - return; - } - hashRows.put(parameterName, value); - rowValues.add(new Row(parameterName, value)); - fireTableRowsInserted(rowValues.size() - 1, rowValues.size() - 1); - } - - public String newParameterName() { - int i = 0; - while (hashRows.containsKey("Parameter " + i)) { - i++; - } - return "Parameter " + i; + private void setParameters(Map parameters, Map> parameterTypes) { + rowValues.clear(); + parameters.forEach((k, v) -> rowValues.add(new Row(k, v, parameterTypes.get(k)))); + fireTableDataChanged(); } public Map getParameters() { + Map hashRows = new HashMap<>(); + rowValues.forEach(row -> hashRows.put(row.name, row.value)); return hashRows; } @@ -96,7 +77,7 @@ public Class getColumnClass(int columnIndex) { @Override public boolean isCellEditable(int rowIndex, int columnIndex) { - return true; + return columnIndex == 1; } @Override @@ -108,21 +89,19 @@ public Object getValueAt(int rowIndex, int columnIndex) { return row.value; } + public Class getParameterValueAt(int rowIndex) { + return rowValues.get(rowIndex).clazz; + } + @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { var row = rowValues.get(rowIndex); if (columnIndex == 1) { - rowValues.set(rowIndex, new Row(row.name, aValue)); - hashRows.put(row.name, aValue); - } else { - if (hashRows.containsKey((String) aValue)) { - logger.formattedErrorDialog("Parameter already exists", - "Could not rename parameter %s, another parameters with the same name is already present.", aValue); - return; + if (aValue.getClass().equals(row.clazz)) { + rowValues.set(rowIndex, new Row(row.name, aValue, row.clazz)); + } else { + logger.error("Invalid value type: " + aValue.getClass() + " for " + row.clazz, "Invalid value type"); } - rowValues.set(rowIndex, new Row((String) aValue, row.value)); - hashRows.remove(row.name); - hashRows.put((String) aValue, row.value); } } } diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java b/megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java new file mode 100644 index 00000000000..285ab049708 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/SpinnerCellEditor.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import javax.swing.*; +import javax.swing.table.TableCellEditor; +import java.awt.*; + +public class SpinnerCellEditor extends AbstractCellEditor implements TableCellEditor { + private final JSpinner spinner; + + public SpinnerCellEditor(double defaultValue, double min, double max, double step) { + spinner = new JSpinner(new SpinnerNumberModel(min, min, max, step)); + spinner.setValue(defaultValue); + JComponent editor = spinner.getEditor(); + if (editor instanceof JSpinner.DefaultEditor) { + JFormattedTextField textField = ((JSpinner.DefaultEditor) editor).getTextField(); + textField.setHorizontalAlignment(JFormattedTextField.LEFT); + } + } + + @Override + public Object getCellEditorValue() { + return spinner.getValue(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + spinner.setValue(value); + return spinner; + } +} diff --git a/megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java b/megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java new file mode 100644 index 00000000000..7e2ef2e0487 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/ai/editor/TWConsiderationClass.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.client.ui.swing.ai.editor; + +import megamek.ai.utility.Consideration; +import megamek.client.bot.duchess.ai.utility.tw.considerations.*; + +public enum TWConsiderationClass { + + MyUnitArmor(MyUnitArmor.class), + MyUnitIsCrippled(MyUnitIsCrippled.class), + MyUnitUnderThreat(MyUnitUnderThreat.class), + TargetUnitsArmor(TargetUnitsArmor.class), + TargetUnitsHaveRole(TargetUnitsHaveRole.class), + MyUnitRoleIs(MyUnitRoleIs.class), + TargetWithinRange(TargetWithinRange.class), + TargetWithinOptimalRange(TargetWithinOptimalRange.class); + + private final Class> considerationClass; + + TWConsiderationClass(Class> considerationClass) { + this.considerationClass = considerationClass; + } + + public Class> getConsiderationClass() { + return considerationClass; + } + + public static TWConsiderationClass fromClass(Class considerationClass) { + for (TWConsiderationClass twConsiderationClass : values()) { + if (twConsiderationClass.considerationClass.equals(considerationClass)) { + return twConsiderationClass; + } + } + return null; + } +}