Fix AI not targeting triggered Random Charm (#8955)

This commit is contained in:
tool4ever
2025-10-19 20:57:11 +02:00
committed by GitHub
parent 80a4a42575
commit 6f38479190
10 changed files with 33 additions and 24 deletions

View File

@@ -377,7 +377,7 @@ public class AiController {
if (card.isSaga()) { if (card.isSaga()) {
for (final Trigger tr : card.getTriggers()) { for (final Trigger tr : card.getTriggers()) {
if (tr.getMode() != TriggerType.CounterAdded || !tr.isChapter()) { if (!tr.isChapter()) {
continue; continue;
} }
@@ -393,6 +393,7 @@ public class AiController {
return false; return false;
} }
// usually later chapters make use of an earlier one
break; break;
} }
} }

View File

@@ -1274,9 +1274,15 @@ public class PlayerControllerAi extends PlayerController {
} }
} }
private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory) { private boolean prepareSingleSa(final Card host, SpellAbility sa, boolean isMandatory) {
if (sa.getApi() == ApiType.Charm) { if (sa.getApi() == ApiType.Charm) {
return CharmEffect.makeChoices(sa); if (!CharmEffect.makeChoices(sa)) {
return false;
}
if (!sa.hasParam("Random")) {
return true;
}
sa = sa.getSubAbility();
} }
if (sa.hasParam("TargetingPlayer")) { if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(host, sa.getParam("TargetingPlayer"), sa).get(0); Player targetingPlayer = AbilityUtils.getDefinedPlayers(host, sa.getParam("TargetingPlayer"), sa).get(0);

View File

@@ -634,6 +634,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
// not urgent, get the largest creature possible // not urgent, get the largest creature possible
// TODO checkETBEffects
return ComputerUtilCard.getBestCreatureAI(list); return ComputerUtilCard.getBestCreatureAI(list);
} }
@@ -1546,10 +1547,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (fetchList.isEmpty()) { if (fetchList.isEmpty()) {
return null; return null;
} }
String type = sa.getParam("ChangeType"); String type = sa.getParamOrDefault("ChangeType", "");
if (type == null) {
type = "Card";
}
Card c = null; Card c = null;
final Player activator = sa.getActivatingPlayer(); final Player activator = sa.getActivatingPlayer();

View File

@@ -94,18 +94,21 @@ public class CharmAi extends SpellAbilityAi {
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) { private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
List<AbilitySub> chosenList = Lists.newArrayList(); List<AbilitySub> chosenList = Lists.newArrayList();
AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
boolean allowRepeat = sa.hasParam("CanRepeatModes"); // FIXME: unused for now, the AI doesn't know how to effectively handle repeated choices // TODO unused for now, the AI doesn't know how to effectively handle repeated choices
boolean allowRepeat = sa.hasParam("CanRepeatModes");
// Pawprint // Pawprint
final int pawprintLimit = sa.hasParam("Pawprint") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Pawprint"), sa) : 0; final int pawprintLimit = sa.hasParam("Pawprint") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Pawprint"), sa) : 0;
if (pawprintLimit > 0) { if (pawprintLimit > 0) {
Collections.reverse(choices); // try to pay for the more expensive subs first // try to pay for the more expensive subs first
Collections.reverse(choices);
} }
int pawprintAmount = 0; int pawprintAmount = 0;
// First pass using standard canPlayAi() for good choices // First pass using standard canPlayAi() for good choices
for (AbilitySub sub : choices) { for (AbilitySub sub : choices) {
sub.setActivatingPlayer(ai); sub.setActivatingPlayer(ai);
// TODO refactor to obtain the AiAbilityDecision instead, then we can check all to sort by value
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) { if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
if (pawprintLimit > 0) { if (pawprintLimit > 0) {
int curPawprintAmount = AbilityUtils.calculateAmount(sub.getHostCard(), sub.getParamOrDefault("Pawprint", "0"), sub); int curPawprintAmount = AbilityUtils.calculateAmount(sub.getHostCard(), sub.getParamOrDefault("Pawprint", "0"), sub);
@@ -116,7 +119,8 @@ public class CharmAi extends SpellAbilityAi {
} }
chosenList.add(sub); chosenList.add(sub);
if (chosenList.size() == num) { if (chosenList.size() == num) {
return chosenList; // maximum choices reached // maximum choices reached
return chosenList;
} }
} }
} }
@@ -145,7 +149,8 @@ public class CharmAi extends SpellAbilityAi {
} }
} }
if (chosenList.size() < min) { if (chosenList.size() < min) {
chosenList.clear(); // not enough choices // not enough choices
chosenList.clear();
} }
return chosenList; return chosenList;
} }

View File

@@ -72,16 +72,14 @@ public class ChooseCardNameAi extends SpellAbilityAi {
// 5 percent chance to cast per opposing card with a non mana ability // 5 percent chance to cast per opposing card with a non mana ability
if (MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size()) { if (MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (mandatory) { if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)

View File

@@ -1175,7 +1175,7 @@ public class CountersPutAi extends CountersAi {
@Override @Override
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) { public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
if (sa.hasParam("ReadAhead")) { if (sa.isKeyword(Keyword.READ_AHEAD)) {
return 1; return 1;
} }
return max; return max;

View File

@@ -982,6 +982,9 @@ public class AbilityUtils {
for (final Card c : getDefinedCards(card, "Targeted", sa)) { for (final Card c : getDefinedCards(card, "Targeted", sa)) {
players.add(c.getOwner()); players.add(c.getOwner());
} }
for (final SpellAbility s : getDefinedSpellAbilities(card, "Targeted", sa)) {
players.add(s.getHostCard().getOwner());
}
} else if (defined.equals("TargetedAndYou") && sa instanceof SpellAbility) { } else if (defined.equals("TargetedAndYou") && sa instanceof SpellAbility) {
final SpellAbility saTargeting = ((SpellAbility)sa).getSATargetingPlayer(); final SpellAbility saTargeting = ((SpellAbility)sa).getSATargetingPlayer();
if (saTargeting != null) { if (saTargeting != null) {
@@ -1118,10 +1121,8 @@ public class AbilityUtils {
final String replacingType = defined.substring(8); final String replacingType = defined.substring(8);
o = root.getReplacingObject(AbilityKey.fromString(replacingType)); o = root.getReplacingObject(AbilityKey.fromString(replacingType));
} }
if (o != null) { if (o instanceof Player) {
if (o instanceof Player) { players.add((Player) o);
players.add((Player) o);
}
} }
} else if (defined.startsWith("Non")) { } else if (defined.startsWith("Non")) {
players.addAll(game.getPlayersInTurnOrder()); players.addAll(game.getPlayersInTurnOrder());

View File

@@ -2496,7 +2496,7 @@ public class CardFactoryUtil {
inst.addReplacement(re); inst.addReplacement(re);
} else if (keyword.equals("Read ahead")) { } else if (keyword.equals("Read ahead")) {
String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | Secondary$ True | ReplacementResult$ Updated | Description$ Choose a chapter and start with that many lore counters."; String repeffstr = "Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | Secondary$ True | ReplacementResult$ Updated | Description$ Choose a chapter and start with that many lore counters.";
String effStr = "DB$ PutCounter | Defined$ Self | CounterType$ LORE | ETB$ True | UpTo$ True | UpToMin$ 1 | ReadAhead$ True | CounterNum$ FinalChapterNr"; String effStr = "DB$ PutCounter | Defined$ Self | CounterType$ LORE | ETB$ True | UpTo$ True | UpToMin$ 1 | CounterNum$ FinalChapterNr";
SpellAbility saCounter = AbilityFactory.getAbility(effStr, card); SpellAbility saCounter = AbilityFactory.getAbility(effStr, card);
saCounter.setSVar("FinalChapterNr", "Count$FinalChapterNr"); saCounter.setSVar("FinalChapterNr", "Count$FinalChapterNr");

View File

@@ -4,7 +4,7 @@ Types:Artifact
K:Flash K:Flash
K:ETBReplacement:Other:DBNameCard K:ETBReplacement:Other:DBNameCard
SVar:DBNameCard:DB$ NameCard | Defined$ You | SpellDescription$ As CARDNAME enters, choose a card name. SVar:DBNameCard:DB$ NameCard | Defined$ You | SpellDescription$ As CARDNAME enters, choose a card name.
S:Mode$ RaiseCost | EffectZone$ Battlefield | ValidCard$ Card.NamedCard | Type$ Spell | Activator$ Player | Amount$ 3 | Description$ Spells with the chosen name cost 3 more to cast. S:Mode$ RaiseCost | EffectZone$ Battlefield | ValidCard$ Card.NamedCard | Type$ Spell | Activator$ Player | Amount$ 3 | Description$ Spells with the chosen name cost {3} more to cast.
S:Mode$ CantBeActivated | ValidCard$ Card.NamedCard | ValidSA$ Activated.!ManaAbility | Description$ Activated abilities of sources with the chosen name can't be activated unless they're mana abilities. S:Mode$ CantBeActivated | ValidCard$ Card.NamedCard | ValidSA$ Activated.!ManaAbility | Description$ Activated abilities of sources with the chosen name can't be activated unless they're mana abilities.
AI:RemoveDeck:Random AI:RemoveDeck:Random
Oracle:Flash\nAs Disruptor Flute enters, choose a card name.\nSpells with the chosen name cost 3 more to cast.\nActivated abilities of sources with the chosen name can't be activated unless they're mana abilities. Oracle:Flash\nAs Disruptor Flute enters, choose a card name.\nSpells with the chosen name cost {3} more to cast.\nActivated abilities of sources with the chosen name can't be activated unless they're mana abilities.

View File

@@ -5,7 +5,7 @@ PT:2/2
K:Trample K:Trample
K:etbCounter:P1P1:X:no Condition:NICKNAME enters with a +1/+1 counter on him for each land you control. K:etbCounter:P1P1:X:no Condition:NICKNAME enters with a +1/+1 counter on him for each land you control.
SVar:X:Count$Valid Land.YouCtrl SVar:X:Count$Valid Land.YouCtrl
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigChangeZone | TriggerDescription$ When NICKNAME dies, return this card to your hand. T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigReturn | TriggerDescription$ When NICKNAME dies, return this card to your hand.
SVar:TrigReturn:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Hand SVar:TrigReturn:DB$ ChangeZone | Defined$ TriggeredNewCardLKICopy | Origin$ Graveyard | Destination$ Hand
DeckHas:Ability$Counters DeckHas:Ability$Counters
Oracle:Trample (This creature can deal excess combat damage to the player it's attacking.)\nMichelangelo enters with a +1/+1 counter on him for each land you control.\nWhen Michelangelo dies, return this card to your hand. Oracle:Trample (This creature can deal excess combat damage to the player it's attacking.)\nMichelangelo enters with a +1/+1 counter on him for each land you control.\nWhen Michelangelo dies, return this card to your hand.