Merge branch 'blockfix' into 'master'

AiBlockController: fix for menace + trample

See merge request core-developers/forge!5614
This commit is contained in:
Michael Kamensky
2021-10-21 12:36:13 +00:00
16 changed files with 43 additions and 52 deletions

View File

@@ -185,7 +185,7 @@ public class AiBlockController {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : attackersLeft) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1) {
continue;
}
@@ -296,7 +296,7 @@ public class AiBlockController {
// 6. Blockers that don't survive until the next turn anyway
for (final Card attacker : attackersLeft) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1) {
continue;
}
@@ -537,7 +537,7 @@ public class AiBlockController {
// Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() <= 1) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) <= 1) {
continue;
}
@@ -593,7 +593,7 @@ public class AiBlockController {
List<Card> killingBlockers;
for (final Card attacker : attackersLeft) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1) {
continue;
}
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
@@ -642,7 +642,7 @@ public class AiBlockController {
Card attacker = attackers.get(0);
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0);
@@ -691,7 +691,7 @@ public class AiBlockController {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : currentAttackers) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() <= 1) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) <= 1) {
continue;
}
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
@@ -729,8 +729,7 @@ public class AiBlockController {
// "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
for (final Card attacker : tramplingAttackers) {
if (((StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, combat.getDefenderPlayerByAttacker(attacker)).getLeft() > 1) && !combat.isBlocked(attacker))
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
@@ -829,7 +828,7 @@ public class AiBlockController {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {

View File

@@ -1776,8 +1776,7 @@ public class ComputerUtilCombat {
return true;
}
// Attacker may kill the blocker before he can deal normal
// (secondary) damage
// Attacker may kill the blocker before he can deal normal (secondary) damage
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
if (attackerDamage >= defenderLife) {
@@ -1828,7 +1827,7 @@ public class ComputerUtilCombat {
* @return a boolean.
*/
public static boolean blockerWouldBeDestroyed(Player ai, final Card blocker, Combat combat) {
// TODO THis function only checks if a single attacker at a time would destroy a blocker
// TODO This function only checks if a single attacker at a time would destroy a blocker
// This needs to expand to tally up damage
final List<Card> attackers = combat.getAttackersBlockedBy(blocker);
@@ -2010,8 +2009,7 @@ public class ComputerUtilCombat {
return true;
}
// Attacker may kill the blocker before he can deal normal
// (secondary) damage
// Attacker may kill the blocker before he can deal normal (secondary) damage
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
&& !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
if (defenderDamage >= attackerLife) {

View File

@@ -413,7 +413,7 @@ public class SpecialCardAi {
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY);
int totalDamageToPW = 0;
for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) {
for (Card atk :combat.getAttackersOf(combat.getDefenderByAttacker(source))) {
if (combat.isUnblocked(atk)) {
totalDamageToPW += atk.getNetCombatDamage();
}

View File

@@ -26,7 +26,7 @@ public class PlayAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final String logic = sa.hasParam("AILogic") ? sa.getParam("AILogic") : "";
final String logic = sa.getParamOrDefault("AILogic", "");
final Game game = ai.getGame();
final Card source = sa.getHostCard();

View File

@@ -121,7 +121,7 @@ public class SacrificeAi extends SpellAbilityAi {
// Only cast it if Human has the full amount of valid
// Only cast it if AI doesn't have the full amount of Valid
// TODO: Cast if the type is favorable: my "worst" valid is worse than his "worst" valid
final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1";
final String num = sa.getParamOrDefault("Amount", "1");
int amount = AbilityUtils.calculateAmount(source, num, sa);
if (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) {

View File

@@ -1482,7 +1482,7 @@ public class AbilityUtils {
final Card source = sa.getHostCard();
// The player who has the chance to cancel the ability
final String pays = sa.hasParam("UnlessPayer") ? sa.getParam("UnlessPayer") : "TargetedController";
final String pays = sa.getParamOrDefault("UnlessPayer", "TargetedController");
final FCollectionView<Player> allPayers = getDefinedPlayers(sa.getHostCard(), pays, sa);
final String resolveSubs = sa.getParam("UnlessResolveSubs"); // no value means 'Always'
final boolean execSubsWhenPaid = "WhenPaid".equals(resolveSubs) || StringUtils.isBlank(resolveSubs);
@@ -1565,7 +1565,7 @@ public class AbilityUtils {
" ", ""), sa);
//Check for XColor
ManaCostBeingPaid toPay = new ManaCostBeingPaid(ManaCost.ZERO);
byte xColor = ManaAtom.fromName(sa.hasParam("UnlessXColor") ? sa.getParam("UnlessXColor") : "1");
byte xColor = ManaAtom.fromName(sa.getParamOrDefault("UnlessXColor", "1"));
toPay.increaseShard(ManaCostShard.valueOf(xColor), xCost);
cost = new Cost(toPay.toManaCost(), true);
}

View File

@@ -261,7 +261,7 @@ public abstract class SpellAbilityEffect {
boolean your = location.startsWith("Your");
boolean combat = location.endsWith("Combat");
String desc = sa.hasParam("AtEOTDesc") ? sa.getParam("AtEOTDesc") : "";
String desc = sa.getParamOrDefault("AtEOTDesc", "");
if (your) {
location = location.substring("Your".length());

View File

@@ -33,7 +33,7 @@ public class BalanceEffect extends SpellAbilityEffect {
Player activator = sa.getActivatingPlayer();
Card source = sa.getHostCard();
Game game = activator.getGame();
String valid = sa.hasParam("Valid") ? sa.getParam("Valid") : "Card";
String valid = sa.getParamOrDefault("Valid", "Card");
ZoneType zone = sa.hasParam("Zone") ? ZoneType.smartValueOf(sa.getParam("Zone")) : ZoneType.Battlefield;
int min = Integer.MAX_VALUE;

View File

@@ -182,7 +182,7 @@ public class DiscardEffect extends SpellAbilityEffect {
boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message);
if (runDiscard) {
final String valid = sa.hasParam("DiscardValid") ? sa.getParam("DiscardValid") : "Card";
final String valid = sa.getParamOrDefault("DiscardValid", "Card");
List<Card> list = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), valid, source.getController(), source, sa);
list = CardLists.filter(list, Presets.NON_TOKEN);
CardCollection toDiscard = new CardCollection();
@@ -250,7 +250,7 @@ public class DiscardEffect extends SpellAbilityEffect {
dPHand = p.getController().chooseCardsToRevealFromHand(amount, amount, dPHand);
}
final String valid = sa.hasParam("DiscardValid") ? sa.getParam("DiscardValid") : "Card";
final String valid = sa.getParamOrDefault("DiscardValid", "Card");
String[] dValid = valid.split(",");
CardCollection validCards = CardLists.getValidCards(dPHand, dValid, source.getController(), source, sa);

View File

@@ -220,7 +220,7 @@ public class FlipCoinEffect extends SpellAbilityEffect {
* @return a boolean.
*/
public static boolean flipCoinCall(final Player caller, final SpellAbility sa, final int multiplier) {
String varName = sa.hasParam("SaveNumFlipsToSVar") ? sa.getParam("SaveNumFlipsToSVar") : "X";
String varName = sa.getParamOrDefault("SaveNumFlipsToSVar", "X");
return flipCoinCall(caller, sa, multiplier, varName);
}
public static boolean flipCoinCall(final Player caller, final SpellAbility sa, final int multiplier, final String varName) {

View File

@@ -15,7 +15,7 @@ public class MakeCardEffect extends SpellAbilityEffect {
final Player player = sa.getActivatingPlayer();
final Game game = player.getGame();
String name = sa.hasParam("Name") ? sa.getParam("Name") : sa.getHostCard().getName();
String name = sa.getParamOrDefault("Name", sa.getHostCard().getName());
if (name.equals("ChosenName")) {
name = sa.getHostCard().getChosenName();
}

View File

@@ -292,7 +292,7 @@ public class PumpEffect extends SpellAbilityEffect {
}
}
if (sa.hasParam("RandomKeyword")) {
final String num = sa.hasParam("RandomKWNum") ? sa.getParam("RandomKWNum") : "1";
final String num = sa.getParamOrDefault("RandomKWNum", "1");
final int numkw = AbilityUtils.calculateAmount(host, num, sa);
List<String> choice = Lists.newArrayList();
List<String> total = Lists.newArrayList(keywords);

View File

@@ -32,7 +32,7 @@ public class RestartGameEffect extends SpellAbilityEffect {
ZoneType leaveZone = ZoneType.smartValueOf(sa.hasParam("RestrictFromZone") ? sa.getParam("RestrictFromZone") : null);
restartZones.remove(leaveZone);
String leaveRestriction = sa.hasParam("RestrictFromValid") ? sa.getParam("RestrictFromValid") : "Card";
String leaveRestriction = sa.getParamOrDefault("RestrictFromValid", "Card");
//Card.resetUniqueNumber();
// need this code here, otherwise observables fail

View File

@@ -12,11 +12,11 @@ import forge.game.keyword.Keyword;
public class AttackingBand {
private CardCollection attackers = new CardCollection();
private Boolean blocked = null; // even if all blockers were killed before FS or CD, band remains blocked
public AttackingBand(final List<Card> band) {
attackers.addAll(band);
}
public AttackingBand(final Card card) {
attackers.add(card);
}
@@ -25,14 +25,13 @@ public class AttackingBand {
public void addAttacker(Card card) { attackers.add(card); }
public void removeAttacker(Card card) { attackers.remove(card); }
public static boolean isValidBand(List<Card> band, boolean shareDamage) {
if (band.isEmpty()) {
// An empty band is not a valid band
return false;
}
int bandingCreatures = CardLists.getKeyword(band, Keyword.BANDING).size();
int neededBandingCreatures = shareDamage ? 1 : band.size() - 1;
if (neededBandingCreatures <= bandingCreatures) {
@@ -40,19 +39,19 @@ public class AttackingBand {
// For sharing damage, only one needs to be Banding
return true;
}
// Legends lands, Master of the Hunt, Old Fogey (just in case)
// Since Bands With Other is a dead keyword, no major reason to make this more generic
// But if someone is super motivated, feel free to do it. Just make sure you update Tolaria and Shelkie Brownie
String[] bandsWithString = { "Bands with Other Legendary Creatures", "Bands with Other Creatures named Wolves of the Hunt",
"Bands with Other Dinosaurs" };
String[] validString = { "Legendary.Creature", "Creature.namedWolves of the Hunt", "Dinosaur" };
Card source = band.get(0);
for (int i = 0; i < bandsWithString.length; i++) {
String keyword = bandsWithString[i];
String valid = validString[i];
// Check if a bands with other keyword exists in band, and each creature in the band fits the valid quality
if (!CardLists.getKeyword(band, keyword).isEmpty() &&
CardLists.getValidCards(band, valid, source.getController(), source, null).size() == band.size()) {
@@ -69,10 +68,9 @@ public class AttackingBand {
if (card != null) {
newBand.add(card);
}
return isValidBand(newBand, false);
}
public boolean contains(Card c) {
return attackers.contains(c);
@@ -89,7 +87,7 @@ public class AttackingBand {
// TODO Auto-generated method stub
return attackers.isEmpty();
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@@ -97,5 +95,5 @@ public class AttackingBand {
public String toString() {
return String.format("%s %s", attackers.toString(), blocked == null ? " ? " : blocked.booleanValue() ? ">||" : ">>>" );
}
}

View File

@@ -345,13 +345,6 @@ public class Combat {
return result;
}
public final CardCollection getBlockers(final Card card) {
// If requesting the ordered blocking list pass true, directly.
AttackingBand band = getBandOfAttacker(card);
Collection<Card> blockers = blockedBands.get(band);
return blockers == null ? new CardCollection() : new CardCollection(blockers);
}
public final boolean isBlocked(final Card attacker) {
AttackingBand band = getBandOfAttacker(attacker);
return band != null && Boolean.TRUE.equals(band.isBlocked());
@@ -408,6 +401,10 @@ public class Combat {
return result;
}
public final CardCollection getBlockers(final Card card) {
// If requesting the ordered blocking list pass true, directly.
return getBlockers(getBandOfAttacker(card));
}
public final CardCollection getBlockers(final AttackingBand band) {
Collection<Card> blockers = blockedBands.get(band);
return blockers == null ? new CardCollection() : new CardCollection(blockers);
@@ -512,8 +509,7 @@ public class Combat {
final CardCollection oldBlockers = blockersOrderedForDamageAssignment.get(attacker);
if (oldBlockers == null || oldBlockers.isEmpty()) {
blockersOrderedForDamageAssignment.put(attacker, new CardCollection(blocker));
}
else {
} else {
CardCollection orderedBlockers = playerWhoAttacks.getController().orderBlocker(attacker, blocker, oldBlockers);
blockersOrderedForDamageAssignment.put(attacker, orderedBlockers);
}

View File

@@ -501,7 +501,7 @@ public class CombatUtil {
return true;
}
if ( combat != null ) {
if (combat != null) {
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defendingPlayer).getRight() == combat.getBlockers(attacker).size()) {
return false;
}
@@ -763,7 +763,7 @@ public class CombatUtil {
for (final Card attacker : attackers) {
if (canBlock(attacker, blocker, combat)) {
boolean must = true;
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defending).getLeft() > 1) {
if (getMinNumBlockersForAttacker(attacker, defending) > 1) {
final List<Card> possibleBlockers = Lists.newArrayList(defendersArmy);
possibleBlockers.remove(blocker);
if (!canBeBlocked(attacker, possibleBlockers, combat)) {
@@ -875,7 +875,7 @@ public class CombatUtil {
Player defendingPlayer = combat.getDefenderPlayerByAttacker(attacker);
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defendingPlayer).getLeft() > 1) {
if (getMinNumBlockersForAttacker(attacker, defendingPlayer) > 1) {
final List<Card> blockers = defendingPlayer.getCreaturesInPlay();
blockers.remove(blocker);
if (!canBeBlocked(attacker, blockers, combat)) {
@@ -894,7 +894,7 @@ public class CombatUtil {
&& combat.isAttacking(attacker)) {
boolean canBe = true;
Player defendingPlayer = combat.getDefenderPlayerByAttacker(attacker);
if (StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defendingPlayer).getLeft() > 1) {
if (getMinNumBlockersForAttacker(attacker, defendingPlayer) > 1) {
final List<Card> blockers = freeBlockers != null ? new CardCollection(freeBlockers) : defendingPlayer.getCreaturesInPlay();
blockers.remove(blocker);
if (!canBeBlocked(attacker, blockers, combat)) {
@@ -1099,4 +1099,4 @@ public class CombatUtil {
public static int getMinNumBlockersForAttacker(Card attacker, Player defender) {
return StaticAbilityCantAttackBlock.getMinMaxBlocker(attacker, defender).getLeft();
}
} // end class CombatUtil
}