[Simulated AI] Add support for cards that need multiple targets.

Note: Currently, no special optimizations are made to try to prune decision trees for these, even though they definitely can result in a lot of choices and really slow simulation AI performance.
This commit is contained in:
Myrd
2017-01-08 21:02:24 +00:00
parent 94a5c12604
commit 88634bb0a0
9 changed files with 189 additions and 62 deletions

1
.gitattributes vendored
View File

@@ -158,6 +158,7 @@ forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java -text
forge-ai/src/main/java/forge/ai/simulation/GameCopier.java -text forge-ai/src/main/java/forge/ai/simulation/GameCopier.java -text
forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java -text forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java -text
forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java -text forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java -text
forge-ai/src/main/java/forge/ai/simulation/MultiTargetSelector.java -text
forge-ai/src/main/java/forge/ai/simulation/Plan.java -text forge-ai/src/main/java/forge/ai/simulation/Plan.java -text
forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java -text forge-ai/src/main/java/forge/ai/simulation/PossibleTargetSelector.java -text
forge-ai/src/main/java/forge/ai/simulation/SimulationController.java -text forge-ai/src/main/java/forge/ai/simulation/SimulationController.java -text

View File

@@ -0,0 +1,134 @@
package forge.ai.simulation;
import java.util.ArrayList;
import java.util.List;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
public class MultiTargetSelector {
public static class Targets {
private ArrayList<PossibleTargetSelector.Targets> targets;
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (PossibleTargetSelector.Targets tgt : targets) {
if (sb.length() != 0) {
sb.append(", ");
}
sb.append(tgt.toString());
}
return sb.toString();
}
}
private List<PossibleTargetSelector> selectors;
private List<SpellAbility> targetingSAs;
private int currentIndex;
public MultiTargetSelector(SpellAbility sa, List<AbilitySub> plannedSubs) {
targetingSAs = getTargetingSAs(sa, plannedSubs);
selectors = new ArrayList<>(targetingSAs.size());
for (int i = 0; i < targetingSAs.size(); i++) {
selectors.add(new PossibleTargetSelector(sa, targetingSAs.get(i), i));
}
currentIndex = -1;
}
public boolean hasPossibleTargets() {
if (targetingSAs.isEmpty()) {
return false;
}
for (PossibleTargetSelector selector : selectors) {
if (!selector.hasPossibleTargets()) {
return false;
}
}
return true;
}
public Targets getLastSelectedTargets() {
Targets targets = new Targets();
targets.targets = new ArrayList<>(selectors.size());
for (int i = 0; i < selectors.size(); i++) {
targets.targets.add(selectors.get(i).getLastSelectedTargets());
}
return targets;
}
public boolean selectTargets(Targets targets) {
if (targets.targets.size() != selectors.size()) {
return false;
}
for (int i = 0; i < selectors.size(); i++) {
selectors.get(i).reset();
if (!selectors.get(i).selectTargets(targets.targets.get(i))) {
return false;
}
}
return true;
}
public void reset() {
for (PossibleTargetSelector selector : selectors) {
selector.reset();
}
currentIndex = -1;
}
public void selectTargetsByIndex(int i) {
if (i < currentIndex) {
reset();
}
while (currentIndex < i) {
selectNextTargets();
}
}
public boolean selectNextTargets() {
if (currentIndex == -1) {
for (PossibleTargetSelector selector : selectors) {
if (!selector.selectNextTargets()) {
return false;
}
}
currentIndex = 0;
return true;
}
for (int i = selectors.size() - 1; i >= 0; i--) {
if (selectors.get(i).selectNextTargets()) {
currentIndex++;
return true;
}
selectors.get(i).reset();
selectors.get(i).selectNextTargets();
}
return false;
}
private static boolean conditionsAreMet(SpellAbility saOrSubSa) {
SpellAbilityCondition conditions = saOrSubSa.getConditions();
return conditions == null || conditions.areMet(saOrSubSa);
}
private List<SpellAbility> getTargetingSAs(SpellAbility sa, List<AbilitySub> plannedSubs) {
List<SpellAbility> result = new ArrayList<>();
for (SpellAbility saOrSubSa = sa; saOrSubSa != null; saOrSubSa = saOrSubSa.getSubAbility()) {
if (saOrSubSa.usesTargeting() && conditionsAreMet(saOrSubSa)) {
result.add(saOrSubSa);
}
}
// 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)) {
result.add(sub);
}
}
}
return result;
}
}

View File

@@ -75,7 +75,7 @@ public class Plan {
final Score initialScore; final Score initialScore;
final SpellAbilityRef saRef; final SpellAbilityRef saRef;
PossibleTargetSelector.Targets targets; MultiTargetSelector.Targets targets;
String choice; String choice;
int[] modes; int[] modes;
String modesStr; // for human pretty-print consumption only String modesStr; // for human pretty-print consumption only
@@ -88,7 +88,7 @@ public class Plan {
this.choice = null; this.choice = null;
} }
public Decision(Score initialScore, Decision prevDecision, PossibleTargetSelector.Targets targets) { public Decision(Score initialScore, Decision prevDecision, MultiTargetSelector.Targets targets) {
this.initialScore = initialScore; this.initialScore = initialScore;
this.prevDecision = prevDecision; this.prevDecision = prevDecision;
this.saRef = null; this.saRef = null;

View File

@@ -11,9 +11,7 @@ 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.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
public class PossibleTargetSelector { public class PossibleTargetSelector {
@@ -48,15 +46,17 @@ public class PossibleTargetSelector {
} }
} }
public PossibleTargetSelector(SpellAbility sa) { public PossibleTargetSelector(SpellAbility sa, SpellAbility targetingSa, int targetingSaIndex) {
this(sa, null); this.sa = sa;
this.targetingSa = targetingSa;
this.targetingSaIndex = targetingSaIndex;
this.validTargets = new ArrayList<GameObject>();
generateValidTargets(sa.getHostCard().getController());
} }
public PossibleTargetSelector(SpellAbility sa, List<AbilitySub> plannedModes) { public void reset() {
this.sa = sa; targetIndex = 0;
chooseTargetingSubAbility(plannedModes); validTargets.clear();
this.targetIndex = 0;
this.validTargets = new ArrayList<GameObject>();
generateValidTargets(sa.getHostCard().getController()); generateValidTargets(sa.getHostCard().getController());
} }
@@ -140,36 +140,6 @@ public class PossibleTargetSelector {
} }
} }
private static boolean conditionsAreMet(SpellAbility saOrSubSa) {
SpellAbilityCondition conditions = saOrSubSa.getConditions();
return conditions == null || conditions.areMet(saOrSubSa);
}
private void chooseTargetingSubAbility(List<AbilitySub> plannedSubs) {
// TODO: This needs to handle case where multiple sub-abilities each have targets.
int index = 0;
for (SpellAbility saOrSubSa = sa; saOrSubSa != null; saOrSubSa = saOrSubSa.getSubAbility()) {
if (saOrSubSa.usesTargeting() && conditionsAreMet(saOrSubSa)) {
targetingSaIndex = index;
targetingSa = saOrSubSa;
return;
}
index++;
}
// 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();
} }
@@ -177,7 +147,6 @@ public class PossibleTargetSelector {
private void selectTargetsByIndexImpl(int index) { private void selectTargetsByIndexImpl(int index) {
targetingSa.resetTargets(); targetingSa.resetTargets();
// TODO: smarter about multiple targets, etc...
while (targetingSa.getTargets().getNumTargeted() < maxTargets && index < validTargets.size()) { while (targetingSa.getTargets().getNumTargeted() < maxTargets && index < validTargets.size()) {
targetingSa.getTargets().add(validTargets.get(index++)); targetingSa.getTargets().add(validTargets.get(index++));
} }
@@ -231,8 +200,4 @@ public class PossibleTargetSelector {
targetIndex++; targetIndex++;
return true; return true;
} }
public int getValidTargetsSize() {
return validTargets.size();
}
} }

View File

@@ -76,7 +76,7 @@ public class SimulationController {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), chosenModes, modesStr)); currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), chosenModes, modesStr));
} }
public void evaluateTargetChoices(SpellAbility sa, PossibleTargetSelector.Targets targets) { public void evaluateTargetChoices(SpellAbility sa, MultiTargetSelector.Targets targets) {
currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), targets)); currentStack.add(new Plan.Decision(getCurrentScore(), getLastDecision(), targets));
} }
@@ -125,7 +125,7 @@ public class SimulationController {
} }
private Plan.Decision getLastMergedDecision() { private Plan.Decision getLastMergedDecision() {
PossibleTargetSelector.Targets targets = null; MultiTargetSelector.Targets targets = null;
String choice = null; String choice = null;
int[] modes = null; int[] modes = null;
String modesStr = null; String modesStr = null;
@@ -204,7 +204,7 @@ public class SimulationController {
simulatorStack.remove(simulatorStack.size() - 1); simulatorStack.remove(simulatorStack.size() - 1);
} }
public Score shouldSkipTarget(SpellAbility sa, PossibleTargetSelector.Targets targets, GameSimulator simulator) { public Score shouldSkipTarget(SpellAbility sa, GameSimulator simulator) {
simulatorStack.add(simulator); simulatorStack.add(simulator);
GameObject[] hostAndTarget = getOriginalHostCardAndTarget(sa); GameObject[] hostAndTarget = getOriginalHostCardAndTarget(sa);
simulatorStack.remove(simulatorStack.size() - 1); simulatorStack.remove(simulatorStack.size() - 1);

View File

@@ -22,7 +22,7 @@ public class SpellAbilityChoicesIterator {
private Score bestScoreForMode = new Score(Integer.MIN_VALUE); private Score bestScoreForMode = new Score(Integer.MIN_VALUE);
private boolean advancedToNextMode; private boolean advancedToNextMode;
private Score[] cachedTargetScores; private ArrayList<Score> cachedTargetScores;
private int nextTarget = 0; private int nextTarget = 0;
private Score bestScoreForTarget = new Score(Integer.MIN_VALUE); private Score bestScoreForTarget = new Score(Integer.MIN_VALUE);
@@ -88,20 +88,21 @@ public class SpellAbilityChoicesIterator {
// Note: Can't just keep a TargetSelector object cached because it's // 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 // responsible for setting state on a SA and the SA object changes each
// time since it's a different simulation. // time since it's a different simulation.
PossibleTargetSelector selector = new PossibleTargetSelector(sa); MultiTargetSelector selector = new MultiTargetSelector(sa, null);
if (selector.hasPossibleTargets()) { if (selector.hasPossibleTargets()) {
if (cachedTargetScores == null) { if (cachedTargetScores == null) {
cachedTargetScores = new Score[selector.getValidTargetsSize()]; cachedTargetScores = new ArrayList<>();
nextTarget = -1; nextTarget = -1;
for (int i = 0; i < cachedTargetScores.length; i++) { for (int i = 0; selector.selectNextTargets(); i++) {
selector.selectTargetsByIndex(i); Score score = controller.shouldSkipTarget(sa, simulator);
cachedTargetScores[i] = controller.shouldSkipTarget(sa, selector.getLastSelectedTargets(), simulator); cachedTargetScores.add(score);
if (cachedTargetScores[i] != null) { if (score != null) {
controller.printState(cachedTargetScores[i], sa, " - via estimate (skipped)", false); controller.printState(score, sa, " - via estimate (skipped)", false);
} else if (nextTarget == -1) { } else if (nextTarget == -1) {
nextTarget = i; nextTarget = i;
} }
} }
selector.reset();
// If all targets were cached, we unfortunately have to evaluate the first target again // 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 // 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 // back. This used to be not possible when the PossibleTargetSelector was controlling the
@@ -154,9 +155,9 @@ public class SpellAbilityChoicesIterator {
if (cachedTargetScores != null) { if (cachedTargetScores != null) {
controller.doneEvaluating(bestScoreForTarget); controller.doneEvaluating(bestScoreForTarget);
bestScoreForTarget = new Score(Integer.MIN_VALUE); bestScoreForTarget = new Score(Integer.MIN_VALUE);
while (nextTarget + 1 < cachedTargetScores.length) { while (nextTarget + 1 < cachedTargetScores.size()) {
nextTarget++; nextTarget++;
if (cachedTargetScores[nextTarget] == null) { if (cachedTargetScores.get(nextTarget) == null) {
return true; return true;
} }
} }

View File

@@ -203,7 +203,7 @@ public class SpellAbilityPicker {
} }
// If modes != null, targeting will be done in chooseModeForAbility(). // If modes != null, targeting will be done in chooseModeForAbility().
if (decision.modes == null && decision.targets != null) { if (decision.modes == null && decision.targets != null) {
PossibleTargetSelector selector = new PossibleTargetSelector(sa); MultiTargetSelector selector = new MultiTargetSelector(sa, null);
if (!selector.selectTargets(decision.targets)) { if (!selector.selectTargets(decision.targets)) {
printPlannedActionFailure(decision, "Bad targets"); printPlannedActionFailure(decision, "Bad targets");
return null; return null;
@@ -334,7 +334,7 @@ public class SpellAbilityPicker {
// TODO: Validate that there's no discrepancies between choices and modes? // TODO: Validate that there's no discrepancies between choices and modes?
List<AbilitySub> plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes); List<AbilitySub> plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes);
if (plan.getSelectedDecision().targets != null) { if (plan.getSelectedDecision().targets != null) {
PossibleTargetSelector selector = new PossibleTargetSelector(sa, plannedModes); MultiTargetSelector selector = new MultiTargetSelector(sa, plannedModes);
if (!selector.selectTargets(decision.targets)) { if (!selector.selectTargets(decision.targets)) {
printPlannedActionFailure(decision, "Bad targets for modes"); printPlannedActionFailure(decision, "Bad targets for modes");
return null; return null;

View File

@@ -421,7 +421,7 @@ public class GameSimulatorTest extends SimulationTestCase {
assertNotNull(sa); assertNotNull(sa);
sa.setActivatingPlayer(p); sa.setActivatingPlayer(p);
PossibleTargetSelector selector = new PossibleTargetSelector(sa); MultiTargetSelector selector = new MultiTargetSelector(sa, null);
while (selector.selectNextTargets()) { while (selector.selectNextTargets()) {
GameSimulator sim = createSimulator(game, p); GameSimulator sim = createSimulator(game, p);
sim.simulateSpellAbility(sa); sim.simulateSpellAbility(sa);

View File

@@ -171,4 +171,30 @@ public class SpellAbilityPickerTest extends SimulationTestCase {
String expected = "Fiery Confluence -> " + dmgOppStr + " " + dmgOppStr + " " + dmgOppStr; String expected = "Fiery Confluence -> " + dmgOppStr + " " + dmgOppStr + " " + dmgOppStr;
assertEquals(expected, picker.getPlan().getDecisions().get(0).modesStr); assertEquals(expected, picker.getPlan().getDecisions().get(0).modesStr);
} }
public void testMultipleTargets() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
addCard("Mountain", p);
addCard("Mountain", p);
Card spell = addCardToZone("Arc Trail", p, ZoneType.Hand);
Player opponent = game.getPlayers().get(0);
Card bear = addCard("Runeclaw Bear", opponent);
Card men = addCard("Flying Men", opponent);
opponent.setLife(20, null);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
game.getAction().checkStateEffects(true);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
SpellAbility sa = picker.chooseSpellAbilityToPlay(null);
assertEquals(spell.getSpellAbilities().get(0), sa);
assertEquals(bear, sa.getTargetCard());
assertEquals("2", sa.getParam("NumDmg"));
SpellAbility subSa = sa.getSubAbility();
assertEquals(men, subSa.getTargetCard());
assertEquals("1", subSa.getParam("NumDmg"));
}
} }