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);