[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.
This commit is contained in:
Myrd
2016-12-29 06:36:08 +00:00
parent c8ad6fe56a
commit 1d1b94e757
11 changed files with 424 additions and 145 deletions

View File

@@ -23,6 +23,10 @@
<artifactId>forge-game</artifactId> <artifactId>forge-game</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1584,5 +1584,12 @@ public class AiController {
private List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) { private List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
return filterList(input, SpellAbilityPredicates.isApi(type)); return filterList(input, SpellAbilityPredicates.isApi(type));
} }
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
if (simPicker != null) {
return simPicker.chooseModeForAbility(sa, min, num, allowRepeat);
}
return null;
}
} }

View File

@@ -85,6 +85,9 @@ import forge.util.collect.FCollection;
*/ */
public class ComputerUtil { public class ComputerUtil {
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game) { 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(); game.getStack().freezeStack();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -102,7 +105,9 @@ public class ComputerUtil {
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa); CharmEffect.makeChoices(sa);
} }
if (chooseTargets != null) {
chooseTargets.run();
}
if (sa.hasParam("Bestow")) { if (sa.hasParam("Bestow")) {
sa.getHostCard().animateBestow(); sa.getHostCard().animateBestow();
} }

View File

@@ -491,6 +491,10 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
List<AbilitySub> result = brains.chooseModeForAbility(sa, min, num, allowRepeat);
if (result != null) {
return result;
}
/** /**
* Called when CharmEffect resolves for the AI to select its choices. * Called when CharmEffect resolves for the AI to select its choices.
* The list of chosen options (sa.getChosenList()) should be set by * The list of chosen options (sa.getChosenList()) should be set by

View File

@@ -55,6 +55,14 @@ public class GameCopier {
this.origGame = origGame; this.origGame = origGame;
} }
public Game getOriginalGame() {
return origGame;
}
public Game getCopiedGame() {
return gameObjectMap.getGame();
}
public Game makeCopy() { public Game makeCopy() {
List<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers(); List<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers();
List<RegisteredPlayer> newPlayers = new ArrayList<>(); List<RegisteredPlayer> newPlayers = new ArrayList<>();

View File

@@ -27,6 +27,7 @@ public class GameSimulator {
private GameStateEvaluator eval; private GameStateEvaluator eval;
private List<String> origLines; private List<String> origLines;
private Score origScore; private Score origScore;
private Interceptor interceptor;
public GameSimulator(final SimulationController controller, final Game origGame, final Player origAiPlayer) { public GameSimulator(final SimulationController controller, final Game origGame, final Player origAiPlayer) {
this.controller = controller; this.controller = controller;
@@ -74,6 +75,7 @@ public class GameSimulator {
} }
public void setInterceptor(Interceptor interceptor) { public void setInterceptor(Interceptor interceptor) {
this.interceptor = interceptor;
((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor); ((PlayerControllerAi) aiPlayer.getController()).getAi().getSimulationPicker().setInterceptor(interceptor);
} }
@@ -173,7 +175,15 @@ public class GameSimulator {
} }
System.out.println(); 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. // TODO: Support multiple opponents.

View File

@@ -45,6 +45,8 @@ public class Plan {
final String sa; final String sa;
PossibleTargetSelector.Targets targets; PossibleTargetSelector.Targets targets;
String choice; String choice;
int[] modes;
String modesStr; // for human pretty-print consumption only
public Decision(Score initialScore, Decision prevDecision, SpellAbility sa) { public Decision(Score initialScore, Decision prevDecision, SpellAbility sa) {
this.initialScore = initialScore; this.initialScore = initialScore;
@@ -54,6 +56,14 @@ public class Plan {
this.choice = null; 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) { public Decision(Score initialScore, Decision prevDecision, PossibleTargetSelector.Targets targets) {
this.initialScore = initialScore; this.initialScore = initialScore;
this.prevDecision = prevDecision; this.prevDecision = prevDecision;
@@ -70,9 +80,30 @@ public class Plan {
this.choice = choice.getName(); 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 @Override
public String toString() { 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();
} }
} }
} }

View File

@@ -7,11 +7,11 @@ import java.util.List;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.game.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.SpellAbilityCondition;
import forge.game.spellability.TargetRestrictions; 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<AbilitySub> plannedModes) {
this.sa = sa; this.sa = sa;
chooseTargetingSubAbility(); chooseTargetingSubAbility(plannedModes);
this.targetIndex = 0; this.targetIndex = 0;
this.validTargets = new ArrayList<GameObject>(); this.validTargets = new ArrayList<GameObject>();
generateValidTargets(player); generateValidTargets(sa.getHostCard().getController());
} }
private void generateValidTargets(Player player) { private void generateValidTargets(Player player) {
@@ -141,26 +145,36 @@ public class PossibleTargetSelector {
return conditions == null || conditions.areMet(saOrSubSa); return conditions == null || conditions.areMet(saOrSubSa);
} }
private void chooseTargetingSubAbility() { private void chooseTargetingSubAbility(List<AbilitySub> plannedSubs) {
// TODO: This needs to handle case where multiple sub-abilities each have targets. // TODO: This needs to handle case where multiple sub-abilities each have targets.
SpellAbility saOrSubSa = sa;
int index = 0; int index = 0;
do { for (SpellAbility saOrSubSa = sa; saOrSubSa != null; saOrSubSa = saOrSubSa.getSubAbility()) {
if (saOrSubSa.usesTargeting() && conditionsAreMet(saOrSubSa)) { if (saOrSubSa.usesTargeting() && conditionsAreMet(saOrSubSa)) {
targetingSaIndex = index; targetingSaIndex = index;
targetingSa = saOrSubSa; targetingSa = saOrSubSa;
return; return;
} }
saOrSubSa = saOrSubSa.getSubAbility();
index++; 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() { public boolean hasPossibleTargets() {
return !validTargets.isEmpty(); return !validTargets.isEmpty();
} }
private void selectTargetsByIndex(int index) { private void selectTargetsByIndexImpl(int index) {
targetingSa.resetTargets(); targetingSa.resetTargets();
// TODO: smarter about multiple targets, etc... // TODO: smarter about multiple targets, etc...
@@ -190,11 +204,22 @@ public class PossibleTargetSelector {
return new Targets(targetingSaIndex, validTargets.size(), targetIndex - 1, targetingSa.getTargets().getTargetedString()); return new Targets(targetingSaIndex, validTargets.size(), targetIndex - 1, targetingSa.getTargets().getTargetedString());
} }
public boolean selectTargets(Targets targets) { public boolean selectTargetsByIndex(int targetIndex) {
if (targets.originalTargetCount != validTargets.size() || targets.targetingSaIndex != targetingSaIndex) { if (targetIndex >= validTargets.size()) {
return false; 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; return true;
} }
@@ -202,8 +227,12 @@ public class PossibleTargetSelector {
if (targetIndex >= validTargets.size()) { if (targetIndex >= validTargets.size()) {
return false; return false;
} }
selectTargetsByIndex(targetIndex); selectTargetsByIndexImpl(targetIndex);
targetIndex++; targetIndex++;
return true; return true;
} }
public int getValidTargetsSize() {
return validTargets.size();
}
} }

View File

@@ -19,6 +19,7 @@ public class SimulationController {
private Plan.Decision bestSequence; // last action of sequence private Plan.Decision bestSequence; // last action of sequence
private Score bestScore; private Score bestScore;
private List<CachedEffect> effectCache = new ArrayList<CachedEffect>(); private List<CachedEffect> effectCache = new ArrayList<CachedEffect>();
private GameObject[] currentHostAndTarget;
private static class CachedEffect { private static class CachedEffect {
final GameObject hostCard; final GameObject hostCard;
@@ -71,50 +72,12 @@ public class SimulationController {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), choice)); currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), choice));
} }
private GameObject[] getOriginalHostCardAndTarget(SpellAbility sa) { public void evaluateChosenModes(int[] chosenModes, String modesStr) {
SpellAbility saOrSubSa = sa; currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), chosenModes, modesStr));
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 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)); currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), targets));
return null;
} }
public void doneEvaluating(Score score) { public void doneEvaluating(Score score) {
@@ -130,6 +93,10 @@ public class SimulationController {
} }
public Plan getBestPlan() { public Plan getBestPlan() {
if (!currentStack.isEmpty()) {
throw new RuntimeException("getBestPlan() expects currentStack to be empty!");
}
ArrayList<Plan.Decision> sequence = new ArrayList<Plan.Decision>(); ArrayList<Plan.Decision> sequence = new ArrayList<Plan.Decision>();
Plan.Decision current = bestSequence; Plan.Decision current = bestSequence;
while (current != null) { while (current != null) {
@@ -148,12 +115,42 @@ public class SimulationController {
sequence.get(writeIndex - 1).targets = d.targets; sequence.get(writeIndex - 1).targets = d.targets;
} else if (d.choice != null) { } else if (d.choice != null) {
sequence.get(writeIndex - 1).choice = d.choice; 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(); sequence.subList(writeIndex, sequence.size()).clear();
return new Plan(sequence); 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) { public void push(SpellAbility sa, Score score, GameSimulator simulator) {
GameSimulator.debugPrint("Recursing DEPTH=" + getRecursionDepth()); GameSimulator.debugPrint("Recursing DEPTH=" + getRecursionDepth());
GameSimulator.debugPrint(" With: " + sa); GameSimulator.debugPrint(" With: " + sa);
@@ -167,8 +164,63 @@ public class SimulationController {
GameSimulator.debugPrint("DEPTH"+getRecursionDepth()+" best score " + score + " " + nextSa); 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) { public void possiblyCacheResult(Score score, SpellAbility sa) {
boolean cached = false; String cached = "";
// TODO: Why is the check below needed by tests? // TODO: Why is the check below needed by tests?
if (!currentStack.isEmpty()) { if (!currentStack.isEmpty()) {
@@ -179,28 +231,31 @@ public class SimulationController {
// recurse. // recurse.
if (scoreDelta <= 0 && d.targets != null) { if (scoreDelta <= 0 && d.targets != null) {
// FIXME: Support more than one target in this logic. // FIXME: Support more than one target in this logic.
GameObject[] hostAndTarget = getOriginalHostCardAndTarget(sa); GameObject[] hostAndTarget = currentHostAndTarget;
if (hostAndTarget != null) { if (currentHostAndTarget != null) {
GameStateEvaluator evaluator = new GameStateEvaluator(); GameStateEvaluator evaluator = new GameStateEvaluator();
Player player = sa.getActivatingPlayer(); Player player = sa.getActivatingPlayer();
int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null); int cardScore = evaluator.evalCard(player.getGame(), player, (Card) hostAndTarget[2], null);
effectCache.add(new CachedEffect(hostAndTarget[0], sa, hostAndTarget[1], cardScore, scoreDelta)); 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(); int recursionDepth = getRecursionDepth();
for (int i = 0; i < recursionDepth; i++) for (int i = 0; i < recursionDepth; i++)
System.err.print(" "); System.err.print(" ");
String choice = ""; String str;
if (!currentStack.isEmpty() && currentStack.get(currentStack.size() - 1).choice != null) { if (useStack && !currentStack.isEmpty()) {
choice = " -> " + currentStack.get(currentStack.size() - 1).choice; 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);
} }
} }

View File

@@ -1,24 +1,29 @@
package forge.ai.simulation; package forge.ai.simulation;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import org.apache.commons.math3.util.CombinatoricsUtils;
import forge.ai.AiPlayDecision; import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.ability.ChangeZoneAi; import forge.ai.ability.ChangeZoneAi;
import forge.ai.simulation.GameStateEvaluator.Score; import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.Ability; import forge.game.spellability.Ability;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.SpellAbilityCondition;
import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
public class SpellAbilityPicker { public class SpellAbilityPicker {
@@ -189,14 +194,15 @@ public class SpellAbilityPicker {
// TODO: Other safeguards like list of SAs and maybe the index and such? // TODO: Other safeguards like list of SAs and maybe the index and such?
for (final SpellAbility sa : availableSAs) { for (final SpellAbility sa : availableSAs) {
if (sa.toString().equals(decision.sa)) { if (sa.toString().equals(decision.sa)) {
if (decision.targets != null) { // If modes != null, targeting will be done in chooseModeForAbility().
PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa); if (decision.modes == null && decision.targets != null) {
PossibleTargetSelector selector = new PossibleTargetSelector(sa);
if (!selector.selectTargets(decision.targets)) { if (!selector.selectTargets(decision.targets)) {
badTargets = true; badTargets = true;
break; break;
} }
} }
print("Planned decision " + plan.getNextDecisionIndex() + ": " + abilityToString(sa) + " " + decision.choice); print("Planned decision " + plan.getNextDecisionIndex() + ": " + decision);
return sa; return sa;
} }
} }
@@ -301,87 +307,204 @@ public class SpellAbilityPicker {
return AiPlayDecision.WillPlay; return AiPlayDecision.WillPlay;
} }
private static List<AbilitySub> getModeCombination(List<AbilitySub> choices, int[] modeIndexes) {
ArrayList<AbilitySub> modes = new ArrayList<AbilitySub>();
for (int modeIndex : modeIndexes) {
modes.add(choices.get(modeIndex));
}
return modes;
}
private Score evaluateSa(final SimulationController controller, SpellAbility sa) { private Score evaluateSa(final SimulationController controller, SpellAbility sa) {
controller.evaluateSpellAbility(sa); controller.evaluateSpellAbility(sa);
Score bestScore = new Score(Integer.MIN_VALUE); Score bestScore = new Score(Integer.MIN_VALUE);
PossibleTargetSelector selector = new PossibleTargetSelector(game, player, sa);
if (!selector.hasPossibleTargets()) {
Interceptor interceptor = new Interceptor() { Interceptor interceptor = new Interceptor() {
private Iterator<int[]> modeIterator;
private int[] selectedModes;
private Score bestScoreForMode = new Score(Integer.MIN_VALUE);
private boolean advancedToNextMode;
private Score[] cachedTargetScores;
private int nextTarget = 0;
private Score bestScoreForTarget = new Score(Integer.MIN_VALUE);
private int numChoices = -1; private int numChoices = -1;
private int nextChoice = 0; private int nextChoice = 0;
private Card choice; private Card selectedChoice;
private Score bestScoreForChoice = new Score(Integer.MIN_VALUE);
public List<AbilitySub> chooseModesForAbility(List<AbilitySub> 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<AbilitySub> 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 @Override
public Card chooseCard(CardCollection fetchList) { public Card chooseCard(CardCollection fetchList) {
choice = null;
// Prune duplicates. // Prune duplicates.
HashSet<String> uniqueCards = new HashSet<String>(); HashSet<String> uniqueCards = new HashSet<String>();
for (int i = 0; i < fetchList.size(); i++) { for (int i = 0; i < fetchList.size(); i++) {
Card card = fetchList.get(i); Card card = fetchList.get(i);
if (uniqueCards.add(card.getName()) && uniqueCards.size() == nextChoice + 1) { if (uniqueCards.add(card.getName()) && uniqueCards.size() == nextChoice + 1) {
choice = card; selectedChoice = card;
} }
} }
numChoices = uniqueCards.size(); 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++; nextChoice++;
if (choice != null) { return true;
controller.evaluateCardChoice(choice);
} }
return choice; nextChoice = 0;
numChoices = -1;
} }
if (cachedTargetScores != null) {
@Override controller.doneEvaluating(bestScoreForTarget);
public Card getLastChoice() { bestScoreForTarget = new Score(Integer.MIN_VALUE);
return choice; while (nextTarget + 1 < cachedTargetScores.length) {
nextTarget++;
if (cachedTargetScores[nextTarget] == null) {
return true;
} }
}
@Override nextTarget = -1;
public boolean hasMoreChoices() { cachedTargetScores = null;
return nextChoice < numChoices; }
if (modeIterator != null) {
controller.doneEvaluating(bestScoreForMode);
bestScoreForMode = new Score(Integer.MIN_VALUE);
if (modeIterator.hasNext()) {
selectedModes = modeIterator.next();
advancedToNextMode = true;
return true;
}
modeIterator = null;
}
return false;
} }
}; };
Score lastScore = null;
do { do {
GameSimulator simulator = new GameSimulator(controller, game, player); GameSimulator simulator = new GameSimulator(controller, game, player);
simulator.setInterceptor(interceptor); simulator.setInterceptor(interceptor);
Score score = simulator.simulateSpellAbility(sa); lastScore = simulator.simulateSpellAbility(sa);
if (interceptor.getLastChoice() != null) { if (lastScore.value > bestScore.value) {
controller.doneEvaluating(score); bestScore = lastScore;
} }
if (score.value > bestScore.value) { } while (interceptor.advance(lastScore));
bestScore = score;
}
} while (interceptor.hasMoreChoices());
controller.doneEvaluating(bestScore); controller.doneEvaluating(bestScore);
return bestScore; return bestScore;
} }
TargetChoices tgt = null; public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
while (selector.selectNextTargets()) { if (interceptor != null) {
// Get estimated score from the controller if this SA/target pair has been seen before. List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
Score score = controller.evaluateTargetChoices(sa, selector.getLastSelectedTargets()); return interceptor.chooseModesForAbility(choices, min, num, allowRepeat);
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)");
} }
// TODO: Get rid of the below when no longer needed. if (plan != null && plan.getSelectedDecision() != null && plan.getSelectedDecision().modes != null) {
if (score.value > bestScore.value) { Plan.Decision decision = plan.getSelectedDecision();
bestScore = score; List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
tgt = sa.getTargets(); // TODO: Validate that there's no discrepancies between choices and modes?
sa.resetTargets(); List<AbilitySub> 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;
} }
} }
controller.doneEvaluating(bestScore); return plannedModes;
if (tgt != null) {
sa.setTargets(tgt);
} }
return bestScore; return null;
} }
public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa, public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa,
@@ -422,8 +545,11 @@ public class SpellAbilityPicker {
} }
public interface Interceptor { public interface Interceptor {
public List<AbilitySub> chooseModesForAbility(List<AbilitySub> choices, int min, int num, boolean allowRepeat);
public Card chooseCard(CardCollection fetchList); public Card chooseCard(CardCollection fetchList);
public Card getLastChoice(); public void chooseTargets(SpellAbility sa, GameSimulator simulator);
public boolean hasMoreChoices(); public Card getSelectedChoice();
public int[] getSelectModes();
public boolean advance(Score lastScore);
} }
} }

View File

@@ -522,7 +522,7 @@ public class GameSimulatorTest extends TestCase {
SpellAbility sa = findSAWithPrefix(ajani, "+1: Distribute"); SpellAbility sa = findSAWithPrefix(ajani, "+1: Distribute");
assertNotNull(sa); assertNotNull(sa);
PossibleTargetSelector selector = new PossibleTargetSelector(game, p, sa); PossibleTargetSelector selector = new PossibleTargetSelector(sa);
while (selector.selectNextTargets()) { while (selector.selectNextTargets()) {
GameSimulator sim = createSimulator(game, p); GameSimulator sim = createSimulator(game, p);
sim.simulateSpellAbility(sa); sim.simulateSpellAbility(sa);