mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 12:18:00 +00:00
MagicStack: fix fizzle removing too much Targets (#4873)
* MagicStack: fix fizzle removing too much Targets * Fix Dead Ringers * Clean up useless check
This commit is contained in:
@@ -151,9 +151,12 @@ public class GameSimulator {
|
||||
}
|
||||
|
||||
public Score simulateSpellAbility(SpellAbility origSa) {
|
||||
return simulateSpellAbility(origSa, this.eval);
|
||||
return simulateSpellAbility(origSa, this.eval, true);
|
||||
}
|
||||
public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval) {
|
||||
public Score simulateSpellAbility(SpellAbility origSa, boolean resolve) {
|
||||
return simulateSpellAbility(origSa, this.eval, resolve);
|
||||
}
|
||||
public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval, boolean resolve) {
|
||||
SpellAbility sa;
|
||||
if (origSa instanceof LandAbility) {
|
||||
Card hostCard = (Card) copier.find(origSa.getHostCard());
|
||||
@@ -209,9 +212,11 @@ public class GameSimulator {
|
||||
}
|
||||
}
|
||||
|
||||
if (resolve) {
|
||||
// TODO: Support multiple opponents.
|
||||
Player opponent = aiPlayer.getWeakestOpponent();
|
||||
resolveStack(simGame, opponent);
|
||||
}
|
||||
|
||||
// TODO: If this is during combat, before blockers are declared,
|
||||
// we should simulate how combat will resolve and evaluate that
|
||||
|
||||
@@ -114,9 +114,6 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
|
||||
if (value.equals("Bargain")) {
|
||||
this.bargain = true;
|
||||
}
|
||||
if (value.equals("AllTargetsLegal")) {
|
||||
this.setAllTargetsLegal(true);
|
||||
}
|
||||
if (value.equals("AltCost"))
|
||||
this.altCostPaid = true;
|
||||
|
||||
@@ -215,8 +212,8 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
|
||||
}
|
||||
}
|
||||
|
||||
if (params.containsKey("ConditionShareAllColors")) {
|
||||
this.setShareAllColors(params.get("ConditionShareAllColors"));
|
||||
if (params.containsKey("ConditionNoDifferentColors")) {
|
||||
this.setNoDifferentColors(params.get("ConditionNoDifferentColors"));
|
||||
}
|
||||
|
||||
if (params.containsKey("ConditionManaSpent")) {
|
||||
@@ -305,16 +302,8 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isAllTargetsLegal()) {
|
||||
for (Card c : sa.getTargets().getTargetCards()) {
|
||||
if (!sa.canTarget(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getShareAllColors() != null) {
|
||||
List<Card> tgts = AbilityUtils.getDefinedCards(host, this.getShareAllColors(), sa);
|
||||
if (this.getNoDifferentColors() != null) {
|
||||
List<Card> tgts = AbilityUtils.getDefinedCards(host, this.getNoDifferentColors(), sa);
|
||||
Card first = Iterables.getFirst(tgts, null);
|
||||
if (first == null) {
|
||||
return false;
|
||||
|
||||
@@ -95,8 +95,6 @@ public class SpellAbilityVariables implements Cloneable {
|
||||
private boolean blessing = false;
|
||||
private boolean solved = false;
|
||||
|
||||
private boolean allTargetsLegal = false;
|
||||
|
||||
/** The s is present. */
|
||||
private String isPresent = null;
|
||||
private String isPresent2 = null;
|
||||
@@ -137,7 +135,7 @@ public class SpellAbilityVariables implements Cloneable {
|
||||
private String lifeAmount = "GE1";
|
||||
|
||||
/** The shareAllColors. */
|
||||
private String shareAllColors = null;
|
||||
private String noDifferentColors = null;
|
||||
|
||||
/** The mana spent. */
|
||||
private String manaSpent = "";
|
||||
@@ -360,20 +358,6 @@ public class SpellAbilityVariables implements Cloneable {
|
||||
protected boolean bargain = false;
|
||||
protected boolean foretold = false;
|
||||
|
||||
/**
|
||||
* @return the allTargetsLegal
|
||||
*/
|
||||
public boolean isAllTargetsLegal() {
|
||||
return allTargetsLegal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param allTargets the allTargetsLegal to set
|
||||
*/
|
||||
public void setAllTargetsLegal(boolean allTargets) {
|
||||
this.allTargetsLegal = allTargets;
|
||||
}
|
||||
|
||||
// IsPresent for Valid battlefield stuff
|
||||
|
||||
/**
|
||||
@@ -554,12 +538,11 @@ public class SpellAbilityVariables implements Cloneable {
|
||||
|
||||
public final boolean isSolved() { return this.solved; }
|
||||
|
||||
public String getShareAllColors() {
|
||||
return shareAllColors;
|
||||
public String getNoDifferentColors() {
|
||||
return noDifferentColors;
|
||||
}
|
||||
|
||||
public void setShareAllColors(String shareAllColors) {
|
||||
this.shareAllColors = shareAllColors;
|
||||
public void setNoDifferentColors(String noDifferentColors) {
|
||||
this.noDifferentColors = noDifferentColors;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -643,25 +643,18 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
|
||||
return hasLegalTargeting(sa.getSubAbility());
|
||||
}
|
||||
|
||||
private final boolean hasFizzled(final SpellAbility sa, final Card source, final Boolean parentFizzled) {
|
||||
// Can't fizzle unless there are some targets
|
||||
Boolean fizzle = null;
|
||||
boolean rememberTgt = sa.getRootAbility().hasParam("RememberOriginalTargets");
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (sa.isZeroTargets()) {
|
||||
// Nothing targeted, and nothing needs to be targeted.
|
||||
} else {
|
||||
private final boolean hasFizzled(final SpellAbility sa, final Card source, Boolean fizzle) {
|
||||
List<GameObject> toRemove = Lists.newArrayList();
|
||||
if (sa.usesTargeting() && !sa.isZeroTargets()) {
|
||||
if (fizzle == null) {
|
||||
// don't overwrite previous result
|
||||
fizzle = true;
|
||||
}
|
||||
// Some targets were chosen, fizzling for this subability is now possible
|
||||
//fizzle = true;
|
||||
// With multi-targets, as long as one target is still legal,
|
||||
// we'll try to go through as much as possible
|
||||
final TargetChoices choices = sa.getTargets();
|
||||
for (final GameObject o : Lists.newArrayList(sa.getTargets())) {
|
||||
for (final GameObject o : sa.getTargets()) {
|
||||
boolean invalidTarget = false;
|
||||
if (rememberTgt) {
|
||||
source.addRemembered(o);
|
||||
}
|
||||
if (o instanceof Card) {
|
||||
final Card card = (Card) o;
|
||||
Card current = game.getCardState(card);
|
||||
@@ -675,10 +668,9 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
|
||||
} else {
|
||||
invalidTarget = !sa.canTarget(o);
|
||||
}
|
||||
// TODO remove targets only after the Loop
|
||||
// Remove targets
|
||||
|
||||
if (invalidTarget) {
|
||||
choices.remove(o);
|
||||
toRemove.add(o);
|
||||
} else {
|
||||
fizzle = false;
|
||||
}
|
||||
@@ -689,25 +681,16 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
|
||||
fizzle = false;
|
||||
}
|
||||
}
|
||||
if (fizzle == null) {
|
||||
fizzle = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (sa.getTargetCard() != null) {
|
||||
fizzle = !sa.canTarget(sa.getTargetCard());
|
||||
} else {
|
||||
// Set fizzle to the same as the parent if there's no target info
|
||||
fizzle = parentFizzled;
|
||||
if (sa.getSubAbility() != null) {
|
||||
fizzle = hasFizzled(sa.getSubAbility(), source, fizzle);
|
||||
}
|
||||
|
||||
if (sa.getSubAbility() == null) {
|
||||
if (fizzle != null && fizzle && rememberTgt) {
|
||||
source.clearRemembered();
|
||||
// Remove targets
|
||||
if (sa.usesTargeting() && !sa.isZeroTargets()) {
|
||||
sa.getTargets().removeAll(toRemove);
|
||||
}
|
||||
return fizzle != null && fizzle.booleanValue();
|
||||
}
|
||||
return hasFizzled(sa.getSubAbility(), source, fizzle) && (fizzle == null || fizzle.booleanValue());
|
||||
return fizzle != null && fizzle;
|
||||
}
|
||||
|
||||
public final SpellAbilityStackInstance peek() {
|
||||
|
||||
@@ -2506,4 +2506,34 @@ public class GameSimulationTest extends SimulationTest {
|
||||
AssertJUnit.assertTrue(transformedHeliodToken.isTransformed());
|
||||
AssertJUnit.assertTrue(transformedHeliodToken.isBackSide());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasicSpellFizzling() {
|
||||
Game game = initAndCreateGame();
|
||||
Player p = game.getPlayers().get(0);
|
||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||
|
||||
addCardToZone("Swamp", p, ZoneType.Library);
|
||||
Card bear = addCard("Bear Cub", p);
|
||||
|
||||
addCards("Swamp", 5, p);
|
||||
Card destroy = addCardToZone("Annihilate", p, ZoneType.Hand);
|
||||
SpellAbility destroySA = destroy.getFirstSpellAbility();
|
||||
destroySA.getTargets().add(bear);
|
||||
|
||||
addCards("Island", 2, p);
|
||||
Card fizzle = addCardToZone("Mage's Guile", p, ZoneType.Hand);
|
||||
SpellAbility fizzleSA = fizzle.getFirstSpellAbility();
|
||||
fizzleSA.getTargets().add(bear);
|
||||
|
||||
GameSimulator sim = createSimulator(game, p);
|
||||
game = sim.getSimulatedGameState();
|
||||
|
||||
sim.simulateSpellAbility(destroySA, false);
|
||||
AssertJUnit.assertEquals(1, game.getStackZone().size());
|
||||
sim.simulateSpellAbility(fizzleSA);
|
||||
|
||||
// spell should fizzle so no card was drawn
|
||||
AssertJUnit.assertEquals(0, game.getPlayers().get(0).getCardsIn(ZoneType.Hand).size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
Name:Dead Ringers
|
||||
ManaCost:4 B
|
||||
Types:Sorcery
|
||||
A:SP$ Destroy | Cost$ 4 B | TargetMin$ 2 | TargetMax$ 2 | NoRegen$ True | ValidTgts$ Creature.nonBlack | TgtPrompt$ Select target nonblack creatures | RememberOriginalTargets$ True | SubAbility$ DBCleanup | ConditionShareAllColors$ DirectRemembered | SpellDescription$ Destroy two target nonblack creatures unless either one is a color the other isn't. They can't be regenerated.
|
||||
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
|
||||
A:SP$ Destroy | TargetMin$ 2 | TargetMax$ 2 | NoRegen$ True | ValidTgts$ Creature.nonBlack | TgtPrompt$ Select target nonblack creatures | ConditionNoDifferentColors$ Targeted | ConditionDefined$ Targeted | ConditionPresent$ Card | ConditionPresentCompare$ EQ2 | SpellDescription$ Destroy two target nonblack creatures unless either one is a color the other isn't. They can't be regenerated.
|
||||
AI:RemoveDeck:All
|
||||
Oracle:Destroy two target nonblack creatures unless either one is a color the other isn't. They can't be regenerated.
|
||||
|
||||
@@ -2,8 +2,8 @@ Name:Goblin Welder
|
||||
ManaCost:R
|
||||
Types:Creature Goblin Artificer
|
||||
PT:1/1
|
||||
A:AB$ Pump | Cost$ T | ValidTgts$ Artifact | TgtPrompt$ Select target artifact a player controls | RememberObjects$ ThisTargetedCard | Condition$ AllTargetsLegal | SubAbility$ DBTargetYard | StackDescription$ If both targets are still legal as this ability resolves, {p:TargetedController} simultaneously sacrifices {c:ThisTargetedCard} | SpellDescription$ Choose target artifact a player controls and target artifact card in that player's graveyard. If both targets are still legal as this ability resolves, that player simultaneously sacrifices the artifact and returns the artifact card to the battlefield.
|
||||
SVar:DBTargetYard:DB$ Pump | ValidTgts$ Artifact | TargetsWithDefinedController$ ParentTargetedController | TgtPrompt$ Select target artifact card in that player's graveyard | TgtZone$ Graveyard | PumpZone$ Graveyard | ImprintCards$ ThisTargetedCard | Condition$ AllTargetsLegal | StackDescription$ and returns {c:ThisTargetedCard} to the battlefield. | SubAbility$ DBBranch
|
||||
A:AB$ Pump | Cost$ T | ValidTgts$ Artifact | TgtPrompt$ Select target artifact a player controls | RememberObjects$ ThisTargetedCard | SubAbility$ DBTargetYard | StackDescription$ If both targets are still legal as this ability resolves, {p:TargetedController} simultaneously sacrifices {c:ThisTargetedCard} | SpellDescription$ Choose target artifact a player controls and target artifact card in that player's graveyard. If both targets are still legal as this ability resolves, that player simultaneously sacrifices the artifact and returns the artifact card to the battlefield.
|
||||
SVar:DBTargetYard:DB$ Pump | ValidTgts$ Artifact | TargetsWithDefinedController$ ParentTargetedController | TgtPrompt$ Select target artifact card in that player's graveyard | TgtZone$ Graveyard | PumpZone$ Graveyard | ImprintCards$ ThisTargetedCard | StackDescription$ and returns {c:ThisTargetedCard} to the battlefield. | SubAbility$ DBBranch
|
||||
SVar:DBBranch:DB$ Branch | BranchConditionSVar$ TargetCheck | BranchConditionSVarCompare$ GE2 | TrueSubAbility$ DBSacrifice | FalseSubAbility$ DBCleanup
|
||||
SVar:DBSacrifice:DB$ SacrificeAll | ValidCards$ Card.IsRemembered | SubAbility$ DBReturn
|
||||
SVar:DBReturn:DB$ ChangeZone | Defined$ Imprinted | Origin$ Graveyard | Destination$ Battlefield | SubAbility$ DBCleanup
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
Name:The Fall of Kroog
|
||||
ManaCost:4 R R
|
||||
Types:Sorcery
|
||||
A:SP$ Pump | ValidTgts$ Opponent | SubAbility$ DBDestroy | StackDescription$ SpellDescription | RememberOriginalTargets$ True | SpellDescription$ Choose target opponent. Destroy target land that player controls. CARDNAME deals 3 damage to that player and 1 damage to each creature they control.
|
||||
SVar:DBDestroy:DB$ Destroy | ValidTgts$ Land.ControlledBy ParentTarget,Land.ControlledBy Remembered | TgtPrompt$ Select target land that player controls | SubAbility$ DBDealDamage
|
||||
A:SP$ Pump | ValidTgts$ Opponent | SubAbility$ DBDestroy | StackDescription$ SpellDescription | SpellDescription$ Choose target opponent. Destroy target land that player controls. CARDNAME deals 3 damage to that player and 1 damage to each creature they control.
|
||||
SVar:DBDestroy:DB$ Destroy | ValidTgts$ Land.ControlledBy ParentTarget | TgtPrompt$ Select target land that player controls | SubAbility$ DBDealDamage
|
||||
SVar:DBDealDamage:DB$ DealDamage | Defined$ TargetedPlayer | NumDmg$ 3 | SubAbility$ DBDamageAll | DamageMap$ True | StackDescription$ None
|
||||
SVar:DBDamageAll:DB$ DamageAll | ValidCards$ Creature.ControlledBy TargetedPlayer | NumDmg$ 1 | SubAbility$ DBDamageResolve
|
||||
SVar:DBDamageResolve:DB$ DamageResolve | SubAbility$ DBCleanup
|
||||
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
|
||||
SVar:DBDamageResolve:DB$ DamageResolve
|
||||
Oracle:Choose target opponent. Destroy target land that player controls. The Fall of Kroog deals 3 damage to that player and 1 damage to each creature they control.
|
||||
|
||||
@@ -4,6 +4,6 @@ Types:Legendary Creature Dragon Spirit
|
||||
PT:5/5
|
||||
K:Flying
|
||||
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigSkipPhase | TriggerDescription$ When CARDNAME dies, target player skips their next untap step. Tap up to five target permanents that player controls.
|
||||
SVar:TrigSkipPhase:DB$ SkipPhase | ValidTgts$ Player | Step$ Untap | RememberOriginalTargets$ True | IsCurse$ True | SubAbility$ TrigTap
|
||||
SVar:TrigTap:DB$ Tap | TargetMin$ 0 | TargetMax$ 5 | ValidTgts$ Permanent.ControlledBy ParentTarget,Permanent.ControlledBy Remembered
|
||||
SVar:TrigSkipPhase:DB$ SkipPhase | ValidTgts$ Player | Step$ Untap | IsCurse$ True | SubAbility$ TrigTap
|
||||
SVar:TrigTap:DB$ Tap | TargetMin$ 0 | TargetMax$ 5 | ValidTgts$ Permanent.ControlledBy ParentTarget
|
||||
Oracle:Flying\nWhen Yosei, the Morning Star dies, target player skips their next untap step. Tap up to five target permanents that player controls.
|
||||
|
||||
Reference in New Issue
Block a user