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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,156 @@
|
|||
package mage.player.ai;
|
||||
|
||||
import mage.MageItem;
|
||||
import mage.MageObject;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.player.ai.score.GameStateEvaluator2;
|
||||
import mage.players.PlayableObjectsList;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* AI related code - compare and sort possible targets due target/effect type
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class PossibleTargetsComparator {
|
||||
|
||||
UUID abilityControllerId;
|
||||
Game game;
|
||||
PlayableObjectsList playableItems = new PlayableObjectsList();
|
||||
|
||||
public PossibleTargetsComparator(UUID abilityControllerId, Game game) {
|
||||
this.abilityControllerId = abilityControllerId;
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
public void findPlayableItems() {
|
||||
this.playableItems = this.game.getPlayer(this.abilityControllerId).getPlayableObjects(this.game, Zone.ALL);
|
||||
}
|
||||
|
||||
private int getScoreFromBattlefield(MageItem item) {
|
||||
if (item instanceof Permanent) {
|
||||
// use battlefield score instead simple life
|
||||
return GameStateEvaluator2.evaluatePermanent((Permanent) item, game, false);
|
||||
} else {
|
||||
return getScoreFromLife(item);
|
||||
}
|
||||
}
|
||||
|
||||
private String getName(MageItem item) {
|
||||
if (item instanceof Player) {
|
||||
return ((Player) item).getName();
|
||||
} else if (item instanceof MageObject) {
|
||||
return ((MageObject) item).getName();
|
||||
} else {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private int getScoreFromLife(MageItem item) {
|
||||
// TODO: replace permanent/card life by battlefield score?
|
||||
int res = 0;
|
||||
if (item instanceof Player) {
|
||||
res = ((Player) item).getLife();
|
||||
} else if (item instanceof Card) {
|
||||
Card card = (Card) item;
|
||||
if (card.isPlaneswalker(game)) {
|
||||
res = card.getCounters(game).getCount(CounterType.LOYALTY);
|
||||
} else if (card.isBattle(game)) {
|
||||
res = card.getCounters(game).getCount(CounterType.DEFENSE);
|
||||
} else {
|
||||
int damage = 0;
|
||||
if (card instanceof Permanent) {
|
||||
damage = ((Permanent) card).getDamage();
|
||||
}
|
||||
res = Math.max(0, card.getToughness().getValue() - damage);
|
||||
}
|
||||
// instant
|
||||
if (res == 0) {
|
||||
res = card.getManaValue();
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private boolean isMyItem(MageItem item) {
|
||||
return PossibleTargetsSelector.isMyItem(this.abilityControllerId, item);
|
||||
}
|
||||
|
||||
// sort by name-id at the end, so AI will use same choices in all simulations
|
||||
private final Comparator<MageItem> BY_NAME = (o1, o2) -> getName(o2).compareTo(getName(o1));
|
||||
private final Comparator<MageItem> BY_ID = Comparator.comparing(MageItem::getId);
|
||||
|
||||
private final Comparator<MageItem> BY_ME = (o1, o2) -> Boolean.compare(
|
||||
isMyItem(o2),
|
||||
isMyItem(o1)
|
||||
);
|
||||
|
||||
private final Comparator<MageItem> BY_BIGGER_SCORE = (o1, o2) -> Integer.compare(
|
||||
getScoreFromBattlefield(o2),
|
||||
getScoreFromBattlefield(o1)
|
||||
);
|
||||
|
||||
private final Comparator<MageItem> BY_PLAYABLE = (o1, o2) -> Boolean.compare(
|
||||
this.playableItems.containsObject(o2.getId()),
|
||||
this.playableItems.containsObject(o1.getId())
|
||||
);
|
||||
|
||||
private final Comparator<MageItem> BY_LAND = (o1, o2) -> {
|
||||
boolean isLand1 = o1 instanceof MageObject && ((MageObject) o1).isLand(game);
|
||||
boolean isLand2 = o2 instanceof MageObject && ((MageObject) o2).isLand(game);
|
||||
return Boolean.compare(isLand2, isLand1);
|
||||
};
|
||||
|
||||
private final Comparator<MageItem> BY_TYPE_PLAYER = (o1, o2) -> Boolean.compare(
|
||||
o2 instanceof Player,
|
||||
o1 instanceof Player
|
||||
);
|
||||
|
||||
private final Comparator<MageItem> BY_TYPE_PLANESWALKER = (o1, o2) -> {
|
||||
boolean isPlaneswalker1 = o1 instanceof MageObject && ((MageObject) o1).isPlaneswalker(game);
|
||||
boolean isPlaneswalker2 = o2 instanceof MageObject && ((MageObject) o2).isPlaneswalker(game);
|
||||
return Boolean.compare(isPlaneswalker2, isPlaneswalker1);
|
||||
};
|
||||
|
||||
private final Comparator<MageItem> BY_TYPE_BATTLE = (o1, o2) -> {
|
||||
boolean isBattle1 = o1 instanceof MageObject && ((MageObject) o1).isBattle(game);
|
||||
boolean isBattle2 = o2 instanceof MageObject && ((MageObject) o2).isBattle(game);
|
||||
return Boolean.compare(isBattle2, isBattle1);
|
||||
};
|
||||
|
||||
private final Comparator<MageItem> BY_TYPES = BY_TYPE_PLANESWALKER
|
||||
.thenComparing(BY_TYPE_BATTLE)
|
||||
.thenComparing(BY_TYPE_PLAYER);
|
||||
|
||||
/**
|
||||
* Default sorting for good effects - put the biggest items to the top
|
||||
*/
|
||||
public final Comparator<MageItem> ANY_MOST_VALUABLE_FIRST = BY_TYPES
|
||||
.thenComparing(BY_BIGGER_SCORE)
|
||||
.thenComparing(BY_NAME)
|
||||
.thenComparing(BY_ID);
|
||||
public final Comparator<MageItem> ANY_MOST_VALUABLE_LAST = ANY_MOST_VALUABLE_FIRST.reversed();
|
||||
|
||||
/**
|
||||
* Default sorting for good effects - put own and biggest items to the top
|
||||
*/
|
||||
public final Comparator<MageItem> MY_MOST_VALUABLE_FIRST = BY_ME
|
||||
.thenComparing(ANY_MOST_VALUABLE_FIRST);
|
||||
public final Comparator<MageItem> MY_MOST_VALUABLE_LAST = MY_MOST_VALUABLE_FIRST.reversed();
|
||||
|
||||
/**
|
||||
* Sorting for discard effects - put the biggest unplayable at the top, lands at the end anyway
|
||||
*/
|
||||
public final Comparator<MageItem> ANY_UNPLAYABLE_AND_USELESS = BY_LAND.reversed()
|
||||
.thenComparing(BY_PLAYABLE.reversed())
|
||||
.thenComparing(ANY_MOST_VALUABLE_FIRST);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
package mage.player.ai;
|
||||
|
||||
import mage.MageItem;
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.ControllableOrOwnerable;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.Target;
|
||||
import mage.target.common.TargetCardInGraveyardBattlefieldOrStack;
|
||||
import mage.target.common.TargetDiscard;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI related code - find possible targets and sort it due priority
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class PossibleTargetsSelector {
|
||||
|
||||
Outcome outcome;
|
||||
Target target;
|
||||
UUID abilityControllerId;
|
||||
Ability source;
|
||||
Game game;
|
||||
|
||||
PossibleTargetsComparator comparators;
|
||||
|
||||
// possible targets lists
|
||||
List<MageItem> me = new ArrayList<>();
|
||||
List<MageItem> opponents = new ArrayList<>();
|
||||
List<MageItem> any = new ArrayList<>(); // for outcomes with any target like copy
|
||||
|
||||
public PossibleTargetsSelector(Outcome outcome, Target target, UUID abilityControllerId, Ability source, Game game) {
|
||||
this.outcome = outcome;
|
||||
this.target = target;
|
||||
this.abilityControllerId = abilityControllerId;
|
||||
this.source = source;
|
||||
this.game = game;
|
||||
this.comparators = new PossibleTargetsComparator(abilityControllerId, game);
|
||||
}
|
||||
|
||||
public void findNewTargets(Set<UUID> fromTargetsList) {
|
||||
// collect new valid targets
|
||||
List<MageItem> found = target.possibleTargets(abilityControllerId, source, game).stream()
|
||||
.filter(id -> !target.contains(id))
|
||||
.filter(id -> fromTargetsList == null || fromTargetsList.contains(id))
|
||||
.filter(id -> target.canTarget(abilityControllerId, id, source, game))
|
||||
.map(id -> {
|
||||
Player player = game.getPlayer(id);
|
||||
if (player != null) {
|
||||
return player;
|
||||
} else {
|
||||
return game.getObject(id);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// split targets between me and opponents
|
||||
found.forEach(item -> {
|
||||
if (isMyItem(abilityControllerId, item)) {
|
||||
this.me.add(item);
|
||||
} else {
|
||||
this.opponents.add(item);
|
||||
}
|
||||
this.any.add(item);
|
||||
});
|
||||
|
||||
if (target instanceof TargetDiscard) {
|
||||
// sort due unplayable
|
||||
sortByUnplayableAndUseless();
|
||||
} else {
|
||||
// sort due good/bad outcome
|
||||
sortByMostValuableTargets();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting for any good/bad effects
|
||||
*/
|
||||
private void sortByMostValuableTargets() {
|
||||
if (isGoodEffect()) {
|
||||
// for good effect must choose the biggest objects
|
||||
this.me.sort(comparators.MY_MOST_VALUABLE_FIRST);
|
||||
this.opponents.sort(comparators.MY_MOST_VALUABLE_LAST);
|
||||
this.any.sort(comparators.ANY_MOST_VALUABLE_FIRST);
|
||||
} else {
|
||||
// for bad effect must choose the smallest objects
|
||||
this.me.sort(comparators.MY_MOST_VALUABLE_LAST);
|
||||
this.opponents.sort(comparators.MY_MOST_VALUABLE_FIRST);
|
||||
this.any.sort(comparators.ANY_MOST_VALUABLE_LAST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting for discard
|
||||
*/
|
||||
private void sortByUnplayableAndUseless() {
|
||||
// used
|
||||
// no good or bad effect - you must choose
|
||||
comparators.findPlayableItems();
|
||||
this.me.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
|
||||
this.opponents.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
|
||||
this.any.sort(comparators.ANY_UNPLAYABLE_AND_USELESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority targets. Try to use as much as possible.
|
||||
*/
|
||||
public List<MageItem> getGoodTargets() {
|
||||
if (isAnyEffect()) {
|
||||
return this.any;
|
||||
}
|
||||
|
||||
if (isGoodEffect()) {
|
||||
return this.me;
|
||||
} else {
|
||||
return this.opponents;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional targets. Try to ignore bad targets (e.g. opponent's creatures for your good effect).
|
||||
*/
|
||||
public List<MageItem> getBadTargets() {
|
||||
if (isAnyEffect()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (isGoodEffect()) {
|
||||
return this.opponents;
|
||||
} else {
|
||||
return this.me;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isMyItem(UUID abilityControllerId, MageItem item) {
|
||||
if (item instanceof Player) {
|
||||
return item.getId().equals(abilityControllerId);
|
||||
} else if (item instanceof ControllableOrOwnerable) {
|
||||
return ((ControllableOrOwnerable) item).getControllerOrOwnerId().equals(abilityControllerId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAnyEffect() {
|
||||
boolean isAnyEffect = outcome.anyTargetHasSameValue();
|
||||
|
||||
if (hasGoodExile()) {
|
||||
isAnyEffect = true;
|
||||
}
|
||||
|
||||
return isAnyEffect;
|
||||
}
|
||||
|
||||
private boolean isGoodEffect() {
|
||||
boolean isGoodEffect = outcome.isGood();
|
||||
|
||||
if (hasGoodExile()) {
|
||||
isGoodEffect = true;
|
||||
}
|
||||
|
||||
return isGoodEffect;
|
||||
}
|
||||
|
||||
private boolean hasGoodExile() {
|
||||
// exile workaround: exile is bad, but exile from library or graveyard in most cases is good
|
||||
// (more exiled -- more good things you get, e.g. delve's pay or search cards with same name)
|
||||
if (outcome == Outcome.Exile) {
|
||||
if (Zone.GRAVEYARD.match(target.getZone())
|
||||
|| Zone.LIBRARY.match(target.getZone())) {
|
||||
// TargetCardInGraveyardBattlefieldOrStack - used for additional payment like Craft, so do not allow big cards for it
|
||||
if (!(target instanceof TargetCardInGraveyardBattlefieldOrStack)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package mage.player.ai.ma;
|
||||
package mage.player.ai.score;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
package mage.player.ai;
|
||||
package mage.player.ai.score;
|
||||
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.player.ai.ma.ArtificialScoringSystem;
|
||||
import mage.players.Player;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package mage.player.ai.ma;
|
||||
package mage.player.ai.score;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.keyword.*;
|
||||
|
|
@ -7,6 +7,7 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* TODO: outdated, replace by edh or commander brackets ability score
|
||||
* @author nantuko
|
||||
*/
|
||||
public final class MagicAbility {
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
|
||||
|
||||
package mage.player.ai.simulators;
|
||||
|
||||
import mage.abilities.ActivatedAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.player.ai.ComputerPlayer;
|
||||
import mage.player.ai.PermanentEvaluator;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class ActionSimulator {
|
||||
|
||||
private ComputerPlayer player;
|
||||
private List<Card> playableInstants = new ArrayList<>();
|
||||
private List<ActivatedAbility> playableAbilities = new ArrayList<>();
|
||||
|
||||
private Game game;
|
||||
|
||||
public ActionSimulator(ComputerPlayer player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public void simulate(Game game) {
|
||||
|
||||
}
|
||||
|
||||
public int evaluateState() {
|
||||
// must find all leaved opponents
|
||||
Player opponent = game.getPlayer(game.getOpponents(player.getId(), false).stream().findFirst().orElse(null));
|
||||
if (opponent == null) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
if (game.checkIfGameIsOver()) {
|
||||
if (player.hasLost() || opponent.hasWon()) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
if (opponent.hasLost() || player.hasWon()) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
int value = player.getLife();
|
||||
value -= opponent.getLife();
|
||||
PermanentEvaluator evaluator = new PermanentEvaluator();
|
||||
for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
|
||||
value += evaluator.evaluate(permanent, game);
|
||||
}
|
||||
for (Permanent permanent: game.getBattlefield().getAllActivePermanents(player.getId())) {
|
||||
value -= evaluator.evaluate(permanent, game);
|
||||
}
|
||||
value += player.getHand().size();
|
||||
value -= opponent.getHand().size();
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue