Tibalt, Cosmic Impostor - fixed that emblem can't cast not owned cards (#7598)

* Fixed ability.canChooseTarget not using correct playerId

* Fixed Necrotic Plague

* Revert "Fixed Necrotic Plague"

This reverts commit 7659039670293ce1ea428dad042511d9d75f9da6.

* Set target controller on Necrotic Plague and add check in canChooseTarget

* Add test for Tibalt + Ephemerate interaction

* Tests improved

Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
Daniel Bomar 2021-02-22 13:06:43 -06:00 committed by GitHub
parent b94af941df
commit bb0a995541
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 84 additions and 31 deletions

View file

@ -118,7 +118,7 @@ public class SimulatedPlayerMCTS extends MCTSPlayer {
@Override @Override
public boolean triggerAbility(TriggeredAbility source, Game game) { public boolean triggerAbility(TriggeredAbility source, Game game) {
// logger.info("trigger"); // logger.info("trigger");
if (source != null && source.canChooseTarget(game)) { if (source != null && source.canChooseTarget(game, playerId)) {
Ability ability; Ability ability;
List<Ability> options = getPlayableOptions(source, game); List<Ability> options = getPlayableOptions(source, game);
if (options.isEmpty()) { if (options.isEmpty()) {

View file

@ -183,7 +183,7 @@ class LivingLoreSacrificeEffect extends OneShotEffect {
} }
} }
if (exiledCard != null) { if (exiledCard != null) {
if (exiledCard.getSpellAbility().canChooseTarget(game)) { if (exiledCard.getSpellAbility().canChooseTarget(game, controller.getId())) {
game.getState().setValue("PlayFromNotOwnHandZone" + exiledCard.getId(), Boolean.TRUE); game.getState().setValue("PlayFromNotOwnHandZone" + exiledCard.getId(), Boolean.TRUE);
controller.cast(controller.chooseAbilityForCast(exiledCard, game, true), controller.cast(controller.chooseAbilityForCast(exiledCard, game, true),
game, true, new ApprovingObject(source, game)); game, true, new ApprovingObject(source, game));

View file

@ -81,7 +81,7 @@ class MaelstromArchangelCastEffect extends OneShotEffect {
while (controller.canRespond() && !cancel) { while (controller.canRespond() && !cancel) {
if (controller.chooseTarget(outcome, target, source, game)) { if (controller.chooseTarget(outcome, target, source, game)) {
cardToCast = game.getCard(target.getFirstTarget()); cardToCast = game.getCard(target.getFirstTarget());
if (cardToCast != null && cardToCast.getSpellAbility().canChooseTarget(game)) { if (cardToCast != null && cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) {
cancel = true; cancel = true;
} }
} else { } else {

View file

@ -79,7 +79,7 @@ class MizzixsMasteryEffect extends OneShotEffect {
if (card != null) { if (card != null) {
if (controller.moveCards(card, Zone.EXILED, source, game)) { if (controller.moveCards(card, Zone.EXILED, source, game)) {
Card cardCopy = game.copyCard(card, source, source.getControllerId()); Card cardCopy = game.copyCard(card, source, source.getControllerId());
if (cardCopy.getSpellAbility().canChooseTarget(game) if (cardCopy.getSpellAbility().canChooseTarget(game, controller.getId())
&& controller.chooseUse(outcome, "Cast copy of " && controller.chooseUse(outcome, "Cast copy of "
+ card.getName() + " without paying its mana cost?", source, game)) { + card.getName() + " without paying its mana cost?", source, game)) {
game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), Boolean.TRUE); game.getState().setValue("PlayFromNotOwnHandZone" + cardCopy.getId(), Boolean.TRUE);
@ -135,7 +135,7 @@ class MizzixsMasteryOverloadEffect extends OneShotEffect {
if (controller.chooseTarget(Outcome.PlayForFree, copiedCards, targetCard, source, game)) { if (controller.chooseTarget(Outcome.PlayForFree, copiedCards, targetCard, source, game)) {
Card selectedCard = game.getCard(targetCard.getFirstTarget()); Card selectedCard = game.getCard(targetCard.getFirstTarget());
if (selectedCard != null if (selectedCard != null
&& selectedCard.getSpellAbility().canChooseTarget(game)) { && selectedCard.getSpellAbility().canChooseTarget(game, controller.getId())) {
game.getState().setValue("PlayFromNotOwnHandZone" + selectedCard.getId(), Boolean.TRUE); game.getState().setValue("PlayFromNotOwnHandZone" + selectedCard.getId(), Boolean.TRUE);
controller.cast(controller.chooseAbilityForCast(selectedCard, game, true), controller.cast(controller.chooseAbilityForCast(selectedCard, game, true),
game, true, new ApprovingObject(source, game)); game, true, new ApprovingObject(source, game));

View file

@ -85,6 +85,7 @@ enum NecroticPlagueAdjuster implements TargetAdjuster {
ability.setControllerId(creatureController.getId()); ability.setControllerId(creatureController.getId());
ability.getTargets().clear(); ability.getTargets().clear();
TargetPermanent target = new TargetOpponentsCreaturePermanent(); TargetPermanent target = new TargetOpponentsCreaturePermanent();
target.setTargetController(creatureController.getId());
ability.getTargets().add(target); ability.getTargets().add(target);
} }
} }

View file

@ -97,7 +97,7 @@ class OmnispellAdeptEffect extends OneShotEffect {
} }
realFilter.add(Predicates.not(new CardIdPredicate(cardToCast.getId()))); // remove card from choose dialog (infinite fix) realFilter.add(Predicates.not(new CardIdPredicate(cardToCast.getId()))); // remove card from choose dialog (infinite fix)
if (!cardToCast.getSpellAbility().canChooseTarget(game)) { if (!cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) {
continue; continue;
} }

View file

@ -95,7 +95,7 @@ class OracleOfBonesCastEffect extends OneShotEffect {
if (controller.chooseTarget(outcome, target, source, game)) { if (controller.chooseTarget(outcome, target, source, game)) {
cardToCast = game.getCard(target.getFirstTarget()); cardToCast = game.getCard(target.getFirstTarget());
if (cardToCast != null if (cardToCast != null
&& cardToCast.getSpellAbility().canChooseTarget(game)) { && cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) {
cancel = true; cancel = true;
} }
} else { } else {

View file

@ -127,7 +127,7 @@ class PossibilityStormEffect extends OneShotEffect {
if (card != null && sharesType(card, spell.getCardType()) if (card != null && sharesType(card, spell.getCardType())
&& !card.isLand() && !card.isLand()
&& card.getSpellAbility().canChooseTarget(game)) { && card.getSpellAbility().canChooseTarget(game, spellController.getId())) {
if (spellController.chooseUse(Outcome.PlayForFree, "Cast " + card.getLogName() + " without paying cost?", source, game)) { if (spellController.chooseUse(Outcome.PlayForFree, "Cast " + card.getLogName() + " without paying cost?", source, game)) {
spellController.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game)); spellController.cast(card.getSpellAbility(), game, true, new ApprovingObject(source, game));
} }

View file

@ -179,7 +179,7 @@ class CardCanBeCastPredicate implements Predicate<Card> {
SpellAbility ability = input.getSpellAbility().copy(); SpellAbility ability = input.getSpellAbility().copy();
ability.setControllerId(controllerId); ability.setControllerId(controllerId);
input.adjustTargets(ability, game); input.adjustTargets(ability, game);
return ability.canChooseTarget(game); return ability.canChooseTarget(game, controllerId);
} }
@Override @Override

View file

@ -0,0 +1,35 @@
package org.mage.test.cards.single.khm;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
public class ValkiGodOfLiesTest extends CardTestPlayerBase {
@Test
public void ephmerateTest() {
removeAllCardsFromLibrary(playerB);
addCard(Zone.BATTLEFIELD, playerA, "Badlands", 7);
addCard(Zone.HAND, playerA, "Plains");
addCard(Zone.HAND, playerA, "Valki, God of Lies");
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears");
addCard(Zone.LIBRARY, playerB, "Ephemerate");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tibalt, Cosmic Impostor");
setChoice(playerA, "Tibalt, Cosmic Impostor"); // two etb effects
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: Exile the top card of each player's library.");
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plains");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Tibalt, Cosmic Impostor", 1);
assertPermanentCount(playerA, "Grizzly Bears", 1);
assertGraveyardCount(playerB, "Ephemerate", 1);
}
}

View file

@ -22,8 +22,10 @@ public class NecroticPlagueTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Necrotic Plague", "Sejiri Merfolk"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Necrotic Plague", "Sejiri Merfolk");
setStrictChooseMode(true);
setStopAt(2, PhaseStep.PRECOMBAT_MAIN); setStopAt(2, PhaseStep.PRECOMBAT_MAIN);
execute(); execute();
assertAllCommandsUsed();
assertLife(playerA, 20); assertLife(playerA, 20);
assertLife(playerB, 20); assertLife(playerB, 20);
@ -38,7 +40,7 @@ public class NecroticPlagueTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
/** /**
* Goblin Deathraiders English * Goblin Deathraiders
* Creature Goblin Warrior 3/1, BR * Creature Goblin Warrior 3/1, BR
* Trample * Trample
*/ */
@ -63,9 +65,12 @@ public class NecroticPlagueTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerB, "Sejiri Merfolk"); addCard(Zone.BATTLEFIELD, playerB, "Sejiri Merfolk");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Necrotic Plague", "Sejiri Merfolk"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Necrotic Plague", "Sejiri Merfolk");
addTarget(playerB, "Goblin Deathraiders"); // target for new necro attach
setStrictChooseMode(true);
setStopAt(3, PhaseStep.PRECOMBAT_MAIN); setStopAt(3, PhaseStep.PRECOMBAT_MAIN);
execute(); execute();
assertAllCommandsUsed();
assertLife(playerA, 20); assertLife(playerA, 20);
assertLife(playerB, 20); assertLife(playerB, 20);

View file

@ -123,7 +123,7 @@ public class RandomPlayer extends ComputerPlayer {
@Override @Override
public boolean triggerAbility(TriggeredAbility source, Game game) { public boolean triggerAbility(TriggeredAbility source, Game game) {
if (source != null && source.canChooseTarget(game)) { if (source != null && source.canChooseTarget(game, playerId)) {
Ability ability; Ability ability;
List<Ability> options = getPlayableOptions(source, game); List<Ability> options = getPlayableOptions(source, game);
if (options.isEmpty()) { if (options.isEmpty()) {

View file

@ -320,7 +320,7 @@ public interface Ability extends Controllable, Serializable {
Modes getModes(); Modes getModes();
boolean canChooseTarget(Game game); boolean canChooseTarget(Game game, UUID playerId);
/** /**
* Gets the list of sub-abilities associated with this ability. * Gets the list of sub-abilities associated with this ability.

View file

@ -901,24 +901,36 @@ public abstract class AbilityImpl implements Ability {
} }
@Override @Override
public boolean canChooseTarget(Game game) { public boolean canChooseTarget(Game game, UUID playerId) {
if (this instanceof SpellAbility) { if (this instanceof SpellAbility) {
if (SpellAbilityType.SPLIT_FUSED.equals(((SpellAbility) this).getSpellAbilityType())) { if (SpellAbilityType.SPLIT_FUSED.equals(((SpellAbility) this).getSpellAbilityType())) {
Card card = game.getCard(getSourceId()); Card card = game.getCard(getSourceId());
if (card != null) { if (card != null) {
return canChooseTargetAbility(((SplitCard) card).getLeftHalfCard().getSpellAbility(), game, getControllerId()) return canChooseTargetAbility(((SplitCard) card).getLeftHalfCard().getSpellAbility(), game, playerId)
&& canChooseTargetAbility(((SplitCard) card).getRightHalfCard().getSpellAbility(), game, getControllerId()); && canChooseTargetAbility(((SplitCard) card).getRightHalfCard().getSpellAbility(), game, playerId);
} }
return false; return false;
} }
} }
return canChooseTargetAbility(this, game, getControllerId()); return canChooseTargetAbility(this, game, playerId);
} }
private static boolean canChooseTargetAbility(Ability ability, Game game, UUID controllerId) { private static boolean canChooseTargetAbility(Ability ability, Game game, UUID controllerId) {
int found = 0; int found = 0;
for (Mode mode : ability.getModes().values()) { for (Mode mode : ability.getModes().values()) {
if (mode.getTargets().canChoose(ability.getSourceId(), ability.getControllerId(), game)) { boolean validTargets = true;
for (Target target : mode.getTargets()) {
UUID abilityControllerId = controllerId;
if (target.getTargetController() != null) {
abilityControllerId = target.getTargetController();
}
if (!target.canChoose(ability.getSourceId(), abilityControllerId, game)) {
validTargets = false;
break;
}
}
if (validTargets) {
found++; found++;
if (ability.getModes().isEachModeMoreThanOnce()) { if (ability.getModes().isEachModeMoreThanOnce()) {
return true; return true;

View file

@ -194,7 +194,7 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
|| game.canPlaySorcery(playerId) || game.canPlaySorcery(playerId)
|| null != approvingObject) { || null != approvingObject) {
if (costs.canPay(this, this, playerId, game) if (costs.canPay(this, this, playerId, game)
&& canChooseTarget(game)) { && canChooseTarget(game, playerId)) {
this.activatorId = playerId; this.activatorId = playerId;
return new ActivationStatus(true, approvingObject); return new ActivationStatus(true, approvingObject);
} }

View file

@ -118,13 +118,13 @@ public class SpellAbility extends ActivatedAbilityImpl {
// fused can be called from hand only, so not permitting object allows or other zones checks // fused can be called from hand only, so not permitting object allows or other zones checks
// see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/251926-snapcaster-mage-and-fuse // see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/251926-snapcaster-mage-and-fuse
if (game.getState().getZone(splitCard.getId()) == Zone.HAND) { if (game.getState().getZone(splitCard.getId()) == Zone.HAND) {
return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game) return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)
&& splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game), null); && splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId), null);
} }
} }
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} else { } else {
return new ActivationStatus(canChooseTarget(game), approvingObject); return new ActivationStatus(canChooseTarget(game, playerId), approvingObject);
} }
} }
} }

View file

@ -76,7 +76,7 @@ public class CastWithoutPayingManaCostEffect extends OneShotEffect {
+ cardToCast.getName() + " is no land and has no spell ability!"); + cardToCast.getName() + " is no land and has no spell ability!");
cancel = true; cancel = true;
} }
if (cardToCast.getSpellAbility().canChooseTarget(game)) { if (cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) {
cancel = true; cancel = true;
} }
} }

View file

@ -437,8 +437,8 @@ public class StackAbility extends StackObjImpl implements Ability {
} }
@Override @Override
public boolean canChooseTarget(Game game) { public boolean canChooseTarget(Game game, UUID playerId) {
return ability.canChooseTarget(game); return ability.canChooseTarget(game, playerId);
} }
@Override @Override

View file

@ -1495,7 +1495,7 @@ public abstract class PlayerImpl implements Player, Serializable {
if (sourceObject != null) { if (sourceObject != null) {
sourceObject.adjustTargets(ability, game); sourceObject.adjustTargets(ability, game);
} }
if (ability.canChooseTarget(game)) { if (ability.canChooseTarget(game, playerId)) {
if (ability.isUsesStack()) { if (ability.isUsesStack()) {
game.getStack().push(new StackAbility(ability, playerId)); game.getStack().push(new StackAbility(ability, playerId));
} }
@ -1539,28 +1539,28 @@ public abstract class PlayerImpl implements Player, Serializable {
return useable; return useable;
case SPLIT_FUSED: case SPLIT_FUSED:
if (zone == Zone.HAND) { if (zone == Zone.HAND) {
if (ability.canChooseTarget(game)) { if (ability.canChooseTarget(game, playerId)) {
useable.put(ability.getId(), (SpellAbility) ability); useable.put(ability.getId(), (SpellAbility) ability);
} }
} }
case SPLIT: case SPLIT:
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game)) { if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility()); ((SplitCard) object).getLeftHalfCard().getSpellAbility());
} }
if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game)) { if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(),
((SplitCard) object).getRightHalfCard().getSpellAbility()); ((SplitCard) object).getRightHalfCard().getSpellAbility());
} }
return useable; return useable;
case SPLIT_AFTERMATH: case SPLIT_AFTERMATH:
if (zone == Zone.GRAVEYARD) { if (zone == Zone.GRAVEYARD) {
if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game)) { if (((SplitCard) object).getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(), useable.put(((SplitCard) object).getRightHalfCard().getSpellAbility().getId(),
((SplitCard) object).getRightHalfCard().getSpellAbility()); ((SplitCard) object).getRightHalfCard().getSpellAbility());
} }
} else { } else {
if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game)) { if (((SplitCard) object).getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)) {
useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(), useable.put(((SplitCard) object).getLeftHalfCard().getSpellAbility().getId(),
((SplitCard) object).getLeftHalfCard().getSpellAbility()); ((SplitCard) object).getLeftHalfCard().getSpellAbility());
} }

View file

@ -4,10 +4,10 @@ import mage.abilities.Ability;
import mage.game.Game; import mage.game.Game;
/** /**
*
* @author TheElk801 * @author TheElk801
*/ */
public interface TargetAdjuster { public interface TargetAdjuster {
void adjustTargets(Ability ability, Game game); void adjustTargets(Ability ability, Game game);
} }