mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-16 02:38:02 +00:00
added basic manabase evaluation
This commit is contained in:
119
forge-ai/src/main/java/forge/ai/AIDeckStatistics.java
Normal file
119
forge-ai/src/main/java/forge/ai/AIDeckStatistics.java
Normal 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user