Merge branch 'paycheat' into 'master'

Fix casting SA while refusing to pay CostSacrifice

See merge request core-developers/forge!4881
This commit is contained in:
Michael Kamensky
2021-06-16 03:15:52 +00:00
9 changed files with 66 additions and 42 deletions

View File

@@ -2227,11 +2227,11 @@ public class AiController {
private boolean checkAiSpecificRestrictions(final SpellAbility sa) {
// AI-specific restrictions specified as activation parameters in spell abilities
if (sa.hasParam("AILifeThreshold")) {
return player.getLife() > Integer.parseInt(sa.getParam("AILifeThreshold"));
}
return true;
}

View File

@@ -354,7 +354,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostPutCardToLib cost) {
if (cost.payCostFromSource()) {

View File

@@ -331,11 +331,14 @@ public class ComputerUtil {
}
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
return getCardPreference(ai, activate, pref, typeList, null);
}
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList, SpellAbility sa) {
final Game game = ai.getGame();
String prefDef = "";
if (activate != null) {
prefDef = activate.getSVar("AIPreference");
final String[] prefGroups = activate.getSVar("AIPreference").split("\\|");
final String[] prefGroups = prefDef.split("\\|");
for (String prefGroup : prefGroups) {
final String[] prefValid = prefGroup.trim().split("\\$");
if (prefValid[0].equals(pref) && !prefValid[1].startsWith("Special:")) {
@@ -346,8 +349,8 @@ public class ComputerUtil {
for (String validItem : prefValid[1].split(",")) {
final CardCollection prefList = CardLists.getValidCards(typeList, validItem, activate.getController(), activate, null);
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold");
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold");
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold", sa);
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold", sa);
if (threshold != -1) {
List<Card> toRemove = Lists.newArrayList();
@@ -390,7 +393,7 @@ public class ComputerUtil {
final CardCollection sacMeList = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return (c.hasSVar("SacMe") && (Integer.parseInt(c.getSVar("SacMe")) == priority));
return c.hasSVar("SacMe") && (Integer.parseInt(c.getSVar("SacMe")) == priority);
}
});
if (!sacMeList.isEmpty()) {
@@ -419,6 +422,7 @@ public class ComputerUtil {
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);
}
}
@@ -505,7 +509,7 @@ public class ComputerUtil {
return null;
}
public static int getAIPreferenceParameter(final Card c, final String paramName) {
public static int getAIPreferenceParameter(final Card c, final String paramName, SpellAbility sa) {
if (!c.hasSVar("AIPreferenceParams")) {
return -1;
}
@@ -520,7 +524,21 @@ public class ComputerUtil {
case "CreatureEvalThreshold":
// Threshold of 150 is just below the level of a 1/1 mana dork or a 2/2 baseline creature with no keywords
if (paramName.equals(parName)) {
return Integer.parseInt(parValue);
int num = 0;
try {
num = Integer.parseInt(parValue);
} catch (NumberFormatException nfe) {
String[] valParts = StringUtils.split(parValue, "/");
CardCollection foundCards = AbilityUtils.getDefinedCards(c, valParts[0], sa);
if (!foundCards.isEmpty()) {
num = ComputerUtilCard.evaluateCreature(foundCards.get(0));
}
valParts[0] = Integer.toString(num);
if (valParts.length > 1) {
num = AbilityUtils.doXMath(num, valParts[1], c, sa);
}
}
return num;
}
break;
case "MinCreaturesBelowThreshold":
@@ -543,9 +561,8 @@ public class ComputerUtil {
typeList = CardLists.filter(typeList, CardPredicates.canBeSacrificedBy(ability));
if ((target != null) && target.getController() == ai) {
typeList.remove(target); // don't sacrifice the card we're pumping
}
// don't sacrifice the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, ability, ai);
if (typeList.size() < amount) {
return null;
@@ -573,9 +590,8 @@ public class ComputerUtil {
final Card target, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, sa);
if ((target != null) && target.getController() == ai) {
typeList.remove(target); // don't exile the card we're pumping
}
// don't exile the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);
if (typeList.size() < amount) {
return null;
@@ -594,9 +610,8 @@ public class ComputerUtil {
final Card target, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, sa);
if ((target != null) && target.getController() == ai) {
typeList.remove(target); // don't move the card we're pumping
}
// don't move the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);
if (typeList.size() < amount) {
return null;
@@ -718,12 +733,10 @@ public class ComputerUtil {
}
public static CardCollection chooseReturnType(final Player ai, final String type, final Card activate, final Card target, final int amount, SpellAbility sa) {
final CardCollection typeList =
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
if ((target != null) && target.getController() == ai) {
// don't bounce the card we're pumping
typeList.remove(target);
}
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
// don't bounce the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);
if (typeList.size() < amount) {
return new CardCollection();
@@ -743,7 +756,7 @@ public class ComputerUtil {
CardCollection remaining = new CardCollection(cardlist);
final CardCollection sacrificed = new CardCollection();
final Card host = source.getHostCard();
final int considerSacThreshold = getAIPreferenceParameter(host, "CreatureEvalThreshold");
final int considerSacThreshold = getAIPreferenceParameter(host, "CreatureEvalThreshold", source);
if ("OpponentOnly".equals(source.getParam("AILogic"))) {
if(!source.getActivatingPlayer().isOpponentOf(ai)) {
@@ -3026,6 +3039,6 @@ public class ComputerUtil {
}
}
return false;
}
}

View File

@@ -8,6 +8,8 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -20,6 +22,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
@@ -270,7 +273,10 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
int count = 0;
while (count < amount) {
@@ -320,11 +326,14 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
int count = 0;
while (count < amount) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList, sourceAbility);
if (prefCard == null) {
return false;
}
@@ -407,7 +416,7 @@ public class ComputerUtilCost {
* @return true, if successful
*/
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, sourceAbility,true);
return checkSacrificeCost(ai, cost, source, sourceAbility, true);
}
/**
@@ -420,8 +429,8 @@ public class ComputerUtilCost {
* @param cost
* @return a boolean.
*/
@Deprecated
public static boolean shouldPayCost(final Player ai, final Card hostCard, final Cost cost) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
if (!ai.cantLoseForZeroOrLessLife()) {
@@ -741,4 +750,12 @@ public class ComputerUtilCost {
}
return ObjectUtils.defaultIfNull(val, 0);
}
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
if (source.usesTargeting()) {
final CardCollection targets = new CardCollection(source.getTargets().getTargetCards());
choices = Iterables.filter(choices, Predicates.not(Predicates.and(CardPredicates.isController(ai), Predicates.in(targets))));
}
return new CardCollection(choices);
}
}

View File

@@ -64,11 +64,9 @@ public class CountersPutAi extends SpellAbilityAi {
*/
@Override
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
final String type = sa.getParam("CounterType");
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// TODO Auto-generated method stub
if (!super.willPayCosts(ai, sa, cost, source)) {
return false;
}
@@ -225,8 +223,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
if (sa.canTarget(ai)) {
// don't target itself when its forced to add poison
// counters too
// don't target itself when its forced to add poison counters too
if (!ai.getCounters().isEmpty()) {
if (!eachExisting || ai.getPoisonCounters() < 5) {
sa.getTargets().add(ai);
@@ -480,7 +477,6 @@ public class CountersPutAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
// don't put the counter on the dead creature
if (sacSelf && c.equals(source)) {
return false;
@@ -493,6 +489,8 @@ public class CountersPutAi extends SpellAbilityAi {
Card sacTarget = ComputerUtil.getCardPreference(ai, source, "SacCost", list);
// this card is planned to be sacrificed during cost payment, so don't target it
// (otherwise the AI can cheat by activating this SA and not paying the sac cost, e.g. Extruder)
// TODO needs update if amount > 1 gets printed,
// maybe also check putting the counter on that exact creature is more important than sacrificing it (though unlikely?)
list.remove(sacTarget);
}
@@ -617,7 +615,7 @@ public class CountersPutAi extends SpellAbilityAi {
// Instant +1/+1
if (type.equals("P1P1") && !SpellAbilityAi.isSorcerySpeed(sa)) {
if (!(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
return false; // only if next turn and cost is reusable
return false; // only if next turn and cost is reusable
}
}
}

View File

@@ -540,8 +540,7 @@ public class PumpAi extends PumpAiBase {
list = CardLists.getValidCards(list, tgt.getValidTgts(), ai, source, sa);
if (game.getStack().isEmpty()) {
// If the cost is tapping, don't activate before declare
// attack/block
// If the cost is tapping, don't activate before declare attack/block
if (sa.getPayCosts().hasTapCost()) {
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().isPlayerTurn(ai)) {

View File

@@ -23,7 +23,6 @@ public class SacrificeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
return sacrificeTgtAI(ai, sa);
}

View File

@@ -58,7 +58,6 @@ public class PaymentDecision {
return res;
}
public static PaymentDecision number(int c) {
return new PaymentDecision(c);
}
@@ -77,7 +76,6 @@ public class PaymentDecision {
return new PaymentDecision(null, manas, null, null, null);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/

View File

@@ -7,5 +7,6 @@ S:Mode$ Continuous | Affected$ Creature.ChosenType+YouCtrl | AddPower$ 1 | AddTo
AI:RemoveDeck:Random
SVar:PlayMain1:TRUE
SVar:AIPreference:SacCost$Creature.ChosenType+Other
SVar:AIPreferenceParams:CreatureEvalThreshold$ Targeted/Plus.10
A:AB$ Pump | Cost$ 1 Sac<1/Creature.ChosenType/creature of the chosen type> | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | KW$ Indestructible | SpellDescription$ Target creature you control gains indestructible until end of turn.
Oracle:As Etchings of the Chosen enters the battlefield, choose a creature type.\nCreatures you control of the chosen type get +1/+1.\n{1}, Sacrifice a creature of the chosen type: Target creature you control gains indestructible until end of turn.