package mage.player.ai; import mage.ApprovingObject; import mage.ConditionalMana; import mage.MageObject; import mage.Mana; import mage.abilities.*; import mage.abilities.costs.VariableCost; 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; import mage.cards.Cards; import mage.cards.CardsImpl; import mage.cards.RateCard; import mage.cards.decks.Deck; import mage.cards.decks.DeckValidator; import mage.cards.decks.DeckValidatorFactory; import mage.cards.repository.CardCriteria; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; import mage.choices.Choice; import mage.constants.*; import mage.counters.CounterType; import mage.filter.FilterCard; import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.*; 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.stack.Spell; import mage.game.stack.StackObject; 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; import mage.players.net.UserData; import mage.players.net.UserGroup; import mage.target.*; import mage.target.common.*; import mage.util.*; import org.apache.log4j.Logger; import java.io.Serializable; import java.util.*; import java.util.Map.Entry; /** * AI: basic server side bot with simple actions support (game, draft, construction/sideboarding) * * @author BetaSteward_at_googlemail.com, JayDi85 */ public class ComputerPlayer extends PlayerImpl { private static final Logger log = Logger.getLogger(ComputerPlayer.class); private long lastThinkTime = 0; // msecs for last AI actions calc protected int PASSIVITY_PENALTY = 5; // Penalty value for doing nothing if some actions are available // debug only: set TRUE to debug simulation's code/games (on false sim thread will be stopped after few secs by timeout) protected boolean COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS = false; // DebugUtil.AI_ENABLE_DEBUG_MODE; // AI agents uses game simulation thread for all calcs and it's high CPU consumption // More AI threads - more parallel AI games can be calculate // If you catch errors like ConcurrentModificationException, then AI implementation works with wrong data // (e.g. with original game instead copy) or AI use wrong logic (one sim result depends on another sim result) // How-to use: // * 1 for debug or stable // * 5 for good performance on average computer // * use your's CPU cores for best performance // TODO: add server config to control max AI threads (with CPU cores by default) // TODO: rework AI implementation to use multiple sims calculation instead one by one final static int COMPUTER_MAX_THREADS_FOR_SIMULATIONS = 5;//DebugUtil.AI_ENABLE_DEBUG_MODE ? 1 : 5; private final transient Map unplayable = new TreeMap<>(); private final transient List playableNonInstant = new ArrayList<>(); private final transient List playableInstant = new ArrayList<>(); private final transient List playableAbilities = new ArrayList<>(); private final transient List pickedCards = new ArrayList<>(); private final transient List chosenColors = new ArrayList<>(); // keep current paying cost info for choose dialogs // mana abilities must ask payment too, so keep full chain // TODO: make sure it thread safe for AI simulations (all transient fields above and bottom) private final transient Map lastUnpaidMana = new LinkedHashMap<>(); // 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; public ComputerPlayer(String name, RangeOfInfluence range) { super(name, range); human = false; userData = UserData.getDefaultUserDataView(); userData.setAvatarId(64); userData.setGroupId(UserGroup.COMPUTER.getGroupId()); userData.setFlagName("computer.png"); } protected ComputerPlayer(UUID id) { super(id); human = false; userData = UserData.getDefaultUserDataView(); userData.setAvatarId(64); userData.setGroupId(UserGroup.COMPUTER.getGroupId()); userData.setFlagName("computer.png"); } public ComputerPlayer(final ComputerPlayer player) { super(player); } @Override public boolean chooseMulligan(Game game) { log.debug("chooseMulligan"); if (hand.size() < 6 || isTestsMode() // ignore mulligan in tests || game.getClass().getName().contains("Momir") // ignore mulligan in Momir games ) { return false; } Set lands = hand.getCards(new FilterLandCard(), game); return lands.size() < 2 || lands.size() > hand.size() - 2; } @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game) { return choose(outcome, target, source, game, null); } @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) { if (log.isDebugEnabled()) { log.debug("choose: " + outcome.toString() + ':' + target.toString()); } // 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 sourceId = source != null ? source.getSourceId() : null; boolean required = target.isRequired(sourceId, game); Set possibleTargets = target.possibleTargets(abilityControllerId, source, game); if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getNumberOfTargets()) { required = false; } UUID randomOpponentId; if (target.getTargetController() != null) { randomOpponentId = getRandomOpponent(target.getTargetController(), game); } else if (abilityControllerId != null) { randomOpponentId = getRandomOpponent(abilityControllerId, game); } else { randomOpponentId = getRandomOpponent(playerId, game); } if (target.getOriginalTarget() instanceof TargetPlayer) { return setTargetPlayer(outcome, target, null, abilityControllerId, randomOpponentId, game, required); } if (target.getOriginalTarget() instanceof TargetDiscard) { findPlayables(game); // discard not playable first if (!unplayable.isEmpty()) { for (int i = unplayable.size() - 1; i >= 0; i--) { if (target.canTarget(abilityControllerId, unplayable.values().toArray(new Card[0])[i].getId(), null, game)) { target.add(unplayable.values().toArray(new Card[0])[i].getId(), game); if (target.isChosen(game)) { return true; } } } } if (!hand.isEmpty()) { for (int i = 0; i < hand.size(); i++) { if (target.canTarget(abilityControllerId, hand.toArray(new UUID[0])[i], null, game)) { target.add(hand.toArray(new UUID[0])[i], game); if (target.isChosen(game)) { return true; } } } } return false; } if (target.getOriginalTarget() instanceof TargetControlledPermanent || target.getOriginalTarget() instanceof TargetSacrifice) { List targets; TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget(); targets = threats(abilityControllerId, source, origTarget.getFilter(), game, target.getTargets()); if (!outcome.isGood()) { Collections.reverse(targets); } for (Permanent permanent : targets) { if (origTarget.canTarget(abilityControllerId, permanent.getId(), source, game, false) && !target.getTargets().contains(permanent.getId())) { target.add(permanent.getId(), game); if (target.isChosen(game)) { return true; } } } return false; } if (target.getOriginalTarget() instanceof TargetPermanent) { FilterPermanent filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) { filter = (FilterPermanent) target.getOriginalTarget().getFilter(); } if (filter == null) { throw new IllegalStateException("Unsupported permanent filter in computer's choose method: " + target.getOriginalTarget().getClass().getCanonicalName()); } List targets; if (outcome.isCanTargetAll()) { targets = threats(null, source, filter, game, target.getTargets()); } else { if (outcome.isGood()) { targets = threats(abilityControllerId, source, filter, game, target.getTargets()); } else { targets = threats(randomOpponentId, source, filter, game, target.getTargets()); } if (targets.isEmpty() && target.isRequired()) { if (!outcome.isGood()) { targets = threats(abilityControllerId, source, filter, game, target.getTargets()); } else { targets = threats(randomOpponentId, source, filter, game, target.getTargets()); } } } for (Permanent permanent : targets) { if (target.canTarget(abilityControllerId, permanent.getId(), null, game) && !target.getTargets().contains(permanent.getId())) { // stop to add targets if not needed and outcome is no advantage for AI player if (target.getNumberOfTargets() == target.getTargets().size()) { if (outcome.isGood() && hasOpponent(permanent.getControllerId(), game)) { return true; } if (!outcome.isGood() && !hasOpponent(permanent.getControllerId(), game)) { return true; } } // add the target target.add(permanent.getId(), game); if (target.doneChoosing(game)) { return true; } } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetCardInHand || (target.getZone() == Zone.HAND && (target.getOriginalTarget() instanceof TargetCard))) { List cards = new ArrayList<>(); for (UUID cardId : target.possibleTargets(this.getId(), source, game)) { Card card = game.getCard(cardId); if (card != null) { cards.add(card); } } while ((outcome.isGood() ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game)) && !cards.isEmpty()) { Card pick = pickTarget(abilityControllerId, cards, outcome, target, null, game); if (pick != null) { target.addTarget(pick.getId(), null, game); cards.remove(pick); } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetAnyTarget) { List targets; TargetAnyTarget origTarget = (TargetAnyTarget) target.getOriginalTarget(); if (outcome.isGood()) { targets = threats(abilityControllerId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); } else { targets = threats(randomOpponentId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); } for (Permanent permanent : targets) { List alreadyTargetted = target.getTargets(); if (target.canTarget(abilityControllerId, permanent.getId(), null, game)) { if (alreadyTargetted != null && !alreadyTargetted.contains(permanent.getId())) { target.add(permanent.getId(), game); return true; } } } if (outcome.isGood()) { if (target.canTarget(abilityControllerId, abilityControllerId, null, game)) { target.add(abilityControllerId, game); return true; } } else if (target.canTarget(abilityControllerId, randomOpponentId, null, game)) { target.add(randomOpponentId, game); return true; } if (!required) { return false; } } if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) { List targets; TargetPermanentOrPlayer origTarget = (TargetPermanentOrPlayer) target.getOriginalTarget(); List ownedTargets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); List opponentTargets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); if (outcome.isGood()) { targets = ownedTargets; } else { targets = opponentTargets; } for (Permanent permanent : targets) { List alreadyTargeted = target.getTargets(); if (target.canTarget(abilityControllerId, permanent.getId(), null, game)) { if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { target.add(permanent.getId(), game); return true; } } } if (outcome.isGood()) { if (target.canTarget(abilityControllerId, abilityControllerId, null, game)) { target.add(abilityControllerId, game); return true; } } else if (target.canTarget(abilityControllerId, randomOpponentId, null, game)) { target.add(randomOpponentId, game); return true; } if (!target.isRequired(sourceId, game) || target.getNumberOfTargets() == 0) { return false; } if (target.canTarget(abilityControllerId, randomOpponentId, null, game)) { target.add(randomOpponentId, game); return true; } if (target.canTarget(abilityControllerId, abilityControllerId, null, game)) { target.add(abilityControllerId, game); return true; } if (outcome.isGood()) { // no other valid targets so use a permanent targets = opponentTargets; } else { targets = ownedTargets; } for (Permanent permanent : targets) { List alreadyTargeted = target.getTargets(); if (target.canTarget(abilityControllerId, permanent.getId(), null, game)) { if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { target.add(permanent.getId(), game); return true; } } } return false; } if (target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) { List cards = new ArrayList<>(); for (Player player : game.getPlayers().values()) { for (Card card : player.getGraveyard().getCards(game)) { if (target.canTarget(card.getId(), source, game)) { cards.add(card); } } } // exile cost workaround: exile is bad, but exile from graveyard in most cases is good (more exiled -- more good things you get, e.g. delve's pay) boolean isRealGood = outcome.isGood() || outcome == Outcome.Exile; while ((isRealGood ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game)) && !cards.isEmpty()) { Card pick = pickTarget(abilityControllerId, cards, outcome, target, null, game); if (pick != null) { target.addTarget(pick.getId(), null, game); cards.remove(pick); } else { break; } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetCardInGraveyard || (target.getZone() == Zone.GRAVEYARD && (target.getOriginalTarget() instanceof TargetCard))) { List cards = new ArrayList<>(); for (Player player : game.getPlayers().values()) { for (Card card : player.getGraveyard().getCards(game)) { if (target.canTarget(abilityControllerId, card.getId(), null, game)) { cards.add(card); } } } // exile cost workaround: exile is bad, but exile from graveyard in most cases is good (more exiled -- more good things you get, e.g. delve's pay) boolean isRealGood = outcome.isGood() || outcome == Outcome.Exile; while ((isRealGood ? target.getTargets().size() < target.getMaxNumberOfTargets() : !target.isChosen(game)) && !cards.isEmpty()) { Card pick = pickTarget(abilityControllerId, cards, outcome, target, null, game); if (pick != null) { target.addTarget(pick.getId(), null, game); cards.remove(pick); } else { break; } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetCardInYourGraveyard || target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) { List alreadyTargeted = target.getTargets(); TargetCard originalTarget = (TargetCard) target.getOriginalTarget(); List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards(originalTarget.getFilter(), game)); while (!cards.isEmpty()) { Card card = pickTarget(abilityControllerId, cards, outcome, target, null, game); if (card != null && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) { target.add(card.getId(), game); if (target.isChosen(game)) { return true; } } } return false; } if (target.getOriginalTarget() instanceof TargetCardInExile) { List alreadyTargeted = target.getTargets(); FilterCard filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterCard) { filter = (FilterCard) target.getOriginalTarget().getFilter(); } if (filter == null) { throw new IllegalStateException("Unsupported exile target filter in computer's choose method: " + target.getOriginalTarget().getClass().getCanonicalName()); } List cards = game.getExile().getCards(filter, game); while (!cards.isEmpty()) { Card card = pickTarget(abilityControllerId, cards, outcome, target, null, game); if (card != null && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) { target.add(card.getId(), game); if (target.isChosen(game)) { return true; } } } return false; } if (target.getOriginalTarget() instanceof TargetSource) { Set targets; targets = target.possibleTargets(abilityControllerId, source, game); for (UUID targetId : targets) { MageObject targetObject = game.getObject(targetId); if (targetObject != null) { List alreadyTargeted = target.getTargets(); if (target.canTarget(abilityControllerId, targetObject.getId(), null, game)) { if (alreadyTargeted != null && !alreadyTargeted.contains(targetObject.getId())) { target.add(targetObject.getId(), game); return true; } } } } if (!required) { return false; } throw new IllegalStateException("TargetSource wasn't handled in computer's choose method: " + target.getClass().getCanonicalName()); } if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) { Cards cards = new CardsImpl(possibleTargets); List possibleCards = new ArrayList<>(cards.getCards(game)); while (!target.isChosen(game) && !possibleCards.isEmpty()) { Card pick = pickTarget(abilityControllerId, possibleCards, outcome, target, null, game); if (pick != null) { target.addTarget(pick.getId(), null, game); possibleCards.remove(pick); } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetCard && (target.getZone() == Zone.COMMAND)) { // Hellkite Courser List cardsInCommandZone = new ArrayList<>(); for (Player player : game.getPlayers().values()) { for (Card card : game.getCommanderCardsFromCommandZone(player, CommanderCardType.COMMANDER_OR_OATHBREAKER)) { if (target.canTarget(abilityControllerId, card.getId(), null, game)) { cardsInCommandZone.add(card); } } } while (!target.isChosen(game) && !cardsInCommandZone.isEmpty()) { Card pick = pickTarget(abilityControllerId, cardsInCommandZone, outcome, target, null, game); if (pick != null) { target.addTarget(pick.getId(), null, game); cardsInCommandZone.remove(pick); } } return target.isChosen(game); } throw new IllegalStateException("Target wasn't handled in computer's choose method: " + target.getClass().getCanonicalName()); } //end of choose method @Override public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { if (log.isDebugEnabled()) { log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString()); } // target - real target, make all changes and add targets to it // target.getOriginalTarget() - copy spell effect replaces original target with TargetWithAdditionalFilter // use originalTarget to get filters and target class info // source can be null (as example: legendary rule permanent selection) 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.getAbilityController() != null) { abilityControllerId = target.getAbilityController(); } boolean required = target.isRequired(sourceId, game); Set possibleTargets = target.possibleTargets(abilityControllerId, source, game); if (possibleTargets.isEmpty() || target.getTargets().size() >= target.getNumberOfTargets()) { required = false; } List goodList = new ArrayList<>(); List badList = new ArrayList<>(); List allList = new ArrayList<>(); // TODO: improve to process multiple opponents instead random UUID randomOpponentId; if (target.getTargetController() != null) { randomOpponentId = getRandomOpponent(target.getTargetController(), game); } else if (source != null && source.getControllerId() != null) { randomOpponentId = getRandomOpponent(source.getControllerId(), game); } else { randomOpponentId = getRandomOpponent(playerId, game); } if (target.getOriginalTarget() instanceof TargetPlayer) { return setTargetPlayer(outcome, target, source, abilityControllerId, randomOpponentId, game, required); } // Angel of Serenity trigger if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) { Cards cards = new CardsImpl(possibleTargets); List possibleCards = new ArrayList<>(cards.getCards(game)); for (Card card : possibleCards) { // check permanents first; they have more intrinsic worth if (card instanceof Permanent) { Permanent p = ((Permanent) card); if (outcome.isGood() && p.isControlledBy(abilityControllerId)) { if (target.canTarget(abilityControllerId, p.getId(), source, game)) { if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { break; } target.addTarget(p.getId(), source, game); } } if (!outcome.isGood() && !p.isControlledBy(abilityControllerId)) { if (target.canTarget(abilityControllerId, p.getId(), source, game)) { if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { break; } target.addTarget(p.getId(), source, game); } } } // check the graveyards last if (game.getState().getZone(card.getId()) == Zone.GRAVEYARD) { if (outcome.isGood() && card.isOwnedBy(abilityControllerId)) { if (target.canTarget(abilityControllerId, card.getId(), source, game)) { if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { break; } target.addTarget(card.getId(), source, game); } } if (!outcome.isGood() && !card.isOwnedBy(abilityControllerId)) { if (target.canTarget(abilityControllerId, card.getId(), source, game)) { if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { break; } target.addTarget(card.getId(), source, game); } } } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetDiscard || target.getOriginalTarget() instanceof TargetCardInHand) { if (outcome.isGood()) { // good Cards cards = new CardsImpl(possibleTargets); List cardsInHand = new ArrayList<>(cards.getCards(game)); while (!target.isChosen(game) && !cardsInHand.isEmpty() && target.getMaxNumberOfTargets() > target.getTargets().size()) { Card card = pickBestCard(cardsInHand, Collections.emptyList(), target, source, game); if (card != null) { if (target.canTarget(abilityControllerId, card.getId(), source, game)) { target.addTarget(card.getId(), source, game); cardsInHand.remove(card); if (target.isChosen(game)) { return true; } } } } } else { // bad findPlayables(game); for (Card card : unplayable.values()) { if (possibleTargets.contains(card.getId()) && target.canTarget(abilityControllerId, card.getId(), source, game)) { target.addTarget(card.getId(), source, game); if (target.isChosen(game)) { return true; } } } if (!hand.isEmpty()) { for (Card card : hand.getCards(game)) { if (possibleTargets.contains(card.getId()) && target.canTarget(abilityControllerId, card.getId(), source, game)) { target.addTarget(card.getId(), source, game); if (target.isChosen(game)) { return true; } } } } } return false; } if (target.getOriginalTarget() instanceof TargetControlledPermanent || target.getOriginalTarget() instanceof TargetSacrifice) { TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget(); List targets; targets = threats(abilityControllerId, source, origTarget.getFilter(), game, target.getTargets()); if (!outcome.isGood()) { Collections.reverse(targets); } for (Permanent permanent : targets) { if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { target.addTarget(permanent.getId(), source, game); if (target.getNumberOfTargets() <= target.getTargets().size() && (!outcome.isGood() || target.getMaxNumberOfTargets() <= target.getTargets().size())) { return true; } } } return target.isChosen(game); } // TODO: implemented findBestPlayerTargets // TODO: add findBest*Targets for all target types // TODO: Much of this code needs to be re-written to move code into Target.possibleTargets // A) Having it here makes this function ridiculously long // B) Each time a new target type is added, people must remember to add it here if (target.getOriginalTarget() instanceof TargetPermanent) { FilterPermanent filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) { filter = (FilterPermanent) target.getOriginalTarget().getFilter(); } if (filter == null) { throw new IllegalStateException("Unsupported permanent filter in computer's chooseTarget method: " + target.getOriginalTarget().getClass().getCanonicalName()); } findBestPermanentTargets(outcome, abilityControllerId, sourceId, source, filter, game, target, goodList, badList, allList); // use good list all the time and add maximum targets for (Permanent permanent : goodList) { if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if (target.getTargets().size() >= target.getMaxNumberOfTargets()) { break; } target.addTarget(permanent.getId(), source, game); } } // use bad list only on required target and add minimum targets if (required) { for (Permanent permanent : badList) { if (target.getTargets().size() >= target.getMinNumberOfTargets()) { break; } target.addTarget(permanent.getId(), source, game); } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetAnyTarget) { List targets; TargetAnyTarget origTarget = ((TargetAnyTarget) target.getOriginalTarget()); if (outcome.isGood()) { targets = threats(abilityControllerId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); } else { targets = threats(randomOpponentId, source, ((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); } if (targets.isEmpty()) { if (outcome.isGood()) { if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { return tryAddTarget(target, abilityControllerId, source, game); } } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { return tryAddTarget(target, randomOpponentId, source, game); } } if (targets.isEmpty() && required) { targets = game.getBattlefield().getActivePermanents(((FilterAnyTarget) origTarget.getFilter()).getPermanentFilter(), playerId, game); } for (Permanent permanent : targets) { List alreadyTargeted = target.getTargets(); if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { tryAddTarget(target, permanent.getId(), source, game); } } } if (outcome.isGood()) { if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { return tryAddTarget(target, abilityControllerId, source, game); } } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { return tryAddTarget(target, randomOpponentId, source, game); } //if (!target.isRequired()) return false; } if (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) { List targets; TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget()); if (outcome.isGood()) { targets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); } else { targets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), game, target.getTargets()); } if (targets.isEmpty()) { if (outcome.isGood()) { if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { return tryAddTarget(target, abilityControllerId, source, game); } } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { return tryAddTarget(target, randomOpponentId, source, game); } } if (targets.isEmpty() && target.isRequired(source)) { targets = game.getBattlefield().getActivePermanents(((FilterPermanentOrPlayer) origTarget.getFilter()).getPermanentFilter(), playerId, game); } for (Permanent permanent : targets) { List alreadyTargeted = target.getTargets(); if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { return tryAddTarget(target, permanent.getId(), source, game); } } } } if (target.getOriginalTarget() instanceof TargetPlayerOrPlaneswalker || target.getOriginalTarget() instanceof TargetOpponentOrPlaneswalker) { List targets; TargetPermanentOrPlayer origTarget = ((TargetPermanentOrPlayer) target.getOriginalTarget()); // TODO: in multiplayer game there many opponents - if random opponents don't have targets then AI must use next opponent, but it skips // (e.g. you randomOpponentId must be replaced by List randomOpponents) // normal cycle (good for you, bad for opponents) // possible good/bad permanents if (outcome.isGood()) { targets = threats(abilityControllerId, source, ((FilterPermanentOrPlayer) target.getFilter()).getPermanentFilter(), game, target.getTargets()); } else { targets = threats(randomOpponentId, source, ((FilterPermanentOrPlayer) target.getFilter()).getPermanentFilter(), game, target.getTargets()); } // possible good/bad players if (targets.isEmpty()) { if (outcome.isGood()) { if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { return tryAddTarget(target, abilityControllerId, source, game); } } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { return tryAddTarget(target, randomOpponentId, source, game); } } // can't find targets (e.g. effect is bad, but you need take targets from yourself) if (targets.isEmpty() && required) { targets = game.getBattlefield().getActivePermanents(origTarget.getFilterPermanent(), playerId, game); } // try target permanent for (Permanent permanent : targets) { List alreadyTargeted = target.getTargets(); if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { if (alreadyTargeted != null && !alreadyTargeted.contains(permanent.getId())) { return tryAddTarget(target, permanent.getId(), source, game); } } } // try target player as normal if (outcome.isGood()) { if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { return tryAddTarget(target, abilityControllerId, source, game); } } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { return tryAddTarget(target, randomOpponentId, source, game); } // try target player as bad (bad on itself, good on opponent) for (UUID opponentId : game.getOpponents(abilityControllerId)) { if (target.canTarget(abilityControllerId, opponentId, source, game)) { return tryAddTarget(target, opponentId, source, game); } } if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { return tryAddTarget(target, abilityControllerId, source, game); } return false; } if (target.getOriginalTarget() instanceof TargetCardInGraveyard) { List cards = new ArrayList<>(); for (Player player : game.getPlayers().values()) { cards.addAll(player.getGraveyard().getCards(game)); } Card card = pickTarget(abilityControllerId, cards, outcome, target, source, game); if (card != null) { return tryAddTarget(target, card.getId(), source, game); } //if (!target.isRequired()) return false; } if (target.getOriginalTarget() instanceof TargetCardInLibrary) { List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getLibrary().getCards(game)); Card card = pickTarget(abilityControllerId, cards, outcome, target, source, game); if (card != null) { return tryAddTarget(target, card.getId(), source, game); } return false; } if (target.getOriginalTarget() instanceof TargetCardInYourGraveyard) { List cards = new ArrayList<>(game.getPlayer(abilityControllerId).getGraveyard().getCards((FilterCard) target.getFilter(), game)); while (!target.isChosen(game) && !cards.isEmpty()) { Card card = pickTarget(abilityControllerId, cards, outcome, target, source, game); if (card != null) { target.addTarget(card.getId(), source, game); cards.remove(card); // pickTarget don't remove cards (only on second+ tries) } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetSpell || target.getOriginalTarget() instanceof TargetStackObject) { if (!game.getStack().isEmpty()) { for (StackObject o : game.getStack()) { if (o instanceof Spell && !source.getId().equals(o.getStackAbility().getId()) && target.canTarget(abilityControllerId, o.getStackAbility().getId(), source, game)) { return tryAddTarget(target, o.getId(), source, game); } } } return false; } if (target.getOriginalTarget() instanceof TargetSpellOrPermanent) { // TODO: Also check if a spell should be selected TargetSpellOrPermanent origTarget = (TargetSpellOrPermanent) target.getOriginalTarget(); List targets; boolean outcomeTargets = true; if (outcome.isGood()) { targets = threats(abilityControllerId, source, origTarget.getPermanentFilter(), game, target.getTargets()); } else { targets = threats(randomOpponentId, source, origTarget.getPermanentFilter(), game, target.getTargets()); } if (targets.isEmpty() && required) { targets = threats(null, source, origTarget.getPermanentFilter(), game, target.getTargets()); Collections.reverse(targets); outcomeTargets = false; } for (Permanent permanent : targets) { if (target.canTarget(abilityControllerId, permanent.getId(), source, game)) { target.addTarget(permanent.getId(), source, game); if (!outcomeTargets || target.getMaxNumberOfTargets() <= target.getTargets().size()) { return true; } } } if (!game.getStack().isEmpty()) { for (StackObject stackObject : game.getStack()) { if (stackObject instanceof Spell && source != null && !source.getId().equals(stackObject.getStackAbility().getId())) { if (target.getFilter().match(stackObject, game)) { return tryAddTarget(target, stackObject.getId(), source, game); } } } } return false; } if (target.getOriginalTarget() instanceof TargetCardInOpponentsGraveyard) { List cards = new ArrayList<>(); for (UUID uuid : game.getOpponents(abilityControllerId)) { Player player = game.getPlayer(uuid); if (player != null) { cards.addAll(player.getGraveyard().getCards(game)); } } Card card = pickTarget(abilityControllerId, cards, outcome, target, source, game); if (card != null) { return tryAddTarget(target, card.getId(), source, game); } //if (!target.isRequired()) return false; } if (target.getOriginalTarget() instanceof TargetDefender) { UUID randomDefender = RandomUtil.randomFromCollection(possibleTargets); target.addTarget(randomDefender, source, game); return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetCardInASingleGraveyard) { List cards = new ArrayList<>(); for (Player player : game.getPlayers().values()) { cards.addAll(player.getGraveyard().getCards(game)); } while (!target.isChosen(game) && !cards.isEmpty()) { Card pick = pickTarget(abilityControllerId, cards, outcome, target, source, game); if (pick != null) { target.addTarget(pick.getId(), source, game); cards.remove(pick); // pickTarget don't remove cards (only on second+ tries) } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetCardInExile) { FilterCard filter = null; if (target.getOriginalTarget().getFilter() instanceof FilterCard) { filter = (FilterCard) target.getOriginalTarget().getFilter(); } if (filter == null) { throw new IllegalStateException("Unsupported exile target filter in computer's chooseTarget method: " + target.getOriginalTarget().getClass().getCanonicalName()); } List cards = new ArrayList<>(); for (UUID uuid : target.possibleTargets(source.getControllerId(), source, game)) { Card card = game.getCard(uuid); if (card != null && game.getState().getZone(card.getId()) == Zone.EXILED) { cards.add(card); } } while (!target.isChosen(game) && !cards.isEmpty()) { Card pick = pickTarget(abilityControllerId, cards, outcome, target, source, game); if (pick != null) { target.addTarget(pick.getId(), source, game); cards.remove(pick); // pickTarget don't remove cards (only on second+ tries) } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetActivatedAbility) { List stackObjects = new ArrayList<>(); for (UUID uuid : target.possibleTargets(source.getControllerId(), source, game)) { StackObject stackObject = game.getStack().getStackObject(uuid); if (stackObject != null) { stackObjects.add(stackObject); } } while (!target.isChosen(game) && !stackObjects.isEmpty()) { StackObject pick = stackObjects.get(0); if (pick != null) { target.addTarget(pick.getId(), source, game); stackObjects.remove(0); } } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetActivatedOrTriggeredAbility) { Iterator iterator = target.possibleTargets(source.getControllerId(), source, game).iterator(); while (!target.isChosen(game) && iterator.hasNext()) { target.addTarget(iterator.next(), source, game); } return target.isChosen(game); } if (target.getOriginalTarget() instanceof TargetCardInGraveyardBattlefieldOrStack) { List cards = new ArrayList<>(); for (Player player : game.getPlayers().values()) { cards.addAll(player.getGraveyard().getCards(game)); cards.addAll(game.getBattlefield().getAllActivePermanents(new FilterPermanent(), player.getId(), game)); } Card card = pickTarget(abilityControllerId, cards, outcome, target, source, game); if (card != null) { return tryAddTarget(target, card.getId(), source, game); } } if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) { Cards cards = new CardsImpl(possibleTargets); List possibleCards = new ArrayList<>(cards.getCards(game)); while (!target.isChosen(game) && !possibleCards.isEmpty()) { Card pick = pickTarget(abilityControllerId, possibleCards, outcome, target, source, game); if (pick != null) { target.addTarget(pick.getId(), source, game); possibleCards.remove(pick); } } return target.isChosen(game); } throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName()); } //end of chooseTarget method protected Card pickTarget(UUID abilityControllerId, List cards, Outcome outcome, Target target, Ability source, Game game) { Card card; while (!cards.isEmpty()) { if (outcome.isGood()) { card = pickBestCard(cards, Collections.emptyList(), target, source, game); } else { card = pickWorstCard(cards, Collections.emptyList(), target, source, game); } if (!target.getTargets().contains(card.getId())) { if (source != null) { if (target.canTarget(abilityControllerId, card.getId(), source, 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) { // TODO: make same code for chooseTarget (without filter and target type dependence) if (log.isDebugEnabled()) { log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString()); } UUID sourceId = source != null ? source.getSourceId() : null; // process multiple opponents by random List opponents; if (target.getTargetController() != null) { opponents = new ArrayList<>(game.getOpponents(target.getTargetController())); } else if (source != null && source.getControllerId() != null) { opponents = new ArrayList<>(game.getOpponents(source.getControllerId())); } else { opponents = new ArrayList<>(game.getOpponents(getId())); } Collections.shuffle(opponents); List 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(getId(), 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(getId(), 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(getId(), 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(getId(), permanent.getId(), source, game)) { return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game); } } // players if (outcome.isGood() && target.canTarget(getId(), getId(), source, game)) { return tryAddTarget(target, getId(), target.getAmountRemaining(), source, game); } if (!outcome.isGood() && target.canTarget(getId(), opponentId, source, game)) { return tryAddTarget(target, opponentId, target.getAmountRemaining(), source, game); } // creature for (Permanent permanent : targets) { if (permanent.isCreature(game) && target.canTarget(getId(), 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)) { return false; } for (UUID opponentId : opponents) { if (!outcome.isGood()) { // bad on yourself, uses 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 undestructeable) for (Permanent permanent : targets) { if (permanent.isCreature(game) && target.canTarget(getId(), 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); } } } // creatures - all for (Permanent permanent : targets) { if (permanent.isCreature(game) && target.canTarget(getId(), permanent.getId(), source, game)) { return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game); } } // planeswalkers for (Permanent permanent : targets) { if (permanent.isPlaneswalker(game) && target.canTarget(getId(), permanent.getId(), source, game)) { return tryAddTarget(target, permanent.getId(), target.getAmountRemaining(), source, game); } } } // players for (UUID opponentId : opponents) { if (target.canTarget(getId(), getId(), source, game)) { return tryAddTarget(target, getId(), target.getAmountRemaining(), source, game); } else if (target.canTarget(getId(), opponentId, source, game)) { return tryAddTarget(target, opponentId, target.getAmountRemaining(), source, game); } } // 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; } @Override public boolean priority(Game game) { game.resumeTimer(getTurnControlledBy()); boolean result = priorityPlay(game); game.pauseTimer(getTurnControlledBy()); return result; } private boolean priorityPlay(Game game) { UUID opponentId = game.getOpponents(playerId).iterator().next(); 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; } } pass(game); return true; } // end priorityPlay method protected void playLand(Game game) { log.debug("playLand"); Set 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 lands, Game game) { log.debug("playALand"); //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()); } protected void findPlayables(Game game) { playableInstant.clear(); playableNonInstant.clear(); unplayable.clear(); playableAbilities.clear(); Set 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); } } } } } } } if (log.isDebugEnabled()) { log.debug("findPlayables: " + playableInstant.toString() + "---" + playableNonInstant.toString() + "---" + playableAbilities.toString()); } } @Override public boolean playMana(Ability ability, ManaCost unpaid, String promptText, Game game) { payManaMode = true; lastUnpaidMana.put(ability.getId(), unpaid.copy()); try { return playManaHandling(ability, unpaid, game); } finally { lastUnpaidMana.remove(ability.getId()); payManaMode = false; } } protected boolean playManaHandling(Ability ability, ManaCost unpaid, final Game game) { // log.info("paying for " + unpaid.getText()); Set approvingObjects = game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game); boolean hasApprovingObject = !approvingObjects.isEmpty(); ManaCost cost; List producers; if (unpaid instanceof ManaCosts) { ManaCosts manaCosts = (ManaCosts) unpaid; cost = manaCosts.get(manaCosts.size() - 1); producers = getSortedProducers((ManaCosts) unpaid, game); } else { cost = unpaid; producers = this.getAvailableManaProducers(game); producers.addAll(this.getAvailableManaProducersWithCost(game)); } // use fully compatible colored mana producers first for (MageObject mageObject : producers) { ManaAbility: for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { boolean canPayColoredMana = false; for (Mana mana : manaAbility.getNetMana(game)) { // if mana ability can produce non-useful mana then ignore whole ability here (example: {R} or {G}) // (AI can't choose a good mana option, so make sure any selection option will be compatible with cost) // AI support {Any} choice by lastUnpaidMana, so it can safly used in includesMana if (!unpaid.getMana().includesMana(mana)) { continue ManaAbility; } else if (mana.getAny() > 0) { throw new IllegalArgumentException("Wrong mana calculation: AI do not support color choosing from {Any}"); } if (mana.countColored() > 0) { canPayColoredMana = true; } } // found compatible source - try to pay if (canPayColoredMana && (cost instanceof ColoredManaCost)) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana)) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } } // use any other mana produces for (MageObject mageObject : producers) { // pay all colored costs first for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof ColoredManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } // pay snow covered mana for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof SnowManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } // pay colorless - more restrictive than hybrid (think of it like colored) for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof ColorlessManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } // then pay hybrid for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof HybridManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } // then pay colorless hybrid - more restrictive than mono hybrid for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof ColorlessHybridManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } // then pay mono hybrid for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof MonoHybridManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } // finally pay generic for (ActivatedManaAbilityImpl manaAbility : getManaAbilitiesSortedByManaCount(mageObject, game)) { if (cost instanceof GenericManaCost) { for (Mana netMana : manaAbility.getNetMana(game)) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (hasApprovingObject && !canUseAsThoughManaToPayManaCost(cost, ability, netMana, manaAbility, mageObject, game)) { continue; } if (activateAbility(manaAbility, game)) { return true; } } } } } } if (alreadyTryingToPayPhyrexian) { return false; } // pay phyrexian life costs if (cost.isPhyrexian()) { alreadyTryingToPayPhyrexian = true; // TODO: make sure it's thread safe and protected from modifications (cost/unpaid can be shared between AI simulation threads?) boolean paidPhyrexian = cost.pay(ability, game, ability, playerId, false, null) || hasApprovingObject; alreadyTryingToPayPhyrexian = false; return paidPhyrexian; } // pay special mana like convoke cost (tap for pay) // GUI: user see "special" button while pay spell's cost // TODO: AI can't prioritize special mana types to pay, e.g. it will use first available SpecialAction specialAction = game.getState().getSpecialActions().getControlledBy(this.getId(), true).values() .stream() .findFirst() .orElse(null); ManaOptions specialMana = specialAction == null ? null : specialAction.getManaOptions(ability, game, unpaid); if (specialMana != null) { for (Mana netMana : specialMana) { if (cost.testPay(netMana) || hasApprovingObject) { if (netMana instanceof ConditionalMana && !((ConditionalMana) netMana).apply(ability, game, getId(), cost)) { continue; } if (activateAbility(specialAction, game)) { return true; } // only one time try to pay to skip infinite AI loop break; } } } return false; } boolean canUseAsThoughManaToPayManaCost(ManaCost checkCost, Ability abilityToPay, Mana manaOption, Ability manaAbility, MageObject manaProducer, Game game) { // asThoughMana can change producing mana type, so you must check it here // cause some effects adds additional checks in getAsThoughManaType (example: Draugr Necromancer with snow mana sources) // simulate real asThoughMana usage ManaPoolItem possiblePoolItem; if (manaOption instanceof ConditionalMana) { ConditionalMana conditionalNetMana = (ConditionalMana) manaOption; possiblePoolItem = new ManaPoolItem( conditionalNetMana, manaAbility.getSourceObject(game), conditionalNetMana.getManaProducerOriginalId() != null ? conditionalNetMana.getManaProducerOriginalId() : manaAbility.getOriginalId() ); } else { possiblePoolItem = new ManaPoolItem( manaOption.getRed(), manaOption.getGreen(), manaOption.getBlue(), manaOption.getWhite(), manaOption.getBlack(), manaOption.getGeneric() + manaOption.getColorless(), manaProducer, manaAbility.getOriginalId(), manaOption.getFlag() ); } // cost can contains multiple mana types, must check each type (is it possible to pay a cost) for (ManaType checkType : ManaUtil.getManaTypesInCost(checkCost)) { // affected asThoughMana effect must fit a checkType with pool mana ManaType possibleAsThoughPoolManaType = game.getContinuousEffects().asThoughMana(checkType, possiblePoolItem, abilityToPay.getSourceId(), abilityToPay, abilityToPay.getControllerId(), game); if (possibleAsThoughPoolManaType == null) { continue; // no affected asThough effects } boolean canPay; if (possibleAsThoughPoolManaType == ManaType.COLORLESS) { // colorless can be payed by any color from the pool canPay = possiblePoolItem.count() > 0; } else { // colored must be payed by specific color from the pool (AsThough already changed it to fit with mana pool) canPay = possiblePoolItem.get(possibleAsThoughPoolManaType) > 0; } if (canPay) { return true; } } return false; } private Abilities getManaAbilitiesSortedByManaCount(MageObject mageObject, final Game game) { Abilities manaAbilities = mageObject.getAbilities().getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); if (manaAbilities.size() > 1) { // Sort mana abilities by number of produced manas, to use ability first that produces most mana (maybe also conditional if possible) Collections.sort(manaAbilities, new Comparator() { @Override public int compare(ActivatedManaAbilityImpl a1, ActivatedManaAbilityImpl a2) { int a1Max = 0; for (Mana netMana : a1.getNetMana(game)) { if (netMana.count() > a1Max) { a1Max = netMana.count(); } } int a2Max = 0; for (Mana netMana : a2.getNetMana(game)) { if (netMana.count() > a2Max) { a2Max = netMana.count(); } } return CardUtil.overflowDec(a2Max, a1Max); } }); } return manaAbilities; } /** * returns a list of Permanents that produce mana sorted by the number of * mana the Permanent produces that match the unpaid costs in ascending * order *

* the idea is that we should pay costs first from mana producers that * produce only one type of mana and save the multi-mana producers for those * costs that can't be paid by any other producers * * @param unpaid - the amount of unpaid mana costs * @param game * @return List */ private List getSortedProducers(ManaCosts unpaid, Game game) { List unsorted = this.getAvailableManaProducers(game); unsorted.addAll(this.getAvailableManaProducersWithCost(game)); Map scored = new HashMap<>(); for (MageObject mageObject : unsorted) { int score = 0; for (ManaCost cost : unpaid) { Abilities: for (ActivatedManaAbilityImpl ability : mageObject.getAbilities().getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game)) { for (Mana netMana : ability.getNetMana(game)) { if (cost.testPay(netMana)) { score++; break Abilities; } } } } if (score > 0) { // score mana producers that produce other mana types and have other uses higher score += mageObject.getAbilities().getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game).size(); score += mageObject.getAbilities().getActivatedAbilities(Zone.BATTLEFIELD).size(); if (!mageObject.getCardType(game).contains(CardType.LAND)) { score += 2; } else if (mageObject.getCardType(game).contains(CardType.CREATURE)) { score += 2; } } scored.put(mageObject, score); } return sortByValue(scored); } private List sortByValue(Map map) { List> list = new LinkedList<>(map.entrySet()); Collections.sort(list, new Comparator>() { @Override public int compare(Entry o1, Entry o2) { return (o1.getValue().compareTo(o2.getValue())); } }); List result = new ArrayList<>(); for (Entry entry : list) { result.add(entry.getKey()); } return result; } @Override public int announceXMana(int min, int max, String message, Game game, Ability ability) { log.debug("announceXMana"); //TODO: improve this int numAvailable = getAvailableManaProducers(game).size() - ability.getManaCosts().manaValue(); if (numAvailable < 0) { numAvailable = 0; } else { if (numAvailable < min) { numAvailable = min; } if (numAvailable > max) { numAvailable = max; } } return numAvailable; } @Override public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variablCost) { log.debug("announceXCost"); int value = RandomUtil.nextInt(CardUtil.overflowInc(max, 1)); if (value < min) { value = min; } if (value < max) { value++; } return value; } @Override public void abort() { abort = true; } @Override public void skip() { } @Override public boolean chooseUse(Outcome outcome, String message, Ability source, Game game) { return this.chooseUse(outcome, message, null, null, null, source, game); } @Override public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) { log.debug("chooseUse: " + outcome.isGood()); // Be proactive! Always use abilities, the evaluation function will decide if it's good or not // Otherwise some abilities won't be used by AI like LoseTargetEffect that has "bad" outcome // but still is good when targets opponent return outcome != Outcome.AIDontUseIt; // Added for Desecration Demon sacrifice ability } @Override public boolean choose(Outcome outcome, Choice choice, Game game) { log.debug("choose 3"); //TODO: improve this // 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); } // choose the correct color to pay a spell (use last unpaid ability for color hint) ManaCost unpaid = null; if (!lastUnpaidMana.isEmpty()) { unpaid = new ArrayList<>(lastUnpaidMana.values()).get(lastUnpaidMana.size() - 1); } if (outcome == Outcome.PutManaInPool && unpaid != null && choice.isManaColorChoice()) { if (unpaid.containsColor(ColoredManaSymbol.W) && choice.getChoices().contains("White")) { choice.setChoice("White"); return true; } if (unpaid.containsColor(ColoredManaSymbol.R) && choice.getChoices().contains("Red")) { choice.setChoice("Red"); return true; } if (unpaid.containsColor(ColoredManaSymbol.G) && choice.getChoices().contains("Green")) { choice.setChoice("Green"); return true; } if (unpaid.containsColor(ColoredManaSymbol.U) && choice.getChoices().contains("Blue")) { choice.setChoice("Blue"); return true; } if (unpaid.containsColor(ColoredManaSymbol.B) && choice.getChoices().contains("Black")) { choice.setChoice("Black"); return true; } if (unpaid.getMana().getColorless() > 0 && choice.getChoices().contains("Colorless")) { choice.setChoice("Colorless"); return true; } } // choose by random if (!choice.isChosen()) { choice.setRandomChoice(); } return true; } protected boolean chooseCreatureType(Outcome outcome, Choice choice, Game game) { if (outcome == Outcome.Detriment) { // choose a creature type of opponent on battlefield or graveyard for (Permanent permanent : game.getBattlefield().getActivePermanents(this.getId(), game)) { if (game.getOpponents(this.getId()).contains(permanent.getControllerId()) && permanent.getCardType(game).contains(CardType.CREATURE) && !permanent.getSubtype(game).isEmpty()) { if (choice.getChoices().contains(permanent.getSubtype(game).get(0).toString())) { choice.setChoice(permanent.getSubtype(game).get(0).toString()); break; } } } // or in opponent graveyard if (!choice.isChosen()) { for (UUID opponentId : game.getOpponents(this.getId())) { Player opponent = game.getPlayer(opponentId); for (Card card : opponent.getGraveyard().getCards(game)) { if (card != null && card.getCardType(game).contains(CardType.CREATURE) && !card.getSubtype(game).isEmpty()) { if (choice.getChoices().contains(card.getSubtype(game).get(0).toString())) { choice.setChoice(card.getSubtype(game).get(0).toString()); break; } } } if (choice.isChosen()) { break; } } } } else { // choose a creature type of hand or library for (UUID cardId : this.getHand()) { Card card = game.getCard(cardId); if (card != null && card.getCardType(game).contains(CardType.CREATURE) && !card.getSubtype(game).isEmpty()) { if (choice.getChoices().contains(card.getSubtype(game).get(0).toString())) { choice.setChoice(card.getSubtype(game).get(0).toString()); break; } } } if (!choice.isChosen()) { for (UUID cardId : this.getLibrary().getCardList()) { Card card = game.getCard(cardId); if (card != null && card.getCardType(game).contains(CardType.CREATURE) && !card.getSubtype(game).isEmpty()) { if (choice.getChoices().contains(card.getSubtype(game).get(0).toString())) { choice.setChoice(card.getSubtype(game).get(0).toString()); break; } } } } } return choice.isChosen(); } @Override public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { if (cards == null || cards.isEmpty()) { return target.isRequired(source); } // 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(); } // we still use playerId when getting cards even if they don't control the search List cardChoices = new ArrayList<>(cards.getCards(target.getFilter(), playerId, source, game)); while (!target.doneChoosing(game)) { Card card = pickTarget(abilityControllerId, cardChoices, outcome, target, source, game); if (card != null) { target.addTarget(card.getId(), source, game); cardChoices.remove(card); } else { // We don't have any valid target to choose so stop choosing return target.getTargets().size() >= target.getNumberOfTargets(); } if (outcome == Outcome.Neutral && target.getTargets().size() > target.getNumberOfTargets() + (target.getMaxNumberOfTargets() - target.getNumberOfTargets()) / 2) { return true; } } return true; } @Override public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { log.debug("choose 2"); if (cards == null || cards.isEmpty()) { return true; } // 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(); } List cardChoices = new ArrayList<>(cards.getCards(target.getFilter(), abilityControllerId, source, game)); while (!target.doneChoosing(game)) { Card card = pickTarget(abilityControllerId, cardChoices, outcome, target, source, game); if (card != null) { target.add(card.getId(), game); cardChoices.remove(card); } else { // We don't have any valid target to choose so stop choosing return target.getTargets().size() >= target.getNumberOfTargets(); } if (outcome == Outcome.Neutral && target.getTargets().size() > target.getNumberOfTargets() + (target.getMaxNumberOfTargets() - target.getNumberOfTargets()) / 2) { return true; } } return true; } @Override public boolean choosePile(Outcome outcome, String message, List pile1, List pile2, Game game) { //TODO: improve this return true; } @Override public void selectAttackers(Game game, UUID attackingPlayerId) { log.debug("selectAttackers"); UUID opponentId = game.getCombat().getDefenders().iterator().next(); Attackers attackers = getPotentialAttackers(game); List blockers = getOpponentBlockers(opponentId, game); List 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); } } @Override public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) { log.debug("selectBlockers"); List blockers = getAvailableBlockers(game); CombatSimulator sim = simulateBlock(CombatSimulator.load(game), blockers, game); List 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); } } } @Override public int chooseReplacementEffect(Map effectsMap, Map objectsMap, Game game) { log.debug("chooseReplacementEffect"); //TODO: implement this return 0; } @Override public Mode chooseMode(Modes modes, Ability source, Game game) { log.debug("chooseMode"); if (modes.getMode() != null && modes.getMaxModes(game, source) == modes.getSelectedModes().size()) { // mode was already set by the AI return modes.getMode(); } // spell modes simulated by AI, see addModeOptions // trigger modes chooses here // TODO: add AI support to select best modes, current code uses first valid mode AvailableMode: for (Mode mode : modes.getAvailableModes(source, game)) { for (UUID selectedModeId : modes.getSelectedModes()) { Mode selectedMode = modes.get(selectedModeId); if (selectedMode.getId().equals(mode.getId())) { continue AvailableMode; } } if (mode.getTargets().canChoose(source.getControllerId(), source, game)) { // and where targets are available return mode; } } return null; } @Override public TriggeredAbility chooseTriggeredAbility(List abilities, Game game) { log.debug("chooseTriggeredAbility: " + abilities.toString()); //TODO: improve this if (!abilities.isEmpty()) { return abilities.get(0); } return null; } @Override // TODO: add AI support with outcome and replace random with min/max public int getAmount(int min, int max, String message, Game game) { log.debug("getAmount"); if (min < max && min == 0) { return RandomUtil.nextInt(CardUtil.overflowInc(max, 1)); } return min; } @Override public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, int totalMin, int totalMax, MultiAmountType type, Game game) { log.debug("getMultiAmount"); int needCount = messages.size(); List defaultList = MultiAmountType.prepareDefaultValues(messages, totalMin, totalMax); if (needCount == 0) { return defaultList; } // BAD effect // default list uses minimum possible values, so return it on bad effect // TODO: need something for damage target and mana logic here, current version is useless but better than random if (!outcome.isGood()) { return defaultList; } // GOOD effect // values must be stable, so AI must able to simulate it and choose correct actions // fill max values as much as possible return MultiAmountType.prepareMaxValues(messages, totalMin, totalMax); } @Override public List getAvailableManaProducers(Game game) { return super.getAvailableManaProducers(game); } @Override public void sideboard(Match match, Deck deck) { //TODO: improve this match.submitDeck(playerId, deck); } private static void addBasicLands(Deck deck, String landName, int number) { Set landSets = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()); CardCriteria criteria = new CardCriteria(); if (!landSets.isEmpty()) { criteria.setCodes(landSets.toArray(new String[landSets.size()])); } criteria.rarities(Rarity.LAND).name(landName); List cards = CardRepository.instance.findCards(criteria); if (cards.isEmpty()) { criteria = new CardCriteria(); criteria.rarities(Rarity.LAND).name(landName); criteria.setCodes("M15"); cards = CardRepository.instance.findCards(criteria); } for (int i = 0; i < number; i++) { Card land = cards.get(RandomUtil.nextInt(cards.size())).createCard(); deck.getCards().add(land); } } public static Deck buildDeck(int deckMinSize, List cardPool, final List colors) { return buildDeck(deckMinSize, cardPool, colors, false); } public static Deck buildDeck(int deckMinSize, List cardPool, final List colors, boolean onlyBasicLands) { if (onlyBasicLands) { return buildDeckWithOnlyBasicLands(deckMinSize, cardPool); } else { return buildDeckWithNormalCards(deckMinSize, cardPool, colors); } } public static Deck buildDeckWithOnlyBasicLands(int deckMinSize, List cardPool) { // random cards from card pool Deck deck = new Deck(); final int DECK_SIZE = deckMinSize != 0 ? deckMinSize : 40; List sortedCards = new ArrayList<>(cardPool); if (!sortedCards.isEmpty()) { while (deck.getMaindeckCards().size() < DECK_SIZE) { deck.getCards().add(sortedCards.get(RandomUtil.nextInt(sortedCards.size()))); } return deck; } else { addBasicLands(deck, "Forest", DECK_SIZE); return deck; } } public static Deck buildDeckWithNormalCards(int deckMinSize, List cardPool, final List colors) { // top 23 cards plus basic lands until 40 deck size Deck deck = new Deck(); final int DECK_SIZE = deckMinSize != 0 ? deckMinSize : 40; final int DECK_CARDS_COUNT = Math.floorDiv(deckMinSize * 23, 40); // 23 from 40 final int DECK_LANDS_COUNT = DECK_SIZE - DECK_CARDS_COUNT; // sort card pool by top score List sortedCards = new ArrayList<>(cardPool); Collections.sort(sortedCards, new Comparator() { @Override public int compare(Card o1, Card o2) { Integer score1 = RateCard.rateCard(o1, colors); Integer score2 = RateCard.rateCard(o2, colors); return score2.compareTo(score1); } }); // get top cards int cardNum = 0; while (deck.getMaindeckCards().size() < DECK_CARDS_COUNT && sortedCards.size() > cardNum) { Card card = sortedCards.get(cardNum); if (!card.isBasic()) { deck.getCards().add(card); deck.getSideboard().remove(card); } cardNum++; } // add basic lands by color percent // TODO: compensate for non basic lands Mana mana = new Mana(); for (Card card : deck.getCards()) { mana.add(card.getManaCost().getMana()); } double total = mana.getBlack() + mana.getBlue() + mana.getGreen() + mana.getRed() + mana.getWhite(); // most frequent land is forest by default int mostLand = 0; String mostLandName = "Forest"; if (mana.getGreen() > 0) { int number = (int) Math.round(mana.getGreen() / total * DECK_LANDS_COUNT); addBasicLands(deck, "Forest", number); mostLand = number; } if (mana.getBlack() > 0) { int number = (int) Math.round(mana.getBlack() / total * DECK_LANDS_COUNT); addBasicLands(deck, "Swamp", number); if (number > mostLand) { mostLand = number; mostLandName = "Swamp"; } } if (mana.getBlue() > 0) { int number = (int) Math.round(mana.getBlue() / total * DECK_LANDS_COUNT); addBasicLands(deck, "Island", number); if (number > mostLand) { mostLand = number; mostLandName = "Island"; } } if (mana.getWhite() > 0) { int number = (int) Math.round(mana.getWhite() / total * DECK_LANDS_COUNT); addBasicLands(deck, "Plains", number); if (number > mostLand) { mostLand = number; mostLandName = "Plains"; } } if (mana.getRed() > 0) { int number = (int) Math.round(mana.getRed() / total * DECK_LANDS_COUNT); addBasicLands(deck, "Mountain", number); if (number > mostLand) { mostLandName = "Plains"; } } // adds remaining lands (most popular name) addBasicLands(deck, mostLandName, DECK_SIZE - deck.getMaindeckCards().size()); return deck; } @Override public void construct(Tournament tournament, Deck deck) { DeckValidator deckValidator = DeckValidatorFactory.instance.createDeckValidator(tournament.getOptions().getMatchOptions().getDeckType()); int deckMinSize = deckValidator != null ? deckValidator.getDeckMinSize() : 0; if (deck != null && deck.getMaindeckCards().size() < deckMinSize && !deck.getSideboard().isEmpty()) { if (chosenColors.isEmpty()) { for (Card card : deck.getSideboard()) { rememberPick(card, RateCard.rateCard(card, Collections.emptyList())); } List deckColors = chooseDeckColorsIfPossible(); if (deckColors != null) { chosenColors.addAll(deckColors); } } deck = buildDeck(deckMinSize, new ArrayList<>(deck.getSideboard()), chosenColors); } tournament.submitDeck(playerId, deck); } public Card pickBestCard(List cards, List chosenColors) { if (cards.isEmpty()) { return null; } Card bestCard = null; int maxScore = 0; for (Card card : cards) { int score = RateCard.rateCard(card, chosenColors); if (bestCard == null || score > maxScore) { maxScore = score; bestCard = card; } } return bestCard; } public Card pickBestCard(List cards, List chosenColors, Target target, Ability source, Game game) { if (cards.isEmpty()) { return null; } Card bestCard = null; int maxScore = 0; for (Card card : cards) { int score = RateCard.rateCard(card, chosenColors); boolean betterCard = false; 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 && source != null && game != null) { // but also check it can be targeted betterCard = target.canTarget(getId(), card.getId(), source, game); } else { // target object wasn't provided, so accepting it anyway betterCard = true; } } // is it better than previous one? if (betterCard) { maxScore = score; bestCard = card; } } return bestCard; } public Card pickWorstCard(List cards, List chosenColors, Target target, Ability source, Game game) { if (cards.isEmpty()) { return null; } 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 && source != null && game != null) { // but also check it can be targeted worseCard = target.canTarget(getId(), card.getId(), source, 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; } public Card pickWorstCard(List cards, List chosenColors) { if (cards.isEmpty()) { return null; } Card worstCard = null; int minScore = Integer.MAX_VALUE; for (Card card : cards) { int score = RateCard.rateCard(card, chosenColors); if (worstCard == null || score < minScore) { minScore = score; worstCard = card; } } return worstCard; } @Override public void pickCard(List cards, Deck deck, Draft draft) { if (cards.isEmpty()) { throw new IllegalArgumentException("No cards to pick from."); } try { Card bestCard = pickBestCard(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 = pickBestCard(cards, Collections.emptyList()); int counterPickScore = RateCard.getBaseCardScore(counterPick); // card is really good // take it! if (counterPickScore >= 80) { bestCard = counterPick; maxScore = RateCard.rateCard(bestCard, chosenColors); } } String colors = "not chosen yet"; // remember card if colors are not chosen yet if (chosenColors.isEmpty()) { rememberPick(bestCard, maxScore); List chosen = chooseDeckColorsIfPossible(); if (chosen != null) { chosenColors.addAll(chosen); } } if (!chosenColors.isEmpty()) { colors = ""; for (ColoredManaSymbol symbol : chosenColors) { 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()); draft.addPick(playerId, cards.get(0).getId(), null); } } /** * Remember picked card with its score. * * @param card * @param score */ protected void rememberPick(Card card, int score) { pickedCards.add(new PickedCard(card, score)); } /** * Choose 2 deck colors for draft: 1. there should be at least 3 cards in * card pool 2. at least 2 cards should have different colors 3. get card * colors as chosen starting from most rated card * * @return */ protected List chooseDeckColorsIfPossible() { if (pickedCards.size() > 2) { // sort by score and color mana symbol count in descending order pickedCards.sort(new Comparator() { @Override public int compare(PickedCard o1, PickedCard o2) { if (o1.score.equals(o2.score)) { Integer i1 = RateCard.getColorManaCount(o1.card); Integer i2 = RateCard.getColorManaCount(o2.card); return i2.compareTo(i1); } return o2.score.compareTo(o1.score); } }); Set chosenSymbols = new HashSet<>(); for (PickedCard picked : pickedCards) { int differentColorsInCost = RateCard.getDifferentColorManaCount(picked.card); // choose only color card, but only if they are not too gold if (differentColorsInCost > 0 && differentColorsInCost < 3) { // if some colors were already chosen, total amount shouldn't be more than 3 if (chosenSymbols.size() + differentColorsInCost < 4) { for (String symbol : picked.card.getManaCostSymbols()) { symbol = symbol.replace("{", "").replace("}", ""); if (RateCard.isColoredMana(symbol)) { chosenSymbols.add(symbol); } } } } // only two or three color decks are allowed if (chosenSymbols.size() > 1 && chosenSymbols.size() < 4) { List colorsChosen = new ArrayList<>(); for (String symbol : chosenSymbols) { ColoredManaSymbol manaSymbol = ColoredManaSymbol.lookup(symbol.charAt(0)); if (manaSymbol != null) { colorsChosen.add(manaSymbol); } } if (colorsChosen.size() > 1) { // no need to remember picks anymore pickedCards.clear(); return colorsChosen; } } } } return null; } private static class PickedCard { public Card card; public Integer score; public PickedCard(Card card, int score) { this.card = card; this.score = score; } } protected Attackers getPotentialAttackers(Game game) { log.debug("getAvailableAttackers"); Attackers attackers = new Attackers(); List creatures = super.getAvailableAttackers(game); for (Permanent creature : creatures) { int potential = combatPotential(creature, game); if (potential > 0 && creature.getPower().getValue() > 0) { List 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) { log.debug("combatPotential"); 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 getOpponentBlockers(UUID opponentId, Game game) { FilterCreatureForCombatBlock blockFilter = new FilterCreatureForCombatBlock(); return game.getBattlefield().getAllActivePermanents(blockFilter, opponentId, game); } protected CombatSimulator simulateAttack(Attackers attackers, List blockers, UUID opponentId, Game game) { log.debug("simulateAttack"); List 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 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(), game)); } CombatSimulator test = simulateBlock(combat, blockers, game); if (test.evaluate() > bestResult) { best = test; bestResult = test.evaluate(); } } return best; } protected CombatSimulator simulateBlock(CombatSimulator combat, List blockers, Game game) { log.debug("simulateBlock"); TreeNode simulations; simulations = new TreeNode<>(combat); addBlockSimulations(blockers, simulations, game); combat.simulate(game); return getWorstSimulation(simulations); } protected void addBlockSimulations(List blockers, TreeNode node, Game game) { int numGroups = node.getData().groups.size(); Copier copier = new Copier<>(); for (Permanent blocker : blockers) { List 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 child = new TreeNode<>(combat); node.addChild(child); addBlockSimulations(subList, child, game); combat.simulate(game); } } } } protected List remove(List source, Permanent element) { List newList = new ArrayList<>(); for (Permanent permanent : source) { if (!permanent.equals(element)) { newList.add(permanent); } } return newList; } protected CombatSimulator getBestSimulation(TreeNode simulations) { CombatSimulator best = simulations.getData(); int bestResult = best.evaluate(); for (TreeNode node : simulations.getChildren()) { CombatSimulator bestSub = getBestSimulation(node); if (bestSub.evaluate() > bestResult) { best = node.getData(); bestResult = best.evaluate(); } } return best; } protected CombatSimulator getWorstSimulation(TreeNode simulations) { CombatSimulator worst = simulations.getData(); int worstResult = worst.evaluate(); for (TreeNode node : simulations.getChildren()) { CombatSimulator worstSub = getWorstSimulation(node); if (worstSub.evaluate() < worstResult) { worst = node.getData(); worstResult = worst.evaluate(); } } return worst; } protected void findBestPermanentTargets(Outcome outcome, UUID abilityControllerId, UUID sourceId, Ability source, FilterPermanent filter, Game game, Target target, List goodList, List badList, List allList) { // searching for most valuable/powerfull permanents goodList.clear(); badList.clear(); allList.clear(); List usedTargets = target.getTargets(); // search all for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, abilityControllerId, source, game)) { if (usedTargets.contains(permanent.getId())) { continue; } if (outcome.isGood()) { // good effect if (permanent.isControlledBy(abilityControllerId)) { goodList.add(permanent); } else { badList.add(permanent); } } else { // bad effect if (permanent.isControlledBy(abilityControllerId)) { badList.add(permanent); } else { goodList.add(permanent); } } } // sort from tiny to big (more valuable) PermanentComparator comparator = new PermanentComparator(game); goodList.sort(comparator); badList.sort(comparator); // most valueable goes first in good list Collections.reverse(goodList); // most weakest goes first in bad list (no need to reverse) //Collections.reverse(badList); allList.addAll(goodList); allList.addAll(badList); // "can target all mode" don't need your/opponent lists -- all targets goes with same value if (outcome.isCanTargetAll()) { allList.sort(comparator); // bad sort if (outcome.isGood()) { Collections.reverse(allList); // good sort } goodList.clear(); goodList.addAll(allList); badList.clear(); badList.addAll(allList); } } protected List threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List targets) { return threats(playerId, source, filter, game, targets, true); } protected List threats(UUID playerId, Ability source, FilterPermanent filter, Game game, List targets, boolean mostValueableGoFirst) { // most valuable/powerfull permanents goes at first List 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 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 (mostValueableGoFirst) { Collections.reverse(threats); } return threats; } protected void logList(String message, List list) { StringBuilder sb = new StringBuilder(); sb.append(message).append(": "); for (MageObject object : list) { sb.append(object.getName()).append(','); } log.info(sb.toString()); } protected void logAbilityList(String message, List list) { StringBuilder sb = new StringBuilder(); sb.append(message).append(": "); for (Ability ability : list) { sb.append(ability.getRule()).append(','); } log.debug(sb.toString()); } private void playRemoval(Set 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 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; } } } } } } } } } @Override public void cleanUpOnMatchEnd() { super.cleanUpOnMatchEnd(); } @Override public ComputerPlayer copy() { return new ComputerPlayer(this); } private boolean tryAddTarget(Target target, UUID id, Ability source, Game game) { // workaround to to check successfull targets add int before = target.getTargets().size(); target.addTarget(id, source, game); int after = target.getTargets().size(); return before != after; } private boolean tryAddTarget(Target target, UUID id, int amount, Ability source, Game game) { // workaround to to check successfull targets add int before = target.getTargets().size(); target.addTarget(id, amount, source, game); int after = target.getTargets().size(); return before != after; } /** * Sets a possible target player. Depends on bad/good outcome * * @param source null on choose and non-null on chooseTarget */ private boolean setTargetPlayer(Outcome outcome, Target target, Ability source, UUID abilityControllerId, UUID randomOpponentId, Game game, boolean required) { Outcome affectedOutcome; if (abilityControllerId == this.playerId) { // selects for itself affectedOutcome = outcome; } else { // selects for another player affectedOutcome = Outcome.inverse(outcome); } if (target.getOriginalTarget() instanceof TargetOpponent) { if (source == null) { if (target.canTarget(randomOpponentId, game)) { target.add(randomOpponentId, game); return true; } } else if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { target.addTarget(randomOpponentId, source, game); return true; } for (UUID possibleOpponentId : game.getOpponents(abilityControllerId)) { if (source == null) { if (target.canTarget(possibleOpponentId, game)) { target.add(possibleOpponentId, game); return true; } } else if (target.canTarget(abilityControllerId, possibleOpponentId, source, game)) { target.addTarget(possibleOpponentId, source, game); return true; } } return false; } UUID sourceId = source != null ? source.getSourceId() : null; if (target.getOriginalTarget() instanceof TargetPlayer) { if (affectedOutcome.isGood()) { if (source == null) { // good if (target.canTarget(abilityControllerId, game)) { target.add(abilityControllerId, game); return true; } if (target.isRequired(sourceId, game)) { if (target.canTarget(randomOpponentId, game)) { target.add(randomOpponentId, game); return true; } } } else { // good if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { target.addTarget(abilityControllerId, source, game); return true; } if (target.isRequired(sourceId, game)) { if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { target.addTarget(randomOpponentId, source, game); return true; } } } } else if (source == null) { // bad if (target.canTarget(randomOpponentId, game)) { target.add(randomOpponentId, game); return true; } if (target.isRequired(sourceId, game)) { if (target.canTarget(abilityControllerId, game)) { target.add(abilityControllerId, game); return true; } } } else { // bad if (target.canTarget(abilityControllerId, randomOpponentId, source, game)) { target.addTarget(randomOpponentId, source, game); return true; } if (required) { if (target.canTarget(abilityControllerId, abilityControllerId, source, game)) { target.addTarget(abilityControllerId, source, game); return true; } } } return false; } return false; } /** * Returns an opponent by random * * @param abilityControllerId * @param game * @return */ private UUID getRandomOpponent(UUID abilityControllerId, Game game) { return RandomUtil.randomFromCollection(game.getOpponents(abilityControllerId)); } @Override public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { Map useable = PlayerImpl.getCastableSpellAbilities(game, this.getId(), card, game.getState().getZone(card.getId()), noMana); return useable.values().stream().findFirst().orElse(null); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Player obj = (Player) o; if (this.getId() == null || obj.getId() == null) { return false; } return this.getId().equals(obj.getId()); } @Override public boolean isHuman() { if (human) { throw new IllegalStateException("Computer player can't be Human"); } else { return false; } } @Override public void restore(Player player) { super.restore(player); // restore used in AI simulations // all human players converted to computer and analyse this.human = false; } public long getLastThinkTime() { return lastThinkTime; } public void setLastThinkTime(long lastThinkTime) { this.lastThinkTime = lastThinkTime; } }