added basic manabase evaluation

This commit is contained in:
marthinwurer
2023-02-07 23:38:50 -07:00
parent 56460ae534
commit 502d7946dd
3 changed files with 183 additions and 67 deletions

View File

@@ -0,0 +1,119 @@
package forge.ai;
import forge.card.CardRules;
import forge.card.CardType;
import forge.deck.CardPool;
import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.item.PaperCard;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class AIDeckStatistics {
public float averageCMC = 0;
public float stddevCMC = 0;
public int maxCost = 0;
public int maxColoredCost = 0;
// in WUBRGC order from ManaCost.getColorShardCounts()
public int[] maxPips = null;
// public int[] numSources = new int[6];
public int numLands = 0;
public AIDeckStatistics(float averageCMC, float stddevCMC, int maxCost, int maxColoredCost, int[] maxPips, int numLands) {
this.averageCMC = averageCMC;
this.stddevCMC = stddevCMC;
this.maxCost = maxCost;
this.maxColoredCost = maxColoredCost;
this.maxPips = maxPips;
this.numLands = numLands;
}
public static AIDeckStatistics fromCardList(List<CardRules> cards) {
int totalCMC = 0;
int totalCount = 0;
int numLands = 0;
int maxCost = 0;
int[] maxPips = new int[6];
int maxColoredCost = 0;
for (CardRules rules : cards) {
CardType type = rules.getType();
if (type.isLand()) {
numLands += 1;
} else {
int cost = rules.getManaCost().getCMC();
// TODO use alternate casting costs for this, free spells will usually be cast for free
maxCost = Math.max(maxCost, cost);
totalCMC += cost;
totalCount++;
int[] pips = rules.getManaCost().getColorShardCounts();
int colored_pips = 0;
for (int i = 0; i < pips.length; i++) {
maxPips[i] = Math.max(maxPips[i], pips[i]);
if (i < 5) {
colored_pips += pips[i];
}
}
maxColoredCost = Math.max(maxColoredCost, colored_pips);
}
// TODO implement the number of mana sources
// find the sources
// What about non-mana-ability mana sources?
// fetchlands, ramp spells, etc
}
return new AIDeckStatistics(totalCount == 0 ? 0 : totalCMC / (float)totalCount,
0, // TODO use https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
maxCost,
maxColoredCost,
maxPips,
numLands
);
}
public static AIDeckStatistics fromDeck(Deck deck) {
List<CardRules> rules_list = new ArrayList<>();
for (final Map.Entry<DeckSection, CardPool> deckEntry : deck) {
switch (deckEntry.getKey()) {
case Main:
case Commander:
for (final Map.Entry<PaperCard, Integer> poolEntry : deckEntry.getValue()) {
CardRules rules = poolEntry.getKey().getRules();
rules_list.add(rules);
}
break;
default:
break; //ignore other sections
}
}
return fromCardList(rules_list);
}
public static AIDeckStatistics fromPlayer(Player player) {
Deck deck = player.getRegisteredPlayer().getDeck();
if (deck.isEmpty()) {
// we're in a test or some weird match, search through the hand and library and build the decklist
List<CardRules> rules_list = new ArrayList<>();
for (Card c : player.getAllCards()) {
if (c.getPaperCard() == null) {
continue;
}
rules_list.add(c.getRules());
}
return fromCardList(rules_list);
}
return fromDeck(deck);
}
}

View File

@@ -1,6 +1,8 @@
package forge.ai.simulation;
import forge.ai.AIDeckStatistics;
import forge.ai.CreatureEvaluator;
import forge.card.mana.ManaAtom;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
@@ -17,6 +19,7 @@ import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.max;
import static java.lang.Math.min;
public class GameStateEvaluator {
private boolean debugging = false;
@@ -113,6 +116,7 @@ public class GameStateEvaluator {
score += myCards - aiPlayer.getMaxHandSize();
myCards = aiPlayer.getMaxHandSize();
}
// TODO weight cards in hand more if opponent has discard or if we have looting or can bluff a trick
score += 5 * myCards - 4 * theirCards;
debugPrint(" My life: " + aiPlayer.getLife());
score += 2 * aiPlayer.getLife();
@@ -126,16 +130,13 @@ public class GameStateEvaluator {
score -= 2* opponentLife / (game.getPlayers().size() - 1);
// evaluate mana base quality
score += evalManaBase(game, aiPlayer);
int opponentManaScore = 0;
for (Player opponent : aiPlayer.getOpponents()) {
opponentManaScore += evalManaBase(game, opponent);
}
score -= opponentManaScore / (game.getPlayers().size() - 1);
// get the colors of mana we can produce and the maximum number of pips
// Compare against the maximums in the deck
score += evalManaBase(game, aiPlayer, AIDeckStatistics.fromPlayer(aiPlayer));
// TODO deal with opponents. Do we want to use perfect information to evaluate their manabase?
// int opponentManaScore = 0;
// for (Player opponent : aiPlayer.getOpponents()) {
// opponentManaScore += evalManaBase(game, opponent);
// }
// score -= opponentManaScore / (game.getPlayers().size() - 1);
// TODO evaluate holding mana open for counterspells
@@ -169,7 +170,9 @@ public class GameStateEvaluator {
return new Score(score, summonSickScore);
}
public int evalManaBase(Game game, Player player) {
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;
@@ -178,24 +181,37 @@ public class GameStateEvaluator {
for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
int max_produced = 0;
Set<String> colors_produced = new HashSet<>();
for (SpellAbility m: c.getManaAbilities()) {
m.setActivatingPlayer(c.getController());
int mana_cost = m.getPayCosts().getTotalMana().getCMC();
max_produced = max(max_produced, m.amountOfManaGenerated(true) - mana_cost);
for (AbilityManaPart mp : m.getAllManaParts()) {
colors_produced.addAll(Arrays.asList(mp.mana(m).split(" ")));
for (String part : mp.mana(m).split(" ")) {
counts[ManaAtom.getIndexFromName(part)] += 1;
// TODO handle any
int index = ManaAtom.getIndexFromName(part);
if (index != -1) {
counts[index] += 1;
}
}
}
}
max_total += max_produced;
}
// Compare against the maximums in the deck and in the hand
// TODO check number of castable cards in hand
for (int i = 0; i < counts.length; i++) {
// for each color pip, add 100
value += Math.min(counts[i], statistics.maxPips[i]) * 100;
}
// value for being able to cast all the cards in your deck
value += min(max_total, statistics.maxCost) * 100;
return max_total * 50;
// excess mana is valued less than getting enough to use everything
value += max(0, max_total - statistics.maxCost) * 5;
return value;
}
public int evalCard(Game game, Player aiPlayer, Card c) {

View File

@@ -373,29 +373,6 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
AssertJUnit.assertEquals(desired, sa.getHostCard());
}
@Test
public void targetRainbowLandOverDual() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
Player opponent = game.getPlayers().get(0);
opponent.setLife(20, null);
// start with the opponent having a basic land, a dual, and a rainbow
addCard("Forest", opponent);
addCard("Breeding Pool", opponent);
Card desired = addCard("Mana Confluence", opponent);
addCard("Strip Mine", p);
// It doesn't want to use strip mine in main
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_BLOCKERS, p);
game.getAction().checkStateEffects(true);
// ensure that the land is played
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
SpellAbility sa = picker.chooseSpellAbilityToPlay(null);
AssertJUnit.assertEquals(desired, sa.getTargetCard());
}
@Test
public void targetUtilityLandOverRainbow() {
Game game = initAndCreateGame();
@@ -427,10 +404,28 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
System.out.println("Adding lands to hand");
// add every land to the player's hand
// SimulationController.MAX_DEPTH = 0;
List<Card> funky = new ArrayList<>();
String previous = "";
for (PaperCard c : FModel.getMagicDb().getCommonCards().getAllCards()) {
// Only test one version of a card
if (c.getName().equals(previous)) {
continue;
}
previous = c.getName();
// skip nonland cards
if (!c.getRules().getType().isLand()) {
continue;
}
// System.out.println(c.getName());
// Skip glacial chasm, it's really weird.
if (c.getName().equals("Glacial Chasm")) {
System.out.println("Skipping " + c.getName());
continue;
}
// reset the game
Game game = resetGame();
Player p = game.getPlayers().get(1);
@@ -453,36 +448,22 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
game.getAction().checkStateEffects(true);
// Only add one version at a time
if (c.getName().equals(previous)) {
continue;
}
previous = c.getName();
if (c.getRules().getType().isLand()) {
// Skip glacial chasm, it's really weird.
if (c.getName().equals("Glacial Chasm")) {
System.out.println("Skipping " + c.getName());
// Add the target card to the hand and test it
addCardToZone(c.getName(), p, ZoneType.Hand);
GameStateEvaluator.Score s = new GameStateEvaluator().getScoreForGameState(game, p);
System.out.println("Starting score: " + s);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
List<SpellAbility> candidateSAs = picker.getCandidateSpellsAndAbilities();
for (int i = 0; i < candidateSAs.size(); i++) {
SpellAbility sa = candidateSAs.get(i);
if (sa.isActivatedAbility()) {
continue;
}
addCardToZone(c.getName(), p, ZoneType.Hand);
// Once the card has been added to the hand, test it
GameStateEvaluator.Score s = new GameStateEvaluator().getScoreForGameState(game, p);
System.out.println("Starting score: " + s);
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
List<SpellAbility> candidateSAs = picker.getCandidateSpellsAndAbilities();
for (int i = 0; i < candidateSAs.size(); i++) {
SpellAbility sa = candidateSAs.get(i);
if (sa.isActivatedAbility()) {
continue;
}
GameStateEvaluator.Score value = picker.evaluateSa(new SimulationController(s), game.getPhaseHandler().getPhase(), candidateSAs, i);
System.out.println("sa: " + sa.getHostCard() + ", value: " + value);
if (!(value.value > s.value)) {
funky.add(sa.getHostCard());
}
GameStateEvaluator.Score value = picker.evaluateSa(new SimulationController(s), game.getPhaseHandler().getPhase(), candidateSAs, i);
System.out.println("sa: " + sa.getHostCard() + ", value: " + value);
if (!(value.value > s.value)) {
funky.add(sa.getHostCard());
}
}
}