...
 
Commits (750)
......@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.26-SNAPSHOT</version>
<version>1.6.28-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>
......
......@@ -217,9 +217,22 @@ public class AiAttackController {
}
final Player opp = this.defendingOpponent;
if (ComputerUtilCombat.damageIfUnblocked(attacker, opp, combat, true) > 0) {
return true;
// Damage opponent if unblocked
final int dmgIfUnblocked = ComputerUtilCombat.damageIfUnblocked(attacker, opp, combat, true);
if (dmgIfUnblocked > 0) {
boolean onlyIfExalted = false;
if (combat.getAttackers().isEmpty() && ai.countExaltedBonus() > 0
&& dmgIfUnblocked - ai.countExaltedBonus() == 0) {
// Make sure we're not counting on the Exalted bonus when the AI is planning to attack with more than one creature
onlyIfExalted = true;
}
if (!onlyIfExalted || this.attackers.size() == 1 || this.aiAggression == 6 /* 6 is Exalted attack */) {
return true;
}
}
// Poison opponent if unblocked
if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) {
return true;
}
......@@ -234,7 +247,7 @@ public class AiAttackController {
final CardCollectionView controlledByCompy = ai.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES);
for (final Card c : controlledByCompy) {
for (final Trigger trigger : c.getTriggers()) {
if (ComputerUtilCombat.combatTriggerWillTrigger(attacker, null, trigger, combat)) {
if (ComputerUtilCombat.combatTriggerWillTrigger(attacker, null, trigger, combat, this.attackers)) {
return true;
}
}
......@@ -1140,8 +1153,21 @@ public class AiAttackController {
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, new Predicate<Card>() {
@Override
public boolean apply(Card defender) {
return CombatUtil.canBlock(attacker, defender);
}
});
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
boolean dangerousBlockersPresent = !CardLists.filter(validBlockers, Predicates.or(
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
CardPredicates.hasKeyword(Keyword.LIFELINK))).isEmpty();
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
int defPower = CardLists.getTotalPower(defenders, true);
int defPower = CardLists.getTotalPower(validBlockers, true);
if (!hasCombatEffect) {
for (KeywordInterface inst : attacker.getKeywords()) {
......@@ -1158,10 +1184,9 @@ public class AiAttackController {
// number of factors about the attacking
// context that will be relevant to the attackers decision according to
// the selected strategy
for (final Card defender : defenders) {
for (final Card defender : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if ((isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2)
&& CombatUtil.canBlock(attacker, defender)) {
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) {
......@@ -1199,7 +1224,7 @@ public class AiAttackController {
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && (uselessAttack || noContributionToAttack))) {
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
canKillAllDangerous = false;
}
}
......@@ -1248,8 +1273,9 @@ public class AiAttackController {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = all out attacking");
return true;
case 4: // expecting to at least trade with something
if (canKillAll || (canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked) {
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
|| (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
return true;
......@@ -1257,7 +1283,7 @@ public class AiAttackController {
break;
case 3: // expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers)
|| ((canKillAllDangerous || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|| (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|| !canBeBlocked) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
......@@ -1266,7 +1292,7 @@ public class AiAttackController {
break;
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
(canKillAllDangerous || !canBeKilled))) {
((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
return true;
......
......@@ -1778,7 +1778,7 @@ public class AiController {
+ MyRandom.getRandom().nextInt(3);
return Math.max(remaining, min) / 2;
} else if ("LowestLoseLife".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, ComputerUtil.getOpponentFor(player).getLife())) + 1;
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
} else if ("HighestGetCounter".equals(logic)) {
return MyRandom.getRandom().nextInt(3);
} else if (source.hasSVar("EnergyToPay")) {
......
......@@ -47,6 +47,7 @@ import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
......@@ -76,17 +77,15 @@ public class ComputerUtil {
source.setSplitStateToPlayAbility(sa);
if (sa.isSpell() && !source.isCopiedSpell()) {
if (source.getType().hasStringType("Arcane")) {
sa = AbilityUtils.addSpliceEffects(sa);
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
// otherwise the AI will fail to add the card to stack and that'll knock it out of the game
sa.resetTargets();
if (((PlayerControllerAi) ai.getController()).getAi().canPlaySa(sa) != AiPlayDecision.WillPlay) {
// for whatever reason the AI doesn't want to play the thing with the spliced subs anymore,
// proceeding past this point may result in an illegal play
return false;
}
sa = AbilityUtils.addSpliceEffects(sa);
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
// otherwise the AI will fail to add the card to stack and that'll knock it out of the game
sa.resetTargets();
if (((PlayerControllerAi) ai.getController()).getAi().canPlaySa(sa) != AiPlayDecision.WillPlay) {
// for whatever reason the AI doesn't want to play the thing with the spliced subs anymore,
// proceeding past this point may result in an illegal play
return false;
}
}
......@@ -100,6 +99,8 @@ public class ComputerUtil {
sa.resetPaidHash();
}
sa = GameActionUtil.addExtraKeywordCost(sa);
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa);
}
......@@ -209,9 +210,9 @@ public class ComputerUtil {
}
// this is used for AI's counterspells
public static final boolean playStack(final SpellAbility sa, final Player ai, final Game game) {
public static final boolean playStack(SpellAbility sa, final Player ai, final Game game) {
sa.setActivatingPlayer(ai);
if (!ComputerUtilCost.canPayCost(sa, ai))
if (!ComputerUtilCost.canPayCost(sa, ai))
return false;
final Card source = sa.getHostCard();
......@@ -221,6 +222,9 @@ public class ComputerUtil {
sa.setLastStateGraveyard(game.getLastStateGraveyard());
sa.setHostCard(game.getAction().moveToStack(source, sa));
}
sa = GameActionUtil.addExtraKeywordCost(sa);
final Cost cost = sa.getPayCosts();
if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa);
......@@ -250,13 +254,15 @@ public class ComputerUtil {
}
public static final boolean playSpellAbilityWithoutPayingManaCost(final Player ai, final SpellAbility sa, final Game game) {
final SpellAbility newSA = sa.copyWithNoManaCost();
SpellAbility newSA = sa.copyWithNoManaCost();
newSA.setActivatingPlayer(ai);
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA)) {
return false;
}
newSA = GameActionUtil.addExtraKeywordCost(newSA);
final Card source = newSA.getHostCard();
if (newSA.isSpell() && !source.isCopiedSpell()) {
source.setCastSA(newSA);
......@@ -276,7 +282,7 @@ public class ComputerUtil {
return true;
}
public static final void playNoStack(final Player ai, final SpellAbility sa, final Game game) {
public static final void playNoStack(final Player ai, SpellAbility sa, final Game game) {
sa.setActivatingPlayer(ai);
// TODO: We should really restrict what doesn't use the Stack
if (ComputerUtilCost.canPayCost(sa, ai)) {
......@@ -288,6 +294,8 @@ public class ComputerUtil {
sa.setHostCard(game.getAction().moveToStack(source, sa));
}
sa = GameActionUtil.addExtraKeywordCost(sa);
final Cost cost = sa.getPayCosts();
if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa);
......@@ -798,7 +806,7 @@ public class ComputerUtil {
final int max = Math.min(remaining.size(), amount);
if (exceptSelf) {
if (exceptSelf && max < remaining.size()) {
removedSelf = remaining.remove(source.getHostCard());
}
......@@ -1516,6 +1524,9 @@ public class ComputerUtil {
// iterate from top of stack to find SpellAbility, including sub-abilities,
// that does not match "sa"
SpellAbility spell = si.getSpellAbility(true), sub = spell.getSubAbility();
if (spell.isWrapper()) {
spell = ((WrappedAbility) spell).getWrappedAbility();
}
while (sub != null && sub != sa) {
sub = sub.getSubAbility();
}
......
......@@ -521,7 +521,7 @@ public class ComputerUtilCard {
*/
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
AiBlockController aiBlk = new AiBlockController(ai);
final Player opp = ComputerUtil.getOpponentFor(ai);
final Player opp = ai.getWeakestOpponent();
Combat combat = new Combat(opp);
//Use actual attackers if available, else consider all possible attackers
Combat currentCombat = ai.getGame().getCombat();
......@@ -884,7 +884,7 @@ public class ComputerUtilCard {
List<String> chosen = new ArrayList<String>();
Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
Player opp = ComputerUtil.getOpponentFor(ai);
Player opp = ai.getWeakestOpponent();
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
......@@ -974,7 +974,7 @@ public class ComputerUtilCard {
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
final Player ai = sa.getActivatingPlayer();
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
final Player opp = ComputerUtil.getOpponentFor(ai);
final Player opp = ai.getWeakestOpponent();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final PhaseType phaseType = ph.getPhase();
......@@ -1213,6 +1213,7 @@ public class ComputerUtilCard {
final Game game = ai.getGame();
final PhaseHandler phase = game.getPhaseHandler();
final Combat combat = phase.getCombat();
final boolean main1Preferred = "Main1IfAble".equals(sa.getParam("AILogic")) && phase.is(PhaseType.MAIN1, ai);
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
final boolean loseCardAtEOT = "Sacrifice".equals(sa.getParam("AtEOT")) || "Exile".equals(sa.getParam("AtEOT"))
|| "Destroy".equals(sa.getParam("AtEOT")) || "ExileCombat".equals(sa.getParam("AtEOT"));
......@@ -1250,7 +1251,7 @@ public class ComputerUtilCard {
// will the creature attack (only relevant for sorcery speed)?
if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& phase.isPlayerTurn(ai)
&& SpellAbilityAi.isSorcerySpeed(sa)
&& SpellAbilityAi.isSorcerySpeed(sa) || main1Preferred
&& power > 0
&& ComputerUtilCard.doesCreatureAttackAI(ai, c)) {
return true;
......@@ -1269,7 +1270,7 @@ public class ComputerUtilCard {
}
}
final Player opp = ComputerUtil.getOpponentFor(ai);
final Player opp = ai.getWeakestOpponent();
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
List<Card> oppCreatures = opp.getCreaturesInPlay();
float chance = 0;
......@@ -1456,6 +1457,10 @@ public class ComputerUtilCard {
}
if (totalPowerUnblocked >= opp.getLife()) {
return true;
} else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasNoManaCost()) {
return true; // always activate abilities which cost no mana and which can increase unblocked damage
}
}
}
float value = 1.0f * (pumpedDmg - dmg);
......@@ -1842,8 +1847,45 @@ public class ComputerUtilCard {
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";
// TODO: if there are ever split cards with Evoke or Kicker, factor in the right split option above
if (sa != null) {
if (sa.isEvoke()) {
// if the spell is evoked, will use NeedsToPlayEvoked if available (otherwise falls back to NeedsToPlay)
if (card.hasSVar("NeedsToPlayEvoked")) {
needsToPlayName = "NeedsToPlayEvoked";
}
if (card.hasSVar("NeedsToPlayEvokedVar")) {
needsToPlayVarName = "NeedsToPlayEvokedVar";
}
} else if (sa.isKicked()) {
// if the spell is kicked, uses NeedsToPlayKicked if able and locks out the regular NeedsToPlay check
// for unkicked spells, uses NeedsToPlay
if (card.hasSVar("NeedsToPlayKicked")) {
needsToPlayName = "NeedsToPlayKicked";
} else {
needsToPlayName = "UNUSED";
}
if (card.hasSVar("NeedsToPlayKickedVar")) {
needsToPlayVarName = "NeedsToPlayKickedVar";
} else {
needsToPlayVarName = "UNUSED";
}
}
}
if (card.hasSVar(needsToPlayName)) {
final String needsToPlay = card.getSVar(needsToPlayName);
// A special case which checks that this creature will attack if it's the AI's turn
if (needsToPlay.equalsIgnoreCase("WillAttack")) {
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return ComputerUtilCard.doesSpecifiedCreatureAttackAI(sa.getActivatingPlayer(), card) ?
AiPlayDecision.WillPlay : AiPlayDecision.BadEtbEffects;
} else {
return AiPlayDecision.WillPlay; // not our turn, skip this check for the possible Flash use etc.
}
}
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
......
......@@ -769,6 +769,10 @@ public class ComputerUtilCombat {
*/
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
Combat combat) {
return combatTriggerWillTrigger(attacker, defender, trigger, combat, null);
}
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
Combat combat, final List<Card> plannedAttackers) {
final Game game = attacker.getGame();
final Map<String, String> trigParams = trigger.getMapParams();
boolean willTrigger = false;
......@@ -815,6 +819,9 @@ public class ComputerUtilCombat {
}
}
}
if (trigParams.containsKey("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) {
return false; // won't trigger since the AI is planning to attack with more than one creature
}
}
// defender == null means unblocked
......
......@@ -110,6 +110,15 @@ public class ComputerUtilCost {
&& !source.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (part instanceof CostRemoveAnyCounter) {
if (sa != null) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa).visit(remCounter);
return decision != null;
}
return false;
}
}
return true;
......@@ -585,6 +594,11 @@ public class ComputerUtilCost {
if (c == null || c.isUntapped()) {
return false;
}
} else if ("RiskFactor".equals(aiLogic)) {
final Player activator = sa.getActivatingPlayer();
if (!activator.canDraw() || activator.hasKeyword("You can't draw more than one card each turn.")) {
return false;
}
} else if ("MorePowerful".equals(aiLogic)) {
final int sourceCreatures = sa.getActivatingPlayer().getCreaturesInPlay().size();
final int payerCreatures = payer.getCreaturesInPlay().size();
......@@ -626,7 +640,7 @@ public class ComputerUtilCost {
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)