package forge; import forge.card.cardFactory.CardFactoryUtil; import forge.card.trigger.Trigger; import java.util.ArrayList; import java.util.HashMap; import java.util.Random; //doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer() /** * <p>ComputerUtil_Attack2 class.</p> * * @author Forge * @version $Id: $ */ public class ComputerUtil_Attack2 { //possible attackers and blockers private CardList attackers; private CardList blockers; private CardList playerCreatures; private int blockerLife; private Random random = MyRandom.random; private final int randomInt = random.nextInt(); private CardList humanList; //holds human player creatures private CardList computerList;//holds computer creatures private int aiAggression = 0; // added by Masher, how aggressive the ai attack will be depending on circumstances /** * <p>Constructor for ComputerUtil_Attack2.</p> * * @param possibleAttackers an array of {@link forge.Card} objects. * @param possibleBlockers an array of {@link forge.Card} objects. * @param blockerLife a int. */ public ComputerUtil_Attack2(Card[] possibleAttackers, Card[] possibleBlockers, int blockerLife) { this(new CardList(possibleAttackers), new CardList(possibleBlockers), blockerLife); } /** * <p>Constructor for ComputerUtil_Attack2.</p> * * @param possibleAttackers a {@link forge.CardList} object. * @param possibleBlockers a {@link forge.CardList} object. * @param blockerLife a int. */ public ComputerUtil_Attack2(CardList possibleAttackers, CardList possibleBlockers, int blockerLife) { humanList = new CardList(possibleBlockers.toArray()); humanList = humanList.getType("Creature"); computerList = new CardList(possibleAttackers.toArray()); computerList = computerList.getType("Creature"); playerCreatures = new CardList(possibleBlockers.toArray()); playerCreatures = playerCreatures.getType("Creature"); attackers = getPossibleAttackers(possibleAttackers); blockers = getPossibleBlockers(possibleBlockers, attackers); this.blockerLife = blockerLife; }//constructor /** * <p>sortAttackers.</p> * * @param in a {@link forge.CardList} object. * @return a {@link forge.CardList} object. */ public CardList sortAttackers(CardList in) { CardList list = new CardList(); //Cards with triggers should come first (for Battle Cry) for (Card attacker : in) { ArrayList<Trigger> registeredTriggers = AllZone.getTriggerHandler().getRegisteredTriggers(); for (Trigger trigger : registeredTriggers) { HashMap<String, String> trigParams = trigger.getMapParams(); if (trigParams.get("Mode").equals("Attacks") && trigger.getHostCard().equals(attacker)) list.add(attacker); } } for (Card attacker : in) { if (!list.contains(attacker)) list.add(attacker); } return list; }//sortAttackers() //Is there any reward for attacking? (for 0/1 creatures there is not) /** * <p>isEffectiveAttacker.</p> * * @param attacker a {@link forge.Card} object. * @param combat a {@link forge.Combat} object. * @return a boolean. */ public boolean isEffectiveAttacker(Card attacker, Combat combat) { if (CombatUtil.damageIfUnblocked(attacker, AllZone.getHumanPlayer(), combat) > 0) return true; if (CombatUtil.poisonIfUnblocked(attacker, AllZone.getHumanPlayer(), combat) > 0) return true; ArrayList<Trigger> registeredTriggers = AllZone.getTriggerHandler().getRegisteredTriggers(); for (Trigger trigger : registeredTriggers) if (CombatUtil.combatTriggerWillTrigger(attacker, null, trigger, combat) && trigger.getHostCard().getController().isComputer()) return true; return false; } /** * <p>getPossibleAttackers.</p> * * @param in a {@link forge.CardList} object. * @return a {@link forge.CardList} object. */ public CardList getPossibleAttackers(CardList in) { CardList list = new CardList(in.toArray()); list = list.filter(new CardListFilter() { public boolean addCard(Card c) { return CombatUtil.canAttack(c); } }); return list; }//getPossibleAttackers() /** * <p>getPossibleBlockers.</p> * * @param blockers a {@link forge.CardList} object. * @param attackers a {@link forge.CardList} object. * @return a {@link forge.CardList} object. */ public CardList getPossibleBlockers(CardList blockers, CardList attackers) { CardList possibleBlockers = new CardList(blockers.toArray()); final CardList attackerList = new CardList(attackers.toArray()); possibleBlockers = possibleBlockers.filter(new CardListFilter() { public boolean addCard(Card c) { if (!c.isCreature()) return false; for (Card attacker : attackerList) { if (CombatUtil.canBlock(attacker, c)) return true; } return false; } }); return possibleBlockers; }//getPossibleBlockers() //this checks to make sure that the computer player //doesn't lose when the human player attacks //this method is used by getAttackers() /** * <p>notNeededAsBlockers.</p> * * @param attackers a {@link forge.CardList} object. * @param combat a {@link forge.Combat} object. * @return a {@link forge.CardList} object. */ public CardList notNeededAsBlockers(CardList attackers, Combat combat) { CardList notNeededAsBlockers = new CardList(attackers.toArray()); CardListUtil.sortAttackLowFirst(attackers); int blockersNeeded = attackers.size(); //don't hold back creatures that can't block any of the human creatures CardList list = getPossibleBlockers(attackers, humanList); for (int i = 0; i < list.size(); i++) { if (!doesHumanAttackAndWin(i)) { blockersNeeded = i; break; } else notNeededAsBlockers.remove(list.get(i)); } if (blockersNeeded == list.size()) { // Human will win unless everything is kept back to block return notNeededAsBlockers; } // Increase the total number of blockers needed by 1 if Finest Hour in play // (human will get an extra first attack with a creature that untaps) // In addition, if the computer guesses it needs no blockers, make sure that // it won't be surprised by Exalted int humanExaltedBonus = countExaltedBonus(AllZone.getHumanPlayer()); if (humanExaltedBonus > 0) { int nFinestHours = AllZoneUtil.getPlayerCardsInPlay(AllZone.getHumanPlayer(), "Finest Hour").size(); if ((blockersNeeded == 0 || nFinestHours > 0) && humanList.size() > 0) { // // total attack = biggest creature + exalted, *2 if Rafiq is in play int humanBaseAttack = getAttack(humanList.get(0)) + humanExaltedBonus; if (nFinestHours > 0) { // For Finest Hour, one creature could attack and get the bonus TWICE humanBaseAttack = humanBaseAttack + humanExaltedBonus; } int totalExaltedAttack = AllZoneUtil.isCardInPlay("Rafiq of the Many", AllZone.getHumanPlayer()) ? 2 * humanBaseAttack : humanBaseAttack; if ((AllZone.getComputerPlayer().getLife() - 3) <= totalExaltedAttack) { // We will lose if there is an Exalted attack -- keep one blocker if (blockersNeeded == 0 && notNeededAsBlockers.size() > 0) notNeededAsBlockers.remove(0); // Finest Hour allows a second Exalted attack: keep a blocker for that too if (nFinestHours > 0 && notNeededAsBlockers.size() > 0) notNeededAsBlockers.remove(0); } } } //re-add creatures with vigilance for (Card c : attackers) { if (c.hasKeyword("Vigilance")) notNeededAsBlockers.add(c); } return notNeededAsBlockers; } //this uses a global variable, which isn't perfect /** * <p>doesHumanAttackAndWin.</p> * * @param nBlockingCreatures a int. * @return a boolean. */ public boolean doesHumanAttackAndWin(int nBlockingCreatures) { int totalAttack = 0; int stop = humanList.size() - nBlockingCreatures; for (int i = 0; i < stop; i++) totalAttack += getAttack(humanList.get(i)); //originally -3 so the computer will try to stay at 3 life //0 now to prevent the AI from not attacking when it's got low life //(seems to happen too often) return AllZone.getComputerPlayer().getLife() <= totalAttack; } /** * <p>doAssault.</p> * * @return a boolean. */ private boolean doAssault() { //Beastmaster Ascension if (AllZoneUtil.isCardInPlay("Beastmaster Ascension", AllZone.getComputerPlayer()) && attackers.size() > 1) { CardList beastions = AllZoneUtil.getCardsInZone(Constant.Zone.Battlefield, AllZone.getComputerPlayer()). getName("Beastmaster Ascension"); int minCreatures = 7; for (Card beastion : beastions) { int counters = beastion.getCounters(Counters.QUEST); minCreatures = Math.min(minCreatures, 7 - counters); } if (attackers.size() >= minCreatures) return true; } //I think this is right but the assault code may still be a little off CardListUtil.sortAttackLowFirst(attackers); int totalAttack = 0; //presumes the Human will block for (int i = 0; i < (attackers.size() - blockers.size()); i++) totalAttack += getAttack(attackers.get(i)); return blockerLife <= totalAttack; }//doAssault() /** * <p>chooseDefender.</p> * * @param c a {@link forge.Combat} object. * @param bAssault a boolean. */ public void chooseDefender(Combat c, boolean bAssault) { // TODO: split attackers to different planeswalker/human // AI will only attack one Defender per combat for now ArrayList<Object> defs = c.getDefenders(); if (defs.size() == 1 || bAssault) { c.setCurrentDefender(0); return; } // Randomnly determine who EVERYONE is attacking // would be better to determine more individually int n = MyRandom.random.nextInt(defs.size()); c.setCurrentDefender(n); return; } /** * <p>Getter for the field <code>attackers</code>.</p> * * @return a {@link forge.Combat} object. */ public Combat getAttackers() { //if this method is called multiple times during a turn, //it will always return the same value //randomInt is used so that the computer doesn't always //do the same thing on turn 3 if he had the same creatures in play //I know this is a little confusing random.setSeed(AllZone.getPhase().getTurn() + randomInt); Combat combat = new Combat(); combat.setAttackingPlayer(AllZone.getCombat().getAttackingPlayer()); combat.setDefendingPlayer(AllZone.getCombat().getDefendingPlayer()); combat.setDefenders(AllZone.getCombat().getDefenders()); boolean bAssault = doAssault(); // Determine who will be attacked chooseDefender(combat, bAssault); CardList attackersLeft = new CardList(attackers.toArray()); //Atackers that don't really have a choice for (Card attacker : attackers) { if ((attacker.hasKeyword("CARDNAME attacks each turn if able.") || attacker.hasKeyword("At the beginning of the end step, destroy CARDNAME.") || attacker.hasKeyword("At the beginning of the end step, exile CARDNAME.") || attacker.hasKeyword("At the beginning of the end step, sacrifice CARDNAME.") || attacker.getSacrificeAtEOT() || attacker.getSirenAttackOrDestroy()) && CombatUtil.canAttack(attacker, combat)) { combat.addAttacker(attacker); attackersLeft.remove(attacker); } } // ******************* // start of edits // ******************* int computerForces = 0; int playerForces = 0; int playerForcesForAttritionalAttack = 0; // examine the potential forces CardList nextTurnAttackers = new CardList(); int candidateCounterAttackDamage = 0; int candidateTotalBlockDamage = 0; for (Card pCard : playerCreatures) { // if the creature can attack next turn add it to counter attackers list if (CombatUtil.canAttackNextTurn(pCard)) { nextTurnAttackers.add(pCard); if (pCard.getNetCombatDamage() > 0) { candidateCounterAttackDamage += pCard.getNetCombatDamage(); candidateTotalBlockDamage += pCard.getNetCombatDamage(); playerForces += 1; // player forces they might use to attack } } // increment player forces that are relevant to an attritional attack - includes walls if (CombatUtil.canBlock(pCard)) { playerForcesForAttritionalAttack += 1; } } // find the potential counter attacking damage compared to AI life total double aiLifeToPlayerDamageRatio = 1000000; if (candidateCounterAttackDamage > 0) aiLifeToPlayerDamageRatio = (double) AllZone.getComputerPlayer().life / candidateCounterAttackDamage; // get the potential damage and strength of the AI forces CardList candidateAttackers = new CardList(); int candidateUnblockedDamage = 0; for (Card pCard : computerList) { // if the creature can attack then it's a potential attacker this turn, assume summoning sickness creatures will be able to if (CombatUtil.canAttackNextTurn(pCard)) { candidateAttackers.add(pCard); if (pCard.getNetCombatDamage() > 0) { candidateUnblockedDamage += CombatUtil.damageIfUnblocked(pCard, AllZone.getHumanPlayer(), combat); computerForces += 1; } } } // find the potential damage ratio the AI can cause double playerLifeToDamageRatio = 1000000; if (candidateUnblockedDamage > 0) playerLifeToDamageRatio = (double) AllZone.getHumanPlayer().life / candidateUnblockedDamage; /*System.out.println(String.valueOf(aiLifeToPlayerDamageRatio) + " = ai life to player damage ratio"); System.out.println(String.valueOf(playerLifeToDamageRatio) + " = player life ai player damage ratio");*/ // determine if the ai outnumbers the player int outNumber = computerForces - playerForces; // compare the ratios, higher = better for ai double ratioDiff = aiLifeToPlayerDamageRatio - playerLifeToDamageRatio; /* System.out.println(String.valueOf(ratioDiff) + " = ratio difference, higher = better for ai"); System.out.println(String.valueOf(outNumber) + " = outNumber, higher = better for ai"); */ // ********************* // if outnumber and superior ratio work out whether attritional all out attacking will work // attritional attack will expect some creatures to die but to achieve victory by sheer weight // of numbers attacking turn after turn. It's not calculate very carefully, the accuracy // can probably be improved // ********************* boolean doAttritionalAttack = false; // get list of attackers ordered from low power to high CardListUtil.sortAttackLowFirst(attackers); // get player life total int playerLife = AllZone.getHumanPlayer().life; // get the list of attackers up to the first blocked one CardList attritionalAttackers = new CardList(); for (int x = 0; x < attackers.size() - playerForces; x++) { attritionalAttackers.add(attackers.getCard(x)); } // until the attackers are used up or the player would run out of life int attackRounds = 1; while (attritionalAttackers.size() > 0 && playerLife > 0 && attackRounds < 99) { // sum attacker damage int damageThisRound = 0; for (int y = 0; y < attritionalAttackers.size(); y++) { damageThisRound += attritionalAttackers.getCard(y).getNetCombatDamage(); } // remove from player life playerLife -= damageThisRound; // shorten attacker list by the length of the blockers - assuming all blocked are killed for convenience for (int z = 0; z < playerForcesForAttritionalAttack; z++) { if (attritionalAttackers.size() > 0) { attritionalAttackers.remove(attritionalAttackers.size() - 1); } } attackRounds += 1; if (playerLife <= 0) { doAttritionalAttack = true; } } //System.out.println(doAttritionalAttack + " = do attritional attack"); // ********************* // end attritional attack calculation // ********************* // ********************* // see how long until unblockable attackers will be fatal // ********************* double unblockableDamage = 0; double turnsUntilDeathByUnblockable = 0; boolean doUnblockableAttack = false; for (Card attacker : attackers) { boolean isUnblockableCreature = true; // check blockers individually, as the bulk canBeBlocked doesn't check all circumstances for (Card blocker : blockers) { if (CombatUtil.canBlock(attacker, blocker)) { isUnblockableCreature = false; } } if (isUnblockableCreature) { unblockableDamage += CombatUtil.damageIfUnblocked(attacker, AllZone.getHumanPlayer(), combat); } } if (unblockableDamage > 0) { turnsUntilDeathByUnblockable = AllZone.getHumanPlayer().life / unblockableDamage; } if (unblockableDamage > AllZone.getHumanPlayer().life) { doUnblockableAttack = true; } // ***************** // end see how long until unblockable attackers will be fatal // ***************** // decide on attack aggression based on a comparison of forces, life totals and other considerations // some bad "magic numbers" here, TODO replace with nice descriptive variable names if ((ratioDiff > 0 && doAttritionalAttack)) { // (playerLifeToDamageRatio <= 1 && ratioDiff >= 1 && outNumber > 0) || aiAggression = 5; // attack at all costs } else if ((playerLifeToDamageRatio < 2 && ratioDiff >= 0) || ratioDiff > 3 || (ratioDiff > 0 && outNumber > 0)) { aiAggression = 3; // attack expecting to kill creatures or damage player. } else if (ratioDiff >= 0 || ratioDiff + outNumber >= -1) { // at 0 ratio expect to potentially gain an advantage by attacking first // if the ai has a slight advantage // or the ai has a significant advantage numerically but only a slight disadvantage damage/life aiAggression = 2; // attack expecting to destroy creatures/be unblockable } else if (ratioDiff < 0 && aiLifeToPlayerDamageRatio > 1) { // the player is overmatched but there are a few turns before death aiAggression = 2; // attack expecting to destroy creatures/be unblockable } else if (doUnblockableAttack || ((ratioDiff * -1) < turnsUntilDeathByUnblockable)) { aiAggression = 1; // look for unblockable creatures that might be able to attack for a bit of // fatal damage even if the player is significantly better } else if (ratioDiff < 0) { aiAggression = 0; } // stay at home to block System.out.println(String.valueOf(aiAggression) + " = ai aggression"); // **************** // End of edits // **************** //Exalted if (combat.getAttackers().length == 0 && (countExaltedBonus(AllZone.getComputerPlayer()) >= 3 || AllZoneUtil.isCardInPlay("Rafiq of the Many", AllZone.getComputerPlayer()) || AllZoneUtil.getPlayerCardsInPlay(AllZone.getComputerPlayer(), "Battlegrace Angel").size() >= 2 || (AllZoneUtil.getPlayerCardsInPlay(AllZone.getComputerPlayer(), "Finest Hour").size() >= 1) && AllZone.getPhase().isFirstCombat()) && !bAssault) { int biggest = 0; Card att = null; for (int i = 0; i < attackersLeft.size(); i++) { if (getAttack(attackersLeft.get(i)) > biggest) { biggest = getAttack(attackersLeft.get(i)); att = attackersLeft.get(i); } } if (att != null && CombatUtil.canAttack(att, combat)) combat.addAttacker(att); System.out.println("Exalted"); } //do assault (all creatures attack) if the computer would win the game //or if the computer has 4 creatures and the player has 1 else if (bAssault) { System.out.println("Assault"); CardListUtil.sortAttack(attackersLeft); for (int i = 0; i < attackersLeft.size(); i++) if (CombatUtil.canAttack(attackersLeft.get(i), combat)) combat.addAttacker(attackersLeft.get(i)); } else { System.out.println("Normal attack"); attackersLeft = notNeededAsBlockers(attackersLeft, combat); System.out.println(attackersLeft.size()); attackersLeft = sortAttackers(attackersLeft); for (int i = 0; i < attackersLeft.size(); i++) { Card attacker = attackersLeft.get(i); int totalFirstStrikeBlockPower = 0; if (!attacker.hasFirstStrike() && !attacker.hasDoubleStrike()) totalFirstStrikeBlockPower = CombatUtil.getTotalFirstStrikeBlockPower(attacker, AllZone.getHumanPlayer()); if (shouldAttack(attacker, blockers, combat) && (totalFirstStrikeBlockPower < attacker.getKillDamage() || aiAggression == 5) && CombatUtil.canAttack(attacker, combat)) combat.addAttacker(attacker); } }//getAttackers() return combat; }//getAttackers() /** * <p>countExaltedBonus.</p> * * @param player a {@link forge.Player} object. * @return a int. */ public int countExaltedBonus(Player player) { CardList list = AllZoneUtil.getPlayerCardsInPlay(player); list = list.filter(new CardListFilter() { public boolean addCard(Card c) { return c.hasKeyword("Exalted"); } }); return list.size(); } /** * <p>getAttack.</p> * * @param c a {@link forge.Card} object. * @return a int. */ public int getAttack(Card c) { int n = c.getNetCombatDamage(); if (c.hasKeyword("Double Strike")) n *= 2; return n; } /** * <p>shouldAttack.</p> * * @param attacker a {@link forge.Card} object. * @param defenders a {@link forge.CardList} object. * @param combat a {@link forge.Combat} object. * @return a boolean. */ public boolean shouldAttack(Card attacker, CardList defenders, Combat combat) { boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blockers boolean canKillAll = true; // indicates if the attacker can kill all single blockers boolean canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect boolean isWorthLessThanAllKillers = true; boolean canBeBlocked = false; if (!isEffectiveAttacker(attacker, combat)) return false; // look at the attacker in relation to the blockers to establish a number of factors about the attacking // context that will be relevant to the attackers decision according to the selected strategy for (Card defender : defenders) { if (CombatUtil.canBlock(attacker, defender)) { //, combat )) { canBeBlocked = true; if (CombatUtil.canDestroyAttacker(attacker, defender, combat, false)) { canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature // see if the defending creature is of higher or lower value. We don't want to attack only to lose value if (CardFactoryUtil.evaluateCreature(defender) <= CardFactoryUtil.evaluateCreature(attacker)) { isWorthLessThanAllKillers = false; } } // see if this attacking creature can destroy this defender, if not record that it can't kill everything if (!CombatUtil.canDestroyBlocker(defender, attacker, combat, false)) { canKillAll = false; if (defender.hasKeyword("Wither") || defender.hasKeyword("Infect")) { canKillAllDangerous = false; // there is a dangerous creature that can survive an attack from this creature } } } } // if the creature cannot block and can kill all opponents they might as well attack, they do nothing staying back if (canKillAll && !CombatUtil.canBlock(attacker) && isWorthLessThanAllKillers) { System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player"); return true; } // decide if the creature should attack based on the prevailing strategy choice in aiAggression switch (aiAggression) { case 5: // all out attacking System.out.println(attacker.getName() + " = all out attacking"); return true; case 4: // expecting to at least trade with something if (canKillAll || (canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked) { System.out.println(attacker.getName() + " = attacking expecting to at least trade with something"); return true; } case 3: // expecting to at least kill a creature of equal value, not be blocked if ((canKillAll && isWorthLessThanAllKillers) || (canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked) { System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable"); return true; } case 2: // attack expecting to attract a group block or destroying a single blocker and surviving if ((canKillAll && !canBeKilledByOne) || !canBeBlocked) { System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block"); return true; } case 1: // unblockable creatures only if (!canBeBlocked) { System.out.println(attacker.getName() + " = attacking expecting not to be blocked"); return true; } } return false; // don't attack } }//end class ComputerUtil_Attack2