mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
- refactor: migrated AI's target amount code to shared selection logic; - ai: fixed game freezes on some use cases; - tests: added AI's testable dialogs for target amount; - tests: improved load tests result table, added game cycles stats; - Dwarven Catapult - fixed game error on usage;
This commit is contained in:
parent
8e7a7e9fc6
commit
f3e18e245f
23 changed files with 502 additions and 390 deletions
|
|
@ -20,9 +20,7 @@ import mage.cards.repository.CardInfo;
|
|||
import mage.cards.repository.CardRepository;
|
||||
import mage.choices.Choice;
|
||||
import mage.constants.*;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.filter.common.FilterCreatureForCombatBlock;
|
||||
import mage.filter.common.FilterLandCard;
|
||||
import mage.filter.common.FilterNonlandCard;
|
||||
|
|
@ -158,16 +156,7 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
return false;
|
||||
}
|
||||
|
||||
// controller hints:
|
||||
// - target.getTargetController(), this.getId() -- player that must makes choices (must be same with this.getId)
|
||||
// - target.getAbilityController(), abilityControllerId -- affected player/controller for all actions/filters
|
||||
// - affected controller can be different from target controller (another player makes choices for controller)
|
||||
// sometimes a target selection can be made from a player that does not control the ability
|
||||
UUID abilityControllerId = playerId;
|
||||
if (target.getTargetController() != null
|
||||
&& target.getAbilityController() != null) {
|
||||
abilityControllerId = target.getAbilityController();
|
||||
}
|
||||
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
|
||||
|
||||
// nothing to choose, e.g. X=0
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
|
|
@ -175,13 +164,11 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
|
||||
// default logic for any targets
|
||||
boolean isAddedSomething = false;
|
||||
PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game);
|
||||
possibleTargetsSelector.findNewTargets(fromCards);
|
||||
// good targets -- choose as much as possible
|
||||
for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
|
||||
target.add(item.getId(), game);
|
||||
isAddedSomething = true;
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -192,9 +179,9 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
break;
|
||||
}
|
||||
target.add(item.getId(), game);
|
||||
isAddedSomething = true;
|
||||
}
|
||||
return isAddedSomething;
|
||||
|
||||
return target.isChosen(game) && !target.getTargets().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -273,147 +260,154 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
|
||||
@Override
|
||||
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
|
||||
// TODO: make same code for chooseTarget (without filter and target type dependence)
|
||||
|
||||
// nothing to choose, e.g. X=0
|
||||
target.prepareAmount(source, game);
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UUID sourceId = source != null ? source.getSourceId() : null;
|
||||
|
||||
// sometimes a target selection can be made from a player that does not control the ability
|
||||
UUID abilityControllerId = playerId;
|
||||
if (target.getTargetController() != null
|
||||
&& target.getAbilityController() != null) {
|
||||
abilityControllerId = target.getAbilityController();
|
||||
}
|
||||
|
||||
// process multiple opponents by random
|
||||
List<UUID> opponents = new ArrayList<>(game.getOpponents(getId(), true));
|
||||
Collections.shuffle(opponents);
|
||||
|
||||
List<Permanent> targets;
|
||||
|
||||
// ONE KILL PRIORITY: player -> planeswalker -> creature
|
||||
if (outcome == Outcome.Damage) {
|
||||
// player kill
|
||||
for (UUID opponentId : opponents) {
|
||||
Player opponent = game.getPlayer(opponentId);
|
||||
if (opponent != null
|
||||
&& target.canTarget(abilityControllerId, opponentId, source, game)
|
||||
&& opponent.getLife() <= target.getAmountRemaining()) {
|
||||
return tryAddTarget(target, opponentId, opponent.getLife(), source, game);
|
||||
}
|
||||
}
|
||||
|
||||
// permanents kill
|
||||
for (UUID opponentId : opponents) {
|
||||
targets = threats(opponentId, source, StaticFilters.FILTER_PERMANENT_CREATURE_OR_PLANESWALKER_A, game, target.getTargets());
|
||||
|
||||
// planeswalker kill
|
||||
for (Permanent permanent : targets) {
|
||||
if (permanent.isPlaneswalker(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
int loy = permanent.getCounters(game).getCount(CounterType.LOYALTY);
|
||||
if (loy <= target.getAmountRemaining()) {
|
||||
return tryAddTarget(target, permanent.getId(), loy, source, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// creature kill
|
||||
for (Permanent permanent : targets) {
|
||||
if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
if (permanent.getToughness().getValue() <= target.getAmountRemaining()) {
|
||||
return tryAddTarget(target, permanent.getId(), permanent.getToughness().getValue(), source, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NORMAL PRIORITY: planeswalker -> player -> creature
|
||||
// own permanents will be checked multiple times... that's ok
|
||||
for (UUID opponentId : opponents) {
|
||||
if (outcome.isGood()) {
|
||||
targets = threats(getId(), source, StaticFilters.FILTER_PERMANENT, game, target.getTargets());
|
||||
} else {
|
||||
targets = threats(opponentId, source, StaticFilters.FILTER_PERMANENT, game, target.getTargets());
|
||||
}
|
||||
|
||||
// planeswalkers
|
||||
for (Permanent permanent : targets) {
|
||||
if (permanent.isPlaneswalker(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
|
||||
}
|
||||
}
|
||||
|
||||
// players
|
||||
if (outcome.isGood() && target.canTarget(abilityControllerId, getId(), source, game)) {
|
||||
return tryAddTarget(target, getId(), target.getAmountRemaining(), source, game);
|
||||
}
|
||||
if (!outcome.isGood() && target.canTarget(abilityControllerId, opponentId, source, game)) {
|
||||
return tryAddTarget(target, opponentId, target.getAmountRemaining(), source, game);
|
||||
}
|
||||
|
||||
// creature
|
||||
for (Permanent permanent : targets) {
|
||||
if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BAD PRIORITY, e.g. need bad target on yourself or good target on opponent
|
||||
// priority: creature (non killable, killable) -> planeswalker -> player
|
||||
if (!target.isRequired(sourceId, game)) {
|
||||
if (target.getMaxNumberOfTargets() == 0 && target.getMinNumberOfTargets() == 0) {
|
||||
return false;
|
||||
}
|
||||
for (UUID opponentId : opponents) {
|
||||
if (!outcome.isGood()) {
|
||||
// bad on yourself, uses the weakest targets
|
||||
targets = threats(getId(), source, StaticFilters.FILTER_PERMANENT, game, target.getTargets(), false);
|
||||
} else {
|
||||
targets = threats(opponentId, source, StaticFilters.FILTER_PERMANENT, game, target.getTargets(), false);
|
||||
}
|
||||
|
||||
// creatures - non killable (TODO: add extra skill checks like indestructible)
|
||||
for (Permanent permanent : targets) {
|
||||
if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
int safeDamage = Math.min(permanent.getToughness().getValue() - 1, target.getAmountRemaining());
|
||||
if (safeDamage > 0) {
|
||||
return tryAddTarget(target, permanent.getId(), safeDamage, source, game);
|
||||
UUID abilityControllerId = target.getAffectedAbilityControllerId(getId());
|
||||
|
||||
// nothing to choose, e.g. X=0
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PossibleTargetsSelector possibleTargetsSelector = new PossibleTargetsSelector(outcome, target, abilityControllerId, source, game);
|
||||
possibleTargetsSelector.findNewTargets(null);
|
||||
|
||||
// nothing to choose, e.g. no valid targets
|
||||
if (!possibleTargetsSelector.hasAnyTargets()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// KILL PRIORITY
|
||||
if (outcome == Outcome.Damage) {
|
||||
// opponent first
|
||||
for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
break;
|
||||
}
|
||||
if (target.contains(item.getId()) || !(item instanceof Player)) {
|
||||
continue;
|
||||
}
|
||||
int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
|
||||
if (leftLife > 0 && leftLife <= target.getAmountRemaining()) {
|
||||
target.addTarget(item.getId(), leftLife, source, game);
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// creatures - all
|
||||
for (Permanent permanent : targets) {
|
||||
if (permanent.isCreature(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
|
||||
// opponent's creatures second
|
||||
for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
break;
|
||||
}
|
||||
if (target.contains(item.getId()) || (item instanceof Player)) {
|
||||
continue;
|
||||
}
|
||||
int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
|
||||
if (leftLife > 0 && leftLife <= target.getAmountRemaining()) {
|
||||
target.addTarget(item.getId(), leftLife, source, game);
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// planeswalkers
|
||||
for (Permanent permanent : targets) {
|
||||
if (permanent.isPlaneswalker(game) && target.canTarget(abilityControllerId, permanent.getId(), source, game)) {
|
||||
return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game);
|
||||
// opponent's any
|
||||
for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
break;
|
||||
}
|
||||
if (target.contains(item.getId())) {
|
||||
continue;
|
||||
}
|
||||
target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// own - non-killable
|
||||
for (MageItem item : possibleTargetsSelector.getBadTargets()) {
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
break;
|
||||
}
|
||||
if (target.contains(item.getId())) {
|
||||
continue;
|
||||
}
|
||||
// stop as fast as possible on bad outcome
|
||||
if (target.isChosen(game)) {
|
||||
return !target.getTargets().isEmpty();
|
||||
}
|
||||
int leftLife = PossibleTargetsComparator.getLifeForDamage(item, game);
|
||||
if (leftLife > 1) {
|
||||
target.addTarget(item.getId(), Math.min(leftLife - 1, target.getAmountRemaining()), source, game);
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// own - any
|
||||
for (MageItem item : possibleTargetsSelector.getBadTargets()) {
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
break;
|
||||
}
|
||||
if (target.contains(item.getId())) {
|
||||
continue;
|
||||
}
|
||||
// stop as fast as possible on bad outcome
|
||||
if (target.isChosen(game)) {
|
||||
return !target.getTargets().isEmpty();
|
||||
}
|
||||
target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return target.isChosen(game);
|
||||
}
|
||||
// players
|
||||
for (UUID opponentId : opponents) {
|
||||
if (target.canTarget(abilityControllerId, getId(), source, game)) {
|
||||
// on itself
|
||||
return tryAddTarget(target, getId(), target.getAmountRemaining(), source, game);
|
||||
} else if (target.canTarget(abilityControllerId, opponentId, source, game)) {
|
||||
// on opponent
|
||||
return tryAddTarget(target, opponentId, target.getAmountRemaining(), source, game);
|
||||
|
||||
// non-damage effect like counters - give all to first valid item
|
||||
for (MageItem item : possibleTargetsSelector.getGoodTargets()) {
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
break;
|
||||
}
|
||||
if (target.contains(item.getId())) {
|
||||
continue;
|
||||
}
|
||||
target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (MageItem item : possibleTargetsSelector.getBadTargets()) {
|
||||
if (target.getAmountRemaining() <= 0) {
|
||||
break;
|
||||
}
|
||||
if (target.contains(item.getId())) {
|
||||
continue;
|
||||
}
|
||||
// stop as fast as possible on bad outcome
|
||||
if (target.isChosen(game)) {
|
||||
return !target.getTargets().isEmpty();
|
||||
}
|
||||
target.addTarget(item.getId(), target.getAmountRemaining(), source, game);
|
||||
if (target.isChoiceCompleted(abilityControllerId, source, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// it's ok on no targets available
|
||||
log.warn("No proper AI target handling or can't find permanents/cards to target: " + target.getClass().getName());
|
||||
return false;
|
||||
return target.isChosen(game) && !target.getTargets().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1451,9 +1445,9 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
|
||||
// sometimes a target selection can be made from a player that does not control the ability
|
||||
UUID abilityControllerId = playerId;
|
||||
if (target != null && target.getAbilityController() != null) {
|
||||
abilityControllerId = target.getAbilityController();
|
||||
UUID abilityControllerId = this.getId();
|
||||
if (target != null) {
|
||||
abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
|
||||
}
|
||||
|
||||
Card bestCard = null;
|
||||
|
|
@ -1490,9 +1484,9 @@ public class ComputerPlayer extends PlayerImpl {
|
|||
}
|
||||
|
||||
// sometimes a target selection can be made from a player that does not control the ability
|
||||
UUID abilityControllerId = playerId;
|
||||
if (target != null && target.getAbilityController() != null) {
|
||||
abilityControllerId = target.getAbilityController();
|
||||
UUID abilityControllerId = this.getId();
|
||||
if (target != null) {
|
||||
abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
|
||||
}
|
||||
|
||||
Card worstCard = null;
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ public class PossibleTargetsComparator {
|
|||
}
|
||||
}
|
||||
|
||||
private int getScoreFromLife(MageItem item) {
|
||||
// TODO: replace permanent/card life by battlefield score?
|
||||
public static int getLifeForDamage(MageItem item, Game game) {
|
||||
int res = 0;
|
||||
if (item instanceof Player) {
|
||||
res = ((Player) item).getLife();
|
||||
|
|
@ -71,12 +70,16 @@ public class PossibleTargetsComparator {
|
|||
}
|
||||
res = Math.max(0, card.getToughness().getValue() - damage);
|
||||
}
|
||||
// instant
|
||||
if (res == 0) {
|
||||
res = card.getManaValue();
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private int getScoreFromLife(MageItem item) {
|
||||
// TODO: replace permanent/card life by battlefield score?
|
||||
int res = getLifeForDamage(item, game);
|
||||
if (res == 0 && item instanceof Card) {
|
||||
res = ((Card) item).getManaValue();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
@ -139,13 +142,6 @@ public class PossibleTargetsComparator {
|
|||
.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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -85,13 +85,13 @@ public class PossibleTargetsSelector {
|
|||
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.me.sort(comparators.ANY_MOST_VALUABLE_FIRST);
|
||||
this.opponents.sort(comparators.ANY_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.me.sort(comparators.ANY_MOST_VALUABLE_LAST);
|
||||
this.opponents.sort(comparators.ANY_MOST_VALUABLE_FIRST);
|
||||
this.any.sort(comparators.ANY_MOST_VALUABLE_LAST);
|
||||
}
|
||||
}
|
||||
|
|
@ -181,4 +181,8 @@ public class PossibleTargetsSelector {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean hasAnyTargets() {
|
||||
return !this.any.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,10 @@ public final class MagicAbility {
|
|||
put(DoubleStrikeAbility.getInstance().getRule(), 100);
|
||||
put(new ExaltedAbility().getRule(), 10);
|
||||
put(FirstStrikeAbility.getInstance().getRule(), 50);
|
||||
put(FlashAbility.getInstance().getRule(), 0);
|
||||
put(FlashAbility.getInstance().getRule(), 20);
|
||||
put(FlyingAbility.getInstance().getRule(), 50);
|
||||
put(new ForestwalkAbility().getRule(), 10);
|
||||
put(HasteAbility.getInstance().getRule(), 0);
|
||||
put(HasteAbility.getInstance().getRule(), 20);
|
||||
put(IndestructibleAbility.getInstance().getRule(), 150);
|
||||
put(InfectAbility.getInstance().getRule(), 60);
|
||||
put(IntimidateAbility.getInstance().getRule(), 50);
|
||||
|
|
@ -47,7 +47,7 @@ public final class MagicAbility {
|
|||
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 2; // more abilities - more score in any use cases
|
||||
}
|
||||
return scores.get(ability.getRule());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue