mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-11 16:26:22 +00:00
Improved AI tapland playing (#6751)
This commit is contained in:
@@ -23,11 +23,14 @@ import com.google.common.collect.Lists;
|
||||
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.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static forge.ai.ComputerUtilMana.getAvailableManaEstimate;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* AiController class.
|
||||
@@ -534,8 +540,10 @@ public class AiController {
|
||||
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 @@ public class AiController {
|
||||
|
||||
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 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
|
||||
//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<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
basic_counts[i] = num;
|
||||
}
|
||||
}
|
||||
return ComputerUtilCard.getBestLandToPlayAI(landList);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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<SpellAbility> possibleCounters) {
|
||||
@@ -1047,7 +1138,7 @@ public class AiController {
|
||||
neededMana = 0;
|
||||
}
|
||||
|
||||
int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false);
|
||||
int hasMana = getAvailableManaEstimate(player, false);
|
||||
if (hasMana < neededMana - 1) {
|
||||
return true;
|
||||
}
|
||||
@@ -1456,7 +1547,7 @@ public class AiController {
|
||||
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 class AiController {
|
||||
}
|
||||
|
||||
// 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?
|
||||
|
||||
@@ -177,7 +177,6 @@ public class GameStateEvaluator {
|
||||
// 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
|
||||
|
||||
@@ -64,6 +64,8 @@ import org.apache.commons.lang3.tuple.Triple;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Card class.
|
||||
@@ -1751,7 +1753,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
|
||||
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<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
|
||||
repParams.put(AbilityKey.CounterType, counterName);
|
||||
@@ -3361,6 +3363,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
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 class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
||||
if (withDeathtouch && lethal > 0) {
|
||||
excessCharacteristics.add(1);
|
||||
} else {
|
||||
excessCharacteristics.add(Math.max(0, lethal));
|
||||
excessCharacteristics.add(max(0, lethal));
|
||||
}
|
||||
}
|
||||
if (this.isPlaneswalker()) {
|
||||
|
||||
Reference in New Issue
Block a user