mirror of
https://github.com/correl/mage.git
synced 2024-11-14 19:19:32 +00:00
* AI: fixed rollback errors on play cards with target stack (Diplomatic Escort, Not of This World, etc);
This commit is contained in:
parent
009e55c4f0
commit
86f6d39f5a
5 changed files with 126 additions and 101 deletions
|
@ -819,10 +819,13 @@ public class ComputerPlayer extends PlayerImpl implements Player {
|
|||
return target.isChosen();
|
||||
}
|
||||
|
||||
if (target.getOriginalTarget() instanceof TargetSpell) {
|
||||
if (target.getOriginalTarget() instanceof TargetSpell
|
||||
|| target.getOriginalTarget() instanceof TargetStackObject) {
|
||||
if (!game.getStack().isEmpty()) {
|
||||
for (StackObject o : game.getStack()) {
|
||||
if (o instanceof Spell && !source.getId().equals(o.getStackAbility().getId())) {
|
||||
if (o instanceof Spell
|
||||
&& !source.getId().equals(o.getStackAbility().getId())
|
||||
&& target.canTarget(abilityControllerId, o.getStackAbility().getId(), source, game)) {
|
||||
return tryAddTarget(target, o.getId(), source, game);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package mage.cards.n;
|
|||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.Mode;
|
||||
import mage.abilities.Modes;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.effects.common.CounterTargetEffect;
|
||||
|
@ -10,19 +9,17 @@ import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect;
|
|||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.*;
|
||||
import mage.filter.Filter;
|
||||
import mage.filter.FilterStackObject;
|
||||
import mage.filter.common.FilterControlledPermanent;
|
||||
import mage.filter.common.FilterCreaturePermanent;
|
||||
import mage.filter.predicate.mageobject.PowerPredicate;
|
||||
import mage.filter.predicate.other.TargetsPermanentPredicate;
|
||||
import mage.game.Game;
|
||||
import mage.game.stack.Spell;
|
||||
import mage.game.stack.StackAbility;
|
||||
import mage.game.stack.StackObject;
|
||||
import mage.target.Target;
|
||||
import mage.target.TargetObject;
|
||||
import mage.target.TargetStackObject;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
@ -30,13 +27,19 @@ import java.util.UUID;
|
|||
*/
|
||||
public final class NotOfThisWorld extends CardImpl {
|
||||
|
||||
private static final FilterStackObject filter = new FilterStackObject("spell or ability that targets a permanent you control");
|
||||
|
||||
static {
|
||||
filter.add(new TargetsPermanentPredicate(new FilterControlledPermanent()));
|
||||
}
|
||||
|
||||
public NotOfThisWorld(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.TRIBAL, CardType.INSTANT}, "{7}");
|
||||
this.subtype.add(SubType.ELDRAZI);
|
||||
|
||||
// Counter target spell or ability that targets a permanent you control.
|
||||
this.getSpellAbility().addTarget(new TargetStackObjectTargetingControlledPermanent());
|
||||
this.getSpellAbility().addEffect(new CounterTargetEffect());
|
||||
this.getSpellAbility().addTarget(new TargetStackObject(filter));
|
||||
|
||||
// Not of This World costs {7} less to cast if it targets a spell or ability that targets a creature you control with power 7 or greater.
|
||||
this.addAbility(new SimpleStaticAbility(Zone.STACK, new SpellCostReductionSourceEffect(7, NotOfThisWorldCondition.instance)));
|
||||
|
@ -52,92 +55,6 @@ public final class NotOfThisWorld extends CardImpl {
|
|||
}
|
||||
}
|
||||
|
||||
class TargetStackObjectTargetingControlledPermanent extends TargetObject {
|
||||
|
||||
TargetStackObjectTargetingControlledPermanent() {
|
||||
this.minNumberOfTargets = 1;
|
||||
this.maxNumberOfTargets = 1;
|
||||
this.zone = Zone.STACK;
|
||||
this.targetName = "spell or ability that targets a permanent you control";
|
||||
}
|
||||
|
||||
private TargetStackObjectTargetingControlledPermanent(final TargetStackObjectTargetingControlledPermanent target) {
|
||||
super(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
throw new UnsupportedOperationException("Not supported."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTarget(UUID id, Ability source, Game game) {
|
||||
StackObject stackObject = game.getStack().getStackObject(id);
|
||||
return (stackObject instanceof Spell) || (stackObject instanceof StackAbility);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChoose(UUID sourceId, UUID sourceControllerId, Game game) {
|
||||
return canChoose(sourceControllerId, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChoose(UUID sourceControllerId, Game game) {
|
||||
return game.getStack()
|
||||
.stream()
|
||||
.filter(stackObject -> stackObject instanceof Spell || stackObject instanceof StackAbility)
|
||||
.map(StackObject::getStackAbility)
|
||||
.map(Ability::getModes)
|
||||
.map(Modes::values)
|
||||
.flatMap(Collection::stream)
|
||||
.map(Mode::getTargets)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(target -> !target.isNotTarget())
|
||||
.map(Target::getTargets)
|
||||
.flatMap(Collection::stream)
|
||||
.map(game::getPermanentOrLKIBattlefield)
|
||||
.anyMatch(permanent -> permanent != null && permanent.isControlledBy(sourceControllerId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> possibleTargets(UUID sourceId, UUID sourceControllerId,
|
||||
Game game) {
|
||||
return possibleTargets(sourceControllerId, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> possibleTargets(UUID sourceControllerId, Game game) {
|
||||
Set<UUID> possibleTargets = new HashSet<>();
|
||||
game.getStack().stream().forEach(stackObject -> {
|
||||
if (!(stackObject instanceof Spell || stackObject instanceof StackAbility)) {
|
||||
return;
|
||||
}
|
||||
boolean flag = stackObject
|
||||
.getStackAbility()
|
||||
.getModes()
|
||||
.values()
|
||||
.stream()
|
||||
.map(Mode::getTargets)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(target -> !target.isNotTarget())
|
||||
.map(Target::getTargets)
|
||||
.flatMap(Collection::stream)
|
||||
.map(game::getPermanentOrLKIBattlefield)
|
||||
.anyMatch(permanent -> permanent != null && permanent.isControlledBy(sourceControllerId));
|
||||
if (flag) {
|
||||
possibleTargets.add(stackObject.getId());
|
||||
}
|
||||
});
|
||||
return possibleTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetStackObjectTargetingControlledPermanent copy() {
|
||||
return new TargetStackObjectTargetingControlledPermanent(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum NotOfThisWorldCondition implements Condition {
|
||||
|
||||
instance;
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
package org.mage.test.AI.basic;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class TargetStackObjectByAITest extends CardTestPlayerBaseWithAIHelps {
|
||||
|
||||
// only PlayerA is AI controlled
|
||||
// use case: java.lang.IllegalStateException: Target wasn't handled. class:class mage.target.TargetStackObject
|
||||
|
||||
@Test
|
||||
public void test_TargetStack_Manual() {
|
||||
// {U}, {T}, Discard a card: Counter target spell or ability that targets a creature.
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Diplomatic Escort", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 1);
|
||||
addCard(Zone.HAND, playerB, "Swamp", 1);
|
||||
//
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
|
||||
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
|
||||
|
||||
// A attack and B response
|
||||
|
||||
// attack creature
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears");
|
||||
// counter it
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{U}, {T}, Discard a card", "Lightning Bolt", "Lightning Bolt");
|
||||
setChoice(playerB, "Swamp"); // discard
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
assertGraveyardCount(playerB, "Grizzly Bears", 0);
|
||||
assertGraveyardCount(playerA, "Lightning Bolt", 1);
|
||||
assertTapped("Diplomatic Escort", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_TargetStack_ChooseByAI() {
|
||||
// {U}, {T}, Discard a card: Counter target spell or ability that targets a creature.
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Diplomatic Escort", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 1);
|
||||
addCard(Zone.HAND, playerB, "Swamp", 1);
|
||||
//
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
|
||||
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
|
||||
|
||||
// A attack and B response
|
||||
|
||||
// attack creature
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears");
|
||||
// counter it
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{U}, {T}, Discard a card"); // AI choose target
|
||||
//setChoice(playerB, "Swamp"); // discard
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
//setStrictChooseMode(true); // AI must choose
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
assertGraveyardCount(playerB, "Grizzly Bears", 0);
|
||||
assertGraveyardCount(playerA, "Lightning Bolt", 1);
|
||||
assertTapped("Diplomatic Escort", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_TargetStack_PlayByAI() {
|
||||
// {U}, {T}, Discard a card: Counter target spell or ability that targets a creature.
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Diplomatic Escort", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 1);
|
||||
addCard(Zone.HAND, playerB, "Swamp", 1);
|
||||
//
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
|
||||
addCard(Zone.HAND, playerA, "Lightning Bolt", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
|
||||
|
||||
// A attack and B response
|
||||
|
||||
// attack creature
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Grizzly Bears");
|
||||
// AI must counter it
|
||||
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerB);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true); // AI play with full simulation
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
assertGraveyardCount(playerB, "Grizzly Bears", 0);
|
||||
assertGraveyardCount(playerA, "Lightning Bolt", 1);
|
||||
assertTapped("Diplomatic Escort", true);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
package mage.filter.predicate.other;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Mode;
|
||||
import mage.filter.FilterPermanent;
|
||||
|
@ -12,8 +10,9 @@ import mage.game.permanent.Permanent;
|
|||
import mage.game.stack.StackObject;
|
||||
import mage.target.Target;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author LoneFox
|
||||
*/
|
||||
public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate<ObjectSourcePlayer<MageObject>> {
|
||||
|
@ -31,6 +30,9 @@ public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate<Ob
|
|||
for (UUID modeId : object.getStackAbility().getModes().getSelectedModes()) {
|
||||
Mode mode = object.getStackAbility().getModes().get(modeId);
|
||||
for (Target target : mode.getTargets()) {
|
||||
if (target.isNotTarget()) {
|
||||
continue;
|
||||
}
|
||||
for (UUID targetId : target.getTargets()) {
|
||||
Permanent permanent = game.getPermanentOrLKIBattlefield(targetId);
|
||||
if (permanent != null && targetFilter.match(permanent, input.getSourceId(), input.getPlayerId(), game)) {
|
||||
|
|
|
@ -58,7 +58,8 @@ public class TargetStackObject extends TargetObject {
|
|||
public boolean canChoose(UUID sourceId, UUID sourceControllerId, Game game) {
|
||||
int count = 0;
|
||||
for (StackObject stackObject : game.getStack()) {
|
||||
if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) && filter.match(stackObject, sourceId, sourceControllerId, game)) {
|
||||
if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId())
|
||||
&& filter.match(stackObject, sourceId, sourceControllerId, game)) {
|
||||
count++;
|
||||
if (count >= this.minNumberOfTargets) {
|
||||
return true;
|
||||
|
@ -77,7 +78,8 @@ public class TargetStackObject extends TargetObject {
|
|||
public Set<UUID> possibleTargets(UUID sourceId, UUID sourceControllerId, Game game) {
|
||||
Set<UUID> possibleTargets = new HashSet<>();
|
||||
for (StackObject stackObject : game.getStack()) {
|
||||
if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId()) && filter.match(stackObject, sourceId, sourceControllerId, game)) {
|
||||
if (game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId())
|
||||
&& filter.match(stackObject, sourceId, sourceControllerId, game)) {
|
||||
possibleTargets.add(stackObject.getId());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue