mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 03:08:02 +00:00
[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:
@@ -23,6 +23,10 @@
|
||||
<artifactId>forge-game</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -1584,5 +1584,12 @@ public class AiController {
|
||||
private List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -491,6 +491,10 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
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.
|
||||
* The list of chosen options (sa.getChosenList()) should be set by
|
||||
|
||||
@@ -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<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers();
|
||||
List<RegisteredPlayer> newPlayers = new ArrayList<>();
|
||||
|
||||
@@ -27,6 +27,7 @@ public class GameSimulator {
|
||||
private GameStateEvaluator eval;
|
||||
private List<String> 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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AbilitySub> plannedModes) {
|
||||
this.sa = sa;
|
||||
chooseTargetingSubAbility();
|
||||
chooseTargetingSubAbility(plannedModes);
|
||||
this.targetIndex = 0;
|
||||
this.validTargets = new ArrayList<GameObject>();
|
||||
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<AbilitySub> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class SimulationController {
|
||||
private Plan.Decision bestSequence; // last action of sequence
|
||||
private Score bestScore;
|
||||
private List<CachedEffect> effectCache = new ArrayList<CachedEffect>();
|
||||
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<Plan.Decision> sequence = new ArrayList<Plan.Decision>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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) {
|
||||
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<int[]> 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<String> uniqueCards = new HashSet<String>();
|
||||
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<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
|
||||
public Card chooseCard(CardCollection fetchList) {
|
||||
// Prune duplicates.
|
||||
HashSet<String> uniqueCards = new HashSet<String>();
|
||||
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<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
|
||||
if (interceptor != null) {
|
||||
List<AbilitySub> 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<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
|
||||
// TODO: Validate that there's no discrepancies between choices and modes?
|
||||
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;
|
||||
}
|
||||
}
|
||||
return plannedModes;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa,
|
||||
CardCollection fetchList, Player player2, Player decider) {
|
||||
if (interceptor != null) {
|
||||
@@ -422,8 +545,11 @@ public class SpellAbilityPicker {
|
||||
}
|
||||
|
||||
public interface Interceptor {
|
||||
public List<AbilitySub> chooseModesForAbility(List<AbilitySub> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user