Merge branch 'fix' into 'master'

Simulation: fix for Pack Rat tokens (bad diff because of missing CMC)

See merge request core-developers/forge!5980
This commit is contained in:
Michael Kamensky
2021-12-26 15:33:17 +00:00
19 changed files with 119 additions and 136 deletions

View File

@@ -514,9 +514,8 @@ public class AiAttackController {
if (Iterables.any(oppBattlefield, Predicates.and(CardPredicates.Presets.UNTAPPED, CardPredicates.Presets.LANDS))) {
maxBlockersAfterCrew = Integer.MAX_VALUE;
break;
} else {
maxBlockersAfterCrew--;
}
maxBlockersAfterCrew--;
} else if (cardType.hasSubtype("Vehicle") && !cardType.isCreature()) {
maxBlockersAfterCrew--;
}

View File

@@ -2436,7 +2436,7 @@ public class ComputerUtil {
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
CardCollection computerlist = ai.getCreaturesInPlay();
return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
case "Judgment":
if (votes.isEmpty()) {
CardCollection list = new CardCollection();
@@ -2446,9 +2446,8 @@ public class ComputerUtil {
}
}
return ComputerUtilCard.getBestAI(list);
} else {
return Iterables.getFirst(votes.keySet(), null);
}
return Iterables.getFirst(votes.keySet(), null);
case "Protection":
if (votes.isEmpty()) {
List<String> restrictedToColors = Lists.newArrayList();
@@ -2459,9 +2458,8 @@ public class ComputerUtil {
}
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
} else {
return Iterables.getFirst(votes.keySet(), null);
}
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
// try to mill opponent with Quill vote
if (opponent && !controller.cantLose()) {

View File

@@ -2292,9 +2292,8 @@ public class ComputerUtilCombat {
}
if (!withoutAbilities) {
return canGainKeyword(combatant, Lists.newArrayList(keyword), combat);
} else {
return false;
}
return false;
}
public final static boolean canGainKeyword(final Card combatant, final List<String> keywords, final Combat combat) {

View File

@@ -306,52 +306,50 @@ public class ComputerUtilMana {
continue;
}
if (saHost != null) {
if (ma.getPayCosts().hasTapCost() && AiCardMemory.isRememberedCard(ai, ma.getHostCard(), MemorySet.PAYS_TAP_COST)) {
if (ma.getPayCosts().hasTapCost() && AiCardMemory.isRememberedCard(ai, ma.getHostCard(), MemorySet.PAYS_TAP_COST)) {
continue;
}
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa)) {
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"))
&& ma.getHostCard() == saHost.getEnchantingCard()
&& ma.getPayCosts().hasTapCost()) {
continue;
}
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa)) {
// If a manland was previously animated this turn, do not tap it to animate another manland
if (saHost.isLand() && ma.getHostCard().isLand()
&& ai.getController().isAI()
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
continue;
}
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma.getHostCard(), ma, ma.isTrigger())) {
} else if (sa.getApi() == ApiType.Pump) {
if ((saHost.isInstant() || saHost.isSorcery())
&& ma.getHostCard().isCreature()
&& ai.getController().isAI()
&& ma.getPayCosts().hasTapCost()
&& sa.getTargets().getTargetCards().contains(ma.getHostCard())) {
// do not activate pump instants/sorceries targeting creatures by tapping targeted
// creatures for mana (for example, Servant of the Conduit)
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"))
&& ma.getHostCard() == saHost.getEnchantingCard()
&& ma.getPayCosts().hasTapCost()) {
} else if (sa.getApi() == ApiType.Attach
&& "AvoidPayingWithAttachTarget".equals(saHost.getSVar("AIPaymentPreference"))) {
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
// be tapped to pay its own cost if there's another untapped land like that available
if (ma.getHostCard().equals(sa.getTargetCard())) {
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.nameEquals(ma.getHostCard().getName()), CardPredicates.Presets.UNTAPPED)).size() > 1) {
continue;
}
// If a manland was previously animated this turn, do not tap it to animate another manland
if (saHost.isLand() && ma.getHostCard().isLand()
&& ai.getController().isAI()
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
continue;
}
} else if (sa.getApi() == ApiType.Pump) {
if ((saHost.isInstant() || saHost.isSorcery())
&& ma.getHostCard().isCreature()
&& ai.getController().isAI()
&& ma.getPayCosts().hasTapCost()
&& sa.getTargets().getTargetCards().contains(ma.getHostCard())) {
// do not activate pump instants/sorceries targeting creatures by tapping targeted
// creatures for mana (for example, Servant of the Conduit)
continue;
}
} else if (sa.getApi() == ApiType.Attach
&& "AvoidPayingWithAttachTarget".equals(saHost.getSVar("AIPaymentPreference"))) {
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
// be tapped to pay its own cost if there's another untapped land like that available
if (ma.getHostCard().equals(sa.getTargetCard())) {
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.nameEquals(ma.getHostCard().getName()), CardPredicates.Presets.UNTAPPED)).size() > 1) {
continue;
}
}
}
}

View File

@@ -1023,9 +1023,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (blink) {
return c.isToken();
} else {
return c.isToken() || c.getCMC() > 0;
}
return c.isToken() || c.getCMC() > 0;
}
});
}

View File

@@ -315,7 +315,6 @@ public class ControlGainAi extends SpellAbilityAi {
} else {
return this.canPlayAI(ai, sa);
}
} // pumpDrawbackAI()
@Override

View File

@@ -153,10 +153,9 @@ public abstract class DamageAiBase extends SpellAbilityAi {
}
if (value < 0.2f) { //hard floor to reduce ridiculous odds for instants over time
return false;
} else {
final float chance = MyRandom.getRandom().nextFloat();
return chance < value;
}
final float chance = MyRandom.getRandom().nextFloat();
return chance < value;
}
}

View File

@@ -811,9 +811,8 @@ public class DamageDealAi extends DamageAiBase {
if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && ComputerUtilCombat.getDamageToKill(c, false) <= restDamage) {
if (c.getController().equals(ai)) {
return false;
} else {
urgent = true;
}
urgent = true;
}
if (c.getController().isOpponentOf(ai) ^ c.getName().equals("Stuffy Doll")) {
positive = true;

View File

@@ -198,7 +198,7 @@ public class EffectAi extends SpellAbilityAi {
final Card host = saTop.getHostCard();
if (saTop.getActivatingPlayer() != ai // from opponent
&& host.canDamagePrevented(false) // no prevent damage
&& host != null && (host.isInstant() || host.isSorcery())
&& (host.isInstant() || host.isSorcery())
&& !host.hasKeyword("Prevent all damage that would be dealt by CARDNAME.")) { // valid target
final ApiType type = saTop.getApi();
if (type == ApiType.DealDamage || type == ApiType.DamageAll) { // burn spell

View File

@@ -286,12 +286,11 @@ public class FightAi extends SpellAbilityAi {
private static boolean shouldFight(Card fighter, Card opponent, int pumpAttack, int pumpDefense) {
if (canKill(fighter, opponent, pumpAttack)) {
if (!canKill(opponent, fighter, -pumpDefense)) { // can survive
return true;
} else {
if (MyRandom.getRandom().nextInt(20) < (opponent.getCMC() - fighter.getCMC())) { // trade
return true;
}
}
return true;
}
if (MyRandom.getRandom().nextInt(20) < (opponent.getCMC() - fighter.getCMC())) { // trade
return true;
}
}
return false;
}

View File

@@ -233,10 +233,14 @@ public class GameCopier {
if (card.isPaired()) {
otherCard.setPairedWith(cardMap.get(card.getPairedWith()));
}
if (card.getCopiedPermanent() != null) {
// TODO would it be safe to simply reuse the prototype?
otherCard.setCopiedPermanent(CardFactory.copyCard(card.getCopiedPermanent(), false));
}
// TODO: Verify that the above relationships are preserved bi-directionally or not.
}
}
private static final boolean USE_FROM_PAPER_CARD = true;
private Card createCardCopy(Game newGame, Player newOwner, Card c) {
if (c.isToken() && !c.isImmutable()) {

View File

@@ -547,9 +547,8 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
public String getSVar(final String name) {
if (sVars.containsKey(name)) {
return sVars.get(name);
} else {
return getSVarFallback().getSVar(name);
}
return getSVarFallback().getSVar(name);
}
@Override

View File

@@ -1422,7 +1422,7 @@ public class AbilityUtils {
// do blessing there before condition checks
if (sa.isSpell() && sa.isBlessing() && !sa.getHostCard().isPermanent()) {
if (pl != null && pl.getZone(ZoneType.Battlefield).size() >= 10) {
if (pl.getZone(ZoneType.Battlefield).size() >= 10) {
pl.setBlessing(true);
}
}
@@ -1871,9 +1871,8 @@ public class AbilityUtils {
list = CardLists.getValidCards(list, k[1].split(","), sa.getActivatingPlayer(), c, sa);
if (k[0].contains("TotalToughness")) {
return doXMath(Aggregates.sum(list, CardPredicates.Accessors.fnGetNetToughness), expr, c, ctb);
} else {
return doXMath(list.size(), expr, c, ctb);
}
return doXMath(list.size(), expr, c, ctb);
}
if (sq[0].startsWith("LastStateGraveyard")) {
@@ -3208,9 +3207,8 @@ public class AbilityUtils {
} else if (s[0].contains("DivideEvenlyDown")) {
if (secondaryNum == 0) {
return 0;
} else {
return num / secondaryNum;
}
return num / secondaryNum;
} else if (s[0].contains("Mod")) {
return num % secondaryNum;
} else if (s[0].contains("Abs")) {
@@ -3218,15 +3216,13 @@ public class AbilityUtils {
} else if (s[0].contains("LimitMax")) {
if (num < secondaryNum) {
return num;
} else {
return secondaryNum;
}
return secondaryNum;
} else if (s[0].contains("LimitMin")) {
if (num > secondaryNum) {
return num;
} else {
return secondaryNum;
}
return secondaryNum;
} else {
return num;

View File

@@ -371,8 +371,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return false;
}
public long getTransformedTimestamp() { return transformedTimestamp; }
public void incrementTransformedTimestamp() { this.transformedTimestamp++; }
public long getTransformedTimestamp() { return transformedTimestamp; }
public void incrementTransformedTimestamp() { this.transformedTimestamp++; }
public CardState getCurrentState() {
return currentState;
@@ -3037,7 +3037,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return tokenCard;
}
public final void setTokenCard(boolean tokenC) {
if (tokenCard = tokenC) { return; }
if (tokenCard == tokenC) { return; }
tokenCard = tokenC;
view.updateTokenCard(this);
}
@@ -5232,9 +5232,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
// this is the amount of damage a creature needs to receive before it dies
public final int getLethal() {
if (hasKeyword("Lethal damage dealt to CARDNAME is determined by its power rather than its toughness.")) {
return getNetPower(); }
else {
return getNetToughness(); }
return getNetPower();
}
return getNetToughness();
}
// this is the minimal damage a trampling creature has to assign to a blocker

View File

@@ -185,7 +185,6 @@ public class TokenInfo {
}
}
if (!typeMap.isEmpty()) {
CardType type = new CardType(result.getType());
final boolean nameGenerated = result.getName().endsWith(" Token");
boolean typeChanged = false;

View File

@@ -135,56 +135,54 @@ public class CostDiscard extends CostPartWithList {
if (this.payCostFromSource()) {
return source.canBeDiscardedBy(ability, effect);
}
else if (type.equals("Hand")) {
// trying to discard an empty hand always work even with Tamiyo
if (payer.getZone(ZoneType.Hand).isEmpty()) {
return true;
}
return payer.canDiscardBy(ability, effect);
// this will always work
}
else if (type.equals("LastDrawn")) {
final Card c = payer.getLastDrawnCard();
return handList.contains(c);
}
else if (type.equals("DifferentNames")) {
Set<String> cardNames = Sets.newHashSet();
for (Card c : handList) {
cardNames.add(c.getName());
}
return cardNames.size() >= amount;
}
else {
if (type.equals("Hand")) {
// trying to discard an empty hand always work even with Tamiyo
if (payer.getZone(ZoneType.Hand).isEmpty()) {
return true;
}
return payer.canDiscardBy(ability, effect);
// this will always work
boolean sameName = false;
if (type.contains("+WithSameName")) {
sameName = true;
type = TextUtil.fastReplace(type, "+WithSameName", "");
}
else if (type.equals("LastDrawn")) {
final Card c = payer.getLastDrawnCard();
return handList.contains(c);
if (!type.equals("Random") && !type.contains("X")) {
// Knollspine Invocation fails to activate without the above conditional
handList = CardLists.getValidCards(handList, type.split(";"), payer, source, ability);
}
else if (type.equals("DifferentNames")) {
Set<String> cardNames = Sets.newHashSet();
for (Card c : handList) {
cardNames.add(c.getName());
}
return cardNames.size() >= amount;
}
else {
boolean sameName = false;
if (type.contains("+WithSameName")) {
sameName = true;
type = TextUtil.fastReplace(type, "+WithSameName", "");
}
if (!type.equals("Random") && !type.contains("X")) {
// Knollspine Invocation fails to activate without the above conditional
handList = CardLists.getValidCards(handList, type.split(";"), payer, source, ability);
}
if (sameName) {
for (Card c : handList) {
if (CardLists.filter(handList, CardPredicates.nameEquals(c.getName())).size() > 1) {
return true;
}
}
return false;
}
int adjustment = 0;
if (source.isInZone(ZoneType.Hand) && payer.equals(source.getOwner())) {
// If this card is in my hand, I can't use it to pay for it's own cost
if (handList.contains(source)) {
adjustment = 1;
if (sameName) {
for (Card c : handList) {
if (CardLists.filter(handList, CardPredicates.nameEquals(c.getName())).size() > 1) {
return true;
}
}
return false;
}
int adjustment = 0;
if (source.isInZone(ZoneType.Hand) && payer.equals(source.getOwner())) {
// If this card is in my hand, I can't use it to pay for it's own cost
if (handList.contains(source)) {
adjustment = 1;
}
}
if (amount > handList.size() - adjustment) {
// not enough cards in hand to pay
return false;
}
if (amount > handList.size() - adjustment) {
// not enough cards in hand to pay
return false;
}
}
return true;

View File

@@ -148,13 +148,12 @@ public class CostPartMana extends CostPart {
int timesToPay = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getSVar("NumTimes"), sa);
if (timesToPay == 0) {
return null;
} else {
ManaCostBeingPaid totalMana = new ManaCostBeingPaid(getManaToPay());
for (int i = 1; i < timesToPay; i++) {
totalMana.addManaCost(getManaToPay());
}
return totalMana.toManaCost();
}
ManaCostBeingPaid totalMana = new ManaCostBeingPaid(getManaToPay());
for (int i = 1; i < timesToPay; i++) {
totalMana.addManaCost(getManaToPay());
}
return totalMana.toManaCost();
}
return getManaToPay();
}

View File

@@ -434,8 +434,8 @@ public class ReplacementHandler {
private void getPossibleReplaceDamageList(PlayerCollection players, final boolean isCombat, final CardDamageMap damageMap, final SpellAbility cause) {
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
final GameEntity target = et.getKey();
int playerIndex = (target instanceof Player ? players.indexOf(((Player) target)) :
players.indexOf(((Card) target).getController()));
int playerIndex = target instanceof Player ? players.indexOf(((Player) target)) :
players.indexOf(((Card) target).getController());
if (playerIndex == -1) continue;
Map<ReplacementEffect, List<Map<AbilityKey, Object>>> replaceCandidateMap = replaceDamageList.get(playerIndex);
for (Map.Entry<Card, Integer> e : et.getValue().entrySet()) {
@@ -501,8 +501,8 @@ public class ReplacementHandler {
Map<ReplacementEffect, List<Map<AbilityKey, Object>>> newReplaceCandidateMap = replaceCandidateMap;
if (!target.equals(newTarget)) {
PlayerCollection players = game.getPlayersInTurnOrder();
int playerIndex = (newTarget instanceof Player ? players.indexOf(((Player) newTarget)) :
players.indexOf(((Card) newTarget).getController()));
int playerIndex = newTarget instanceof Player ? players.indexOf(((Player) newTarget)) :
players.indexOf(((Card) newTarget).getController());
newReplaceCandidateMap = replaceDamageList.get(playerIndex);
}

View File

@@ -386,9 +386,8 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
return InputConfirm.confirm(this, CardView.get(attacker),
localizer.getMessage("lblAssignCombatDamageWerentBlocked"));
} else {
return false;
}
return false;
}
@Override