Compare commits

..

2 Commits

Author SHA1 Message Date
Hans Mackowiak
a21656521f Update Card.java
fix
2025-07-07 13:10:09 +02:00
Hans Mackowiak
bdec4095c0 Use ImmutableList.Builder
Use ImmutableList.Builder instead of ImmutableList.of(Iterables.concat())
2025-07-07 12:58:05 +02:00
2347 changed files with 8851 additions and 185192 deletions

View File

@@ -1,4 +1,4 @@
# ⚔️ Forge: The Magic: The Gathering Rules Engine
# ⚔️ Forge: The Magic: The Gathering Rules Engine
Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
@@ -47,13 +47,11 @@ Embark on a thrilling single-player journey where you can:
- Challenge diverse AI opponents.
- Collect cards and items to boost your abilities.
<img width="1282" height="752" alt="Shandalar World" src="https://github.com/user-attachments/assets/9af31471-d688-442f-9418-9807d8635b72" />
![Adventure Mode](https://downloads.cardforge.org/images/site/adventure-mode.png "Adventure Mode")
### 🔍 Quest Modes
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" />
### 🤖 AI Formats
Test your skills against AI in multiple formats:
- **Sealed**
@@ -63,8 +61,6 @@ Test your skills against AI in multiple formats:
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
---
## 💬 Support & Community

View File

@@ -1,9 +0,0 @@
package forge.ai;
public record AiAbilityDecision(int rating, AiPlayDecision decision) {
private static int MIN_RATING = 30;
public boolean willingToPlay() {
return rating > MIN_RATING && decision.willingToPlay();
}
}

View File

@@ -145,15 +145,13 @@ public class AiAttackController {
sa.setActivatingPlayer(defender);
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
continue;
}
if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
} else if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
continue;
}
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
if (animatedCopy.isCreature()) {
// TODO imprecise, only works 100% for colorless mana
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
sa.getPayCosts().getTotalMana().getCMC() : 0;
sa.getPayCosts().getTotalMana().getCMC() : 0; // FIXME: imprecise, only works 100% for colorless mana
if (totalMana - manaReserved >= saCMC) {
manaReserved += saCMC;
defenders.add(animatedCopy);

View File

@@ -59,6 +59,7 @@ public class AiCardMemory {
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
BOUNCED_THIS_TURN, // These cards were bounced this turn
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part

View File

@@ -66,11 +66,13 @@ import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import java.util.*;
import java.util.concurrent.FutureTask;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -481,7 +483,7 @@ public class AiController {
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
// don't play MDFC land if other side is spell and enough lands are available
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
return false;
}
@@ -909,14 +911,56 @@ public class AiController {
}
}
// this is the "heaviest" check, which also sets up targets, defines X, etc.
AiPlayDecision canPlay = canPlaySa(sa);
int oldCMC = -1;
boolean xCost = sa.costHasX() || host.hasKeyword(Keyword.STRIVE) || sa.getApi() == ApiType.Charm;
if (!xCost) {
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
return AiPlayDecision.CantAfford;
}
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
if (!sa.getAllTargetChoices().isEmpty()) {
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
}
}
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
if (canPlay != AiPlayDecision.WillPlay) {
return canPlay;
}
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// Account for possible Ward after the spell is fully targeted
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
if (!sa.isSpell() || sa.isCounterableBy(null)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
xCost |= wardCost.getTotalMana().getCMC() > 0;
}
SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
return AiPlayDecision.CostNotAcceptable;
}
}
}
}
}
// check if some target raised cost
if (!xCost && oldCMC > -1) {
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
if (finalCMC > oldCMC) {
xCost = true;
}
}
if (xCost && !ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
return AiPlayDecision.CantAfford;
@@ -929,6 +973,8 @@ public class AiController {
return AiPlayDecision.CantAfford;
}
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
// are willing to play the SA
return AiPlayDecision.WillPlay;
}
@@ -971,7 +1017,7 @@ public class AiController {
Sentry.setExtra("Card", card.getName());
Sentry.setExtra("SA", sa.toString());
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayWithSubs(player, sa).willingToPlay();
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);
// remove added extra
Sentry.removeExtra("Card");
@@ -1349,9 +1395,9 @@ public class AiController {
if (spell instanceof SpellApiBased) {
boolean chance = false;
if (withoutPayingManaCost) {
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory).willingToPlay();
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
} else {
chance = SpellApiToAi.Converter.get(spell).doTrigger(player, spell, mandatory);
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
}
if (!chance) {
return AiPlayDecision.TargetingFailed;
@@ -1664,7 +1710,8 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
final ExecutorService executor = Executors.newSingleThreadExecutor();
Future<SpellAbility> future = executor.submit(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1742,18 +1789,11 @@ public class AiController {
return null;
});
Thread t = new Thread(future);
t.start();
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
try {
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
try {
t.stop();
} catch (UnsupportedOperationException ex) {
// Android and Java 20 dropped support to stop so sadly thread will keep running
future.cancel(true);
}
future.cancel(true);
return null;
}
}
@@ -1764,9 +1804,9 @@ public class AiController {
for (int i = 0; i < numToExile; i++) {
Card chosen = null;
for (final Card c : grave) {
// Exile noncreatures first in case we can revive
// Might wanna do some additional checking here for Flashback and the like
for (final Card c : grave) { // Exile noncreatures first in
// case we can revive. Might wanna do some additional
// checking here for Flashback and the like.
if (!c.isCreature()) {
chosen = c;
break;
@@ -1787,12 +1827,12 @@ public class AiController {
return toExile;
}
public boolean doTrigger(SpellAbility sa, boolean mandatory) {
if (sa instanceof WrappedAbility)
return doTrigger(((WrappedAbility) sa).getWrappedAbility(), mandatory);
if (sa.getApi() != null)
return SpellApiToAi.Converter.get(sa).doTrigger(player, sa, mandatory);
if (sa.getPayCosts() == Cost.Zero && !sa.usesTargeting()) {
public boolean doTrigger(SpellAbility spell, boolean mandatory) {
if (spell instanceof WrappedAbility)
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
if (spell.getApi() != null)
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
return true;
}

View File

@@ -767,12 +767,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
public PaymentDecision visit(CostRemoveCounter cost) {
final String amount = cost.getAmount();
final String type = cost.getType();
final GameEntityCounterTable counterTable = new GameEntityCounterTable();
// TODO Help AI filter card with most useless counters and put those counters in countertable for things like
// Moxite Refinery, similar to CostRemoveAnyCounter
// Probably a lot of that decision making can be re-used or pulled out for both PaymentDecisions to use
if (cost.counter == null) return null;
int c;
@@ -801,8 +795,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
for (Card card : typeList) {
if (card.getCounters(cost.counter) >= c) {
counterTable.put(null, card, cost.counter, c);
return PaymentDecision.counters(counterTable);
return PaymentDecision.card(card, c);
}
}
return null;
@@ -813,8 +806,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
counterTable.put(null, source, cost.counter, c);
return PaymentDecision.counters(counterTable);
return PaymentDecision.card(source, c);
}
@Override

View File

@@ -1,52 +1,21 @@
package forge.ai;
public enum AiPlayDecision {
// Play decision reasons
WillPlay,
MandatoryPlay,
PlayToEmptyHand,
ImpactCombat,
ResponseToStackResolve,
AddBoardPresence,
Removal,
Tempo,
CardAdvantage,
// Play later decisions
WaitForCombat,
WaitForMain2,
WaitForEndOfTurn,
StackNotEmpty,
AnotherTime,
// Don't play decision reasons
WillPlay,
CantPlaySa,
CantPlayAi,
CantAfford,
CantAffordX,
DoesntImpactCombat,
DoesntImpactGame,
MissingLogic,
WaitForMain2,
AnotherTime,
MissingNeededCards,
TimingRestrictions,
MissingPhaseRestrictions,
ConditionsNotMet,
NeedsToPlayCriteriaNotMet,
StopRunawayActivations,
TargetingFailed,
CostNotAcceptable,
LifeInDanger,
WouldDestroyLegend,
WouldDestroyOtherPlaneswalker,
WouldBecomeZeroToughnessCreature,
WouldDestroyWorldEnchantment,
BadEtbEffects,
CurseEffects;
public boolean willingToPlay() {
return switch (this) {
case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true;
default -> false;
};
}
CurseEffects
}

View File

@@ -864,7 +864,7 @@ public class ComputerUtil {
// Run non-mandatory trigger.
// These checks only work if the Executing SpellAbility is an Ability_Sub.
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTrigger(ai, exSA, false)) {
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
// AI would not run this trigger if given the chance
return sacrificed;
}
@@ -1074,80 +1074,6 @@ public class ComputerUtil {
return prevented;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
@@ -1319,6 +1245,80 @@ public class ComputerUtil {
return false;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final SpellAbility sub = sa.getSubAbility();
@@ -1327,6 +1327,7 @@ public class ComputerUtil {
return true;
}
// Cipher spells
if (sub != null) {
final ApiType api = sub.getApi();
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
@@ -1384,14 +1385,13 @@ public class ComputerUtil {
// returns true if the AI should stop using the ability
public static boolean preventRunAwayActivations(final SpellAbility sa) {
if (!sa.isActivatedAbility()) {
return false;
}
int activations = sa.getActivationsThisTurn();
//10 activations should still be acceptable
if (activations < 10) {
if (!sa.isIntrinsic()) {
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
}
if (activations < 10) { //10 activations per turn should still be acceptable
return false;
}
@@ -1621,6 +1621,7 @@ public class ComputerUtil {
damage = dmg;
}
// Triggered abilities
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
for (final Trigger t : c.getTriggers()) {
if (TriggerType.Attacks.equals(t.getMode())) {
@@ -2551,7 +2552,7 @@ public class ComputerUtil {
String logic = sa.getParam("AILogic");
switch (logic) {
case "Torture":
return options.get(1);
return "Torture";
case "GraceOrCondemnation":
List<ZoneType> graceZones = new ArrayList<ZoneType>();
graceZones.add(ZoneType.Battlefield);
@@ -2559,12 +2560,12 @@ public class ComputerUtil {
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
return options.get(aiGrace > humanGrace ? 0 : 1);
return aiGrace > humanGrace ? "Grace" : "Condemnation";
case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
CardCollection computerlist = ai.getCreaturesInPlay();
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
case "Judgment":
if (votes.isEmpty()) {
CardCollection list = new CardCollection();
@@ -2578,71 +2579,68 @@ public class ComputerUtil {
return Iterables.getFirst(votes.keySet(), null);
case "Protection":
if (votes.isEmpty()) {
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
List<String> restrictedToColors = Lists.newArrayList();
for (Object o : options) {
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
restrictedToColors.put(sp.getOriginalDescription(), sp);
if (o instanceof String) {
restrictedToColors.add((String) o);
}
}
}
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
}
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
SpellAbility feather = (SpellAbility)options.get(0);
SpellAbility quill = (SpellAbility)options.get(1);
// try to mill opponent with Quill vote
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
int numQuill = votes.get(quill).size();
int numQuill = votes.get("Quill").size();
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? feather : quill;
return opponent ? "Feather" : "Quill";
}
// if source is not on the battlefield anymore, choose +1/+1 ones
if (!game.getCardState(source).isInPlay()) {
return opponent ? feather : quill;
return opponent ? "Feather" : "Quill";
}
// if no hand cards, try to mill opponent
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
return opponent ? quill : feather;
return opponent ? "Quill" : "Feather";
}
// AI has something to discard
if (ai.equals(controller)) {
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
return quill;
return "Quill";
}
}
// default card draw and discard are better than +1/+1 counter
return opponent ? feather : quill;
return opponent ? "Feather" : "Quill";
case "StrengthOrNumbers":
SpellAbility strength = (SpellAbility)options.get(0);
SpellAbility numbers = (SpellAbility)options.get(1);
// similar to fabricate choose +1/+1 or Token
int numStrength = votes.get(strength).size();
int numNumbers = votes.get(numbers).size();
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
int numStrength = votes.get("Strength").size();
int numNumbers = votes.get("Numbers").size();
Card token = TokenAi.spawnToken(controller, numbers);
Card token = TokenAi.spawnToken(controller, saToken);
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? strength : numbers;
return opponent ? "Strength" : "Numbers";
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? strength : numbers;
return opponent ? "Strength" : "Numbers";
}
// token would not survive
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
return opponent ? numbers : strength;
return opponent ? "Numbers" : "Strength";
}
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
@@ -2663,40 +2661,35 @@ public class ComputerUtil {
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
case "SproutOrHarvest":
SpellAbility sprout = (SpellAbility)options.get(0);
SpellAbility harvest = (SpellAbility)options.get(1);
// lifegain would hurt or has no effect
if (opponent) {
if (lifegainNegative(controller, source)) {
return harvest;
return "Harvest";
}
} else {
if (lifegainNegative(controller, source)) {
return sprout;
return "Sprout";
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? sprout : harvest;
return opponent ? "Sprout" : "Harvest";
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? sprout : harvest;
return opponent ? "Sprout" : "Harvest";
}
// TODO add Lifegain to +1/+1 counters trigger
// for now +1/+1 counters are better
return opponent ? harvest : sprout;
return opponent ? "Harvest" : "Sprout";
case "DeathOrTaxes":
SpellAbility death = (SpellAbility)options.get(0);
SpellAbility taxes = (SpellAbility)options.get(1);
int numDeath = votes.get(death).size();
int numTaxes = votes.get(taxes).size();
int numDeath = votes.get("Death").size();
int numTaxes = votes.get("Taxes").size();
if (opponent) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
@@ -2704,29 +2697,29 @@ public class ComputerUtil {
// would need to sacrifice more creatures than AI has
// sacrifice even more
if (aiCreatures.size() <= numDeath) {
return death;
return "Death";
}
// would need to discard more cards than it has
if (aiCardsInHand.size() <= numTaxes) {
return taxes;
return "Taxes";
}
// has cards with SacMe or Token
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
return death;
return "Death";
}
// has cards with DiscardMe
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
return taxes;
return "Taxes";
}
// discard is probably less worse than sacrifice
return taxes;
return "Taxes";
} else {
// ai is first voter or ally of controller
// both are not affected, but if opponents control creatures, sacrifice is worse
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
}
default:
return Iterables.getFirst(options, null);

View File

@@ -345,10 +345,6 @@ public class ComputerUtilAbility {
if (source.hasSVar("AIPriorityModifier")) {
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
}
// try to use it before it's gone
if (source.isInPlay() && source.hasSVar("EndOfTurnLeavePlay")) {
p += 1;
}
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
p -= 10;
}

View File

@@ -919,14 +919,14 @@ public class ComputerUtilCard {
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
}
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
for (byte c : MagicColor.WUBRG) {
if ((colors & c) != 0) {
return MagicColor.toLongString(c);
}
}
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
return restrictedToColors.get(0); // no difference, there was no prominent color
}
public static List<String> getColorByProminence(final List<Card> list) {
@@ -1819,18 +1819,18 @@ public class ComputerUtilCard {
* @param sa Pump* or CounterPut*
* @return
*/
public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) {
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
if (!sa.usesTargeting()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
for (final Card card : cards) {
if (objects.contains(card)) {
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
return true;
}
}
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
@@ -1849,11 +1849,11 @@ public class ComputerUtilCard {
}
if (!sa.isTargetNumberValid()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
public static boolean isUselessCreature(Player ai, Card c) {

View File

@@ -5,7 +5,6 @@ import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import forge.game.GameObject;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -140,13 +139,11 @@ public class ComputerUtilCost {
if (type.equals("CARDNAME")) {
if (source.getAbilityText().contains("Bloodrush")) {
continue;
}
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
} else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
// Better do something than just discard stuff
return true;
}
return false;
}
typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) {
@@ -251,7 +248,11 @@ public class ComputerUtilCost {
// Does the AI want to use Sacrifice All?
return false;
} else {
int c = part.getAbilityAmount(sourceAbility);
Integer c = part.convertAmount();
if (c == null) {
c = part.getAbilityAmount(sourceAbility);
}
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
if (choices != null) {
@@ -521,12 +522,13 @@ public class ComputerUtilCost {
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
}
boolean cannotBeCountered = false;
// Check for stuff like Nether Void
int extraManaNeeded = 0;
if (!effect) {
boolean cannotBeCountered = !sa.isCounterableBy(null);
if (sa instanceof Spell) {
cannotBeCountered = !sa.isCounterableBy(null);
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) {
@@ -576,24 +578,12 @@ public class ComputerUtilCost {
}
}
// Account for possible Ward after the spell is fully targeted
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
if (!sa.isTrigger() && !cannotBeCountered) {
Set<GameObject> distinctObjects = Sets.newHashSet();
// Ward - will be accounted for when rechecking a targeted ability
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
if (!distinctObjects.add(tgt)) {
continue;
}
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
// don't use API converter since it might have special part logic not meant for Ward cost
SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(player, sa, wardCost, sa.getHostCard())) {
return false;
}
if (wardCost.hasManaCost()) {
extraManaNeeded += wardCost.getTotalMana().getCMC();
}
@@ -617,7 +607,6 @@ public class ComputerUtilCost {
}
}
// TODO both of these call CostAdjustment.adjust, try to reuse instead
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
}

View File

@@ -226,7 +226,7 @@ public class ComputerUtilMana {
}
public static SpellAbility chooseManaAbility(ManaCostBeingPaid cost, SpellAbility sa, Player ai, ManaCostShard toPay,
Collection<SpellAbility> maList, boolean checkCosts) {
Collection<SpellAbility> saList, boolean checkCosts) {
Card saHost = sa.getHostCard();
// CastTotalManaSpent (AIPreference:ManaFrom$Type or AIManaPref$ Type)
@@ -240,12 +240,12 @@ public class ComputerUtilMana {
manaSourceType = sa.getParam("AIManaPref");
}
if (manaSourceType != "") {
List<SpellAbility> filteredList = Lists.newArrayList(maList);
List<SpellAbility> filteredList = Lists.newArrayList(saList);
switch (manaSourceType) {
case "Snow":
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().isSnow()
&& ab2.getHostCard() != null && !ab2.getHostCard().isSnow() ? -1 : 1);
maList = filteredList;
saList = filteredList;
break;
case "Treasure":
// Try to spend only one Treasure if possible
@@ -253,22 +253,22 @@ public class ComputerUtilMana {
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
SpellAbility first = filteredList.get(0);
if (first.getHostCard() != null && first.getHostCard().getType().hasSubtype("Treasure")) {
maList.remove(first);
saList.remove(first);
List<SpellAbility> updatedList = Lists.newArrayList();
updatedList.add(first);
updatedList.addAll(maList);
maList = updatedList;
updatedList.addAll(saList);
saList = updatedList;
}
break;
case "TreasureMax":
// Ok to spend as many Treasures as possible
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure")
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
maList = filteredList;
saList = filteredList;
break;
case "NotSameCard":
String hostName = sa.getHostCard().getName();
maList = filteredList.stream()
saList = filteredList.stream()
.filter(saPay -> !saPay.getHostCard().getName().equals(hostName))
.collect(Collectors.toList());
break;
@@ -277,7 +277,7 @@ public class ComputerUtilMana {
}
}
for (final SpellAbility ma : maList) {
for (final SpellAbility ma : saList) {
// this rarely seems like a good idea
if (ma.getHostCard() == saHost) {
continue;
@@ -336,7 +336,7 @@ public class ComputerUtilMana {
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
continue;
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
for (SpellAbility ab : maList) {
for (SpellAbility ab : saList) {
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
if (!ab.getHostCard().isTapped()) {
paymentChoice = ab;
@@ -590,12 +590,12 @@ public class ComputerUtilMana {
while (!cost.isPaid()) {
toPay = getNextShardToPay(cost, sourcesForShards);
Collection<SpellAbility> maList = sourcesForShards.get(toPay);
if (maList == null) {
Collection<SpellAbility> saList = sourcesForShards.get(toPay);
if (saList == null) {
break;
}
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, maList, true);
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, saList, true);
if (saPayment == null) {
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)) {
@@ -666,7 +666,6 @@ public class ComputerUtilMana {
return true;
}
int phyLifeToPay = 2;
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
boolean hasConverge = sa.getHostCard().hasConverge();
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test, checkPlayable, hasConverge);
@@ -694,12 +693,13 @@ public class ComputerUtilMana {
}
if (sourcesForShards == null && !purePhyrexian) {
// no mana abilities to use for paying
break;
break; // no mana abilities to use for paying
}
toPay = getNextShardToPay(cost, sourcesForShards);
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
Collection<SpellAbility> saList = null;
if (hasConverge &&
(toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) {
@@ -752,14 +752,9 @@ public class ComputerUtilMana {
}
if (saPayment == null) {
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(phyLifeToPay, false, sa)
|| (ai.getLife() <= phyLifeToPay && !ai.cantLoseForZeroOrLessLife())) {
// cannot pay
break;
}
if (test) {
phyLifeToPay += 2;
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)
|| (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
break; // cannot pay
}
if (sa.hasParam("AIPhyrexianPayment")) {
@@ -963,6 +958,7 @@ public class ComputerUtilMana {
if (checkCosts) {
// Check if AI can still play this mana ability
ma.setActivatingPlayer(ai);
// if the AI can't pay the additional costs skip the mana ability
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) {
return false;
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
@@ -980,9 +976,8 @@ public class ComputerUtilMana {
continue;
}
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s))){
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s)))
return true;
}
}
return false;
}
@@ -1508,7 +1503,7 @@ public class ComputerUtilMana {
AbilitySub sub = m.getSubAbility();
// We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
continue;
}
needsLimitedResources = true; // TODO: check for good drawbacks (gainLife)
@@ -1588,7 +1583,7 @@ public class ComputerUtilMana {
// don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
continue;
}
}

View File

@@ -264,14 +264,12 @@ public abstract class GameState {
}
if (c.hasMergedCard()) {
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
// we have to go by the current top card name here
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
} else {
String suffix = c.hasPaperFoil() ? "+" : "";
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
.append("|Art:").append(c.getPaperCard().getArtIndex());
}
}
@@ -321,21 +319,18 @@ public abstract class GameState {
newText.append(":Cloaked");
}
}
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
if (c.getMeldedWith() != null) {
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
newText.append(":");
newText.append(c.getMeldedWith().getName()).append(suffix);
}
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
if (c.isModal()) {
newText.append("|Modal");
} else {
newText.append("|Transformed");
newText.append(c.getMeldedWith().getName());
}
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
if (c.getPlayerAttachedTo() != null) {
@@ -1268,7 +1263,7 @@ public abstract class GameState {
} else if (cardinfo[0].startsWith("T:")) {
String tokenStr = cardinfo[0].substring(2);
PaperToken token = StaticData.instance().getAllTokens().getToken(tokenStr,
setCode != null ? setCode : CardEdition.UNKNOWN_CODE);
setCode != null ? setCode : CardEdition.UNKNOWN.getName());
if (token == null) {
System.err.println("ERROR: Tried to create a non-existent token named " + cardinfo[0] + " when loading game state!");
continue;
@@ -1316,8 +1311,8 @@ public abstract class GameState {
if (info.endsWith("Cloaked")) {
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
}
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
c.setState(CardStateName.Backside, true);
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
c.setBackSide(true);
} else if (info.startsWith("Flipped")) {
c.setState(CardStateName.Flipped, true);
@@ -1335,6 +1330,9 @@ public abstract class GameState {
}
c.setState(CardStateName.Meld, true);
c.setBackSide(true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
c.setBackSide(true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";

View File

@@ -214,7 +214,7 @@ public class SpecialAiLogic {
}
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
public static AiAbilityDecision doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
@@ -222,14 +222,14 @@ public class SpecialAiLogic {
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
if (numOtherCreats == 0) {
// Cut short if there's nothing to sac at all
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
// Check if the standard Aristocrats logic applies first (if in the right conditions for it)
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
if (isDeclareBlockers || isThreatened) {
if (doAristocratLogic(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -247,7 +247,7 @@ public class SpecialAiLogic {
if (countersSa == null) {
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!");
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
final Game game = ai.getGame();
@@ -263,7 +263,7 @@ public class SpecialAiLogic {
relevantCreats.remove(source);
if (relevantCreats.isEmpty()) {
// No relevant creatures to sac
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa);
@@ -287,20 +287,16 @@ public class SpecialAiLogic {
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat))
);
if (!forcedSacTgts.isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
if (source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
return source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg;
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
@@ -313,7 +309,7 @@ public class SpecialAiLogic {
);
if (sacTgts.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
@@ -321,10 +317,7 @@ public class SpecialAiLogic {
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
if (source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
}
} else {
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
@@ -336,11 +329,7 @@ public class SpecialAiLogic {
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
);
if (sacFodder.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return !sacFodder.isEmpty();
}
}
@@ -371,10 +360,10 @@ public class SpecialAiLogic {
// FIXME: We're emulating the UnlessCost on the SA to run the proper checks.
// This is hacky, but it works. Perhaps a cleaner way exists?
sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost"));
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
sa.getMapParams().remove("UnlessCost");
} else {
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
}
return willPlay;
}

View File

@@ -78,17 +78,16 @@ public class SpecialCardAi {
// Arena and Magus of the Arena
public static class Arena {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
// TODO This is basically removal, so we may want to play this at other times
if (!game.getPhaseHandler().is(PhaseType.END_OF_TURN) || game.getPhaseHandler().getNextTurn() != ai) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
return false; // at opponent's EOT only, to conserve mana
}
CardCollection aiCreatures = ai.getCreaturesInPlay();
if (aiCreatures.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
for (Player opp : ai.getOpponents()) {
@@ -112,11 +111,11 @@ public class SpecialCardAi {
if (canKillAll) {
sa.getTargets().clear();
sa.getTargets().add(aiCreature);
return new AiAbilityDecision(100, AiPlayDecision.Removal);
return true;
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return sa.isTargetNumberValid();
}
}
@@ -204,7 +203,7 @@ public class SpecialCardAi {
// Chain of Acid
public static class ChainOfAcid {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS);
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
@@ -214,22 +213,13 @@ public class SpecialCardAi {
// which it can only distinguish by their CMC, considering >CMC higher value).
// Currently ensures that the AI will still have lands provided that the human player goes to
// destroy all the AI's lands in order (to avoid manalock).
if (!OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2) {
// If there are enough lands, target the worst non-creature permanent of the opponent
Card worstOppPerm = ComputerUtilCard.getWorstAI(OppPerms);
if (worstOppPerm != null) {
sa.resetTargets();
sa.getTargets().add(worstOppPerm);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return !OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2;
}
}
// Chain of Smog
public static class ChainOfSmog {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
if (ai.getCardsIn(ZoneType.Hand).isEmpty()) {
// to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point)
// TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a
@@ -245,10 +235,10 @@ public class SpecialCardAi {
sa.getParent().resetTargets();
sa.getParent().getTargets().add(targOpp);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
@@ -400,7 +390,7 @@ public class SpecialCardAi {
// Donate
public static class Donate {
public static AiAbilityDecision considerTargetingOpponent(final Player ai, final SpellAbility sa) {
public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) {
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
@@ -410,7 +400,7 @@ public class SpecialCardAi {
// All opponents have hexproof or something like that
if (Iterables.isEmpty(oppList)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// filter for player who does not have donate target already
@@ -428,30 +418,31 @@ public class SpecialCardAi {
if (opp != null) {
sa.resetTargets();
sa.getTargets().add(opp);
return true;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// No targets found to donate, so do nothing.
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) {
public static boolean considerDonatingPermanent(final Player ai, final SpellAbility sa) {
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
sa.resetTargets();
sa.getTargets().add(donateTarget);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// Should never get here because targetOpponent, called before targetPermanentToDonate, should already have made the AI bail
System.err.println("Warning: Donate AI failed at SpecialCardAi.Donate#targetPermanentToDonate despite successfully targeting an opponent first.");
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
// Electrostatic Pummeler
public static class ElectrostaticPummeler {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
Game game = ai.getGame();
Combat combat = game.getCombat();
@@ -464,13 +455,13 @@ public class SpecialCardAi {
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop);
if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg)
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
// Do not activate if damage will be prevented
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
return false;
}
// Activate Electrostatic Pummeler's pump only as a combat trick
@@ -479,14 +470,14 @@ public class SpecialCardAi {
// We'll try to deal lethal trample/unblocked damage, so remember the card for attack
// and wait until declare blockers step.
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
return false;
}
if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
boolean isBlocking = combat.isBlocking(source);
@@ -511,11 +502,11 @@ public class SpecialCardAi {
}
if (totalDamageToPW >= oppT + loyalty) {
// Already enough damage to take care of the planeswalker
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
return false;
}
if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) {
// Can pump to kill the planeswalker, go for it
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
}
}
@@ -536,31 +527,31 @@ public class SpecialCardAi {
// We can deal a lot of damage (either a lot of damage directly to the opponent,
// or kill the blocker(s) and damage the opponent at the same time, so go for it
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
}
}
if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) {
// Can't survive first strike or double strike, don't pump
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
return false;
}
if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) {
// Can't pump enough to kill the blockers and survive, don't pump
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
return false;
}
if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) {
// Already enough to kill the blockers and survive, don't overpump
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
return false;
}
if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.isWitherDamage()
&& predictedPT.getLeft() <= oppT) {
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
return false;
}
// If we got here, it should be a favorable combat pump, resulting in at least one
// opposing creature dying, and hopefully with the Pummeler surviving combat.
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
}
public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) {
@@ -637,15 +628,15 @@ public class SpecialCardAi {
// Fell the Mighty
public static class FellTheMighty {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollection aiList = ai.getCreaturesInPlay();
if (aiList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
CardLists.sortByPowerAsc(aiList);
Card lowest = aiList.get(0);
if (!sa.canTarget(lowest)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield),
@@ -655,9 +646,9 @@ public class SpecialCardAi {
if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) {
sa.resetTargets();
sa.getTargets().add(lowest);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
@@ -692,25 +683,25 @@ public class SpecialCardAi {
// Gideon Blackblade
public static class GideonBlackblade {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
sa.resetTargets();
CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
if (!otb.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(otb));
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
// Goblin Polka Band
public static class GoblinPolkaBand {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
int maxPotentialTgts = ai.getOpponents().getCreaturesInPlay().filter(CardPredicates.UNTAPPED).size();
int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false);
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
if (numTgts == 0) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// Set Announce
@@ -720,7 +711,7 @@ public class SpecialCardAi {
List<GameEntity> validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true);
sa.resetTargets();
sa.getTargets().addAll(Aggregates.random(validTgts, numTgts));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -929,12 +920,12 @@ public class SpecialCardAi {
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
public static class LivingDeath {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
// if there's another reanimator card currently suspended, don't cast a new one until the previous
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -945,7 +936,7 @@ public class SpecialCardAi {
if (aiCreaturesInGY.isEmpty()) {
// nothing in graveyard, so cut short
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
for (Card c : ai.getCreaturesInPlay()) {
@@ -977,30 +968,17 @@ public class SpecialCardAi {
}
// if we get more value out of this than our opponent does (hopefully), go for it
if ((aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold);
}
}
// Maze's End
public static class MazesEnd {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (availableGates.isEmpty()) {
// No gates available, so don't activate Maze's End
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty();
}
public static Card considerCardToGet(final Player ai, final SpellAbility sa)
@@ -1064,33 +1042,29 @@ public class SpecialCardAi {
return exiledWith == null || (tgt != null && ComputerUtilCard.evaluateCreature(tgt) > ComputerUtilCard.evaluateCreature(exiledWith));
}
public static AiAbilityDecision considerCopy(final Player ai, final SpellAbility sa) {
public static boolean considerCopy(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final Card exiledWith = source.getImprintedCards().isEmpty() ? null : source.getImprintedCards().getFirst();
if (exiledWith == null) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
// We want to either be able to attack with the creature, or keep it until our opponent's end of turn as a
// potential blocker
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
return ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
|| (ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && ai.getGame().getCombat() != null
&& !ai.getGame().getCombat().getAttackers().isEmpty())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
&& !ai.getGame().getCombat().getAttackers().isEmpty());
}
}
// Momir Vig, Simic Visionary Avatar
public static class MomirVigAvatar {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
// In MoJhoSto, prefer Jhoira sorcery ability from time to time
@@ -1101,7 +1075,7 @@ public class SpecialCardAi {
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA);
if (ai.getLandsInPlay().size() >= numLandsForJhoira && MyRandom.percentTrue(chanceToPrefJhoira)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
}
@@ -1110,7 +1084,7 @@ public class SpecialCardAi {
// Some basic strategy for Momir
if (tokenSize < 2) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
if (tokenSize > 11) {
@@ -1119,7 +1093,7 @@ public class SpecialCardAi {
sa.setXManaCostPaid(tokenSize);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -1158,13 +1132,13 @@ public class SpecialCardAi {
// Necropotence
public static class Necropotence {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
Game game = ai.getGame();
int computerHandSize = ai.getZone(ZoneType.Hand).size();
int maxHandSize = ai.getMaxHandSize();
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false; // nothing to draw from the library
}
if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
@@ -1172,7 +1146,7 @@ public class SpecialCardAi {
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
// (not sure how to detect the presence of such effects yet)
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
PhaseHandler ph = game.getPhaseHandler();
@@ -1194,33 +1168,23 @@ public class SpecialCardAi {
// We're in a situation when we have nothing castable in hand, something needs to be done
if (!blackViseOTB) {
// exile-loot +1 card when at max hand size, hoping to get a workable spell or land
if (computerHandSize + exiledWithNecro - 1 == maxHandSize) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return computerHandSize + exiledWithNecro - 1 == maxHandSize;
} else {
// Loot to 7 in presence of Black Vise, hoping to find what to do
// NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead?
if (computerHandSize + exiledWithNecro <= maxHandSize) {
// Loot to 7, hoping to find something playable
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// Loot to 8, hoping to find something playable
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return computerHandSize + exiledWithNecro <= maxHandSize;
}
} else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) {
// try not to overdraw in presence of Black Vise
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) {
// Only draw until we reach max hand size
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) {
// Only activate in AI's own turn (sans the exception above)
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -1340,7 +1304,7 @@ public class SpecialCardAi {
}
}
public static AiAbilityDecision considerSecondTarget(final Player ai, final SpellAbility sa) {
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = sa.getParent().getTargetCard();
CardCollection candidates = ai.getOpponents().getCardsIn(ZoneType.Battlefield).filter(
CardPredicates.sharesCardTypeWith(firstTgt).and(CardPredicates.isTargetableBy(sa)));
@@ -1348,105 +1312,89 @@ public class SpecialCardAi {
if (secondTgt != null) {
sa.resetTargets();
sa.getTargets().add(secondTgt);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
// Price of Progress
public static class PriceOfProgress {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
// Don't play in early game - opponent likely still has lands to play
if (ai.getGame().getPhaseHandler().getTurn() < 10) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
int aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
// TODO Better if we actually calculate the true damage
boolean willDieToPCasting = (ai.getLife() <= aiLands * 2);
if (!willDieToPCasting) {
boolean hasBridge = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
// Do we have a card in play that makes us want to empty out hand?
if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) {
hasBridge = true;
break;
}
}
// Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll
// even if suboptimal play, but don't waste the card too early even then!
if (hasBridge) {
return new AiAbilityDecision(100, AiPlayDecision.PlayToEmptyHand);
boolean hasBridge = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
// Do we have a card in play that makes us want to empty out hand?
if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) {
hasBridge = true;
break;
}
}
boolean willPlay = true;
// Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll
// even if suboptimal play, but don't waste the card too early even then!
if ((hasBridge) && (ai.getGame().getPhaseHandler().getTurn() >= 10)) {
return true;
}
for (Player opp : ai.getOpponents()) {
int oppLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
// Don't if no enemy nonbasic lands
if (oppLands == 0) {
willPlay = false;
continue;
}
// Always if enemy would die and we don't!
// TODO : predict actual damage instead of assuming it'll be 2*lands
// Don't if we lose, unless we lose anyway to unblocked creatures next turn
if (willDieToPCasting &&
if ((ai.getLife() <= aiLands * 2) &&
(!(ComputerUtil.aiLifeInDanger(ai, true, 0)) && ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2))) {
willPlay = false;
return false;
}
// Do if we can win
if (opp.getLife() <= oppLands * 2) {
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
if ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2) {
return true;
}
// Don't if we'd lose a larger percentage of our remaining life than enemy
if ((aiLands / ((double) ai.getLife())) >
(oppLands / ((double) ai.getOpponentsSmallestLifeTotal()))) {
willPlay = false;
return false;
}
// Don't if no enemy nonbasic lands
if (oppLands == 0) {
return false;
}
// Don't if loss is equal in percentage but we lose more points
if (((aiLands / ((double) ai.getLife())) == (oppLands / ((double) ai.getOpponentsSmallestLifeTotal())))
&& (aiLands > oppLands)) {
willPlay = false;
return false;
}
}
if (willPlay) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
}
}
// Sarkhan the Mad
public static class SarkhanTheMad {
public static AiAbilityDecision considerDig(final Player ai, final SpellAbility sa) {
if (sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
public static boolean considerDig(final Player ai, final SpellAbility sa) {
return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1;
}
public static AiAbilityDecision considerMakeDragon(final Player ai, final SpellAbility sa) {
public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) {
// TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier?
CardCollection creatures = ai.getCreaturesInPlay();
boolean hasValidTgt = !CardLists.filter(creatures, t -> t.getNetPower() < 5 && t.getNetToughness() < 5).isEmpty();
if (hasValidTgt) {
Card worstCreature = ComputerUtilCard.getWorstCreatureAI(creatures);
sa.getTargets().add(worstCreature);
return new AiAbilityDecision(100, AiPlayDecision.AddBoardPresence);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) {
int minLife = weakestOpp.getLife();
@@ -1502,7 +1450,7 @@ public class SpecialCardAi {
// Sorin, Vengeful Bloodlord
public static class SorinVengefulBloodlord {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
CardPredicates.CREATURES
@@ -1516,7 +1464,7 @@ public class SpecialCardAi {
CardLists.sortByCmcDesc(creaturesToGet);
if (creaturesToGet.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// pick the best creature that will stay on the battlefield
@@ -1532,10 +1480,10 @@ public class SpecialCardAi {
sa.resetTargets();
sa.getTargets().add(best);
sa.setXManaCostPaid(best.getCMC());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
@@ -1649,27 +1597,23 @@ public class SpecialCardAi {
// The One Ring
public static class TheOneRing {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN);
if (ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.LifeInDanger);
return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1;
}
}
// The Scarab God
public static class TheScarabGod {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
@@ -1680,19 +1624,13 @@ public class SpecialCardAi {
sa.getTargets().add(worstOwnCreat);
}
if (!sa.getTargets().isEmpty()) {
// If we have a target, we can play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// No valid targets, can't play this ability
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return sa.getTargets().size() > 0;
}
}
// Timetwister
public static class Timetwister {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
int maxOppHandSize = 0;
@@ -1706,14 +1644,7 @@ public class SpecialCardAi {
}
// use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand
if (aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD) {
// if the AI has less than 3 cards in hand or the opponent has more than 3 cards in hand than the AI
// then the AI is willing to play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// otherwise, don't play this ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD;
}
}
@@ -1774,12 +1705,12 @@ public class SpecialCardAi {
// Volrath's Shapeshifter
public static class VolrathsShapeshifter {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
public static boolean consider(final Player ai, final SpellAbility sa) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
// try not to do this too early to at least attempt to avoid situations where the AI
// would cast a spell which would ruin the shapeshifting
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
return false;
}
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
@@ -1795,15 +1726,11 @@ public class SpecialCardAi {
if (topGY == null
|| !topGY.isCreature()
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
if ( numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false);
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) {

View File

@@ -39,75 +39,70 @@ import forge.util.collect.FCollectionView;
*/
public abstract class SpellAbilityAi {
public final AiAbilityDecision canPlayWithSubs(final Player aiPlayer, final SpellAbility sa) {
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (!decision.willingToPlay() && !"PlayForSub".equals(sa.getParam("AILogic"))) {
return decision;
public final boolean canPlayAIWithSubs(final Player aiPlayer, final SpellAbility sa) {
if (!canPlayAI(aiPlayer, sa)) {
return false;
}
final AbilitySub subAb = sa.getSubAbility();
if (subAb == null) {
return decision;
}
return chkDrawbackWithSubs(aiPlayer, subAb);
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb);
}
/**
* Handles the AI decision to play a "main" SpellAbility
*/
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(sa.getHostCard(), sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(source, sa)) {
return false;
}
return canPlayWithoutRestrict(ai, sa);
}
protected AiAbilityDecision canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final Cost cost = sa.getPayCosts();
if (sa.hasParam("AICheckCanPlayWithDefinedX")) {
// FIXME: can this somehow be simplified without the need for an extra AI hint?
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
}
if (!checkConditions(ai, sa, sa.getConditions())) {
SpellAbility sub = sa.getSubAbility();
if (sub != null && !checkConditions(ai, sub, sub.getConditions())) {
return false;
}
}
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
final boolean alwaysOnDiscard = "AlwaysOnDiscard".equals(logic) && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
if (!checkAiLogic(ai, sa, logic)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
return false;
}
} else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
} else if (ComputerUtil.preventRunAwayActivations(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
return false;
}
AiAbilityDecision decision = checkApiLogic(ai, sa);
if (!decision.willingToPlay()) {
return decision;
if (!checkApiLogic(ai, sa)) {
return false;
}
// needs to be after API logic because needs to check possible X Cost
// needs to be after API logic because needs to check possible X Cost?
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
return false;
}
// for cards like Figure of Destiny
// (it's unlikely many valid effect would work like this -
// and while in theory AI could turn some conditions true in response that's far too advanced as default)
if (!checkConditions(ai, sa)) {
SpellAbility sub = sa.getSubAbility();
if (sub == null || !checkConditions(ai, sub)) {
return new AiAbilityDecision(0, AiPlayDecision.NeedsToPlayCriteriaNotMet);
}
}
return decision;
return true;
}
protected boolean checkConditions(final Player ai, final SpellAbility sa) {
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {
// copy it to disable some checks that the AI need to check extra
SpellAbilityCondition con = (SpellAbilityCondition) sa.getConditions().copy();
con = (SpellAbilityCondition) con.copy();
// if manaspent, check if AI can pay the colored mana as cost
if (!con.getManaSpent().isEmpty()) {
@@ -121,6 +116,40 @@ public abstract class SpellAbilityAi {
return con.areMet(sa);
}
/**
* Checks if the AI will play a SpellAbility with the specified AiLogic
*/
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (aiLogic.equals("CheckCondition")) {
SpellAbility saCopy = sa.copy();
saCopy.setActivatingPlayer(ai);
return saCopy.metConditions();
}
return !("Never".equals(aiLogic));
}
/**
* Checks if the AI is willing to pay for additional costs
* <p>
* Evaluated costs are: life, discard, sacrifice and counter-removal
*/
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return false;
}
return true;
}
/**
* Checks if the AI will play a SpellAbility based on its phase restrictions
*/
@@ -130,38 +159,19 @@ public abstract class SpellAbilityAi {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
final String logic) {
if (logic.equals("AtOppEOT")) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
return checkPhaseRestrictions(ai, sa, ph);
}
/**
* Checks if the AI will play a SpellAbility with the specified AiLogic
*/
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Never".equals(aiLogic)) {
return false;
}
if (!"Once".equals(aiLogic)) {
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
}
return true;
}
/**
* The rest of the logic not covered by the canPlayAI template is defined here
*/
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
// 80% chance to play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return MyRandom.getRandom().nextFloat() < .8f; // random success
}
public final boolean doTrigger(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
// this evaluation order is currently intentional as it does more stuff that helps avoiding some crashes
if (!ComputerUtilCost.canPayCost(sa, aiPlayer, true) && !mandatory) {
return false;
@@ -173,48 +183,28 @@ public abstract class SpellAbilityAi {
return sa.isTargetNumberValid();
}
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory).willingToPlay();
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory);
}
public final AiAbilityDecision doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
AiAbilityDecision decision = doTriggerNoCost(aiPlayer, sa, mandatory);
if (!decision.willingToPlay() && !"Always".equals(sa.getParam("AILogic"))) {
return decision;
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
return false;
}
final AbilitySub subAb = sa.getSubAbility();
if (subAb == null) {
if (decision.willingToPlay()) {
return decision;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
decision = chkDrawbackWithSubs(aiPlayer, subAb);
if (decision.willingToPlay()) {
return decision;
}
if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
}
/**
* Handles the AI decision to play a triggered SpellAbility
*/
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
AiAbilityDecision decision = canPlayWithoutRestrict(aiPlayer, sa);
if (decision.willingToPlay() && (!mandatory || sa.isTargetNumberValid())) {
// This is a weird check. Why do we care if its not mandatory if we WANT to do it?
return decision;
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
return true;
}
// not mandatory, short way out
if (!mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// invalid target might prevent it
@@ -230,13 +220,82 @@ public abstract class SpellAbilityAi {
if (sa.canTarget(p)) {
sa.resetTargets();
sa.getTargets().add(p);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/**
* Handles the AI decision to play a sub-SpellAbility
*/
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
// sub-SpellAbility might use targets too
if (sa.usesTargeting()) {
// no Candidates, no adding to Stack
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
return false;
}
// but if it does, it should override this function
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return false;
}
return true;
}
/**
* <p>
* isSorcerySpeed.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
}
/**
* <p>
* playReusable.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
PhaseHandler phase = ai.getGame().getPhaseHandler();
// TODO probably also consider if winter orb or similar are out
if (sa instanceof AbilitySub) {
return true; // This is only true for Drawbacks and triggers
}
if (!sa.getPayCosts().isReusuableResource()) {
return false;
}
if (ComputerUtil.playImmediately(ai, sa)) {
return true;
}
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
return true;
}
if (sa.isSpell() && !sa.isBuyback()) {
return false;
}
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
}
/**
@@ -245,35 +304,9 @@ public abstract class SpellAbilityAi {
* @param ab
* @return
*/
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
final AbilitySub subAb = ab.getSubAbility();
AiAbilityDecision decision = SpellApiToAi.Converter.get(ab).chkDrawback(ab, aiPlayer);
if (!decision.willingToPlay()) {
return decision;
}
if (subAb == null) {
return decision;
}
return chkDrawbackWithSubs(aiPlayer, subAb);
}
/**
* Handles the AI decision to play a sub-SpellAbility
*/
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
// sub-SpellAbility might use targets too
if (sa.usesTargeting()) {
// no Candidates, no adding to Stack
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// but if it does, it should override this function
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
@@ -281,6 +314,25 @@ public abstract class SpellAbilityAi {
return true;
}
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payNever = "Never".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && isMine)) {
return false;
}
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
}
@SuppressWarnings("unchecked")
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
boolean hasPlayer = false;
@@ -360,46 +412,6 @@ public abstract class SpellAbilityAi {
return MyRandom.getRandom().nextBoolean();
}
/**
* Checks if the AI is willing to pay for additional costs
* <p>
* Evaluated costs are: life, discard, sacrifice and counter-removal
*/
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return false;
}
return true;
}
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payNever = "Never".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && isMine)) {
return false;
}
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
}
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, Player player, List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();
@@ -409,14 +421,14 @@ public abstract class SpellAbilityAi {
Cost fullCost = opt.getCost().copy().add(costSoFar);
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
// Playability check for Kicker
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
SpellAbility kickedSaCopy = fullCostSa.copy();
kickedSaCopy.addOptionalCost(opt.getType());
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
copy.setCastSA(kickedSaCopy);
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
// don't choose kickers we don't want to play
continue;
continue; // don't choose kickers we don't want to play
}
}
@@ -428,56 +440,4 @@ public abstract class SpellAbilityAi {
return chosenOptCosts;
}
/**
* <p>
* isSorcerySpeed.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
}
/**
* <p>
* playReusable.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
PhaseHandler phase = ai.getGame().getPhaseHandler();
// TODO probably also consider if winter orb or similar are out
if (sa instanceof AbilitySub) {
return true; // This is only true for Drawbacks and triggers
}
if (!sa.getPayCosts().isReusuableResource()) {
return false;
}
if (ComputerUtil.playImmediately(ai, sa)) {
return true;
}
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
return true;
}
if (sa.isSpell() && !sa.isBuyback()) {
return false;
}
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -10,6 +8,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
@@ -17,69 +16,78 @@ import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// AI cannot use this properly until he can use SAs during Humans turn
final Card source = sa.getHostCard();
final Player opp = ai.getStrongestOpponent();
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
if (!sa.usesTargeting()) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (!defined.contains(opp)) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
} else {
sa.resetTargets();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
return super.checkApiLogic(ai, sa);
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return randomReturn;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getStrongestOpponent();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
if (null == tgt) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(opp)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return defined.contains(opp);
}
} else {
sa.resetTargets();
sa.getTargets().add(opp);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI cannot use this properly until he can use SAs during Humans turn
final Card source = sa.getHostCard();
boolean randomReturn = true;
if (!sa.usesTargeting()) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
sa.resetTargets();
sa.getTargets().add(ai.getWeakestOpponent());
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return randomReturn;
}
@Override

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -13,8 +11,8 @@ import forge.game.spellability.SpellAbility;
public class AddPhaseAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false;
}
}

View File

@@ -17,8 +17,6 @@
*/
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
@@ -40,7 +38,7 @@ import java.util.List;
public class AddTurnAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
@@ -49,41 +47,41 @@ public class AddTurnAi extends SpellAbilityAi {
if (sa.canTarget(ai) && (mandatory || !ai.getGame().getReplacementHandler().wouldExtraTurnBeSkipped(ai))) {
sa.getTargets().add(ai);
} else if (mandatory) {
for (final Player ally : ai.getAllies()) {
for (final Player ally : ai.getAllies()) {
if (sa.canTarget(ally)) {
sa.getTargets().add(ally);
break;
sa.getTargets().add(ally);
break;
}
}
}
if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) {
sa.getTargets().add(opp);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
final List<Player> tgtPlayers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
for (final Player p : tgtPlayers) {
if (p.isOpponentOf(ai) && !mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
// TODO: improve ai for Sage of Hours
if (!StringUtils.isNumeric(sa.getParam("NumTurns"))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return StringUtils.isNumeric(sa.getParam("NumTurns"));
// not sure if the AI should be playing with cards that give the
// Human more turns.
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return doTriggerNoCost(aiPlayer, sa, false);
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return doTriggerAINoCost(aiPlayer, sa, false);
}
}

View File

@@ -1,23 +1,30 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class AdvanceCrankAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
int nextSprocket = (ai.getCrankCounter() % 3) + 1;
int crankCount = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isContraptionOnSprocket(nextSprocket));
if (crankCount < 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return super.canPlay(ai, sa);
//Could evaluate whether we actually want to crank those, but this is probably fine for now.
if(crankCount < 2)
return false;
return super.canPlayAI(ai, sa);
}
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if(logic.equals("AtOppEOT"))
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -18,7 +16,7 @@ import java.util.Map;
public class AlterAttributeAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true"));
String[] attributes = sa.getParam("Attributes").split(",");
@@ -26,7 +24,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
// TODO add targeting logic
// needed for Suspected
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
@@ -38,7 +36,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
case "Solved":
// there is currently no effect that would un-solve something
if (!c.isSolved() && activate) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
break;
case "Suspect":
@@ -46,21 +44,21 @@ public class AlterAttributeAi extends SpellAbilityAi {
// is Suspected good or bad?
// currently Suspected is better
if (!activate) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
case "Saddle":
case "Saddled":
// AI should not try to Saddle again?
if (c.isSaddled()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
@Override

View File

@@ -1,7 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -14,8 +13,8 @@ public class AlwaysPlayAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
}
@Override

View File

@@ -3,8 +3,6 @@ package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -21,28 +19,24 @@ import java.util.Map;
public class AmassAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army");
Card host = sa.getHostCard();
final Game game = ai.getGame();
if (!aiArmies.isEmpty()) {
if (aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1))) {
// If AI has an Army that can receive counters, play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// AI has Armies but none can receive counters, so don't play
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
}
return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
}
final String type = sa.getParam("Type");
final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army";
StringBuilder sb = new StringBuilder("b_0_0_");
sb.append(sa.getOriginalParam("Type").toLowerCase()).append("_army");
final String tokenScript = sb.toString();
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
if (token == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
token.setController(ai, 0);
@@ -69,11 +63,7 @@ public class AmassAi extends SpellAbilityAi {
//reset static abilities
game.getAction().checkStaticAbilities(false);
if (result) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return result;
}
@Override
@@ -92,12 +82,8 @@ public class AmassAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return checkApiLogic(ai, sa);
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || checkApiLogic(ai, sa);
}
@Override

View File

@@ -142,129 +142,130 @@ public class AnimateAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = aiPlayer.getGame();
final PhaseHandler ph = game.getPhaseHandler();
if (!sa.metConditions() && sa.getSubAbility() == null) {
return false; // what is this for?
}
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
// Should I animate a card before i have to sacrifice something better?
if (!isAnimatedThisTurn(aiPlayer, source)) {
rememberAnimatedThisTurn(aiPlayer, source);
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
return true; // interrupt sacrifice
}
}
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa, new CardCollection())) {
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
return false; // prevent crewing with equal or better creatures
}
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
sa.setXManaCostPaid(xPay);
}
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
}
if (!sa.usesTargeting()) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped()
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
&& !c.isEquipping();
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped()
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
&& !c.isEquipping();
// for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
if (!c.hasKeyword(keyword)) {
bFlag = true;
// for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
if (!c.hasKeyword(keyword)) {
bFlag = true;
}
}
}
}
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
// also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, source)) {
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return false;
}
Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return false;
}
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return false;
}
// also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
}
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
}
return bFlag; // All of the defined stuff is animated, not very useful
} else {
sa.resetTargets();
return animateTgtAI(sa);
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, source);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
AiAbilityDecision decision;
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
decision = animateTgtAI(sa);
if (decision.willingToPlay()) {
return decision;
} else if (!mandatory) {
return decision;
} else {
if(animateTgtAI(sa))
return true;
else if (!mandatory)
return false;
else {
// fallback if animate is mandatory
sa.resetTargets();
List<Card> list = CardUtil.getValidCardsToTarget(sa);
if (list.isEmpty()) {
return decision;
return false;
}
Card toAnimate = ComputerUtilCard.getWorstAI(list);
rememberAnimatedThisTurn(aiPlayer, toAnimate);
sa.getTargets().add(toAnimate);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
@@ -272,14 +273,9 @@ public class AnimateAi extends SpellAbilityAi {
return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2);
}
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
if (sa.getMaxTargets() == 0) {
// this happens if an optional cost is skipped, e.g. Brave the Wilds
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
}
private boolean animateTgtAI(final SpellAbility sa) {
final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean alwaysActivatePWAbility = sa.isPwAbility()
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
@@ -291,13 +287,15 @@ public class AnimateAi extends SpellAbilityAi {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
final Game game = ai.getGame();
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
// list is empty, no possible targets
if (list.isEmpty() && !alwaysActivatePWAbility) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// something is used for animate into creature
@@ -364,7 +362,7 @@ public class AnimateAi extends SpellAbilityAi {
// data is empty, no good targets
if (data.isEmpty() && !alwaysActivatePWAbility) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// get the best creature to be animated
@@ -387,18 +385,17 @@ public class AnimateAi extends SpellAbilityAi {
holdAnimatedTillMain2(ai, worst);
if (!ComputerUtilMana.canPayManaCost(sa, ai, 0, sa.isTrigger())) {
releaseHeldTillMain2(ai, worst);
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
}
rememberAnimatedThisTurn(ai, worst);
sa.getTargets().add(worst);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (logic.equals("SetPT")) {
// TODO: 1. Teach the AI to use this to save the creature from direct damage;
// 2. Determine the best target in a smarter way?
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
Card buffed = becomeAnimated(worst, sa);
@@ -406,7 +403,7 @@ public class AnimateAi extends SpellAbilityAi {
&& (buffed.getNetPower() - worst.getNetPower() >= 3 || !ComputerUtilCard.doesCreatureAttackAI(ai, worst))) {
sa.getTargets().add(worst);
rememberAnimatedThisTurn(ai, worst);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -418,7 +415,7 @@ public class AnimateAi extends SpellAbilityAi {
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated);
if (isValuableAttacker || isValuableBlocker)
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
@@ -428,23 +425,25 @@ public class AnimateAi extends SpellAbilityAi {
if(worst != null) {
sa.getTargets().add(worst);
rememberAnimatedThisTurn(ai, worst);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
if (sa.hasParam("AITgts") && !list.isEmpty()) {
//No logic, but we do have preferences. Pick the best among those?
Card best = ComputerUtilCard.getBestAI(list);
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
if(best != null) {
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return true;
}
}
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until
// this can do a reasonably good job of picking a good target
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
public static Card becomeAnimated(final Card card, final SpellAbility sa) {

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -11,30 +9,24 @@ import forge.game.spellability.SpellAbility;
public class AnimateAllAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String logic = sa.getParamOrDefault("AILogic", "");
if ("CreatureAdvantage".equals(logic) && !aiPlayer.getCreaturesInPlay().isEmpty()) {
// TODO: improve this or implement a better logic for abilities like Oko, the Trickster ultimate
for (Card c : aiPlayer.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(aiPlayer, c)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
if ("Always".equals(logic)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return "Always".equals(logic);
} // end animateAllCanPlayAI()
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
}
}

View File

@@ -1,13 +1,13 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
@@ -16,32 +16,30 @@ import java.util.List;
public class AssembleContraptionAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
//Pulls double duty as the OpenAttraction API. Same logic; usually good to do as long as we have the appropriate cards.
CardCollectionView deck = getDeck(ai, sa);
if(deck.isEmpty())
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
AiAbilityDecision superDecision = super.canPlay(ai, sa);
if (!superDecision.willingToPlay())
return superDecision;
if(!super.canPlayAI(ai, sa))
return false;
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
xPay = Math.max(xPay, deck.size());
if (xPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
sa.getRootAbility().setXManaCostPaid(xPay);
}
if(sa.hasParam("DefinedContraption") && sa.usesTargeting()) {
if (getGoodReassembleTarget(ai, sa) == null) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return getGoodReassembleTarget(ai, sa) != null;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
private static CardCollectionView getDeck(Player ai, SpellAbility sa) {
@@ -50,11 +48,11 @@ public class AssembleContraptionAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.getRootAbility().setXManaCostPaid(xPay);
}
@@ -64,7 +62,7 @@ public class AssembleContraptionAi extends SpellAbilityAi {
if(target != null)
sa.getTargets().add(target);
else
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return super.checkApiLogic(ai, sa);
@@ -86,16 +84,26 @@ public class AssembleContraptionAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if(!mandatory && getDeck(aiPlayer, sa).isEmpty())
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if(logic.equals("AtOppEOT"))
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
return super.checkPhaseRestrictions(ai, sa, ph);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if(getDeck(aiPlayer, sa).isEmpty())
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return super.chkDrawback(sa, aiPlayer);
return false;
return super.chkAIDrawback(sa, aiPlayer);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if(!mandatory && getDeck(aiPlayer, sa).isEmpty())
return false;
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}
}

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -12,11 +10,11 @@ import java.util.Map;
public class AssignGroupAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
// otherwise the AI considers the card playable.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {

View File

@@ -45,14 +45,24 @@ public class AttachAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
// TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures
// and gaining card advantage
if (source.hasKeyword("MayFlashSac") && !ai.canCastSorcery()) {
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
return false;
}
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}
if (source.isAura() && sa.isSpell() && !source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
@@ -60,16 +70,20 @@ public class AttachAi extends SpellAbilityAi {
// TODO: Add some extra checks for where the AI may want to cast a replacement aura
// on another creature and keep it when the original enchanted creature is useless
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
return false;
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
// Attach spells always have a target
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
AiAbilityDecision attachDecision = attachPreference(sa, tgt, false);
if (!attachDecision.willingToPlay()) {
return attachDecision;
if (!attachPreference(sa, tgt, false)) {
return false;
}
}
@@ -80,7 +94,7 @@ public class AttachAi extends SpellAbilityAi {
}
if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai)))
&& source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
@@ -88,7 +102,7 @@ public class AttachAi extends SpellAbilityAi {
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
sa.setXManaCostPaid(xPay);
@@ -98,10 +112,10 @@ public class AttachAi extends SpellAbilityAi {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
effectExile.setActivatingPlayer(ai);
final List<Card> targets = CardUtil.getValidCardsToTarget(effectExile);
return !targets.isEmpty() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return !targets.isEmpty();
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
private boolean doAdvancedFlashAuraLogic(Player ai, SpellAbility sa, Card attachTarget) {
@@ -941,8 +955,9 @@ public class AttachAi extends SpellAbilityAi {
* @return true, if successful
*/
@Override
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
final Card card = sa.getHostCard();
// Check if there are any valid targets
List<GameObject> targets = new ArrayList<>();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
@@ -954,44 +969,23 @@ public class AttachAi extends SpellAbilityAi {
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
Card newTarget = (Card) targets.get(0);
//don't equip human creatures
if (newTarget.getController().isOpponentOf(ai)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
//don't equip a worse creature
if (card.isEquipping()) {
Card oldTarget = card.getEquipping();
if (ComputerUtilCard.evaluateCreature(oldTarget) > ComputerUtilCard.evaluateCreature(newTarget)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean stacking = !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
if (!stacking) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// don't equip creatures that don't gain anything
return !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player ai) {
if (sa.isTrigger() && sa.usesTargeting()) {
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
if (tgt != null) {
sa.resetTargets();
sa.getTargets().add(tgt);
}
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return true;
}
private static boolean isAuraSpell(final SpellAbility sa) {
@@ -1011,13 +1005,13 @@ public class AttachAi extends SpellAbilityAi {
* the mandatory
* @return true, if successful
*/
private static AiAbilityDecision attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
GameObject o;
boolean spellCanTargetPlayer = false;
if (isAuraSpell(sa)) {
Card source = sa.getHostCard();
if (!source.hasKeyword(Keyword.ENCHANT)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
String ko = ki.getOriginal();
@@ -1042,11 +1036,11 @@ public class AttachAi extends SpellAbilityAi {
}
if (o == null) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
sa.getTargets().add(o);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/**
@@ -1698,6 +1692,25 @@ public class AttachAi extends SpellAbilityAi {
return chosen;
}
@Override
public boolean chkAIDrawback(final SpellAbility sa, final Player ai) {
// TODO for targeting optional Halvar trigger, needs to be coordinated with PumpAi to make it playable
if (sa.isTrigger() && sa.usesTargeting()) {
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
if (tgt != null) {
sa.resetTargets();
sa.getTargets().add(tgt);
}
return sa.isTargetNumberValid();
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return true;
}
return false;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
@@ -13,7 +11,7 @@ import forge.util.MyRandom;
public class BalanceAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String logic = sa.getParam("AILogic");
int diff = 0;
Player opp = aiPlayer.getWeakestOpponent();
@@ -39,7 +37,7 @@ public class BalanceAi extends SpellAbilityAi {
if (diff < 0) {
// Don't sacrifice permanents even if opponent has a ton of cards in hand
return new AiAbilityDecision(0, forge.ai.AiPlayDecision.CantPlayAi);
return false;
}
final CardCollectionView humHand = opp.getCardsIn(ZoneType.Hand);
@@ -47,7 +45,6 @@ public class BalanceAi extends SpellAbilityAi {
diff += 0.5 * (humHand.size() - compHand.size());
// Larger differential == more chance to actually cast this spell
boolean willPlay = diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
return new AiAbilityDecision(willPlay ? 100 : 0, willPlay ? forge.ai.AiPlayDecision.WillPlay : AiPlayDecision.StopRunawayActivations);
return diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
}
}

View File

@@ -1,7 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -17,51 +16,55 @@ import forge.game.zone.ZoneType;
public class BecomesBlockedAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = aiPlayer.getGame();
if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| !game.getPhaseHandler().getPlayerTurn().isOpponentOf(aiPlayer)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (tgt != null) {
sa.resetTargets();
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
list = CardLists.getTargetableCards(list, sa);
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
sa.resetTargets();
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
list = CardLists.getTargetableCards(list, sa);
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
while (sa.canAddMoreTarget()) {
Card choice = null;
while (sa.canAddMoreTarget()) {
Card choice = null;
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (list.isEmpty()) {
return false;
}
choice = ComputerUtilCard.getBestCreatureAI(list);
choice = ComputerUtilCard.getBestCreatureAI(list);
if (choice == null) { // can't find anything left
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (choice == null) { // can't find anything left
return false;
}
list.remove(choice);
sa.getTargets().add(choice);
}
list.remove(choice);
sa.getTargets().add(choice);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
// TODO - implement AI
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
boolean chance;
// TODO - implement AI
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
chance = false;
return chance;
}
}

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiAttackController;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -12,13 +10,14 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
public class BidLifeAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -27,31 +26,31 @@ public class BidLifeAi extends SpellAbilityAi {
if (tgt.canTgtCreature()) {
List<Card> list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
Card c = ComputerUtilCard.getBestCreatureAI(list);
if (sa.canTarget(c)) {
sa.getTargets().add(c);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else if (tgt.getZone().contains(ZoneType.Stack)) {
if (game.getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final SpellAbility topSA = game.getStack().peekAbility();
if (!topSA.isCounterableBy(sa) || aiPlayer.equals(topSA.getActivatingPlayer())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (sa.canTargetSpellAbility(topSA)) {
sa.getTargets().add(topSA);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
}
}

View File

@@ -17,8 +17,6 @@
*/
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -48,9 +46,9 @@ public final class BondAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
} // end bondCanPlayAI()
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
@@ -58,7 +56,7 @@ public final class BondAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
return true;
}
}

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialAiLogic;
import forge.ai.SpecialCardAi;
@@ -23,18 +21,16 @@ public class BranchAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("GrislySigil".equals(aiLogic)) {
boolean result = SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
return SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
} else if ("BranchCounter".equals(aiLogic)) {
boolean result = SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa);
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
return SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa); // Bring the Ending, Anticognition (hacky implementation)
} else if ("TgtAttacker".equals(aiLogic)) {
final Combat combat = aiPlayer.getGame().getCombat();
if (combat == null || combat.getAttackingPlayer() != aiPlayer) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final CardCollection attackers = combat.getAttackers();
@@ -49,20 +45,16 @@ public class BranchAi extends SpellAbilityAi {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(attackers));
}
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
return sa.isTargetNumberValid();
}
// TODO: expand for other cases where the AI is needed to make a decision on a branch
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (decision.willingToPlay() || mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return canPlayAI(aiPlayer, sa) || mandatory;
}
@Override

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -12,15 +10,15 @@ public class CannotPlayAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false;
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return canPlayAI(aiPlayer, sa);
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.player.Player;
@@ -17,36 +15,34 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// TODO: Extend this if possible for cards that have this as an activated ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
final String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("WeakestOppExceptCtrl")) {
PlayerCollection targetableOpps = aiPlayer.getOpponents();
targetableOpps.remove(sa.getHostCard().getController());
if (targetableOpps.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
@Override
@@ -67,3 +63,4 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
return (T)weakestTargetableOpp;
}
}

View File

@@ -1,6 +1,9 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.card.Card;
@@ -18,7 +21,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Game game = sa.getHostCard().getGame();
final SpellAbility topSa = game.getStack().isEmpty() ? null
: ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
@@ -29,50 +32,47 @@ public class ChangeTargetsAi extends SpellAbilityAi {
// The AI can't otherwise play this ability, but should at least not
// miss mandatory activations (e.g. triggers).
if (sa.isMandatory()) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return sa.isMandatory();
}
private AiAbilityDecision doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
private boolean doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
// For cards like Spellskite that retarget spells to itself
if (topSa == null) {
// nothing on stack, so nothing to target
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final TargetChoices topTargets = topSa.getTargets();
final Card topHost = topSa.getHostCard();
if (!sa.getTargets().isEmpty() && sa.isTrigger()) {
if (sa.getTargets().size() != 0 && sa.isTrigger()) {
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) {
// if this does not target at all or already targets host, no need to redirect it again
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
for (Card tgt : topTargets.getTargetCards()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) {
// We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites),
// no need to retarget again to another one
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) {
// make sure not to redirect our own abilities
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (!topSa.canTarget(sa.getHostCard())) {
// don't try targeting it if we can't legally target the host card with it in the first place
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (!sa.canTarget(topSa)) {
// don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face)
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) {
@@ -85,22 +85,22 @@ public class ChangeTargetsAi extends SpellAbilityAi {
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
&& topTargets.contains(aiPlayer)) {
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
Card firstCard = topTargets.getFirstTargetedCard();
// if we're not the target don't intervene unless we can steal a buff
if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
Player firstPlayer = topTargets.getFirstTargetedPlayer();
if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.resetTargets();
sa.getTargets().add(topSa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}

View File

@@ -46,30 +46,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static CardCollection multipleCardsToChoose = new CardCollection();
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
if (sa.isHidden()) {
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)
&& !"Battlefield".equals(sa.getParam("Destination")) && !source.isLand()) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard) {
CostDiscard cd = (CostDiscard) part;
// this is mainly for typecycling
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
return false;
}
}
}
}
return true;
}
if (sa.isCraft()) {
CardCollection payingCards = new CardCollection();
int needed = 0;
@@ -153,12 +129,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
// Checks for "return true" unlike checkAiLogic()
multipleCardsToChoose.clear();
String aiLogic = sa.getParam("AILogic");
if (aiLogic != null) {
if (aiLogic.equals("Always")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
@@ -178,18 +156,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (aiLogic.equals("MazesEnd")) {
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
} else if (aiLogic.equals("Pongify")) {
if (sa.isTargetNumberValid()) {
// Pre-targeted in checkAiLogic
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
} else if (aiLogic.equals("ReturnCastable")) {
if (!sa.getHostCard().getExiledCards().isEmpty()
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return !sa.getHostCard().getExiledCards().isEmpty()
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false);
}
}
if (sa.isHidden()) {
@@ -208,7 +178,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.isHidden()) {
return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
}
@@ -227,7 +197,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (sa.isReplacementAbility() && "Command".equals(sa.getParam("Destination")) && "ReplacedCard".equals(sa.getParam("Defined"))) {
@@ -236,10 +206,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if ("Always".equals(aiLogic)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if ("IfNotBuffed".equals(aiLogic)) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true; // debuffed by opponent's auras to the level that it becomes useless
}
int delta = 0;
for (Card enc : sa.getHostCard().getEnchantedBy()) {
@@ -249,17 +219,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
delta++;
}
}
if (delta <= 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return delta <= 0;
} else if ("SaviorOfOllenbock".equals(aiLogic)) {
if (SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa);
}
if (sa.isHidden()) {
@@ -288,8 +250,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -303,35 +267,75 @@ public class ChangeZoneAi extends SpellAbilityAi {
} catch (IllegalArgumentException ex) {
// This happens when Origin is something like
// "Graveyard,Library" (Doomsday)
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
final String destination = sa.getParam("Destination");
if (sa.isNinjutsu()) {
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
}
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
&& !(destination.equals("Battlefield") && !source.isLand())) {
return false;
}
if (ai.getGame().getCombat() == null) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
boolean lowerCMC = false;
for (Card attacker : attackers) {
if (attacker.getCMC() < source.getCMC()) {
lowerCMC = true;
break;
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostDiscard) {
CostDiscard cd = (CostDiscard) part;
// this is mainly for typecycling
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
return false;
}
}
}
}
if (!lowerCMC) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
if (sa.isNinjutsu()) {
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
return false;
}
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
return false;
}
if (ai.getGame().getCombat() == null) {
return false;
}
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
boolean lowerCMC = false;
for (Card attacker : attackers) {
if (attacker.getCMC() < source.getCMC()) {
lowerCMC = true;
break;
}
}
if (!lowerCMC) {
return false;
}
}
}
// don't play if the conditions aren't met, unless it would trigger a beneficial sub-condition
if (!activateForCost && !sa.metConditions()) {
final AbilitySub abSub = sa.getSubAbility();
if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) {
if (!abSub.metConditions()) {
return false;
}
} else {
return false;
}
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
Iterable<Player> pDefined = Lists.newArrayList(source.getController());
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null && tgt.canTgtPlayer()) {
@@ -343,7 +347,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.getTargets().add(ai);
}
if (!sa.isTargetNumberValid()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
pDefined = sa.getTargets().getTargetPlayers();
} else {
@@ -387,12 +391,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (!activateForCost && list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("Atarka's Command".equals(sourceName)
&& (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) {
// be strict on playing lands off charms
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
String num = sa.getParamOrDefault("ChangeNum", "1");
@@ -400,60 +404,55 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
if (xPay == 0) return false;
xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay);
} else {
// Figure out the X amount, bail if it's zero (nothing will change zone).
int xValue = AbilityUtils.calculateAmount(source, "X", sa);
if (xValue == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
}
}
if (sourceName.equals("Temur Sabertooth")) {
// activated bounce + pump
boolean pumpDecision = ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible"));
AiAbilityDecision saveDecision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility());
if (pumpDecision || saveDecision.willingToPlay()) {
if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) ||
ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility())) {
for (Card c : list) {
if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(source)) {
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
return true;
}
}
}
if (canBouncePermanent(ai, sa, list) != null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return canBouncePermanent(ai, sa, list) != null;
}
}
if (ComputerUtil.playImmediately(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// don't use fetching to top of library/graveyard before main2
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& !sa.hasParam("ActivationPhases")) {
if (!destination.equals("Battlefield") && !destination.equals("Hand")) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Only tutor something in main1 if hand is almost empty
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand")
&& !aiLogic.equals("AnyMainPhase")) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
if (ComputerUtil.waitForBlocking(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
}
/**
@@ -465,7 +464,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static AiAbilityDecision hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
private static boolean hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
// if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -477,11 +476,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (!isCurse && sa.canTarget(aiPlayer)) {
sa.getTargets().add(aiPlayer);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/**
@@ -495,7 +494,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a boolean.
* @return a boolean.
*/
private static AiAbilityDecision hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
private static boolean hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
@@ -508,7 +507,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
* (bounce useful permanent).
*/
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -546,15 +545,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
pDefined = sa.getTargets().getTargetPlayers();
if (Iterables.isEmpty(pDefined)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
}
@@ -568,10 +567,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// *********** Utility functions for Hidden ********************
@@ -674,7 +673,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static AiAbilityDecision knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Retrieve either this card, or target Cards in Graveyard
final List<ZoneType> origin = Lists.newArrayList();
@@ -686,16 +685,20 @@ public class ChangeZoneAi extends SpellAbilityAi {
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if (sa.usesTargeting()) {
if (!isPreferredTarget(ai, sa, false, false)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
// non-targeted retrieval
final List<Card> retrieval = sa.knownDetermineDefined(sa.getParam("Defined"));
if (retrieval == null || retrieval.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// return this card from graveyard: cards like Hammer of Bogardan
@@ -706,7 +709,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// (dying or losing control of)
if (origin.contains(ZoneType.Battlefield)) {
if (ai.getGame().getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final AbilitySub abSub = sa.getSubAbility();
@@ -719,7 +722,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!(destination.equals(ZoneType.Exile)
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
&& !destination.equals(ZoneType.Hand)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, sa);
@@ -731,13 +734,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (!contains) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
if (destination == ZoneType.Battlefield) {
if (ComputerUtil.isETBprevented(retrieval.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// predict whether something may put a ETBing creature below zero toughness
@@ -747,7 +750,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card copy = CardCopyService.getLKICopy(c);
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
if (copy.getNetToughness() <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -761,12 +764,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (nothingWillReturn) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
}
/*
@@ -780,7 +784,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("SurvivalOfTheFittest")) {
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
return true;
@@ -839,26 +843,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static AiAbilityDecision knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
if ("MimicVat".equals(sa.getParam("AILogic"))) {
if (SpecialCardAi.MimicVat.considerExile(aiPlayer, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa);
}
if (!sa.usesTargeting()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (!isPreferredTarget(aiPlayer, sa, false, true)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else {
// if we are here, we have a target
// so we can play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return isPreferredTarget(aiPlayer, sa, false, true);
}
/**
@@ -1499,7 +1493,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (choice == null) { // can't find anything left
if (sa.getTargets().isEmpty() || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (sa.getTargets().size() == 0 || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
sa.resetTargets();
return false;
}
@@ -1527,21 +1521,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a boolean.
* @return a boolean.
*/
private static AiAbilityDecision knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
final String logic = sa.getParamOrDefault("AILogic", "");
if ("DeathgorgeScavenger".equals(logic)) {
if (SpecialCardAi.DeathgorgeScavenger.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
} else if ("ExtraplanarLens".equals(logic)) {
if (SpecialCardAi.ExtraplanarLens.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
} else if ("ExileCombatThreat".equals(logic)) {
return doExileCombatThreatLogic(ai, sa);
}
@@ -1553,27 +1539,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!list.isEmpty()) {
final Card attachedTo = list.get(0);
// This code is for the Dragon auras
if (!attachedTo.getController().isOpponentOf(ai)) {
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI is the controller of the attachedTo card, then it is a valid target.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return !attachedTo.getController().isOpponentOf(ai);
}
}
} else if (isPreferredTarget(ai, sa, mandatory, true)) {
// do nothing
} else {
if (isUnpreferredTarget(ai, sa, mandatory)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else return isUnpreferredTarget(ai, sa, mandatory);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, Player player, final Player decider) {
@@ -1804,7 +1777,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return super.chooseSingleAttackableEntity(ai, sa, options, params);
}
private AiAbilityDecision doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
@@ -1823,14 +1796,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.resetTargets();
sa.getTargets().add(bestRet);
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
private AiAbilityDecision doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
PhaseHandler ph = ai.getGame().getPhaseHandler();
String logic = sa.getParam("AILogic");
@@ -1838,7 +1811,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!ph.is(PhaseType.MAIN2)) {
// Should be given a chance to cast other spells as well as to use a previously upgraded creature
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
return false;
}
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
@@ -1877,14 +1850,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!listGoal.isEmpty()) {
// make sure we're upgrading sacCMC->goalCMC
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + sacCMC);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
// no candidates to upgrade
return false;
}
public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
public boolean doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
@SuppressWarnings("unchecked")
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>)sa.getReplacingObject(AbilityKey.OriginalParams);
SpellAbility causeSa = (SpellAbility)originalParams.get(AbilityKey.Cause);
@@ -1893,13 +1867,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (Objects.equals(ZoneType.Hand, destination)) {
// If the commander is being moved to your hand, don't replace since its easier to cast it again
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
if (sa.getHostCard().getName().contains("Squee, the Immortal") &&
(destination == ZoneType.Graveyard || destination == ZoneType.Exile)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
@@ -1908,38 +1882,28 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (subApi == ApiType.ChangeZone && "Exile".equals(causeSub.getParam("Origin"))
&& "Battlefield".equals(causeSub.getParam("Destination"))) {
// A blink effect implemented using ChangeZone API
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else // This is an intrinsic effect that blinks the card (e.g. Obzedat, Ghost Council), no need to
// return the commander to the Command zone.
if (subApi == ApiType.DelayedTrigger) {
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
// A blink effect implemented using a delayed trigger
if (!"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
if (causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|| !causeSa.getActivatingPlayer().equals(aiPlayer)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return !"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"));
}
} else return causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|| !causeSa.getActivatingPlayer().equals(aiPlayer);
}
// Normally we want the commander back in Command zone to recast it later
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
// Normally we want the commander back in Command zone to recast him later
return true;
}
public static AiAbilityDecision doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
public static boolean doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
final Combat combat = aiPlayer.getGame().getCombat();
if (combat == null) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
Card choice = null;
@@ -1974,9 +1938,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (choice != null) {
sa.getTargets().add(choice);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
public static Card doExilePreferenceLogic(final Player aiPlayer, final SpellAbility sa, CardCollection fetchList) {

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
@@ -21,7 +19,7 @@ import java.util.Map;
public class ChangeZoneAllAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// Change Zone All, can be any type moving from one zone to another
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
@@ -34,14 +32,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll");
if (!aiLogicAllowsDiscard) {
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
return false;
}
}
}
@@ -61,29 +59,31 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// Ugin AI: always try to sweep before considering +1
if (sourceName.equals("Ugin, the Spirit Dragon")) {
boolean result = SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
}
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
if ("LivingDeath".equals(aiLogic)) {
// Living Death AI
return SpecialCardAi.LivingDeath.consider(ai, sa);
} else if ("Timetwister".equals(aiLogic)) {
// Timetwister AI
return SpecialCardAi.Timetwister.consider(ai, sa);
} else if ("RetDiscardedThisTurn".equals(aiLogic)) {
boolean result = !ai.getDiscardedThisTurn().isEmpty() && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
// e.g. Shadow of the Grave
return ai.getDiscardedThisTurn().size() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
} else if ("ExileGraveyards".equals(aiLogic)) {
for (Player opp : ai.getOpponents()) {
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.CREATURES);
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) {
PlayerCollection players = ai.getOpponents();
players.add(ai);
@@ -98,48 +98,68 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
bestTgt = player;
}
}
if (bestTgt != null) {
sa.resetTargets();
sa.getTargets().add(bestTgt);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// TODO improve restrictions on when the AI would want to use this
// spBounceAll has some AI we can compare to.
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
if (!sa.usesTargeting()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
// TODO: improve logic for non-targeted SAs of this type (most are currently AI:RemoveDeck:All, e.g. Memory Jar)
return true;
} else {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
// get the one with the most handsize
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
}
} else if (origin.equals(ZoneType.Battlefield)) {
// this statement is assuming the AI is trying to use this spell offensively
// if the AI is using it defensively, then something else needs to occur
// if only creatures are affected evaluate both lists and pass only
// if human creatures are more valuable
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
// set the target
if (oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
computerType = new CardCollection();
}
int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units)
int nonCreatureEvalThreshold = 3; // CMC difference
if (ai.getController().isAI()) {
@@ -161,80 +181,103 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
// Life is in serious danger, return all creatures from the battlefield to wherever
// so they don't deal lethal damage
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard
.evaluateCreatureList(oppType)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
} // mass zone change for non-creatures: evaluate both lists by CMC and pass only if human
// permanents are more valuable
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
.evaluatePermanentList(oppType)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Don't cast during main1?
if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) {
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
return false;
}
} else if (origin.equals(ZoneType.Graveyard)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = Collections.max(oppList, AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
} else if (destination.equals(ZoneType.Library) && "Card.YouOwn".equals(sa.getParam("ChangeType"))) {
boolean result = (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
return (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
&& !ComputerUtil.isPlayingReanimator(ai);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (origin.equals(ZoneType.Exile)) {
if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// minimum card advantage unless the hand will be fully reloaded
int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
boolean noDiscard = aiLogic.contains(".noDiscard");
if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
// Try to gain some card advantage if the card will die anyway
// TODO: ideally, should evaluate the hand value and not discard good hands to it
return true;
}
}
boolean result = (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
}
} else if (origin.equals(ZoneType.Stack)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
// TODO
return false;
}
if (destination.equals(ZoneType.Battlefield)) {
if (sa.hasParam("GainControl")) {
// Check if the cards are valuable enough
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
.evaluateCreatureList(oppType)) < 400) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
.evaluatePermanentList(oppType)) < 6) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
// don't activate if human gets more back than AI does
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(computerType) <= (ComputerUtilCard
.evaluateCreatureList(oppType) + 100)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
.evaluatePermanentList(oppType) + 2)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
boolean result = ((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance;
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return (((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance);
}
/**
@@ -249,11 +292,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
// if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something:
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/* (non-Javadoc)
@@ -285,90 +328,127 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, final SpellAbility sa, boolean mandatory) {
// Change Zone All, can be any type moving from one zone to another
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) {
boolean result = ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
// there is no specific AI to support playing it in a smarter way. Feel free to expand.
return ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
}
CardCollectionView humanType = ai.getOpponents().getCardsIn(origin);
humanType = AbilityUtils.filterListByType(humanType, sa.getParam("ChangeType"), sa);
CardCollectionView computerType = ai.getCardsIn(origin);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
// TODO improve restrictions on when the AI would want to use this
// spBounceAll has some AI we can compare to.
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
// get the one with the most handsize
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty() || mandatory) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
}
} else if (origin.equals(ZoneType.Battlefield)) {
// if mandatory, no need to evaluate
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// this statement is assuming the AI is trying to use this spell offensively
// if the AI is using it defensively, then something else needs to occur
// if only creatures are affected evaluate both lists and pass only
// if human creatures are more valuable
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(computerType) >= ComputerUtilCard.evaluateCreatureList(humanType)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are more valuable
else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
return false;
}
} else if (origin.equals(ZoneType.Graveyard)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return sa.isTargetNumberValid() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return sa.isTargetNumberValid();
}
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = oppList.max(
AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty() || mandatory) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
}
} else if (origin.equals(ZoneType.Exile)) {
} else if (origin.equals(ZoneType.Stack)) {
// currently only exists indirectly (e.g. Summary Dismissal via PlayAi)
}
if (destination.equals(ZoneType.Battlefield)) {
// if mandatory, no need to evaluate
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (sa.hasParam("GainControl")) {
// Check if the cards are valuable enough
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
boolean result = (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean result = (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
return (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
return (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
.evaluatePermanentList(humanType)) >= 1;
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// don't activate if human gets more back than AI does
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
boolean result = ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean result = ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
return ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}

View File

@@ -9,6 +9,7 @@ import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.Collections;
@@ -17,7 +18,7 @@ import java.util.Map;
public class CharmAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
@@ -69,10 +70,10 @@ public class CharmAi extends SpellAbilityAi {
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
chosenList = chooseOptionsAi(sa, choices, ai, true, num, min);
if (chosenList.isEmpty() && min != 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -80,7 +81,7 @@ public class CharmAi extends SpellAbilityAi {
sa.setChosenList(chosenList);
if (choiceForOpp) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return true;
}
if (sa.isSpell()) {
@@ -88,7 +89,8 @@ public class CharmAi extends SpellAbilityAi {
CharmEffect.chainAbilities(sa, chosenList);
}
return super.checkApiLogic(ai, sa);
// prevent run-away activations - first time will always return true
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
}
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
@@ -274,10 +276,10 @@ public class CharmAi extends SpellAbilityAi {
}
@Override
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
// choices were already targeted
if (ab.getRootAbility().getChosenList() != null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return super.chkDrawbackWithSubs(aiPlayer, ab);
}

View File

@@ -7,6 +7,7 @@ import forge.game.Game;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
@@ -25,19 +26,19 @@ public class ChooseCardAi extends SpellAbilityAi {
* The rest of the logic not covered by the canPlayAI template is defined here
*/
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) {
sa.resetTargets();
// search targetable Opponents
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.getTargets().add(Iterables.getFirst(oppList, null));
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/**
@@ -134,12 +135,21 @@ public class ChooseCardAi extends SpellAbilityAi {
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if (sa.hasParam("AILogic") && !checkAiLogic(ai, sa, sa.getParam("AILogic"))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
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 checkApiLogic(ai, sa);
return super.checkPhaseRestrictions(ai, sa, ph);
}
/* (non-Javadoc)

View File

@@ -22,20 +22,16 @@ import java.util.Map;
public class ChooseCardNameAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
if (sa.hasParam("AILogic")) {
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
return false;
}
String logic = sa.getParam("AILogic");
if (logic.equals("CursedScroll")) {
if (SpecialCardAi.CursedScroll.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return SpecialCardAi.CursedScroll.consider(ai, sa);
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -47,13 +43,13 @@ public class ChooseCardNameAi extends SpellAbilityAi {
sa.getTargets().add(ai);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("PithingNeedle".equals(aiLogic)) {
// Make sure theres something in play worth Needlings.
@@ -61,27 +57,18 @@ public class ChooseCardNameAi extends SpellAbilityAi {
CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonManaActivatedAbility", ai, sa.getHostCard(), sa);
if (oppPerms.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
Card card = ComputerUtilCard.getBestPlaneswalkerAI(oppPerms);
if (card != null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// 5 percent chance to cast per opposing card with a non mana ability
if (MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size();
}
return mandatory;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)

View File

@@ -11,45 +11,40 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
public class ChooseColorAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final PhaseHandler ph = game.getPhaseHandler();
if (!sa.hasParam("AILogic")) {
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
return false;
}
final String logic = sa.getParam("AILogic");
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if ("Nykthos, Shrine to Nyx".equals(sourceName)) {
if (SpecialCardAi.NykthosShrineToNyx.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return SpecialCardAi.NykthosShrineToNyx.consider(ai, sa);
}
if ("Oona, Queen of the Fae".equals(sourceName)) {
if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
// Set PayX here to maximum value.
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if ("Addle".equals(sourceName)) {
// TODO Why is this not in the AI logic?
// Why are we specifying the weakest opponent?
if (!ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
return !ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
}
if (logic.equals("MostExcessOpponentControls")) {
@@ -59,10 +54,10 @@ public class ChooseColorAi extends SpellAbilityAi {
int excess = ComputerUtilCard.evaluatePermanentList(opplist) - ComputerUtilCard.evaluatePermanentList(ailist);
if (excess > 4) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (logic.equals("MostProminentInComputerDeck")) {
if ("Astral Cornucopia".equals(sourceName)) {
// activate in Main 2 hoping that the extra mana surplus will make a difference
@@ -70,28 +65,22 @@ public class ChooseColorAi extends SpellAbilityAi {
CardCollectionView permanents = CardLists.filter(ai.getCardsIn(ZoneType.Hand),
CardPredicates.NONLAND_PERMANENTS);
if (!permanents.isEmpty() && ph.is(PhaseType.MAIN2, ai)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai);
}
} else if (logic.equals("HighestDevotionToColor")) {
// currently only works more or less reliably in Main2 to cast own spells
if (!ph.is(PhaseType.MAIN2, ai)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
return false;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.Direction;
import forge.game.Game;
@@ -20,11 +18,11 @@ public class ChooseDirectionAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final String logic = sa.getParam("AILogic");
final Game game = sa.getActivatingPlayer().getGame();
if (logic == null) {
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
return false;
} else {
if ("Aminatou".equals(logic)) {
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
@@ -35,24 +33,19 @@ public class ChooseDirectionAi extends SpellAbilityAi {
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Right));
int leftValue = Aggregates.sum(left, Card::getCMC);
int rightValue = Aggregates.sum(right, Card::getCMC);
if (aiValue <= leftValue && aiValue <= rightValue) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return aiValue <= leftValue && aiValue <= rightValue;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return canPlay(ai, sa);
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
}
}

View File

@@ -1,18 +1,17 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiAttackController;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
public class ChooseEvenOddAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
if (!sa.hasParam("AILogic")) {
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
return false;
}
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -20,17 +19,16 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
}
}

View File

@@ -7,6 +7,7 @@ import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
@@ -19,6 +20,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ChooseGenericAi extends SpellAbilityAi {
@Override
@@ -27,10 +29,13 @@ public class ChooseGenericAi extends SpellAbilityAi {
return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
if (SpellApiToAi.Converter.get(sb).canPlayWithSubs(ai, sb).willingToPlay()) {
if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) {
return true;
}
}
} else if ("AtOppEOT".equals(aiLogic)) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
} else if ("Always".equals(aiLogic)) {
return true;
}
@@ -38,46 +43,35 @@ public class ChooseGenericAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.hasParam("AILogic")) {
// This is equivilant to what was here before but feels bad
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
return sa.hasParam("AILogic");
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
AiAbilityDecision decision;
if (sa.isTrigger()) {
decision = doTriggerNoCost(aiPlayer, sa, sa.isMandatory());
} else {
decision = checkApiLogic(aiPlayer, sa);
}
return decision;
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
}
@Override
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) {
for (final Player p : aiPlayer.getOpponents()) {
if (p.canBeTargetedBy(sa)) {
sa.resetTargets();
sa.getTargets().add(p);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
return true; // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
}
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}
@Override
@@ -268,7 +262,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
List<SpellAbility> filtered = Lists.newArrayList();
// filter first for the spells which can be done
for (SpellAbility sp : spells) {
if (SpellApiToAi.Converter.get(sp).canPlayWithSubs(player, sp).willingToPlay()) {
if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) {
filtered.add(sp);
}
}

View File

@@ -1,18 +1,21 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
public class ChooseNumberAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
return false;
} else if (aiLogic.equals("SweepCreatures")) {
int maxChoiceLimit = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Max"), sa);
int ownCreatureCount = aiPlayer.getCreaturesInPlay().size();
@@ -27,24 +30,17 @@ public class ChooseNumberAi extends SpellAbilityAi {
}
if (refOpp == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false; // no opponent has any creatures
}
int evalAI = ComputerUtilCard.evaluateCreatureList(aiPlayer.getCreaturesInPlay());
int evalOpp = ComputerUtilCard.evaluateCreatureList(refOpp.getCreaturesInPlay());
if (aiPlayer.getLifeLostLastTurn() + aiPlayer.getLifeLostThisTurn() == 0 && evalAI > evalOpp) {
// we're not pressured and our stuff seems better, don't do it yet
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false; // we're not pressured and our stuff seems better, don't do it yet
}
if (ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit)) {
// we have more creatures than the opponent, or we have less than the opponent but more than the max choice limit
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// we have less creatures than the opponent and less than the max choice limit
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit);
}
if (sa.usesTargeting()) {
@@ -53,17 +49,16 @@ public class ChooseNumberAi extends SpellAbilityAi {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
}
}

View File

@@ -2,8 +2,6 @@ package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
@@ -17,18 +15,18 @@ import java.util.Map;
public class ChoosePlayerAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean canPlayAI(Player ai, SpellAbility sa) {
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return canPlay(ai, sa);
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
return canPlay(ai, sa);
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return canPlayAI(ai, sa);
}
@Override

View File

@@ -1,7 +1,10 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
@@ -11,6 +14,7 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -28,13 +32,21 @@ public class ChooseSourceAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
// TODO: AI Support! Currently this is copied from AF ChooseCard.
// When implementing AI, I believe AI also needs to be made aware of the damage sources chosen
// to be prevented (e.g. so the AI doesn't attack with a creature that will not deal any damage
// to the player because a CoP was pre-activated on it - unless, of course, there's another
// possible reason to attack with that creature).
final Card host = sa.getHostCard();
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
if (abCost != null) {
if (!willPayCosts(ai, sa, abCost, source)) {
return false;
}
}
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -42,7 +54,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
if (sa.hasParam("AILogic")) {
@@ -51,11 +63,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (!game.getStack().isEmpty()) {
final SpellAbility topStack = game.getStack().peekAbility();
if (sa.hasParam("Choices") && !topStack.matchesValid(topStack.getHostCard(), sa.getParam("Choices").split(","))) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
final ApiType threatApi = topStack.getApi();
if (threatApi != ApiType.DealDamage && threatApi != ApiType.DamageAll) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
final Card threatSource = topStack.getHostCard();
@@ -67,17 +79,13 @@ public class ChooseSourceAi extends SpellAbilityAi {
}
if (!objects.contains(ai) || topStack.hasParam("NoPrevention")) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
int dmg = AbilityUtils.calculateAmount(threatSource, topStack.getParam("NumDmg"), topStack);
if (ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0;
}
if (game.getPhaseHandler().getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
if (sa.hasParam("Choices")) {
@@ -90,13 +98,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
}
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0;
});
if (choices.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return !choices.isEmpty();
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override

View File

@@ -21,37 +21,23 @@ import java.util.Set;
public class ChooseTypeAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
return false;
} else if ("MostProminentComputerControls".equals(aiLogic)) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
if (doMirrorEntityLogic(aiPlayer, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (!chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return doMirrorEntityLogic(aiPlayer, sa);
}
return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty();
} else if ("MostProminentComputerControlsOrOwns".equals(aiLogic)) {
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty()
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty();
} else if ("MostProminentOppControls".equals(aiLogic)) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty()
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
}
return doTriggerNoCost(aiPlayer, sa, false);
return doTriggerAINoCost(aiPlayer, sa, false);
}
private boolean doMirrorEntityLogic(Player aiPlayer, SpellAbility sa) {
@@ -115,7 +101,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean isCurse = sa.isCurse();
if (sa.usesTargeting()) {
@@ -147,16 +133,16 @@ public class ChooseTypeAi extends SpellAbilityAi {
}
if (!sa.isTargetNumberValid()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false; // nothing to target?
}
} else {
for (final Player p : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa)) {
if (p.isOpponentOf(ai) && !mandatory && !isCurse) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
private String chooseType(SpellAbility sa, CardCollectionView cards) {

View File

@@ -2,8 +2,6 @@ package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -24,15 +22,14 @@ public class ClashAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
boolean legalAction = true;
if (sa.usesTargeting()) {
legalAction = selectTarget(aiPlayer, sa);
}
return legalAction ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return legalAction;
}
/*
@@ -42,17 +39,14 @@ public class ClashAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
boolean legalAction = true;
if (sa.usesTargeting()) {
legalAction = selectTarget(ai, sa);
if (!legalAction) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return legalAction;
}
/*
@@ -110,6 +104,7 @@ public class ClashAi extends SpellAbilityAi {
}
}
return !sa.getTargets().isEmpty();
return sa.getTargets().size() > 0;
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.game.ability.AbilityUtils;
@@ -14,8 +12,7 @@ import forge.game.trigger.TriggerType;
public class ClassLevelUpAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
// TODO does leveling up affect combat? Otherwise wait for Main2
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
Card host = sa.getHostCard();
final int level = host.getClassLevel() + 1;
for (StaticAbility stAb : host.getStaticAbilities()) {
@@ -28,12 +25,12 @@ public class ClassLevelUpAi extends SpellAbilityAi {
continue;
}
SpellAbility effect = t.ensureAbility();
if (!SpellApiToAi.Converter.get(effect).doTrigger(aiPlayer, effect, false)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) {
return false;
}
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -20,7 +18,7 @@ import java.util.Map;
public class CloneAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
@@ -38,6 +36,10 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?"
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
return false;
}
PhaseHandler phase = game.getPhaseHandler();
if (!sa.usesTargeting()) {
@@ -64,19 +66,18 @@ public class CloneAi extends SpellAbilityAi {
}
if (!bFlag) { // All of the defined stuff is cloned, not very useful
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
} else {
sa.resetTargets();
useAbility &= cloneTgtAI(sa);
}
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return useAbility;
} // end cloneCanPlayAI()
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
// AI should only activate this during Human's turn
boolean chance = true;
@@ -84,12 +85,11 @@ public class CloneAi extends SpellAbilityAi {
chance = cloneTgtAI(sa);
}
return chance ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return chance;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
Card host = sa.getHostCard();
boolean chance = true;
@@ -111,11 +111,7 @@ public class CloneAi extends SpellAbilityAi {
// Eventually, we can call the trigger of ETB abilities with
// not mandatory as part of the checks to cast something
if (mandatory || chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return chance || mandatory;
}
/**

View File

@@ -1,6 +1,9 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -11,16 +14,16 @@ import forge.game.zone.ZoneType;
public class ConniveAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
if (!ai.canDraw()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false; // can't draw anything
}
Card host = sa.getHostCard();
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
if (num == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false; // Won't do anything
}
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
@@ -38,7 +41,7 @@ public class ConniveAi extends SpellAbilityAi {
sa.resetTargets();
while (sa.canAddMoreTarget()) {
if ((list.isEmpty() && sa.isTargetNumberValid() && !sa.getTargets().isEmpty())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (list.isEmpty()) {
@@ -50,7 +53,7 @@ public class ConniveAi extends SpellAbilityAi {
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
return sa.isTargetNumberValid();
}
Card choice = ComputerUtilCard.getBestCreatureAI(list);
@@ -63,17 +66,13 @@ public class ConniveAi extends SpellAbilityAi {
list.clear();
}
}
if (!sa.getTargets().isEmpty() && sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return !sa.getTargets().isEmpty() && sa.isTargetNumberValid();
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!ai.canDraw() && !mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false; // can't draw anything
}
boolean preferred = true;
@@ -86,7 +85,7 @@ public class ConniveAi extends SpellAbilityAi {
while (sa.canAddMoreTarget()) {
if (mandatory) {
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (list.isEmpty() && preferred) {
@@ -99,13 +98,14 @@ public class ConniveAi extends SpellAbilityAi {
// Still an empty list, but we have to choose something (mandatory); expand targeting to
// include AI's own cards to see if there's anything targetable (e.g. Plague Belcher).
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
preferred = false;
}
}
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
return sa.isTargetNumberValid();
}
Card choice = ComputerUtilCard.getBestCreatureAI(list);
@@ -118,10 +118,7 @@ public class ConniveAi extends SpellAbilityAi {
list.clear();
}
}
return new AiAbilityDecision(
sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0,
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed
);
return true;
}
}

View File

@@ -1,7 +1,9 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -11,6 +13,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
public class ControlExchangeAi extends SpellAbilityAi {
@@ -18,7 +21,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
protected boolean canPlayAI(Player ai, final SpellAbility sa) {
Card object1 = null;
Card object2 = null;
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -38,40 +41,35 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.getTargets().add(object2);
}
if (object1 == null || object2 == null) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (ComputerUtilCard.evaluateCreature(object1) > ComputerUtilCard.evaluateCreature(object2) + 40) {
sa.getTargets().add(object1);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
if (mandatory) {
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return decision;
return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid();
} else {
return canPlay(aiPlayer, sa);
return canPlayAI(aiPlayer, sa);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (!sa.usesTargeting()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -92,7 +90,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty())
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
Card best = ComputerUtilCard.getBestAI(list);
@@ -108,7 +106,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
// Defined card is better than this one, try to avoid trade
if (!best.equals(realBest)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
@@ -117,10 +115,10 @@ public class ControlExchangeAi extends SpellAbilityAi {
return doTrigTwoTargetsLogic(aiPlayer, sa, best);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
private AiAbilityDecision doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final int creatureThreshold = 100; // TODO: make this configurable from the AI profile
final int nonCreatureThreshold = 2;
@@ -132,30 +130,30 @@ public class ControlExchangeAi extends SpellAbilityAi {
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
Card aiWorst = ComputerUtilCard.getWorstAI(list);
if (aiWorst == null) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (aiWorst != bestFirstTgt) {
if (bestFirstTgt.isCreature() && aiWorst.isCreature()) {
if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
// TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine
if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
sa.clearTargets();
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}

View File

@@ -65,7 +65,7 @@ import java.util.Map;
*/
public class ControlGainAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
final List<String> lose = Lists.newArrayList();
if (sa.hasParam("LoseControl")) {
@@ -81,30 +81,22 @@ public class ControlGainAi extends SpellAbilityAi {
if (sa.hasParam("AllValid")) {
CardCollectionView tgtCards = opponents.getCardsIn(ZoneType.Battlefield);
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
if (tgtCards.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return !tgtCards.isEmpty();
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else {
sa.resetTargets();
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return targetingPlayer.getController().chooseTargetsFor(sa);
}
if (tgt.canOnlyTgtOpponent()) {
List<Player> oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (tgt.isRandomTarget()) {
@@ -119,12 +111,12 @@ public class ControlGainAi extends SpellAbilityAi {
if (lose.contains("EOT")
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !sa.isTrigger()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (sa.hasParam("Defined")) {
// no need to target, we'll pick up the target from Defined
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
CardCollection list = opponents.getCardsIn(ZoneType.Battlefield);
@@ -173,7 +165,7 @@ public class ControlGainAi extends SpellAbilityAi {
});
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
int creatures = 0, artifacts = 0, planeswalkers = 0, lands = 0, enchantments = 0;
@@ -202,7 +194,7 @@ public class ControlGainAi extends SpellAbilityAi {
if (list.isEmpty()) {
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -265,41 +257,39 @@ public class ControlGainAi extends SpellAbilityAi {
}
}
return new AiAbilityDecision(
sa.isTargetNumberValid() ? 100 : 0,
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed);
return true;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
if (sa.hasParam("TargetingPlayer") || (mandatory && !this.canPlay(ai, sa).willingToPlay())) {
if (sa.hasParam("TargetingPlayer") || (!this.canPlayAI(ai, sa) && mandatory)) {
if (sa.getTargetRestrictions().canOnlyTgtOpponent()) {
List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
sa.getTargets().add(Aggregates.random(oppList));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, final Player ai) {
public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
final Game game = ai.getGame();
// Special card logic that is processed elsewhere
@@ -315,7 +305,7 @@ public class ControlGainAi extends SpellAbilityAi {
CardCollectionView tgtCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
if (tgtCards.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
}
final List<String> lose = Lists.newArrayList();
@@ -324,14 +314,10 @@ public class ControlGainAi extends SpellAbilityAi {
lose.addAll(Lists.newArrayList(sa.getParam("LoseControl").split(",")));
}
if (lose.contains("EOT")
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
} else {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return !lose.contains("EOT")
|| !game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
} else {
return this.canPlay(ai, sa);
return this.canPlayAI(ai, sa);
}
}

View File

@@ -18,8 +18,6 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -43,22 +41,24 @@ import java.util.Map;
*/
public class ControlGainVariantAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
String logic = sa.getParam("AILogic");
if ("GainControlOwns".equals(logic)) {
List<Card> list = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), crd -> crd.isCreature() && !crd.getController().equals(crd.getOwner()));
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
for (final Card c : list) {
if (ai.equals(c.getController())) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override

View File

@@ -22,52 +22,45 @@ import java.util.function.Predicate;
public class CopyPermanentAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
Card source = sa.getHostCard();
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if ("MomirAvatar".equals(aiLogic)) {
return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
} else if ("MimicVat".equals(aiLogic)) {
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
} else if ("AtEOT".equals(aiLogic)) {
if (ph.is(PhaseType.END_OF_TURN)) {
if (ph.getPlayerTurn() == aiPlayer) {
// If it's the AI's turn, it can activate at EOT
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If it's not the AI's turn, it can't activate at EOT
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// Not at EOT phase
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
} if ("DuplicatePerms".equals(aiLogic)) {
return ph.is(PhaseType.END_OF_TURN);
} else if ("AtOppEOT".equals(aiLogic)) {
return ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn() != aiPlayer;
} else if ("DuplicatePerms".equals(aiLogic)) {
final List<Card> valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if (valid.size() < 2) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
}
if (sa.hasParam("AtEOT") && !ph.is(PhaseType.MAIN1)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
if (sa.hasParam("Defined")) {
// If there needs to be an imprinted card, don't activate the ability if nothing was imprinted yet (e.g. Mimic Vat)
if (sa.getParam("Defined").equals("Imprinted.ExiledWithSource") && source.getImprintedCards().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
}
if (sa.isEmbalm() || sa.isEternalize()) {
// E.g. Vizier of Many Faces: check to make sure it makes sense to make the token now
AiPlayDecision decision = ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa);
if (decision != AiPlayDecision.WillPlay) {
return new AiAbilityDecision(0, decision);
if (ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa) != AiPlayDecision.WillPlay) {
return false;
}
}
@@ -82,45 +75,37 @@ public class CopyPermanentAi extends SpellAbilityAi {
sa.resetTargets();
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return targetingPlayer.getController().chooseTargetsFor(sa);
} else if (sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer()) {
if (!sa.isCurse()) {
if (sa.canTarget(aiPlayer)) {
sa.getTargets().add(aiPlayer);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else {
for (Player p : aiPlayer.getYourTeam()) {
if (sa.canTarget(p)) {
sa.getTargets().add(p);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
for (Player p : aiPlayer.getOpponents()) {
if (sa.canTarget(p)) {
sa.getTargets().add(p);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
return doTriggerNoCost(aiPlayer, sa, false);
return doTriggerAINoCost(aiPlayer, sa, false);
}
}
@Override
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
final Card host = sa.getHostCard();
final Player activator = sa.getActivatingPlayer();
final Game game = host.getGame();
@@ -143,13 +128,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
//Nothing to target
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
CardCollection betterList = CardLists.filter(list, CardPredicates.isRemAIDeck().negate());
if (betterList.isEmpty()) {
if (!mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
list = betterList;
@@ -161,7 +146,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
if (felidarGuardian.size() > 0) {
// can copy a Felidar Guardian and combo off, so let's do it
sa.getTargets().add(felidarGuardian.get(0));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -170,9 +155,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -192,9 +177,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -209,22 +194,20 @@ public class CopyPermanentAi extends SpellAbilityAi {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host, sa);
Collection<Card> betterChoices = getBetterOptions(aiPlayer, sa, choices, !mandatory);
if (betterChoices.isEmpty()) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return mandatory;
}
} else {
// if no targeting, it should always be ok
}
if ("TriggeredCardController".equals(sa.getParam("Controller"))) {
Card trigCard = (Card)sa.getTriggeringObject(AbilityKey.Card);
if (!mandatory && trigCard != null && trigCard.getController().isOpponentOf(aiPlayer)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/* (non-Javadoc)

View File

@@ -17,15 +17,14 @@ import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
Game game = aiPlayer.getGame();
int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK);
int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF);
String logic = sa.getParamOrDefault("AILogic", "");
if (game.getStack().isEmpty()) {
boolean result = sa.isMandatory() || "Always".equals(logic);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return sa.isMandatory() || "Always".equals(logic);
}
final SpellAbility top = game.getStack().peekAbility();
@@ -42,40 +41,47 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (!MyRandom.percentTrue(chance)
&& !"Always".equals(logic)
&& !"AlwaysIfViable".equals(logic)
&& !"OnceIfViable".equals(logic)
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("OnceIfViable".equals(logic)) {
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
return false;
}
}
if (sa.usesTargeting()) {
// Filter AI-specific targets if provided
if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
if (!top.getActivatingPlayer().equals(aiPlayer)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
if (top.isWrapper() || top.isActivatedAbility()) {
// Shouldn't even try with triggered or wrapped abilities at this time, will crash
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (top.getApi() == ApiType.CopySpellAbility) {
// Don't try to copy a copy ability, too complex for the AI to handle
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (top.getApi() == ApiType.Mana) {
// would lead to Stack Overflow by trying to play this again
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) {
if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
// If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
// it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if (top.hasParam("ConditionManaSpent") || top.getHostCard().hasSVar("AINoCopy")) {
// Mana spent is not copied, so these spells generally do nothing when copied.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
// Don't try to copy anything you can't understand how to handle
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// A copy is necessary to properly test the SA before targeting the copied spell, otherwise the copy SA will fizzle.
@@ -93,49 +99,32 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (decision == AiPlayDecision.WillPlay) {
sa.getTargets().add(top);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return true;
}
return new AiAbilityDecision(0, decision);
}
}
// the AI should not miss mandatory activations
boolean result = sa.isMandatory() || "Always".equals(logic);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return sa.isMandatory() || "Always".equals(logic);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
String logic = sa.getParamOrDefault("AILogic", "");
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (logic.contains("Always")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return mandatory || logic.contains("Always"); // this includes logic like AlwaysIfViable
}
@Override
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
}
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (!decision.willingToPlay()) {
if (sa.isMandatory()) {
return super.chkDrawback(sa, aiPlayer);
}
}
return decision;
return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer));
}
@Override
@@ -149,7 +138,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
// Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then
// run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents.
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(player, sa).willingToPlay();
return SpecialCardAi.ChainOfAcid.consider(player, sa);
}
return true;

View File

@@ -26,11 +26,13 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class CounterAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
boolean toReturn = true;
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Game game = ai.getGame();
@@ -38,12 +40,22 @@ public class CounterAi extends SpellAbilityAi {
SpellAbility tgtSA = null;
if (game.getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}
if ("Force of Will".equals(sourceName)) {
if (!SpecialCardAi.ForceOfWill.consider(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -51,19 +63,19 @@ public class CounterAi extends SpellAbilityAi {
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
if ((topSA.isSpell() && !topSA.isCounterableBy(sa)) || ai.getYourTeam().contains(topSA.getActivatingPlayer())) {
// might as well check for player's friendliness
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return false;
}
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided
if (sa.hasParam("AITgts") && (topSA.getHostCard() == null
|| !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (sa.hasParam("CounterNoManaSpell") && topSA.getTotalManaSpent() > 0) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (sa.hasParam("UnlessCost") && "TargetedController".equals(sa.getParamOrDefault("UnlessPayer", "TargetedController"))) {
@@ -72,7 +84,7 @@ public class CounterAi extends SpellAbilityAi {
CostDiscard discardCost = unlessCost.getCostPartByType(CostDiscard.class);
if ("Hand".equals(discardCost.getType())) {
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -88,11 +100,10 @@ public class CounterAi extends SpellAbilityAi {
tgtCMC += topSA.getPayCosts().getTotalMana().countX() > 0 ? 3 : 0; // TODO: somehow determine the value of X paid and account for it?
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
// This spell doesn't target. Must be a "Coutner All" or "Counter trigger" type of ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
@@ -111,13 +122,13 @@ public class CounterAi extends SpellAbilityAi {
}
if (toPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of the time
if (!playReusable(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
}
@@ -136,15 +147,15 @@ public class CounterAi extends SpellAbilityAi {
if (sa.hasParam("AILogic")) {
String logic = sa.getParam("AILogic");
if ("Never".equals(logic)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts
int minCMC = Integer.parseInt(logic.substring(7));
if (tgtCMC < minCMC) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if ("NullBrooch".equals(logic)) {
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -223,40 +234,40 @@ public class CounterAi extends SpellAbilityAi {
}
if (dontCounter) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return toReturn;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
return doTriggerNoCost(aiPlayer, sa, true);
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return doTriggerAINoCost(aiPlayer, sa, true);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Game game = ai.getGame();
if (sa.usesTargeting()) {
if (game.getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
sa.resetTargets();
if (mandatory && !sa.canAddMoreTarget()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory);
SpellAbility tgtSA = pair.getLeft();
if (tgtSA == null) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
sa.getTargets().add(tgtSA);
if (!mandatory && !pair.getRight()) {
// If not mandatory and not preferred, bail out after setting target
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
@@ -277,13 +288,14 @@ public class CounterAi extends SpellAbilityAi {
if (!mandatory) {
if (toPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of the time
// If this is a reusable Resource, feel free to play it most
// of the time
if (!playReusable(ai,sa) || (MyRandom.getRandom().nextFloat() < .4)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
}
}
@@ -300,7 +312,7 @@ public class CounterAi extends SpellAbilityAi {
// force the Human into making decisions)
// But really it should be more picky about how it counters things
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
public Pair<SpellAbility, Boolean> chooseTargetSpellAbility(Game game, SpellAbility sa, Player ai, boolean mandatory) {
@@ -350,11 +362,11 @@ public class CounterAi extends SpellAbilityAi {
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
// ward or human misplay
final Card source = sa.getHostCard();
final Game game = source.getGame();
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
for (SpellAbility toBeCountered : spells) {
// ward or human misplay
if (!toBeCountered.isCounterableBy(sa)) {
return false;
}
@@ -369,7 +381,7 @@ public class CounterAi extends SpellAbilityAi {
}
// no reason to pay if we don't plan to confirm
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false).willingToPlay()) {
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
return false;
}
// TODO check hasFizzled

View File

@@ -45,13 +45,13 @@ public abstract class CountersAi extends SpellAbilityAi {
* </p>
*
* @param list
* a {@link CardCollectionView} object.
* a {@link forge.CardList} object.
* @param type
* a {@link String} object.
* a {@link java.lang.String} object.
* @param amount
* a int.
* @param ai a {@link Player} object.
* @return a {@link Card} object.
* @param newParam TODO
* @return a {@link forge.game.card.Card} object.
*/
public static Card chooseCursedTarget(final CardCollectionView list, final String type, final int amount, final Player ai) {
Card choice;
@@ -65,7 +65,7 @@ public abstract class CountersAi extends SpellAbilityAi {
// try to kill the best killable creature, or reduce the best one
// but try not to target a Undying Creature
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING);
if (!killable.isEmpty()) {
if (killable.size() > 0) {
choice = ComputerUtilCard.getBestCreatureAI(killable);
} else {
choice = ComputerUtilCard.getBestCreatureAI(list);
@@ -83,10 +83,10 @@ public abstract class CountersAi extends SpellAbilityAi {
* </p>
*
* @param list
* a {@link CardCollectionView} object.
* a {@link forge.CardList} object.
* @param type
* a {@link String} object.
* @return a {@link Card} object.
* a {@link java.lang.String} object.
* @return a {@link forge.game.card.Card} object.
*/
public static Card chooseBoonTarget(final CardCollectionView list, final String type) {
Card choice = null;

View File

@@ -1,7 +1,9 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
@@ -19,25 +21,19 @@ import java.util.Map;
public class CountersMoveAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
AiAbilityDecision decision = new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) {
sa.resetTargets();
decision = moveTgtAI(ai, sa);
if (!decision.willingToPlay()) {
return decision;
if (!moveTgtAI(ai, sa)) {
return false;
}
}
if (!playReusable(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (MyRandom.getRandom().nextFloat() < .8f) {
return decision;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return MyRandom.getRandom().nextFloat() < .8f; // random success
}
@Override
@@ -113,13 +109,12 @@ public class CountersMoveAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
sa.resetTargets();
AiAbilityDecision decision = moveTgtAI(ai, sa);
if (!decision.willingToPlay() && !mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
if (!moveTgtAI(ai, sa) && !mandatory) {
return false;
}
if (!sa.isTargetNumberValid() && mandatory) {
@@ -127,18 +122,18 @@ public class CountersMoveAi extends SpellAbilityAi {
List<Card> tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
if (tgtCards.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
final Card card = ComputerUtilCard.getWorstAI(tgtCards);
sa.getTargets().add(card);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else {
// no target Probably something like Graft
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
final Card host = sa.getHostCard();
@@ -150,7 +145,7 @@ public class CountersMoveAi extends SpellAbilityAi {
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
if (srcCards.isEmpty() || destCards.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
final Card src = srcCards.get(0);
@@ -158,21 +153,21 @@ public class CountersMoveAi extends SpellAbilityAi {
// for such Trigger, do not move counter to another players creature
if (!dest.getController().equals(ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (ComputerUtilCard.isUselessCreature(ai, dest)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (dest.hasSVar("EndOfTurnLeavePlay")) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (cType != null) {
if (!dest.canReceiveCounters(cType)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final int amount = calcAmount(sa, cType);
int a = src.getCounters(cType);
if (a < amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final Card srcCopy = CardCopyService.getLKICopy(src);
@@ -186,31 +181,27 @@ public class CountersMoveAi extends SpellAbilityAi {
int newEval = ComputerUtilCard.evaluateCreature(srcCopy) + ComputerUtilCard.evaluateCreature(destCopy);
if (newEval < oldEval) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
// check for some specific AI preferences
if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
if (!cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0;
}
}
// no target
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return true;
}
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if (sa.usesTargeting()) {
sa.resetTargets();
return moveTgtAI(ai, sa);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
private static int calcAmount(final SpellAbility sa, final CounterType cType) {
@@ -235,7 +226,7 @@ public class CountersMoveAi extends SpellAbilityAi {
return amount;
}
private AiAbilityDecision moveTgtAI(final Player ai, final SpellAbility sa) {
private boolean moveTgtAI(final Player ai, final SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = ai.getGame();
final String type = sa.getParam("CounterType");
@@ -253,7 +244,7 @@ public class CountersMoveAi extends SpellAbilityAi {
if (destCards.isEmpty()) {
// something went wrong
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
final Card dest = destCards.get(0);
@@ -262,7 +253,7 @@ public class CountersMoveAi extends SpellAbilityAi {
tgtCards.remove(dest);
if (cType != null && !dest.canReceiveCounters(cType)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// prefered logic for this: try to steal counter
@@ -294,7 +285,7 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -338,14 +329,14 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else if (sa.getMaxTargets() == 2) {
// TODO
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// SA uses target for Defined
// Source => Targeted
@@ -353,12 +344,12 @@ public class CountersMoveAi extends SpellAbilityAi {
if (srcCards.isEmpty()) {
// something went wrong
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
final Card src = srcCards.get(0);
if (cType != null && src.getCounters(cType) <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
Card lkiWithCounters = CardCopyService.getLKICopy(src);
@@ -411,14 +402,14 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (!isMandatoryTrigger) {
// no good target
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
@@ -448,10 +439,10 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}

View File

@@ -1,7 +1,9 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
@@ -19,41 +21,42 @@ import java.util.Map;
public class CountersMultiplyAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
return setTargets(ai, sa);
}
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final CounterType counterType = getCounterType(sa);
// defined are mostly Self or Creatures you control
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
list = CardLists.filter(list, c -> {
if (!c.hasCounters()) {
return false;
}
if (!sa.usesTargeting()) {
// defined are mostly Self or Creatures you control
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (counterType != null) {
if (c.getCounters(counterType) <= 0) {
list = CardLists.filter(list, c -> {
if (!c.hasCounters()) {
return false;
}
if (!c.canReceiveCounters(counterType)) {
return false;
}
} else {
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
// has negative counter it would double
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
if (counterType != null) {
if (c.getCounters(counterType) <= 0) {
return false;
}
if (!c.canReceiveCounters(counterType)) {
return false;
}
} else {
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
// has negative counter it would double
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
return false;
}
}
}
return true;
});
if (list.isEmpty()) {
return false;
}
return true;
});
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} else {
return setTargets(ai, sa);
}
return super.checkApiLogic(ai, sa);
@@ -82,27 +85,24 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
AiAbilityDecision decision = setTargets(ai, sa);
if (decision.willingToPlay()) {
return decision;
if (setTargets(ai, sa)) {
return true;
} else if (mandatory) {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
Card safeMatch = list.stream()
.filter(CardPredicates.hasCounters().negate())
.findFirst().orElse(null);
sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch);
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return mandatory;
}
private CounterType getCounterType(SpellAbility sa) {
@@ -117,7 +117,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
return null;
}
private AiAbilityDecision setTargets(Player ai, SpellAbility sa) {
private boolean setTargets(Player ai, SpellAbility sa) {
final CounterType counterType = getCounterType(sa);
final Game game = ai.getGame();
@@ -173,10 +173,10 @@ public class CountersMultiplyAi extends SpellAbilityAi {
// targeting does failed
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
private void addTargetsByCounterType(final Player ai, final SpellAbility sa, final CardCollection list,

View File

@@ -16,7 +16,7 @@ import java.util.Map;
public class CountersProliferateAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final List<Card> cperms = Lists.newArrayList();
boolean allyExpOrEnergy = false;
@@ -68,34 +68,25 @@ public class CountersProliferateAi extends SpellAbilityAi {
}));
}
if (!cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy) {
// AI will play it if there are any counters to proliferate
// or if there are no counters, but AI has experience or energy counters
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
boolean chance = true;
// TODO Make sure Human has poison counters or there are some counters
// we want to proliferate
return new AiAbilityDecision(
chance ? 100 : 0,
chance ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi
);
return chance;
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if ("Always".equals(sa.getParam("AILogic"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return checkApiLogic(ai, sa);

View File

@@ -53,7 +53,8 @@ public class CountersPutAi extends CountersAi {
// disable moving counters (unless a specialized AI logic supports it)
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter remCounter) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType counterType = remCounter.counter;
if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) {
return false;
@@ -97,7 +98,7 @@ public class CountersPutAi extends CountersAi {
}
}
if (sa.isKeyword(Keyword.LEVEL_UP)) {
if (sa.hasParam("LevelUp")) {
// creatures enchanted by curse auras have low priority
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
for (Card aura : source.getEnchantedBy()) {
@@ -118,7 +119,7 @@ public class CountersPutAi extends CountersAi {
}
@Override
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Cost abCost = sa.getPayCosts();
@@ -159,7 +160,7 @@ public class CountersPutAi extends CountersAi {
PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9));
if (!poisonList.isEmpty()) {
sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife()));
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
return true;
}
}
@@ -175,7 +176,7 @@ public class CountersPutAi extends CountersAi {
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
if (best != null) {
sa.getTargets().add(best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
CardCollection aiCreat = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
@@ -195,7 +196,7 @@ public class CountersPutAi extends CountersAi {
best = ComputerUtilCard.getBestAI(aiCreat);
if (best != null) {
sa.getTargets().add(best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -204,22 +205,28 @@ public class CountersPutAi extends CountersAi {
if (!ai.getCounters().isEmpty()) {
if (!eachExisting || ai.getPoisonCounters() < 5) {
sa.getTargets().add(ai);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("AlwaysWithNoTgt".equals(logic)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if ("Never".equals(logic)) {
return false;
} else if ("AlwaysWithNoTgt".equals(logic)) {
return true;
} else if ("AristocratCounters".equals(logic)) {
return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa);
} else if ("PayEnergy".equals(logic)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if ("PayEnergyConservatively".equals(logic)) {
boolean onlyInCombat = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
@@ -228,10 +235,10 @@ public class CountersPutAi extends CountersAi {
if (playAggro) {
// aggro profiles ignore conservative play for this AI logic
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if (ph.inCombat() && source != null) {
if (ai.getGame().getCombat().isAttacking(source) && !onlyDefensive) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
} else if (ai.getGame().getCombat().isBlocking(source)) {
// when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker
CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(source);
@@ -241,27 +248,28 @@ public class CountersPutAi extends CountersAi {
int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
if (source.getNetToughness() + numActivations > totBlkPower
|| source.getNetPower() + numActivations >= totBlkToughness) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
}
}
} else if (sa.getSubAbility() != null
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ANIMATED_THIS_TURN)) {
// Bristling Hydra: save from death using a ping activation
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return true;
}
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
// outside of combat, this logic only works if the relevant AI profile option is enabled
// and if there is enough energy saved
if (!onlyInCombat) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
} else if (logic.equals("MarkOppCreature")) {
if (!ph.is(PhaseType.END_OF_TURN)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
return false;
}
Predicate<Card> predicate = CardPredicates.hasCounter(CounterType.getType(type));
@@ -273,12 +281,12 @@ public class CountersPutAi extends CountersAi {
Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
sa.resetTargets();
sa.getTargets().add(bestCreat);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else if (logic.equals("CheckDFC")) {
// for cards like Ludevic's Test Subject
if (!source.canTransform(null)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if (logic.startsWith("MoveCounter")) {
return doMoveCounterLogic(ai, sa, ph);
@@ -287,15 +295,8 @@ public class CountersPutAi extends CountersAi {
if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) {
// don't use this for mana until after combat
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
return new AiAbilityDecision(25, AiPlayDecision.WaitForMain2);
}
if (willActivate) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return willActivate;
} else if (logic.equals("ChargeToBestCMC")) {
return doChargeToCMCLogic(ai, sa);
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
@@ -304,11 +305,15 @@ public class CountersPutAi extends CountersAi {
return SpecialCardAi.TheOneRing.consider(ai, sa);
}
if (!sa.metConditions() && sa.getSubAbility() == null) {
return false;
}
if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence
CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility());
if (!prot.isEmpty()) {
sa.getTargets().add(prot.get(0));
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
}
}
@@ -316,13 +321,13 @@ public class CountersPutAi extends CountersAi {
CardCollection creatsYouCtrl = ai.getCreaturesInPlay();
List<Card> leastToughness = Aggregates.listWithMin(creatsYouCtrl, Card::getNetToughness);
if (leastToughness.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
// TODO If Creature that would be Bolstered for some reason is useless, also return False
}
if (sa.hasParam("Monstrosity") && source.isMonstrous()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// TODO handle proper calculation of X values based on Cost
@@ -337,7 +342,7 @@ public class CountersPutAi extends CountersAi {
Combat combat = game.getCombat();
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return doCombatAdaptLogic(source, amount, combat);
}
@@ -364,12 +369,12 @@ public class CountersPutAi extends CountersAi {
// This will "rewind" clockwork cards when they fall to 50% power or below, consider improving
if (curCtrs > Math.ceil(maxCtrs / 2.0)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
amount = Math.min(amount, maxCtrs - curCtrs);
if (amount <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -381,14 +386,14 @@ public class CountersPutAi extends CountersAi {
.mapToInt(Card::getCMC)
.max().orElse(0);
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
// don't use it if no counters to add
if (amount <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("Polukranos".equals(logic)) {
@@ -415,14 +420,20 @@ public class CountersPutAi extends CountersAi {
}
}
if (!canSurvive) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
found = true;
break;
}
if (!found) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
if ("AtOppEOT".equals(logic)) {
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
return true;
}
}
@@ -433,19 +444,17 @@ public class CountersPutAi extends CountersAi {
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target
if (sa.getMinTargets() < 2) {
AiAbilityDecision decision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa);
if (decision.willingToPlay()) {
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
Card c = sa.getTargetCard();
if (sa.getTargets().size() > 1) {
sa.resetTargets();
sa.getTargets().add(c);
}
sa.addDividedAllocation(c, amount);
return decision;
return true;
} else {
if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return false;
}
}
}
@@ -489,7 +498,7 @@ public class CountersPutAi extends CountersAi {
}
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
@@ -498,9 +507,9 @@ public class CountersPutAi extends CountersAi {
&& sa.isPwAbility()
&& sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
&& sa.isTargetNumberValid()
&& sa.getTargets().isEmpty()
&& sa.getTargets().size() == 0
&& ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (sourceName.equals("Abzan Charm")) {
@@ -522,11 +531,11 @@ public class CountersPutAi extends CountersAi {
}
}
if (left == 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
sa.resetTargets();
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// target loop
@@ -534,7 +543,7 @@ public class CountersPutAi extends CountersAi {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -566,9 +575,10 @@ public class CountersPutAi extends CountersAi {
// check if other choice will already be played
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
}
if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
if (!source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|| ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) {
if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) {
if (abCost == null || abCost == Cost.Zero
|| (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) {
// only use at opponent EOT unless it is free
choice = chooseBoonTarget(list, type);
}
@@ -582,7 +592,7 @@ public class CountersPutAi extends CountersAi {
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -598,14 +608,14 @@ public class CountersPutAi extends CountersAi {
choice = null;
}
if (sa.getTargets().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
// Don't activate Curse abilities on my cards and non-curse abilities
// on my opponents
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
@@ -613,50 +623,51 @@ public class CountersPutAi extends CountersAi {
// activating this ability.
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Instant +1/+1
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false; // only if next turn and cost is reusable
}
}
// Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
boolean immediately = ComputerUtil.playImmediately(ai, sa);
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
return false;
}
if (immediately) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (!type.equals("P1P1") && !type.equals("M1M1") && !sa.hasParam("ActivationPhases")) {
// Don't use non P1P1/M1M1 counters before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
return false;
}
if (ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
}
if (ComputerUtil.waitForBlocking(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
public AiAbilityDecision chkDrawback(final SpellAbility sa, Player ai) {
public boolean chkAIDrawback(final SpellAbility sa, Player ai) {
boolean chance = true;
final Game game = ai.getGame();
Card choice = null;
final String type = sa.getParam("CounterType");
@@ -690,9 +701,9 @@ public class CountersPutAi extends CountersAi {
while (sa.canAddMoreTarget()) {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid()
|| sa.getTargets().isEmpty()) {
|| sa.getTargets().size() == 0) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
break;
}
@@ -713,9 +724,9 @@ public class CountersPutAi extends CountersAi {
}
if (choice == null) { // can't find anything left
if ((!sa.isTargetNumberValid()) || (sa.getTargets().isEmpty())) {
if ((!sa.isTargetNumberValid()) || (sa.getTargets().size() == 0)) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -730,14 +741,15 @@ public class CountersPutAi extends CountersAi {
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return chance;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// boolean chance = true;
boolean preferred = true;
CardCollection list;
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
@@ -758,14 +770,9 @@ public class CountersPutAi extends CountersAi {
}
if ("ChargeToBestCMC".equals(aiLogic)) {
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
if (decision.willingToPlay()) {
return decision;
} else if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return doChargeToCMCLogic(ai, sa) || mandatory;
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
return doChargeToOppCtrlCMCLogic(ai, sa) || mandatory;
}
if (!sa.usesTargeting()) {
@@ -794,7 +801,7 @@ public class CountersPutAi extends CountersAi {
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
if (playerList.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// try to choose player with less creatures
@@ -810,9 +817,8 @@ public class CountersPutAi extends CountersAi {
if (type.equals("P1P1")) {
nPump = amount;
}
AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump);
if (decision.willingToPlay()) {
return decision;
if (FightAi.canFightAi(ai, sa, nPump, nPump)) {
return true;
}
}
@@ -833,7 +839,7 @@ public class CountersPutAi extends CountersAi {
if (mandatory) {
// When things are mandatory, gotta handle a little differently
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return true;
}
if (list.isEmpty() && preferred) {
@@ -853,7 +859,7 @@ public class CountersPutAi extends CountersAi {
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
return sa.isTargetNumberValid();
}
Card choice = null;
@@ -906,7 +912,7 @@ public class CountersPutAi extends CountersAi {
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
@@ -1114,7 +1120,7 @@ public class CountersPutAi extends CountersAi {
return Iterables.getFirst(options, null);
}
private AiAbilityDecision doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
private boolean doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
// Spikes (Tempest)
// Try not to do it unless at the end of opponent's turn or the creature is threatened
@@ -1127,7 +1133,7 @@ public class CountersPutAi extends CountersAi {
|| (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))));
if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
@@ -1145,45 +1151,45 @@ public class CountersPutAi extends CountersAi {
if (bestTgt != null) {
sa.getTargets().add(bestTgt);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
private AiAbilityDecision doCombatAdaptLogic(Card source, int amount, Combat combat) {
private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) {
if (combat.isAttacking(source)) {
if (!combat.isBlocked(source)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else {
for (Card blockedBy : combat.getBlockers(source)) {
if (blockedBy.getNetToughness() > source.getNetPower()
&& blockedBy.getNetToughness() <= source.getNetPower() + amount) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
if (source.getNetToughness() <= totBlkPower
&& source.getNetToughness() + amount > totBlkPower) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
}
}
} else if (combat.isBlocking(source)) {
for (Card blocked : combat.getAttackersBlockedBy(source)) {
if (blocked.getNetToughness() > source.getNetPower()
&& blocked.getNetToughness() <= source.getNetPower() + amount) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
return true;
}
}
int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower);
if (source.getNetToughness() <= totAtkPower
&& source.getNetToughness() + amount > totAtkPower) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
@Override
@@ -1194,7 +1200,7 @@ public class CountersPutAi extends CountersAi {
return max;
}
private AiAbilityDecision doChargeToCMCLogic(Player ai, SpellAbility sa) {
private boolean doChargeToCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
@@ -1209,14 +1215,10 @@ public class CountersPutAi extends CountersAi {
optimalCMC = cmc;
}
}
if (numCtrs < optimalCMC) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return numCtrs < optimalCMC;
}
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
@@ -1230,12 +1232,6 @@ public class CountersPutAi extends CountersAi {
optimalCMC = cmc;
}
}
if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return numCtrs < optimalCMC;
}
}

View File

@@ -1,13 +1,12 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -16,15 +15,17 @@ import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class CountersPutAllAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
List<Card> hList;
List<Card> cList;
@@ -43,9 +44,28 @@ public class CountersPutAllAi extends SpellAbilityAi {
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
}
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
}
if (logic.equals("AtEOTOrBlock")) {
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
} else if (logic.equals("AtOppEOT")) {
if (!(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
return false;
}
}
@@ -68,23 +88,26 @@ public class CountersPutAllAi extends SpellAbilityAi {
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
}
// prevent run-away activations - first time will always return true
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
if (curse) {
if (type.equals("M1M1")) {
final List<Card> killable = CardLists.filter(hList, c -> c.getNetToughness() <= amount);
if (killable.size() <= 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
if (!(killable.size() > 2)) {
return false;
}
} else {
// make sure compy doesn't harm his stuff more than human's
// stuff
if (cList.size() > hList.size()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
} else {
// human has more things that will benefit, don't play
if (hList.size() >= cList.size()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
//Check for cards that could profit from the ability
@@ -102,21 +125,21 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
}
if (!combatants) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
if (playReusable(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return chance;
}
return super.checkApiLogic(ai, sa);
return ((MyRandom.getRandom().nextFloat() < .6667) && chance);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return canPlay(ai, sa);
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
@@ -127,7 +150,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (sa.usesTargeting()) {
List<Player> players = Lists.newArrayList();
if (!sa.isCurse()) {
@@ -145,23 +168,11 @@ public class CountersPutAllAi extends SpellAbilityAi {
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
sa.resetTargets();
sa.getTargets().add(p);
if (preferred) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return preferred || mandatory;
}
}
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
return mandatory || canPlayAI(aiPlayer, sa);
}
}

View File

@@ -18,7 +18,9 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
@@ -50,13 +52,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
if (doTgt(ai, sa, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return doTgt(ai, sa, false);
}
return super.checkApiLogic(ai, sa);
}
@@ -182,27 +180,11 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
if (doTgt(ai, sa, mandatory)) {
// if we can target, then we can play it
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
// if we can't target, then we can't play it
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (mandatory) {
// if mandatory, just play it
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// if not mandatory, check if we can play it
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return doTgt(ai, sa, mandatory);
}
return mandatory;
}
/*

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
@@ -25,6 +23,14 @@ import java.util.function.Predicate;
public class CountersRemoveAi extends SpellAbilityAi {
@Override
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
}
return super.canPlayWithoutRestrict(ai, sa);
}
/*
* (non-Javadoc)
*
@@ -42,6 +48,24 @@ public class CountersRemoveAi extends SpellAbilityAi {
return super.checkPhaseRestrictions(ai, sa, ph);
}
/*
* (non-Javadoc)
*
* @see
* forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player,
* forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler,
* java.lang.String)
*/
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if ("EndOfOpponentsTurn".equals(logic)) {
if (!ph.is(PhaseType.END_OF_TURN) || !ph.getNextTurn().equals(ai)) {
return false;
}
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
/*
* (non-Javadoc)
*
@@ -49,7 +73,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final String type = sa.getParam("CounterType");
if (sa.usesTargeting()) {
@@ -59,14 +83,14 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!type.matches("Any") && !type.matches("All")) {
final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type));
if (currCounters < 1) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
return super.checkApiLogic(ai, sa);
}
private AiAbilityDecision doTgt(Player ai, SpellAbility sa, boolean mandatory) {
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final Game game = ai.getGame();
@@ -79,7 +103,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(tgt.getZone()), sa);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// Filter AI-specific targets if provided
@@ -97,7 +121,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardPredicates.hasCounter(CounterEnumType.ICE, 3));
if (!depthsList.isEmpty()) {
sa.getTargets().add(depthsList.getFirst());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -110,7 +134,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!planeswalkerList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else if (type.matches("Any")) {
// variable amount for Hex Parasite
@@ -120,7 +144,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (manaLeft == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
amount = manaLeft;
xPay = true;
@@ -142,7 +166,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) {
sa.setXManaCostPaid(ice);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
@@ -161,7 +185,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) {
sa.setXManaCostPaid(best.getCurrentLoyalty());
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// some rules only for amount = 1
@@ -178,7 +202,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiM1M1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// do as P1P1 part
@@ -187,7 +211,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiUndyingList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// TODO stun counters with canRemoveCounters check
@@ -198,7 +222,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardPredicates.hasCounter(CounterEnumType.P1P1));
if (!oppP1P1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// fallback to remove any counter from opponent
@@ -210,7 +234,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
for (final CounterType aType : best.getCounters().keySet()) {
if (!ComputerUtil.isNegativeCounter(aType, best)) {
sa.getTargets().add(best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
@@ -231,7 +255,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else if (type.equals("P1P1")) {
// no special amount for that one yet
@@ -249,7 +273,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
if (!aiList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
@@ -263,7 +287,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!oppList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
} else if (type.equals("TIME")) {
@@ -274,7 +298,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (manaLeft == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
amount = manaLeft;
xPay = true;
@@ -292,7 +316,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) {
sa.setXManaCostPaid(timeCount);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
if (mandatory) {
@@ -301,7 +325,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
if (!adaptCreats.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// Outlast nice target
@@ -312,27 +336,26 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!betterTargets.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
return doTgt(aiPlayer, sa, mandatory);
}
return mandatory ? new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
return mandatory;
}
/*
@@ -346,7 +369,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
GameEntity target = (GameEntity) params.get("Target");
CounterType type = (CounterType) params.get("CounterType");
if (target instanceof Card targetCard) {
if (target instanceof Card) {
Card targetCard = (Card) target;
if (targetCard.getController().isOpponentOf(player)) {
return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
} else {
@@ -357,7 +381,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
}
} else if (target instanceof Player targetPlayer) {
} else if (target instanceof Player) {
Player targetPlayer = (Player) target;
if (targetPlayer.isOpponentOf(player)) {
return !type.is(CounterEnumType.POISON) ? max : min;
} else {

View File

@@ -5,24 +5,38 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.function.Predicate;
public class DamageAllAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Card source = sa.getHostCard();
// prevent run-away activations - first time will always return true
if (MyRandom.getRandom().nextFloat() > Math.pow(.9, sa.getActivationsThisTurn())) {
return false;
}
// abCost stuff that should probably be centralized...
final Cost abCost = sa.getPayCosts();
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}
// wait until stack is empty (prevents duplicate kills)
if (!ai.getGame().getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
return false;
}
int x = -1;
@@ -37,15 +51,11 @@ public class DamageAllAi extends SpellAbilityAi {
if (x == -1) {
if (determineOppToKill(ai, sa, source, dmg) != null) {
// we already know we can kill a player, so go for it
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// look for other value in this (damaging creatures or
// creatures + player, e.g. Pestilence, etc.)
if (evaluateDamageAll(ai, sa, source, dmg) > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return evaluateDamageAll(ai, sa, source, dmg) > 0;
} else {
int best = -1, best_x = -1;
Player bestOpp = determineOppToKill(ai, sa, source, x);
@@ -71,9 +81,9 @@ public class DamageAllAi extends SpellAbilityAi {
if (sa.getSVar(damage).equals("Count$xPaid")) {
sa.setXManaCostPaid(best_x);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -175,7 +185,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard();
final String validP = sa.getParamOrDefault("ValidPlayers", "");
@@ -201,21 +211,21 @@ public class DamageAllAi extends SpellAbilityAi {
}
// Don't get yourself killed
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// if we can kill human, do it
if ((validP.equals("Player") || validP.equals("Opponent") || validP.contains("Targeted"))
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) > ComputerUtilCard
.evaluateCreatureList(humanList)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/**
@@ -248,7 +258,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final String validP = sa.getParamOrDefault("ValidPlayers", "");
@@ -277,24 +287,24 @@ public class DamageAllAi extends SpellAbilityAi {
// If it's not mandatory check a few things
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// Don't get yourself killed
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// if we can kill human, do it
if ((validP.equals("Player") || validP.contains("Opponent") || validP.contains("Targeted"))
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) + 50 >= ComputerUtilCard
.evaluateCreatureList(humanList)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}

View File

@@ -38,7 +38,7 @@ import java.util.Map;
public class DamageDealAi extends DamageAiBase {
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final SpellAbility root = sa.getRootAbility();
final String damage = sa.getParam("NumDmg");
Card source = sa.getHostCard();
@@ -65,19 +65,15 @@ public class DamageDealAi extends DamageAiBase {
continue; // in case the calculation gets messed up somewhere
}
root.setSVar("EnergyToPay", "Number$" + dmg);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (sa.getSVar(damage).equals("Count$xPaid")) {
// Life Drain
if ("XLifeDrain".equals(logic)) {
if (doXLifeDrainLogic(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return doXLifeDrainLogic(ai, sa);
}
// Set PayX here to maximum value.
@@ -87,15 +83,11 @@ public class DamageDealAi extends DamageAiBase {
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
}
}
if (damageTargetAI(ai, sa, dmg, true)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return damageTargetAI(ai, sa, dmg, true);
}
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
@@ -116,7 +108,7 @@ public class DamageDealAi extends DamageAiBase {
boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0);
boolean isLethal = sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -142,10 +134,10 @@ public class DamageDealAi extends DamageAiBase {
if (shouldTgtP(ai, sa, maxDmg, false)) {
sa.resetTargets();
sa.getTargets().add(maxDamaged);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
}
@@ -162,7 +154,7 @@ public class DamageDealAi extends DamageAiBase {
if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
for (Card potentialAtkr : ai.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -183,24 +175,16 @@ public class DamageDealAi extends DamageAiBase {
* Mostly used to ping the player with remaining counters. The issue with
* stacked effects might appear here.
*/
if (damageTargetAI(ai, sa, n, true)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return damageTargetAI(ai, sa, n, true);
} else {
/*
* Only ping when stack is clear to avoid hassle of evaluating stacked effects
* like protection/pumps or over-killing target.
*/
if (ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
}
return ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false);
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if ("NinThePainArtist".equals(logic)) {
// Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard
@@ -209,15 +193,11 @@ public class DamageDealAi extends DamageAiBase {
if (doTarget) {
Card tgt = sa.getTargetCard();
if (tgt != null) {
if (ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController();
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
return false;
}
if (sourceName.equals("Sorin, Grim Nemesis")) {
@@ -229,35 +209,35 @@ public class DamageDealAi extends DamageAiBase {
continue; // in case the calculation gets messed up somewhere
}
sa.setXManaCostPaid(dmg);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (dmg <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
if (ComputerUtil.preventRunAwayActivations(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Try to chain damage/debuff effects
@@ -268,13 +248,13 @@ public class DamageDealAi extends DamageAiBase {
int extraDmg = chainDmg.getValue();
boolean willTargetIfChained = damageTargetAI(ai, sa, dmg + extraDmg, false);
if (!willTargetIfChained) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); // won't play it even in chain
return false; // won't play it even in chain
} else if (willTargetIfChained && chainDmg.getKey().getApi() == ApiType.Pump && sa.getTargets().isTargetingAnyPlayer()) {
// we're trying to chain a pump spell to a damage spell targeting a player, that won't work
// so run an additional check to ensure that we want to cast the current spell separately
sa.resetTargets();
if (!damageTargetAI(ai, sa, dmg, false)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
} else {
// we are about to decide to play this damage spell; if there's something chained to it, reserve mana for
@@ -284,7 +264,7 @@ public class DamageDealAi extends DamageAiBase {
}
} else if (!damageTargetAI(ai, sa, dmg, false)) {
// simple targeting when there is no spell chaining plan
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) ||
@@ -308,12 +288,10 @@ public class DamageDealAi extends DamageAiBase {
if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
final int cmc = sa.getXManaCostPaid();
if (!ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc));
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/**
@@ -954,14 +932,14 @@ public class DamageDealAi extends DamageAiBase {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final String damage = sa.getParam("NumDmg");
int dmg = calculateDamageAmount(sa, source, damage);
// Remove all damage
if (sa.hasParam("Remove")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
@@ -972,18 +950,10 @@ public class DamageDealAi extends DamageAiBase {
if (!sa.usesTargeting()) {
// If it's not mandatory check a few things
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (damageChooseNontargeted(ai, sa, dmg)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return mandatory || damageChooseNontargeted(ai, sa, dmg);
} else {
if (!damageChoosingTargets(ai, sa, sa.getTargetRestrictions(), dmg, mandatory, true) && !mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) {
@@ -1006,7 +976,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
private static int calculateDamageAmount(SpellAbility sa, Card source, String damage) {

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpecialCardAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
@@ -16,7 +14,7 @@ public class DamageEachAi extends DamageAiBase {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final String logic = sa.getParam("AILogic");
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
@@ -24,41 +22,30 @@ public class DamageEachAi extends DamageAiBase {
if (sa.usesTargeting() && weakestOpp != null) {
if ("MadSarkhanUltimate".equals(logic) && !SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.resetTargets();
if (weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife()) {
sa.getTargets().add(weakestOpp);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
sa.getTargets().add(weakestOpp);
return weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife();
}
final String damage = sa.getParam("NumDmg");
final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
if (shouldTgtP(ai, sa, iDmg, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return shouldTgtP(ai, sa, iDmg, false);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
// check AI life before playing this drawback?
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
}
}

View File

@@ -1,11 +1,15 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -20,12 +24,18 @@ import java.util.List;
public class DamagePreventAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Game game = ai.getGame();
final Combat combat = game.getCombat();
boolean chance = false;
final Cost cost = sa.getPayCosts();
if (!willPayCosts(ai, sa, cost, hostCard)) {
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
// As far as I can tell these Defined Cards will only have one of them
@@ -60,7 +70,7 @@ public class DamagePreventAi extends SpellAbilityAi {
chance = flag;
} else { // if nothing on the stack, and it's not declare
// blockers. no need to prevent
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
} // non-targeted
@@ -110,7 +120,7 @@ public class DamagePreventAi extends SpellAbilityAi {
targetables = CardLists.getTargetableCards(targetables, sa);
if (targetables.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
final CardCollection combatants = CardLists.filter(targetables, CardPredicates.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants);
@@ -127,15 +137,11 @@ public class DamagePreventAi extends SpellAbilityAi {
sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(hostCard, sa.getParam("Amount"), sa));
}
if (chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return chance;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean chance = false;
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
@@ -145,11 +151,7 @@ public class DamagePreventAi extends SpellAbilityAi {
chance = preventDamageMandatoryTarget(ai, sa, mandatory);
}
if (chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
}
return chance;
}
/**

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -13,34 +11,24 @@ import java.util.Map;
public class DayTimeAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
if ((sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) || sa.getPayCosts().hasManaCost()) {
// If it involves a cost that may put us at a disadvantage, better activate before own turn if possible
if (!isSorcerySpeed(sa, aiPlayer)) {
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;
} else {
if (ph.is(PhaseType.MAIN2, aiPlayer)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return ph.is(PhaseType.MAIN2, aiPlayer); // Give other things a chance to be cast (e.g. Celestus)
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return true; // TODO: more logic if it's ever a bad idea to trigger this (when non-mandatory)
}
@Override

View File

@@ -1,7 +1,10 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -23,27 +26,27 @@ import java.util.List;
public class DebuffAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
// if there is no target and host card isn't in play, don't activate
final Card source = sa.getHostCard();
final Game game = ai.getGame();
if (!sa.usesTargeting() && !source.isInPlay()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final Cost cost = sa.getPayCosts();
// temporarily disabled until AI is improved
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
}
final PhaseHandler ph = game.getPhaseHandler();
@@ -55,7 +58,7 @@ public class DebuffAi extends SpellAbilityAi {
// Instant-speed pumps should not be cast outside of combat when the
// stack is empty, unless there are specific activation phase requirements
if (!isSorcerySpeed(sa, ai) && !sa.hasParam("ActivationPhases")) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
return false;
}
}
@@ -63,7 +66,7 @@ public class DebuffAi extends SpellAbilityAi {
List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
final Combat combat = game.getCombat();
if (cards.stream().anyMatch(c -> {
return cards.stream().anyMatch(c -> {
if (c.getController().equals(sa.getActivatingPlayer()) || combat == null)
return false;
@@ -72,34 +75,21 @@ public class DebuffAi extends SpellAbilityAi {
}
// don't add duplicate negative keywords
return sa.hasParam("Keywords") && c.hasAnyKeyword(Arrays.asList(sa.getParam("Keywords").split(" & ")));
})) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
});
} else {
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
}
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if (!sa.usesTargeting()) {
// TODO - copied from AF_Pump.pumpDrawbackAI() - what should be here?
} else {
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} // debuffDrawbackAI()
/**
@@ -244,24 +234,18 @@ public class DebuffAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final List<String> kws = sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : new ArrayList<>();
if (!sa.usesTargeting()) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
if (debuffTgtAI(ai, sa, kws, mandatory)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return debuffTgtAI(ai, sa, kws, mandatory);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}

View File

@@ -15,56 +15,43 @@ import forge.game.zone.ZoneType;
public class DelayedTriggerAi extends SpellAbilityAi {
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if ("Always".equals(sa.getParam("AILogic"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
// TODO: improve ai
return true;
}
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
trigsa.setActivatingPlayer(ai);
if (trigsa instanceof AbilitySub) {
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
} else {
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
if (decision == AiPlayDecision.WillPlay) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
trigsa.setActivatingPlayer(ai);
if (!sa.hasParam("OptionalDecider")) {
if (aic.doTrigger(trigsa, true)) {
// If the trigger is mandatory, we can play it
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return aic.doTrigger(trigsa, true);
} else {
if (aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"));
}
}
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// Card-specific logic
String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("SpellCopy")) {
@@ -103,9 +90,9 @@ public class DelayedTriggerAi extends SpellAbilityAi {
});
if (count == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if (logic.equals("NarsetRebound")) {
// should be done in Main2, but it might broke for other cards
//if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
@@ -138,10 +125,10 @@ public class DelayedTriggerAi extends SpellAbilityAi {
});
if (count == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if (logic.equals("SaveCreature")) {
CardCollection ownCreatures = ai.getCreaturesInPlay();
@@ -155,25 +142,19 @@ public class DelayedTriggerAi extends SpellAbilityAi {
if (!ownCreatures.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(ownCreatures));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// Generic logic
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
trigsa.setActivatingPlayer(ai);
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
if (decision == AiPlayDecision.WillPlay) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}
}

View File

@@ -20,7 +20,7 @@ import forge.util.collect.FCollectionView;
public class DestroyAi extends SpellAbilityAi {
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return checkApiLogic(ai, sa);
}
@@ -103,27 +103,36 @@ public class DestroyAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final boolean noRegen = sa.hasParam("NoRegen");
final String logic = sa.getParam("AILogic");
CardCollection list;
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
// Targeting
if (sa.usesTargeting()) {
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
sa.getRootAbility().setXManaCostPaid(xPay);
}
// Assume there where already enough targets chosen by AI Logic Above
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// reset targets before AI Logic part
sa.resetTargets();
int maxTargets;
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if (sa.getRootAbility().costHasManaX() ||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
if (sa.getRootAbility().costHasManaX()) {
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
// need to set XPaid to get the right number for
@@ -136,22 +145,23 @@ public class DestroyAi extends SpellAbilityAi {
if (maxTargets == 0) {
// can't afford X or otherwise target anything
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
return false;
}
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return targetingPlayer.getController().chooseTargetsFor(sa);
}
// AI doesn't destroy own cards if it isn't defined in AI logic
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
if ("FatalPush".equals(logic)) {
final int cmcMax = ai.hasRevolt() ? 4 : 2;
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
}
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
@@ -196,7 +206,7 @@ public class DestroyAi extends SpellAbilityAi {
// Try to avoid targeting creatures that are dead on board
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
// target loop
@@ -211,7 +221,7 @@ public class DestroyAi extends SpellAbilityAi {
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -225,7 +235,7 @@ public class DestroyAi extends SpellAbilityAi {
if ("OppDestroyYours".equals(logic)) {
Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay());
if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
} else if (CardLists.getNotType(list, "Land").isEmpty()) {
@@ -234,7 +244,7 @@ public class DestroyAi extends SpellAbilityAi {
if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) {
// Strip Mine, Wasteland - cut short if the relevant logic fails
if (!doLandForLandRemovalLogic(sa, ai, choice, logic)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
} else {
@@ -244,14 +254,14 @@ public class DestroyAi extends SpellAbilityAi {
//option to hold removal instead only applies for single targeted removal
if (!sa.isTrigger() && sa.getMaxTargets() == 1) {
if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
}
if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -288,22 +298,22 @@ public class DestroyAi extends SpellAbilityAi {
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|| ai.getLife() <= 5)) {
// Basic ai logic for Lethal Vapors
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if ("Always".equals(logic)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (list.isEmpty()
|| !CardLists.filterControlledBy(list, ai).isEmpty()
|| CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final boolean noRegen = sa.hasParam("NoRegen");
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -311,7 +321,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty() || list.size() < sa.getMinTargets()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Try to avoid targeting creatures that are dead on board
@@ -339,7 +349,7 @@ public class DestroyAi extends SpellAbilityAi {
list.removeAll(preferred);
if (preferred.isEmpty() && !mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
while (sa.canAddMoreTarget()) {
@@ -347,12 +357,12 @@ public class DestroyAi extends SpellAbilityAi {
if (!sa.isMinTargetChosen()) {
if (!mandatory) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
} else {
break;
}
} else {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
Card c = ComputerUtilCard.getBestAI(preferred);
@@ -387,18 +397,9 @@ public class DestroyAi extends SpellAbilityAi {
}
}
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return sa.isTargetNumberValid();
} else {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return mandatory;
}
}

View File

@@ -23,23 +23,38 @@ public class DestroyAllAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return doMassRemovalLogic(ai, sa);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return doMassRemovalLogic(aiPlayer, sa);
}
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("FellTheMighty".equals(aiLogic)) {
@@ -49,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi {
return doMassRemovalLogic(ai, sa);
}
public static AiAbilityDecision doMassRemovalLogic(Player ai, SpellAbility sa) {
public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
@@ -57,7 +72,7 @@ public class DestroyAllAi extends SpellAbilityAi {
final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1);
if (logic.equals("Always")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
}
String valid = sa.getParamOrDefault("ValidCards", "");
@@ -77,7 +92,7 @@ public class DestroyAllAi extends SpellAbilityAi {
opplist = CardLists.filter(opplist, predicate);
ailist = CardLists.filter(ailist, predicate);
if (opplist.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (sa.usesTargeting()) {
@@ -86,7 +101,7 @@ public class DestroyAllAi extends SpellAbilityAi {
sa.getTargets().add(opponent);
ailist.clear();
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -95,35 +110,30 @@ public class DestroyAllAi extends SpellAbilityAi {
int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, ailist.size());
int numOppsCanSave = Math.min(CardLists.count(ai.getOpponents().getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, opplist.size());
if (numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (numAiCanSave < ailist.size() && (opplist.size() - numOppsCanSave < ailist.size() - numAiCanSave)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
}
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// if only creatures are affected evaluate both lists and pass only if human creatures are more valuable
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
return false;
}
// test whether the human can kill the ai next turn
@@ -136,42 +146,39 @@ public class DestroyAllAi extends SpellAbilityAi {
}
}
if (!containsAttacker) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
AiBlockController block = new AiBlockController(ai, false);
block.assignBlockersForCombat(combat);
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} // only lands involved
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) {
// TODO Should care about any land recursion, not just Crucible of Worlds
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = opponent.getCreaturesInPlay();
if (!oppCreatures.isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
// check if the AI would lose more lands than the opponent would
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}

View File

@@ -20,21 +20,26 @@ import forge.util.TextUtil;
import java.util.Map;
public class DigAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Card host = sa.getHostCard();
Player libraryOwner = ai;
if (!willPayCosts(ai, sa, sa.getPayCosts(), host)) {
return false;
}
if (sa.usesTargeting()) {
sa.resetTargets();
if (!sa.canTarget(opp)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.getTargets().add(opp);
libraryOwner = opp;
@@ -42,21 +47,29 @@ public class DigAi extends SpellAbilityAi {
// return false if nothing to dig into
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("Never".equals(sa.getParam("AILogic"))) {
return false;
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
return false;
}
}
// don't deck yourself
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
// Don't use draw abilities before main 2 if possible
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final String num = sa.getParam("DigNum");
@@ -74,14 +87,14 @@ public class DigAi extends SpellAbilityAi {
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()) - manaToSave;
if (numCards <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
root.setXManaCostPaid(numCards);
}
}
if (playReusable(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
@@ -89,24 +102,24 @@ public class DigAi extends SpellAbilityAi {
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
&& !ComputerUtil.activateForCost(sa, ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("MadSarkhanDigDmg".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return !ComputerUtil.preventRunAwayActivations(sa);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
// TODO: improve this check in ways that may be specific to a subability
return canPlay(aiPlayer, sa);
return canPlayAI(aiPlayer, sa);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
@@ -124,16 +137,12 @@ public class DigAi extends SpellAbilityAi {
int manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]);
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, true) - manaToSave;
if (numCards <= 0) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(100, AiPlayDecision.CantPlayAi);
}
return mandatory;
}
root.setXManaCostPaid(numCards);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
@@ -194,12 +203,15 @@ public class DigAi extends SpellAbilityAi {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
Card topc = player.getZone(ZoneType.Library).get(0);
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Explorer's Scope")) {
// for Explorer's Scope, always put a land on the battlefield tapped
// (TODO: might not always be a good idea, e.g. when a land ETBing can have detrimental effects)
return true;
} else if ("AlwaysConfirm".equals(sa.getParam("AILogic"))) {
return true;
// AI actions for individual cards (until this AI can be generalized)
if (sa.getHostCard() != null) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Explorer's Scope")) {
// for Explorer's Scope, always put a land on the battlefield tapped
// (TODO: might not always be a good idea, e.g. when a land ETBing can have detrimental effects)
return true;
} else if ("AlwaysConfirm".equals(sa.getParam("AILogic"))) {
return true;
}
}
// looks like perfect code for Delver of Secrets, but what about other cards?

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -12,12 +14,13 @@ import forge.game.zone.ZoneType;
import java.util.Map;
public class DigMultipleAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Card host = sa.getHostCard();
@@ -26,7 +29,7 @@ public class DigMultipleAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.getTargets().add(opp);
libraryOwner = opp;
@@ -34,29 +37,33 @@ public class DigMultipleAi extends SpellAbilityAi {
// return false if nothing to dig into
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("Never".equals(sa.getParam("AILogic"))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
return false;
}
}
// don't deck yourself
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
// Don't use draw abilities before main 2 if possible
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (playReusable(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
@@ -64,14 +71,14 @@ public class DigMultipleAi extends SpellAbilityAi {
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
&& !ComputerUtil.activateForCost(sa, ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return !ComputerUtil.preventRunAwayActivations(sa);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -82,7 +89,7 @@ public class DigMultipleAi extends SpellAbilityAi {
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/* (non-Javadoc)

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
@@ -9,6 +11,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
@@ -16,7 +19,7 @@ import java.util.Map;
public class DigUntilAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
double chance = .4; // 40 percent chance with instant speed stuff
@@ -39,7 +42,7 @@ public class DigUntilAi extends SpellAbilityAi {
// material in the library after using it several times.
// TODO: maybe this should happen for any DigUntil SA with RevealedDestination$ Graveyard?
if (ai.getCardsIn(ZoneType.Library).size() < 20) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("Land.Basic".equals(sa.getParam("Valid"))
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.LANDS_PRODUCING_MANA)) {
@@ -49,7 +52,7 @@ public class DigUntilAi extends SpellAbilityAi {
// This is important for Replenish/Living Death type decks
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -57,7 +60,7 @@ public class DigUntilAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!sa.canTarget(opp)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.getTargets().add(opp);
libraryOwner = opp;
@@ -65,7 +68,7 @@ public class DigUntilAi extends SpellAbilityAi {
if (sa.hasParam("Valid")) {
final String valid = sa.getParam("Valid");
if (CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), valid, source.getController(), source, sa).isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -77,7 +80,7 @@ public class DigUntilAi extends SpellAbilityAi {
if (root.getXManaCostPaid() == null) {
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (numCards <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
root.setXManaCostPaid(numCards);
}
@@ -85,14 +88,15 @@ public class DigUntilAi extends SpellAbilityAi {
// return false if nothing to dig into
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
return randomReturn;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
sa.resetTargets();
if (sa.isCurse()) {
@@ -112,7 +116,7 @@ public class DigUntilAi extends SpellAbilityAi {
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
/* (non-Javadoc)

View File

@@ -26,29 +26,31 @@ import forge.util.collect.FCollectionView;
public class DiscardAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Cost abCost = sa.getPayCosts();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// temporarily disabled until better AI
if (!willPayCosts(ai, sa, abCost, source)) {
return false;
}
if ("Chandra, Flamecaller".equals(sourceName)) {
final int hand = ai.getCardsIn(ZoneType.Hand).size();
if (MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
}
if (aiLogic.equals("VolrathsShapeshifter")) {
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
}
final boolean humanHasHand = !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
final boolean humanHasHand = ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).size() > 0;
if (sa.usesTargeting()) {
if (!discardTargetAI(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
// TODO: Add appropriate restrictions
@@ -62,7 +64,7 @@ public class DiscardAi extends SpellAbilityAi {
} else {
// defined to the human, so that's fine as long the human has cards
if (!humanHasHand) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
} else {
@@ -76,12 +78,12 @@ public class DiscardAi extends SpellAbilityAi {
final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()), ai.getWeakestOpponent()
.getCardsIn(ZoneType.Hand).size());
if (cardsToDiscard < 1) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.setXManaCostPaid(cardsToDiscard);
} else {
if (AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa) < 1) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -111,7 +113,7 @@ public class DiscardAi extends SpellAbilityAi {
}
}
if (numDiscard == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -119,25 +121,27 @@ public class DiscardAi extends SpellAbilityAi {
// Don't use discard abilities before main 2 if possible
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& !sa.hasParam("ActivationPhases") && !aiLogic.startsWith("AnyPhase")) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (aiLogic.equals("AnyPhaseIfFavored")) {
if (ai.getGame().getCombat() != null) {
if (ai.getCardsIn(ZoneType.Hand).size() < ai.getGame().getCombat().getDefenderPlayerByAttacker(source).getCardsIn(ZoneType.Hand).size()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(0.9, sa.getActivationsThisTurn());
// some other variables here, like handsize vs. maxHandSize
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return randomReturn;
}
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
@@ -162,7 +166,7 @@ public class DiscardAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
@@ -172,7 +176,7 @@ public class DiscardAi extends SpellAbilityAi {
} else if (mandatory && sa.canTarget(ai)) {
sa.getTargets().add(ai);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
} else {
@@ -180,7 +184,7 @@ public class DiscardAi extends SpellAbilityAi {
if ("AtLeast2".equals(sa.getParam("AILogic"))) {
final List<Player> players = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
if (players.isEmpty() || players.get(0).getCardsIn(ZoneType.Hand).size() < 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}
@@ -192,22 +196,18 @@ public class DiscardAi extends SpellAbilityAi {
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// Drawback AI improvements
// if parent draws cards, make sure cards in hand + cards drawn > 0
if (sa.usesTargeting()) {
if (discardTargetAI(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return discardTargetAI(ai, sa);
}
// TODO: check for some extra things
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {

View File

@@ -1,6 +1,9 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
@@ -13,8 +16,12 @@ import java.util.Map;
public class DiscoverAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop
}
return true;
}
/**
@@ -29,12 +36,8 @@ public class DiscoverAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return checkApiLogic(ai, sa);
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
return mandatory || checkApiLogic(ai, sa);
}
@Override

View File

@@ -1,23 +1,23 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
import java.util.List;
public class DrainManaAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// AI cannot use this properly until he can use SAs during Humans turn
final Card source = sa.getHostCard();
final Player opp = ai.getWeakestOpponent();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
if (!sa.usesTargeting()) {
// assume we are looking to tap human's stuff
@@ -25,58 +25,56 @@ public class DrainManaAi extends SpellAbilityAi {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (!defined.contains(opp)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
sa.resetTargets();
sa.getTargets().add(opp);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return randomReturn;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getWeakestOpponent();
final Card source = sa.getHostCard();
if (!sa.usesTargeting()) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(opp)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return defined.contains(opp);
}
} else {
sa.resetTargets();
sa.getTargets().add(opp);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI cannot use this properly until he can use SAs during Humans turn
final Card source = sa.getHostCard();
boolean randomReturn = true;
if (!sa.usesTargeting()) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
sa.resetTargets();
sa.getTargets().add(ai.getWeakestOpponent());
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return randomReturn;
}
}

View File

@@ -42,41 +42,43 @@ public class DrawAi extends SpellAbilityAi {
* @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, forge.game.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
if (!targetAI(ai, sa, false)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (sa.usesTargeting()) {
final Player player = sa.getTargets().getFirstTargetedPlayer();
if (player != null && player.isOpponentOf(ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return true;
}
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if (ComputerUtil.playImmediately(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return true;
}
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
return false;
}
if (!canLoot(ai, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) {
// Canopy lands and other cards that sacrifice themselves to draw cards
if (ai.getCardsIn(ZoneType.Hand).isEmpty()
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5)) {
// TODO: make this configurable in the AI profile
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return ai.getCardsIn(ZoneType.Hand).isEmpty()
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5); // TODO: make this configurable in the AI profile
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return true;
}
/*
@@ -159,6 +161,8 @@ public class DrawAi extends SpellAbilityAi {
// LifeLessThan logic presupposes activation as soon as possible in an
// attempt to save the AI from dying
return true;
} else if (logic.equals("AtOppEOT")) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
} else if (logic.equals("RespondToOwnActivation")) {
return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
} else if ((!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
@@ -171,12 +175,8 @@ public class DrawAi extends SpellAbilityAi {
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
if (targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay());
}
/**
@@ -534,16 +534,12 @@ public class DrawAi extends SpellAbilityAi {
} // drawTargetAI()
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!mandatory && !willPayCosts(ai, sa, sa.getPayCosts(), sa.getHostCard())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (targetAI(ai, sa, mandatory)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return targetAI(ai, sa, mandatory);
}
/* (non-Javadoc)

View File

@@ -35,7 +35,7 @@ import java.util.Map;
public class EffectAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
protected boolean canPlayAI(final Player ai,final SpellAbility sa) {
final Game game = ai.getGame();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
String logic = "";
@@ -45,7 +45,12 @@ public class EffectAi extends SpellAbilityAi {
final PhaseHandler phase = game.getPhaseHandler();
if (logic.equals("BeginningOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
randomReturn = true;
} else if (logic.equals("EndOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
return false;
}
randomReturn = true;
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
@@ -59,20 +64,20 @@ public class EffectAi extends SpellAbilityAi {
worthHolding = true;
}
if (!worthHolding) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
randomReturn = true;
}
} else if (logic.equals("RestrictBlocking")) {
if (!phase.isPlayerTurn(ai) || phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)
|| phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (sa.getPayCosts().getTotalMana().countX() > 0 && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
// Set PayX here to half the remaining mana to allow for Main 2 and other combat shenanigans.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai, sa.isTrigger()) / 2;
if (xPay == 0) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
if (xPay == 0) { return false; }
sa.setXManaCostPaid(xPay);
}
@@ -85,27 +90,23 @@ public class EffectAi extends SpellAbilityAi {
int potentialDmg = 0;
List<Card> currentAttackers = new ArrayList<>();
if (possibleBlockers.isEmpty()) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
if (possibleBlockers.isEmpty()) { return false; }
for (final Card creat : possibleAttackers) {
if (CombatUtil.canAttack(creat, opp) && possibleBlockers.size() > 1) {
potentialDmg += creat.getCurrentPower();
if (potentialDmg >= oppLife) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); }
if (potentialDmg >= oppLife) { return true; }
}
if (combat != null && combat.isAttacking(creat)) {
currentAttackers.add(creat);
}
}
if (currentAttackers.size() > possibleBlockers.size()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return currentAttackers.size() > possibleBlockers.size();
} else if (logic.equals("Fog")) {
FogAi fogAi = new FogAi();
if (!fogAi.canPlay(ai, sa).willingToPlay()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
if (!fogAi.canPlayAI(ai, sa)) {
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -123,14 +124,14 @@ public class EffectAi extends SpellAbilityAi {
}
if (!canTgt) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else {
List<Card> list = game.getCombat().getAttackers();
list = CardLists.getTargetableCards(list, sa);
Card target = ComputerUtilCard.getBestCreatureAI(list);
if (target == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.getTargets().add(target);
}
@@ -138,7 +139,7 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true;
} else if (logic.equals("ChainVeil")) {
if (!phase.isPlayerTurn(ai) || !phase.getPhase().equals(PhaseType.MAIN2) || ai.getPlaneswalkersInPlay().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
randomReturn = true;
} else if (logic.equals("WillCastCreature") && ai.isAI()) {
@@ -149,17 +150,17 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true;
} else if (logic.equals("Main1")) {
if (phase.getPhase().isBefore(PhaseType.MAIN1)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
randomReturn = true;
} else if (logic.equals("Main2")) {
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
randomReturn = true;
} else if (logic.equals("Evasion")) {
if (!phase.isPlayerTurn(ai)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
boolean shouldPlay = false;
@@ -184,10 +185,10 @@ public class EffectAi extends SpellAbilityAi {
break;
}
return shouldPlay ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return shouldPlay;
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
if (game.getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
boolean threatened = false;
for (final SpellAbilityStackInstance stackInst : game.getStack()) {
@@ -203,7 +204,7 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = threatened;
} else if (logic.equals("Prevent")) { // prevent burn spell from opponent
if (game.getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final SpellAbility saTop = game.getStack().peekAbility();
final Card host = saTop.getHostCard();
@@ -214,10 +215,10 @@ public class EffectAi extends SpellAbilityAi {
final ApiType type = saTop.getApi();
if (type == ApiType.DealDamage || type == ApiType.DamageAll) { // burn spell
sa.getTargets().add(saTop);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (logic.equals("NoGain")) {
// basic logic to cancel GainLife on stack
if (!game.getStack().isEmpty()) {
@@ -227,14 +228,14 @@ public class EffectAi extends SpellAbilityAi {
while (topStack != null) {
if (topStack.getApi() == ApiType.GainLife) {
if ("You".equals(topStack.getParam("Defined")) || topStack.isTargeting(activator) || (!topStack.usesTargeting() && !topStack.hasParam("Defined"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else if (topStack.getApi() == ApiType.DealDamage && topStack.getHostCard().hasKeyword(Keyword.LIFELINK)) {
Card host = topStack.getHostCard();
for (GameEntity target : topStack.getTargets().getTargetEntities()) {
if (ComputerUtilCombat.predictDamageTo(target,
AbilityUtils.calculateAmount(host, topStack.getParam("NumDmg"), topStack), host, false) > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
@@ -248,11 +249,11 @@ public class EffectAi extends SpellAbilityAi {
final Player attackingPlayer = combat.getAttackingPlayer();
if (attackingPlayer.isOpponentOf(ai) && attackingPlayer.canGainLife()) {
if (ComputerUtilCombat.checkAttackerLifelinkDamage(combat) > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (logic.equals("NonCastCreature")) {
// TODO: add support for more cases with more convoluted API setups
if (!game.getStack().isEmpty()) {
@@ -264,13 +265,13 @@ public class EffectAi extends SpellAbilityAi {
boolean reanimator = "true".equalsIgnoreCase(topStack.getSVar("IsReanimatorCard"));
if (changeZone && (toBattlefield || reanimator)) {
if ("Creature".equals(topStack.getParam("ChangeType")) || topStack.getParamOrDefault("Defined", "").contains("Creature"))
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0,0);
return FightAi.canFightAi(ai, sa, 0, 0);
} else if (logic.equals("Pump")) {
sa.resetTargets();
List<Card> options = CardUtil.getValidCardsToTarget(sa);
@@ -280,55 +281,55 @@ public class EffectAi extends SpellAbilityAi {
}
if (!options.isEmpty() && phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(options));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (logic.equals("Burn")) {
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
SpellAbility burn = sa.getSubAbility();
return SpellApiToAi.Converter.get(burn).canPlayWithSubs(ai, burn).willingToPlay() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn);
} else if (logic.equals("YawgmothsWill")) {
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return SpecialCardAi.YawgmothsWill.consider(ai, sa);
} else if (logic.startsWith("NeedCreatures")) {
// TODO convert to AiCheckSVar
if (ai.getCreaturesInPlay().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (logic.contains(":")) {
String[] k = logic.split(":");
int i = Integer.parseInt(k[1]);
return ai.getCreaturesInPlay().size() >= i ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return ai.getCreaturesInPlay().size() >= i;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if (logic.equals("ReplaySpell")) {
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Graveyard), sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if (logic.equals("PeaceTalks")) {
Player nextPlayer = game.getNextPlayerAfter(ai);
// If opponent doesn't have creatures, preventing attacks don't mean as much
if (nextPlayer.getCreaturesInPlay().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Only cast Peace Talks after you attack just in case you have creatures
if (!phase.is(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Create a pseudo combat and see if my life is in danger
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return randomReturn;
} else if (logic.equals("Bribe")) {
Card host = sa.getHostCard();
Combat combat = game.getCombat();
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
// ideally needs once per combat or something
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (logic.equals("CantRegenerate")) {
if (sa.usesTargeting()) {
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
@@ -349,19 +350,19 @@ public class EffectAi extends SpellAbilityAi {
});
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// TODO check Stack for Effects that would destroy the selected card?
sa.getTargets().add(ComputerUtilCard.getBestAI(list));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if (sa.getParent() != null) {
// sub ability should be okay
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
} else if ("Self".equals(sa.getParam("RememberObjects"))) {
// the ones affecting itself are Nimbus cards, were opponent can activate this effect
Card host = sa.getHostCard();
if (!host.canBeDestroyed()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
@@ -369,18 +370,18 @@ public class EffectAi extends SpellAbilityAi {
List<ReplacementEffect> repDestroyList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
if (repDestroyList.isEmpty() || repDestroyList.stream().anyMatch(CardTraitPredicates.hasParam("Regeneration").negate())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (cantRegenerateCheckCombat(host) || cantRegenerateCheckStack(host)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
} else { //no AILogic
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if ("False".equals(sa.getParam("Stackable"))) {
@@ -389,7 +390,7 @@ public class EffectAi extends SpellAbilityAi {
name = sa.getHostCard().getName() + "'s Effect";
}
if (sa.getActivatingPlayer().isCardInCommand(name)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
@@ -405,20 +406,20 @@ public class EffectAi extends SpellAbilityAi {
break;
}
}
return canTgt ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return canTgt;
} else {
sa.getTargets().add(ai);
}
}
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return randomReturn;
}
@Override
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (sa.hasParam("AILogic")) {
if (canPlay(aiPlayer, sa).willingToPlay()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
if (canPlayAI(aiPlayer, sa)) {
return true; // if false, fall through further to do the mandatory stuff
}
}
@@ -430,7 +431,7 @@ public class EffectAi extends SpellAbilityAi {
if (!oppPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
if (mandatory) {
@@ -440,14 +441,14 @@ public class EffectAi extends SpellAbilityAi {
if (!aiPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}
protected boolean cantRegenerateCheckCombat(Card host) {

View File

@@ -17,7 +17,9 @@
*/
package forge.ai.ability;
import forge.ai.*;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.combat.CombatUtil;
@@ -43,17 +45,19 @@ public final class EncodeAi extends SpellAbilityAi {
* </p>
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @param af
* a {@link forge.game.ability.AbilityFactory} object.
*
* @return a boolean.
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return true;
}
/*

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -14,22 +12,18 @@ import forge.game.spellability.SpellAbility;
public class EndTurnAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory;
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { return false; }
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false;
}
}

View File

@@ -2,8 +2,6 @@ package forge.ai.ability;
import com.google.common.collect.Sets;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
@@ -24,19 +22,19 @@ public class EndureAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// Support for possible targeted Endure (e.g. target creature endures X)
if (sa.usesTargeting()) {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
if (bestCreature == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.resetTargets();
sa.getTargets().add(bestCreature);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
@@ -123,7 +121,7 @@ public class EndureAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// Support for possible targeted Endure (e.g. target creature endures X)
if (sa.usesTargeting()) {
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
@@ -131,16 +129,12 @@ public class EndureAi extends SpellAbilityAi {
if (!list.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
return canPlayAI(aiPlayer, sa) || mandatory;
}
}

View File

@@ -15,19 +15,19 @@ public class ExploreAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// Explore with a target (e.g. Enter the Unknown)
if (sa.usesTargeting()) {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
if (bestCreature == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.resetTargets();
sa.getTargets().add(bestCreature);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
public static boolean shouldPutInGraveyard(Card topCard, Player ai) {
@@ -64,23 +64,19 @@ public class ExploreAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
if (!list.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
return canPlayAI(aiPlayer, sa) || mandatory;
}
}

View File

@@ -24,13 +24,13 @@ public class FightAi extends SpellAbilityAi {
}
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
sa.resetTargets();
final Card source = sa.getHostCard();
// everything is defined or targeted above, can't do anything there unless a specific logic is set
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// Get creature lists
@@ -42,10 +42,8 @@ public class FightAi extends SpellAbilityAi {
// Filter MustTarget requirements
StaticAbilityMustTarget.filterMustTargetCards(ai, humCreatures, sa);
//prevent IndexOutOfBoundsException on MOJHOSTO variant
if (humCreatures.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
if (humCreatures.isEmpty())
return false; //prevent IndexOutOfBoundsException on MOJHOSTO variant
// assumes the triggered card belongs to the ai
if (sa.hasParam("Defined")) {
@@ -56,7 +54,7 @@ public class FightAi extends SpellAbilityAi {
}
}
if (fighter1List.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
Card fighter1 = fighter1List.get(0);
for (Card humanCreature : humCreatures) {
@@ -64,11 +62,10 @@ public class FightAi extends SpellAbilityAi {
&& !canKill(humanCreature, fighter1, 0)) {
// todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(humanCreature);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
// bail at this point, otherwise the AI will overtarget and waste the activation
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false; // bail at this point, otherwise the AI will overtarget and waste the activation
}
if (sa.hasParam("TargetsFromDifferentZone")) {
@@ -80,12 +77,12 @@ public class FightAi extends SpellAbilityAi {
// todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(humanCreature);
sa.getTargets().add(aiCreature);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
for (Card creature1 : humCreatures) {
for (Card creature2 : humCreatures) {
@@ -100,52 +97,42 @@ public class FightAi extends SpellAbilityAi {
// todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(creature1);
sa.getTargets().add(creature2);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
@Override
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
if ("Always".equals(sa.getParam("AILogic"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
return true; // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
}
return checkApiLogic(aiPlayer, sa);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("Grothama")) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (SpecialCardAi.GrothamaAllDevouring.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return mandatory ? true : SpecialCardAi.GrothamaAllDevouring.consider(ai, sa);
}
AiAbilityDecision decision = checkApiLogic(ai, sa);
if (decision.willingToPlay()) {
return decision;
if (checkApiLogic(ai, sa)) {
return true;
}
if (!mandatory) {
return decision;
return false;
}
// if mandatory, we have to play it, so we will try to make a good trade or no trade
//try to make a good trade or no trade
final Card source = sa.getHostCard();
List<Card> humCreatures = ai.getOpponents().getCreaturesInPlay();
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
if (humCreatures.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
//assumes the triggered card belongs to the ai
if (sa.hasParam("Defined")) {
@@ -154,19 +141,19 @@ public class FightAi extends SpellAbilityAi {
if (canKill(aiCreature, humanCreature, 0)
&& ComputerUtilCard.evaluateCreature(humanCreature) > ComputerUtilCard.evaluateCreature(aiCreature)) {
sa.getTargets().add(humanCreature);
return new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay);
return true;
}
}
for (Card humanCreature : humCreatures) {
if (!canKill(humanCreature, aiCreature, 0)) {
sa.getTargets().add(humanCreature);
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
return true;
}
}
sa.getTargets().add(humCreatures.get(0));
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
return true;
}
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
return true;
}
/**
@@ -177,7 +164,7 @@ public class FightAi extends SpellAbilityAi {
* @param power bonus to power
* @return true if fight effect should be played, false otherwise
*/
public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
public static boolean canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
AbilitySub tgtFight = sa.getSubAbility();
@@ -209,7 +196,7 @@ public class FightAi extends SpellAbilityAi {
ComputerUtilCard.sortByEvaluateCreature(aiCreatures);
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
if (humCreatures.isEmpty() || aiCreatures.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
return false;
}
// Evaluate creature pairs
for (Card humanCreature : humCreatures) {
@@ -239,7 +226,7 @@ public class FightAi extends SpellAbilityAi {
tgtFight.resetTargets();
tgtFight.getTargets().add(humanCreature);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
// Other cards that use AILogic PowerDmg and a single target
@@ -249,7 +236,7 @@ public class FightAi extends SpellAbilityAi {
tgtFight.resetTargets();
tgtFight.getTargets().add(humanCreature);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
} else {
@@ -262,12 +249,12 @@ public class FightAi extends SpellAbilityAi {
sa.getTargets().add(aiCreature);
tgtFight.resetTargets();
tgtFight.getTargets().add(humanCreature);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
/**

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -15,56 +13,52 @@ public class FlipACoinAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
if (sa.hasParam("AILogic")) {
String ailogic = sa.getParam("AILogic");
if (ailogic.equals("Never")) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (ailogic.equals("PhaseOut")) {
if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
} else if (ailogic.equals("Bangchuckers")) {
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.resetTargets();
for (Player o : ai.getOpponents()) {
if (sa.canTarget(o) && o.canLoseLife() && !o.cantLoseForZeroOrLessLife()) {
sa.getTargets().add(o);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
for (Card c : ai.getOpponents().getCreaturesInPlay()) {
if (sa.canTarget(c)) {
sa.getTargets().add(c);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} else if (ailogic.equals("KillOrcs")) {
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
sa.resetTargets();
for (Card c : ai.getOpponents().getCreaturesInPlay()) {
if (sa.canTarget(c)) {
sa.getTargets().add(c);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return sa.isTargetNumberValid();
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return canPlay(ai, sa);
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
@@ -16,43 +14,26 @@ import java.util.Map;
public class FlipOntoBattlefieldAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
PhaseHandler ph = sa.getHostCard().getGame().getPhaseHandler();
String logic = sa.getParamOrDefault("AILogic", "");
if (!isSorcerySpeed(sa, aiPlayer) && sa.getPayCosts().hasManaCost()) {
if (ph.is(PhaseType.END_OF_TURN)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
return ph.is(PhaseType.END_OF_TURN);
}
if ("DamageCreatures".equals(logic)) {
int maxToughness = Integer.parseInt(sa.getSubAbility().getParam("NumDmg"));
CardCollectionView rightToughness = CardLists.filter(aiPlayer.getOpponents().getCreaturesInPlay(), card -> card.getNetToughness() <= maxToughness && card.canBeDestroyed());
if (rightToughness.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} else {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return !rightToughness.isEmpty();
}
if (!aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return !aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty();
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return canPlayAI(aiPlayer, sa) || mandatory;
}
@Override

View File

@@ -22,36 +22,36 @@ public class FogAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
final Card hostCard = sa.getHostCard();
final Combat combat = game.getCombat();
// Don't cast it, if the effect is already in place
if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// TODO Test if we can even Fog successfully
if (handleMemoryCheck(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
// Only cast when Stack is empty, so Human uses spells/abilities first
if (!game.getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// TODO Only cast outside of combat if I won't be able to cast inside of combat
if (combat == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// AI should only activate this during Opponents Declare Blockers phase
if (!game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai) ||
!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// TODO Be careful of effects that don't let you cast spells during combat
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
int remainingLife = ComputerUtilCombat.lifeThatWouldRemain(ai, combat);
@@ -61,32 +61,28 @@ public class FogAi extends SpellAbilityAi {
int fogs = countAvailableFogs(ai);
if (fogs > 2 && dmg > 2) {
// Playing a fog deck. If you got them play them.
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
return true;
}
if (dmg > 2 &&
hostCard.hasKeyword(Keyword.BUYBACK) &&
CardLists.count(ai.getCardsIn(ZoneType.Battlefield), Card::isLand) > 3) {
// Constant mists sacrifices a land to buyback. But if AI is running it, they are probably ok sacrificing some lands
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
return true;
}
if ("SeriousDamage".equals(sa.getParam("AILogic"))) {
if (dmg > ai.getLife() / 4) {
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
return true;
} else if (dmg >= 5) {
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
return true;
} else if (ai.getLife() < ai.getStartingLife() / 3) {
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
return true;
}
}
// TODO Compare to poison counters?
// Cast it if life is in danger
if (ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
}
private boolean handleMemoryCheck(Player ai, SpellAbility sa) {
@@ -141,7 +137,7 @@ public class FogAi extends SpellAbilityAi {
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI should only activate this during Human's turn
boolean chance;
final Game game = ai.getGame();
@@ -153,15 +149,11 @@ public class FogAi extends SpellAbilityAi {
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
}
if (chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return chance;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
final Game game = aiPlayer.getGame();
boolean chance;
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer().getWeakestOpponent())) {
@@ -170,10 +162,6 @@ public class FogAi extends SpellAbilityAi {
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
}
if (mandatory || chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return chance || mandatory;
}
}

View File

@@ -1,17 +1,15 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class GameLossAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Player opp = ai.getStrongestOpponent();
if (opp.cantLose()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// Only one SA Lose the Game card right now, which is Door to Nothingness
@@ -19,14 +17,14 @@ public class GameLossAi extends SpellAbilityAi {
if (sa.usesTargeting() && sa.canTarget(opp)) {
sa.resetTargets();
sa.getTargets().add(opp);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
Player loser = ai;
// Phage the Untouchable
@@ -35,7 +33,7 @@ public class GameLossAi extends SpellAbilityAi {
}
if (!mandatory && (loser == ai || loser.cantLose())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (sa.usesTargeting() && sa.canTarget(loser)) {
@@ -43,6 +41,6 @@ public class GameLossAi extends SpellAbilityAi {
sa.getTargets().add(loser);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -12,25 +10,20 @@ public class GameWinAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
if (ai.cantWin()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// If the AI can win the game, it should play this ability.
// This is a special case where the AI should always play the ability if it can win.
protected boolean canPlayAI(Player ai, SpellAbility sa) {
return !ai.cantWin();
// TODO Check conditions are met on card (e.g. Coalition Victory)
// TODO Consider likelihood of SA getting countered
return new AiAbilityDecision(10000, AiPlayDecision.WillPlay);
// In general, don't return true.
// But this card wins the game, I can make an exception for that
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return true;
}
}

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -14,7 +16,7 @@ import java.util.List;
public class GoadAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
@@ -24,7 +26,7 @@ public class GoadAi extends SpellAbilityAi {
List<Card> list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty())
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
if (game.getPlayers().size() > 2) {
// use this part only in multiplayer
@@ -50,7 +52,7 @@ public class GoadAi extends SpellAbilityAi {
if (!betterList.isEmpty()) {
list = betterList;
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
} else {
// single Player, goaded creature would attack ai
@@ -71,52 +73,49 @@ public class GoadAi extends SpellAbilityAi {
if (!betterList.isEmpty()) {
list = betterList;
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}
// AI does not find a good creature to goad.
// because if it would goad a creature it would attack AI.
// AI might not have enough information to block it
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
return false;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
AiAbilityDecision decision = checkApiLogic(ai, sa);
if (decision.willingToPlay()) {
return decision;
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (checkApiLogic(ai, sa)) {
return true;
}
if (!mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
// mandatory play, so we have to play it
if (sa.usesTargeting()) {
if (sa.getTargetRestrictions().canTgtPlayer()) {
for (Player opp : ai.getOpponents()) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
return true;
}
}
if (sa.canTarget(ai)) {
sa.getTargets().add(ai);
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
return true;
}
} else {
List<Card> list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty())
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(list));
return new AiAbilityDecision(30, AiPlayDecision.MandatoryPlay);
return true;
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
}

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -17,7 +15,7 @@ import java.util.List;
public class HauntAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card card = sa.getHostCard();
final Game game = ai.getGame();
if (sa.usesTargeting() && !card.isToken()) {
@@ -26,12 +24,12 @@ public class HauntAi extends SpellAbilityAi {
// nothing to haunt
if (creats.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
final List<Card> oppCreats = CardLists.filterControlledBy(creats, ai.getOpponents());
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppCreats.isEmpty() ? creats : oppCreats));
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
}

View File

@@ -10,65 +10,60 @@ public class ImmediateTriggerAi extends SpellAbilityAi {
// TODO: this class is largely reused from DelayedTriggerAi, consider updating
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("Always")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
trigsa.setActivatingPlayer(ai);
if (trigsa instanceof AbilitySub) {
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
} else {
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
if (decision == AiPlayDecision.WillPlay) {
return new AiAbilityDecision(100, decision);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
// always add to stack, targeting happens after payment
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
trigsa.setActivatingPlayer(ai);
return aic.doTrigger(trigsa, !"You".equals(sa.getParamOrDefault("OptionalDecider", "You"))) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return aic.doTrigger(trigsa, !"You".equals(sa.getParamOrDefault("OptionalDecider", "You")));
}
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("Always")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
if (logic.equals("WeakerCreature")) {
Card ownCreature = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
if (ownCreature == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
int eval = ComputerUtilCard.evaluateCreature(ownCreature);
@@ -80,12 +75,12 @@ public class ImmediateTriggerAi extends SpellAbilityAi {
}
}
if (!foundWorse) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
}
}
trigsa.setActivatingPlayer(ai);
return ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa) == AiPlayDecision.WillPlay ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}
}

View File

@@ -1,7 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -16,10 +15,10 @@ public class InvestigateAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
boolean result = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;
}
@Override

View File

@@ -1,7 +1,9 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.ComputerUtilCard;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -15,22 +17,20 @@ import java.util.Map;
public class LearnAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// For the time being, Learn is treated as universally positive due to being optional
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
return true;
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
return canPlay(aiPlayer, sa);
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return canPlayAI(aiPlayer, sa);
}
@Override

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -23,8 +21,8 @@ public class LegendaryRuleAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#canPlayAI(forge.game.player.Player, forge.card.spellability.SpellAbility)
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // should not get here
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false; // should not get here
}
@Override

Some files were not shown because too many files have changed in this diff Show More