From 3a8213fcca23a8bf4fb84108e26827d44f4ef4fb Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 21 May 2023 23:59:37 +0200 Subject: [PATCH] Chain comparators correctly --- .../src/main/java/forge/ai/AiController.java | 175 +---------------- .../java/forge/ai/ComputerUtilAbility.java | 183 ++++++++++++++++++ .../main/java/forge/ai/ComputerUtilCard.java | 14 +- 3 files changed, 204 insertions(+), 168 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 97066af0878..fb05d635995 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -655,10 +655,11 @@ public class AiController { List all = ComputerUtilAbility.getSpellAbilities(cards, player); try { - Collections.sort(all, saComparator); // put best spells first + Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first + ComputerUtilAbility.sortCreatureSpells(all); } catch (IllegalArgumentException ex) { System.err.println(ex.getMessage()); - String assertex = ComparatorUtil.verifyTransitivity(saComparator, all); + String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all); Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); } @@ -1016,167 +1017,6 @@ public class AiController { return false; } - // not sure "playing biggest spell" matters? - private final static Comparator saComparator = new Comparator() { - @Override - public int compare(final SpellAbility a, final SpellAbility b) { - // sort from highest cost to lowest - // we want the highest costs first - int a1 = a.getPayCosts().getTotalMana().getCMC(); - int b1 = b.getPayCosts().getTotalMana().getCMC(); - - // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs - if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { - return 1; - } else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) { - return -1; - } - - // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True - if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return 1; - } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return -1; - } - - // deprioritize pump spells with pure energy cost (can be activated last, - // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) - int a2 = 0, b2 = 0; - if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { - if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { - a2 = a.getPayCosts().getCostEnergy().convertAmount(); - } - } - if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { - if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { - b2 = b.getPayCosts().getCostEnergy().convertAmount(); - } - } - if (a2 == 0 && b2 > 0) { - return -1; - } else if (b2 == 0 && a2 > 0) { - return 1; - } - - // cast 0 mana cost spells first (might be a Mox) - if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) { - return -1; - } else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) { - return 1; - } - - if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) { - return -1; - } else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) { - return 1; - } - - if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { - // Cheaper Spectacle costs should be preferred - // 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) - if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) { - return 1; - } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) { - return 1; - } - } - - // If both are permanent creature spells, prefer the one that evaluates higher - if (a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { - int evalA = ComputerUtilCard.evaluateCreature(a); - int evalB = ComputerUtilCard.evaluateCreature(b); - if (evalA > evalB) { - a1 += Math.max(1, Math.round(evalA / 100.0f)); - } else if (evalB > evalA) { - b1 += Math.max(1, Math.round(evalB / 100.0f)); - } - } - - a1 += getSpellAbilityPriority(a); - b1 += getSpellAbilityPriority(b); - - return b1 - a1; - } - - private int getSpellAbilityPriority(SpellAbility sa) { - int p = 0; - Card source = sa.getHostCard(); - final Player ai = source == null ? sa.getActivatingPlayer() : source.getController(); - if (ai == null) { - System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa); - return 0; - } - final boolean noCreatures = ai.getCreaturesInPlay().isEmpty(); - - if (source != null) { - // puts creatures in front of spells - if (source.isCreature()) { - p += 1; - } - if (source.hasSVar("AIPriorityModifier")) { - p += Integer.parseInt(source.getSVar("AIPriorityModifier")); - } - if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) { - p -= 10; - } - // don't play equipments before having any creatures - if (source.isEquipment() && noCreatures) { - p -= 9; - } - // don't equip stuff in main 2 if there's more stuff to cast at the moment - if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { - p -= 1; - } - // 1. increase chance of using Surge effects - // 2. non-surged versions are usually inefficient - if (source.getOracleText().contains("surge cost") && !sa.isSurged()) { - p -= 9; - } - // move snap-casted spells to front - if (source.isInZone(ZoneType.Graveyard)) { - if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) { - p += 50; - } - } - // if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count - if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) { - p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS)); - } - } - - // use Surge and Prowl costs when able to - if (sa.isSurged() || sa.isProwl()) { - p += 9; - } - // sort planeswalker abilities with most costly first - if (sa.isPwAbility()) { - final CostPart cost = sa.getPayCosts().getCostParts().get(0); - if (cost instanceof CostRemoveCounter) { - p += cost.convertAmount() == null ? 1 : cost.convertAmount(); - } else if (cost instanceof CostPutCounter) { - p -= cost.convertAmount(); - } - if (sa.hasParam("Ultimate")) { - p += 9; - } - } - - if (ApiType.DestroyAll == sa.getApi()) { - p += 4; - } else if (ApiType.Mana == sa.getApi()) { - p -= 9; - } - - // try to cast mana ritual spells before casting spells to maximize potential mana - if ("ManaRitual".equals(sa.getParam("AILogic"))) { - p += 9; - } - - return p; - } - }; - public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa) { return getCardsToDiscard(numDiscard, uTypes, sa, CardCollection.EMPTY); } @@ -1744,10 +1584,11 @@ public class AiController { return null; try { - Collections.sort(all, saComparator); // put best spells first + Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first + ComputerUtilAbility.sortCreatureSpells(all); } catch (IllegalArgumentException ex) { System.err.println(ex.getMessage()); - String assertex = ComparatorUtil.verifyTransitivity(saComparator, all); + String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all); Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); } @@ -2292,14 +2133,14 @@ public class AiController { } // TODO move to more common place - private List filterList(List input, Predicate pred) { + private static List filterList(List input, Predicate pred) { List filtered = Lists.newArrayList(Iterables.filter(input, pred)); input.removeAll(filtered); return filtered; } // TODO move to more common place - private List filterListByApi(List input, ApiType type) { + public static List filterListByApi(List input, ApiType type) { return filterList(input, SpellAbilityPredicates.isApi(type)); } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index 3eca0f0fcad..df4e8b43f5c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -1,5 +1,7 @@ package forge.ai; +import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -15,6 +17,12 @@ import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; +import forge.game.cost.CostPart; +import forge.game.cost.CostPayEnergy; +import forge.game.cost.CostPutCounter; +import forge.game.cost.CostRemoveCounter; +import forge.game.keyword.Keyword; +import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.OptionalCostValue; import forge.game.spellability.SpellAbility; @@ -223,4 +231,179 @@ public class ComputerUtilAbility { } return true; } + + public final static saComparator saEvaluator = new saComparator(); + + // not sure "playing biggest spell" matters? + public final static class saComparator implements Comparator { + @Override + public int compare(final SpellAbility a, final SpellAbility b) { + return compareEvaluator(a, b, false); + } + public int compareEvaluator(final SpellAbility a, final SpellAbility b, boolean safeToEvaluateCreatures) { + // sort from highest cost to lowest + // we want the highest costs first + int a1 = a.getPayCosts().getTotalMana().getCMC(); + int b1 = b.getPayCosts().getTotalMana().getCMC(); + + // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs + if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) { + return 1; + } else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) { + return -1; + } + + // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True + if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { + return 1; + } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { + return -1; + } + + // deprioritize pump spells with pure energy cost (can be activated last, + // since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler) + int a2 = 0, b2 = 0; + if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) { + if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + a2 = a.getPayCosts().getCostEnergy().convertAmount(); + } + } + if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) { + if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { + b2 = b.getPayCosts().getCostEnergy().convertAmount(); + } + } + if (a2 == 0 && b2 > 0) { + return -1; + } else if (b2 == 0 && a2 > 0) { + return 1; + } + + // cast 0 mana cost spells first (might be a Mox) + if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) { + return -1; + } else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) { + return 1; + } + + if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) { + return -1; + } else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) { + return 1; + } + + if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) { + // Cheaper Spectacle costs should be preferred + // 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) + if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) { + return 1; + } else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) { + return 1; + } + } + + a1 += getSpellAbilityPriority(a); + b1 += getSpellAbilityPriority(b); + + int diff = b1 - a1; + + // If both are creature spells with roughly the same priority sort them after + if (safeToEvaluateCreatures && Math.abs(diff) < 4 && a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) { + return 0; + } + + return diff; + } + + private int getSpellAbilityPriority(SpellAbility sa) { + int p = 0; + Card source = sa.getHostCard(); + final Player ai = source == null ? sa.getActivatingPlayer() : source.getController(); + if (ai == null) { + System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa); + return 0; + } + final boolean noCreatures = ai.getCreaturesInPlay().isEmpty(); + + if (source != null) { + // puts creatures in front of spells + if (source.isCreature()) { + p += 1; + } + if (source.hasSVar("AIPriorityModifier")) { + p += Integer.parseInt(source.getSVar("AIPriorityModifier")); + } + if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) { + p -= 10; + } + // don't play equipments before having any creatures + if (source.isEquipment() && noCreatures) { + p -= 9; + } + // don't equip stuff in main 2 if there's more stuff to cast at the moment + if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + p -= 1; + } + // 1. increase chance of using Surge effects + // 2. non-surged versions are usually inefficient + if (source.getOracleText().contains("surge cost") && !sa.isSurged()) { + p -= 9; + } + // move snap-casted spells to front + if (source.isInZone(ZoneType.Graveyard)) { + if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) { + p += 50; + } + } + // if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count + if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) { + p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS)); + } + } + + // use Surge and Prowl costs when able to + if (sa.isSurged() || sa.isProwl()) { + p += 9; + } + // sort planeswalker abilities with most costly first + if (sa.isPwAbility()) { + final CostPart cost = sa.getPayCosts().getCostParts().get(0); + if (cost instanceof CostRemoveCounter) { + p += cost.convertAmount() == null ? 1 : cost.convertAmount(); + } else if (cost instanceof CostPutCounter) { + p -= cost.convertAmount(); + } + if (sa.hasParam("Ultimate")) { + p += 9; + } + } + + if (ApiType.DestroyAll == sa.getApi()) { + p += 4; + } else if (ApiType.Mana == sa.getApi()) { + p -= 9; + } + + // try to cast mana ritual spells before casting spells to maximize potential mana + if ("ManaRitual".equals(sa.getParam("AILogic"))) { + p += 9; + } + + return p; + } + }; + + public static List sortCreatureSpells(List all) { + List creatures = AiController.filterListByApi(Lists.newArrayList(all), ApiType.PermanentCreature); + Collections.sort(creatures, ComputerUtilCard.EvaluateCreatureSpellComparator); + int idx = 0; + for (int i = 0; i < all.size(); i++) { + if (all.get(i).getApi() == ApiType.PermanentCreature) { + all.set(i, creatures.get(idx)); + idx++; + } + } + return all; + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 464909d9148..ac1ecda800f 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -567,6 +567,18 @@ public class ComputerUtilCard { return evaluateCreature(b) - evaluateCreature(a); } }; + public static final Comparator EvaluateCreatureSpellComparator = new Comparator() { + @Override + public int compare(final SpellAbility a, final SpellAbility b) { + // only reorder if generic priorities can't decide + // TODO ideally we could reuse the value + int comp = ComputerUtilAbility.saEvaluator.compareEvaluator(a, b, true); + if (comp == 0) { + return evaluateCreature(b) - evaluateCreature(a); + } + return comp; + } + }; private static final CreatureEvaluator creatureEvaluator = new CreatureEvaluator(); private static final LandEvaluator landEvaluator = new LandEvaluator(); @@ -596,7 +608,7 @@ public class ComputerUtilCard { host.setState(sa.getCardStateName(), false); } - int eval = creatureEvaluator.evaluateCreature(host); + int eval = evaluateCreature(host); if (currentState != null) { host.setState(currentState, false);