Streamline logic so canPlay always before canPay (#8234)

This commit is contained in:
tool4ever
2025-07-29 09:35:18 +02:00
committed by GitHub
parent 7428b7420a
commit f5e96bc756
4 changed files with 20 additions and 43 deletions

View File

@@ -911,21 +911,8 @@ public class AiController {
}
}
int oldCMC = -1;
boolean xCost = sa.costHasX() || host.hasKeyword(Keyword.STRIVE) || sa.getApi() == ApiType.Charm;
if (!xCost) {
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
return AiPlayDecision.CantAfford;
}
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
if (!sa.getAllTargetChoices().isEmpty()) {
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
}
}
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
// this is the "heaviest" check, which also sets up targets, defines X, etc.
AiPlayDecision canPlay = canPlaySa(sa);
if (canPlay != AiPlayDecision.WillPlay) {
return canPlay;
@@ -940,9 +927,6 @@ public class AiController {
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
xCost |= wardCost.getTotalMana().getCMC() > 0;
}
SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
return AiPlayDecision.CostNotAcceptable;
@@ -952,15 +936,7 @@ public class AiController {
}
}
// check if some target raised cost
if (!xCost && oldCMC > -1) {
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
if (finalCMC > oldCMC) {
xCost = true;
}
}
if (xCost && !ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
return AiPlayDecision.CantAfford;
@@ -973,8 +949,6 @@ public class AiController {
return AiPlayDecision.CantAfford;
}
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
// are willing to play the SA
return AiPlayDecision.WillPlay;
}

View File

@@ -666,6 +666,7 @@ public class ComputerUtilMana {
return true;
}
int phyLifeToPay = 2;
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
boolean hasConverge = sa.getHostCard().hasConverge();
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test, checkPlayable, hasConverge);
@@ -693,13 +694,12 @@ public class ComputerUtilMana {
}
if (sourcesForShards == null && !purePhyrexian) {
break; // no mana abilities to use for paying
// no mana abilities to use for paying
break;
}
toPay = getNextShardToPay(cost, sourcesForShards);
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
Collection<SpellAbility> saList = null;
if (hasConverge &&
(toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) {
@@ -752,9 +752,14 @@ public class ComputerUtilMana {
}
if (saPayment == null) {
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)
|| (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
break; // cannot pay
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(phyLifeToPay, false, sa)
|| (ai.getLife() <= phyLifeToPay && !ai.cantLoseForZeroOrLessLife())) {
// cannot pay
break;
}
if (test) {
phyLifeToPay += 2;
}
if (sa.hasParam("AIPhyrexianPayment")) {

View File

@@ -551,11 +551,11 @@ public class ManaCostBeingPaid {
// The generic portion of a 2/Colored mana, should be lower priority than generic mana
return !ColorSet.fromMask(bill.getColorMask() & paymentColor).isColorless() ? 9 : 1;
}
if (!bill.isPhyrexian()) {
return 10;
}
if (bill.isPhyrexian()) {
return 8;
}
return 10;
}
return 5;
}

View File

@@ -47,13 +47,11 @@ public class InputPayManaOfCostPayment extends InputPayMana {
if (manaCost.payPhyrexian()) {
saPaidFor.setSpendPhyrexianMana(true);
this.phyLifeToLose += 2;
} else {
if (player.hasKeyword("PayLifeInsteadOf:B") && manaCost.hasAnyKind(ManaAtom.BLACK)) {
} else if (player.hasKeyword("PayLifeInsteadOf:B") && manaCost.hasAnyKind(ManaAtom.BLACK)) {
manaCost.decreaseShard(ManaCostShard.BLACK, 1);
this.phyLifeToLose += 2;
}
}
}
this.showMessage();
}