diff --git a/.gitattributes b/.gitattributes index b2efcf6adf9..f9bdfe9c9ed 100644 --- a/.gitattributes +++ b/.gitattributes @@ -158,6 +158,7 @@ forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java -text forge-ai/src/main/java/forge/ai/simulation/GameCopier.java -text forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java -text forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java -text +forge-ai/src/main/java/forge/ai/simulation/MultiTargetSelector.java -text forge-ai/src/main/java/forge/ai/simulation/Plan.java -text forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java -text forge-ai/src/main/java/forge/ai/simulation/SimulationController.java -text diff --git a/forge-ai/src/main/java/forge/ai/simulation/MultiTargetSelector.java b/forge-ai/src/main/java/forge/ai/simulation/MultiTargetSelector.java new file mode 100644 index 00000000000..5e09ddb8f9a --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/simulation/MultiTargetSelector.java @@ -0,0 +1,134 @@ +package forge.ai.simulation; + +import java.util.ArrayList; +import java.util.List; + +import forge.game.spellability.AbilitySub; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.SpellAbilityCondition; + +public class MultiTargetSelector { + public static class Targets { + private ArrayList targets; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (PossibleTargetSelector.Targets tgt : targets) { + if (sb.length() != 0) { + sb.append(", "); + } + sb.append(tgt.toString()); + } + return sb.toString(); + } + } + + private List selectors; + private List targetingSAs; + private int currentIndex; + + public MultiTargetSelector(SpellAbility sa, List plannedSubs) { + targetingSAs = getTargetingSAs(sa, plannedSubs); + selectors = new ArrayList<>(targetingSAs.size()); + for (int i = 0; i < targetingSAs.size(); i++) { + selectors.add(new PossibleTargetSelector(sa, targetingSAs.get(i), i)); + } + currentIndex = -1; + } + + public boolean hasPossibleTargets() { + if (targetingSAs.isEmpty()) { + return false; + } + for (PossibleTargetSelector selector : selectors) { + if (!selector.hasPossibleTargets()) { + return false; + } + } + return true; + } + + public Targets getLastSelectedTargets() { + Targets targets = new Targets(); + targets.targets = new ArrayList<>(selectors.size()); + for (int i = 0; i < selectors.size(); i++) { + targets.targets.add(selectors.get(i).getLastSelectedTargets()); + } + return targets; + } + + public boolean selectTargets(Targets targets) { + if (targets.targets.size() != selectors.size()) { + return false; + } + for (int i = 0; i < selectors.size(); i++) { + selectors.get(i).reset(); + if (!selectors.get(i).selectTargets(targets.targets.get(i))) { + return false; + } + } + return true; + } + + public void reset() { + for (PossibleTargetSelector selector : selectors) { + selector.reset(); + } + currentIndex = -1; + } + + public void selectTargetsByIndex(int i) { + if (i < currentIndex) { + reset(); + } + while (currentIndex < i) { + selectNextTargets(); + } + } + + public boolean selectNextTargets() { + if (currentIndex == -1) { + for (PossibleTargetSelector selector : selectors) { + if (!selector.selectNextTargets()) { + return false; + } + } + currentIndex = 0; + return true; + } + for (int i = selectors.size() - 1; i >= 0; i--) { + if (selectors.get(i).selectNextTargets()) { + currentIndex++; + return true; + } + selectors.get(i).reset(); + selectors.get(i).selectNextTargets(); + } + return false; + } + + private static boolean conditionsAreMet(SpellAbility saOrSubSa) { + SpellAbilityCondition conditions = saOrSubSa.getConditions(); + return conditions == null || conditions.areMet(saOrSubSa); + } + + private List getTargetingSAs(SpellAbility sa, List plannedSubs) { + List result = new ArrayList<>(); + for (SpellAbility saOrSubSa = sa; saOrSubSa != null; saOrSubSa = saOrSubSa.getSubAbility()) { + if (saOrSubSa.usesTargeting() && conditionsAreMet(saOrSubSa)) { + result.add(saOrSubSa); + } + } + // When plannedSubs is provided, also consider them even though they've not yet been added to the + // sub-ability chain. This is the case when we're choosing modes for a charm-style effect. + if (plannedSubs != null) { + for (AbilitySub sub : plannedSubs) { + if (sub.usesTargeting() && conditionsAreMet(sub)) { + result.add(sub); + } + } + } + return result; + } +} diff --git a/forge-ai/src/main/java/forge/ai/simulation/Plan.java b/forge-ai/src/main/java/forge/ai/simulation/Plan.java index 9bb5efbf0ac..78ff5a08a13 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/Plan.java +++ b/forge-ai/src/main/java/forge/ai/simulation/Plan.java @@ -75,7 +75,7 @@ public class Plan { final Score initialScore; final SpellAbilityRef saRef; - PossibleTargetSelector.Targets targets; + MultiTargetSelector.Targets targets; String choice; int[] modes; String modesStr; // for human pretty-print consumption only @@ -88,7 +88,7 @@ public class Plan { this.choice = null; } - public Decision(Score initialScore, Decision prevDecision, PossibleTargetSelector.Targets targets) { + public Decision(Score initialScore, Decision prevDecision, MultiTargetSelector.Targets targets) { this.initialScore = initialScore; this.prevDecision = prevDecision; this.saRef = null; diff --git a/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java b/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java index 78c97814ace..205fb65342e 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java +++ b/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java @@ -11,9 +11,7 @@ import forge.game.GameObject; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.player.Player; -import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; -import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.TargetRestrictions; public class PossibleTargetSelector { @@ -48,15 +46,17 @@ public class PossibleTargetSelector { } } - public PossibleTargetSelector(SpellAbility sa) { - this(sa, null); + public PossibleTargetSelector(SpellAbility sa, SpellAbility targetingSa, int targetingSaIndex) { + this.sa = sa; + this.targetingSa = targetingSa; + this.targetingSaIndex = targetingSaIndex; + this.validTargets = new ArrayList(); + generateValidTargets(sa.getHostCard().getController()); } - public PossibleTargetSelector(SpellAbility sa, List plannedModes) { - this.sa = sa; - chooseTargetingSubAbility(plannedModes); - this.targetIndex = 0; - this.validTargets = new ArrayList(); + public void reset() { + targetIndex = 0; + validTargets.clear(); generateValidTargets(sa.getHostCard().getController()); } @@ -140,36 +140,6 @@ public class PossibleTargetSelector { } } - private static boolean conditionsAreMet(SpellAbility saOrSubSa) { - SpellAbilityCondition conditions = saOrSubSa.getConditions(); - return conditions == null || conditions.areMet(saOrSubSa); - } - - private void chooseTargetingSubAbility(List plannedSubs) { - // TODO: This needs to handle case where multiple sub-abilities each have targets. - int index = 0; - for (SpellAbility saOrSubSa = sa; saOrSubSa != null; saOrSubSa = saOrSubSa.getSubAbility()) { - if (saOrSubSa.usesTargeting() && conditionsAreMet(saOrSubSa)) { - targetingSaIndex = index; - targetingSa = saOrSubSa; - return; - } - index++; - } - // When plannedSubs is provided, also consider them even though they've not yet been added to the - // sub-ability chain. This is the case when we're choosing modes for a charm-style effect. - if (plannedSubs != null) { - for (AbilitySub sub : plannedSubs) { - if (sub.usesTargeting() && conditionsAreMet(sub)) { - targetingSaIndex = index; - targetingSa = sub; - return; - } - index++; - } - } - } - public boolean hasPossibleTargets() { return !validTargets.isEmpty(); } @@ -177,7 +147,6 @@ public class PossibleTargetSelector { private void selectTargetsByIndexImpl(int index) { targetingSa.resetTargets(); - // TODO: smarter about multiple targets, etc... while (targetingSa.getTargets().getNumTargeted() < maxTargets && index < validTargets.size()) { targetingSa.getTargets().add(validTargets.get(index++)); } @@ -231,8 +200,4 @@ public class PossibleTargetSelector { targetIndex++; return true; } - - public int getValidTargetsSize() { - return validTargets.size(); - } } diff --git a/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java b/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java index e093899a881..999dae5398a 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java @@ -76,7 +76,7 @@ public class SimulationController { currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), chosenModes, modesStr)); } - public void evaluateTargetChoices(SpellAbility sa, PossibleTargetSelector.Targets targets) { + public void evaluateTargetChoices(SpellAbility sa, MultiTargetSelector.Targets targets) { currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), targets)); } @@ -125,7 +125,7 @@ public class SimulationController { } private Plan.Decision getLastMergedDecision() { - PossibleTargetSelector.Targets targets = null; + MultiTargetSelector.Targets targets = null; String choice = null; int[] modes = null; String modesStr = null; @@ -204,7 +204,7 @@ public class SimulationController { simulatorStack.remove(simulatorStack.size() - 1); } - public Score shouldSkipTarget(SpellAbility sa, PossibleTargetSelector.Targets targets, GameSimulator simulator) { + public Score shouldSkipTarget(SpellAbility sa, GameSimulator simulator) { simulatorStack.add(simulator); GameObject[] hostAndTarget = getOriginalHostCardAndTarget(sa); simulatorStack.remove(simulatorStack.size() - 1); diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityChoicesIterator.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityChoicesIterator.java index fe0a59a45f8..95c61012208 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityChoicesIterator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityChoicesIterator.java @@ -22,7 +22,7 @@ public class SpellAbilityChoicesIterator { private Score bestScoreForMode = new Score(Integer.MIN_VALUE); private boolean advancedToNextMode; - private Score[] cachedTargetScores; + private ArrayList cachedTargetScores; private int nextTarget = 0; private Score bestScoreForTarget = new Score(Integer.MIN_VALUE); @@ -88,20 +88,21 @@ public class SpellAbilityChoicesIterator { // Note: Can't just keep a TargetSelector object cached because it's // responsible for setting state on a SA and the SA object changes each // time since it's a different simulation. - PossibleTargetSelector selector = new PossibleTargetSelector(sa); + MultiTargetSelector selector = new MultiTargetSelector(sa, null); if (selector.hasPossibleTargets()) { if (cachedTargetScores == null) { - cachedTargetScores = new Score[selector.getValidTargetsSize()]; + cachedTargetScores = new ArrayList<>(); nextTarget = -1; - for (int i = 0; i < cachedTargetScores.length; i++) { - selector.selectTargetsByIndex(i); - cachedTargetScores[i] = controller.shouldSkipTarget(sa, selector.getLastSelectedTargets(), simulator); - if (cachedTargetScores[i] != null) { - controller.printState(cachedTargetScores[i], sa, " - via estimate (skipped)", false); + for (int i = 0; selector.selectNextTargets(); i++) { + Score score = controller.shouldSkipTarget(sa, simulator); + cachedTargetScores.add(score); + if (score != null) { + controller.printState(score, sa, " - via estimate (skipped)", false); } else if (nextTarget == -1) { nextTarget = i; } } + selector.reset(); // If all targets were cached, we unfortunately have to evaluate the first target again // because at this point we're already running the simulation code and there's no turning // back. This used to be not possible when the PossibleTargetSelector was controlling the @@ -154,9 +155,9 @@ public class SpellAbilityChoicesIterator { if (cachedTargetScores != null) { controller.doneEvaluating(bestScoreForTarget); bestScoreForTarget = new Score(Integer.MIN_VALUE); - while (nextTarget + 1 < cachedTargetScores.length) { + while (nextTarget + 1 < cachedTargetScores.size()) { nextTarget++; - if (cachedTargetScores[nextTarget] == null) { + if (cachedTargetScores.get(nextTarget) == null) { return true; } } diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java index 34eb6bef2aa..b091782cca6 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java @@ -203,7 +203,7 @@ public class SpellAbilityPicker { } // If modes != null, targeting will be done in chooseModeForAbility(). if (decision.modes == null && decision.targets != null) { - PossibleTargetSelector selector = new PossibleTargetSelector(sa); + MultiTargetSelector selector = new MultiTargetSelector(sa, null); if (!selector.selectTargets(decision.targets)) { printPlannedActionFailure(decision, "Bad targets"); return null; @@ -334,7 +334,7 @@ public class SpellAbilityPicker { // TODO: Validate that there's no discrepancies between choices and modes? List plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes); if (plan.getSelectedDecision().targets != null) { - PossibleTargetSelector selector = new PossibleTargetSelector(sa, plannedModes); + MultiTargetSelector selector = new MultiTargetSelector(sa, plannedModes); if (!selector.selectTargets(decision.targets)) { printPlannedActionFailure(decision, "Bad targets for modes"); return null; diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java index 891574eec5d..09a4d861246 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulatorTest.java @@ -421,7 +421,7 @@ public class GameSimulatorTest extends SimulationTestCase { assertNotNull(sa); sa.setActivatingPlayer(p); - PossibleTargetSelector selector = new PossibleTargetSelector(sa); + MultiTargetSelector selector = new MultiTargetSelector(sa, null); while (selector.selectNextTargets()) { GameSimulator sim = createSimulator(game, p); sim.simulateSpellAbility(sa); diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java index 90a988cec1e..950a616c4b8 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerTest.java @@ -171,4 +171,30 @@ public class SpellAbilityPickerTest extends SimulationTestCase { String expected = "Fiery Confluence -> " + dmgOppStr + " " + dmgOppStr + " " + dmgOppStr; assertEquals(expected, picker.getPlan().getDecisions().get(0).modesStr); } + + public void testMultipleTargets() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCard("Mountain", p); + addCard("Mountain", p); + Card spell = addCardToZone("Arc Trail", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + Card bear = addCard("Runeclaw Bear", opponent); + Card men = addCard("Flying Men", opponent); + opponent.setLife(20, null); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + assertEquals(spell.getSpellAbilities().get(0), sa); + assertEquals(bear, sa.getTargetCard()); + assertEquals("2", sa.getParam("NumDmg")); + SpellAbility subSa = sa.getSubAbility(); + assertEquals(men, subSa.getTargetCard()); + assertEquals("1", subSa.getParam("NumDmg")); + } }