Fix remembering players leading to crash for AI with Shifting Shadow (#2587)

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
This commit is contained in:
tool4ever
2023-02-28 11:02:54 +01:00
committed by GitHub
parent 0997762008
commit 1b655458e7
11 changed files with 21 additions and 25 deletions

View File

@@ -2492,12 +2492,12 @@ public class ComputerUtilCombat {
return poison; return poison;
} }
public static GameEntity addAttackerToCombat(SpellAbility sa, Card attacker, FCollection<GameEntity> defenders) { public static GameEntity addAttackerToCombat(SpellAbility sa, Card attacker, Iterable<? extends GameEntity> defenders) {
Combat combat = sa.getHostCard().getGame().getCombat(); Combat combat = sa.getHostCard().getGame().getCombat();
if (combat != null) { if (combat != null) {
// 1. If the card that spawned the attacker was sent at a planeswalker, attack the same. Consider improving. // 1. If the card that spawned the attacker was sent at a planeswalker, attack the same. Consider improving.
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard()); GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
if (def instanceof Card && ((Card)def).isPlaneswalker() && defenders.contains(def)) { if (def instanceof Card && ((Card)def).isPlaneswalker() && Iterables.contains(defenders, def)) {
return def; return def;
} }
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably. // 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.

View File

@@ -33,7 +33,6 @@ import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollection;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.*; import java.util.*;
@@ -1759,7 +1758,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) { public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
// Called when attaching Aura to player or adding creature to combat // Called when attaching Aura to player or adding creature to combat
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
return AttachAi.attachToPlayerAIPreferences(ai, sa, true, (List<Player>)options); return AttachAi.attachToPlayerAIPreferences(ai, sa, true, (List<Player>)options);
} }
@@ -1767,7 +1766,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override @Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) { protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
// should not be reached // should not be reached
return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params); return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params);

View File

@@ -34,7 +34,6 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollection;
public class CopyPermanentAi extends SpellAbilityAi { public class CopyPermanentAi extends SpellAbilityAi {
@Override @Override
@@ -254,7 +253,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
@Override @Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) { protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
final List<Card> cards = new PlayerCollection(options).getCreaturesInPlay(); final List<Card> cards = new PlayerCollection(options).getCreaturesInPlay();
Card chosen = ComputerUtilCard.getBestCreatureAI(cards); Card chosen = ComputerUtilCard.getBestCreatureAI(cards);
@@ -264,7 +263,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
@Override @Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) { protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
// should not be reached // should not be reached
return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params); return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params);

View File

@@ -27,7 +27,6 @@ import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.TextUtil; import forge.util.TextUtil;
import forge.util.collect.FCollection;
public class DigAi extends SpellAbilityAi { public class DigAi extends SpellAbilityAi {
@@ -190,7 +189,7 @@ public class DigAi extends SpellAbilityAi {
@Override @Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) { public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
// an opponent choose a card from // an opponent choose a card from
return Iterables.getFirst(options, null); return Iterables.getFirst(options, null);
@@ -199,7 +198,7 @@ public class DigAi extends SpellAbilityAi {
@Override @Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) { protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
// should not be reached // should not be reached
return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params); return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params);

View File

@@ -40,7 +40,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollection;
/** /**
* <p> * <p>
@@ -321,7 +320,7 @@ public class TokenAi extends SpellAbilityAi {
@Override @Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) { protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
return Iterables.getFirst(options, null); return Iterables.getFirst(options, null);
} }
@@ -332,7 +331,7 @@ public class TokenAi extends SpellAbilityAi {
@Override @Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) { protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) { if (params != null && params.containsKey("Attacker")) {
return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), options);
} }
// should not be reached // should not be reached
return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params); return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params);

View File

@@ -288,12 +288,12 @@ public class AbilityUtils {
} else { } else {
System.err.println("Warning: couldn't find trigger SA in the chain of SpellAbility " + sa); System.err.println("Warning: couldn't find trigger SA in the chain of SpellAbility " + sa);
} }
} else if (defined.equals("FirstRemembered")) { } else if (defined.equals("RememberedFirst")) {
Object o = hostCard.getFirstRemembered(); Object o = hostCard.getFirstRemembered();
if (o instanceof Card) { if (o instanceof Card) {
cards.add(game.getCardState((Card) o)); cards.add(game.getCardState((Card) o));
} }
} else if (defined.equals("LastRemembered")) { } else if (defined.equals("RememberedLast")) {
Object o = Iterables.getLast(hostCard.getRemembered(), null); Object o = Iterables.getLast(hostCard.getRemembered(), null);
if (o instanceof Card) { if (o instanceof Card) {
cards.add(game.getCardState((Card) o)); cards.add(game.getCardState((Card) o));

View File

@@ -2,12 +2,12 @@ Name:Mangara's Tome
ManaCost:5 ManaCost:5
Types:Artifact Types:Artifact
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigSearch | TriggerDescription$ When CARDNAME enters the battlefield, search your library for five cards, exile them in a face-down pile, and shuffle that pile. Then shuffle your library. T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigSearch | TriggerDescription$ When CARDNAME enters the battlefield, search your library for five cards, exile them in a face-down pile, and shuffle that pile. Then shuffle your library.
SVar:TrigSearch:DB$ ChangeZone | ChangeNum$ 5 | ChangeType$ Card | Origin$ Library | Destination$ Exile | ShuffleChangedPile$ True | ExileFaceDown$ True | RememberChanged$ True SVar:TrigSearch:DB$ ChangeZone | ChangeNum$ 5 | Mandatory$ True | ChangeType$ Card | Origin$ Library | Destination$ Exile | ShuffleChangedPile$ True | ExileFaceDown$ True | RememberChanged$ True
T:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | Static$ True | Execute$ TrigForget T:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | Static$ True | Execute$ TrigForget
SVar:TrigForget:DB$ Pump | ForgetObjects$ TriggeredCard SVar:TrigForget:DB$ Pump | ForgetObjects$ TriggeredCard
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ TrigReset | Static$ True T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ TrigReset | Static$ True
SVar:TrigReset:DB$ Cleanup | ClearRemembered$ True SVar:TrigReset:DB$ Cleanup | ClearRemembered$ True
A:AB$ Effect | Cost$ 2 | RememberObjects$ FirstRemembered | Triggers$ TrigLeavePlay | ReplacementEffects$ DrawReplace | ImprintCards$ Self | SpellDescription$ The next time you would draw a card this turn, instead put the top card of the exiled pile into its owner's hand. A:AB$ Effect | Cost$ 2 | RememberObjects$ RememberedFirst | Triggers$ TrigLeavePlay | ReplacementEffects$ DrawReplace | ImprintCards$ Self | SpellDescription$ The next time you would draw a card this turn, instead put the top card of the exiled pile into its owner's hand.
SVar:DrawReplace:Event$ Draw | ValidPlayer$ You | ReplaceWith$ RepMangarasTome | Description$ The next time you would draw a card this turn, instead put the top card of the exiled pile into its owner's hand. SVar:DrawReplace:Event$ Draw | ValidPlayer$ You | ReplaceWith$ RepMangarasTome | Description$ The next time you would draw a card this turn, instead put the top card of the exiled pile into its owner's hand.
SVar:TrigLeavePlay:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.IsImprinted | Execute$ ExileEffect | Static$ True SVar:TrigLeavePlay:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.IsImprinted | Execute$ ExileEffect | Static$ True
SVar:RepMangarasTome:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Hand | SubAbility$ ExileEffect SVar:RepMangarasTome:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Hand | SubAbility$ ExileEffect

View File

@@ -2,13 +2,13 @@ Name:Parallel Thoughts
ManaCost:3 U U ManaCost:3 U U
Types:Enchantment Types:Enchantment
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigSearch | TriggerDescription$ When CARDNAME enters the battlefield, search your library for seven cards, exile them in a face-down pile, and shuffle that pile. Then shuffle your library. T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigSearch | TriggerDescription$ When CARDNAME enters the battlefield, search your library for seven cards, exile them in a face-down pile, and shuffle that pile. Then shuffle your library.
SVar:TrigSearch:DB$ ChangeZone | ChangeNum$ 7 | ChangeType$ Card | Origin$ Library | Destination$ Exile | ShuffleChangedPile$ True | ExileFaceDown$ True | RememberChanged$ True SVar:TrigSearch:DB$ ChangeZone | ChangeNum$ 7 | ChangeType$ Card | Mandatory$ True | Origin$ Library | Destination$ Exile | ShuffleChangedPile$ True | ExileFaceDown$ True | RememberChanged$ True
T:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | Static$ True | Execute$ TrigForget T:Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ Exile | Destination$ Any | Static$ True | Execute$ TrigForget
SVar:TrigForget:DB$ Pump | ForgetObjects$ TriggeredCard SVar:TrigForget:DB$ Pump | ForgetObjects$ TriggeredCard
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ TrigReset | Static$ True T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ TrigReset | Static$ True
SVar:TrigReset:DB$ Cleanup | ClearRemembered$ True SVar:TrigReset:DB$ Cleanup | ClearRemembered$ True
R:Event$ Draw | ActiveZones$ Battlefield | ValidPlayer$ You | ReplaceWith$ RepParallelThoughts | Optional$ True | OptionalDecider$ You | Description$ If you would draw a card, you may instead put the top card of the pile you exiled into your hand. R:Event$ Draw | ActiveZones$ Battlefield | ValidPlayer$ You | ReplaceWith$ RepParallelThoughts | Optional$ True | OptionalDecider$ You | Description$ If you would draw a card, you may instead put the top card of the pile you exiled into your hand.
SVar:RepParallelThoughts:DB$ ChangeZone | Defined$ FirstRemembered | Origin$ Exile | Destination$ Hand SVar:RepParallelThoughts:DB$ ChangeZone | Defined$ RememberedFirst | Origin$ Exile | Destination$ Hand
AI:RemoveDeck:All AI:RemoveDeck:All
AI:RemoveDeck:Random AI:RemoveDeck:Random
Oracle:When Parallel Thoughts enters the battlefield, search your library for seven cards, exile them in a face-down pile, and shuffle that pile. Then shuffle your library.\nIf you would draw a card, you may instead put the top card of the pile you exiled into your hand. Oracle:When Parallel Thoughts enters the battlefield, search your library for seven cards, exile them in a face-down pile, and shuffle that pile. Then shuffle your library.\nIf you would draw a card, you may instead put the top card of the pile you exiled into your hand.

View File

@@ -10,6 +10,6 @@ SVar:ShadowUpkeepTrig:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZo
SVar:ShadowRememberSelf:DB$ Pump | ImprintCards$ OriginalHost | SubAbility$ ShadowDestroyEnchanted SVar:ShadowRememberSelf:DB$ Pump | ImprintCards$ OriginalHost | SubAbility$ ShadowDestroyEnchanted
SVar:ShadowDestroyEnchanted:DB$ Destroy | Defined$ Self | SubAbility$ ShadowRevealCards SVar:ShadowDestroyEnchanted:DB$ Destroy | Defined$ Self | SubAbility$ ShadowRevealCards
SVar:ShadowRevealCards:DB$ DigUntil | Valid$ Creature | ValidDescription$ creature | FoundDestination$ Battlefield | RevealedDestination$ Library | RevealedLibraryPosition$ -1 | RevealRandomOrder$ True | RememberFound$ True | SubAbility$ ShadowReattach SVar:ShadowRevealCards:DB$ DigUntil | Valid$ Creature | ValidDescription$ creature | FoundDestination$ Battlefield | RevealedDestination$ Library | RevealedLibraryPosition$ -1 | RevealRandomOrder$ True | RememberFound$ True | SubAbility$ ShadowReattach
SVar:ShadowReattach:DB$ Attach | Defined$ LastRemembered | Object$ Imprinted | SubAbility$ DBCleanup SVar:ShadowReattach:DB$ Attach | Defined$ RememberedLast | Object$ Imprinted | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearImprinted$ True SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearImprinted$ True
Oracle:Enchant creature\nEnchanted creature has haste and "At the beginning of your upkeep, destroy this creature. Reveal cards from the top of your library until you reveal a creature card. Put that card onto the battlefield and attach Shifting Shadow to it, then put all other cards revealed this way on the bottom of your library in a random order." Oracle:Enchant creature\nEnchanted creature has haste and "At the beginning of your upkeep, destroy this creature. Reveal cards from the top of your library until you reveal a creature card. Put that card onto the battlefield and attach Shifting Shadow to it, then put all other cards revealed this way on the bottom of your library in a random order."

View File

@@ -359,7 +359,7 @@ public final class CardScriptParser {
"Self", "OriginalHost", "EffectSource", "Equipped", "Enchanted", "Self", "OriginalHost", "EffectSource", "Equipped", "Enchanted",
"TopOfLibrary", "BottomOfLibrary", "Targeted", "ThisTargetedCard", "TopOfLibrary", "BottomOfLibrary", "Targeted", "ThisTargetedCard",
"ParentTarget", "Remembered", "DirectRemembered", "ParentTarget", "Remembered", "DirectRemembered",
"DelayTriggerRemembered", "FirstRemembered", "Clones", "Imprinted", "DelayTriggerRemembered", "RememberedFirst", "Clones", "Imprinted",
"ChosenCard", "SacrificedCards", "Sacrificed", "DiscardedCards", "ChosenCard", "SacrificedCards", "Sacrificed", "DiscardedCards",
"Discarded", "ExiledCards", "Exiled", "TappedCards", "Tapped", "Discarded", "ExiledCards", "Exiled", "TappedCards", "Tapped",
"UntappedCards", "Untapped", "Parent", "SourceFirstSpell"); "UntappedCards", "Untapped", "Parent", "SourceFirstSpell");

View File

@@ -743,14 +743,14 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
Card show = null; Card show = null;
Object o = null; Object o = null;
switch (sa.getParam("ShowCardInPrompt")) { switch (sa.getParam("ShowCardInPrompt")) {
case "FirstRemembered": case "RememberedFirst":
o = sa.getHostCard().getFirstRemembered(); o = sa.getHostCard().getFirstRemembered();
if (o instanceof Card) { if (o instanceof Card) {
show = (Card) o; show = (Card) o;
} }
break; break;
case "LastRemembered": case "RememberedLast":
o = sa.getHostCard().getFirstRemembered(); o = Iterables.getLast(sa.getHostCard().getRemembered(), null);
if (o instanceof Card) { if (o instanceof Card) {
show = (Card) o; show = (Card) o;
} }