* AI: fixed rollback errors on play cards with target stack (Diplomatic Escort, Not of This World, etc);

This commit is contained in:
Oleg Agafonov 2020-03-09 13:49:07 +04:00
parent 009e55c4f0
commit 86f6d39f5a
5 changed files with 126 additions and 101 deletions

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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)) {

View file

@ -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());
}
}