mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 19:28:01 +00:00
CountersMoveAi: total refacor of AI class, now it has better logic for Tgt -> Defined and Source -> Tgt and for without Tgt
This commit is contained in:
@@ -1,129 +1,449 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
public class CountersMoveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what
|
||||
// the expected targets could be
|
||||
final Random r = MyRandom.getRandom();
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!moveTgtAI(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SpellAbilityAi.playReusable(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return MyRandom.getRandom().nextFloat() < .8f; // random success
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
|
||||
final Card host = sa.getHostCard();
|
||||
final String type = sa.getParam("CounterType");
|
||||
final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type);
|
||||
|
||||
// Don't tap creatures that may be able to block
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CounterType.P1P1.equals(cType) && sa.hasParam("Source")) {
|
||||
int amount = calcAmount(sa, cType);
|
||||
final List<Card> srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa);
|
||||
if (ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
// opponent Creature with +1/+1 counter does attack
|
||||
// try to steal counter from it to kill it
|
||||
if (ph.inCombat() && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
for (final Card c : srcCards) {
|
||||
// source is not controlled by current player
|
||||
if (!ph.isPlayerTurn(c.getController())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int a = c.getCounters(cType);
|
||||
if (a < amount) {
|
||||
continue;
|
||||
}
|
||||
if (ph.getCombat().isAttacking(c)) {
|
||||
// get copy of creature with removed Counter
|
||||
final Card cpy = CardUtil.getLKICopy(c);
|
||||
// cant use substract on Copy
|
||||
cpy.setCounters(cType, a - amount);
|
||||
|
||||
// a removed counter would kill it
|
||||
if (cpy.getNetToughness() <= cpy.getDamage()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// something you can't block, try to reduce its
|
||||
// attack
|
||||
if (!ComputerUtilCard.canBeBlockedProfitably(ai, cpy)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// for Simic Fluxmage and other
|
||||
if (!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else if (CounterType.P1P1.equals(cType) && sa.hasParam("Defined")) {
|
||||
// something like Cyptoplast Root-kin
|
||||
if (ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
if (ph.inCombat() && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
|
||||
}
|
||||
}
|
||||
// for Simic Fluxmage and other
|
||||
if (!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
|
||||
if (!moveTgtAI(ai, sa) && !mandatory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sa.isTargetNumberValid() && mandatory) {
|
||||
final Game game = ai.getGame();
|
||||
List<Card> tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
if (tgtCards.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card card = ComputerUtilCard.getWorstAI(tgtCards);
|
||||
sa.getTargets().add(card);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// no target Probably something like Graft
|
||||
|
||||
if (mandatory) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
final String type = sa.getParam("CounterType");
|
||||
final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type);
|
||||
|
||||
final List<Card> srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa);
|
||||
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
|
||||
|
||||
if (srcCards.isEmpty() || destCards.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card src = srcCards.get(0);
|
||||
final Card dest = destCards.get(0);
|
||||
|
||||
// for such Trigger, do not move counter to another players creature
|
||||
if (!dest.getController().equals(ai)) {
|
||||
return false;
|
||||
} else if (isUselessCreature(ai, dest)) {
|
||||
return false;
|
||||
} else if (dest.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cType != null) {
|
||||
if (!dest.canReceiveCounters(cType)) {
|
||||
return false;
|
||||
}
|
||||
final int amount = calcAmount(sa, cType);
|
||||
int a = src.getCounters(cType);
|
||||
if (a < amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card srcCopy = CardUtil.getLKICopy(src);
|
||||
// cant use substract on Copy
|
||||
srcCopy.setCounters(cType, a - amount);
|
||||
|
||||
final Card destCopy = CardUtil.getLKICopy(dest);
|
||||
destCopy.setCounters(cType, dest.getCounters(cType) + amount);
|
||||
|
||||
int oldEval = ComputerUtilCard.evaluateCreature(src) + ComputerUtilCard.evaluateCreature(dest);
|
||||
int newEval = ComputerUtilCard.evaluateCreature(srcCopy) + ComputerUtilCard.evaluateCreature(destCopy);
|
||||
|
||||
if (newEval < oldEval) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// no target
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!moveTgtAI(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int calcAmount(final SpellAbility sa, final CounterType cType) {
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
final String amountStr = sa.getParam("CounterNum");
|
||||
|
||||
// TODO handle proper calculation of X values based on Cost
|
||||
int amount = 0;
|
||||
if (!sa.getParam("CounterNum").equals("All")) {
|
||||
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||
}
|
||||
// don't use it if no counters to add
|
||||
if (amount <= 0) {
|
||||
return false;
|
||||
|
||||
if (amountStr.equals("All")) {
|
||||
// sa has Source, otherwise Source is the Target
|
||||
if (sa.hasParam("Source")) {
|
||||
final List<Card> srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa);
|
||||
for (final Card c : srcCards) {
|
||||
amount += c.getCounters(cType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
amount = AbilityUtils.calculateAmount(host, amountStr, sa);
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
boolean chance = false;
|
||||
private boolean moveTgtAI(final Player ai, final SpellAbility sa) {
|
||||
|
||||
if (SpellAbilityAi.playReusable(ai, sa)) {
|
||||
return chance;
|
||||
}
|
||||
|
||||
return ((r.nextFloat() < .6667) && chance);
|
||||
} // moveCounterCanPlayAI
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card host = sa.getHostCard();
|
||||
final TargetRestrictions abTgt = sa.getTargetRestrictions();
|
||||
final Game game = ai.getGame();
|
||||
final String type = sa.getParam("CounterType");
|
||||
final String amountStr = sa.getParam("CounterNum");
|
||||
int amount = 0;
|
||||
if (!sa.getParam("CounterNum").equals("All")) {
|
||||
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||
}
|
||||
boolean chance = false;
|
||||
boolean preferred = true;
|
||||
final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type);
|
||||
|
||||
final CounterType cType = CounterType.valueOf(sa.getParam("CounterType"));
|
||||
final List<Card> srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa);
|
||||
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
|
||||
if ((srcCards.size() > 0 && sa.getParam("CounterNum").equals("All"))) {
|
||||
amount = srcCards.get(0).getCounters(cType);
|
||||
}
|
||||
if (abTgt == null) {
|
||||
if ((srcCards.size() > 0)
|
||||
&& cType.equals(CounterType.P1P1) // move +1/+1 counters away
|
||||
// from
|
||||
// permanents that cannot use
|
||||
// them
|
||||
&& (destCards.size() > 0) && destCards.get(0).getController() == ai
|
||||
&& (!srcCards.get(0).isCreature() || srcCards.get(0).hasStartOfKeyword("CARDNAME can't attack"))) {
|
||||
List<Card> tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
chance = true;
|
||||
}
|
||||
} else { // targeted
|
||||
final Player player = sa.isCurse() ? ai.getOpponent() : ai;
|
||||
CardCollectionView list = CardLists.getTargetableCards(player.getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.getValidCards(list, abTgt.getValidTgts(), host.getController(), host, sa);
|
||||
if (list.isEmpty() && mandatory) {
|
||||
// If there isn't any prefered cards to target, gotta choose
|
||||
// non-preferred ones
|
||||
list = CardLists.getTargetableCards(player.getOpponent().getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.getValidCards(list, abTgt.getValidTgts(), host.getController(), host, sa);
|
||||
preferred = false;
|
||||
}
|
||||
// Not mandatory, or the the list was regenerated and is still
|
||||
// empty,
|
||||
// so return false since there are no targets
|
||||
if (list.isEmpty()) {
|
||||
if (sa.hasParam("Defined")) {
|
||||
final int amount = calcAmount(sa, cType);
|
||||
tgtCards = CardLists.filter(tgtCards, CardPredicates.hasCounter(cType));
|
||||
|
||||
// SA uses target for Source
|
||||
// Target => Defined
|
||||
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
|
||||
|
||||
if (destCards.isEmpty()) {
|
||||
// something went wrong
|
||||
return false;
|
||||
}
|
||||
|
||||
Card choice = null;
|
||||
final Card dest = destCards.get(0);
|
||||
|
||||
// Choose targets here:
|
||||
if (sa.isCurse()) {
|
||||
if (preferred) {
|
||||
choice = CountersAi.chooseCursedTarget(list, type, amount);
|
||||
}
|
||||
else if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
}
|
||||
else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
// remove dest from targets, because move doesn't work that way
|
||||
tgtCards.remove(dest);
|
||||
|
||||
if (cType != null && !dest.canReceiveCounters(cType)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
if (preferred) {
|
||||
choice = CountersAi.chooseBoonTarget(list, type);
|
||||
|
||||
// prefered logic for this: try to steal counter
|
||||
List<Card> oppList = CardLists.filterControlledBy(tgtCards, ai.getOpponents());
|
||||
if (!oppList.isEmpty()) {
|
||||
List<Card> best = CardLists.filter(oppList, new Predicate<Card>() {
|
||||
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// do not weak a useless creature if able
|
||||
if (isUselessCreature(ai, card)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card srcCardCpy = CardUtil.getLKICopy(card);
|
||||
// cant use substract on Copy
|
||||
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
|
||||
|
||||
// do not steal a P1P1 from Undying if it would die
|
||||
// this way
|
||||
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
|
||||
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying") || card.isToken()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// if no Prefered found, try normal list
|
||||
if (best.isEmpty()) {
|
||||
best = oppList;
|
||||
}
|
||||
else if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
|
||||
Card card = ComputerUtilCard.getBestCreatureAI(best);
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
choice = Aggregates.random(list);
|
||||
|
||||
}
|
||||
|
||||
// from your creature, try to take from the weakest
|
||||
FCollection<Player> ally = ai.getAllies();
|
||||
ally.add(ai);
|
||||
|
||||
List<Card> aiList = CardLists.filterControlledBy(tgtCards, ally);
|
||||
if (!aiList.isEmpty()) {
|
||||
List<Card> best = CardLists.filter(aiList, new Predicate<Card>() {
|
||||
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// gain from useless
|
||||
if (isUselessCreature(ai, card)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// source would leave the game
|
||||
if (card.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// try to remove P1P1 from undying or evolve
|
||||
if (CounterType.P1P1.equals(cType)) {
|
||||
if (card.hasKeyword("Undying") || card.hasKeyword("Evolve")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (best.isEmpty()) {
|
||||
best = aiList;
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getWorstCreatureAI(best);
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - I think choice can be null here. Is that ok for
|
||||
// addTarget()?
|
||||
sa.getTargets().add(choice);
|
||||
return false;
|
||||
} else {
|
||||
// SA uses target for Defined
|
||||
// Source => Targeted
|
||||
final List<Card> srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa);
|
||||
|
||||
if (srcCards.isEmpty()) {
|
||||
// something went wrong
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card src = srcCards.get(0);
|
||||
if (cType != null) {
|
||||
if (src.getCounters(cType) <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<Card> aiList = CardLists.filterControlledBy(tgtCards, ai);
|
||||
if (!aiList.isEmpty()) {
|
||||
List<Card> best = CardLists.filter(aiList, new Predicate<Card>() {
|
||||
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// gain from useless
|
||||
if (isUselessCreature(ai, card)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// source would leave the game
|
||||
if (card.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cType != null) {
|
||||
if (CounterType.P1P1.equals(cType) && card.hasKeyword("Undying")) {
|
||||
return false;
|
||||
}
|
||||
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!card.canReceiveCounters(cType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (best.isEmpty()) {
|
||||
best = aiList;
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getBestCreatureAI(best);
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// move counter to opponents creature but only if you can not steal
|
||||
// them
|
||||
// try to move to something useless or something that would leave
|
||||
// play
|
||||
List<Card> oppList = CardLists.filterControlledBy(tgtCards, ai.getOpponents());
|
||||
if (!oppList.isEmpty()) {
|
||||
List<Card> best = CardLists.filter(oppList, new Predicate<Card>() {
|
||||
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// gain from useless
|
||||
if (!isUselessCreature(ai, card)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// source would leave the game
|
||||
if (!card.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (best.isEmpty()) {
|
||||
best = aiList;
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getBestCreatureAI(best);
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return chance;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user