mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 20:28:00 +00:00
Make a helper class for evaluating a creature, so that it can be more easily customized through subclassing.
This commit is contained in:
@@ -364,6 +364,9 @@ public class ComputerUtilCard {
|
||||
return ComputerUtilCard.evaluateCreature(b) - ComputerUtilCard.evaluateCreature(a);
|
||||
}
|
||||
};
|
||||
|
||||
private static final CreatureEvaluator creatureEvaluator = new CreatureEvaluator();
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* evaluateCreature.
|
||||
@@ -374,200 +377,7 @@ public class ComputerUtilCard {
|
||||
* @return a int.
|
||||
*/
|
||||
public static int evaluateCreature(final Card c) {
|
||||
return evaluateCreatureDebug(c, null);
|
||||
}
|
||||
private static int addValue(int value, Function<String, Void> out, String text) {
|
||||
if (out != null && value != 0) {
|
||||
out.apply(value + " via " + text);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
private static int subValue(int value, Function<String, Void> out, String text) {
|
||||
return -addValue(-value, out, text);
|
||||
}
|
||||
public static int evaluateCreatureDebug(final Card c, Function<String, Void> out) {
|
||||
int value = 80;
|
||||
if (!c.isToken()) {
|
||||
value += addValue(80, out, "non-token"); // tokens should be worth less than actual cards
|
||||
}
|
||||
int power = c.getNetCombatDamage();
|
||||
final int toughness = c.getNetToughness();
|
||||
for (String keyword : c.getKeywords()) {
|
||||
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
|
||||
power = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
value += addValue(power * 15, out, "power");
|
||||
value += addValue(toughness * 10, out, "toughness");
|
||||
value += addValue(c.getCMC() * 5, out, "cmc");
|
||||
|
||||
// Evasion keywords
|
||||
if (c.hasKeyword("Flying")) {
|
||||
value += addValue(power * 10, out, "flying");
|
||||
}
|
||||
if (c.hasKeyword("Horsemanship")) {
|
||||
value += addValue(power * 10, out, "horses");
|
||||
}
|
||||
if (c.hasKeyword("Unblockable")) {
|
||||
value += addValue(power * 10, out, "unblockable");
|
||||
} else {
|
||||
if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||
value += addValue(power * 6, out, "thorns");
|
||||
}
|
||||
if (c.hasKeyword("Fear")) {
|
||||
value += addValue(power * 6, out, "fear");
|
||||
}
|
||||
if (c.hasKeyword("Intimidate")) {
|
||||
value += addValue(power * 6, out, "intimidate");
|
||||
}
|
||||
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
|
||||
value += addValue(power * 3, out, "block-restrict");
|
||||
}
|
||||
}
|
||||
|
||||
// Other good keywords
|
||||
if (power > 0) {
|
||||
if (c.hasKeyword("Double Strike")) {
|
||||
value += addValue(10 + (power * 15), out, "ds");
|
||||
} else if (c.hasKeyword("First Strike")) {
|
||||
value += addValue(10 + (power * 5), out, "fs");
|
||||
}
|
||||
if (c.hasKeyword("Deathtouch")) {
|
||||
value += addValue(25, out, "dt");
|
||||
}
|
||||
if (c.hasKeyword("Lifelink")) {
|
||||
value += addValue(power * 10, out, "lifelink");
|
||||
}
|
||||
if (power > 1 && c.hasKeyword("Trample")) {
|
||||
value += addValue((power - 1) * 5, out, "trample");
|
||||
}
|
||||
if (c.hasKeyword("Vigilance")) {
|
||||
value += addValue((power * 5) + (toughness * 5), out, "vigilance");
|
||||
}
|
||||
if (c.hasKeyword("Wither")) {
|
||||
value += addValue(power * 10, out, "Wither");
|
||||
}
|
||||
if (c.hasKeyword("Infect")) {
|
||||
value += addValue(power * 15, out, "infect");
|
||||
}
|
||||
value += addValue(c.getKeywordMagnitude("Rampage"), out, "rampage");
|
||||
}
|
||||
|
||||
value += addValue(c.getKeywordMagnitude("Bushido") * 16, out, "bushido");
|
||||
value += addValue(c.getAmountOfKeyword("Flanking") * 15, out, "flanking");
|
||||
value += addValue(c.getAmountOfKeyword("Exalted") * 15, out, "exalted");
|
||||
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, out, "eldrazi");
|
||||
value += addValue(c.getKeywordMagnitude("Absorb") * 11, out, "absorb");
|
||||
|
||||
// Defensive Keywords
|
||||
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
|
||||
value += addValue(5, out, "reach");
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
|
||||
value += addValue(3, out, "shadow-block");
|
||||
}
|
||||
|
||||
// Protection
|
||||
if (c.hasKeyword("Indestructible")) {
|
||||
value += addValue(70, out, "darksteel");
|
||||
}
|
||||
if (c.hasKeyword("Prevent all damage that would be dealt to CARDNAME.")) {
|
||||
value += addValue(60, out, "cho-manno");
|
||||
} else if (c.hasKeyword("Prevent all combat damage that would be dealt to CARDNAME.")) {
|
||||
value += addValue(50, out, "fogbank");
|
||||
}
|
||||
if (c.hasKeyword("Hexproof")) {
|
||||
value += addValue(35, out, "hexproof");
|
||||
} else if (c.hasKeyword("Shroud")) {
|
||||
value += addValue(30, out, "shroud");
|
||||
}
|
||||
if (c.hasStartOfKeyword("Protection")) {
|
||||
value += addValue(20, out, "protection");
|
||||
}
|
||||
if (c.hasStartOfKeyword("PreventAllDamageBy")) {
|
||||
value += addValue(10, out, "prevent-dmg");
|
||||
}
|
||||
|
||||
// Bad keywords
|
||||
if (c.hasKeyword("Defender") || c.hasKeyword("CARDNAME can't attack.")) {
|
||||
value -= subValue((power * 9) + 40, out, "defender");
|
||||
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
|
||||
value -= subValue(40, out, "sac-end");
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME can't block.")) {
|
||||
value -= subValue(10, out, "cant-block");
|
||||
} else if (c.hasKeyword("CARDNAME attacks each turn if able.")
|
||||
|| c.hasKeyword("CARDNAME attacks each combat if able.")) {
|
||||
value -= subValue(10, out, "must-attack");
|
||||
} else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
|
||||
value -= subValue(10, out, "must-attack-player");
|
||||
} else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
|
||||
value -= subValue(toughness * 5, out, "reverse-reach");
|
||||
}
|
||||
|
||||
if (c.hasSVar("DestroyWhenDamaged")) {
|
||||
value -= subValue((toughness - 1) * 9, out, "dies-to-dmg");
|
||||
}
|
||||
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.")) {
|
||||
value = addValue(50 + (c.getCMC() * 5), out, "useless"); // reset everything - useless
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
|
||||
if (c.isTapped()) {
|
||||
value = addValue(50 + (c.getCMC() * 5), out, "tapped-useless"); // reset everything - useless
|
||||
} else {
|
||||
value -= subValue(50, out, "doesnt-untap");
|
||||
}
|
||||
}
|
||||
if (c.hasSVar("EndOfTurnLeavePlay")) {
|
||||
value -= subValue(50, out, "eot-leaves");
|
||||
} else if (c.hasStartOfKeyword("Cumulative upkeep")) {
|
||||
value -= subValue(30, out, "cupkeep");
|
||||
} else if (c.hasStartOfKeyword("At the beginning of your upkeep, sacrifice CARDNAME unless you pay")) {
|
||||
value -= subValue(20, out, "sac-unless");
|
||||
} else if (c.hasStartOfKeyword("(Echo unpaid)")) {
|
||||
value -= subValue(10, out, "echo-unpaid");
|
||||
}
|
||||
|
||||
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
|
||||
value -= subValue(20, out, "upkeep-dmg");
|
||||
}
|
||||
if (c.hasStartOfKeyword("Fading")) {
|
||||
value -= subValue(20, out, "fading");
|
||||
}
|
||||
if (c.hasStartOfKeyword("Vanishing")) {
|
||||
value -= subValue(20, out, "vanishing");
|
||||
}
|
||||
if (c.getSVar("Targeting").equals("Dies")) {
|
||||
value -= subValue(25, out, "dies");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(10, out, "sa"+sa);
|
||||
}
|
||||
}
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, out, "manadork");
|
||||
}
|
||||
|
||||
if (c.isUntapped()) {
|
||||
value += addValue(1, out, "untapped");
|
||||
}
|
||||
|
||||
// paired creatures are more valuable because they grant a bonus to the other creature
|
||||
if (c.isPaired()) {
|
||||
value += addValue(14, out, "paired");
|
||||
}
|
||||
|
||||
if (!c.getEncodedCards().isEmpty()) {
|
||||
value += addValue(24, out, "encoded");
|
||||
}
|
||||
return value;
|
||||
return creatureEvaluator.evaluateCreature(c);
|
||||
}
|
||||
|
||||
public static int evaluatePermanentList(final CardCollectionView list) {
|
||||
|
||||
198
forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
Normal file
198
forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package forge.ai;
|
||||
|
||||
import forge.game.card.Card;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class CreatureEvaluator {
|
||||
public int evaluateCreature(final Card c) {
|
||||
int value = 80;
|
||||
if (!c.isToken()) {
|
||||
value += addValue(80, "non-token"); // tokens should be worth less than actual cards
|
||||
}
|
||||
int power = c.getNetCombatDamage();
|
||||
final int toughness = c.getNetToughness();
|
||||
for (String keyword : c.getKeywords()) {
|
||||
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
|
||||
power = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
value += addValue(power * 15, "power");
|
||||
value += addValue(toughness * 10, "toughness");
|
||||
value += addValue(c.getCMC() * 5, "cmc");
|
||||
|
||||
// Evasion keywords
|
||||
if (c.hasKeyword("Flying")) {
|
||||
value += addValue(power * 10, "flying");
|
||||
}
|
||||
if (c.hasKeyword("Horsemanship")) {
|
||||
value += addValue(power * 10, "horses");
|
||||
}
|
||||
if (c.hasKeyword("Unblockable")) {
|
||||
value += addValue(power * 10, "unblockable");
|
||||
} else {
|
||||
if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||
value += addValue(power * 6, "thorns");
|
||||
}
|
||||
if (c.hasKeyword("Fear")) {
|
||||
value += addValue(power * 6, "fear");
|
||||
}
|
||||
if (c.hasKeyword("Intimidate")) {
|
||||
value += addValue(power * 6, "intimidate");
|
||||
}
|
||||
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
|
||||
value += addValue(power * 3, "block-restrict");
|
||||
}
|
||||
}
|
||||
|
||||
// Other good keywords
|
||||
if (power > 0) {
|
||||
if (c.hasKeyword("Double Strike")) {
|
||||
value += addValue(10 + (power * 15), "ds");
|
||||
} else if (c.hasKeyword("First Strike")) {
|
||||
value += addValue(10 + (power * 5), "fs");
|
||||
}
|
||||
if (c.hasKeyword("Deathtouch")) {
|
||||
value += addValue(25, "dt");
|
||||
}
|
||||
if (c.hasKeyword("Lifelink")) {
|
||||
value += addValue(power * 10, "lifelink");
|
||||
}
|
||||
if (power > 1 && c.hasKeyword("Trample")) {
|
||||
value += addValue((power - 1) * 5, "trample");
|
||||
}
|
||||
if (c.hasKeyword("Vigilance")) {
|
||||
value += addValue((power * 5) + (toughness * 5), "vigilance");
|
||||
}
|
||||
if (c.hasKeyword("Wither")) {
|
||||
value += addValue(power * 10, "Wither");
|
||||
}
|
||||
if (c.hasKeyword("Infect")) {
|
||||
value += addValue(power * 15, "infect");
|
||||
}
|
||||
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
|
||||
}
|
||||
|
||||
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
|
||||
value += addValue(c.getAmountOfKeyword("Flanking") * 15, "flanking");
|
||||
value += addValue(c.getAmountOfKeyword("Exalted") * 15, "exalted");
|
||||
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
|
||||
value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
|
||||
|
||||
// Defensive Keywords
|
||||
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
|
||||
value += addValue(5, "reach");
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
|
||||
value += addValue(3, "shadow-block");
|
||||
}
|
||||
|
||||
// Protection
|
||||
if (c.hasKeyword("Indestructible")) {
|
||||
value += addValue(70, "darksteel");
|
||||
}
|
||||
if (c.hasKeyword("Prevent all damage that would be dealt to CARDNAME.")) {
|
||||
value += addValue(60, "cho-manno");
|
||||
} else if (c.hasKeyword("Prevent all combat damage that would be dealt to CARDNAME.")) {
|
||||
value += addValue(50, "fogbank");
|
||||
}
|
||||
if (c.hasKeyword("Hexproof")) {
|
||||
value += addValue(35, "hexproof");
|
||||
} else if (c.hasKeyword("Shroud")) {
|
||||
value += addValue(30, "shroud");
|
||||
}
|
||||
if (c.hasStartOfKeyword("Protection")) {
|
||||
value += addValue(20, "protection");
|
||||
}
|
||||
if (c.hasStartOfKeyword("PreventAllDamageBy")) {
|
||||
value += addValue(10, "prevent-dmg");
|
||||
}
|
||||
|
||||
// Bad keywords
|
||||
if (c.hasKeyword("Defender") || c.hasKeyword("CARDNAME can't attack.")) {
|
||||
value -= subValue((power * 9) + 40, "defender");
|
||||
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
|
||||
value -= subValue(40, "sac-end");
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME can't block.")) {
|
||||
value -= subValue(10, "cant-block");
|
||||
} else if (c.hasKeyword("CARDNAME attacks each turn if able.")
|
||||
|| c.hasKeyword("CARDNAME attacks each combat if able.")) {
|
||||
value -= subValue(10, "must-attack");
|
||||
} else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
|
||||
value -= subValue(10, "must-attack-player");
|
||||
} else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
|
||||
value -= subValue(toughness * 5, "reverse-reach");
|
||||
}
|
||||
|
||||
if (c.hasSVar("DestroyWhenDamaged")) {
|
||||
value -= subValue((toughness - 1) * 9, "dies-to-dmg");
|
||||
}
|
||||
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.")) {
|
||||
value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
|
||||
if (c.isTapped()) {
|
||||
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
|
||||
} else {
|
||||
value -= subValue(50, "doesnt-untap");
|
||||
}
|
||||
}
|
||||
if (c.hasSVar("EndOfTurnLeavePlay")) {
|
||||
value -= subValue(50, "eot-leaves");
|
||||
} else if (c.hasStartOfKeyword("Cumulative upkeep")) {
|
||||
value -= subValue(30, "cupkeep");
|
||||
} else if (c.hasStartOfKeyword("At the beginning of your upkeep, sacrifice CARDNAME unless you pay")) {
|
||||
value -= subValue(20, "sac-unless");
|
||||
} else if (c.hasStartOfKeyword("(Echo unpaid)")) {
|
||||
value -= subValue(10, "echo-unpaid");
|
||||
}
|
||||
|
||||
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
|
||||
value -= subValue(20, "upkeep-dmg");
|
||||
}
|
||||
if (c.hasStartOfKeyword("Fading")) {
|
||||
value -= subValue(20, "fading");
|
||||
}
|
||||
if (c.hasStartOfKeyword("Vanishing")) {
|
||||
value -= subValue(20, "vanishing");
|
||||
}
|
||||
if (c.getSVar("Targeting").equals("Dies")) {
|
||||
value -= subValue(25, "dies");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(10, "sa"+sa);
|
||||
}
|
||||
}
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, "manadork");
|
||||
}
|
||||
|
||||
if (c.isUntapped()) {
|
||||
value += addValue(1, "untapped");
|
||||
}
|
||||
|
||||
// paired creatures are more valuable because they grant a bonus to the other creature
|
||||
if (c.isPaired()) {
|
||||
value += addValue(14, "paired");
|
||||
}
|
||||
|
||||
if (!c.getEncodedCards().isEmpty()) {
|
||||
value += addValue(24, "encoded");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected int addValue(int value, String text) {
|
||||
return value;
|
||||
}
|
||||
protected int subValue(int value, String text) {
|
||||
return -addValue(-value, text);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.game.Game;
|
||||
@@ -101,13 +99,6 @@ public class GameSimulator {
|
||||
|
||||
public static boolean debugPrint;
|
||||
public static ArrayList<String> debugLines;
|
||||
public static Function<String, Void> debugPrintFunction = new Function<String, Void>() {
|
||||
@Override
|
||||
public Void apply(String str) {
|
||||
debugPrint(str);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
public static void debugPrint(String str) {
|
||||
if (debugPrint) {
|
||||
System.out.println(str);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package forge.ai.simulation;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.CreatureEvaluator;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
@@ -8,9 +8,12 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
public class GameStateEvaluator {
|
||||
private boolean debugging = false;
|
||||
private SimulationCreatureEvaluator eval = new SimulationCreatureEvaluator();
|
||||
|
||||
public void setDebugging(boolean debugging) {
|
||||
this.debugging = debugging;
|
||||
}
|
||||
|
||||
public int getScoreForGameState(Game game, Player aiPlayer) {
|
||||
if (game.isGameOver()) {
|
||||
return game.getOutcome().getWinningPlayer() == aiPlayer ? Integer.MAX_VALUE : Integer.MIN_VALUE;
|
||||
@@ -71,7 +74,7 @@ public class GameStateEvaluator {
|
||||
protected int evalCard(Game game, Player aiPlayer, Card c) {
|
||||
// TODO: These should be based on other considerations - e.g. in relation to opponents state.
|
||||
if (c.isCreature()) {
|
||||
return ComputerUtilCard.evaluateCreatureDebug(c, debugging ? GameSimulator.debugPrintFunction : null);
|
||||
return eval.evaluateCreature(c);
|
||||
} else if (c.isLand()) {
|
||||
return 100;
|
||||
} else if (c.isEnchantingCard()) {
|
||||
@@ -84,4 +87,14 @@ public class GameStateEvaluator {
|
||||
return 50 + 30 * c.getCMC();
|
||||
}
|
||||
}
|
||||
|
||||
private class SimulationCreatureEvaluator extends CreatureEvaluator {
|
||||
@Override
|
||||
protected int addValue(int value, String text) {
|
||||
if (debugging && value != 0) {
|
||||
GameSimulator.debugPrint(value + " via " + text);
|
||||
}
|
||||
return super.addValue(value, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user