Merge branch 'master' into 'master'

Reimplement the AI logic for Astral cards again.

See merge request core-developers/forge!5696
This commit is contained in:
Michael Kamensky
2021-10-31 05:15:56 +00:00
10 changed files with 87 additions and 12 deletions

View File

@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import forge.game.GameEntity;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
@@ -616,6 +617,28 @@ public class SpecialCardAi {
} }
} }
// Goblin Polka Band
public static class GoblinPolkaBand {
public static boolean consider(final Player ai, final SpellAbility sa) {
int maxPotentialTgts = Lists.newArrayList(Iterables.filter(ai.getOpponents().getCreaturesInPlay(), CardPredicates.Presets.UNTAPPED)).size();
int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R");
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
if (numTgts == 0) {
return false;
}
// Set Announce
sa.getHostCard().setSVar("TgtNum", String.valueOf(numTgts));
// Simulate random targeting
List<GameEntity> validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true);
sa.resetTargets();
sa.getTargets().addAll(Aggregates.random(validTgts, numTgts));
return true;
}
}
// Guilty Conscience // Guilty Conscience
public static class GuiltyConscience { public static class GuiltyConscience {
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) { public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
@@ -1157,6 +1180,33 @@ public class SpecialCardAi {
} }
} }
// Power Struggle
public static class PowerStruggle {
public static boolean considerFirstTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = (Card)Aggregates.random(sa.getTargetRestrictions().getAllCandidates(sa, true));
if (firstTgt != null) {
sa.getTargets().add(firstTgt);
return true;
} else {
return false;
}
}
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = sa.getParent().getTargetCard();
Iterable<Card> candidates = Iterables.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
Predicates.and(CardPredicates.sharesCardTypeWith(firstTgt), CardPredicates.isTargetableBy(sa)));
Card secondTgt = Aggregates.random(candidates);
if (secondTgt != null) {
sa.resetTargets();
sa.getTargets().add(secondTgt);
return true;
} else {
return false;
}
}
}
// Price of Progress // Price of Progress
public static class PriceOfProgress { public static class PriceOfProgress {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {

View File

@@ -24,6 +24,7 @@ import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
@@ -146,6 +147,16 @@ public class ChooseCardAi extends SpellAbilityAi {
return checkApiLogic(ai, sa); return checkApiLogic(ai, sa);
} }
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("AtOppEOT")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
}
return super.checkPhaseRestrictions(ai, sa, ph);
}
/* (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

@@ -4,6 +4,7 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -80,6 +81,10 @@ public class ControlExchangeAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if ("PowerStruggle".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.PowerStruggle.considerSecondTarget(aiPlayer, sa);
}
// for TrigTwoTargets logic, only get the opponents' cards for the first target // for TrigTwoTargets logic, only get the opponents' cards for the first target
CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ? CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ?
aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) : aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) :

View File

@@ -436,6 +436,11 @@ public class PumpAi extends PumpAiBase {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
sa.resetTargets(); sa.resetTargets();
if ("PowerStruggle".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.PowerStruggle.considerFirstTarget(ai, sa);
}
if (sa.hasParam("TargetingPlayer") && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) { if (sa.hasParam("TargetingPlayer") && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer); sa.setTargetingPlayer(targetingPlayer);

View File

@@ -1,11 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiController; import forge.ai.*;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -48,6 +43,10 @@ public class TapAi extends TapAiBase {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Cost abCost = sa.getPayCosts(); final Cost abCost = sa.getPayCosts();
if ("GoblinPolkaBand".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.GoblinPolkaBand.consider(ai, sa);
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
return false; return false;
} }

View File

@@ -98,7 +98,12 @@ public class TokenAi extends SpellAbilityAi {
sa.getRootAbility().setXManaCostPaid(x); sa.getRootAbility().setXManaCostPaid(x);
} }
if (x <= 0) { if (x <= 0) {
return false; // 0 tokens or 0 toughness token(s) if ("RandomPT".equals(sa.getParam("AILogic"))) {
// e.g. Necropolis of Azar - we're guaranteed at least 1 toughness from the ability
x = 1;
} else {
return false; // 0 tokens or 0 toughness token(s)
}
} }
} }

View File

@@ -2,7 +2,7 @@ Name:Goblin Polka Band
ManaCost:R R ManaCost:R R
Types:Creature Goblin Types:Creature Goblin
PT:1/1 PT:1/1
A:AB$ Tap | Announce$ TgtNum | AnnounceTitle$ any number of creatures to target | Cost$ X 2 T | XColor$ R | CostDesc$ {2}, {T}: | ValidTgts$ Creature.untapped | TargetMin$ TgtNum | TargetMax$ TgtNum | TargetsAtRandom$ True | RememberTargets$ True | SubAbility$ GoblinHangover | SpellDescription$ Tap any number of random target creatures. Goblins tapped in this way do not untap during their controllers' next untap phases. This ability costs {R} more to activate for each target. A:AB$ Tap | Announce$ TgtNum | AnnounceTitle$ any number of creatures to target | Cost$ X 2 T | XColor$ R | CostDesc$ {2}, {T}: | ValidTgts$ Creature.untapped | TargetMin$ TgtNum | TargetMax$ TgtNum | TargetsAtRandom$ True | RememberTargets$ True | AILogic$ GoblinPolkaBand | SubAbility$ GoblinHangover | SpellDescription$ Tap any number of random target creatures. Goblins tapped in this way do not untap during their controllers' next untap phases. This ability costs {R} more to activate for each target.
SVar:GoblinHangover:DB$ PumpAll | ValidCards$ Goblin.IsRemembered | KW$ HIDDEN This card doesn't untap during your next untap step. | Duration$ Permanent SVar:GoblinHangover:DB$ PumpAll | ValidCards$ Goblin.IsRemembered | KW$ HIDDEN This card doesn't untap during your next untap step. | Duration$ Permanent
SVar:TgtNum:Number$0 SVar:TgtNum:Number$0
SVar:X:SVar$TgtNum SVar:X:SVar$TgtNum

View File

@@ -2,6 +2,6 @@ Name:Power Struggle
ManaCost:2 U U U ManaCost:2 U U U
Types:Enchantment Types:Enchantment
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Player | TriggerZones$ Battlefield | Execute$ TrigPump | TriggerController$ TriggeredPlayer | TriggerDescription$ At the beginning of each player's upkeep, that player exchanges control of random target artifact, creature or land he or she controls, for control of random target permanent of the same type that a random opponent controls. T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Player | TriggerZones$ Battlefield | Execute$ TrigPump | TriggerController$ TriggeredPlayer | TriggerDescription$ At the beginning of each player's upkeep, that player exchanges control of random target artifact, creature or land he or she controls, for control of random target permanent of the same type that a random opponent controls.
SVar:TrigPump:DB$ Pump | TargetsWithDefinedController$ TriggeredPlayer | ValidTgts$ Artifact,Creature,Land | TargetsAtRandom$ True | SubAbility$ DBExchangeControl SVar:TrigPump:DB$ Pump | TargetsWithDefinedController$ TriggeredPlayer | ValidTgts$ Artifact,Creature,Land | TargetsAtRandom$ True | AILogic$ PowerStruggle | SubAbility$ DBExchangeControl
SVar:DBExchangeControl:DB$ ExchangeControl | Defined$ ParentTarget | ValidTgts$ Artifact,Creature,Land | TargetsWithDefinedController$ Player.OpponentOf TriggeredPlayer | TargetsWithSharedCardType$ ParentTarget | TargetsAtRandom$ True SVar:DBExchangeControl:DB$ ExchangeControl | Defined$ ParentTarget | ValidTgts$ Artifact,Creature,Land | TargetsWithDefinedController$ Player.OpponentOf TriggeredPlayer | TargetsWithSharedCardType$ ParentTarget | TargetsAtRandom$ True | AILogic$ PowerStruggle
Oracle:At the beginning of each player's upkeep, that player exchanges control of random target artifact, creature or land he or she controls, for control of random target permanent of the same type that a random opponent controls. Oracle:At the beginning of each player's upkeep, that player exchanges control of random target artifact, creature or land he or she controls, for control of random target permanent of the same type that a random opponent controls.

View File

@@ -3,7 +3,7 @@ ManaCost:2 B B
Types:Enchantment Types:Enchantment
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.nonBlack | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever a nonblack creature dies, put a husk counter on CARDNAME. T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.nonBlack | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever a nonblack creature dies, put a husk counter on CARDNAME.
SVar:TrigPutCounter:DB$ PutCounter | CounterType$ HUSK SVar:TrigPutCounter:DB$ PutCounter | CounterType$ HUSK
A:AB$ Token | Cost$ 5 SubCounter<1/HUSK> | TokenScript$ spawn_of_azar | TokenPower$ X | TokenToughness$ Y | SpellDescription$ Create an X/Y black Spawn creature token with swampwalk named Spawn of Azar, where X and Y are numbers chosen at random from 1 to 3. A:AB$ Token | Cost$ 5 SubCounter<1/HUSK> | TokenScript$ spawn_of_azar | TokenPower$ X | TokenToughness$ Y | AILogic$ RandomPT | SpellDescription$ Create an X/Y black Spawn creature token with swampwalk named Spawn of Azar, where X and Y are numbers chosen at random from 1 to 3.
SVar:X:Count$Random.1.3 SVar:X:Count$Random.1.3
SVar:Y:Count$Random.1.3 SVar:Y:Count$Random.1.3
DeckHas:Ability$Counters & Ability$Token DeckHas:Ability$Counters & Ability$Token

View File

@@ -1,7 +1,7 @@
Name:Pandora's Box Name:Pandora's Box
ManaCost:5 ManaCost:5
Types:Artifact Types:Artifact
A:AB$ ChooseCard | Cost$ 3 T | Choices$ Creature | AtRandom$ True | AllCards$ True | SubAbility$ DBRepeatEach | StackDescription$ SpellDescription | SpellDescription$ Choose a creature card at random from all players' decklists. Each player flips a coin. Each player whose coin comes up heads creates a token that's a copy of that card. A:AB$ ChooseCard | Cost$ 3 T | Choices$ Creature | AtRandom$ True | AllCards$ True | SubAbility$ DBRepeatEach | AILogic$ AtOppEOT | StackDescription$ SpellDescription | SpellDescription$ Choose a creature card at random from all players' decklists. Each player flips a coin. Each player whose coin comes up heads creates a token that's a copy of that card.
SVar:DBRepeatEach:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ DBFlip | SubAbility$ DBCleanup SVar:DBRepeatEach:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ DBFlip | SubAbility$ DBCleanup
SVar:DBFlip:DB$ FlipACoin | Flipper$ Remembered | NoCall$ True | HeadsSubAbility$ DBCopyPermanent SVar:DBFlip:DB$ FlipACoin | Flipper$ Remembered | NoCall$ True | HeadsSubAbility$ DBCopyPermanent
SVar:DBCopyPermanent:DB$ CopyPermanent | Defined$ ChosenCard | Controller$ Remembered SVar:DBCopyPermanent:DB$ CopyPermanent | Defined$ ChosenCard | Controller$ Remembered