From 1d1b94e75704d9e1b514960ad25542648d616a3c Mon Sep 17 00:00:00 2001 From: Myrd Date: Thu, 29 Dec 2016 06:36:08 +0000 Subject: [PATCH] [Simulated AI] Teach simulated AI how to choose card modes (e.g. on Charms and Commands). Note: There's still a limitation in the simulated AI where it doesn't know what to do when multiple effects from an ability require targets. This can be addressed in the future to support things like Cryptic Command to both counter a spell and bounce a permanent. --- forge-ai/pom.xml | 6 +- .../src/main/java/forge/ai/AiController.java | 7 + .../src/main/java/forge/ai/ComputerUtil.java | 7 +- .../java/forge/ai/PlayerControllerAi.java | 4 + .../java/forge/ai/simulation/GameCopier.java | 8 + .../forge/ai/simulation/GameSimulator.java | 12 +- .../main/java/forge/ai/simulation/Plan.java | 33 ++- .../ai/simulation/PossibleTargetSelector.java | 57 +++- .../ai/simulation/SimulationController.java | 159 ++++++---- .../ai/simulation/SpellAbilityPicker.java | 274 +++++++++++++----- .../ai/simulation/GameSimulatorTest.java | 2 +- 11 files changed, 424 insertions(+), 145 deletions(-) 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);