Unblockable fixes (#1662)

* Add shortcuts

* Fix cards

* Clean up

* Fix CantBlockBy checks

* Fix stack overflow

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
This commit is contained in:
tool4ever
2022-10-10 05:57:30 +02:00
committed by GitHub
parent 1082797c07
commit 839a9c8402
19 changed files with 53 additions and 28 deletions

View File

@@ -359,7 +359,7 @@ public class AiAttackController {
} }
}); });
final List<Card> notNeededAsBlockers = new CardCollection(attackers); final CardCollection notNeededAsBlockers = new CardCollection(attackers);
// don't hold back creatures that can't block any of the human creatures // don't hold back creatures that can't block any of the human creatures
final List<Card> blockers = getPossibleBlockers(attackers, opponentsAttackers, true); final List<Card> blockers = getPossibleBlockers(attackers, opponentsAttackers, true);
@@ -378,7 +378,7 @@ public class AiAttackController {
int thresholdMod = 0; int thresholdMod = 0;
int lastAcceptableBaselineLife = 0; int lastAcceptableBaselineLife = 0;
if (pilotsNonAggroDeck) { if (pilotsNonAggroDeck) {
lastAcceptableBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers)); lastAcceptableBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, notNeededAsBlockers);
if (!ai.isCardInPlay("Laboratory Maniac")) { if (!ai.isCardInPlay("Laboratory Maniac")) {
// AI is getting milled out // AI is getting milled out
thresholdMod += 3 - Math.min(ai.getCardsIn(ZoneType.Library).size(), 3); thresholdMod += 3 - Math.min(ai.getCardsIn(ZoneType.Library).size(), 3);
@@ -397,7 +397,7 @@ public class AiAttackController {
continue; continue;
} }
notNeededAsBlockers.add(c); notNeededAsBlockers.add(c);
int currentBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers)); int currentBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, notNeededAsBlockers);
// AI doesn't know from what it will lose, so it might still keep an unnecessary blocker back sometimes // AI doesn't know from what it will lose, so it might still keep an unnecessary blocker back sometimes
if (currentBaselineLife == Integer.MIN_VALUE) { if (currentBaselineLife == Integer.MIN_VALUE) {
notNeededAsBlockers.remove(c); notNeededAsBlockers.remove(c);
@@ -839,6 +839,7 @@ public class AiAttackController {
if (attackMax != -1 && combat.getAttackers().size() >= attackMax) if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
return aiAggression; return aiAggression;
// TODO if lifeInDanger use chance to hold back some
if (canAttackWrapper(attacker, defender) && isEffectiveAttacker(ai, attacker, combat, defender)) { if (canAttackWrapper(attacker, defender) && isEffectiveAttacker(ai, attacker, combat, defender)) {
combat.addAttacker(attacker, defender); combat.addAttacker(attacker, defender);
} }
@@ -848,7 +849,7 @@ public class AiAttackController {
} }
// Cards that are remembered to attack anyway (e.g. temporarily stolen creatures) // Cards that are remembered to attack anyway (e.g. temporarily stolen creatures)
if (ai.getController() instanceof PlayerControllerAi) { if (ai.getController().isAI()) {
// Only do this if |ai| is actually an AI - as we could be trying to predict how the human will attack. // Only do this if |ai| is actually an AI - as we could be trying to predict how the human will attack.
for (Card attacker : this.attackers) { for (Card attacker : this.attackers) {
if (AiCardMemory.isRememberedCard(ai, attacker, AiCardMemory.MemorySet.MANDATORY_ATTACKERS)) { if (AiCardMemory.isRememberedCard(ai, attacker, AiCardMemory.MemorySet.MANDATORY_ATTACKERS)) {
@@ -894,7 +895,7 @@ public class AiAttackController {
aiAggression = 6; aiAggression = 6;
for (Card attacker : this.attackers) { for (Card attacker : this.attackers) {
// reached max, breakup // reached max, breakup
if (attackMax != -1 && combat.getAttackers().size() >= attackMax) if (combat.getAttackers().size() >= attackMax)
break; break;
if (canAttackWrapper(attacker, defender) && shouldAttack(attacker, this.blockers, combat, defender)) { if (canAttackWrapper(attacker, defender) && shouldAttack(attacker, this.blockers, combat, defender)) {
combat.addAttacker(attacker, defender); combat.addAttacker(attacker, defender);

View File

@@ -1061,7 +1061,7 @@ public class AiBlockController {
// remove all attackers that can't be blocked anyway // remove all attackers that can't be blocked anyway
for (final Card a : attackers) { for (final Card a : attackers) {
if (!CombatUtil.canBeBlocked(a, ai)) { if (!CombatUtil.canBeBlocked(a, null, ai)) { // pass null to skip redundant checks for performance
attackersLeft.remove(a); attackersLeft.remove(a);
} }
} }

View File

@@ -1055,11 +1055,9 @@ public class AiController {
// Cheaper Spectacle costs should be preferred // Cheaper Spectacle costs should be preferred
// FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not? // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not?
// (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs) // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs)
if (a.isSpectacle() && !b.isSpectacle() if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) {
&& a.getPayCosts().getTotalMana().getCMC() < b.getPayCosts().getTotalMana().getCMC()) {
return 1; return 1;
} else if (b.isSpectacle() && !a.isSpectacle() } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) {
&& b.getPayCosts().getTotalMana().getCMC() < a.getPayCosts().getTotalMana().getCMC()) {
return 1; return 1;
} }
} }
@@ -1088,6 +1086,9 @@ public class AiController {
if (source.hasSVar("AIPriorityModifier")) { if (source.hasSVar("AIPriorityModifier")) {
p += Integer.parseInt(source.getSVar("AIPriorityModifier")); p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
} }
if (ComputerUtilCard.isCardRemAIDeck(source)) {
p -= 10;
}
// don't play equipments before having any creatures // don't play equipments before having any creatures
if (source.isEquipment() && noCreatures) { if (source.isEquipment() && noCreatures) {
p -= 9; p -= 9;
@@ -1691,6 +1692,7 @@ public class AiController {
Iterables.removeIf(saList, new Predicate<SpellAbility>() { Iterables.removeIf(saList, new Predicate<SpellAbility>() {
@Override @Override
public boolean apply(final SpellAbility spellAbility) { //don't include removedAI cards if somehow the AI can play the ability or gain control of unsupported card public boolean apply(final SpellAbility spellAbility) { //don't include removedAI cards if somehow the AI can play the ability or gain control of unsupported card
// TODO allow when experimental profile?
return spellAbility instanceof LandAbility || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard())); return spellAbility instanceof LandAbility || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard()));
} }
}); });

View File

@@ -385,6 +385,9 @@ public class ComputerUtilCard {
* @return the card * @return the card
*/ */
public static Card getBestCreatureAI(final Iterable<Card> list) { public static Card getBestCreatureAI(final Iterable<Card> list) {
if (Iterables.size(list) == 1) {
return Iterables.get(list, 0);
}
return Aggregates.itemWithMax(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator); return Aggregates.itemWithMax(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator);
} }
@@ -397,6 +400,9 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object. * @return a {@link forge.game.card.Card} object.
*/ */
public static Card getWorstCreatureAI(final Iterable<Card> list) { public static Card getWorstCreatureAI(final Iterable<Card> list) {
if (Iterables.size(list) == 1) {
return Iterables.get(list, 0);
}
return Aggregates.itemWithMin(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator); return Aggregates.itemWithMin(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator);
} }
@@ -410,6 +416,9 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object. * @return a {@link forge.game.card.Card} object.
*/ */
public static Card getBestCreatureToBounceAI(final CardCollectionView list) { public static Card getBestCreatureToBounceAI(final CardCollectionView list) {
if (Iterables.size(list) == 1) {
return Iterables.get(list, 0);
}
final int tokenBonus = 60; final int tokenBonus = 60;
Card biggest = null; Card biggest = null;
int biggestvalue = -1; int biggestvalue = -1;

View File

@@ -1565,8 +1565,9 @@ public class AttachAi extends SpellAbilityAi {
boolean canBeBlocked = false; boolean canBeBlocked = false;
for (Player opp : ai.getOpponents()) { for (Player opp : ai.getOpponents()) {
if (CombatUtil.canBeBlocked(card, opp)) { if (CombatUtil.canBeBlocked(card, null, opp)) {
canBeBlocked = true; canBeBlocked = true;
break;
} }
} }

View File

@@ -69,10 +69,10 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true; randomReturn = true;
} }
} else if (logic.equals("Fog")) { } else if (logic.equals("Fog")) {
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) { if (phase.isPlayerTurn(sa.getActivatingPlayer())) {
return false; return false;
} }
if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { if (!phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false; return false;
} }
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
@@ -216,9 +216,12 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("Fight")) { } else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0, 0); return FightAi.canFightAi(ai, sa, 0, 0);
} else if (logic.equals("Pump")) { } else if (logic.equals("Pump")) {
if (SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(ai, sa)) { List<Card> options = ai.getCreaturesInPlay();
if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && !options.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(options));
return true; return true;
} }
return false;
} else if (logic.equals("Burn")) { } else if (logic.equals("Burn")) {
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack) // for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
SpellAbility burn = sa.getSubAbility(); SpellAbility burn = sa.getSubAbility();
@@ -245,7 +248,7 @@ public class EffectAi extends SpellAbilityAi {
Card host = sa.getHostCard(); Card host = sa.getHostCard();
Combat combat = game.getCombat(); Combat combat = game.getCombat();
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host) if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
&& game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS) && phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) { && !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
return true; return true;

View File

@@ -102,7 +102,7 @@ public final class EncodeAi extends SpellAbilityAi {
public boolean apply(final Card c) { public boolean apply(final Card c) {
boolean canAttackOpponent = false; boolean canAttackOpponent = false;
for (Player opp : ai.getOpponents()) { for (Player opp : ai.getOpponents()) {
if (CombatUtil.canAttack(c, opp) && !CombatUtil.canBeBlocked(c, opp)) { if (CombatUtil.canAttack(c, opp) && !CombatUtil.canBeBlocked(c, null, opp)) {
canAttackOpponent = true; canAttackOpponent = true;
break; break;
} }

View File

@@ -336,7 +336,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
Keyword.FLANKING).isEmpty(); Keyword.FLANKING).isEmpty();
} else if (keyword.startsWith("Trample")) { } else if (keyword.startsWith("Trample")) {
return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card))) return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
&& CombatUtil.canBeBlocked(card, opp) && CombatUtil.canBeBlocked(card, null, opp)
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& newPower > 1 && newPower > 1
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)); && Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));

View File

@@ -555,10 +555,19 @@ public class CombatUtil {
return false; return false;
} }
} }
// Unblockable check
for (final Card ca : attacker.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (stAb.applyAbility("CantBlockBy", attacker, null)) {
return false;
}
}
}
return canBeBlocked(attacker, defendingPlayer); return canBeBlocked(attacker, defendingPlayer);
} }
// can the attacker be blocked at all?
/** /**
* <p> * <p>
* canBeBlocked. * canBeBlocked.

View File

@@ -302,7 +302,7 @@ public class StaticAbility extends CardTraitBase implements IIdentifiable, Clone
if (mode.equals("CantAttack")) { if (mode.equals("CantAttack")) {
return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target); return StaticAbilityCantAttackBlock.applyCantAttackAbility(this, card, target);
} else if (mode.equals("CantBlockBy") && target instanceof Card) { } else if (mode.equals("CantBlockBy")) { // null allowed, so no instanceof check
return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target); return StaticAbilityCantAttackBlock.applyCantBlockByAbility(this, card, (Card)target);
} else if (mode.equals("CanAttackIfHaste")) { } else if (mode.equals("CanAttackIfHaste")) {
return StaticAbilityCantAttackBlock.applyCanAttackHasteAbility(this, card, target); return StaticAbilityCantAttackBlock.applyCanAttackHasteAbility(this, card, target);

View File

@@ -115,7 +115,7 @@ public class StaticAbilityCantAttackBlock {
if (stAb.hasParam("ValidBlocker")) { if (stAb.hasParam("ValidBlocker")) {
boolean stillblock = true; boolean stillblock = true;
for (final String v : stAb.getParam("ValidBlocker").split(",")) { for (final String v : stAb.getParam("ValidBlocker").split(",")) {
if (blocker.isValid(v, host.getController(), host, stAb)) { if (blocker != null && blocker.isValid(v, host.getController(), host, stAb)) {
stillblock = false; stillblock = false;
//Dragon Hunter check //Dragon Hunter check
if (v.contains("withoutReach") && blocker.hasStartOfKeyword("IfReach")) { if (v.contains("withoutReach") && blocker.hasStartOfKeyword("IfReach")) {

View File

@@ -3,5 +3,5 @@ ManaCost:3 W U
Types:Creature Human Knight Types:Creature Human Knight
PT:2/5 PT:2/5
K:Vigilance K:Vigilance
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
Oracle:Vigilance\nAzorius Knight-Arbiter can't be blocked. Oracle:Vigilance\nAzorius Knight-Arbiter can't be blocked.

View File

@@ -2,7 +2,7 @@ Name:Gray Harbor Merfolk
ManaCost:1 U ManaCost:1 U
Types:Creature Merfolk Rogue Types:Creature Merfolk Rogue
PT:0/3 PT:0/3
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
S:Mode$ Continuous | Affected$ Card.Self | AddPower$ 2 | IsPresent$ Creature.IsCommander+YouCtrl,Planeswalker.IsCommander+YouCtrl | Description$ CARDNAME gets +2/+0 as long as you control a commander that's a creature or planeswalker. S:Mode$ Continuous | Affected$ Card.Self | AddPower$ 2 | IsPresent$ Creature.IsCommander+YouCtrl,Planeswalker.IsCommander+YouCtrl | Description$ CARDNAME gets +2/+0 as long as you control a commander that's a creature or planeswalker.
AI:RemoveDeck:NonCommander AI:RemoveDeck:NonCommander
Oracle:Gray Harbor Merfolk can't be blocked.\nGray Harbor Merfolk gets +2/+0 as long as you control a commander that's a creature or planeswalker. Oracle:Gray Harbor Merfolk can't be blocked.\nGray Harbor Merfolk gets +2/+0 as long as you control a commander that's a creature or planeswalker.

View File

@@ -2,5 +2,5 @@ Name:Jhessian Infiltrator
ManaCost:G U ManaCost:G U
Types:Creature Human Rogue Types:Creature Human Rogue
PT:2/2 PT:2/2
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
Oracle:Jhessian Infiltrator can't be blocked. Oracle:Jhessian Infiltrator can't be blocked.

View File

@@ -2,5 +2,5 @@ Name:Phantom Warrior
ManaCost:1 U U ManaCost:1 U U
Types:Creature Illusion Warrior Types:Creature Illusion Warrior
PT:2/2 PT:2/2
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
Oracle:Phantom Warrior can't be blocked. Oracle:Phantom Warrior can't be blocked.

View File

@@ -2,5 +2,5 @@ Name:Plasma Elemental
ManaCost:5 U ManaCost:5 U
Types:Creature Elemental Types:Creature Elemental
PT:4/1 PT:4/1
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
Oracle:Plasma Elemental can't be blocked. Oracle:Plasma Elemental can't be blocked.

View File

@@ -2,7 +2,7 @@ Name:Soulsworn Spirit
ManaCost:3 U ManaCost:3 U
Types:Creature Spirit Types:Creature Spirit
PT:2/1 PT:2/1
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ Detain | TriggerDescription$ When CARDNAME enters the battlefield, detain target creature an opponent controls. (Until your next turn, that creature can't attack or block and its activated abilities can't be activated.) T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ Detain | TriggerDescription$ When CARDNAME enters the battlefield, detain target creature an opponent controls. (Until your next turn, that creature can't attack or block and its activated abilities can't be activated.)
SVar:Detain:DB$ Pump | KW$ HIDDEN CARDNAME can't attack or block. & HIDDEN CARDNAME's activated abilities can't be activated. | IsCurse$ True | Duration$ UntilYourNextTurn | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature your opponent controls to detain. SVar:Detain:DB$ Pump | KW$ HIDDEN CARDNAME can't attack or block. & HIDDEN CARDNAME's activated abilities can't be activated. | IsCurse$ True | Duration$ UntilYourNextTurn | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature your opponent controls to detain.
SVar:PlayMain1:TRUE SVar:PlayMain1:TRUE

View File

@@ -2,5 +2,5 @@ Name:Triton Shorestalker
ManaCost:U ManaCost:U
Types:Creature Merfolk Rogue Types:Creature Merfolk Rogue
PT:1/1 PT:1/1
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
Oracle:Triton Shorestalker can't be blocked. Oracle:Triton Shorestalker can't be blocked.

View File

@@ -2,7 +2,7 @@ Name:Vedalken Infiltrator
ManaCost:1 U ManaCost:1 U
Types:Creature Vedalken Rogue Types:Creature Vedalken Rogue
PT:1/3 PT:1/3
K:Unblockable S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
S:Mode$ Continuous | Affected$ Card.Self | AddPower$ 1 | Condition$ Metalcraft | Description$ Metalcraft — CARDNAME gets +1/+0 as long as you control three or more artifacts. S:Mode$ Continuous | Affected$ Card.Self | AddPower$ 1 | Condition$ Metalcraft | Description$ Metalcraft — CARDNAME gets +1/+0 as long as you control three or more artifacts.
SVar:BuffedBy:Artifact SVar:BuffedBy:Artifact
Oracle:Vedalken Infiltrator can't be blocked.\nMetalcraft — Vedalken Infiltrator gets +1/+0 as long as you control three or more artifacts. Oracle:Vedalken Infiltrator can't be blocked.\nMetalcraft — Vedalken Infiltrator gets +1/+0 as long as you control three or more artifacts.