mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
- refactor: removed outdated/unused code from AI, battlefield score, targeting, etc; - ai: removed all targeting and filters related logic from ComputerPlayer; - ai: improved targeting with shared and simple logic due effect's outcome and possible targets priority; - ai: improved get amount selection (now AI will not choose big and useless values); - ai: improved spell selection on free cast (now AI will try to cast spells with valid targets only); - tests: improved result table with first bad dialog info for fast access (part of #13638);
This commit is contained in:
parent
1fe0d92c86
commit
ffe902d25e
27 changed files with 777 additions and 1615 deletions
|
|
@ -23,6 +23,7 @@ import mage.game.stack.StackAbility;
|
|||
import mage.game.stack.StackObject;
|
||||
import mage.player.ai.ma.optimizers.TreeOptimizer;
|
||||
import mage.player.ai.ma.optimizers.impl.*;
|
||||
import mage.player.ai.score.GameStateEvaluator2;
|
||||
import mage.player.ai.util.CombatInfo;
|
||||
import mage.player.ai.util.CombatUtil;
|
||||
import mage.players.Player;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package mage.player.ai;
|
|||
import mage.abilities.Ability;
|
||||
import mage.constants.RangeOfInfluence;
|
||||
import mage.game.Game;
|
||||
import mage.player.ai.score.GameStateEvaluator2;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.util.Date;
|
||||
|
|
|
|||
|
|
@ -1,239 +0,0 @@
|
|||
package mage.player.ai;
|
||||
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.player.ai.ma.ArtificialScoringSystem;
|
||||
import mage.players.Player;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.constants.Outcome;
|
||||
|
||||
/**
|
||||
* @author nantuko
|
||||
* <p>
|
||||
* This evaluator is only good for two player games
|
||||
*/
|
||||
public final class GameStateEvaluator2 {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GameStateEvaluator2.class);
|
||||
|
||||
public static final int WIN_GAME_SCORE = 100000000;
|
||||
public static final int LOSE_GAME_SCORE = -WIN_GAME_SCORE;
|
||||
|
||||
public static final int HAND_CARD_SCORE = 5;
|
||||
|
||||
public static PlayerEvaluateScore evaluate(UUID playerId, Game game) {
|
||||
return evaluate(playerId, game, true);
|
||||
}
|
||||
|
||||
public static PlayerEvaluateScore evaluate(UUID playerId, Game game, boolean useCombatPermanentScore) {
|
||||
// TODO: add multi opponents support, so AI can take better actions
|
||||
Player player = game.getPlayer(playerId);
|
||||
// must find all leaved opponents
|
||||
Player opponent = game.getPlayer(game.getOpponents(playerId, false).stream().findFirst().orElse(null));
|
||||
if (opponent == null) {
|
||||
return new PlayerEvaluateScore(playerId, WIN_GAME_SCORE);
|
||||
}
|
||||
|
||||
if (game.checkIfGameIsOver()) {
|
||||
if (player.hasLost()
|
||||
|| opponent.hasWon()) {
|
||||
return new PlayerEvaluateScore(playerId, LOSE_GAME_SCORE);
|
||||
}
|
||||
if (opponent.hasLost()
|
||||
|| player.hasWon()) {
|
||||
return new PlayerEvaluateScore(playerId, WIN_GAME_SCORE);
|
||||
}
|
||||
}
|
||||
|
||||
int playerLifeScore = 0;
|
||||
int opponentLifeScore = 0;
|
||||
if (player.getLife() <= 0) { // we don't want a tie
|
||||
playerLifeScore = ArtificialScoringSystem.LOSE_GAME_SCORE;
|
||||
} else if (opponent.getLife() <= 0) {
|
||||
playerLifeScore = ArtificialScoringSystem.WIN_GAME_SCORE;
|
||||
} else {
|
||||
playerLifeScore = ArtificialScoringSystem.getLifeScore(player.getLife());
|
||||
opponentLifeScore = ArtificialScoringSystem.getLifeScore(opponent.getLife()); // TODO: minus
|
||||
}
|
||||
|
||||
int playerPermanentsScore = 0;
|
||||
int opponentPermanentsScore = 0;
|
||||
try {
|
||||
StringBuilder sbPlayer = new StringBuilder();
|
||||
StringBuilder sbOpponent = new StringBuilder();
|
||||
|
||||
// add values of player
|
||||
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) {
|
||||
int onePermScore = evaluatePermanent(permanent, game, useCombatPermanentScore);
|
||||
playerPermanentsScore += onePermScore;
|
||||
if (logger.isDebugEnabled()) {
|
||||
sbPlayer.append(permanent.getName()).append('[').append(onePermScore).append("] ");
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
sbPlayer.insert(0, playerPermanentsScore + " - ");
|
||||
sbPlayer.insert(0, "Player..: ");
|
||||
logger.debug(sbPlayer);
|
||||
}
|
||||
|
||||
// add values of opponent
|
||||
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(opponent.getId())) {
|
||||
int onePermScore = evaluatePermanent(permanent, game, useCombatPermanentScore);
|
||||
opponentPermanentsScore += onePermScore;
|
||||
if (logger.isDebugEnabled()) {
|
||||
sbOpponent.append(permanent.getName()).append('[').append(onePermScore).append("] ");
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
sbOpponent.insert(0, opponentPermanentsScore + " - ");
|
||||
sbOpponent.insert(0, "Opponent: ");
|
||||
logger.debug(sbOpponent);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
|
||||
// TODO: add card evaluator like permanent evaluator
|
||||
// - same card on battlefield must score x2 compared to hand, so AI will want to play it;
|
||||
// - other zones must score cards same way, example: battlefield = x, hand = x * 0.1, graveyard = x * 0.5, exile = x * 0.3
|
||||
// - possible bug in wrong score: instant and sorcery on hand will be more valuable compared to other zones,
|
||||
// so AI will keep it in hand. Possible fix: look at card type and apply zones multipliers due special
|
||||
// table like:
|
||||
// * battlefield needs in creatures and enchantments/auras;
|
||||
// * hand needs in instants and sorceries
|
||||
// * graveyard needs in anything after battlefield and hand;
|
||||
// * exile needs in nothing;
|
||||
// * commander zone needs in nothing;
|
||||
// - additional improve: use revealed data to score opponent's hand:
|
||||
// * known card by card evaluator;
|
||||
// * unknown card by max value (so AI will use reveal to make opponent's total score lower -- is it helps???)
|
||||
int playerHandScore = player.getHand().size() * HAND_CARD_SCORE;
|
||||
int opponentHandScore = opponent.getHand().size() * HAND_CARD_SCORE;
|
||||
|
||||
int score = (playerLifeScore - opponentLifeScore)
|
||||
+ (playerPermanentsScore - opponentPermanentsScore)
|
||||
+ (playerHandScore - opponentHandScore);
|
||||
logger.debug(score
|
||||
+ " total Score (life:" + (playerLifeScore - opponentLifeScore)
|
||||
+ " permanents:" + (playerPermanentsScore - opponentPermanentsScore)
|
||||
+ " hand:" + (playerHandScore - opponentHandScore) + ')');
|
||||
return new PlayerEvaluateScore(
|
||||
playerId,
|
||||
playerLifeScore, playerHandScore, playerPermanentsScore,
|
||||
opponentLifeScore, opponentHandScore, opponentPermanentsScore);
|
||||
}
|
||||
|
||||
public static int evaluatePermanent(Permanent permanent, Game game, boolean useCombatPermanentScore) {
|
||||
// prevent AI from attaching bad auras to its own permanents ex: Brainwash and Demonic Torment (no immediate penalty on the battlefield)
|
||||
int value = 0;
|
||||
if (!permanent.getAttachments().isEmpty()) {
|
||||
for (UUID attachmentId : permanent.getAttachments()) {
|
||||
Permanent attachment = game.getPermanent(attachmentId);
|
||||
for (Ability a : attachment.getAbilities(game)) {
|
||||
for (Effect e : a.getEffects()) {
|
||||
if (e.getOutcome().equals(Outcome.Detriment)
|
||||
&& attachment.getControllerId().equals(permanent.getControllerId())) {
|
||||
value -= 1000; // seems to work well ; -300 is not effective enough
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
value += ArtificialScoringSystem.getFixedPermanentScore(game, permanent);
|
||||
value += ArtificialScoringSystem.getDynamicPermanentScore(game, permanent);
|
||||
if (useCombatPermanentScore) {
|
||||
value += ArtificialScoringSystem.getCombatPermanentScore(game, permanent);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static class PlayerEvaluateScore {
|
||||
|
||||
private UUID playerId;
|
||||
private int playerLifeScore = 0;
|
||||
private int playerHandScore = 0;
|
||||
private int playerPermanentsScore = 0;
|
||||
|
||||
private int opponentLifeScore = 0;
|
||||
private int opponentHandScore = 0;
|
||||
private int opponentPermanentsScore = 0;
|
||||
|
||||
private int specialScore = 0; // special score (ignore all others, e.g. for win/lose game states)
|
||||
|
||||
public PlayerEvaluateScore(UUID playerId, int specialScore) {
|
||||
this.playerId = playerId;
|
||||
this.specialScore = specialScore;
|
||||
}
|
||||
|
||||
public PlayerEvaluateScore(UUID playerId,
|
||||
int playerLifeScore, int playerHandScore, int playerPermanentsScore,
|
||||
int opponentLifeScore, int opponentHandScore, int opponentPermanentsScore) {
|
||||
this.playerId = playerId;
|
||||
this.playerLifeScore = playerLifeScore;
|
||||
this.playerHandScore = playerHandScore;
|
||||
this.playerPermanentsScore = playerPermanentsScore;
|
||||
this.opponentLifeScore = opponentLifeScore;
|
||||
this.opponentHandScore = opponentHandScore;
|
||||
this.opponentPermanentsScore = opponentPermanentsScore;
|
||||
}
|
||||
|
||||
public UUID getPlayerId() {
|
||||
return this.playerId;
|
||||
}
|
||||
|
||||
public int getPlayerScore() {
|
||||
return playerLifeScore + playerHandScore + playerPermanentsScore;
|
||||
}
|
||||
|
||||
public int getOpponentScore() {
|
||||
return opponentLifeScore + opponentHandScore + opponentPermanentsScore;
|
||||
}
|
||||
|
||||
public int getTotalScore() {
|
||||
if (specialScore != 0) {
|
||||
return specialScore;
|
||||
} else {
|
||||
return getPlayerScore() - getOpponentScore();
|
||||
}
|
||||
}
|
||||
|
||||
public int getPlayerLifeScore() {
|
||||
return playerLifeScore;
|
||||
}
|
||||
|
||||
public int getPlayerHandScore() {
|
||||
return playerHandScore;
|
||||
}
|
||||
|
||||
public int getPlayerPermanentsScore() {
|
||||
return playerPermanentsScore;
|
||||
}
|
||||
|
||||
public String getPlayerInfoFull() {
|
||||
return "Life:" + playerLifeScore
|
||||
+ ", Hand:" + playerHandScore
|
||||
+ ", Perm:" + playerPermanentsScore;
|
||||
}
|
||||
|
||||
public String getPlayerInfoShort() {
|
||||
return "L:" + playerLifeScore
|
||||
+ ",H:" + playerHandScore
|
||||
+ ",P:" + playerPermanentsScore;
|
||||
}
|
||||
|
||||
public String getOpponentInfoFull() {
|
||||
return "Life:" + opponentLifeScore
|
||||
+ ", Hand:" + opponentHandScore
|
||||
+ ", Perm:" + opponentPermanentsScore;
|
||||
}
|
||||
|
||||
public String getOpponentInfoShort() {
|
||||
return "L:" + opponentLifeScore
|
||||
+ ",H:" + opponentHandScore
|
||||
+ ",P:" + opponentPermanentsScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
package mage.player.ai.ma;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.keyword.HasteAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author ubeefx, nantuko
|
||||
*/
|
||||
public final class ArtificialScoringSystem {
|
||||
|
||||
public static final int WIN_GAME_SCORE = 100000000;
|
||||
public static final int LOSE_GAME_SCORE = -WIN_GAME_SCORE;
|
||||
|
||||
private static final int[] LIFE_SCORES = {0, 1000, 2000, 3000, 4000, 4500, 5000, 5500, 6000, 6500, 7000, 7400, 7800, 8200, 8600, 9000, 9200, 9400, 9600, 9800, 10000};
|
||||
private static final int MAX_LIFE = LIFE_SCORES.length - 1;
|
||||
private static final int UNKNOWN_CARD_SCORE = 300;
|
||||
private static final int PERMANENT_SCORE = 300;
|
||||
private static final int LIFE_ABOVE_MULTIPLIER = 100;
|
||||
|
||||
public static int getCardDefinitionScore(final Game game, final Card card) {
|
||||
int value = 3; //TODO: add new rating system card value
|
||||
if (card.isLand(game)) {
|
||||
int score = (int) ((value / 2.0f) * 50);
|
||||
//TODO: check this for "any color" lands
|
||||
//TODO: check this for dual and filter lands
|
||||
/*for (Mana mana : card.getMana()) {
|
||||
score += 50;
|
||||
}*/
|
||||
score += card.getMana().size() * 50;
|
||||
return score;
|
||||
}
|
||||
|
||||
final int score = value * 100 - card.getManaCost().manaValue() * 20;
|
||||
if (card.getCardType(game).contains(CardType.CREATURE)) {
|
||||
return score + (card.getPower().getValue() + card.getToughness().getValue()) * 10;
|
||||
} else {
|
||||
return score + (/*card.getRemoval()*50*/+(card.getRarity() == null ? 0 : card.getRarity().getRating() * 30));
|
||||
}
|
||||
}
|
||||
|
||||
public static int getFixedPermanentScore(final Game game, final Permanent permanent) {
|
||||
//TODO: cache it inside Card
|
||||
int score = getCardDefinitionScore(game, permanent);
|
||||
score += PERMANENT_SCORE;
|
||||
if (permanent.getCardType(game).contains(CardType.CREATURE)) {
|
||||
// TODO: implement in the mage core
|
||||
//score + =cardDefinition.getActivations().size()*50;
|
||||
//score += cardDefinition.getManaActivations().size()*80;
|
||||
} else {
|
||||
if (permanent.hasSubtype(SubType.EQUIPMENT, game)) {
|
||||
score += 100;
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
public static int getDynamicPermanentScore(final Game game, final Permanent permanent) {
|
||||
|
||||
int score = permanent.getCounters(game).getCount(CounterType.CHARGE) * 30;
|
||||
score += permanent.getCounters(game).getCount(CounterType.LEVEL) * 30;
|
||||
score -= permanent.getDamage() * 2;
|
||||
if (permanent.getCardType(game).contains(CardType.CREATURE)) {
|
||||
final int power = permanent.getPower().getValue();
|
||||
final int toughness = permanent.getToughness().getValue();
|
||||
int abilityScore = 0;
|
||||
for (Ability ability : permanent.getAbilities(game)) {
|
||||
abilityScore += MagicAbility.getAbilityScore(ability);
|
||||
}
|
||||
score += power * 300 + getPositive(toughness) * 200 + abilityScore * (getPositive(power) + 1) / 2;
|
||||
int enchantments = 0;
|
||||
int equipments = 0;
|
||||
for (UUID uuid : permanent.getAttachments()) {
|
||||
MageObject object = game.getObject(uuid);
|
||||
if (object instanceof Card) {
|
||||
Card card = (Card) object;
|
||||
// TODO: implement getOutcomeTotal for permanents and cards too (not only attachments)
|
||||
int outcomeScore = card.getAbilities(game).getOutcomeTotal();
|
||||
if (card.getCardType(game).contains(CardType.ENCHANTMENT)) {
|
||||
enchantments = enchantments + outcomeScore * 100;
|
||||
} else {
|
||||
equipments = equipments + outcomeScore * 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
score += equipments + enchantments;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
public static int getCombatPermanentScore(final Game game, final Permanent permanent) {
|
||||
int score = 0;
|
||||
if (!canTap(game, permanent)) {
|
||||
score += getTappedScore(game, permanent);
|
||||
}
|
||||
if (permanent.getCardType(game).contains(CardType.CREATURE)) {
|
||||
if (!permanent.canAttack(null, game)) {
|
||||
score -= 100;
|
||||
}
|
||||
if (!permanent.canBlockAny(game)) {
|
||||
score -= 30;
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
private static boolean canTap(Game game, Permanent permanent) {
|
||||
return !permanent.isTapped()
|
||||
&& (!permanent.hasSummoningSickness()
|
||||
|| !permanent.getCardType(game).contains(CardType.CREATURE)
|
||||
|| permanent.getAbilities(game).contains(HasteAbility.getInstance()));
|
||||
}
|
||||
|
||||
private static int getPositive(int value) {
|
||||
return Math.max(0, value);
|
||||
}
|
||||
|
||||
public static int getTappedScore(Game game, final Permanent permanent) {
|
||||
if (permanent.isCreature(game)) {
|
||||
return -100;
|
||||
} else if (permanent.isLand(game)) {
|
||||
return -20; // means probably no mana available (should be greater than passivity penalty
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getLifeScore(final int life) {
|
||||
if (life > MAX_LIFE) {
|
||||
return LIFE_SCORES[MAX_LIFE] + (life - MAX_LIFE) * LIFE_ABOVE_MULTIPLIER;
|
||||
} else if (life >= 0) {
|
||||
return LIFE_SCORES[life];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getManaScore(final int amount) {
|
||||
return -amount;
|
||||
}
|
||||
|
||||
public static int getAttackerScore(final Permanent attacker) {
|
||||
//TODO: implement this
|
||||
/*int score = attacker.getPower().getValue() * 5 + attacker.lethalDamage * 2 - attacker.candidateBlockers.length;
|
||||
for (final MagicCombatCreature blocker : attacker.candidateBlockers) {
|
||||
|
||||
score -= blocker.power;
|
||||
}
|
||||
// Dedicated attacker.
|
||||
if (attacker.hasAbility(MagicAbility.AttacksEachTurnIfAble) || attacker.hasAbility(MagicAbility.CannotBlock)) {
|
||||
score += 10;
|
||||
}
|
||||
// Abilities for attacking.
|
||||
if (attacker.hasAbility(MagicAbility.Trample) || attacker.hasAbility(MagicAbility.Vigilance)) {
|
||||
score += 8;
|
||||
}
|
||||
// Dangerous to block.
|
||||
if (!attacker.normalDamage || attacker.hasAbility(MagicAbility.FirstStrike) || attacker.hasAbility(MagicAbility.Indestructible)) {
|
||||
score += 7;
|
||||
}
|
||||
*/
|
||||
int score = 0;
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package mage.player.ai.ma;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.keyword.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author nantuko
|
||||
*/
|
||||
public final class MagicAbility {
|
||||
|
||||
private static Map<String, Integer> scores = new HashMap<String, Integer>() {{
|
||||
put(DeathtouchAbility.getInstance().getRule(), 60);
|
||||
put(DefenderAbility.getInstance().getRule(), -100);
|
||||
put(DoubleStrikeAbility.getInstance().getRule(), 100);
|
||||
put(DoubleStrikeAbility.getInstance().getRule(), 100);
|
||||
put(new ExaltedAbility().getRule(), 10);
|
||||
put(FirstStrikeAbility.getInstance().getRule(), 50);
|
||||
put(FlashAbility.getInstance().getRule(), 0);
|
||||
put(FlyingAbility.getInstance().getRule(), 50);
|
||||
put(new ForestwalkAbility().getRule(), 10);
|
||||
put(HasteAbility.getInstance().getRule(), 0);
|
||||
put(IndestructibleAbility.getInstance().getRule(), 150);
|
||||
put(InfectAbility.getInstance().getRule(), 60);
|
||||
put(IntimidateAbility.getInstance().getRule(), 50);
|
||||
put(new IslandwalkAbility().getRule(), 10);
|
||||
put(new MountainwalkAbility().getRule(), 10);
|
||||
put(new PlainswalkAbility().getRule(), 10);
|
||||
put(ReachAbility.getInstance().getRule(), 20);
|
||||
put(ShroudAbility.getInstance().getRule(), 60);
|
||||
put(new SwampwalkAbility().getRule(), 10);
|
||||
put(TrampleAbility.getInstance().getRule(), 30);
|
||||
put(new CantBeBlockedSourceAbility().getRule(), 100);
|
||||
put(VigilanceAbility.getInstance().getRule(), 20);
|
||||
put(WitherAbility.getInstance().getRule(), 30);
|
||||
// gatecrash
|
||||
put(new EvolveAbility().getRule(), 50);
|
||||
put(new ExtortAbility().getRule(), 30);
|
||||
|
||||
|
||||
}};
|
||||
|
||||
public static int getAbilityScore(Ability ability) {
|
||||
if (!scores.containsKey(ability.getRule())) {
|
||||
//System.err.println("Couldn't find ability score: " + ability.getClass().getSimpleName() + " - " + ability.toString());
|
||||
//TODO: add handling protection from ..., levelup, kicker, etc. abilities
|
||||
return 0;
|
||||
}
|
||||
return scores.get(ability.getRule());
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import mage.game.permanent.Permanent;
|
|||
import mage.game.turn.CombatDamageStep;
|
||||
import mage.game.turn.EndOfCombatStep;
|
||||
import mage.game.turn.Step;
|
||||
import mage.player.ai.GameStateEvaluator2;
|
||||
import mage.player.ai.score.GameStateEvaluator2;
|
||||
import mage.players.Player;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue