AI, refactor: removed outdated and unused code like simple priority and combat implementation, simple combat damage calculator, etc (part of #13638, #13766)

This commit is contained in:
Oleg Agafonov 2025-06-27 16:37:07 +04:00
parent 64bfa21b0c
commit 135c594de1
8 changed files with 29 additions and 901 deletions

View file

@ -3,10 +3,6 @@ package mage.player.ai;
import mage.*;
import mage.abilities.*;
import mage.abilities.costs.mana.*;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect;
import mage.abilities.keyword.*;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.abilities.mana.ManaOptions;
import mage.cards.Card;
@ -20,21 +16,12 @@ import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.choices.Choice;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreatureForCombatBlock;
import mage.filter.common.FilterLandCard;
import mage.filter.common.FilterNonlandCard;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.events.GameEvent;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.tournament.Tournament;
import mage.player.ai.simulators.CombatGroupSimulator;
import mage.player.ai.simulators.CombatSimulator;
import mage.player.ai.simulators.CreatureSimulator;
import mage.players.ManaPoolItem;
import mage.players.Player;
import mage.players.PlayerImpl;
@ -51,14 +38,16 @@ import java.util.*;
import java.util.Map.Entry;
/**
* AI: basic server side bot with simple actions support (game, draft, construction/sideboarding)
* AI: basic server side bot with simple actions support (game, draft, construction/sideboarding).
* Full and minimum implementation of all choose dialogs to allow AI to start and finish a real game.
* Used as parent class for any AI implementations.
* <p>
*
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public class ComputerPlayer extends PlayerImpl {
private static final Logger log = Logger.getLogger(ComputerPlayer.class);
private static final Logger logger = Logger.getLogger(ComputerPlayer.class);
protected static final int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available
@ -78,11 +67,7 @@ public class ComputerPlayer extends PlayerImpl {
final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 1;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5;
// TODO: delete after target rework
private final transient Map<Mana, Card> unplayable = new TreeMap<>();
private final transient List<Card> playableNonInstant = new ArrayList<>();
private final transient List<Card> playableInstant = new ArrayList<>();
private final transient List<ActivatedAbility> playableAbilities = new ArrayList<>();
// remember picked cards for better draft choices
private final transient List<PickedCard> pickedCards = new ArrayList<>();
private final transient List<ColoredManaSymbol> chosenColors = new ArrayList<>();
@ -94,8 +79,6 @@ public class ComputerPlayer extends PlayerImpl {
// For stopping infinite loops when trying to pay Phyrexian mana when the player can't spend life and no other sources are available
private transient boolean alreadyTryingToPayPhyrexian;
private transient long lastThinkTime = 0; // time in ms for last AI actions calc
public ComputerPlayer(String name, RangeOfInfluence range) {
super(name, range);
human = false;
@ -228,36 +211,6 @@ public class ComputerPlayer extends PlayerImpl {
return makeChoice(outcome, target, source, game, null);
}
@Deprecated // TODO: replace by source only version
protected Card selectCard(UUID abilityControllerId, List<Card> cards, Outcome outcome, Target target, Game game) {
return selectCardInner(abilityControllerId, cards, outcome, target, null, game);
}
/**
* @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
*/
protected Card selectCardInner(UUID abilityControllerId, List<Card> cards, Outcome outcome, Target target, Ability targetingSource, Game game) {
Card card;
while (!cards.isEmpty()) {
if (outcome.isGood()) {
card = selectBestCardInner(cards, Collections.emptyList(), target, targetingSource, game);
} else {
card = selectWorstCardInner(cards, Collections.emptyList(), target, targetingSource, game);
}
if (!target.getTargets().contains(card.getId())) {
if (targetingSource != null) {
if (target.canTarget(abilityControllerId, card.getId(), targetingSource, game)) {
return card;
}
} else {
return card;
}
}
cards.remove(card); // TODO: research parent code - is it depends on original list? Can be bugged
}
return null;
}
@Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
@ -412,247 +365,9 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public boolean priority(Game game) {
game.resumeTimer(getTurnControlledBy());
boolean result = priorityPlay(game);
game.pauseTimer(getTurnControlledBy());
return result;
}
private boolean priorityPlay(Game game) {
// TODO: simplify and delete, never called in real game due ComputerPlayer7's simulations usage
UUID opponentId = getRandomOpponent(game);
if (game.isActivePlayer(playerId)) {
if (game.isMainPhase() && game.getStack().isEmpty()) {
playLand(game);
}
switch (game.getTurnStepType()) {
case UPKEEP:
// TODO: is it needs here? Need research (e.g. for better choose in upkeep triggers)?
findPlayables(game);
break;
case DRAW:
break;
case PRECOMBAT_MAIN:
findPlayables(game);
if (!playableAbilities.isEmpty()) {
for (ActivatedAbility ability : playableAbilities) {
if (ability.canActivate(playerId, game).canActivate()) {
if (ability.getEffects().hasOutcome(ability, Outcome.PutLandInPlay)) {
if (this.activateAbility(ability, game)) {
return true;
}
}
if (ability.getEffects().hasOutcome(ability, Outcome.PutCreatureInPlay)) {
if (getOpponentBlockers(opponentId, game).size() <= 1) {
if (this.activateAbility(ability, game)) {
return true;
}
}
}
}
}
}
break;
case DECLARE_BLOCKERS:
findPlayables(game);
playRemoval(game.getCombat().getBlockers(), game);
playDamage(game.getCombat().getBlockers(), game);
break;
case END_COMBAT:
findPlayables(game);
playDamage(game.getCombat().getBlockers(), game);
break;
case POSTCOMBAT_MAIN:
findPlayables(game);
if (game.getStack().isEmpty()) {
if (!playableNonInstant.isEmpty()) {
for (Card card : playableNonInstant) {
if (card.getSpellAbility().canActivate(playerId, game).canActivate()) {
if (this.activateAbility(card.getSpellAbility(), game)) {
return true;
}
}
}
}
if (!playableAbilities.isEmpty()) {
for (ActivatedAbility ability : playableAbilities) {
if (ability.canActivate(playerId, game).canActivate()) {
if (!(ability.getEffects().get(0) instanceof BecomesCreatureSourceEffect)) {
if (this.activateAbility(ability, game)) {
return true;
}
}
}
}
}
}
break;
}
} else {
//respond to opponent events
switch (game.getTurnStepType()) {
case UPKEEP:
findPlayables(game);
break;
case DECLARE_ATTACKERS:
findPlayables(game);
playRemoval(game.getCombat().getAttackers(), game);
playDamage(game.getCombat().getAttackers(), game);
break;
case END_COMBAT:
findPlayables(game);
playDamage(game.getCombat().getAttackers(), game);
break;
}
}
// minimum implementation for do nothing
pass(game);
return true;
} // end priorityPlay method
protected void playLand(Game game) {
Set<Card> lands = new LinkedHashSet<>();
for (Card landCard : hand.getCards(new FilterLandCard(), game)) {
// remove lands that can not be played
boolean canPlay = false;
for (Ability ability : landCard.getAbilities(game)) {
if (ability instanceof PlayLandAbility) {
if (!game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, landCard.getId(), ability, playerId), null, game, true)) {
canPlay = true;
}
}
}
if (canPlay) {
lands.add(landCard);
}
}
while (!lands.isEmpty() && this.canPlayLand()) {
if (lands.size() == 1) {
this.playLand(lands.iterator().next(), game, false);
} else {
playALand(lands, game);
}
}
}
protected void playALand(Set<Card> lands, Game game) {
//play a land that will allow us to play an unplayable
for (Mana mana : unplayable.keySet()) {
for (Card card : lands) {
for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.BATTLEFIELD)) {
for (Mana netMana : ability.getNetMana(game)) {
if (netMana.enough(mana)) {
this.playLand(card, game, false);
lands.remove(card);
return;
}
}
}
}
}
//play a land that will get us closer to playing an unplayable
for (Mana mana : unplayable.keySet()) {
for (Card card : lands) {
for (ActivatedManaAbilityImpl ability : card.getAbilities(game).getActivatedManaAbilities(Zone.BATTLEFIELD)) {
for (Mana netMana : ability.getNetMana(game)) {
if (mana.contains(netMana)) {
this.playLand(card, game, false);
lands.remove(card);
return;
}
}
}
}
}
//play first available land
this.playLand(lands.iterator().next(), game, false);
lands.remove(lands.iterator().next());
}
@Deprecated // TODO: delete after target rework
protected void findPlayables(Game game) {
playableInstant.clear();
playableNonInstant.clear();
unplayable.clear();
playableAbilities.clear();
Set<Card> nonLands = hand.getCards(new FilterNonlandCard(), game);
ManaOptions available = getManaAvailable(game);
// available.addMana(manaPool.getMana());
for (Card card : nonLands) {
ManaOptions options = card.getManaCost().getOptions();
if (!card.getManaCost().getVariableCosts().isEmpty()) {
//don't use variable mana costs unless there is at least 3 extra mana for X
for (Mana option : options) {
option.add(Mana.GenericMana(3));
}
}
for (Mana mana : options) {
for (Mana avail : available) {
if (mana.enough(avail)) {
SpellAbility ability = card.getSpellAbility();
GameEvent castEvent = GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability, playerId);
castEvent.setZone(game.getState().getZone(card.getMainCard().getId()));
if (ability != null && ability.canActivate(playerId, game).canActivate()
&& !game.getContinuousEffects().preventedByRuleModification(castEvent, ability, game, true)) {
if (card.isInstant(game)
|| card.hasAbility(FlashAbility.getInstance(), game)) {
playableInstant.add(card);
} else {
playableNonInstant.add(card);
}
}
} else if (!playableInstant.contains(card) && !playableNonInstant.contains(card)) {
unplayable.put(mana.needed(avail), card);
}
}
}
}
// TODO: wtf?! change to player.getPlayable
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) {
for (ActivatedAbility ability : permanent.getAbilities().getActivatedAbilities(Zone.BATTLEFIELD)) {
if (!ability.isManaActivatedAbility() && ability.canActivate(playerId, game).canActivate()) {
if (ability instanceof EquipAbility && permanent.getAttachedTo() != null) {
continue;
}
ManaOptions abilityOptions = ability.getManaCosts().getOptions();
if (!ability.getManaCosts().getVariableCosts().isEmpty()) {
//don't use variable mana costs unless there is at least 3 extra mana for X
for (Mana option : abilityOptions) {
option.add(Mana.GenericMana(3));
}
}
if (abilityOptions.isEmpty()) {
playableAbilities.add(ability);
} else {
for (Mana mana : abilityOptions) {
for (Mana avail : available) {
if (mana.enough(avail)) {
playableAbilities.add(ability);
}
}
}
}
}
}
}
for (Card card : graveyard.getCards(game)) {
for (ActivatedAbility ability : card.getAbilities(game).getActivatedAbilities(Zone.GRAVEYARD)) {
if (ability.canActivate(playerId, game).canActivate()) {
ManaOptions abilityOptions = ability.getManaCosts().getOptions();
if (abilityOptions.isEmpty()) {
playableAbilities.add(ability);
} else {
for (Mana mana : abilityOptions) {
for (Mana avail : available) {
if (mana.enough(avail)) {
playableAbilities.add(ability);
}
}
}
}
}
}
}
return false;
}
@Override
@ -1053,7 +768,9 @@ public class ComputerPlayer extends PlayerImpl {
// choose creature type
// TODO: WTF?! Creature types dialog text can changes, need to replace that code
if (choice.getMessage() != null && (choice.getMessage().equals("Choose creature type") || choice.getMessage().equals("Choose a creature type"))) {
chooseCreatureType(outcome, choice, game);
if (chooseCreatureType(outcome, choice, game)) {
return true;
}
}
// choose the correct color to pay a spell (use last unpaid ability for color hint)
@ -1170,39 +887,12 @@ public class ComputerPlayer extends PlayerImpl {
@Override
public void selectAttackers(Game game, UUID attackingPlayerId) {
UUID opponentId = game.getCombat().getDefenders().iterator().next();
Attackers attackers = getPotentialAttackers(game);
List<Permanent> blockers = getOpponentBlockers(opponentId, game);
List<Permanent> actualAttackers = new ArrayList<>();
if (blockers.isEmpty()) {
actualAttackers = attackers.getAttackers();
} else if (attackers.size() - blockers.size() >= game.getPlayer(opponentId).getLife()) {
actualAttackers = attackers.getAttackers();
} else {
CombatSimulator combat = simulateAttack(attackers, blockers, opponentId, game);
if (combat.rating > 2) {
for (CombatGroupSimulator group : combat.groups) {
this.declareAttacker(group.attackers.get(0).id, group.defenderId, game, false);
}
}
}
for (Permanent attacker : actualAttackers) {
this.declareAttacker(attacker.getId(), opponentId, game, false);
}
// do nothing, parent class must implement it
}
@Override
public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) {
List<Permanent> blockers = getAvailableBlockers(game);
CombatSimulator sim = simulateBlock(CombatSimulator.load(game), blockers, game);
List<CombatGroup> groups = game.getCombat().getGroups();
for (int i = 0; i < groups.size(); i++) {
for (CreatureSimulator creature : sim.groups.get(i).blockers) {
groups.get(i).addBlocker(creature.id, playerId, game);
}
}
// do nothing, parent class must implement it
}
@Override
@ -1431,25 +1121,11 @@ public class ComputerPlayer extends PlayerImpl {
tournament.submitDeck(playerId, deck);
}
@Deprecated // TODO: replace by source only version
public Card selectBestCard(List<Card> cards, List<ColoredManaSymbol> chosenColors) {
return selectBestCardInner(cards, chosenColors, null, null, null);
}
/**
* @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
*/
public Card selectBestCardInner(List<Card> cards, List<ColoredManaSymbol> chosenColors, Target target, Ability targetingSource, Game game) {
public Card makePickCard(List<Card> cards, List<ColoredManaSymbol> chosenColors) {
if (cards.isEmpty()) {
return null;
}
// sometimes a target selection can be made from a player that does not control the ability
UUID abilityControllerId = this.getId();
if (target != null) {
abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
}
Card bestCard = null;
int maxScore = 0;
for (Card card : cards) {
@ -1458,13 +1134,7 @@ public class ComputerPlayer extends PlayerImpl {
if (bestCard == null) { // we need any card to prevent NPE in callers
betterCard = true;
} else if (score > maxScore) { // we need better card
if (target != null && targetingSource != null && game != null) {
// but also check it can be targeted
betterCard = target.canTarget(abilityControllerId, card.getId(), targetingSource, game);
} else {
// target object wasn't provided, so accepting it anyway
betterCard = true;
}
betterCard = true;
}
// is it better than previous one?
if (betterCard) {
@ -1475,59 +1145,21 @@ public class ComputerPlayer extends PlayerImpl {
return bestCard;
}
/**
* @param targetingSource null on non-target choice like choose and source on targeting choice like chooseTarget
*/
public Card selectWorstCardInner(List<Card> cards, List<ColoredManaSymbol> chosenColors, Target target, Ability targetingSource, Game game) {
if (cards.isEmpty()) {
return null;
}
// sometimes a target selection can be made from a player that does not control the ability
UUID abilityControllerId = this.getId();
if (target != null) {
abilityControllerId = target.getAffectedAbilityControllerId(this.getId());
}
Card worstCard = null;
int minScore = Integer.MAX_VALUE;
for (Card card : cards) {
int score = RateCard.rateCard(card, chosenColors);
boolean worseCard = false;
if (worstCard == null) { // we need any card to prevent NPE in callers
worseCard = true;
} else if (score < minScore) { // we need worse card
if (target != null && targetingSource != null && game != null) {
// but also check it can be targeted
worseCard = target.canTarget(abilityControllerId, card.getId(), targetingSource, game);
} else {
// target object wasn't provided, so accepting it anyway
worseCard = true;
}
}
// is it worse than previous one?
if (worseCard) {
minScore = score;
worstCard = card;
}
}
return worstCard;
}
@Override
public void pickCard(List<Card> cards, Deck deck, Draft draft) {
// method used by DRAFT bot too
if (cards.isEmpty()) {
throw new IllegalArgumentException("No cards to pick from.");
}
try {
Card bestCard = selectBestCard(cards, chosenColors);
Card bestCard = makePickCard(cards, chosenColors);
int maxScore = RateCard.rateCard(bestCard, chosenColors);
int pickedCardRate = RateCard.getBaseCardScore(bestCard);
if (pickedCardRate <= 30) {
// if card is bad
// try to counter pick without any color restriction
Card counterPick = selectBestCard(cards, Collections.emptyList());
Card counterPick = makePickCard(cards, Collections.emptyList());
int counterPickScore = RateCard.getBaseCardScore(counterPick);
// card is perfect
// take it!
@ -1552,10 +1184,9 @@ public class ComputerPlayer extends PlayerImpl {
colors += symbol.toString();
}
}
log.debug("[DEBUG] AI picked: " + bestCard.getName() + ", score=" + maxScore + ", deck colors=" + colors);
draft.addPick(playerId, bestCard.getId(), null);
} catch (Exception e) {
log.debug("Exception during AI pick card for draft playerId= " + getId());
logger.error("Error during AI pick card for draft playerId = " + getId(), e);
draft.addPick(playerId, cards.get(0).getId(), null);
}
}
@ -1629,100 +1260,6 @@ public class ComputerPlayer extends PlayerImpl {
}
}
protected Attackers getPotentialAttackers(Game game) {
Attackers attackers = new Attackers();
List<Permanent> creatures = super.getAvailableAttackers(game);
for (Permanent creature : creatures) {
int potential = combatPotential(creature, game);
if (potential > 0 && creature.getPower().getValue() > 0) {
List<Permanent> l = attackers.get(potential);
if (l == null) {
attackers.put(potential, l = new ArrayList<>());
}
l.add(creature);
}
}
return attackers;
}
protected int combatPotential(Permanent creature, Game game) {
if (!creature.canAttack(null, game)) {
return 0;
}
int potential = creature.getPower().getValue();
potential += creature.getAbilities().getEvasionAbilities().size();
potential += creature.getAbilities().getProtectionAbilities().size();
potential += creature.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) ? 1 : 0;
potential += creature.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId()) ? 2 : 0;
potential += creature.getAbilities().containsKey(TrampleAbility.getInstance().getId()) ? 1 : 0;
return potential;
}
protected List<Permanent> getOpponentBlockers(UUID opponentId, Game game) {
FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock();
return game.getBattlefield().getAllActivePermanents(blockFilter, opponentId, game);
}
protected CombatSimulator simulateAttack(Attackers attackers, List<Permanent> blockers, UUID opponentId, Game game) {
List<Permanent> attackersList = attackers.getAttackers();
CombatSimulator best = new CombatSimulator();
int bestResult = 0;
//use binary digits to calculate powerset of attackers
int powerElements = (int) Math.pow(2, attackersList.size());
for (int i = 1; i < powerElements; i++) {
String binary = Integer.toBinaryString(i);
while (binary.length() < attackersList.size()) {
binary = '0' + binary;
}
List<Permanent> trialAttackers = new ArrayList<>();
for (int j = 0; j < attackersList.size(); j++) {
if (binary.charAt(j) == '1') {
trialAttackers.add(attackersList.get(j));
}
}
CombatSimulator combat = new CombatSimulator();
for (Permanent permanent : trialAttackers) {
combat.groups.add(new CombatGroupSimulator(opponentId, Arrays.asList(permanent.getId()), new ArrayList<UUID>(), game));
}
CombatSimulator test = simulateBlock(combat, blockers, game);
if (test.evaluate() > bestResult) {
best = test;
bestResult = test.evaluate();
}
}
return best;
}
protected CombatSimulator simulateBlock(CombatSimulator combat, List<Permanent> blockers, Game game) {
TreeNode<CombatSimulator> simulations;
simulations = new TreeNode<>(combat);
addBlockSimulations(blockers, simulations, game);
combat.simulate(game);
return getWorstSimulation(simulations);
}
protected void addBlockSimulations(List<Permanent> blockers, TreeNode<CombatSimulator> node, Game game) {
int numGroups = node.getData().groups.size();
Copier<CombatSimulator> copier = new Copier<>();
for (Permanent blocker : blockers) {
List<Permanent> subList = remove(blockers, blocker);
for (int i = 0; i < numGroups; i++) {
if (node.getData().groups.get(i).canBlock(blocker, game)) {
CombatSimulator combat = copier.copy(node.getData());
combat.groups.get(i).blockers.add(new CreatureSimulator(blocker));
TreeNode<CombatSimulator> child = new TreeNode<>(combat);
node.addChild(child);
addBlockSimulations(subList, child, game);
combat.simulate(game);
}
}
}
}
protected List<Permanent> remove(List<Permanent> source, Permanent element) {
List<Permanent> newList = new ArrayList<>();
for (Permanent permanent : source) {
@ -1733,106 +1270,13 @@ public class ComputerPlayer extends PlayerImpl {
return newList;
}
protected CombatSimulator getBestSimulation(TreeNode<CombatSimulator> simulations) {
CombatSimulator best = simulations.getData();
int bestResult = best.evaluate();
for (TreeNode<CombatSimulator> node : simulations.getChildren()) {
CombatSimulator bestSub = getBestSimulation(node);
if (bestSub.evaluate() > bestResult) {
best = node.getData();
bestResult = best.evaluate();
}
}
return best;
}
protected CombatSimulator getWorstSimulation(TreeNode<CombatSimulator> simulations) {
CombatSimulator worst = simulations.getData();
int worstResult = worst.evaluate();
for (TreeNode<CombatSimulator> node : simulations.getChildren()) {
CombatSimulator worstSub = getWorstSimulation(node);
if (worstSub.evaluate() < worstResult) {
worst = node.getData();
worstResult = worst.evaluate();
}
}
return worst;
}
protected List<Permanent> threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List<UUID> targets) {
return threats(playerId, source, filter, game, targets, true);
}
protected List<Permanent> threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List<UUID> targets, boolean mostValuableGoFirst) {
// most valuable/powerfully permanents goes at first
List<Permanent> threats;
if (playerId == null) {
threats = game.getBattlefield().getActivePermanents(filter, this.getId(), source, game); // all permanents within the range of the player
} else {
FilterPermanent filterCopy = filter.copy();
filterCopy.add(new ControllerIdPredicate(playerId));
threats = game.getBattlefield().getActivePermanents(filter, this.getId(), source, game);
}
Iterator<Permanent> it = threats.iterator();
while (it.hasNext()) { // remove permanents already targeted
Permanent test = it.next();
if (targets.contains(test.getId()) || (playerId != null && !test.getControllerId().equals(playerId))) {
it.remove();
}
}
Collections.sort(threats, new PermanentComparator(game));
if (mostValuableGoFirst) {
Collections.reverse(threats);
}
return threats;
}
protected void logList(String message, List<MageObject> list) {
StringBuilder sb = new StringBuilder();
sb.append(message).append(": ");
for (MageObject object : list) {
sb.append(object.getName()).append(',');
}
log.info(sb.toString());
}
private void playRemoval(Set<UUID> creatures, Game game) {
for (UUID creatureId : creatures) {
for (Card card : this.playableInstant) {
if (card.getSpellAbility().canActivate(playerId, game).canActivate()) {
for (Effect effect : card.getSpellAbility().getEffects()) {
if (effect.getOutcome() == Outcome.DestroyPermanent || effect.getOutcome() == Outcome.ReturnToHand) {
if (card.getSpellAbility().getTargets().get(0).canTarget(creatureId, card.getSpellAbility(), game)) {
if (this.activateAbility(card.getSpellAbility(), game)) {
return;
}
}
}
}
}
}
}
}
private void playDamage(Set<UUID> creatures, Game game) {
for (UUID creatureId : creatures) {
Permanent creature = game.getPermanent(creatureId);
for (Card card : this.playableInstant) {
if (card.getSpellAbility().canActivate(playerId, game).canActivate()) {
for (Effect effect : card.getSpellAbility().getEffects()) {
if (effect instanceof DamageTargetEffect) {
if (card.getSpellAbility().getTargets().get(0).canTarget(creatureId, card.getSpellAbility(), game)) {
if (((DamageTargetEffect) effect).getAmount() > (creature.getPower().getValue() - creature.getDamage())) {
if (this.activateAbility(card.getSpellAbility(), game)) {
return;
}
}
}
}
}
}
}
}
logger.info(sb.toString());
}
@Override
@ -1845,22 +1289,6 @@ public class ComputerPlayer extends PlayerImpl {
return new ComputerPlayer(this);
}
private boolean tryAddTarget(Target target, UUID id, int amount, Ability source, Game game) {
// workaround to check successfully targets add
int before = target.getTargets().size();
target.addTarget(id, amount, source, game);
int after = target.getTargets().size();
return before != after;
}
/**
* Returns an opponent by random
*/
@Deprecated // TODO: rework all usages and replace to all possible opponents instead single
private UUID getRandomOpponent(Game game) {
return RandomUtil.randomFromCollection(game.getOpponents(getId(), true));
}
@Override
public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) {
Map<UUID, SpellAbility> usable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), card, game.getState().getZone(card.getId()), noMana);
@ -1905,12 +1333,4 @@ public class ComputerPlayer extends PlayerImpl {
// all human players converted to computer and analyse
this.human = false;
}
public long getLastThinkTime() {
return lastThinkTime;
}
public void setLastThinkTime(long lastThinkTime) {
this.lastThinkTime = lastThinkTime;
}
}

View file

@ -1,148 +0,0 @@
package mage.player.ai.simulators;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class CombatGroupSimulator implements Serializable {
public List<CreatureSimulator> attackers = new ArrayList<>();
public List<CreatureSimulator> blockers = new ArrayList<>();
public UUID defenderId;
public boolean defenderIsPlaneswalker;
public int unblockedDamage;
private CreatureSimulator attacker;
public CombatGroupSimulator(UUID defenderId, List<UUID> attackers, List<UUID> blockers, Game game) {
this.defenderId = defenderId;
for (UUID attackerId: attackers) {
Permanent permanent = game.getPermanent(attackerId);
this.attackers.add(new CreatureSimulator(permanent));
}
for (UUID blockerId: blockers) {
Permanent permanent = game.getPermanent(blockerId);
this.blockers.add(new CreatureSimulator(permanent));
}
//NOTE: assumes no banding
attacker = this.attackers.get(0);
}
private boolean hasFirstOrDoubleStrike() {
for (CreatureSimulator creature: attackers) {
if (creature.hasDoubleStrike || creature.hasFirstStrike)
return true;
}
for (CreatureSimulator creature: blockers) {
if (creature.hasDoubleStrike || creature.hasFirstStrike)
return true;
}
return false;
}
public boolean canBlock(Permanent blocker, Game game) {
return blocker.canBlock(attacker.id, game);
}
public void simulateCombat(Game game) {
unblockedDamage = 0;
if (hasFirstOrDoubleStrike())
assignDamage(true, game);
assignDamage(false, game);
}
private void assignDamage(boolean first, Game game) {
if (blockers.isEmpty()) {
if (canDamage(attacker, first))
unblockedDamage += attacker.power;
}
else if (blockers.size() == 1) {
CreatureSimulator blocker = blockers.get(0);
if (canDamage(attacker, first)) {
if (attacker.hasTrample) {
int lethalDamage = blocker.getLethalDamage(game);
if (attacker.power > lethalDamage) {
blocker.damage += lethalDamage;
unblockedDamage += attacker.power - lethalDamage;
}
else {
blocker.damage += attacker.power;
}
}
}
if (canDamage(blocker, first)) {
attacker.damage += blocker.power;
}
}
else {
int damage = attacker.power;
for (CreatureSimulator blocker: blockers) {
if (damage > 0 && canDamage(attacker, first)) {
int lethalDamage = blocker.getLethalDamage(game);
if (damage > lethalDamage) {
blocker.damage += lethalDamage;
damage -= lethalDamage;
}
else {
blocker.damage += damage;
damage = 0;
}
}
if (canDamage(blocker, first)) {
attacker.damage += blocker.power;
}
}
if (damage > 0) {
if (attacker.hasTrample) {
unblockedDamage += damage;
}
else {
blockers.get(0).damage += damage;
}
}
}
}
private boolean canDamage(CreatureSimulator creature, boolean first) {
if (first && (creature.hasFirstStrike || creature.hasDoubleStrike))
return true;
if (!first && (!creature.hasFirstStrike || creature.hasDoubleStrike))
return true;
return false;
}
/**
* returns 3 attacker survives blockers destroyed
* returns 2 both destroyed
* returns 1 both survive
* returns 0 attacker destroyed blockers survive
*
* @return int
*/
public int evaluateCombat() {
int survivingBlockers = 0;
for (CreatureSimulator blocker: blockers) {
if (blocker.damage < blocker.toughness)
survivingBlockers++;
}
if (attacker.isDead()) {
if (survivingBlockers > 0) {
return 0;
}
return 2;
}
else {
if (survivingBlockers > 0) {
return 1;
}
return 3;
}
}
}

View file

@ -1,86 +0,0 @@
package mage.player.ai.simulators;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class CombatSimulator implements Serializable {
public List<CombatGroupSimulator> groups = new ArrayList<>();
public List<UUID> defenders = new ArrayList<>();
public Map<UUID, Integer> playersLife = new HashMap<>();
public Map<UUID, Integer> planeswalkerLoyalty = new HashMap<>();
public UUID attackerId;
public int rating = 0;
public static CombatSimulator load(Game game) {
CombatSimulator simCombat = new CombatSimulator();
for (CombatGroup group: game.getCombat().getGroups()) {
simCombat.groups.add(new CombatGroupSimulator(group.getDefenderId(), group.getAttackers(), group.getBlockers(), game));
}
for (UUID defenderId: game.getCombat().getDefenders()) {
simCombat.defenders.add(defenderId);
Player player = game.getPlayer(defenderId);
if (player != null) {
simCombat.playersLife.put(defenderId, player.getLife());
}
else {
Permanent permanent = game.getPermanent(defenderId);
simCombat.planeswalkerLoyalty.put(defenderId, permanent.getCounters(game).getCount(CounterType.LOYALTY));
}
}
return simCombat;
}
public CombatSimulator() {}
public void clear() {
groups.clear();
defenders.clear();
attackerId = null;
}
public void simulate(Game game) {
for (CombatGroupSimulator group: groups) {
group.simulateCombat(game);
}
}
public int evaluate() {
Map<UUID, Integer> damage = new HashMap<>();
int result = 0;
for (CombatGroupSimulator group: groups) {
if (!damage.containsKey(group.defenderId)) {
damage.put(group.defenderId, group.unblockedDamage);
}
else {
damage.put(group.defenderId, damage.get(group.defenderId) + group.unblockedDamage);
}
}
//check for lethal damage to player
for (Entry<UUID, Integer> entry: playersLife.entrySet()) {
if (damage.containsKey(entry.getKey()) && entry.getValue() <= damage.get(entry.getKey())) {
//TODO: check for protection
//NOTE: not applicable for mulitplayer games
return Integer.MAX_VALUE;
}
}
for (CombatGroupSimulator group: groups) {
result += group.evaluateCombat();
}
rating = result;
return result;
}
}

View file

@ -1,57 +0,0 @@
package mage.player.ai.simulators;
import mage.abilities.keyword.DoubleStrikeAbility;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.io.Serializable;
import java.util.List;
import java.util.UUID;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class CreatureSimulator implements Serializable {
public UUID id;
public int damage;
public int power;
public int toughness;
public boolean hasFirstStrike;
public boolean hasDoubleStrike;
public boolean hasTrample;
public Permanent permanent;
public CreatureSimulator(Permanent permanent) {
this.id = permanent.getId();
this.damage = permanent.getDamage();
this.power = permanent.getPower().getValue();
this.toughness = permanent.getToughness().getValue();
this.hasDoubleStrike = permanent.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId());
this.hasFirstStrike = permanent.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId());
this.hasTrample = permanent.getAbilities().containsKey(TrampleAbility.getInstance().getId());
this.permanent = permanent;
}
public boolean isDead() {
return damage >= toughness;
}
public int getLethalDamage(Game game) {
List<FilterCreaturePermanent> usePowerInsteadOfToughnessForDamageLethalityFilters = game.getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters();
/*
* for handling Zilortha, Strength Incarnate:
* 2020-04-17
* Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on.
*/
boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream()
.anyMatch(filter -> filter.match(permanent, game));
int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ?
// Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isnt destroyed unless it has at least 1 damage marked on it.
Math.max(power, 1) : toughness;
return Math.max(lethalDamageThreshold - damage, 0);
}
}