...
 
Commits (378)
......@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.26-SNAPSHOT</version>
<version>1.6.27-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)) {
......@@ -1249,7 +1274,7 @@ public class AiAttackController {
System.out.println(attacker.getName() + " = all out attacking");
return true;
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
if (canKillAll || (canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
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");
......@@ -1258,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");
......@@ -1267,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;
}
}
......@@ -1516,6 +1515,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,17 +1847,45 @@ public class ComputerUtilCard {
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";
if (sa != null && sa.isEvoke()) {
if (card.hasSVar("NeedsToPlayEvoked")) {
needsToPlayName = "NeedsToPlayEvoked";
}
if (card.hasSVar("NeedsToPlayEvokedVar")) {
needsToPlayVarName = "NeedsToPlayEvokedVar";
// 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
......
......@@ -635,7 +635,7 @@ public class ComputerUtilCost {
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (ComputerUtil.getOpponentFor(payer).getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
}
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
......
......@@ -97,6 +97,9 @@ public abstract class GameState {
private String precastHuman = null;
private String precastAI = null;
private String putOnStackHuman = null;
private String putOnStackAI = null;
private int turn = 1;
private boolean removeSummoningSickness = false;
......@@ -535,6 +538,13 @@ public abstract class GameState {
precastAI = categoryValue;
}
else if (categoryName.endsWith("putonstack")) {
if (isHuman)
putOnStackHuman = categoryValue;
else
putOnStackAI = categoryValue;
}
else if (categoryName.endsWith("manapool")) {
if (isHuman)
humanManaPool = categoryValue;
......@@ -614,6 +624,9 @@ public abstract class GameState {
game.getTriggerHandler().setSuppressAllTriggers(false);
// SAs added to stack cause triggers to fire, as if the relevant SAs were cast
handleAddSAsToStack(game);
// Combat only works for 1v1 matches for now (which are the only matches dev mode supports anyway)
// Note: triggers may fire during combat declarations ("whenever X attacks, ...", etc.)
if (newPhase == PhaseType.COMBAT_DECLARE_ATTACKERS || newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS) {
......@@ -803,6 +816,9 @@ public abstract class GameState {
}
private void executeScript(Game game, Card c, String sPtr) {
executeScript(game, c, sPtr, false);
}
private void executeScript(Game game, Card c, String sPtr, boolean putOnStack) {
int tgtID = TARGET_NONE;
if (sPtr.contains("->")) {
String tgtDef = sPtr.substring(sPtr.lastIndexOf("->") + 2);
......@@ -878,13 +894,17 @@ public abstract class GameState {
sa.setActivatingPlayer(c.getController());
handleScriptedTargetingForSA(game, sa, tgtID);
sa.resolve();
if (putOnStack) {
game.getStack().addAndUnfreeze(sa);
} else {
sa.resolve();
// resolve subabilities
SpellAbility subSa = sa.getSubAbility();
while (subSa != null) {
subSa.resolve();
subSa = subSa.getSubAbility();
// resolve subabilities
SpellAbility subSa = sa.getSubAbility();
while (subSa != null) {
subSa.resolve();
subSa = subSa.getSubAbility();
}
}
}
......@@ -906,7 +926,28 @@ public abstract class GameState {
}
}
private void handleAddSAsToStack(final Game game) {
Player human = game.getPlayers().get(0);
Player ai = game.getPlayers().get(1);
if (putOnStackHuman != null) {
String[] spellList = TextUtil.split(putOnStackHuman, ';');
for (String spell : spellList) {
precastSpellFromCard(spell, human, game, true);
}
}
if (putOnStackAI != null) {
String[] spellList = TextUtil.split(putOnStackAI, ';');
for (String spell : spellList) {
precastSpellFromCard(spell, ai, game, true);
}
}
}
private void precastSpellFromCard(String spellDef, final Player activator, final Game game) {
precastSpellFromCard(spellDef, activator, game, false);
}
private void precastSpellFromCard(String spellDef, final Player activator, final Game game, final boolean putOnStack) {
int tgtID = TARGET_NONE;
String scriptID = "";
......@@ -931,7 +972,7 @@ public abstract class GameState {
SpellAbility sa = null;
if (!scriptID.isEmpty()) {
executeScript(game, c, scriptID);
executeScript(game, c, scriptID, putOnStack);
return;
}
......@@ -940,7 +981,11 @@ public abstract class GameState {
handleScriptedTargetingForSA(game, sa, tgtID);
sa.resolve();
if (putOnStack) {
game.getStack().addAndUnfreeze(sa);
} else {
sa.resolve();
}
}
private void handleMarkedDamage() {
......@@ -1167,6 +1212,14 @@ public abstract class GameState {
// TODO: improve this for game states with more than two players
String tgt = info.substring(info.indexOf(':') + 1);
cardToEnchantPlayerId.put(c, tgt.equalsIgnoreCase("AI") ? TARGET_AI : TARGET_HUMAN);
} else if (info.startsWith("Owner:")) {
// TODO: improve this for game states with more than two players
Player human = player.getGame().getPlayers().get(0);
Player ai = player.getGame().getPlayers().get(1);
String owner = info.substring(info.indexOf(':') + 1);
Player controller = c.getController();
c.setOwner(owner.equalsIgnoreCase("AI") ? ai : human);
c.setController(controller, c.getGame().getNextTimestamp());
} else if (info.startsWith("Ability:")) {
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
......
......@@ -552,11 +552,48 @@ public class PlayerControllerAi extends PlayerController {
@Override
public CardCollectionView londonMulliganReturnCards(final Player mulliganingPlayer, int cardsToReturn) {
// TODO This is awful. Don't do this.
// TODO This is better than it was before, but still suboptimal (but fast).
// Maybe score a bunch of hands based on projected hand size and return the "duds"
CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand));
int numLandsDesired = (mulliganingPlayer.getStartingHandSize() - cardsToReturn) / 2;
CardCollection toReturn = new CardCollection();
for (int i = 0; i < cardsToReturn; i++) {
hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.filter(toReturn, Presets.LANDS).size();
// If we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {
CardCollection producingLands = CardLists.filter(landsInHand, Presets.LANDS_PRODUCING_MANA);
CardCollection nonProducingLands = CardLists.filter(landsInHand, Predicates.not(Presets.LANDS_PRODUCING_MANA));
Card worstLand = nonProducingLands.isEmpty() ? ComputerUtilCard.getWorstLand(producingLands)
: ComputerUtilCard.getWorstLand(nonProducingLands);
toReturn.add(worstLand);
continue;
}
// See if we'd scry something to the bottom in this situation. If we want to, probably get rid of it.
CardCollection scryBottom = new CardCollection();
for (Card c : hand) {
// Lands are evaluated separately above, factoring in the number of cards to be returned to the library
if (!c.isLand() && !toReturn.contains(c) && !willPutCardOnTop(c)) {
scryBottom.add(c);
}
}
if (!scryBottom.isEmpty()) {
CardLists.sortByCmcDesc(scryBottom);
toReturn.add(scryBottom.getFirst()); // assume the max CMC one is worse since we're not guaranteed to have lands for it
continue;
}
return CardCollection.getView(hand.subList(0, cardsToReturn));
// If we don't want to scry anything to the bottom, remove the worst card that we have in order to satisfy
// the requirement
toReturn.add(ComputerUtilCard.getWorstAI(hand));
}
return CardCollection.getView(toReturn);
}
@Override
......@@ -1068,7 +1105,7 @@ public class PlayerControllerAi extends PlayerController {
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) {
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = ComputerUtil.getOpponentFor(player).getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = player.getWeakestOpponent().getCardsIn(ZoneType.Library);
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic");
......@@ -1190,6 +1227,18 @@ public class PlayerControllerAi extends PlayerController {
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
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 = CardUtil.getLKICopy(chosen.getHostCard());
copy.addOptionalCostPaid(opt.getType());
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
continue; // don't choose kickers we don't want to play
}
}
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
......
......@@ -1126,6 +1126,44 @@ public class SpecialCardAi {
}
}
// Sorin, Vengeful Bloodlord
public static class SorinVengefulBloodlord {
public static boolean consider(final Player ai, final SpellAbility sa) {
int loyalty = sa.getHostCard().getCounters(CounterType.LOYALTY);
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.lessCMC(loyalty - 1), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
final Card copy = CardUtil.getLKICopy(card);
ComputerUtilCard.applyStaticContPT(ai.getGame(), copy, null);
return copy.getNetToughness() > 0;
}
}));
CardLists.sortByCmcDesc(creaturesToGet);
if (creaturesToGet.isEmpty()) {
return false;
}
// pick the best creature that will stay on the battlefield
Card best = creaturesToGet.getFirst();
for (Card c : creaturesToGet) {
if (best != c && ComputerUtilCard.evaluateCreature(c, true, false) >
ComputerUtilCard.evaluateCreature(best, true, false)) {
best = c;
}
}
if (best != null) {
sa.resetTargets();
sa.getTargets().add(best);
return true;
}
return false;
}
}
// Survival of the Fittest
public static class SurvivalOfTheFittest {
public static Card considerDiscardTarget(final Player ai) {
......
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
......@@ -22,7 +21,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Player opp = ComputerUtil.getOpponentFor(ai);
final Player opp = ai.getWeakestOpponent();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
......@@ -46,7 +45,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ComputerUtil.getOpponentFor(ai);
final Player opp = ai.getWeakestOpponent();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
......@@ -87,7 +86,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
}
} else {
sa.resetTargets();
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
sa.getTargets().add(ai.getWeakestOpponent());
}
return randomReturn;
......
......@@ -10,6 +10,7 @@ import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.cost.CostPutCounter;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
......@@ -77,13 +78,13 @@ public class AnimateAi extends SpellAbilityAi {
num = (num == null) ? "1" : num;
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), topStack);
ai.getWeakestOpponent(), topStack.getHostCard(), topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
ComputerUtilCard.sortByEvaluateCreature(list);
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
Card animatedCopy = becomeAnimated(source, sa);
list.add(animatedCopy);
list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(),
list = CardLists.getValidCards(list, valid.split(","), ai.getWeakestOpponent(), topStack.getHostCard(),
topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
......@@ -109,11 +110,16 @@ public class AnimateAi extends SpellAbilityAi {
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) {
return false;
}
// Don't animate if the AI won't attack anyway
// Don't animate if the AI won't attack anyway or use as a potential blocker
Player opponent = ai.getWeakestOpponent();
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
// the AI will waste resources
boolean activateAsPotentialBlocker = sa.hasParam("UntilYourNextTurn")
&& ai.getGame().getPhaseHandler().getNextTurn() != ai
&& source.isPermanent();
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent")) {
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) {
return false;
}
return true;
......@@ -245,6 +251,11 @@ public class AnimateAi extends SpellAbilityAi {
private boolean animateTgtAI(final SpellAbility sa) {
final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker")
&& sa.getPayCosts() != null
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
&& sa.getTargetRestrictions() != null
&& sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0;
final CardType types = new CardType();
if (sa.hasParam("Types")) {
......@@ -264,7 +275,7 @@ public class AnimateAi extends SpellAbilityAi {
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
// list is empty, no possible targets
if (list.isEmpty()) {
if (list.isEmpty() && !alwaysActivatePWAbility) {