Merge branch 'Card-Forge:master' into master

This commit is contained in:
TabletopGeneral
2023-01-19 00:10:11 -05:00
committed by GitHub
108 changed files with 2503 additions and 1496 deletions

View File

@@ -179,7 +179,7 @@ public class AiAttackController {
List<Player> opps = Lists.newArrayList(ai.getOpponents());
if (forCombatDmg) {
for (Player p : ai.getOpponents().threadSafeIterable()) {
for (Player p : ai.getOpponents()) {
if (p.isMonarch() && ai.canBecomeMonarch()) {
// just increase the odds for now instead of being fully predictable
// as it could lead to other too complex factors giving this reasoning negative impact

View File

@@ -782,15 +782,9 @@ public class AiController {
return AiPlayDecision.CantAfford;
}
}
if (wardCost.hasSpecificCostType(CostPayLife.class)) {
int lifeToPay = wardCost.getCostPartByType(CostPayLife.class).convertAmount();
if (lifeToPay > player.getLife() || (lifeToPay == player.getLife() && !player.cantLoseForZeroOrLessLife())) {
return AiPlayDecision.CantAfford;
}
}
if (wardCost.hasSpecificCostType(CostDiscard.class)
&& wardCost.getCostPartByType(CostDiscard.class).convertAmount() > player.getCardsIn(ZoneType.Hand).size()) {
return AiPlayDecision.CantAfford;
SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(player, sa , wardCost, host)) {
return AiPlayDecision.CostNotAcceptable;
}
}
}
@@ -1473,11 +1467,9 @@ public class AiController {
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
}
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
CardCollection playBeforeLand = CardLists.filter(
player.getCardsIn(ZoneType.Hand), CardPredicates.hasSVar("PlayBeforeLandDrop")
);
if (!playBeforeLand.isEmpty()) {
SpellAbility wantToPlayBeforeLand = chooseSpellAbilityToPlayFromList(
ComputerUtilAbility.getSpellAbilities(playBeforeLand, player), false
@@ -1486,7 +1478,8 @@ public class AiController {
return singleSpellAbilityList(wantToPlayBeforeLand);
}
}
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
if (landsWannaPlay != null) {
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);

View File

@@ -448,7 +448,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, isEffect(), c, null);
return PaymentDecision.card(list);
return list == null ? null : PaymentDecision.card(list);
}
@Override

View File

@@ -33,6 +33,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.ChooseGenericEffectAi;
import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi;
@@ -449,14 +450,27 @@ public class ComputerUtil {
}
// try everything when about to die
if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
final CardCollection nonCreatures = CardLists.getNotType(typeList, "Creature");
if (!nonCreatures.isEmpty()) {
return ComputerUtilCard.getWorstAI(nonCreatures);
} else if (!typeList.isEmpty()) {
// TODO make sure survival is possible in case the creature blocks a trampler
return ComputerUtilCard.getWorstAI(typeList);
if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// in some rare situations the call to lifeInDanger could lead us back here, this will prevent an overflow
boolean preventReturn = sa != null && sa.isManaAbility();
if (preventReturn) {
AiCardMemory.rememberCard(ai, sa.getHostCard(), MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL);
}
boolean danger = ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat());
if (preventReturn) {
AiCardMemory.forgetCard(ai, sa.getHostCard(), MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL);
}
if (danger) {
final CardCollection nonCreatures = CardLists.getNotType(typeList, "Creature");
if (!nonCreatures.isEmpty()) {
return ComputerUtilCard.getWorstAI(nonCreatures);
} else if (!typeList.isEmpty()) {
// TODO make sure survival is possible in case the creature blocks a trampler
return ComputerUtilCard.getWorstAI(typeList);
}
}
}
}
@@ -609,7 +623,7 @@ public class ComputerUtil {
int count = 0;
while (count < amount) {
Card prefCard = getCardPreference(ai, source, "SacCost", typeList);
Card prefCard = getCardPreference(ai, source, "SacCost", typeList, ability);
if (prefCard == null) {
prefCard = ComputerUtilCard.getWorstAI(typeList);
}

View File

@@ -45,6 +45,7 @@ import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostUntap;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection;
import forge.game.keyword.KeywordInterface;
@@ -1417,12 +1418,14 @@ public class ComputerUtilCard {
double nonCombatChance = 0.0f;
double combatChance = 0.0f;
// non-combat Haste: has an activated ability with tap cost
for (SpellAbility ab : c.getSpellAbilities()) {
Cost abCost = ab.getPayCosts();
if (abCost != null && abCost.hasTapCost()
&& (!abCost.hasManaCost() || ComputerUtilMana.canPayManaCost(ab, ai, 0, false))) {
nonCombatChance += 0.5f;
break;
if (c.isAbilitySick()) {
for (SpellAbility ab : c.getSpellAbilities()) {
Cost abCost = ab.getPayCosts();
if (abCost != null && (abCost.hasTapCost() || abCost.hasSpecificCostType(CostUntap.class))
&& (!abCost.hasManaCost() || ComputerUtilMana.canPayManaCost(ab, ai, sa.getPayCosts().getTotalMana().getCMC(), false))) {
nonCombatChance += 0.5f;
break;
}
}
}
// combat Haste: only grant it if the creature will attack
@@ -1730,6 +1733,7 @@ public class ComputerUtilCard {
}
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
pumped.addChangedCardKeywordsInternal(toCopy, null, false, timestamp2, 0, false);
pumped.updateKeywordsCache(pumped.getCurrentState());
applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
return pumped;
}
@@ -1916,7 +1920,7 @@ public class ComputerUtilCard {
}
}
if (!canBeBlocked) {
boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
boolean threat = ComputerUtilCombat.getAttack(atk) >= ai.getLife() - lifeInDanger;
if (!priorityRemovalOnlyInDanger || threat) {
priorityCards.add(atk);
}

View File

@@ -328,10 +328,10 @@ public class ComputerUtilCombat {
if (blockers.size() == 0
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) {
unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& getAttack(attacker) > totalShieldDamage(attacker, blockers)) {
if (!attacker.hasKeyword(Keyword.INFECT)) {
damage += getAttack(attacker) - totalShieldDamage(attacker, blockers);
} else if (attacker.hasKeyword(Keyword.TRAMPLE) && !attacker.hasKeyword(Keyword.INFECT)) {
int dmgAfterShielding = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (dmgAfterShielding > 0) {
damage += dmgAfterShielding;
}
}
}
@@ -369,13 +369,14 @@ public class ComputerUtilCombat {
if (blockers.size() == 0
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) {
unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& getAttack(attacker) > totalShieldDamage(attacker, blockers)) {
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (attacker.hasKeyword(Keyword.INFECT)) {
poison += trampleDamage;
if (trampleDamage > 0) {
if (attacker.hasKeyword(Keyword.INFECT)) {
poison += trampleDamage;
}
poison += predictPoisonFromTriggers(attacker, ai, trampleDamage);
}
poison += predictPoisonFromTriggers(attacker, ai, trampleDamage);
}
}
@@ -686,7 +687,7 @@ public class ComputerUtilCombat {
final int defenderDefense = blocker.getLethalDamage() - flankingMagnitude + defBushidoMagnitude;
return defenderDefense;
} // shieldDamage
}
// For AI safety measures like Regeneration
/**
@@ -2053,7 +2054,6 @@ public class ComputerUtilCombat {
if (block.size() == 1) {
final Card blocker = block.getFirst();
// trample
if (hasTrample) {
int dmgToKill = getEnoughDamageToKill(blocker, dmgCanDeal, attacker, true);
@@ -2109,7 +2109,7 @@ public class ComputerUtilCombat {
}
}
return damageMap;
} // setAssignedDamage()
}
// how much damage is enough to kill the creature (for AI)
/**

View File

@@ -234,7 +234,7 @@ public class ComputerUtilCost {
return true;
}
public static boolean checkForManaSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility, final boolean effect) {
public static boolean checkForManaSacrificeCost(final Player ai, final Cost cost, final SpellAbility sourceAbility, final boolean effect) {
// TODO cheating via autopay can still happen, need to get the real ai player from controlledBy
if (cost == null || !ai.isAI()) {
return true;
@@ -247,18 +247,17 @@ public class ComputerUtilCost {
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_SAC_COST));
}
if (part.payCostFromSource()) {
list.add(source);
list.add(sourceAbility.getHostCard());
} else if (part.getType().equals("OriginalHost")) {
list.add(sourceAbility.getOriginalHost());
} else if (part.getAmount().equals("All")) {
// Does the AI want to use Sacrifice All?
return false;
} else {
final String amount = part.getAmount();
Integer c = part.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, amount, sourceAbility);
c = part.getAbilityAmount(sourceAbility);
}
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
@@ -636,7 +635,7 @@ public class ComputerUtilCost {
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
} // canPayCost()
}
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();

View File

@@ -316,10 +316,6 @@ public class ComputerUtilMana {
continue;
}
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma.getHostCard(), ma, ma.isTrigger())) {
continue;
}
if (sa.getApi() == ApiType.Animate) {
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined"))
@@ -381,9 +377,15 @@ public class ComputerUtilMana {
continue;
}
if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts, cost.getXManaCostPaidByColor())) {
return paymentChoice;
if (!canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts, cost.getXManaCostPaidByColor())) {
continue;
}
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma, ma.isTrigger())) {
continue;
}
return paymentChoice;
}
return null;
}

View File

@@ -620,6 +620,9 @@ public abstract class GameState {
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
// prevent interactions with objects from old state
game.copyLastState();
// Set negative or zero life after state effects if need be, important for some puzzles that rely on
// pre-setting negative life (e.g. PS_NEO4).
for (int i = 0; i < playerStates.size(); i++) {

View File

@@ -43,8 +43,6 @@ import forge.game.card.CardView;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment;
import forge.game.cost.CostExile;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.keyword.KeywordInterface;
@@ -698,24 +696,6 @@ public class PlayerControllerAi extends PlayerController {
ability.setActivatingPlayer(c.getController(), true);
ability.setCardState(sa.getCardState());
// FIXME: This is a hack to check if the AI can play the "exile from library" pay costs (Cumulative Upkeep,
// e.g. Thought Lash). We have to do it and bail early if the AI can't pay, because otherwise the AI will
// pay the cost partially, which should not be possible
int nExileLib = 0;
List<CostPart> parts = CostAdjustment.adjust(cost, sa).getCostParts();
for (final CostPart part : parts) {
if (part instanceof CostExile) {
CostExile exile = (CostExile) part;
if (exile.from == ZoneType.Library) {
nExileLib += exile.convertAmount();
}
}
}
if (nExileLib > c.getController().getCardsIn(ZoneType.Library).size()) {
return false;
}
// - End of hack for Exile a card from library Cumulative Upkeep -
if (ComputerUtilCost.canPayCost(ability, c.getController(), true)) {
ComputerUtil.playNoStack(c.getController(), ability, getGame(), true);
// transfer this info for Balduvian Fallen

View File

@@ -1580,7 +1580,7 @@ public class AttachAi extends SpellAbilityAi {
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.equals("Haste")) {
return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped()
return card.hasSickness() && ph.isPlayerTurn(ai) && !card.isTapped()
&& card.getNetCombatDamage() + powerBonus > 0
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& ComputerUtilCombat.canAttackNextTurn(card);

View File

@@ -113,9 +113,9 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return CombatUtil.canAttack(card, ai) || CombatUtil.canBlock(card, true);
}
if (!ph.isPlayerTurn(ai)) {
return CombatUtil.canAttack(card, ai)
&& (card.getNetCombatDamage() > 0)
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
return card.getNetCombatDamage() > 0
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& CombatUtil.canAttack(card, ai);
} else {
if (ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| ph.getPhase().isBefore(PhaseType.MAIN1)) {
@@ -129,7 +129,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
&& (combat == null || !combat.isAttacking(c))) {
return false;
}
return CombatUtil.canAttack(c, card.getController()) || (combat != null && combat.isAttacking(c));
return (combat != null && combat.isAttacking(c)) || CombatUtil.canAttack(c, card.getController());
}
});
return CombatUtil.canBlockAtLeastOne(card, attackers);
@@ -148,8 +148,8 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
// the cards controller needs to be the one attacked
return CombatUtil.canAttack(c, card.getController()) || (combat != null && combat.isAttacking(c)
&& card.getController().equals(combat.getDefenderPlayerByAttacker(c)));
return (combat != null && combat.isAttacking(c) && card.getController().equals(combat.getDefenderPlayerByAttacker(c))) ||
CombatUtil.canAttack(c, card.getController());
}
});
return CombatUtil.canBlockAtLeastOne(card, attackers);
@@ -199,7 +199,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
final boolean evasive = keyword.endsWith("Shadow");
// give evasive keywords to creatures that can or do attack
if (evasive) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));
@@ -231,7 +231,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
}
}
}
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& Iterables.any(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
@@ -244,25 +244,25 @@ public abstract class PumpAiBase extends SpellAbilityAi {
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
return true;
}
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& !CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
Keyword.HORSEMANSHIP).isEmpty();
} else if (keyword.endsWith("Intimidate")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& !CardLists.getNotType(CardLists.filter(
opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)), "Artifact").isEmpty();
} else if (keyword.endsWith("Fear")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& !CardLists.getNotColor(CardLists.getNotType(CardLists.filter(
opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)), "Artifact"), MagicColor.BLACK).isEmpty();
} else if (keyword.endsWith("Haste")) {
return card.hasSickness() && !ph.isPlayerTurn(opp) && !card.isTapped()
return CombatUtil.isAttackerSick(card, opp) && !ph.isPlayerTurn(opp) && !card.isTapped()
&& newPower > 0
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& ComputerUtilCombat.canAttackNextTurn(card);
@@ -293,7 +293,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
}
return false;
} else if (keyword.startsWith("Bushido")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !opp.getCreaturesInPlay().isEmpty()
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));
@@ -320,22 +320,22 @@ public abstract class PumpAiBase extends SpellAbilityAi {
}
return false;
} else if (keyword.equals("Double Strike")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& newPower > 0
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS);
} else if (keyword.startsWith("Rampage")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& newPower > 0
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)).size() >= 2;
} else if (keyword.startsWith("Flanking")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& newPower > 0
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
Keyword.FLANKING).isEmpty();
} else if (keyword.startsWith("Trample")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& CombatUtil.canBeBlocked(card, null, opp)
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 1
@@ -347,8 +347,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
if (combat != null && combat.isBlocking(card) && !card.hasKeyword(Keyword.WITHER)) {
return true;
}
return (!ph.isPlayerTurn(opp))
&& (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS);
} else if (keyword.endsWith("Wither")) {
if (newPower <= 0 || card.hasKeyword(Keyword.INFECT)) {
@@ -376,25 +375,25 @@ public abstract class PumpAiBase extends SpellAbilityAi {
} else if (keyword.equals("Persist")) {
return card.getBaseToughness() > 1 && !card.hasKeyword(Keyword.UNDYING);
} else if (keyword.equals("Islandwalk")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& !CardLists.getType(opp.getLandsInPlay(), "Island").isEmpty()
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));
} else if (keyword.equals("Swampwalk")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& !CardLists.getType(opp.getLandsInPlay(), "Swamp").isEmpty()
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));
} else if (keyword.equals("Mountainwalk")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& !CardLists.getType(opp.getLandsInPlay(), "Mountain").isEmpty()
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));
} else if (keyword.equals("Forestwalk")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
return !ph.isPlayerTurn(opp) && ((combat != null && combat.isAttacking(card)) || CombatUtil.canAttack(card, opp))
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 0
&& !CardLists.getType(opp.getLandsInPlay(), "Forest").isEmpty()

View File

@@ -206,12 +206,7 @@ public class GameCopier {
Card newCard = map.map(origHostCard);
SpellAbility newSa = null;
if (origSa.isSpell()) {
for (SpellAbility sa : newCard.getAllSpellAbilities()) {
if (sa.getDescription().equals(origSa.getDescription())) {
newSa = sa;
break;
}
}
newSa = findSAInCard(origSa, newCard);
}
if (newSa != null) {
newSa.setActivatingPlayer(map.map(origSa.getActivatingPlayer()), true);

View File

@@ -11,6 +11,10 @@ public class MultiTargetSelector {
public static class Targets {
private ArrayList<PossibleTargetSelector.Targets> targets;
public int size() {
return targets.size();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -24,8 +28,8 @@ public class MultiTargetSelector {
}
}
private List<PossibleTargetSelector> selectors;
private List<SpellAbility> targetingSAs;
private final List<PossibleTargetSelector> selectors;
private final List<SpellAbility> targetingSAs;
private int currentIndex;
public MultiTargetSelector(SpellAbility sa, List<AbilitySub> plannedSubs) {
@@ -52,8 +56,8 @@ public class MultiTargetSelector {
public Targets getLastSelectedTargets() {
Targets targets = new Targets();
targets.targets = new ArrayList<>(selectors.size());
for (int i = 0; i < selectors.size(); i++) {
targets.targets.add(selectors.get(i).getLastSelectedTargets());
for (PossibleTargetSelector selector : selectors) {
targets.targets.add(selector.getLastSelectedTargets());
}
return targets;
}
@@ -78,34 +82,62 @@ public class MultiTargetSelector {
currentIndex = -1;
}
public void selectTargetsByIndex(int i) {
public boolean selectTargetsByIndex(int i) {
// The caller is telling us to select the i-th possible set of targets.
if (i < currentIndex) {
reset();
}
while (currentIndex < i) {
selectNextTargets();
if (!selectNextTargets()) {
return false;
}
}
return true;
}
public boolean selectNextTargets() {
if (currentIndex == -1) {
for (PossibleTargetSelector selector : selectors) {
if (!selector.selectNextTargets()) {
private boolean selectTargetsStartingFrom(int selectorIndex) {
// Don't reset the current selector, as it still has the correct list of targets set and has
// to remember its current/next target index. Subsequent selectors need a reset since their
// possible targets may change based on what was chosen for earlier ones.
if (selectors.get(selectorIndex).selectNextTargets()) {
for (int i = selectorIndex + 1; i < selectors.size(); i++) {
selectors.get(i).reset();
if (!selectors.get(i).selectNextTargets()) {
return false;
}
}
currentIndex = 0;
return true;
}
for (int i = selectors.size() - 1; i >= 0; i--) {
if (selectors.get(i).selectNextTargets()) {
currentIndex++;
return false;
}
public boolean selectNextTargets() {
if (selectors.size() == 0) {
return false;
}
if (currentIndex == -1) {
// Select the first set of targets (calls selectNextTargets() on each selector).
if (selectTargetsStartingFrom(0)) {
currentIndex = 0;
return true;
}
selectors.get(i).reset();
selectors.get(i).selectNextTargets();
// No possible targets.
return false;
}
return false;
// Subsequent call, first try selecting a new target for the last selector. If that doesn't
// work, backtrack (decrement selector index) and try selecting targets from there.
// This approach ensures that leaf selectors (end of list) are advanced first, before
// previous ones, so that we get an AA,AB,BA,BB ordering.
int selectorIndex = selectors.size() - 1;
while (!selectTargetsStartingFrom(selectorIndex)) {
if (selectorIndex == 0) {
// No more possible targets.
return false;
}
selectorIndex--;
}
currentIndex++;
return true;
}
private static boolean conditionsAreMet(SpellAbility saOrSubSa) {

View File

@@ -17,12 +17,11 @@ import forge.game.spellability.TargetRestrictions;
public class PossibleTargetSelector {
private final SpellAbility sa;
private SpellAbility targetingSa;
private int targetingSaIndex;
private final SpellAbility targetingSa;
private final int targetingSaIndex;
private int maxTargets;
private TargetRestrictions tgt;
private int targetIndex;
private List<GameObject> validTargets;
private int nextTargetIndex;
private final List<GameObject> validTargets = new ArrayList<>();
public static class Targets {
final int targetingSaIndex;
@@ -36,7 +35,7 @@ public class PossibleTargetSelector {
this.targetIndex = targetIndex;
this.description = description;
if (targetIndex < 0 || targetIndex >= originalTargetCount) {
if (targetIndex != -1 && (targetIndex < 0 || targetIndex >= originalTargetCount)) {
throw new IllegalArgumentException("Invalid targetIndex=" + targetIndex);
}
}
@@ -51,12 +50,11 @@ public class PossibleTargetSelector {
this.sa = sa;
this.targetingSa = targetingSa;
this.targetingSaIndex = targetingSaIndex;
this.validTargets = new ArrayList<>();
generateValidTargets(sa.getHostCard().getController());
reset();
}
public void reset() {
targetIndex = 0;
nextTargetIndex = 0;
validTargets.clear();
generateValidTargets(sa.getHostCard().getController());
}
@@ -67,7 +65,7 @@ public class PossibleTargetSelector {
}
sa.setActivatingPlayer(player, true);
targetingSa.resetTargets();
tgt = targetingSa.getTargetRestrictions();
TargetRestrictions tgt = targetingSa.getTargetRestrictions();
maxTargets = tgt.getMaxTargets(sa.getHostCard(), targetingSa);
SimilarTargetSkipper skipper = new SimilarTargetSkipper();
@@ -80,8 +78,8 @@ public class PossibleTargetSelector {
}
private static class SimilarTargetSkipper {
private ArrayListMultimap<String, Card> validTargetsMap = ArrayListMultimap.create();
private HashMap<Card, String> cardTypeStrings = new HashMap<>();
private final ArrayListMultimap<String, Card> validTargetsMap = ArrayListMultimap.create();
private final HashMap<Card, String> cardTypeStrings = new HashMap<>();
private HashMap<Card, Integer> creatureScores;
private int getCreatureScore(Card c) {
@@ -190,16 +188,7 @@ public class PossibleTargetSelector {
}
public Targets getLastSelectedTargets() {
return new Targets(targetingSaIndex, validTargets.size(), targetIndex - 1, targetingSa.getTargets().toString());
}
public boolean selectTargetsByIndex(int targetIndex) {
if (targetIndex >= validTargets.size()) {
return false;
}
selectTargetsByIndexImpl(targetIndex);
this.targetIndex = targetIndex + 1;
return true;
return new Targets(targetingSaIndex, validTargets.size(), nextTargetIndex - 1, targetingSa.getTargets().toString());
}
public boolean selectTargets(Targets targets) {
@@ -208,16 +197,16 @@ public class PossibleTargetSelector {
return false;
}
selectTargetsByIndexImpl(targets.targetIndex);
this.targetIndex = targets.targetIndex + 1;
this.nextTargetIndex = targets.targetIndex + 1;
return true;
}
public boolean selectNextTargets() {
if (targetIndex >= validTargets.size()) {
if (nextTargetIndex >= validTargets.size()) {
return false;
}
selectTargetsByIndexImpl(targetIndex);
targetIndex++;
selectTargetsByIndexImpl(nextTargetIndex);
nextTargetIndex++;
return true;
}
}

View File

@@ -81,9 +81,11 @@ public class SimulationController {
}
public void doneEvaluating(Score score) {
if (score.value > bestScore.value) {
// if we're here during a deeper level this hasn't been called for the level above yet
// in such case we need to check that this decision has really lead to the improvement in score
if (getLastDecision().initialScore.value < score.value && score.value > bestScore.value) {
bestScore = score;
bestSequence = currentStack.get(currentStack.size() - 1);
bestSequence = getLastDecision();
}
currentStack.remove(currentStack.size() - 1);
}