diff --git a/forge-ai/pom.xml b/forge-ai/pom.xml
index 69059a5bfb4..56985edff09 100644
--- a/forge-ai/pom.xml
+++ b/forge-ai/pom.xml
@@ -23,6 +23,10 @@
forge-game
${project.version}
-
+
+ org.apache.commons
+ commons-math3
+ 3.6.1
+
diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java
index 5ce5e3a75a8..c1e66d44445 100644
--- a/forge-ai/src/main/java/forge/ai/AiController.java
+++ b/forge-ai/src/main/java/forge/ai/AiController.java
@@ -1584,5 +1584,12 @@ public class AiController {
private List filterListByApi(List input, ApiType type) {
return filterList(input, SpellAbilityPredicates.isApi(type));
}
+
+ public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
+ if (simPicker != null) {
+ return simPicker.chooseModeForAbility(sa, min, num, allowRepeat);
+ }
+ return null;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
index c59b633bb13..240244828bb 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
@@ -85,6 +85,9 @@ import forge.util.collect.FCollection;
*/
public class ComputerUtil {
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game) {
+ return handlePlayingSpellAbility(ai, sa, game, null);
+ }
+ public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game, Runnable chooseTargets) {
game.getStack().freezeStack();
final Card source = sa.getHostCard();
@@ -102,7 +105,9 @@ public class ComputerUtil {
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa);
}
-
+ if (chooseTargets != null) {
+ chooseTargets.run();
+ }
if (sa.hasParam("Bestow")) {
sa.getHostCard().animateBestow();
}
diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
index 1b44466b1af..94bd4a8daa0 100644
--- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
+++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
@@ -491,6 +491,10 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
+ List result = brains.chooseModeForAbility(sa, min, num, allowRepeat);
+ if (result != null) {
+ return result;
+ }
/**
* Called when CharmEffect resolves for the AI to select its choices.
* The list of chosen options (sa.getChosenList()) should be set by
diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
index 303cf6cc959..aa51649a5a4 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
@@ -55,6 +55,14 @@ public class GameCopier {
this.origGame = origGame;
}
+ public Game getOriginalGame() {
+ return origGame;
+ }
+
+ public Game getCopiedGame() {
+ return gameObjectMap.getGame();
+ }
+
public Game makeCopy() {
List origPlayers = origGame.getMatch().getPlayers();
List newPlayers = new ArrayList<>();
diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java
index 54babd5269f..43a4cc89246 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java
@@ -27,6 +27,7 @@ public class GameSimulator {
private GameStateEvaluator eval;
private List origLines;
private Score origScore;
+ private Interceptor interceptor;
public GameSimulator(final SimulationController controller, final Game origGame, final Player origAiPlayer) {
this.controller = controller;
@@ -74,6 +75,7 @@ public class GameSimulator {
}
public void setInterceptor(Interceptor interceptor) {
+ this.interceptor = interceptor;
((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor);
}
@@ -173,7 +175,15 @@ public class GameSimulator {
}
System.out.println();
}
- ComputerUtil.handlePlayingSpellAbility(aiPlayer, sa, simGame);
+ final SpellAbility playingSa = sa;
+ ComputerUtil.handlePlayingSpellAbility(aiPlayer, sa, simGame, new Runnable() {
+ @Override
+ public void run() {
+ if (interceptor != null) {
+ interceptor.chooseTargets(playingSa, GameSimulator.this);
+ }
+ }
+ });
}
// TODO: Support multiple opponents.
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 cb77a5e7333..514dd1abbd9 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/Plan.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/Plan.java
@@ -45,6 +45,8 @@ public class Plan {
final String sa;
PossibleTargetSelector.Targets targets;
String choice;
+ int[] modes;
+ String modesStr; // for human pretty-print consumption only
public Decision(Score initialScore, Decision prevDecision, SpellAbility sa) {
this.initialScore = initialScore;
@@ -54,6 +56,14 @@ public class Plan {
this.choice = null;
}
+ public Decision(Score initialScore, Decision prevDecision, String saString) {
+ this.initialScore = initialScore;
+ this.prevDecision = prevDecision;
+ this.sa = saString;
+ this.targets = null;
+ this.choice = null;
+ }
+
public Decision(Score initialScore, Decision prevDecision, PossibleTargetSelector.Targets targets) {
this.initialScore = initialScore;
this.prevDecision = prevDecision;
@@ -70,9 +80,30 @@ public class Plan {
this.choice = choice.getName();
}
+ public Decision(Score initialScore, Decision prevDecision, int[] modes, String modesStr) {
+ this.initialScore = initialScore;
+ this.prevDecision = prevDecision;
+ this.sa = null;
+ this.targets = null;
+ this.choice = null;
+ this.modes = modes;
+ this.modesStr = modesStr;
+ }
+
@Override
public String toString() {
- return "[initScore=" + initialScore + " " + sa + " " + targets + " " + choice + "]";
+ StringBuilder sb = new StringBuilder();
+ sb.append("[initScore=").append(initialScore).append(" ");
+ if (modesStr != null) {
+ sb.append(modesStr);
+ } else {
+ sb.append(sa);
+ }
+ if (targets != null) {
+ sb.append(" (targets: ").append(targets).append(")");
+ }
+ sb.append("]");
+ return sb.toString();
}
}
}
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 263cecfa3fc..37cdece9501 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java
@@ -7,11 +7,11 @@ import java.util.List;
import com.google.common.collect.ArrayListMultimap;
import forge.ai.ComputerUtilCard;
-import forge.game.Game;
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;
@@ -48,12 +48,16 @@ public class PossibleTargetSelector {
}
}
- public PossibleTargetSelector(Game game, Player player, SpellAbility sa) {
+ public PossibleTargetSelector(SpellAbility sa) {
+ this(sa, null);
+ }
+
+ public PossibleTargetSelector(SpellAbility sa, List plannedModes) {
this.sa = sa;
- chooseTargetingSubAbility();
+ chooseTargetingSubAbility(plannedModes);
this.targetIndex = 0;
this.validTargets = new ArrayList();
- generateValidTargets(player);
+ generateValidTargets(sa.getHostCard().getController());
}
private void generateValidTargets(Player player) {
@@ -141,26 +145,36 @@ public class PossibleTargetSelector {
return conditions == null || conditions.areMet(saOrSubSa);
}
- private void chooseTargetingSubAbility() {
+ private void chooseTargetingSubAbility(List plannedSubs) {
// TODO: This needs to handle case where multiple sub-abilities each have targets.
- SpellAbility saOrSubSa = sa;
int index = 0;
- do {
+ for (SpellAbility saOrSubSa = sa; saOrSubSa != null; saOrSubSa = saOrSubSa.getSubAbility()) {
if (saOrSubSa.usesTargeting() && conditionsAreMet(saOrSubSa)) {
targetingSaIndex = index;
targetingSa = saOrSubSa;
return;
}
- saOrSubSa = saOrSubSa.getSubAbility();
index++;
- } while (saOrSubSa != null);
+ }
+ // 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();
}
- private void selectTargetsByIndex(int index) {
+ private void selectTargetsByIndexImpl(int index) {
targetingSa.resetTargets();
// TODO: smarter about multiple targets, etc...
@@ -190,11 +204,22 @@ public class PossibleTargetSelector {
return new Targets(targetingSaIndex, validTargets.size(), targetIndex - 1, targetingSa.getTargets().getTargetedString());
}
- public boolean selectTargets(Targets targets) {
- if (targets.originalTargetCount != validTargets.size() || targets.targetingSaIndex != targetingSaIndex) {
+ public boolean selectTargetsByIndex(int targetIndex) {
+ if (targetIndex >= validTargets.size()) {
return false;
}
- selectTargetsByIndex(targets.targetIndex);
+ selectTargetsByIndexImpl(targetIndex);
+ this.targetIndex = targetIndex + 1;
+ return true;
+ }
+
+ public boolean selectTargets(Targets targets) {
+ if (targets.originalTargetCount != validTargets.size() || targets.targetingSaIndex != targetingSaIndex) {
+ System.err.println("Expected: " + validTargets.size() + " " + targetingSaIndex + " got: " + targets.originalTargetCount + " " + targets.targetingSaIndex);
+ return false;
+ }
+ selectTargetsByIndexImpl(targets.targetIndex);
+ this.targetIndex = targets.targetIndex + 1;
return true;
}
@@ -202,8 +227,12 @@ public class PossibleTargetSelector {
if (targetIndex >= validTargets.size()) {
return false;
}
- selectTargetsByIndex(targetIndex);
+ selectTargetsByIndexImpl(targetIndex);
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 f8bf7db31b5..110d2aaa015 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/SimulationController.java
@@ -19,6 +19,7 @@ public class SimulationController {
private Plan.Decision bestSequence; // last action of sequence
private Score bestScore;
private List effectCache = new ArrayList();
+ private GameObject[] currentHostAndTarget;
private static class CachedEffect {
final GameObject hostCard;
@@ -71,50 +72,12 @@ public class SimulationController {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), choice));
}
- private GameObject[] getOriginalHostCardAndTarget(SpellAbility sa) {
- SpellAbility saOrSubSa = sa;
- do {
- if (saOrSubSa.usesTargeting()) {
- break;
- }
- saOrSubSa = saOrSubSa.getSubAbility();
- } while (saOrSubSa != null);
-
- if (saOrSubSa == null || saOrSubSa.getTargets() == null || saOrSubSa.getTargets().getTargets().size() != 1) {
- return null;
- }
- GameObject target = saOrSubSa.getTargets().getTargets().get(0);
- GameObject originalTarget = target;
- if (!(target instanceof Card)) { return null; }
- GameObject hostCard = sa.getHostCard();
- for (int i = simulatorStack.size() - 1; i >= 0; i--) {
- GameCopier copier = simulatorStack.get(i).getGameCopier();
- target = copier.reverseFind(target);
- hostCard = copier.reverseFind(hostCard);
- }
- return new GameObject[] { hostCard, target, originalTarget };
+ public void evaluateChosenModes(int[] chosenModes, String modesStr) {
+ currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), chosenModes, modesStr));
}
- public Score evaluateTargetChoices(SpellAbility sa, PossibleTargetSelector.Targets targets) {
- GameObject[] hostAndTarget = getOriginalHostCardAndTarget(sa);
- if (hostAndTarget != null) {
- String saString = sa.toString();
- for (CachedEffect effect : effectCache) {
- if (effect.hostCard == hostAndTarget[0] && effect.target == hostAndTarget[1] && effect.sa.equals(saString)) {
- GameStateEvaluator evaluator = new GameStateEvaluator();
- Player player = sa.getActivatingPlayer();
- int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null);
- if (cardScore == effect.targetScore) {
- Score currentScore = getCurrentScore();
- // TODO: summonSick score?
- return new Score(currentScore.value + effect.scoreDelta, currentScore.summonSickValue);
- }
- }
- }
- }
-
+ public void evaluateTargetChoices(SpellAbility sa, PossibleTargetSelector.Targets targets) {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), targets));
- return null;
}
public void doneEvaluating(Score score) {
@@ -130,6 +93,10 @@ public class SimulationController {
}
public Plan getBestPlan() {
+ if (!currentStack.isEmpty()) {
+ throw new RuntimeException("getBestPlan() expects currentStack to be empty!");
+ }
+
ArrayList sequence = new ArrayList();
Plan.Decision current = bestSequence;
while (current != null) {
@@ -148,12 +115,42 @@ public class SimulationController {
sequence.get(writeIndex - 1).targets = d.targets;
} else if (d.choice != null) {
sequence.get(writeIndex - 1).choice = d.choice;
+ } else if (d.modes != null) {
+ sequence.get(writeIndex - 1).modes = d.modes;
+ sequence.get(writeIndex - 1).modesStr = d.modesStr;
}
}
sequence.subList(writeIndex, sequence.size()).clear();
return new Plan(sequence);
}
-
+
+ private Plan.Decision getLastMergedDecision() {
+ PossibleTargetSelector.Targets targets = null;
+ String choice = null;
+ int[] modes = null;
+ String modesStr = null;
+
+ Plan.Decision d = currentStack.get(currentStack.size() - 1);
+ while (d.sa == null) {
+ if (d.targets != null) {
+ targets = d.targets;
+ } else if (d.choice != null) {
+ choice = d.choice;
+ } else if (d.modes != null) {
+ modes = d.modes;
+ modesStr = d.modesStr;
+ }
+ d = d.prevDecision;
+ }
+
+ Plan.Decision merged = new Plan.Decision(d.initialScore, d.prevDecision, d.sa);
+ merged.targets = targets;
+ merged.choice = choice;
+ merged.modes = modes;
+ merged.modesStr = modesStr;
+ return merged;
+ }
+
public void push(SpellAbility sa, Score score, GameSimulator simulator) {
GameSimulator.debugPrint("Recursing DEPTH=" + getRecursionDepth());
GameSimulator.debugPrint(" With: " + sa);
@@ -167,8 +164,63 @@ public class SimulationController {
GameSimulator.debugPrint("DEPTH"+getRecursionDepth()+" best score " + score + " " + nextSa);
}
+ public GameObject[] getOriginalHostCardAndTarget(SpellAbility sa) {
+ SpellAbility saOrSubSa = sa;
+ while (saOrSubSa != null && !saOrSubSa.usesTargeting()) {
+ saOrSubSa = saOrSubSa.getSubAbility();
+ }
+
+ if (saOrSubSa == null || saOrSubSa.getTargets() == null || saOrSubSa.getTargets().getTargets().size() != 1) {
+ return null;
+ }
+ GameObject target = saOrSubSa.getTargets().getTargets().get(0);
+ GameObject originalTarget = target;
+ if (!(target instanceof Card)) { return null; }
+ Card hostCard = sa.getHostCard();
+ for (int i = simulatorStack.size() - 1; i >= 0; i--) {
+ GameCopier copier = simulatorStack.get(i).getGameCopier();
+ if (copier.getCopiedGame() != hostCard.getGame()) {
+ throw new RuntimeException("Expected hostCard and copier game to match!");
+ }
+ if (copier.getCopiedGame() != ((Card) target).getGame()) {
+ throw new RuntimeException("Expected target and copier game to match!");
+ }
+ target = copier.reverseFind(target);
+ hostCard = (Card) copier.reverseFind(hostCard);
+ }
+ return new GameObject[] { hostCard, target, originalTarget };
+ }
+
+ public void setHostAndTarget(SpellAbility sa, GameSimulator simulator) {
+ simulatorStack.add(simulator);
+ currentHostAndTarget = getOriginalHostCardAndTarget(sa);
+ simulatorStack.remove(simulatorStack.size() - 1);
+ }
+
+ public Score shouldSkipTarget(SpellAbility sa, PossibleTargetSelector.Targets targets, GameSimulator simulator) {
+ simulatorStack.add(simulator);
+ GameObject[] hostAndTarget = getOriginalHostCardAndTarget(sa);
+ simulatorStack.remove(simulatorStack.size() - 1);
+ if (hostAndTarget != null) {
+ String saString = sa.toString();
+ for (CachedEffect effect : effectCache) {
+ if (effect.hostCard == hostAndTarget[0] && effect.target == hostAndTarget[1] && effect.sa.equals(saString)) {
+ GameStateEvaluator evaluator = new GameStateEvaluator();
+ Player player = sa.getActivatingPlayer();
+ int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null);
+ if (cardScore == effect.targetScore) {
+ Score currentScore = getCurrentScore();
+ // TODO: summonSick score?
+ return new Score(currentScore.value + effect.scoreDelta, currentScore.summonSickValue);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
public void possiblyCacheResult(Score score, SpellAbility sa) {
- boolean cached = false;
+ String cached = "";
// TODO: Why is the check below needed by tests?
if (!currentStack.isEmpty()) {
@@ -179,28 +231,31 @@ public class SimulationController {
// recurse.
if (scoreDelta <= 0 && d.targets != null) {
// FIXME: Support more than one target in this logic.
- GameObject[] hostAndTarget = getOriginalHostCardAndTarget(sa);
- if (hostAndTarget != null) {
+ GameObject[] hostAndTarget = currentHostAndTarget;
+ if (currentHostAndTarget != null) {
GameStateEvaluator evaluator = new GameStateEvaluator();
Player player = sa.getActivatingPlayer();
int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null);
effectCache.add(new CachedEffect(hostAndTarget[0], sa, hostAndTarget[1], cardScore, scoreDelta));
- cached = true;
+ cached = " (added to cache)";
}
}
}
- printState(score, sa, cached ? " (added to cache)" : "");
+ currentHostAndTarget = null;
+ printState(score, sa, cached, true);
}
- public void printState(Score score, SpellAbility origSa, String suffix) {
+ public void printState(Score score, SpellAbility origSa, String suffix, boolean useStack) {
int recursionDepth = getRecursionDepth();
for (int i = 0; i < recursionDepth; i++)
System.err.print(" ");
- String choice = "";
- if (!currentStack.isEmpty() && currentStack.get(currentStack.size() - 1).choice != null) {
- choice = " -> " + currentStack.get(currentStack.size() - 1).choice;
+ String str;
+ if (useStack && !currentStack.isEmpty()) {
+ str = getLastMergedDecision().toString();
+ } else {
+ str = SpellAbilityPicker.abilityToString(origSa);
}
- System.err.println(recursionDepth + ": [" + score.value + "] " + SpellAbilityPicker.abilityToString(origSa) + choice + suffix);
+ System.err.println(recursionDepth + ": [" + score.value + "] " + str + suffix);
}
}
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 a638084cacf..9238ecaf3d1 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
@@ -1,24 +1,29 @@
package forge.ai.simulation;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
+import org.apache.commons.math3.util.CombinatoricsUtils;
+
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.Game;
+import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.Ability;
+import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
-import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType;
public class SpellAbilityPicker {
@@ -189,14 +194,15 @@ public class SpellAbilityPicker {
// TODO: Other safeguards like list of SAs and maybe the index and such?
for (final SpellAbility sa : availableSAs) {
if (sa.toString().equals(decision.sa)) {
- if (decision.targets != null) {
- PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa);
+ // If modes != null, targeting will be done in chooseModeForAbility().
+ if (decision.modes == null && decision.targets != null) {
+ PossibleTargetSelector selector = new PossibleTargetSelector(sa);
if (!selector.selectTargets(decision.targets)) {
badTargets = true;
break;
}
}
- print("Planned decision " + plan.getNextDecisionIndex() + ": " + abilityToString(sa) + " " + decision.choice);
+ print("Planned decision " + plan.getNextDecisionIndex() + ": " + decision);
return sa;
}
}
@@ -301,89 +307,206 @@ public class SpellAbilityPicker {
return AiPlayDecision.WillPlay;
}
+ private static List getModeCombination(List choices, int[] modeIndexes) {
+ ArrayList modes = new ArrayList();
+ for (int modeIndex : modeIndexes) {
+ modes.add(choices.get(modeIndex));
+ }
+ return modes;
+ }
+
private Score evaluateSa(final SimulationController controller, SpellAbility sa) {
controller.evaluateSpellAbility(sa);
Score bestScore = new Score(Integer.MIN_VALUE);
- PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa);
- if (!selector.hasPossibleTargets()) {
- Interceptor interceptor = new Interceptor() {
- private int numChoices = -1;
- private int nextChoice = 0;
- private Card choice;
+ Interceptor interceptor = new Interceptor() {
+ private Iterator modeIterator;
+ private int[] selectedModes;
+ private Score bestScoreForMode = new Score(Integer.MIN_VALUE);
+ private boolean advancedToNextMode;
- @Override
- public Card chooseCard(CardCollection fetchList) {
- choice = null;
- // Prune duplicates.
- HashSet uniqueCards = new HashSet();
- for (int i = 0; i < fetchList.size(); i++) {
- Card card = fetchList.get(i);
- if (uniqueCards.add(card.getName()) && uniqueCards.size() == nextChoice + 1) {
- choice = card;
+ private Score[] cachedTargetScores;
+ private int nextTarget = 0;
+ private Score bestScoreForTarget = new Score(Integer.MIN_VALUE);
+
+ private int numChoices = -1;
+ private int nextChoice = 0;
+ private Card selectedChoice;
+ private Score bestScoreForChoice = new Score(Integer.MIN_VALUE);
+
+ public List chooseModesForAbility(List choices, int min, int num, boolean allowRepeat) {
+ if (modeIterator == null) {
+ // TODO: Below doesn't support allowRepeat!
+ modeIterator = CombinatoricsUtils.combinationsIterator(choices.size(), num);
+ selectedModes = modeIterator.next();
+ advancedToNextMode = true;
+ }
+ // Note: If modeIterator already existed, selectedModes would have been updated in advance().
+ List result = getModeCombination(choices, selectedModes);
+ if (advancedToNextMode) {
+ StringBuilder sb = new StringBuilder();
+ for (AbilitySub sub : result) {
+ if (sb.length() > 0) {
+ sb.append(" ");
+ }
+ sb.append(sub);
+ }
+ controller.evaluateChosenModes(selectedModes, sb.toString());
+ advancedToNextMode = false;
+ }
+ return result;
+ }
+
+ @Override
+ public Card chooseCard(CardCollection fetchList) {
+ // Prune duplicates.
+ HashSet uniqueCards = new HashSet();
+ for (int i = 0; i < fetchList.size(); i++) {
+ Card card = fetchList.get(i);
+ if (uniqueCards.add(card.getName()) && uniqueCards.size() == nextChoice + 1) {
+ selectedChoice = card;
+ }
+ }
+ numChoices = uniqueCards.size();
+ if (selectedChoice != null) {
+ controller.evaluateCardChoice(selectedChoice);
+ }
+ return selectedChoice;
+ }
+
+ @Override
+ public void chooseTargets(SpellAbility sa, GameSimulator simulator) {
+ // 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);
+ if (selector.hasPossibleTargets()) {
+ if (cachedTargetScores == null) {
+ cachedTargetScores = new Score[selector.getValidTargetsSize()];
+ 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);
+ } else if (nextTarget == -1) {
+ nextTarget = i;
+ }
+ }
+ // 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
+ // flow. :(
+ if (nextTarget == -1) { nextTarget = 0; }
+ }
+ selector.selectTargetsByIndex(nextTarget);
+ controller.setHostAndTarget(sa, simulator);
+ // The hierarchy is modes -> targets -> choices. In the presence of multiple choices, we want to call
+ // evaluate just once at the top level. We can do this by only calling when numChoices is -1.
+ if (numChoices == -1) {
+ controller.evaluateTargetChoices(sa, selector.getLastSelectedTargets());
+ }
+ return;
+ }
+ }
+
+ @Override
+ public Card getSelectedChoice() {
+ return selectedChoice;
+ }
+
+ @Override
+ public int[] getSelectModes() {
+ return selectedModes;
+ }
+
+ @Override
+ public boolean advance(Score lastScore) {
+ if (lastScore.value > bestScoreForChoice.value) {
+ bestScoreForChoice = lastScore;
+ }
+ if (lastScore.value > bestScoreForTarget.value) {
+ bestScoreForTarget = lastScore;
+ }
+ if (lastScore.value > bestScoreForMode.value) {
+ bestScoreForMode = lastScore;
+ }
+
+ if (numChoices != -1) {
+ if (selectedChoice != null) {
+ controller.doneEvaluating(bestScoreForChoice);
+ }
+ bestScoreForChoice = new Score(Integer.MIN_VALUE);
+ selectedChoice = null;
+ if (nextChoice + 1 < numChoices) {
+ nextChoice++;
+ return true;
+ }
+ nextChoice = 0;
+ numChoices = -1;
+ }
+ if (cachedTargetScores != null) {
+ controller.doneEvaluating(bestScoreForTarget);
+ bestScoreForTarget = new Score(Integer.MIN_VALUE);
+ while (nextTarget + 1 < cachedTargetScores.length) {
+ nextTarget++;
+ if (cachedTargetScores[nextTarget] == null) {
+ return true;
}
}
- numChoices = uniqueCards.size();
- nextChoice++;
- if (choice != null) {
- controller.evaluateCardChoice(choice);
+ nextTarget = -1;
+ cachedTargetScores = null;
+ }
+ if (modeIterator != null) {
+ controller.doneEvaluating(bestScoreForMode);
+ bestScoreForMode = new Score(Integer.MIN_VALUE);
+ if (modeIterator.hasNext()) {
+ selectedModes = modeIterator.next();
+ advancedToNextMode = true;
+ return true;
}
- return choice;
+ modeIterator = null;
}
-
- @Override
- public Card getLastChoice() {
- return choice;
- }
-
- @Override
- public boolean hasMoreChoices() {
- return nextChoice < numChoices;
- }
- };
-
- do {
- GameSimulator simulator = new GameSimulator(controller, game, player);
- simulator.setInterceptor(interceptor);
- Score score = simulator.simulateSpellAbility(sa);
- if (interceptor.getLastChoice() != null) {
- controller.doneEvaluating(score);
- }
- if (score.value > bestScore.value) {
- bestScore = score;
- }
- } while (interceptor.hasMoreChoices());
- controller.doneEvaluating(bestScore);
- return bestScore;
- }
-
- TargetChoices tgt = null;
- while (selector.selectNextTargets()) {
- // Get estimated score from the controller if this SA/target pair has been seen before.
- Score score = controller.evaluateTargetChoices(sa, selector.getLastSelectedTargets());
- if (score == null) {
- // First time we see this, evaluate!
- GameSimulator simulator = new GameSimulator(controller, game, player);
- score = simulator.simulateSpellAbility(sa);
- controller.doneEvaluating(score);
- } else {
- controller.printState(score, sa, " - via estimate (skipped)");
+ return false;
}
- // TODO: Get rid of the below when no longer needed.
- if (score.value > bestScore.value) {
- bestScore = score;
- tgt = sa.getTargets();
- sa.resetTargets();
+ };
+
+ Score lastScore = null;
+ do {
+ GameSimulator simulator = new GameSimulator(controller, game, player);
+ simulator.setInterceptor(interceptor);
+ lastScore = simulator.simulateSpellAbility(sa);
+ if (lastScore.value > bestScore.value) {
+ bestScore = lastScore;
}
- }
+ } while (interceptor.advance(lastScore));
controller.doneEvaluating(bestScore);
-
- if (tgt != null) {
- sa.setTargets(tgt);
- }
return bestScore;
}
+ public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
+ if (interceptor != null) {
+ List choices = CharmEffect.makePossibleOptions(sa);
+ return interceptor.chooseModesForAbility(choices, min, num, allowRepeat);
+ }
+ if (plan != null && plan.getSelectedDecision() != null && plan.getSelectedDecision().modes != null) {
+ Plan.Decision decision = plan.getSelectedDecision();
+ List choices = CharmEffect.makePossibleOptions(sa);
+ // TODO: Validate that there's no discrepancies between choices and modes?
+ List plannedModes = getModeCombination(choices, decision.modes);
+ if (plan.getSelectedDecision().targets != null) {
+ PossibleTargetSelector selector = new PossibleTargetSelector(sa, plannedModes);
+ if (!selector.selectTargets(decision.targets)) {
+ print("Failed to continue planned action (" + decision.sa + "). Cause:");
+ print(" Bad targets for modes!");
+ return null;
+ }
+ }
+ return plannedModes;
+ }
+ return null;
+ }
+
public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List origin, SpellAbility sa,
CardCollection fetchList, Player player2, Player decider) {
if (interceptor != null) {
@@ -422,8 +545,11 @@ public class SpellAbilityPicker {
}
public interface Interceptor {
+ public List chooseModesForAbility(List choices, int min, int num, boolean allowRepeat);
public Card chooseCard(CardCollection fetchList);
- public Card getLastChoice();
- public boolean hasMoreChoices();
+ public void chooseTargets(SpellAbility sa, GameSimulator simulator);
+ public Card getSelectedChoice();
+ public int[] getSelectModes();
+ public boolean advance(Score lastScore);
}
}
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 dcfee42634d..0f2c6940171 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
@@ -522,7 +522,7 @@ public class GameSimulatorTest extends TestCase {
SpellAbility sa = findSAWithPrefix(ajani, "+1: Distribute");
assertNotNull(sa);
- PossibleTargetSelector selector = new PossibleTargetSelector(game, p, sa);
+ PossibleTargetSelector selector = new PossibleTargetSelector(sa);
while (selector.selectNextTargets()) {
GameSimulator sim = createSimulator(game, p);
sim.simulateSpellAbility(sa);