mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 04:38:00 +00:00
[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:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user