From 502d7946ddaf16aecccf17bbc4dc80e3f3640f56 Mon Sep 17 00:00:00 2001 From: marthinwurer Date: Tue, 7 Feb 2023 23:38:50 -0700 Subject: [PATCH] added basic manabase evaluation --- .../main/java/forge/ai/AIDeckStatistics.java | 119 ++++++++++++++++++ .../ai/simulation/GameStateEvaluator.java | 46 ++++--- .../SpellAbilityPickerSimulationTest.java | 85 +++++-------- 3 files changed, 183 insertions(+), 67 deletions(-) create mode 100644 forge-ai/src/main/java/forge/ai/AIDeckStatistics.java diff --git a/forge-ai/src/main/java/forge/ai/AIDeckStatistics.java b/forge-ai/src/main/java/forge/ai/AIDeckStatistics.java new file mode 100644 index 00000000000..1bd567a64de --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/AIDeckStatistics.java @@ -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 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 rules_list = new ArrayList<>(); + for (final Map.Entry deckEntry : deck) { + switch (deckEntry.getKey()) { + case Main: + case Commander: + for (final Map.Entry 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 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); + + } + +} 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 348b1f24af0..32998d0bd79 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java @@ -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 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) { diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java index 8aeae5ceb33..50131064c3b 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/SpellAbilityPickerSimulationTest.java @@ -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 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 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 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()); } } }