diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index a14c00c48ea..a3f8900b085 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -23,11 +23,14 @@ import forge.ai.AiCardMemory.MemorySet; import forge.ai.ability.ChangeZoneAi; import forge.ai.ability.LearnAi; +import forge.ai.simulation.GameStateEvaluator; import forge.ai.simulation.SpellAbilityPicker; import forge.card.CardStateName; import forge.card.CardType; import forge.card.MagicColor; +import forge.card.mana.ManaAtom; import forge.card.mana.ManaCost; +import forge.card.mana.ManaCostShard; import forge.deck.Deck; import forge.deck.DeckSection; import forge.game.*; @@ -71,6 +74,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static forge.ai.ComputerUtilMana.getAvailableManaEstimate; +import static java.lang.Math.max; + /** *

* AiController class. @@ -534,8 +540,10 @@ private Card chooseBestLandToPlay(CardCollection landList) { landList = unreflectedLands; } - //try to skip lands that enter the battlefield tapped + // try to skip lands that enter the battlefield tapped if we might want to play something this turn if (!nonLandsInHand.isEmpty()) { + // get the tapped and non-tapped lands + CardCollection tappedLands = new CardCollection(); CardCollection nonTappedLands = new CardCollection(); for (Card land : landList) { // check replacement effects if land would enter tapped or not @@ -570,11 +578,48 @@ private Card chooseBestLandToPlay(CardCollection landList) { nonTappedLands.add(land); } + + // if we have the choice, see if we can play an untapped land if (!nonTappedLands.isEmpty()) { - landList = nonTappedLands; + // get the costs of the nonland cards in hand and the mana we have available. + // If adding one won't make something new castable, then pick a tapland. + int max_inc = 0; + for (Card c : nonTappedLands) { + max_inc = max(max_inc, c.getMaxManaProduced()); + } + // If we have a lot of mana, prefer untapped lands. + // We're either topdecking or have drawn enough the tempo no longer matters. + int mana_available = getAvailableManaEstimate(player); + if (mana_available > 6) { + landList = nonTappedLands; + } + // check for lands with no mana abilities + else if (max_inc > 0) { + boolean found = false; + for (Card c : nonLandsInHand) { + // TODO make this work better with split cards and Monocolored Hybrid + ManaCost cost = c.getManaCost(); + // check for incremental cmc + // check for X cost spells + if (cost.getCMC() == max_inc + mana_available || + (cost.getShardCount(ManaCostShard.X) > 0 && cost.getCMC() >= mana_available)) { + found = true; + break; + } + } + + if (found) { + landList = nonTappedLands; + } + } } } + // Early out if we only have one card left + if (landList.size() == 1) { + return landList.get(0); + } + // Choose first land to be able to play a one drop if (player.getLandsInPlay().isEmpty()) { CardCollection oneDrops = CardLists.filter(nonLandsInHand, CardPredicates.hasCMC(1)); @@ -595,40 +640,86 @@ private Card chooseBestLandToPlay(CardCollection landList) { } } - //play lands with a basic type that is needed the most + // play lands with a basic type and/or color that is needed the most final CardCollectionView landsInBattlefield = player.getCardsIn(ZoneType.Battlefield); final List basics = Lists.newArrayList(); + // what colors are available? + int[] counts = new int[6]; // in WUBRGC order + + for (Card c : player.getCardsIn(ZoneType.Battlefield)) { + for (SpellAbility m: c.getManaAbilities()) { + m.setActivatingPlayer(c.getController()); + for (AbilityManaPart mp : m.getAllManaParts()) { + for (String part : mp.mana(m).split(" ")) { + // TODO handle any + int index = ManaAtom.getIndexFromName(part); + if (index != -1) { + counts[index] += 1; + } + } + } + } + } + // what types can I go get? + int[] basic_counts = new int[5]; // in WUBRG order for (final String name : MagicColor.Constant.BASIC_LANDS) { if (!CardLists.getType(landList, name).isEmpty()) { basics.add(name); } } if (!basics.isEmpty()) { - // Which basic land is least available - int minSize = Integer.MAX_VALUE; - String minType = null; - - for (String b : basics) { + for (int i = 0; i < MagicColor.Constant.BASIC_LANDS.size(); i++) { + String b = MagicColor.Constant.BASIC_LANDS.get(i); final int num = CardLists.getType(landsInBattlefield, b).size(); - if (num < minSize) { - minType = b; - minSize = num; - } - } + basic_counts[i] = num; + } + } + // pick the land with the best score. + // use the evaluation plus a modifier for each new color pip and basic type + Card toReturn = Aggregates.itemWithMax(IterableUtil.filter(landList, Card::hasPlayableLandFace), + (card -> { + // base score is for the evaluation score + int score = GameStateEvaluator.evaluateLand(card); + // add for new basic type + for (String cardType: card.getType()) { + int index = MagicColor.Constant.BASIC_LANDS.indexOf(cardType); + if (index != -1 && basic_counts[index] == 0) { + score += 25; + } + } - if (minType != null) { - landList = CardLists.getType(landList, minType); - } - // pick dual lands if available - if (landList.anyMatch(CardPredicates.NONBASIC_LANDS)) { - landList = CardLists.filter(landList, CardPredicates.NONBASIC_LANDS); - } - } - return ComputerUtilCard.getBestLandToPlayAI(landList); - } + // TODO handle fetchlands and what they can fetch for + // determine new color pips + int[] card_counts = new int[6]; // in WUBRGC order + for (SpellAbility m: card.getManaAbilities()) { + m.setActivatingPlayer(card.getController()); + for (AbilityManaPart mp : m.getAllManaParts()) { + for (String part : mp.mana(m).split(" ")) { + // TODO handle any + int index = ManaAtom.getIndexFromName(part); + if (index != -1) { + card_counts[index] += 1; + } + } + } + } + + // use 1 / x+1 for diminishing returns + // TODO use max pips of each color in the deck from deck statistics to weight this + for (int i = 0; i < card_counts.length; i++) { + int diff = (card_counts[i] * 50) / (counts[i] + 1); + score += diff; + } + + // TODO utility lands only if we have enough to pay their costs + // TODO Tron lands and other lands that care about land counts + + return score; + })); + return toReturn; } // if return true, go to next phase private SpellAbility chooseCounterSpell(final List possibleCounters) { @@ -1047,7 +1138,7 @@ private boolean canPlaySpellWithoutBuyback(Card card, SpellAbility sa) { neededMana = 0; } - int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false); + int hasMana = getAvailableManaEstimate(player, false); if (hasMana < neededMana - 1) { return true; } @@ -1456,7 +1547,7 @@ private boolean isSafeToHoldLandDropForMain2(Card landToPlay) { int minCMCInHand = Aggregates.min(inHand, Card::getCMC); if (minCMCInHand == Integer.MAX_VALUE) minCMCInHand = 0; - int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true); + int predictedMana = getAvailableManaEstimate(player, true); boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && minCMCInHand > 0 && !isTapLand; boolean cantCastAnythingNow = predictedMana < minCMCInHand; @@ -1979,7 +2070,7 @@ public int attemptToAssist(SpellAbility sa, int max, int request) { } // AI has decided to help. Now let's figure out how much they can help - int mana = ComputerUtilMana.getAvailableManaEstimate(player, true); + int mana = getAvailableManaEstimate(player, true); // TODO We should make a logical guess here, but for now just uh yknow randomly decide? // What do I want to play next? Can I still pay for that and have mana left over to help? diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java index 2c2bf24ae83..979c61052fc 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java @@ -177,7 +177,6 @@ public int evalManaBase(Game game, Player player, AiDeckStatistics statistics) { // TODO should these be fixed quantities or should they be linear out of like 1000/(desired - total)? int value = 0; // get the colors of mana we can produce and the maximum number of pips - int max_colored = 0; int max_total = 0; // this logic taken from ManaCost.getColorShardCounts() int[] counts = new int[6]; // in WUBRGC order diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 79b2571d951..5d85d6b612d 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -64,6 +64,8 @@ import java.util.*; import java.util.Map.Entry; +import static java.lang.Math.max; + /** *

* Card class. @@ -1751,7 +1753,7 @@ public final int subtractCounter(final CounterType counterName, final int n, fin public final int subtractCounter(final CounterType counterName, final int n, final Player remover, final boolean isDamage) { int oldValue = getCounters(counterName); - int newValue = Math.max(oldValue - n, 0); + int newValue = max(oldValue - n, 0); final Map repParams = AbilityKey.mapFromAffected(this); repParams.put(AbilityKey.CounterType, counterName); @@ -3361,6 +3363,16 @@ public final boolean canProduceSameManaTypeWith(final Card c) { return canProduceColorMana(colors); } + public final int getMaxManaProduced() { + int max_produced = 0; + for (SpellAbility m: getManaAbilities()) { + m.setActivatingPlayer(getController()); + int mana_cost = m.getPayCosts().getTotalMana().getCMC(); + max_produced = max(max_produced, m.amountOfManaGenerated(true) - mana_cost); + } + return max_produced; + } + public final void clearFirstSpell() { currentState.clearFirstSpell(); } @@ -6184,7 +6196,7 @@ public final int getExcessDamageValue(boolean withDeathtouch) { if (withDeathtouch && lethal > 0) { excessCharacteristics.add(1); } else { - excessCharacteristics.add(Math.max(0, lethal)); + excessCharacteristics.add(max(0, lethal)); } } if (this.isPlaneswalker()) {