mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-16 10:48:00 +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;
|
package forge.ai.simulation;
|
||||||
|
|
||||||
|
import forge.ai.AIDeckStatistics;
|
||||||
import forge.ai.CreatureEvaluator;
|
import forge.ai.CreatureEvaluator;
|
||||||
|
import forge.card.mana.ManaAtom;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CounterEnumType;
|
import forge.game.card.CounterEnumType;
|
||||||
@@ -17,6 +19,7 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
public class GameStateEvaluator {
|
public class GameStateEvaluator {
|
||||||
private boolean debugging = false;
|
private boolean debugging = false;
|
||||||
@@ -113,6 +116,7 @@ public class GameStateEvaluator {
|
|||||||
score += myCards - aiPlayer.getMaxHandSize();
|
score += myCards - aiPlayer.getMaxHandSize();
|
||||||
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;
|
score += 5 * myCards - 4 * theirCards;
|
||||||
debugPrint(" My life: " + aiPlayer.getLife());
|
debugPrint(" My life: " + aiPlayer.getLife());
|
||||||
score += 2 * aiPlayer.getLife();
|
score += 2 * aiPlayer.getLife();
|
||||||
@@ -126,16 +130,13 @@ public class GameStateEvaluator {
|
|||||||
score -= 2* opponentLife / (game.getPlayers().size() - 1);
|
score -= 2* opponentLife / (game.getPlayers().size() - 1);
|
||||||
|
|
||||||
// evaluate mana base quality
|
// evaluate mana base quality
|
||||||
score += evalManaBase(game, aiPlayer);
|
score += evalManaBase(game, aiPlayer, AIDeckStatistics.fromPlayer(aiPlayer));
|
||||||
int opponentManaScore = 0;
|
// TODO deal with opponents. Do we want to use perfect information to evaluate their manabase?
|
||||||
for (Player opponent : aiPlayer.getOpponents()) {
|
// int opponentManaScore = 0;
|
||||||
opponentManaScore += evalManaBase(game, opponent);
|
// for (Player opponent : aiPlayer.getOpponents()) {
|
||||||
}
|
// opponentManaScore += evalManaBase(game, opponent);
|
||||||
score -= opponentManaScore / (game.getPlayers().size() - 1);
|
// }
|
||||||
|
// 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
|
|
||||||
|
|
||||||
// TODO evaluate holding mana open for counterspells
|
// TODO evaluate holding mana open for counterspells
|
||||||
|
|
||||||
@@ -169,7 +170,9 @@ public class GameStateEvaluator {
|
|||||||
return new Score(score, summonSickScore);
|
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
|
// get the colors of mana we can produce and the maximum number of pips
|
||||||
int max_colored = 0;
|
int max_colored = 0;
|
||||||
int max_total = 0;
|
int max_total = 0;
|
||||||
@@ -178,24 +181,37 @@ public class GameStateEvaluator {
|
|||||||
|
|
||||||
for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
|
for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
|
||||||
int max_produced = 0;
|
int max_produced = 0;
|
||||||
Set<String> colors_produced = new HashSet<>();
|
|
||||||
for (SpellAbility m: c.getManaAbilities()) {
|
for (SpellAbility m: c.getManaAbilities()) {
|
||||||
m.setActivatingPlayer(c.getController());
|
m.setActivatingPlayer(c.getController());
|
||||||
int mana_cost = m.getPayCosts().getTotalMana().getCMC();
|
int mana_cost = m.getPayCosts().getTotalMana().getCMC();
|
||||||
max_produced = max(max_produced, m.amountOfManaGenerated(true) - mana_cost);
|
max_produced = max(max_produced, m.amountOfManaGenerated(true) - mana_cost);
|
||||||
for (AbilityManaPart mp : m.getAllManaParts()) {
|
for (AbilityManaPart mp : m.getAllManaParts()) {
|
||||||
colors_produced.addAll(Arrays.asList(mp.mana(m).split(" ")));
|
|
||||||
for (String part : 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;
|
max_total += max_produced;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare against the maximums in the deck and in the hand
|
// 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) {
|
public int evalCard(Game game, Player aiPlayer, Card c) {
|
||||||
|
|||||||
@@ -373,29 +373,6 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
|
|||||||
AssertJUnit.assertEquals(desired, sa.getHostCard());
|
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
|
@Test
|
||||||
public void targetUtilityLandOverRainbow() {
|
public void targetUtilityLandOverRainbow() {
|
||||||
Game game = initAndCreateGame();
|
Game game = initAndCreateGame();
|
||||||
@@ -427,10 +404,28 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
|
|||||||
System.out.println("Adding lands to hand");
|
System.out.println("Adding lands to hand");
|
||||||
|
|
||||||
// add every land to the player's hand
|
// add every land to the player's hand
|
||||||
// SimulationController.MAX_DEPTH = 0;
|
|
||||||
List<Card> funky = new ArrayList<>();
|
List<Card> funky = new ArrayList<>();
|
||||||
String previous = "";
|
String previous = "";
|
||||||
for (PaperCard c : FModel.getMagicDb().getCommonCards().getAllCards()) {
|
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
|
// reset the game
|
||||||
Game game = resetGame();
|
Game game = resetGame();
|
||||||
Player p = game.getPlayers().get(1);
|
Player p = game.getPlayers().get(1);
|
||||||
@@ -453,36 +448,22 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
|
|||||||
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
|
||||||
game.getAction().checkStateEffects(true);
|
game.getAction().checkStateEffects(true);
|
||||||
|
|
||||||
// Only add one version at a time
|
// Add the target card to the hand and test it
|
||||||
if (c.getName().equals(previous)) {
|
addCardToZone(c.getName(), p, ZoneType.Hand);
|
||||||
continue;
|
|
||||||
}
|
GameStateEvaluator.Score s = new GameStateEvaluator().getScoreForGameState(game, p);
|
||||||
previous = c.getName();
|
System.out.println("Starting score: " + s);
|
||||||
if (c.getRules().getType().isLand()) {
|
SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
|
||||||
// Skip glacial chasm, it's really weird.
|
List<SpellAbility> candidateSAs = picker.getCandidateSpellsAndAbilities();
|
||||||
if (c.getName().equals("Glacial Chasm")) {
|
for (int i = 0; i < candidateSAs.size(); i++) {
|
||||||
System.out.println("Skipping " + c.getName());
|
SpellAbility sa = candidateSAs.get(i);
|
||||||
|
if (sa.isActivatedAbility()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
GameStateEvaluator.Score value = picker.evaluateSa(new SimulationController(s), game.getPhaseHandler().getPhase(), candidateSAs, i);
|
||||||
addCardToZone(c.getName(), p, ZoneType.Hand);
|
System.out.println("sa: " + sa.getHostCard() + ", value: " + value);
|
||||||
|
if (!(value.value > s.value)) {
|
||||||
// Once the card has been added to the hand, test it
|
funky.add(sa.getHostCard());
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user