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()) {