Merge branch 'master' of git.cardforge.org:core-developers/forge into agetian-master

This commit is contained in:
Michael Kamensky
2021-06-05 17:34:06 +03:00
1889 changed files with 19680 additions and 14391 deletions

2
.gitignore vendored
View File

@@ -32,6 +32,8 @@ bin
gen gen
*.log *.log
# Ignore macOS Spotlight rubbish
.DS_Store
# TODO: specify what these ignores are for (releasing?) # TODO: specify what these ignores are for (releasing?)

View File

@@ -2,7 +2,7 @@
[Official GitLab repo](https://git.cardforge.org/core-developers/forge). [Official GitLab repo](https://git.cardforge.org/core-developers/forge).
Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated) Dev instructions here: [Getting Started](https://git.cardforge.org/core-developers/forge/-/wikis/(SM-autoconverted)-how-to-get-started-developing-forge) (Somewhat outdated)
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968) Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
@@ -168,11 +168,11 @@ The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desk
## IntelliJ ## IntelliJ
Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/wikis/intellij-setup). Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/-/wikis/Development/intellij-setup).
## Card Scripting ## Card Scripting
Visit [this page](https://www.slightlymagic.net/wiki/Forge_API) for information on scripting. Visit [this page](https://git.cardforge.org/core-developers/forge/-/wikis/Card-scripting-API/Card-scripting-API) for information on scripting.
Card scripting resources are found in the forge-gui/res/ path. Card scripting resources are found in the forge-gui/res/ path.

View File

@@ -54,7 +54,6 @@ import forge.util.TextUtil;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
/** /**
* <p> * <p>
* ComputerUtil_Attack2 class. * ComputerUtil_Attack2 class.
@@ -412,12 +411,10 @@ public class AiAttackController {
final Player opp = this.defendingOpponent; final Player opp = this.defendingOpponent;
// Increase the total number of blockers needed by 1 if Finest Hour in // Increase the total number of blockers needed by 1 if Finest Hour in play
// play
// (human will get an extra first attack with a creature that untaps) // (human will get an extra first attack with a creature that untaps)
// In addition, if the computer guesses it needs no blockers, make sure // In addition, if the computer guesses it needs no blockers, make sure
// that // that it won't be surprised by Exalted
// it won't be surprised by Exalted
final int humanExaltedBonus = opp.countExaltedBonus(); final int humanExaltedBonus = opp.countExaltedBonus();
if (humanExaltedBonus > 0) { if (humanExaltedBonus > 0) {
@@ -427,8 +424,7 @@ public class AiAttackController {
// total attack = biggest creature + exalted, *2 if Rafiq is in play // total attack = biggest creature + exalted, *2 if Rafiq is in play
int humanBasePower = getAttack(this.oppList.get(0)) + humanExaltedBonus; int humanBasePower = getAttack(this.oppList.get(0)) + humanExaltedBonus;
if (finestHour) { if (finestHour) {
// For Finest Hour, one creature could attack and get the // For Finest Hour, one creature could attack and get the bonus TWICE
// bonus TWICE
humanBasePower = humanBasePower + humanExaltedBonus; humanBasePower = humanBasePower + humanExaltedBonus;
} }
final int totalExaltedAttack = opp.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower final int totalExaltedAttack = opp.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower
@@ -449,7 +445,6 @@ public class AiAttackController {
return notNeededAsBlockers; return notNeededAsBlockers;
} }
// this uses a global variable, which isn't perfect
public final boolean doesHumanAttackAndWin(final Player ai, final int nBlockingCreatures) { public final boolean doesHumanAttackAndWin(final Player ai, final int nBlockingCreatures) {
int totalAttack = 0; int totalAttack = 0;
int totalPoison = 0; int totalPoison = 0;
@@ -681,7 +676,6 @@ public class AiAttackController {
* @return a {@link forge.game.combat.Combat} object. * @return a {@link forge.game.combat.Combat} object.
*/ */
public final void declareAttackers(final Combat combat) { public final void declareAttackers(final Combat combat) {
if (this.attackers.isEmpty()) { if (this.attackers.isEmpty()) {
return; return;
} }
@@ -716,7 +710,7 @@ public class AiAttackController {
int attackMax = restrict.getMax(); int attackMax = restrict.getMax();
if (attackMax == -1) { if (attackMax == -1) {
// check with the local limitations vs. the chosen defender // check with the local limitations vs. the chosen defender
attackMax = ComputerUtilCombat.getMaxAttackersFor(defender); attackMax = restrict.getDefenderMax().get(defender) == null ? -1 : restrict.getDefenderMax().get(defender);
} }
if (attackMax == 0) { if (attackMax == 0) {
@@ -776,7 +770,6 @@ public class AiAttackController {
return; return;
} }
if (bAssault) { if (bAssault) {
if (LOG_AI_ATTACKS) if (LOG_AI_ATTACKS)
System.out.println("Assault"); System.out.println("Assault");
@@ -851,7 +844,6 @@ public class AiAttackController {
return; return;
} }
// ******************* // *******************
// Evaluate the creature forces // Evaluate the creature forces
// ******************* // *******************
@@ -935,12 +927,9 @@ public class AiAttackController {
// ********************* // *********************
// if outnumber and superior ratio work out whether attritional all out // if outnumber and superior ratio work out whether attritional all out
// attacking will work // attacking will work attritional attack will expect some creatures to die but to achieve
// 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
// victory by sheer weight // carefully, the accuracy can probably be improved
// of numbers attacking turn after turn. It's not calculate very
// carefully, the accuracy
// can probably be improved
// ********************* // *********************
boolean doAttritionalAttack = false; boolean doAttritionalAttack = false;
// get list of attackers ordered from low power to high // get list of attackers ordered from low power to high
@@ -974,7 +963,6 @@ public class AiAttackController {
doAttritionalAttack = true; doAttritionalAttack = true;
} }
} }
// System.out.println(doAttritionalAttack + " = do attritional attack");
// ********************* // *********************
// end attritional attack calculation // end attritional attack calculation
// ********************* // *********************
@@ -1024,11 +1012,9 @@ public class AiAttackController {
// end see how long until unblockable attackers will be fatal // end see how long until unblockable attackers will be fatal
// ***************** // *****************
// decide on attack aggression based on a comparison of forces, life // decide on attack aggression based on a comparison of forces, life
// totals and other considerations // totals and other considerations some bad "magic numbers" here
// some bad "magic numbers" here, TODO replace with nice descriptive // TODO replace with nice descriptive variable names
// variable names
if (ratioDiff > 0 && doAttritionalAttack) { if (ratioDiff > 0 && doAttritionalAttack) {
this.aiAggression = 5; // attack at all costs this.aiAggression = 5; // attack at all costs
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0)) } else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
@@ -1223,9 +1209,8 @@ public class AiAttackController {
} }
// look at the attacker in relation to the blockers to establish a // look at the attacker in relation to the blockers to establish a
// number of factors about the attacking // number of factors about the attacking context that will be relevant
// context that will be relevant to the attackers decision according to // to the attackers decision according to the selected strategy
// the selected strategy
for (final Card defender : validBlockers) { for (final Card defender : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check // if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) { if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
@@ -1299,14 +1284,11 @@ public class AiAttackController {
} }
if (numberOfPossibleBlockers > 2 if (numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, combat)) || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, this.defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, combat))) { || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, this.defendingOpponent))) {
canBeBlocked = true; canBeBlocked = true;
} }
/*System.out.println(attacker + " canBeKilledByOne: " + canBeKilledByOne + " canKillAll: " // decide if the creature should attack based on the prevailing strategy choice in aiAggression
+ canKillAll + " isWorthLessThanAllKillers: " + isWorthLessThanAllKillers + " canBeBlocked: " + canBeBlocked);*/
// decide if the creature should attack based on the prevailing strategy
// choice in aiAggression
switch (this.aiAggression) { switch (this.aiAggression) {
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) { if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
@@ -1562,4 +1544,4 @@ public class AiAttackController {
return true; return true;
} }
} // end class ComputerUtil_Attack2 }

View File

@@ -180,11 +180,9 @@ public class AiBlockController {
// Good Blocks means a good trade or no trade // Good Blocks means a good trade or no trade
private void makeGoodBlocks(final Combat combat) { private void makeGoodBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : attackersLeft) { for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)) { || attacker.hasKeyword(Keyword.MENACE)) {
@@ -192,7 +190,6 @@ public class AiBlockController {
} }
Card blocker = null; Card blocker = null;
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true); final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
final List<Card> safeBlockers = getSafeBlockers(combat, attacker, blockers); final List<Card> safeBlockers = getSafeBlockers(combat, attacker, blockers);
@@ -305,7 +302,6 @@ public class AiBlockController {
} }
Card blocker = null; Card blocker = null;
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true); final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
for (Card b : blockers) { for (Card b : blockers) {
@@ -366,10 +362,9 @@ public class AiBlockController {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker) final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// if the total damage of the blockgang was not enough // if the total damage of the blockgang was not enough
// without but is enough with this blocker finish the // without but is enough with this blocker finish the blockgang
// blockgang
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) < damageNeeded if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) < damageNeeded
|| CombatUtil.needsBlockers(attacker) > blockGang.size()) { || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size()) {
blockGang.add(blocker); blockGang.add(blocker);
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) >= damageNeeded) { if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) >= damageNeeded) {
currentAttackers.remove(attacker); currentAttackers.remove(attacker);
@@ -407,7 +402,7 @@ public class AiBlockController {
boolean foundDoubleBlock = false; // if true, a good double block is found boolean foundDoubleBlock = false; // if true, a good double block is found
// AI can't handle good blocks with more than three creatures yet // AI can't handle good blocks with more than three creatures yet
if (CombatUtil.needsBlockers(attacker) > (considerTripleBlock ? 3 : 2)) { if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
continue; continue;
} }
@@ -444,7 +439,7 @@ public class AiBlockController {
final int addedValue = ComputerUtilCard.evaluateCreature(blocker); final int addedValue = ComputerUtilCard.evaluateCreature(blocker);
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker) final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size()) if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
&& !(damageNeeded > currentDamage + additionalDamage) && !(damageNeeded > currentDamage + additionalDamage)
// The attacker will be killed // The attacker will be killed
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage() && (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
@@ -454,8 +449,7 @@ public class AiBlockController {
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat))) || (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
// or life is in danger // or life is in danger
&& CombatUtil.canBlock(attacker, blocker, combat)) { && CombatUtil.canBlock(attacker, blocker, combat)) {
// this is needed for attackers that can't be blocked by // this is needed for attackers that can't be blocked by more than 1
// more than 1
currentAttackers.remove(attacker); currentAttackers.remove(attacker);
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
if (CombatUtil.canBlock(attacker, leader, combat)) { if (CombatUtil.canBlock(attacker, leader, combat)) {
@@ -496,7 +490,7 @@ public class AiBlockController {
final int addedValue3 = ComputerUtilCard.evaluateCreature(secondBlocker); final int addedValue3 = ComputerUtilCard.evaluateCreature(secondBlocker);
final int netCombatDamage = attacker.getNetCombatDamage(); final int netCombatDamage = attacker.getNetCombatDamage();
if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size()) if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
&& !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3) && !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
// The attacker will be killed // The attacker will be killed
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage && ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
@@ -510,8 +504,7 @@ public class AiBlockController {
// or life is in danger // or life is in danger
&& CombatUtil.canBlock(attacker, secondBlocker, combat) && CombatUtil.canBlock(attacker, secondBlocker, combat)
&& CombatUtil.canBlock(attacker, thirdBlocker, combat)) { && CombatUtil.canBlock(attacker, thirdBlocker, combat)) {
// this is needed for attackers that can't be blocked by // this is needed for attackers that can't be blocked by more than 1
// more than 1
currentAttackers.remove(attacker); currentAttackers.remove(attacker);
combat.addBlocker(attacker, thirdBlocker); combat.addBlocker(attacker, thirdBlocker);
if (CombatUtil.canBlock(attacker, secondBlocker, combat)) { if (CombatUtil.canBlock(attacker, secondBlocker, combat)) {
@@ -587,12 +580,10 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object. * @param combat a {@link forge.game.combat.Combat} object.
*/ */
private void makeTradeBlocks(final Combat combat) { private void makeTradeBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
List<Card> killingBlockers; List<Card> killingBlockers;
for (final Card attacker : attackersLeft) { for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword(Keyword.MENACE) || attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
@@ -628,7 +619,6 @@ public class AiBlockController {
// Chump Blocks (should only be made if life is in danger) // Chump Blocks (should only be made if life is in danger)
private void makeChumpBlocks(final Combat combat) { private void makeChumpBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
makeChumpBlocks(combat, currentAttackers); makeChumpBlocks(combat, currentAttackers);
@@ -639,7 +629,6 @@ public class AiBlockController {
} }
private void makeChumpBlocks(final Combat combat, List<Card> attackers) { private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
if (attackers.isEmpty() || !ComputerUtilCombat.lifeInDanger(ai, combat)) { if (attackers.isEmpty() || !ComputerUtilCombat.lifeInDanger(ai, combat)) {
return; return;
} }
@@ -694,11 +683,9 @@ public class AiBlockController {
// Block creatures with "can't be blocked except by two or more creatures" // Block creatures with "can't be blocked except by two or more creatures"
private void makeMultiChumpBlocks(final Combat combat) { private void makeMultiChumpBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : currentAttackers) { for (final Card attacker : currentAttackers) {
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
&& !attacker.hasKeyword(Keyword.MENACE) && !attacker.hasKeyword(Keyword.MENACE)
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { && !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
@@ -730,14 +717,12 @@ public class AiBlockController {
/** Reinforce blockers blocking attackers with trample (should only be made if life is in danger) */ /** Reinforce blockers blocking attackers with trample (should only be made if life is in danger) */
private void reinforceBlockersAgainstTrample(final Combat combat) { private void reinforceBlockersAgainstTrample(final Combat combat) {
List<Card> chumpBlockers; List<Card> chumpBlockers;
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE); List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock)); tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
// TODO - should check here for a "rampage-like" trigger that replaced // TODO - should check here for a "rampage-like" trigger that replaced the keyword:
// the keyword:
// "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it." // "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
for (final Card attacker : tramplingAttackers) { for (final Card attacker : tramplingAttackers) {
@@ -764,7 +749,6 @@ public class AiBlockController {
/** Support blockers not destroying the attacker with more blockers to try to kill the attacker */ /** Support blockers not destroying the attacker with more blockers to try to kill the attacker */
private void reinforceBlockersToKill(final Combat combat) { private void reinforceBlockersToKill(final Combat combat) {
List<Card> safeBlockers; List<Card> safeBlockers;
List<Card> blockers; List<Card> blockers;
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock)); List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock));
@@ -1036,27 +1020,21 @@ public class AiBlockController {
} else { } else {
lifeInDanger = false; lifeInDanger = false;
} }
// if life is still in danger // Reinforce blockers blocking attackers with trample if life is still in danger
// Reinforce blockers blocking attackers with trample if life is
// still
// in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat); reinforceBlockersAgainstTrample(combat);
} else { } else {
lifeInDanger = false; lifeInDanger = false;
} }
// Support blockers not destroying the attacker with more blockers // Support blockers not destroying the attacker with more blockers
// to // to try to kill the attacker
// try to kill the attacker
if (!lifeInDanger) { if (!lifeInDanger) {
reinforceBlockersToKill(combat); reinforceBlockersToKill(combat);
} }
// == 2. If the AI life would still be in danger make a safer // == 2. If the AI life would still be in danger make a safer approach ==
// approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); // reset every block clearBlockers(combat, possibleBlockers); // reset every block assignment
// assignment
makeTradeBlocks(combat); // choose necessary trade blocks makeTradeBlocks(combat); // choose necessary trade blocks
// if life is in danger // if life is in danger
makeGoodBlocks(combat); makeGoodBlocks(combat);
@@ -1066,8 +1044,7 @@ public class AiBlockController {
} else { } else {
lifeInDanger = false; lifeInDanger = false;
} }
// Reinforce blockers blocking attackers with trample if life is // Reinforce blockers blocking attackers with trample if life is still in danger
// still in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat); reinforceBlockersAgainstTrample(combat);
} else { } else {
@@ -1077,11 +1054,9 @@ public class AiBlockController {
reinforceBlockersToKill(combat); reinforceBlockersToKill(combat);
} }
// == 3. If the AI life would be in serious danger make an even // == 3. If the AI life would be in serious danger make an even safer approach ==
// safer approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); // reset every block clearBlockers(combat, possibleBlockers); // reset every block assignment
// assignment
makeChumpBlocks(combat); // choose chump blocks makeChumpBlocks(combat); // choose chump blocks
if (ComputerUtilCombat.lifeInDanger(ai, combat)) { if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeTradeBlocks(combat); // choose necessary trade makeTradeBlocks(combat); // choose necessary trade
@@ -1090,15 +1065,13 @@ public class AiBlockController {
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeGoodBlocks(combat); makeGoodBlocks(combat);
} }
// Reinforce blockers blocking attackers with trample if life is // Reinforce blockers blocking attackers with trample if life is still in danger
// still in danger
else { else {
reinforceBlockersAgainstTrample(combat); reinforceBlockersAgainstTrample(combat);
} }
makeGangBlocks(combat); makeGangBlocks(combat);
// Support blockers not destroying the attacker with more // Support blockers not destroying the attacker with more
// blockers // blockers to try to kill the attacker
// to try to kill the attacker
reinforceBlockersToKill(combat); reinforceBlockersToKill(combat);
} }
} }
@@ -1108,7 +1081,7 @@ public class AiBlockController {
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able.")); chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
// if an attacker with lure attacks - all that can block // if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) { for (final Card blocker : blockersLeft) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat)) { if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
chumpBlockers.add(blocker); chumpBlockers.add(blocker);
} }
} }
@@ -1118,7 +1091,7 @@ public class AiBlockController {
blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false); blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false);
for (final Card blocker : blockers) { for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker) if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
&& (CombatUtil.mustBlockAnAttacker(blocker, combat) && (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
|| blocker.hasKeyword("CARDNAME blocks each turn if able.") || blocker.hasKeyword("CARDNAME blocks each turn if able.")
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) { || blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
@@ -1137,7 +1110,6 @@ public class AiBlockController {
} }
} }
// check to see if it's possible to defend a Planeswalker under attack with a chump block, // check to see if it's possible to defend a Planeswalker under attack with a chump block,
// unless life is low enough to be more worried about saving preserving the life total // unless life is low enough to be more worried about saving preserving the life total
if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) { if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) {

View File

@@ -34,6 +34,7 @@ import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.ExploreAi; import forge.ai.ability.ExploreAi;
import forge.ai.ability.LearnAi; import forge.ai.ability.LearnAi;
import forge.ai.simulation.SpellAbilityPicker; import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.deck.CardPool; import forge.deck.CardPool;
@@ -46,6 +47,7 @@ import forge.game.Game;
import forge.game.GameActionUtil; import forge.game.GameActionUtil;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GlobalRuleChange; import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased; import forge.game.ability.SpellApiBased;
@@ -78,6 +80,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplaceMoved; import forge.game.replacement.ReplaceMoved;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility; import forge.game.spellability.LandAbility;
@@ -307,6 +310,8 @@ public class AiController {
} }
exSA.setTrigger(tr); exSA.setTrigger(tr);
// need to set TriggeredObject
exSA.setTriggeringObject(AbilityKey.Card, card);
// for trigger test, need to ignore the conditions // for trigger test, need to ignore the conditions
SpellAbilityCondition cons = exSA.getConditions(); SpellAbilityCondition cons = exSA.getConditions();
@@ -432,17 +437,22 @@ public class AiController {
} }
} }
// don't play the land if it has cycling and enough lands are available
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand); final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
CardCollection lands = new CardCollection(battlefield); CardCollection lands = new CardCollection(battlefield);
lands.addAll(hand); lands.addAll(hand);
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS); lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc); int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
// don't play MDFC land if other side is spell and enough lands are available
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
return false;
}
// don't play the land if it has cycling and enough lands are available
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
for (final SpellAbility sa : spellAbilities) { for (final SpellAbility sa : spellAbilities) {
if (sa.isCycling()) { if (sa.isCycling()) {
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
return false; return false;
} }
} }
@@ -503,20 +513,35 @@ public class AiController {
//try to skip lands that enter the battlefield tapped //try to skip lands that enter the battlefield tapped
if (!nonLandsInHand.isEmpty()) { if (!nonLandsInHand.isEmpty()) {
CardCollection nonTappeddLands = new CardCollection(); CardCollection nonTappedLands = new CardCollection();
for (Card land : landList) { for (Card land : landList) {
// Is this the best way to check if a land ETB Tapped? // check replacement effects if land would enter tapped or not
if (land.hasSVar("ETBTappedSVar")) { final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(land);
repParams.put(AbilityKey.Origin, land.getZone().getZoneType());
repParams.put(AbilityKey.Destination, ZoneType.Battlefield);
repParams.put(AbilityKey.Source, land);
boolean foundTapped = false;
for (ReplacementEffect re : player.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.Other)) {
SpellAbility reSA = re.ensureAbility();
if (reSA == null || !ApiType.Tap.equals(reSA.getApi())) {
continue; continue;
} }
// Glacial Fortress and friends reSA.setActivatingPlayer(reSA.getHostCard().getController());
if (land.hasSVar("ETBCheckSVar") && CardFactoryUtil.xCount(land, land.getSVar("ETBCheckSVar")) == 0) { if (reSA.metConditions()) {
foundTapped = true;
break;
}
}
if (foundTapped) {
continue; continue;
} }
nonTappeddLands.add(land);
nonTappedLands.add(land);
} }
if (!nonTappeddLands.isEmpty()) { if (!nonTappedLands.isEmpty()) {
landList = nonTappeddLands; landList = nonTappedLands;
} }
} }
@@ -588,7 +613,6 @@ public class AiController {
sa.setActivatingPlayer(player); sa.setActivatingPlayer(player);
// check everything necessary // check everything necessary
AiPlayDecision opinion = canPlayAndPayFor(currentSA); AiPlayDecision opinion = canPlayAndPayFor(currentSA);
//PhaseHandler ph = game.getPhaseHandler(); //PhaseHandler ph = game.getPhaseHandler();
// System.out.printf("Ai thinks '%s' of %s @ %s %s >>> \n", opinion, sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase()); // System.out.printf("Ai thinks '%s' of %s @ %s %s >>> \n", opinion, sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase());
@@ -1301,15 +1325,6 @@ public class AiController {
} }
public boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message) { public boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message) {
if (logic.equalsIgnoreCase("ProtectFriendly")) {
final Player controller = hostCard.getController();
if (affected instanceof Player) {
return !((Player) affected).isOpponentOf(controller);
}
if (affected instanceof Card) {
return !((Card) affected).getController().isOpponentOf(controller);
}
}
return true; return true;
} }
@@ -1408,8 +1423,6 @@ public class AiController {
private List<SpellAbility> singleSpellAbilityList(SpellAbility sa) { private List<SpellAbility> singleSpellAbilityList(SpellAbility sa) {
if (sa == null) { return null; } if (sa == null) { return null; }
// System.out.println("Chosen to play: " + sa);
final List<SpellAbility> abilities = Lists.newArrayList(); final List<SpellAbility> abilities = Lists.newArrayList();
abilities.add(sa); abilities.add(sa);
return abilities; return abilities;
@@ -1454,6 +1467,13 @@ public class AiController {
if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) { if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) {
final List<SpellAbility> abilities = Lists.newArrayList(); final List<SpellAbility> abilities = Lists.newArrayList();
// TODO extend this logic to evaluate MDFC with both sides land
// this can only happen if its a MDFC land
if (!land.isLand()) {
land.setState(CardStateName.Modal, true);
land.setBackSide(true);
}
LandAbility la = new LandAbility(land, player, null); LandAbility la = new LandAbility(land, player, null);
la.setCardState(land.getCurrentState()); la.setCardState(land.getCurrentState());
if (la.canPlay()) { if (la.canPlay()) {
@@ -1704,10 +1724,8 @@ public class AiController {
for (int i = 0; i < numToExile; i++) { for (int i = 0; i < numToExile; i++) {
Card chosen = null; Card chosen = null;
for (final Card c : grave) { // Exile noncreatures first in for (final Card c : grave) { // Exile noncreatures first in
// case we can revive. Might // case we can revive. Might wanna do some additional
// wanna do some additional // checking here for Flashback and the like.
// checking here for Flashback
// and the like.
if (!c.isCreature()) { if (!c.isCreature()) {
chosen = c; chosen = c;
break; break;
@@ -1746,12 +1764,21 @@ public class AiController {
* @param sa the sa * @param sa the sa
* @return true, if successful * @return true, if successful
*/ */
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa) { public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
Card hostCard = effect.getHostCard(); Card hostCard = effect.getHostCard();
if (hostCard.hasAlternateState()) { if (hostCard.hasAlternateState()) {
hostCard = game.getCardState(hostCard); hostCard = game.getCardState(hostCard);
} }
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
final Player controller = hostCard.getController();
if (affected instanceof Player) {
return !((Player) affected).isOpponentOf(controller);
}
if (affected instanceof Card) {
return !((Card) affected).getController().isOpponentOf(controller);
}
}
if (effect.hasParam("AICheckSVar")) { if (effect.hasParam("AICheckSVar")) {
System.out.println("aiShouldRun?" + sa); System.out.println("aiShouldRun?" + sa);
final String svarToCheck = effect.getParam("AICheckSVar"); final String svarToCheck = effect.getParam("AICheckSVar");
@@ -1766,7 +1793,7 @@ public class AiController {
compareTo = Integer.parseInt(strCmpTo); compareTo = Integer.parseInt(strCmpTo);
} catch (final Exception ignored) { } catch (final Exception ignored) {
if (sa == null) { if (sa == null) {
compareTo = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(strCmpTo)); compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
} else { } else {
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa); compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
} }
@@ -1776,7 +1803,7 @@ public class AiController {
int left = 0; int left = 0;
if (sa == null) { if (sa == null) {
left = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(svarToCheck)); left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
} else { } else {
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa); left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
} }
@@ -1866,11 +1893,13 @@ public class AiController {
} else if ("LowestLoseLife".equals(logic)) { } else if ("LowestLoseLife".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1; return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
} else if ("HighestLoseLife".equals(logic)) { } else if ("HighestLoseLife".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1; return Math.min(player.getLife() -1,MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1);
} else if ("HighestGetCounter".equals(logic)) { } else if ("HighestGetCounter".equals(logic)) {
return MyRandom.getRandom().nextInt(3); return MyRandom.getRandom().nextInt(3);
} else if (source.hasSVar("EnergyToPay")) { } else if (source.hasSVar("EnergyToPay")) {
return AbilityUtils.calculateAmount(source, source.getSVar("EnergyToPay"), sa); return AbilityUtils.calculateAmount(source, source.getSVar("EnergyToPay"), sa);
} else if ("Vermin".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.max(player.getLife() - 5, 0));
} }
return max; return max;
} }
@@ -1965,7 +1994,6 @@ public class AiController {
return result; return result;
} }
// this is where the computer cheats // this is where the computer cheats
// changes AllZone.getComputerPlayer().getZone(Zone.Library) // changes AllZone.getComputerPlayer().getZone(Zone.Library)
@@ -2245,10 +2273,8 @@ public class AiController {
} }
} }
// AI logic for choosing which replacement effect to apply // AI logic for choosing which replacement effect to apply happens here.
// happens here.
return Iterables.getFirst(list, null); return Iterables.getFirst(list, null);
} }
} }

View File

@@ -280,7 +280,7 @@ public class ComputerUtil {
SpellAbility newSA = sa.copyWithNoManaCost(); SpellAbility newSA = sa.copyWithNoManaCost();
newSA.setActivatingPlayer(ai); newSA.setActivatingPlayer(ai);
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA)) { if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0)) {
return false; return false;
} }
@@ -1125,7 +1125,7 @@ public class ComputerUtil {
creatures2.add(creatures.get(i)); creatures2.add(creatures.get(i));
} }
} }
if (((creatures2.size() + CardUtil.getThisTurnCast("Creature.YouCtrl", vengevines.get(0)).size()) > 1) if (((creatures2.size() + CardUtil.getThisTurnCast("Creature.YouCtrl", vengevines.get(0), null).size()) > 1)
&& card.isCreature() && card.getManaCost().getCMC() <= 3) { && card.isCreature() && card.getManaCost().getCMC() <= 3) {
return true; return true;
} }
@@ -2253,10 +2253,8 @@ public class ComputerUtil {
String chosen = ""; String chosen = "";
if (kindOfType.equals("Card")) { if (kindOfType.equals("Card")) {
// TODO // TODO
// computer will need to choose a type // computer will need to choose a type based on whether it needs a creature or land,
// based on whether it needs a creature or land, // otherwise, lib search for most common type left then, reveal chosenType to Human
// otherwise, lib search for most common type left
// then, reveal chosenType to Human
if (game.getPhaseHandler().is(PhaseType.UNTAP) && logic == null) { // Storage Matrix if (game.getPhaseHandler().is(PhaseType.UNTAP) && logic == null) { // Storage Matrix
double amount = 0; double amount = 0;
for (String type : CardType.getAllCardTypes()) { for (String type : CardType.getAllCardTypes()) {
@@ -2434,8 +2432,7 @@ public class ComputerUtil {
if (!source.canReceiveCounters(p1p1Type)) { if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Feather" : "Quill"; return opponent ? "Feather" : "Quill";
} }
// if source is not on the battlefield anymore, choose +1/+1 // if source is not on the battlefield anymore, choose +1/+1 ones
// ones
if (!game.getCardState(source).isInZone(ZoneType.Battlefield)) { if (!game.getCardState(source).isInZone(ZoneType.Battlefield)) {
return opponent ? "Feather" : "Quill"; return opponent ? "Feather" : "Quill";
} }
@@ -2477,8 +2474,7 @@ public class ComputerUtil {
return opponent ? "Numbers" : "Strength"; return opponent ? "Numbers" : "Strength";
} }
// TODO check for ETB to +1/+1 counters // TODO check for ETB to +1/+1 counters or over another trigger like lifegain
// or over another trigger like lifegain
int tokenScore = ComputerUtilCard.evaluateCreature(token); int tokenScore = ComputerUtilCard.evaluateCreature(token);
@@ -2556,8 +2552,7 @@ public class ComputerUtil {
return "Taxes"; return "Taxes";
} else { } else {
// ai is first voter or ally of controller // ai is first voter or ally of controller
// both are not affected, but if opponents controll creatures, // both are not affected, but if opponents control creatures, sacrifice is worse
// sacrifice is worse
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death"; return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
} }
default: default:
@@ -2854,7 +2849,6 @@ public class ComputerUtil {
} }
public static boolean lifegainNegative(final Player player, final Card source, final int n) { public static boolean lifegainNegative(final Player player, final Card source, final int n) {
if (!player.canGainLife()) { if (!player.canGainLife()) {
return false; return false;
} }

View File

@@ -76,7 +76,7 @@ public class ComputerUtilCard {
} }
}); });
} }
return ComputerUtilCard.getMostExpensivePermanentAI(all); return getMostExpensivePermanentAI(all);
} }
/** /**
@@ -265,16 +265,14 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object. * @return a {@link forge.game.card.Card} object.
*/ */
public static Card getBestAI(final Iterable<Card> list) { public static Card getBestAI(final Iterable<Card> list) {
// Get Best will filter by appropriate getBest list if ALL of the list // Get Best will filter by appropriate getBest list if ALL of the list is of that type
// is of that type
if (Iterables.all(list, CardPredicates.Presets.CREATURES)) { if (Iterables.all(list, CardPredicates.Presets.CREATURES)) {
return ComputerUtilCard.getBestCreatureAI(list); return ComputerUtilCard.getBestCreatureAI(list);
} }
if (Iterables.all(list, CardPredicates.Presets.LANDS)) { if (Iterables.all(list, CardPredicates.Presets.LANDS)) {
return getBestLandAI(list); return getBestLandAI(list);
} }
// TODO - Once we get an EvaluatePermanent this should call // TODO - Once we get an EvaluatePermanent this should call getBestPermanent()
// getBestPermanent()
return ComputerUtilCard.getMostExpensivePermanentAI(list); return ComputerUtilCard.getMostExpensivePermanentAI(list);
} }
@@ -383,7 +381,7 @@ public class ComputerUtilCard {
} }
if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) { if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) {
return ComputerUtilCard.getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS)); return getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
} }
final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES); final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES);
@@ -393,7 +391,7 @@ public class ComputerUtilCard {
List<Card> lands = CardLists.filter(list, CardPredicates.Presets.LANDS); List<Card> lands = CardLists.filter(list, CardPredicates.Presets.LANDS);
if (lands.size() > 6) { if (lands.size() > 6) {
return ComputerUtilCard.getWorstLand(lands); return getWorstLand(lands);
} }
if (hasEnchantmants || hasArtifacts) { if (hasEnchantmants || hasArtifacts) {
@@ -410,8 +408,7 @@ public class ComputerUtilCard {
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES)); return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
} }
// Planeswalkers fall through to here, lands will fall through if there // Planeswalkers fall through to here, lands will fall through if there aren't very many
// aren't very many
return getCheapestPermanentAI(list, null, false); return getCheapestPermanentAI(list, null, false);
} }
@@ -444,7 +441,7 @@ public class ComputerUtilCard {
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() { public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
@Override @Override
public int compare(final Card a, final Card b) { public int compare(final Card a, final Card b) {
return ComputerUtilCard.evaluateCreature(b) - ComputerUtilCard.evaluateCreature(a); return evaluateCreature(b) - evaluateCreature(a);
} }
}; };
@@ -882,9 +879,9 @@ public class ComputerUtilCard {
public static final Predicate<Deck> AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate<Deck>() { public static final Predicate<Deck> AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate<Deck>() {
@Override @Override
public boolean apply(Deck d) { public boolean apply(Deck d) {
for(Entry<DeckSection, CardPool> cp: d) { for (Entry<DeckSection, CardPool> cp: d) {
for(Entry<PaperCard, Integer> e : cp.getValue()) { for (Entry<PaperCard, Integer> e : cp.getValue()) {
if ( e.getKey().getRules().getAiHints().getRemAIDecks() ) if (e.getKey().getRules().getAiHints().getRemAIDecks())
return false; return false;
} }
} }
@@ -982,7 +979,7 @@ public class ComputerUtilCard {
for(byte c : MagicColor.WUBRG) { for(byte c : MagicColor.WUBRG) {
String devotionCode = "Count$Devotion." + MagicColor.toLongString(c); String devotionCode = "Count$Devotion." + MagicColor.toLongString(c);
int devotion = CardFactoryUtil.xCount(sa.getHostCard(), devotionCode); int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa);
if (devotion > curDevotion && !CardLists.filter(hand, CardPredicates.isColor(c)).isEmpty()) { if (devotion > curDevotion && !CardLists.filter(hand, CardPredicates.isColor(c)).isEmpty()) {
curDevotion = devotion; curDevotion = devotion;
chosenColor = MagicColor.toLongString(c); chosenColor = MagicColor.toLongString(c);
@@ -1644,11 +1641,12 @@ public class ComputerUtilCard {
copiedKeywords.insertAll(pumped.getKeywords()); copiedKeywords.insertAll(pumped.getKeywords());
List<KeywordInterface> toCopy = Lists.newArrayList(); List<KeywordInterface> toCopy = Lists.newArrayList();
for (KeywordInterface k : c.getKeywords()) { for (KeywordInterface k : c.getKeywords()) {
if (!copiedKeywords.contains(k.getOriginal())) { KeywordInterface copiedKI = k.copy(c, true);
if (k.getHidden()) { if (!copiedKeywords.contains(copiedKI.getOriginal())) {
pumped.addHiddenExtrinsicKeyword(k); if (copiedKI.getHidden()) {
pumped.addHiddenExtrinsicKeyword(copiedKI);
} else { } else {
toCopy.add(k); toCopy.add(copiedKI);
} }
} }
} }
@@ -1678,37 +1676,27 @@ public class ComputerUtilCard {
// remove old boost that might be copied // remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) { for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getTimestamp(), stAb.getId()); vCard.removePTBoost(c.getTimestamp(), stAb.getId());
final Map<String, String> params = stAb.getMapParams(); if (!stAb.getParam("Mode").equals("Continuous")) {
if (!params.get("Mode").equals("Continuous")) {
continue; continue;
} }
if (!params.containsKey("Affected")) { if (!stAb.hasParam("Affected")) {
continue; continue;
} }
if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) { if (!stAb.hasParam("AddPower") && !stAb.hasParam("AddToughness")) {
continue; continue;
} }
final String valid = params.get("Affected"); if (!vCard.isValid(stAb.getParam("Affected").split(","), c.getController(), c, stAb)) {
if (!vCard.isValid(valid, c.getController(), c, null)) {
continue; continue;
} }
int att = 0; int att = 0;
if (params.containsKey("AddPower")) { if (stAb.hasParam("AddPower")) {
String addP = params.get("AddPower"); String addP = stAb.getParam("AddPower");
if (addP.equals("AffectedX")) { att = AbilityUtils.calculateAmount(addP.startsWith("Affected") ? vCard : c, addP, stAb, true);
att = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addP));
} else {
att = AbilityUtils.calculateAmount(c, addP, stAb);
}
} }
int def = 0; int def = 0;
if (params.containsKey("AddToughness")) { if (stAb.hasParam("AddToughness")) {
String addT = params.get("AddToughness"); String addT = stAb.getParam("AddToughness");
if (addT.equals("AffectedY")) { def = AbilityUtils.calculateAmount(addT.startsWith("Affected") ? vCard : c, addT, stAb, true);
def = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addT));
} else {
def = AbilityUtils.calculateAmount(c, addT, stAb);
}
} }
vCard.addPTBoost(att, def, c.getTimestamp(), stAb.getId()); vCard.addPTBoost(att, def, c.getTimestamp(), stAb.getId());
} }

View File

@@ -33,7 +33,6 @@ import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CardUtil; import forge.game.card.CardUtil;
@@ -335,7 +334,7 @@ public class ComputerUtilCombat {
*/ */
public static int resultingPoison(final Player ai, final Combat combat) { public static int resultingPoison(final Player ai, final Combat combat) {
// ai can't get poision counters, so the value can't change // ai can't get poison counters, so the value can't change
if (!ai.canReceiveCounters(CounterEnumType.POISON)) { if (!ai.canReceiveCounters(CounterEnumType.POISON)) {
return ai.getPoisonCounters(); return ai.getPoisonCounters();
} }
@@ -413,7 +412,6 @@ public class ComputerUtilCombat {
return false; return false;
} }
// check for creatures that must be blocked // check for creatures that must be blocked
final List<Card> attackers = combat.getAttackersOf(ai); final List<Card> attackers = combat.getAttackersOf(ai);
@@ -493,8 +491,7 @@ public class ComputerUtilCombat {
} }
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, final int payment) { public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, final int payment) {
// life in danger only cares about the player's life. Not about a // life in danger only cares about the player's life. Not about a Planeswalkers life
// Planeswalkers life
if (ai.cantLose() || combat == null) { if (ai.cantLose() || combat == null) {
return false; return false;
} }
@@ -569,8 +566,7 @@ public class ComputerUtilCombat {
return damage; return damage;
} }
// This calculates the amount of damage a blocker in a blockgang can deal to // This calculates the amount of damage a blocker in a blockgang can deal to the attacker
// the attacker
/** /**
* <p> * <p>
* dealsDamageAsBlocker. * dealsDamageAsBlocker.
@@ -583,7 +579,6 @@ public class ComputerUtilCombat {
* @return a int. * @return a int.
*/ */
public static int dealsDamageAsBlocker(final Card attacker, final Card defender) { public static int dealsDamageAsBlocker(final Card attacker, final Card defender) {
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender); int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
@@ -648,7 +643,6 @@ public class ComputerUtilCombat {
* @return a int. * @return a int.
*/ */
public static int totalShieldDamage(final Card attacker, final List<Card> defenders) { public static int totalShieldDamage(final Card attacker, final List<Card> defenders) {
int defenderDefense = 0; int defenderDefense = 0;
for (final Card defender : defenders) { for (final Card defender : defenders) {
@@ -672,7 +666,6 @@ public class ComputerUtilCombat {
* @return a int. * @return a int.
*/ */
public static int shieldDamage(final Card attacker, final Card blocker) { public static int shieldDamage(final Card attacker, final Card blocker) {
if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) { if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) {
return 0; return 0;
} }
@@ -777,7 +770,6 @@ public class ComputerUtilCombat {
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger, public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
Combat combat, final List<Card> plannedAttackers) { Combat combat, final List<Card> plannedAttackers) {
final Game game = attacker.getGame(); final Game game = attacker.getGame();
final Map<String, String> trigParams = trigger.getMapParams();
boolean willTrigger = false; boolean willTrigger = false;
final Card source = trigger.getHostCard(); final Card source = trigger.getHostCard();
if (combat == null) { if (combat == null) {
@@ -800,29 +792,27 @@ public class ComputerUtilCombat {
if (combat.isAttacking(attacker)) { if (combat.isAttacking(attacker)) {
return false; // The trigger should have triggered already return false; // The trigger should have triggered already
} }
if (trigParams.containsKey("ValidCard")) { if (trigger.hasParam("ValidCard")) {
if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(",")) if (!trigger.matchesValidParam("ValidCard", attacker)
&& !(combat.isAttacking(source) && trigger.matchesValid(source, && !(combat.isAttacking(source) && trigger.matchesValidParam("ValidCard", source)
trigParams.get("ValidCard").split(",")) && !trigger.hasParam("Alone"))) {
&& !trigParams.containsKey("Alone"))) {
return false; return false;
} }
} }
if (trigParams.containsKey("Attacked")) { if (trigger.hasParam("Attacked")) {
if (combat.isAttacking(attacker)) { if (combat.isAttacking(attacker)) {
GameEntity attacked = combat.getDefenderByAttacker(attacker); if (!trigger.matchesValidParam("Attacked", combat.getDefenderByAttacker(attacker))) {
if (!trigger.matchesValid(attacked, trigParams.get("Attacked").split(","))) {
return false; return false;
} }
} else { } else {
if ("You,Planeswalker.YouCtrl".equals(trigParams.get("Attacked"))) { if ("You,Planeswalker.YouCtrl".equals(trigger.getParam("Attacked"))) {
if (source.getController() == attacker.getController()) { if (source.getController() == attacker.getController()) {
return false; return false;
} }
} }
} }
} }
if (trigParams.containsKey("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) { if (trigger.hasParam("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) {
return false; // won't trigger since the AI is planning to attack with more than one creature return false; // won't trigger since the AI is planning to attack with more than one creature
} }
} }
@@ -830,12 +820,10 @@ public class ComputerUtilCombat {
// defender == null means unblocked // defender == null means unblocked
if ((defender == null) && mode == TriggerType.AttackerUnblocked) { if ((defender == null) && mode == TriggerType.AttackerUnblocked) {
willTrigger = true; willTrigger = true;
if (trigParams.containsKey("ValidCard")) { if (!trigger.matchesValidParam("ValidCard", attacker)) {
if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
return false; return false;
} }
} }
}
if (defender == null) { if (defender == null) {
return willTrigger; return willTrigger;
@@ -843,8 +831,8 @@ public class ComputerUtilCombat {
if (mode == TriggerType.Blocks) { if (mode == TriggerType.Blocks) {
willTrigger = true; willTrigger = true;
if (trigParams.containsKey("ValidBlocked")) { if (trigger.hasParam("ValidBlocked")) {
String validBlocked = trigParams.get("ValidBlocked"); String validBlocked = trigger.getParam("ValidBlocked");
if (validBlocked.contains(".withLesserPower")) { if (validBlocked.contains(".withLesserPower")) {
// Have to check this restriction here as triggering objects aren't set yet, so // Have to check this restriction here as triggering objects aren't set yet, so
// ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE // ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE
@@ -857,8 +845,8 @@ public class ComputerUtilCombat {
return false; return false;
} }
} }
if (trigParams.containsKey("ValidCard")) { if (trigger.hasParam("ValidCard")) {
String validBlocker = trigParams.get("ValidCard"); String validBlocker = trigger.getParam("ValidCard");
if (validBlocker.contains(".withLesserPower")) { if (validBlocker.contains(".withLesserPower")) {
// Have to check this restriction here as triggering objects aren't set yet, so // Have to check this restriction here as triggering objects aren't set yet, so
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE // ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
@@ -873,29 +861,23 @@ public class ComputerUtilCombat {
} }
} else if (mode == TriggerType.AttackerBlocked || mode == TriggerType.AttackerBlockedByCreature) { } else if (mode == TriggerType.AttackerBlocked || mode == TriggerType.AttackerBlockedByCreature) {
willTrigger = true; willTrigger = true;
if (trigParams.containsKey("ValidBlocker")) { if (!trigger.matchesValidParam("ValidBlocker", defender)) {
if (!trigger.matchesValid(defender, trigParams.get("ValidBlocker").split(","))) {
return false; return false;
} }
} if (!trigger.matchesValidParam("ValidCard", attacker)) {
if (trigParams.containsKey("ValidCard")) {
if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
return false; return false;
} }
}
} else if (mode == TriggerType.DamageDone) { } else if (mode == TriggerType.DamageDone) {
willTrigger = true; willTrigger = true;
if (trigParams.containsKey("ValidSource")) { if (trigger.hasParam("ValidSource")) {
if (!(trigger.matchesValid(defender, trigParams.get("ValidSource").split(",")) if (!(trigger.matchesValidParam("ValidSource", defender)
&& defender.getNetCombatDamage() > 0 && defender.getNetCombatDamage() > 0
&& (!trigParams.containsKey("ValidTarget") && trigger.matchesValidParam("ValidTarget", attacker))) {
|| trigger.matchesValid(attacker, trigParams.get("ValidTarget").split(","))))) {
return false; return false;
} }
if (!(trigger.matchesValid(attacker, trigParams.get("ValidSource").split(",")) if (!(trigger.matchesValidParam("ValidSource", attacker)
&& attacker.getNetCombatDamage() > 0 && attacker.getNetCombatDamage() > 0
&& (!trigParams.containsKey("ValidTarget") && trigger.matchesValidParam("ValidTarget", defender))) {
|| trigger.matchesValid(defender, trigParams.get("ValidTarget").split(","))))) {
return false; return false;
} }
} }
@@ -944,30 +926,22 @@ public class ComputerUtilCombat {
} }
final Game game = attacker.getGame(); final Game game = attacker.getGame();
// look out for continuous static abilities that only care for blocking // look out for continuous static abilities that only care for blocking creatures
// creatures
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command)); final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) { for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) { for (final StaticAbility stAb : card.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams(); if (!stAb.getParam("Mode").equals("Continuous")) {
if (!params.get("Mode").equals("Continuous")) {
continue; continue;
} }
if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) { if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
continue; continue;
} }
final String valid = TextUtil.fastReplace(params.get("Affected"), "blocking", "Creature"); final String valid = TextUtil.fastReplace(stAb.getParam("Affected"), "blocking", "Creature");
if (!blocker.isValid(valid, card.getController(), card, null)) { if (!blocker.isValid(valid, card.getController(), card, stAb)) {
continue; continue;
} }
if (params.containsKey("AddPower")) { if (stAb.hasParam("AddPower")) {
if (params.get("AddPower").equals("X")) { power += AbilityUtils.calculateAmount(card, stAb.getParam("AddPower"), stAb);
power += CardFactoryUtil.xCount(card, card.getSVar("X"));
} else if (params.get("AddPower").equals("Y")) {
power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
} else {
power += Integer.valueOf(params.get("AddPower"));
}
} }
} }
} }
@@ -1252,31 +1226,23 @@ public class ComputerUtilCombat {
theTriggers.addAll(blocker.getTriggers()); theTriggers.addAll(blocker.getTriggers());
} }
// look out for continuous static abilities that only care for attacking // look out for continuous static abilities that only care for attacking creatures
// creatures
if (!withoutCombatStaticAbilities) { if (!withoutCombatStaticAbilities) {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command)); final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) { for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) { for (final StaticAbility stAb : card.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams(); if (!stAb.getParam("Mode").equals("Continuous")) {
if (!params.get("Mode").equals("Continuous")) {
continue; continue;
} }
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) { if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
continue; continue;
} }
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature"); final String valid = TextUtil.fastReplace(stAb.getParam("Affected"), "attacking", "Creature");
if (!attacker.isValid(valid, card.getController(), card, null)) { if (!attacker.isValid(valid, card.getController(), card, stAb)) {
continue; continue;
} }
if (params.containsKey("AddPower")) { if (stAb.hasParam("AddPower")) {
if (params.get("AddPower").equals("X")) { power += AbilityUtils.calculateAmount(card, stAb.getParam("AddPower"), stAb);
power += CardFactoryUtil.xCount(card, card.getSVar("X"));
} else if (params.get("AddPower").equals("Y")) {
power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
} else {
power += Integer.valueOf(params.get("AddPower"));
}
} }
} }
} }
@@ -1350,7 +1316,7 @@ public class ComputerUtilCombat {
} else if (bonus.contains("TriggeredAttacker$CardToughness")) { } else if (bonus.contains("TriggeredAttacker$CardToughness")) {
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness()))); bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness())));
} }
power += CardFactoryUtil.xCount(source, bonus); power += AbilityUtils.calculateAmount(source, bonus, sa);
} }
} }
@@ -1447,8 +1413,7 @@ public class ComputerUtilCombat {
theTriggers.addAll(blocker.getTriggers()); theTriggers.addAll(blocker.getTriggers());
} }
// look out for continuous static abilities that only care for attacking // look out for continuous static abilities that only care for attacking creatures
// creatures
if (!withoutCombatStaticAbilities) { if (!withoutCombatStaticAbilities) {
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield); final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
for (final Card card : cardList) { for (final Card card : cardList) {
@@ -1540,7 +1505,7 @@ public class ComputerUtilCombat {
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
} }
toughness += CardFactoryUtil.xCount(source, bonus); toughness += AbilityUtils.calculateAmount(source, bonus, sa);
} }
} else if (ApiType.PumpAll.equals(sa.getApi())) { } else if (ApiType.PumpAll.equals(sa.getApi())) {
@@ -1573,7 +1538,7 @@ public class ComputerUtilCombat {
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1"); bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
} }
toughness += CardFactoryUtil.xCount(source, bonus); toughness += AbilityUtils.calculateAmount(source, bonus, sa);
} }
} }
} }
@@ -2256,11 +2221,12 @@ public class ComputerUtilCombat {
* @return a int. * @return a int.
*/ */
public final static int getDamageToKill(final Card c) { public final static int getDamageToKill(final Card c) {
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields(); int damageShield = c.getPreventNextDamageTotalShields();
int killDamage = c.getLethalDamage() + damageShield;
if ((killDamage > c.getPreventNextDamageTotalShields()) if ((killDamage > damageShield)
&& c.hasSVar("DestroyWhenDamaged")) { && c.hasSVar("DestroyWhenDamaged")) {
killDamage = 1 + c.getPreventNextDamageTotalShields(); killDamage = 1 + damageShield;
} }
return killDamage; return killDamage;
@@ -2282,7 +2248,6 @@ public class ComputerUtilCombat {
*/ */
public final static int predictDamageTo(final Player target, final int damage, final Card source, final boolean isCombat) { public final static int predictDamageTo(final Player target, final int damage, final Card source, final boolean isCombat) {
final Game game = target.getGame(); final Game game = target.getGame();
int restDamage = damage; int restDamage = damage;
@@ -2291,39 +2256,38 @@ public class ComputerUtilCombat {
// Predict replacement effects // Predict replacement effects
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final ReplacementEffect re : ca.getReplacementEffects()) { for (final ReplacementEffect re : ca.getReplacementEffects()) {
Map<String, String> params = re.getMapParams(); if (!re.getMode().equals(ReplacementType.DamageDone) ||
if (!re.getMode().equals(ReplacementType.DamageDone) || !params.containsKey("PreventionEffect")) { (!re.hasParam("PreventionEffect") && !re.hasParam("Prevent"))) {
continue; continue;
} }
// Immortal Coil prevents the damage but has a similar negative effect // Immortal Coil prevents the damage but has a similar negative effect
if ("Immortal Coil".equals(ca.getName())) { if ("Immortal Coil".equals(ca.getName())) {
continue; continue;
} }
if (params.containsKey("ValidSource") if (!re.matchesValidParam("ValidSource", source)) {
&& !source.isValid(params.get("ValidSource"), ca.getController(), ca, null)) {
continue; continue;
} }
if (params.containsKey("ValidTarget") if (!re.matchesValidParam("ValidTarget", source)) {
&& !target.isValid(params.get("ValidTarget"), ca.getController(), ca, null)) {
continue; continue;
} }
if (params.containsKey("IsCombat")) { if (re.hasParam("IsCombat")) {
if (params.get("IsCombat").equals("True")) { if (re.getParam("IsCombat").equals("True") != isCombat) {
if (!isCombat) {
continue;
}
} else {
if (isCombat) {
continue; continue;
} }
} }
if (re.hasParam("Prevent")) {
return 0;
} else if (re.getOverridingAbility() != null) {
SpellAbility repSA = re.getOverridingAbility();
if (repSA.getApi() == ApiType.ReplaceDamage) {
return Math.max(0, restDamage - AbilityUtils.calculateAmount(ca, repSA.getParam("Amount"), repSA));
}
} }
return 0; return 0;
} }
} }
restDamage = target.staticDamagePrevention(restDamage, source, isCombat, true); restDamage = target.staticDamagePrevention(restDamage, 0, source, isCombat);
return restDamage; return restDamage;
} }
@@ -2344,13 +2308,7 @@ public class ComputerUtilCombat {
// This function helps the AI calculate the actual amount of damage an // This function helps the AI calculate the actual amount of damage an
// effect would deal // effect would deal
public final static int predictDamageTo(final Card target, final int damage, final Card source, final boolean isCombat) { public final static int predictDamageTo(final Card target, final int damage, final Card source, final boolean isCombat) {
return predictDamageTo(target, damage, 0, source, isCombat);
int restDamage = damage;
restDamage = target.staticReplaceDamage(restDamage, source, isCombat);
restDamage = target.staticDamagePrevention(restDamage, source, isCombat, true);
return restDamage;
} }
@@ -2372,7 +2330,6 @@ public class ComputerUtilCombat {
* @return a int. * @return a int.
*/ */
public final static int predictDamageTo(final Card target, final int damage, final int possiblePrevention, final Card source, final boolean isCombat) { public final static int predictDamageTo(final Card target, final int damage, final int possiblePrevention, final Card source, final boolean isCombat) {
int restDamage = damage; int restDamage = damage;
restDamage = target.staticReplaceDamage(restDamage, source, isCombat); restDamage = target.staticReplaceDamage(restDamage, source, isCombat);
@@ -2382,7 +2339,6 @@ public class ComputerUtilCombat {
} }
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) { public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) { if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) {
return true; return true;
} }
@@ -2490,7 +2446,14 @@ public class ComputerUtilCombat {
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList( List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(
ReplacementType.DamageDone, repParams, ReplacementLayer.Other); ReplacementType.DamageDone, repParams, ReplacementLayer.Other);
return !list.isEmpty(); for (final ReplacementEffect re : list) {
Map<String, String> params = re.getMapParams();
if (params.containsKey("Prevent") ||
(re.getOverridingAbility() != null && re.getOverridingAbility().getApi() != ApiType.ReplaceDamage && re.getOverridingAbility().getApi() != ApiType.ReplaceEffect)) {
return true;
}
}
return false;
} }
public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) { public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) {
@@ -2499,20 +2462,6 @@ public class ComputerUtilCombat {
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife(); return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
} }
public static int getMaxAttackersFor(final GameEntity defender) {
if (defender instanceof Player) {
for (final Card card : ((Player) defender).getCardsIn(ZoneType.Battlefield)) {
if (card.hasKeyword("No more than one creature can attack you each combat.")) {
return 1;
} else if (card.hasKeyword("No more than two creatures can attack you each combat.")) {
return 2;
}
}
}
return -1;
}
public static List<Card> categorizeAttackersByEvasion(List<Card> attackers) { public static List<Card> categorizeAttackersByEvasion(List<Card> attackers) {
List<Card> categorizedAttackers = Lists.newArrayList(); List<Card> categorizedAttackers = Lists.newArrayList();
@@ -2602,5 +2551,3 @@ public class ComputerUtilCombat {
return false; return false;
} }
} }

View File

@@ -3,6 +3,7 @@ package forge.ai;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import forge.game.ability.ApiType;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -156,7 +157,7 @@ public class ComputerUtilCost {
if (typeList.size() > ai.getMaxHandSize()) { if (typeList.size() > ai.getMaxHandSize()) {
continue; continue;
} }
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null); int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList); Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
@@ -557,7 +558,6 @@ public class ComputerUtilCost {
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic); boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined"); boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payNever = "Never".equals(aiLogic); boolean payNever = "Never".equals(aiLogic);
boolean shockland = "Shockland".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer); boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; } if (payNever) { return false; }
@@ -572,26 +572,6 @@ public class ComputerUtilCost {
if (sa.getHostCard() == null || payer.equals(sa.getHostCard().getController())) { if (sa.getHostCard() == null || payer.equals(sa.getHostCard().getController())) {
return false; return false;
} }
} else if (shockland) {
if (payer.getLife() > 3 && payer.canPayLife(2)) {
final int landsize = payer.getLandsInPlay().size() + 1;
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
// if the new land size would equal the CMC of a card in AIs hand, consider playing it untapped,
// otherwise don't bother running other checks
if (landsize != c.getCMC()) {
continue;
}
// try to determine in the AI is actually planning to play a spell ability from the card
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
// try to determine if the mana shards provided by the lands would be applicable to pay the mana cost
boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
if (canPay && willPlay) {
return true;
}
}
}
return false;
} else if ("Paralyze".equals(aiLogic)) { } else if ("Paralyze".equals(aiLogic)) {
final Card c = source.getEnchantingCard(); final Card c = source.getEnchantingCard();
if (c == null || c.isUntapped()) { if (c == null || c.isUntapped()) {
@@ -629,6 +609,29 @@ public class ComputerUtilCost {
return false; return false;
} }
// Check for shocklands and similar ETB replacement effects
if (sa.hasParam("ETB") && sa.getApi().equals(ApiType.Tap)) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
final CostPayLife lifeCost = (CostPayLife) part;
Integer amount = lifeCost.convertAmount();
if (payer.getLife() > (amount + 1) && payer.canPayLife(amount)) {
final int landsize = payer.getLandsInPlay().size() + 1;
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
// Check if the AI has enough lands to play the card
if (landsize != c.getCMC()) {
continue;
}
// Check if the AI intends to play the card and if it can pay for it with the mana it has
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
return canPay && willPlay;
}
}
}
}
}
// AI will only pay when it's not already payed and only opponents abilities // AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) { if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
return false; return false;

View File

@@ -609,7 +609,6 @@ public class ComputerUtilMana {
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay); String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
//System.out.println(manaProduced);
payMultipleMana(cost, manaProduced, ai); payMultipleMana(cost, manaProduced, ai);
// remove from available lists // remove from available lists

View File

@@ -162,9 +162,9 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value -= subValue(10, "must-attack"); value -= subValue(10, "must-attack");
} else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) { } else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
value -= subValue(10, "must-attack-player"); value -= subValue(10, "must-attack-player");
} else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) { }/* else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
value -= subValue(toughness * 5, "reverse-reach"); value -= subValue(toughness * 5, "reverse-reach");
} }//*/
if (c.hasSVar("DestroyWhenDamaged")) { if (c.hasSVar("DestroyWhenDamaged")) {
value -= subValue((toughness - 1) * 9, "dies-to-dmg"); value -= subValue((toughness - 1) * 9, "dies-to-dmg");

View File

@@ -1215,8 +1215,7 @@ public abstract class GameState {
boolean tapped = c.isTapped(); boolean tapped = c.isTapped();
boolean sickness = c.hasSickness(); boolean sickness = c.hasSickness();
Map<CounterType, Integer> counters = c.getCounters(); Map<CounterType, Integer> counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters // Note: Not clearCounters() since we want to keep the counters var as-is.
// var as-is.
c.setCounters(Maps.newHashMap()); c.setCounters(Maps.newHashMap());
if (c.isAura()) { if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere // dummy "enchanting" to indicate that the card will be force-attached elsewhere

View File

@@ -131,6 +131,12 @@ public class PlayerControllerAi extends PlayerController {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, damageDealt, defender, overrideOrder); return ComputerUtilCombat.distributeAIDamage(attacker, blockers, damageDealt, defender, overrideOrder);
} }
@Override
public Map<GameEntity, Integer> divideShield(Card effectSource, Map<GameEntity, Integer> affected, int shieldAmount) {
// TODO: AI current can't use this so this is not implemented.
return new HashMap<>();
}
@Override @Override
public Integer announceRequirements(SpellAbility ability, String announce) { public Integer announceRequirements(SpellAbility ability, String announce) {
// For now, these "announcements" are made within the AI classes of the appropriate SA effects // For now, these "announcements" are made within the AI classes of the appropriate SA effects
@@ -568,8 +574,8 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question) { public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
return brains.aiShouldRun(replacementEffect, effectSA); return brains.aiShouldRun(replacementEffect, effectSA, affected);
} }
@Override @Override
@@ -649,7 +655,6 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public boolean playChosenSpellAbility(SpellAbility sa) { public boolean playChosenSpellAbility(SpellAbility sa) {
// System.out.println("Playing sa: " + sa);
if (sa instanceof LandAbility) { if (sa instanceof LandAbility) {
if (sa.canPlay()) { if (sa.canPlay()) {
sa.resolve(); sa.resolve();
@@ -788,7 +793,8 @@ public class PlayerControllerAi extends PlayerController {
case "BetterTgtThanRemembered": case "BetterTgtThanRemembered":
if (source.getRememberedCount() > 0) { if (source.getRememberedCount() > 0) {
Card rem = (Card) source.getFirstRemembered(); Card rem = (Card) source.getFirstRemembered();
if (!rem.isInZone(ZoneType.Battlefield)) { // avoid pumping opponent creature
if (!rem.isInZone(ZoneType.Battlefield) || rem.getController().isOpponentOf(source.getController())) {
return true; return true;
} }
for (Card c : source.getController().getCreaturesInPlay()) { for (Card c : source.getController().getCreaturesInPlay()) {
@@ -998,6 +1004,7 @@ public class PlayerControllerAi extends PlayerController {
emptyAbility.setActivatingPlayer(player); emptyAbility.setActivatingPlayer(player);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects()); emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setSVars(sa.getSVars()); emptyAbility.setSVars(sa.getSVars());
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) { if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) {
ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost
return true; return true;

View File

@@ -39,7 +39,6 @@ import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CardUtil; import forge.game.card.CardUtil;
@@ -379,7 +378,7 @@ public class SpecialCardAi {
} }
// Do not activate if damage will be prevented // Do not activate if damage will be prevented
if (source.staticDamagePrevention(predictedPT.getLeft(), source, true, true) == 0) { if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
return false; return false;
} }
@@ -1085,7 +1084,7 @@ public class SpecialCardAi {
return false; return false;
} }
String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield)); String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield));
int devotion = CardFactoryUtil.xCount(sa.getHostCard(), "Count$Devotion." + prominentColor); int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), "Count$Devotion." + prominentColor, sa);
int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0); int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0);
// do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage) // do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage)

View File

@@ -193,7 +193,7 @@ public abstract class SpellAbilityAi {
* Handles the AI decision to play a triggered SpellAbility * Handles the AI decision to play a triggered SpellAbility
*/ */
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (canPlayWithoutRestrict(aiPlayer, sa)) { if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
return true; return true;
} }

View File

@@ -98,6 +98,7 @@ public enum SpellApiToAi {
.put(ApiType.Learn, LearnAi.class) .put(ApiType.Learn, LearnAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class) .put(ApiType.LoseLife, LifeLoseAi.class)
.put(ApiType.LosesGame, GameLossAi.class) .put(ApiType.LosesGame, GameLossAi.class)
.put(ApiType.MakeCard, AlwaysPlayAi.class)
.put(ApiType.Mana, ManaEffectAi.class) .put(ApiType.Mana, ManaEffectAi.class)
.put(ApiType.ManaReflected, CannotPlayAi.class) .put(ApiType.ManaReflected, CannotPlayAi.class)
.put(ApiType.Manifest, ManifestAi.class) .put(ApiType.Manifest, ManifestAi.class)

View File

@@ -100,30 +100,30 @@ public class AnimateAi extends SpellAbilityAi {
} }
// Don't use instant speed animate abilities before AI's COMBAT_BEGIN // Don't use instant speed animate abilities before AI's COMBAT_BEGIN
if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa) if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) { && !sa.hasParam("ActivationPhases") && !"Permanent".equals(sa.getParam("Duration"))) {
return false; return false;
} }
// Don't use instant speed animate abilities outside human's // Don't use instant speed animate abilities outside human's
// COMBAT_DECLARE_ATTACKERS or if no attackers // COMBAT_DECLARE_ATTACKERS or if no attackers
if (ph.getPlayerTurn().isOpponentOf(ai) && !sa.hasParam("Permanent") if (ph.getPlayerTurn().isOpponentOf(ai) && !"Permanent".equals(sa.getParam("Duration"))
&& (!ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS) && (!ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| game.getCombat() != null && game.getCombat().getAttackersOf(ai).isEmpty())) { || game.getCombat() != null && game.getCombat().getAttackersOf(ai).isEmpty())) {
return false; return false;
} }
// Don't activate during MAIN2 unless this effect is permanent // Don't activate during MAIN2 unless this effect is permanent
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) { if (ph.is(PhaseType.MAIN2) && !"Permanent".equals(sa.getParam("Duration")) && !"UntilYourNextTurn".equals(sa.getParam("Duration"))) {
return false; return false;
} }
// Don't animate if the AI won't attack anyway or use as a potential blocker // Don't animate if the AI won't attack anyway or use as a potential blocker
Player opponent = ai.getWeakestOpponent(); Player opponent = ai.getWeakestOpponent();
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise // Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
// the AI will waste resources // the AI will waste resources
boolean activateAsPotentialBlocker = sa.hasParam("UntilYourNextTurn") boolean activateAsPotentialBlocker = "UntilYourNextTurn".equals(sa.getParam("Duration"))
&& ai.getGame().getPhaseHandler().getNextTurn() != ai && ai.getGame().getPhaseHandler().getNextTurn() != ai
&& source.isPermanent(); && source.isPermanent();
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6 if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES) && Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) { && !sa.hasParam("AILogic") && !"Permanent".equals(sa.getParam("Duration")) && !activateAsPotentialBlocker) {
return false; return false;
} }
return true; return true;
@@ -156,7 +156,7 @@ public class AnimateAi extends SpellAbilityAi {
&& !c.isEquipping(); && !c.isEquipping();
// for creatures that could be improved (like Figure of Destiny) // for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) { if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5; int power = -5;
if (sa.hasParam("Power")) { if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa); power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
@@ -179,7 +179,7 @@ public class AnimateAi extends SpellAbilityAi {
} }
} }
if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) { if (!SpellAbilityAi.isSorcerySpeed(sa) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.hasParam("Crew") && c.isCreature()) { if (sa.hasParam("Crew") && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature // Do not try to crew a vehicle which is already a creature
return false; return false;
@@ -278,7 +278,7 @@ public class AnimateAi extends SpellAbilityAi {
Map<Card, Integer> data = Maps.newHashMap(); Map<Card, Integer> data = Maps.newHashMap();
for (final Card c : list) { for (final Card c : list) {
// don't use Permanent animate on something that would leave the field // don't use Permanent animate on something that would leave the field
if (c.hasSVar("EndOfTurnLeavePlay") && sa.hasParam("Permanent")) { if (c.hasSVar("EndOfTurnLeavePlay") && "Permanent".equals(sa.getParam("Duration"))) {
continue; continue;
} }
@@ -312,9 +312,9 @@ public class AnimateAi extends SpellAbilityAi {
// if its player turn, // if its player turn,
// check if its Permanent or that creature would attack // check if its Permanent or that creature would attack
if (ph.isPlayerTurn(ai)) { if (ph.isPlayerTurn(ai)) {
if (!sa.hasParam("Permanent") if (!"Permanent".equals(sa.getParam("Duration"))
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy) && !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
&& !sa.hasParam("UntilHostLeavesPlay")) { && !"UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
continue; continue;
} }
} }
@@ -360,8 +360,7 @@ public class AnimateAi extends SpellAbilityAi {
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or // This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things // two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until // that animate a target. Those can just use AI:RemoveDeck:All until
// this can do a reasonably // this can do a reasonably good job of picking a good target
// good job of picking a good target
return false; return false;
} }

View File

@@ -555,12 +555,7 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
evenBetterList = CardLists.filter(betterList, new Predicate<Card>() { evenBetterList = CardLists.filter(betterList, CardPredicates.Presets.UNTAPPED);
@Override
public boolean apply(final Card c) {
return c.isUntapped();
}
});
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
@@ -734,17 +729,15 @@ public class AttachAi extends SpellAbilityAi {
*/ */
private static Card attachAISpecificCardPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, private static Card attachAISpecificCardPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource) { final Card attachSource) {
// I know this isn't much better than Hardcoding, but some cards need it for now
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
Card chosen = null; Card chosen = null;
if ("Guilty Conscience".equals(sourceName)) { if ("Guilty Conscience".equals(sourceName)) {
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list); chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
} else if ("Bonds of Faith".equals(sourceName)) { } else if (sa.hasParam("AIValid")) {
chosen = doPumpOrCurseAILogic(ai, sa, list, "Human"); // TODO: Make the AI recognize which cards to pump based on the card's abilities alone
} else if ("Clutch of Undeath".equals(sourceName)) { chosen = doPumpOrCurseAILogic(ai, sa, list, sa.getParam("AIValid"));
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
} }
// If Mandatory (brought directly into play without casting) gotta // If Mandatory (brought directly into play without casting) gotta
@@ -768,7 +761,7 @@ public class AttachAi extends SpellAbilityAi {
int powerBuff = 0; int powerBuff = 0;
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) { for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) { if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) {
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), null); powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), stAb);
} }
} }
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) { if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
@@ -1646,7 +1639,7 @@ public class AttachAi extends SpellAbilityAi {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1; return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
} else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) { } else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty(); return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty();
} else if (keyword.endsWith("CARDNAME can't block.") || keyword.contains("CantBlock")) { } else if (keyword.endsWith("CARDNAME can't block.")) {
return CombatUtil.canBlock(card, true); return CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) { } else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
for (SpellAbility ability : card.getSpellAbilities()) { for (SpellAbility ability : card.getSpellAbilities()) {
@@ -1699,7 +1692,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.getController().equals(ai)) { if (!c.getController().equals(ai)) {
return false; return false;
} }
return c.getType().hasCreatureType(type); return c.isValid(type, ai, sa.getHostCard(), sa);
} }
}); });
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() { List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
@@ -1709,7 +1702,7 @@ public class AttachAi extends SpellAbilityAi {
if (c.getController().equals(ai)) { if (c.getController().equals(ai)) {
return false; return false;
} }
return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c); return !c.isValid(type, ai, sa.getHostCard(), sa) && !ComputerUtilCard.isUselessCreature(ai, c);
} }
}); });

View File

@@ -165,7 +165,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
} }
} }
if (isHidden(sa)) { if (sa.isHidden()) {
return hiddenOriginCanPlayAI(aiPlayer, sa); return hiddenOriginCanPlayAI(aiPlayer, sa);
} }
return knownOriginCanPlayAI(aiPlayer, sa); return knownOriginCanPlayAI(aiPlayer, sa);
@@ -182,21 +182,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/ */
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (isHidden(sa)) { if (sa.isHidden()) {
return hiddenOriginPlayDrawbackAI(aiPlayer, sa); return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
} }
return knownOriginPlayDrawbackAI(aiPlayer, sa); return knownOriginPlayDrawbackAI(aiPlayer, sa);
} }
private static boolean isHidden(SpellAbility sa) {
boolean hidden = sa.hasParam("Hidden");
if (!hidden && sa.hasParam("Origin")) {
hidden = ZoneType.isHidden(sa.getParam("Origin"));
}
return hidden;
}
/** /**
* <p> * <p>
* changeZoneTriggerAINoCost. * changeZoneTriggerAINoCost.
@@ -232,7 +223,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return delta <= 0; return delta <= 0;
} }
if (isHidden(sa)) { if (sa.isHidden()) {
return hiddenTriggerAI(aiPlayer, sa, mandatory); return hiddenTriggerAI(aiPlayer, sa, mandatory);
} }
return knownOriginTriggerAI(aiPlayer, sa, mandatory); return knownOriginTriggerAI(aiPlayer, sa, mandatory);
@@ -788,7 +779,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN); return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
} }
if (isHidden(sa)) { if (sa.isHidden()) {
return true; return true;
} }
@@ -1316,8 +1307,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static Card canBouncePermanent(final Player ai, SpellAbility sa, CardCollectionView list) { private static Card canBouncePermanent(final Player ai, SpellAbility sa, CardCollectionView list) {
Game game = ai.getGame(); Game game = ai.getGame();
// filter out untargetables // filter out untargetables
CardCollectionView aiPermanents = CardLists CardCollectionView aiPermanents = CardLists.filterControlledBy(list, ai);
.filterControlledBy(list, ai);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS); CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
// Felidar Guardian + Saheeli Rai combo support // Felidar Guardian + Saheeli Rai combo support
@@ -1870,6 +1860,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
})); }));
} }
if (!data.isEmpty()) {
// JAVA 1.8 use Map.Entry.comparingByValue() somehow // JAVA 1.8 use Map.Entry.comparingByValue() somehow
Map.Entry<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() { Map.Entry<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() {
@Override @Override
@@ -1888,6 +1879,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.getTargets().add(c); sa.getTargets().add(c);
return true; return true;
} }
}
return false; return false;
} }
@@ -1987,8 +1979,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean setPayX = false; boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true; setPayX = true;
// TODO use ComputerUtilCost.getMaxXValue if able toPay = ComputerUtilCost.getMaxXValue(sa, ai);
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else { } else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
} }

View File

@@ -52,8 +52,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
} }
} else if ("GideonBlackblade".equals(aiLogic)) { } else if ("GideonBlackblade".equals(aiLogic)) {
return SpecialCardAi.GideonBlackblade.consider(ai, sa); return SpecialCardAi.GideonBlackblade.consider(ai, sa);
} else if ("SoulEcho".equals(aiLogic)) {
return doTriggerAINoCost(ai, sa, true);
} else if ("Always".equals(aiLogic)) { } else if ("Always".equals(aiLogic)) {
return true; return true;
} }
@@ -70,7 +68,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
*/ */
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return checkApiLogic(aiPlayer, sa); return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
} }
@Override @Override

View File

@@ -56,7 +56,7 @@ public class CloneAi extends SpellAbilityAi {
bFlag |= (!c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn())); bFlag |= (!c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn()));
// for creatures that could be improved (like Figure of Destiny) // for creatures that could be improved (like Figure of Destiny)
if (c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) { if (c.isCreature() && (!sa.hasParam("Duration") || (!c.isTapped() && !c.isSick()))) {
int power = -5; int power = -5;
if (sa.hasParam("Power")) { if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa); power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
@@ -72,8 +72,7 @@ public class CloneAi extends SpellAbilityAi {
} }
if (!bFlag) { // All of the defined stuff is cloned, not very if (!bFlag) { // All of the defined stuff is cloned, not very useful
// useful
return false; return false;
} }
} else { } else {
@@ -246,7 +245,7 @@ public class CloneAi extends SpellAbilityAi {
// Combat_Begin step // Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN) if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) { && !sa.hasParam("ActivationPhases") && sa.hasParam("Duration")) {
return false; return false;
} }
@@ -257,6 +256,6 @@ public class CloneAi extends SpellAbilityAi {
} }
// don't activate during main2 unless this effect is permanent // don't activate during main2 unless this effect is permanent
return !ph.is(PhaseType.MAIN2) || sa.hasParam("Permanent"); return !ph.is(PhaseType.MAIN2) || !sa.hasParam("Duration");
} }
} }

View File

@@ -31,8 +31,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
CardCollection list = CardCollection list =
CardLists.getValidCards(AiAttackController.choosePreferredDefenderPlayer(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); CardLists.getValidCards(AiAttackController.choosePreferredDefenderPlayer(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on // AI won't try to grab cards that are filtered out of AI decks on purpose
// purpose
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {

View File

@@ -110,8 +110,7 @@ public class ControlGainAi extends SpellAbilityAi {
} }
} }
// Don't steal something if I can't Attack without, or prevent it from // Don't steal something if I can't Attack without, or prevent it from blocking at least
// blocking at least
if (lose.contains("EOT") if (lose.contains("EOT")
&& ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !sa.isTrigger()) { && !sa.isTrigger()) {
@@ -211,6 +210,7 @@ public class ControlGainAi extends SpellAbilityAi {
} }
} }
// TODO check life of controller and consider stealing from another opponent so the risk of your army disappearing is spread out
while (t == null) { while (t == null) {
// filter by MustTarget requirement // filter by MustTarget requirement
CardCollection originalList = new CardCollection(list); CardCollection originalList = new CardCollection(list);
@@ -264,7 +264,6 @@ public class ControlGainAi extends SpellAbilityAi {
} }
return true; return true;
} }
@Override @Override

View File

@@ -113,8 +113,7 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false; boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true; setPayX = true;
// TODO use ComputerUtilCost.getMaxXValue toPay = Math.min(ComputerUtilCost.getMaxXValue(sa, ai), usableManaSources + 1);
toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
} else { } else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
} }
@@ -124,8 +123,7 @@ public class CounterAi extends SpellAbilityAi {
} }
if (toPay <= usableManaSources) { if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of // If this is a reusable Resource, feel free to play it most of the time
// the time
if (!SpellAbilityAi.playReusable(ai,sa)) { if (!SpellAbilityAi.playReusable(ai,sa)) {
return false; return false;
} }

View File

@@ -120,8 +120,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
// pre filter targetable cards with counters and can receive one of // pre filter targetable cards with counters and can receive one of them
// them
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override

View File

@@ -866,6 +866,7 @@ public class CountersPutAi extends SpellAbilityAi {
choice = Aggregates.random(list); choice = Aggregates.random(list);
} }
} }
}
if (choice != null && divided) { if (choice != null && divided) {
int alloc = Math.max(amount / totalTargets, 1); int alloc = Math.max(amount / totalTargets, 1);
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) { if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
@@ -875,8 +876,6 @@ public class CountersPutAi extends SpellAbilityAi {
left -= alloc; left -= alloc;
} }
} }
}
if (choice != null) { if (choice != null) {
sa.getTargets().add(choice); sa.getTargets().add(choice);
list.remove(choice); list.remove(choice);

View File

@@ -111,8 +111,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
final CardCollectionView hand = comp.getCardsIn(ZoneType.Hand); final CardCollectionView hand = comp.getCardsIn(ZoneType.Hand);
if ((enemy.getLife() - restDamage) < 5) { if ((enemy.getLife() - restDamage) < 5) {
// drop the human to less than 5 // drop the human to less than 5 life
// life
return true; return true;
} }

View File

@@ -30,7 +30,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
@@ -41,7 +40,6 @@ import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices; import forge.game.spellability.TargetChoices;
@@ -132,7 +130,7 @@ public class DamageDealAi extends DamageAiBase {
// Set PayX here to maximum value. It will be adjusted later depending on the target. // Set PayX here to maximum value. It will be adjusted later depending on the target.
sa.setXManaCostPaid(dmg); sa.setXManaCostPaid(dmg);
} else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) { } else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) {
dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less dmg = AbilityUtils.calculateAmount(source, damage, sa) - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) { } else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact // cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
if (sa.getTargetRestrictions().canTgtPlayer()) { if (sa.getTargetRestrictions().canTgtPlayer()) {
@@ -734,12 +732,10 @@ public class DamageDealAi extends DamageAiBase {
} }
// When giving priority to targeting Creatures for mandatory // When giving priority to targeting Creatures for mandatory
// triggers // triggers feel free to add the Human after we run out of good targets
// feel free to add the Human after we run out of good targets
// TODO: add check here if card is about to die from something // TODO: add check here if card is about to die from something
// on the stack // on the stack or from taking combat damage
// or from taking combat damage
final Cost abCost = sa.getPayCosts(); final Cost abCost = sa.getPayCosts();
boolean freePing = immediately || abCost == null boolean freePing = immediately || abCost == null
@@ -796,8 +792,7 @@ public class DamageDealAi extends DamageAiBase {
} }
} }
} }
// TODO: Improve Damage, we shouldn't just target the player just // TODO: Improve Damage, we shouldn't just target the player just because we can
// because we can
if (sa.canTarget(enemy) && tcs.size() < tgt.getMaxTargets(source, sa)) { if (sa.canTarget(enemy) && tcs.size() < tgt.getMaxTargets(source, sa)) {
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai)) if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2)) || (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
@@ -985,7 +980,6 @@ public class DamageDealAi extends DamageAiBase {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String damage = sa.getParam("NumDmg"); final String damage = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(source, damage, sa); int dmg = AbilityUtils.calculateAmount(source, damage, sa);
@@ -1042,7 +1036,7 @@ public class DamageDealAi extends DamageAiBase {
saTgt = saTgt.getParent(); saTgt = saTgt.getParent();
} }
Player opponent = ai.getOpponents().min(PlayerPredicates.compareByLife()); Player opponent = ai.getWeakestOpponent();
// TODO: somehow account for the possible cost reduction? // TODO: somehow account for the possible cost reduction?
int dmg = ComputerUtilMana.determineLeftoverMana(sa, ai, saTgt.getParam("XColor")); int dmg = ComputerUtilMana.determineLeftoverMana(sa, ai, saTgt.getParam("XColor"));
@@ -1085,7 +1079,7 @@ public class DamageDealAi extends DamageAiBase {
saTgt.resetTargets(); saTgt.resetTargets();
saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent); saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent);
sa.setXManaCostPaid(dmg); saTgt.setXManaCostPaid(dmg);
return true; return true;
} }

View File

@@ -45,9 +45,8 @@ public class DigAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) { if (!opp.canBeTargetedBy(sa)) {
return false; return false;
} else {
sa.getTargets().add(opp);
} }
sa.getTargets().add(opp);
libraryOwner = opp; libraryOwner = opp;
} }

View File

@@ -28,9 +28,8 @@ public class DigMultipleAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) { if (!opp.canBeTargetedBy(sa)) {
return false; return false;
} else {
sa.getTargets().add(opp);
} }
sa.getTargets().add(opp);
libraryOwner = opp; libraryOwner = opp;
} }

View File

@@ -60,9 +60,8 @@ public class DigUntilAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
if (!sa.canTarget(opp)) { if (!sa.canTarget(opp)) {
return false; return false;
} else {
sa.getTargets().add(opp);
} }
sa.getTargets().add(opp);
libraryOwner = opp; libraryOwner = opp;
} else { } else {
if (sa.hasParam("Valid")) { if (sa.hasParam("Valid")) {

View File

@@ -257,7 +257,7 @@ public class DrawAi extends SpellAbilityAi {
} else { } else {
numCards = ComputerUtilCost.getMaxXValue(sa, ai); numCards = ComputerUtilCost.getMaxXValue(sa, ai);
// try not to overdraw // try not to overdraw
int safeDraw = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3); int safeDraw = Math.abs(Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3));
if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent
numCards = Math.min(numCards, safeDraw); numCards = Math.min(numCards, safeDraw);
@@ -377,7 +377,7 @@ public class DrawAi extends SpellAbilityAi {
// checks what the ai prevent from casting it on itself // checks what the ai prevent from casting it on itself
// if spell is not mandatory // if spell is not mandatory
if (aiTarget && !ai.cantLose()) { if (aiTarget && !ai.cantLose()) {
if (numCards >= computerLibrarySize) { if (numCards >= computerLibrarySize - 3) {
if (xPaid) { if (xPaid) {
numCards = computerLibrarySize - 1; numCards = computerLibrarySize - 1;
if (numCards <= 0 && !mandatory) { if (numCards <= 0 && !mandatory) {
@@ -422,8 +422,7 @@ public class DrawAi extends SpellAbilityAi {
} }
root.setXManaCostPaid(numCards); root.setXManaCostPaid(numCards);
} else { } else {
// Don't draw too many cards and then risk discarding // Don't draw too many cards and then risk discarding cards at EOT
// cards at EOT
if (!drawback && !mandatory) { if (!drawback && !mandatory) {
return false; return false;
} }
@@ -441,7 +440,7 @@ public class DrawAi extends SpellAbilityAi {
continue; continue;
} }
// use xPaid abilties only for itself // use xPaid abilities only for itself
if (xPaid) { if (xPaid) {
continue; continue;
} }
@@ -493,7 +492,7 @@ public class DrawAi extends SpellAbilityAi {
// TODO: consider if human is the defined player // TODO: consider if human is the defined player
// ability is not targeted // ability is not targeted
if (numCards >= computerLibrarySize) { if (numCards >= computerLibrarySize - 3) {
if (ai.isCardInPlay("Laboratory Maniac")) { if (ai.isCardInPlay("Laboratory Maniac")) {
return true; return true;
} }
@@ -509,8 +508,7 @@ public class DrawAi extends SpellAbilityAi {
&& game.getPhaseHandler().isPlayerTurn(ai) && game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger() && !sa.isTrigger()
&& !assumeSafeX) { && !assumeSafeX) {
// Don't draw too many cards and then risk discarding cards at // Don't draw too many cards and then risk discarding cards at EOT
// EOT
if (!drawback) { if (!drawback) {
return false; return false;
} }

View File

@@ -79,7 +79,7 @@ public class EffectAi extends SpellAbilityAi {
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
return false; return false;
} }
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) { if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false; return false;
} }
if (!ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) { if (!ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {

View File

@@ -29,7 +29,7 @@ public class FogAi extends SpellAbilityAi {
final Card hostCard = sa.getHostCard(); final Card hostCard = sa.getHostCard();
// Don't cast it, if the effect is already in place // Don't cast it, if the effect is already in place
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) { if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false; return false;
} }

View File

@@ -13,8 +13,7 @@ public class GameLossAi extends SpellAbilityAi {
return false; return false;
} }
// Only one SA Lose the Game card right now, which is Door to // Only one SA Lose the Game card right now, which is Door to Nothingness
// Nothingness
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
@@ -29,19 +28,23 @@ public class GameLossAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
Player loser = ai;
// Phage the Untouchable // Phage the Untouchable
// (Final Fortune would need to attach it's delayed trigger to a // (Final Fortune would need to attach it's delayed trigger to a
// specific turn, which can't be done yet) // specific turn, which can't be done yet)
Player opp = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard()); if (ai.getGame().getCombat() != null) {
loser = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard());
}
if (!mandatory && opp.cantLose()) { if (!mandatory && loser.cantLose()) {
return false; return false;
} }
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(opp); sa.getTargets().add(loser);
} }
return true; return true;

View File

@@ -266,8 +266,7 @@ public class LifeGainAi extends SpellAbilityAi {
} }
if (!hasTgt && mandatory) { if (!hasTgt && mandatory) {
// need to target something but its neither negative against // need to target something but its neither negative against
// opponents, // opponents, nor positive against allies
// nor posive against allies
// hurting ally is probably better than healing opponent // hurting ally is probably better than healing opponent
// look for Lifegain not Negative (case of lifegain negated) // look for Lifegain not Negative (case of lifegain negated)
@@ -295,8 +294,7 @@ public class LifeGainAi extends SpellAbilityAi {
sa.getTargets().add(ally); sa.getTargets().add(ally);
hasTgt = true; hasTgt = true;
} }
// better heal opponent which most life then the one with the // better heal opponent which most life then the one with the lowest
// lowest
if (!hasTgt) { if (!hasTgt) {
Player opp = opps.max(PlayerPredicates.compareByLife()); Player opp = opps.max(PlayerPredicates.compareByLife());
sa.getTargets().add(opp); sa.getTargets().add(opp);

View File

@@ -28,7 +28,6 @@ public class LifeLoseAi extends SpellAbilityAi {
*/ */
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) { public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final PlayerCollection tgtPlayers = getPlayers(ai, sa); final PlayerCollection tgtPlayers = getPlayers(ai, sa);
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -60,7 +59,6 @@ public class LifeLoseAi extends SpellAbilityAi {
return true; return true;
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
@@ -123,12 +121,12 @@ public class LifeLoseAi extends SpellAbilityAi {
return false; return false;
} }
final PlayerCollection tgtPlayers = getPlayers(ai, sa);
if (ComputerUtil.playImmediately(ai, sa)) { if (ComputerUtil.playImmediately(ai, sa)) {
return true; return true;
} }
final PlayerCollection tgtPlayers = getPlayers(ai, sa);
// TODO: check against the amount we could obtain when multiple activations are possible
PlayerCollection filteredPlayer = tgtPlayers PlayerCollection filteredPlayer = tgtPlayers
.filter(Predicates.and(PlayerPredicates.isOpponentOf(ai), PlayerPredicates.lifeLessOrEqualTo(amount))); .filter(Predicates.and(PlayerPredicates.isOpponentOf(ai), PlayerPredicates.lifeLessOrEqualTo(amount)));
// killing opponents asap // killing opponents asap

View File

@@ -21,6 +21,7 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -214,13 +215,7 @@ public class MillAi extends SpellAbilityAi {
} }
// get targeted or defined Player with largest library // get targeted or defined Player with largest library
// TODO in Java 8 find better way final Player m = Collections.max(list, PlayerPredicates.compareByZoneSize(ZoneType.Library));
final Player m = Collections.max(list, new Comparator<Player>() {
@Override
public int compare(Player arg0, Player arg1) {
return arg0.getCardsIn(ZoneType.Library).size() - arg1.getCardsIn(ZoneType.Library).size();
}
});
int cardsToDiscard = m.getCardsIn(ZoneType.Library).size(); int cardsToDiscard = m.getCardsIn(ZoneType.Library).size();

View File

@@ -297,7 +297,7 @@ public class PermanentAi extends SpellAbilityAi {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) { if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false; return false;
} }
return checkApiLogic(ai, sa); return mandatory || checkApiLogic(ai, sa);
} }
} }

View File

@@ -8,7 +8,6 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
/** /**
@@ -40,10 +39,8 @@ public class PermanentNoncreatureAi extends PermanentAi {
if (host.hasSVar("OblivionRing")) { if (host.hasSVar("OblivionRing")) {
SpellAbility effectExile = AbilityFactory.getAbility(host.getSVar("TrigExile"), host); SpellAbility effectExile = AbilityFactory.getAbility(host.getSVar("TrigExile"), host);
final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0); final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0);
final TargetRestrictions tgt = effectExile.getTargetRestrictions(); effectExile.setActivatingPlayer(ai);
final CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, host, CardCollection targets = CardLists.getTargetableCards(game.getCardsIn(origin), effectExile);
effectExile);
CardCollection targets = CardLists.getTargetableCards(list, sa);
if (sourceName.equals("Suspension Field") if (sourceName.equals("Suspension Field")
|| sourceName.equals("Detention Sphere")) { || sourceName.equals("Detention Sphere")) {
// existing "exile until leaves" enchantments only target // existing "exile until leaves" enchantments only target

View File

@@ -28,7 +28,6 @@ import forge.game.card.CardPredicates;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell; import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates; import forge.game.spellability.SpellAbilityPredicates;
@@ -159,15 +158,6 @@ public class PlayAi extends SpellAbilityAi {
return true; return true;
} }
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
// as called from PlayEffect:173
return true;
}
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
*/ */

View File

@@ -400,7 +400,6 @@ public class PumpAi extends PumpAiBase {
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) { if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
return true; return true;
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) { } else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords); Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai) if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)
|| game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) { || game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
@@ -438,7 +437,7 @@ public class PumpAi extends PumpAiBase {
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) && game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !(sa.isCurse() && defense < 0) && !(sa.isCurse() && defense < 0)
&& !containsNonCombatKeyword(keywords) && !containsNonCombatKeyword(keywords)
&& !sa.hasParam("UntilYourNextTurn") && !"UntilYourNextTurn".equals(sa.getParam("Duration"))
&& !"Snapcaster".equals(sa.getParam("AILogic")) && !"Snapcaster".equals(sa.getParam("AILogic"))
&& !isFight) { && !isFight) {
return false; return false;

View File

@@ -44,7 +44,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false; return false;
} }
public boolean grantsUsefulExtraBlockOpts(final Player ai, final SpellAbility sa, final Card card, List<String> keywords) { public boolean grantsUsefulExtraBlockOpts(final Player ai, final SpellAbility sa, final Card card, List<String> keywords) {
PhaseHandler ph = ai.getGame().getPhaseHandler(); PhaseHandler ph = ai.getGame().getPhaseHandler();
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords); Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
@@ -110,7 +109,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
&& (card.getNetCombatDamage() > 0) && (card.getNetCombatDamage() > 0)
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS); && !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
} else if (keyword.endsWith("CARDNAME can't attack or block.")) { } else if (keyword.endsWith("CARDNAME can't attack or block.")) {
if (sa.hasParam("UntilYourNextTurn")) { if ("UntilYourNextTurn".equals(sa.getParam("Duration"))) {
return CombatUtil.canAttack(card, ai) || CombatUtil.canBlock(card, true); return CombatUtil.canAttack(card, ai) || CombatUtil.canBlock(card, true);
} }
if (!ph.isPlayerTurn(ai)) { if (!ph.isPlayerTurn(ai)) {
@@ -154,14 +153,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
} }
}); });
return CombatUtil.canBlockAtLeastOne(card, attackers); return CombatUtil.canBlockAtLeastOne(card, attackers);
} else if (keyword.endsWith("CantBlockCardUIDSource")) { // can't block CARDNAME this turn
if (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| ph.getPhase().isBefore(PhaseType.MAIN1) || !CombatUtil.canBlock(sa.getHostCard(), card)) {
return false;
}
// target needs to be a creature, controlled by the player which is attacked
return !sa.getHostCard().isTapped() || (combat != null && combat.isAttacking(sa.getHostCard())
&& card.getController().equals(combat.getDefenderPlayerByAttacker(sa.getHostCard())));
} else if (keyword.endsWith("This card doesn't untap during your next untap step.")) { } else if (keyword.endsWith("This card doesn't untap during your next untap step.")) {
return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai) return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai)
&& Untap.canUntap(card); && Untap.canUntap(card);
@@ -480,7 +471,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
} }
}); // leaves all creatures that will be destroyed }); // leaves all creatures that will be destroyed
} // -X/-X end } // -X/-X end
else if (attack < 0 && !game.getPhaseHandler().isPreventCombatDamageThisTurn()) { else if (attack < 0 && !game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
// spells that give -X/0 // spells that give -X/0
boolean isMyTurn = game.getPhaseHandler().isPlayerTurn(ai); boolean isMyTurn = game.getPhaseHandler().isPlayerTurn(ai);
if (isMyTurn) { if (isMyTurn) {
@@ -514,7 +505,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
else { else {
final boolean addsKeywords = !keywords.isEmpty(); final boolean addsKeywords = !keywords.isEmpty();
if (addsKeywords) { if (addsKeywords) {
// If the keyword can prevent a creature from attacking, see if there's some kind of viable prioritization // If the keyword can prevent a creature from attacking, see if there's some kind of viable prioritization
if (keywords.contains("CARDNAME can't attack.") || keywords.contains("CARDNAME can't attack or block.") if (keywords.contains("CARDNAME can't attack.") || keywords.contains("CARDNAME can't attack or block.")
|| keywords.contains("HIDDEN CARDNAME can't attack.") || keywords.contains("HIDDEN CARDNAME can't attack or block.")) { || keywords.contains("HIDDEN CARDNAME can't attack.") || keywords.contains("HIDDEN CARDNAME can't attack or block.")) {
@@ -540,8 +530,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
protected boolean containsNonCombatKeyword(final List<String> keywords) { protected boolean containsNonCombatKeyword(final List<String> keywords) {
for (final String keyword : keywords) { for (final String keyword : keywords) {
// since most keywords are combat relevant check for those that are // since most keywords are combat relevant check for those that are not
// not
if (keyword.endsWith("This card doesn't untap during your next untap step.") if (keyword.endsWith("This card doesn't untap during your next untap step.")
|| keyword.endsWith("Shroud") || keyword.endsWith("Hexproof")) { || keyword.endsWith("Shroud") || keyword.endsWith("Hexproof")) {
return true; return true;

View File

@@ -113,7 +113,7 @@ public class PumpAllAi extends PumpAiBase {
if (phase.isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) if (phase.isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| phase.isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || phase.isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()) || game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())
|| game.getPhaseHandler().isPreventCombatDamageThisTurn()) { || game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false; return false;
} }
int totalPower = 0; int totalPower = 0;
@@ -151,6 +151,12 @@ public class PumpAllAi extends PumpAiBase {
return true; return true;
} }
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
// important to call canPlay first so targets are added if needed
return canPlayAI(ai, sa) || mandatory;
}
boolean pumpAgainstRemoval(Player ai, SpellAbility sa, List<Card> comp) { boolean pumpAgainstRemoval(Player ai, SpellAbility sa, List<Card> comp) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true); final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
for (final Card c : comp) { for (final Card c : comp) {

View File

@@ -49,14 +49,6 @@ import forge.game.zone.ZoneType;
*/ */
public class RegenerateAi extends SpellAbilityAi { public class RegenerateAi extends SpellAbilityAi {
// Ex: A:SP$Regenerate | Cost$W | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$Regenerate
// target creature.
// http://www.slightlymagic.net/wiki/Forge_AbilityFactory#Regenerate
// **************************************************************
// ********************* Regenerate ****************************
// **************************************************************
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -65,8 +57,7 @@ public class RegenerateAi extends SpellAbilityAi {
boolean chance = false; boolean chance = false;
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) { if (tgt == null) {
// As far as I can tell these Defined Cards will only have one of // As far as I can tell these Defined Cards will only have one of them
// them
final List<Card> list = AbilityUtils.getDefinedCards(hostCard, sa.getParam("Defined"), sa); final List<Card> list = AbilityUtils.getDefinedCards(hostCard, sa.getParam("Defined"), sa);
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
@@ -105,8 +96,7 @@ public class RegenerateAi extends SpellAbilityAi {
} }
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
// check stack for something on the stack will kill anything i // check stack for something on the stack will kill anything i control
// control
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true); final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
final List<Card> threatenedTargets = new ArrayList<>(); final List<Card> threatenedTargets = new ArrayList<>();
@@ -191,8 +181,7 @@ public class RegenerateAi extends SpellAbilityAi {
} }
} }
// TODO see if something on the stack is about to kill something i // TODO see if something on the stack is about to kill something i can target
// can target
// choose my best X without regen // choose my best X without regen
if (CardLists.getNotType(compTargetables, "Creature").isEmpty()) { if (CardLists.getNotType(compTargetables, "Creature").isEmpty()) {

View File

@@ -18,7 +18,7 @@ public class RepeatAi extends SpellAbilityAi {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (!opp.canBeTargetedBy(sa)) { if (!sa.canTarget(opp)) {
return false; return false;
} }
sa.resetTargets(); sa.resetTargets();

View File

@@ -119,11 +119,9 @@ public class ScryAi extends SpellAbilityAi {
return false; return false;
} }
double chance = .4; // 40 percent chance of milling with instant speed double chance = .4; // 40 percent chance of milling with instant speed stuff
// stuff
if (SpellAbilityAi.isSorcerySpeed(sa)) { if (SpellAbilityAi.isSorcerySpeed(sa)) {
chance = .667; // 66.7% chance for sorcery speed (since it will chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
// never activate EOT)
} }
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1); boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);

View File

@@ -52,8 +52,7 @@ public class SetStateAi extends SpellAbilityAi {
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
// Gross generalization, but this always considers alternate // Gross generalization, but this always considers alternate states more powerful
// states more powerful
return !sa.getHostCard().isInAlternateState(); return !sa.getHostCard().isInAlternateState();
} }

View File

@@ -17,8 +17,7 @@ public class ShuffleAi extends SpellAbilityAi {
return aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2, aiPlayer); return aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2, aiPlayer);
} }
// not really sure when the compy would use this; maybe only after a // not really sure when the compy would use this; maybe only after a human
// human
// deliberately put a card on top of their library // deliberately put a card on top of their library
return false; return false;
/* /*

View File

@@ -11,6 +11,7 @@ import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
@@ -309,9 +310,8 @@ public abstract class TapAiBase extends SpellAbilityAi {
return true; return true;
} }
// TODO: use Defined to determine, if this is an unfavorable result final List<Card> pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
return pDefined.isEmpty() || (pDefined.get(0).isUntapped() && pDefined.get(0).getController() != ai);
return true;
} else { } else {
sa.resetTargets(); sa.resetTargets();
if (tapPrefTargeting(ai, source, sa, mandatory)) { if (tapPrefTargeting(ai, source, sa, mandatory)) {

View File

@@ -12,7 +12,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -49,7 +49,7 @@ public class TapAllAi extends SpellAbilityAi {
} }
validTappables = CardLists.getValidCards(validTappables, valid, source.getController(), source, sa); validTappables = CardLists.getValidCards(validTappables, valid, source.getController(), source, sa);
validTappables = CardLists.filter(validTappables, Presets.UNTAPPED); validTappables = CardLists.filter(validTappables, CardPredicates.Presets.UNTAPPED);
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
String logic = sa.getParam("AILogic"); String logic = sa.getParam("AILogic");
@@ -69,18 +69,8 @@ public class TapAllAi extends SpellAbilityAi {
return false; return false;
} }
final List<Card> human = CardLists.filter(validTappables, new Predicate<Card>() { final List<Card> human = CardLists.filterControlledBy(validTappables, opp);
@Override final List<Card> compy = CardLists.filterControlledBy(validTappables, ai);
public boolean apply(final Card c) {
return c.getController().equals(opp);
}
});
final List<Card> compy = CardLists.filter(validTappables, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getController().equals(ai);
}
});
if (human.size() <= compy.size()) { if (human.size() <= compy.size()) {
return false; return false;
} }
@@ -102,7 +92,7 @@ public class TapAllAi extends SpellAbilityAi {
final Game game = source.getGame(); final Game game = source.getGame();
CardCollectionView tmpList = game.getCardsIn(ZoneType.Battlefield); CardCollectionView tmpList = game.getCardsIn(ZoneType.Battlefield);
tmpList = CardLists.getValidCards(tmpList, valid, source.getController(), source, sa); tmpList = CardLists.getValidCards(tmpList, valid, source.getController(), source, sa);
tmpList = CardLists.filter(tmpList, Presets.UNTAPPED); tmpList = CardLists.filter(tmpList, CardPredicates.Presets.UNTAPPED);
return tmpList; return tmpList;
} }

View File

@@ -19,8 +19,7 @@ public class TapOrUntapAi extends TapAiBase {
if (!sa.usesTargeting()) { if (!sa.usesTargeting()) {
// assume we are looking to tap human's stuff // assume we are looking to tap human's stuff
// TODO - check for things with untap abilities, and don't tap // TODO - check for things with untap abilities, and don't tap those.
// those.
boolean bFlag = false; boolean bFlag = false;
for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) { for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) {
@@ -40,6 +39,4 @@ public class TapOrUntapAi extends TapAiBase {
return randomReturn; return randomReturn;
} }
} }

View File

@@ -66,7 +66,7 @@ public class UntapAi extends SpellAbilityAi {
if (!sa.usesTargeting()) { if (!sa.usesTargeting()) {
final List<Card> pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); final List<Card> pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai; return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
} else { } else {
return untapPrefTargeting(ai, sa, false); return untapPrefTargeting(ai, sa, false);
} }
@@ -82,9 +82,8 @@ public class UntapAi extends SpellAbilityAi {
return false; return false;
} }
// TODO: use Defined to determine, if this is an unfavorable result
final List<Card> pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); final List<Card> pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai; return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
} else { } else {
if (untapPrefTargeting(ai, sa, mandatory)) { if (untapPrefTargeting(ai, sa, mandatory)) {
return true; return true;

View File

@@ -8,6 +8,7 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -29,6 +30,10 @@ public class UntapAllAi extends SpellAbilityAi {
valid = sa.getParam("ValidCards"); valid = sa.getParam("ValidCards");
} }
list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa); list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa);
// don't untap if only opponent benefits
PlayerCollection goodControllers = aiPlayer.getAllies();
goodControllers.add(aiPlayer);
list = CardLists.filter(list, CardPredicates.isControlledByAnyOf(goodControllers));
return !list.isEmpty(); return !list.isEmpty();
} }
return false; return false;

View File

@@ -88,7 +88,6 @@ public class GameCopier {
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters())); newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn()); newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn()); newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
newPlayer.setPreventNextDamage(origPlayer.getPreventNextDamage());
newPlayer.getManaPool().add(origPlayer.getManaPool()); newPlayer.getManaPool().add(origPlayer.getManaPool());
newPlayer.setCommanders(origPlayer.getCommanders()); // will be fixed up below newPlayer.setCommanders(origPlayer.getCommanders()); // will be fixed up below
playerMap.put(origPlayer, newPlayer); playerMap.put(origPlayer, newPlayer);

View File

@@ -151,6 +151,11 @@ public class StaticData {
return this.editions; return this.editions;
} }
public final CardEdition.Collection getCustomEditions(){
return this.customEditions;
}
private List<CardEdition> sortedEditions; private List<CardEdition> sortedEditions;
public final List<CardEdition> getSortedEditions() { public final List<CardEdition> getSortedEditions() {
if (sortedEditions == null) { if (sortedEditions == null) {
@@ -158,12 +163,24 @@ public class StaticData {
for (CardEdition set : editions) { for (CardEdition set : editions) {
sortedEditions.add(set); sortedEditions.add(set);
} }
if (customEditions.size() > 0){
for (CardEdition set : customEditions) {
sortedEditions.add(set);
}
}
Collections.sort(sortedEditions); Collections.sort(sortedEditions);
Collections.reverse(sortedEditions); //put newer sets at the top Collections.reverse(sortedEditions); //put newer sets at the top
} }
return sortedEditions; return sortedEditions;
} }
public CardEdition getCardEdition(String setCode){
CardEdition edition = this.editions.get(setCode);
if (edition == null) // try custom editions
edition = this.customEditions.get(setCode);
return edition;
}
public PaperCard getOrLoadCommonCard(String cardName, String setCode, int artIndex, boolean foil) { public PaperCard getOrLoadCommonCard(String cardName, String setCode, int artIndex, boolean foil) {
PaperCard card = commonCards.getCard(cardName, setCode, artIndex); PaperCard card = commonCards.getCard(cardName, setCode, artIndex);
boolean isCustom = false; boolean isCustom = false;

View File

@@ -6,7 +6,6 @@ package forge.card;
*/ */
public class CardAiHints { public class CardAiHints {
private final boolean isRemovedFromAIDecks; private final boolean isRemovedFromAIDecks;
private final boolean isRemovedFromRandomDecks; private final boolean isRemovedFromRandomDecks;
private final boolean isRemovedFromNonCommanderDecks; private final boolean isRemovedFromNonCommanderDecks;
@@ -15,7 +14,6 @@ public class CardAiHints {
private final DeckHints deckNeeds; private final DeckHints deckNeeds;
private final DeckHints deckHas; private final DeckHints deckHas;
public CardAiHints(boolean remAi, boolean remRandom, boolean remUnlessCommander, DeckHints dh, DeckHints dn, DeckHints has) { public CardAiHints(boolean remAi, boolean remRandom, boolean remUnlessCommander, DeckHints dh, DeckHints dn, DeckHints has) {
isRemovedFromAIDecks = remAi; isRemovedFromAIDecks = remAi;
isRemovedFromRandomDecks = remRandom; isRemovedFromRandomDecks = remRandom;
@@ -90,5 +88,4 @@ public class CardAiHints {
} }
} }
} }

View File

@@ -533,6 +533,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override @Override
public int getPrintCount(String cardName, String edition) { public int getPrintCount(String cardName, String edition) {
int cnt = 0; int cnt = 0;
if (edition == null || cardName == null)
return cnt;
for (PaperCard pc : getAllCards(cardName)) { for (PaperCard pc : getAllCards(cardName)) {
if (pc.getEdition().equals(edition)) { if (pc.getEdition().equals(edition)) {
cnt++; cnt++;
@@ -544,6 +546,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override @Override
public int getMaxPrintCount(String cardName) { public int getMaxPrintCount(String cardName) {
int max = -1; int max = -1;
if (cardName == null)
return max;
for (PaperCard pc : getAllCards(cardName)) { for (PaperCard pc : getAllCards(cardName)) {
if (max < pc.getArtIndex()) { if (max < pc.getArtIndex()) {
max = pc.getArtIndex(); max = pc.getArtIndex();
@@ -555,6 +559,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override @Override
public int getArtCount(String cardName, String setName) { public int getArtCount(String cardName, String setName) {
int cnt = 0; int cnt = 0;
if (cardName == null || setName == null)
return cnt;
Collection<PaperCard> cards = getAllCards(cardName); Collection<PaperCard> cards = getAllCards(cardName);
if (null == cards) { if (null == cards) {
@@ -628,6 +634,23 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
})); }));
} }
public Collection<PaperCard> getAllNonPromosNonReprintsNoAlt() {
return Lists.newArrayList(Iterables.filter(getAllCardsNoAlt(), new Predicate<PaperCard>() {
@Override
public boolean apply(final PaperCard paperCard) {
CardEdition edition = null;
try {
edition = editions.getEditionByCodeOrThrow(paperCard.getEdition());
if (edition.getType() == Type.PROMOS||edition.getType() == Type.REPRINT)
return false;
} catch (Exception ex) {
return false;
}
return true;
}
}));
}
public String getName(final String cardName) { public String getName(final String cardName) {
if (alternateName.containsKey(cardName)) { if (alternateName.containsKey(cardName)) {
return alternateName.get(cardName); return alternateName.get(cardName);
@@ -749,7 +772,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
public PaperCard createUnsupportedCard(String cardName) { public PaperCard createUnsupportedCard(String cardName) {
CardRequest request = CardRequest.fromString(cardName); CardRequest request = CardRequest.fromString(cardName);
CardEdition cardEdition = CardEdition.UNKNOWN; CardEdition cardEdition = CardEdition.UNKNOWN;
CardRarity cardRarity = CardRarity.Unknown; CardRarity cardRarity = CardRarity.Unknown;
@@ -790,7 +812,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1); return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1);
} }
private final Editor editor = new Editor(); private final Editor editor = new Editor();

View File

@@ -19,7 +19,6 @@ package forge.card;
import java.io.File; import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -121,13 +120,16 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
// commonly used printsheets with collector number // commonly used printsheets with collector number
public enum EditionSectionWithCollectorNumbers { public enum EditionSectionWithCollectorNumbers {
CARDS("cards"), CARDS("cards"),
SPECIAL_SLOT("special slot"), //to help with convoluted boosters
PRECON_PRODUCT("precon product"), PRECON_PRODUCT("precon product"),
BORDERLESS("borderless"), BORDERLESS("borderless"),
SHOWCASE("showcase"), SHOWCASE("showcase"),
EXTENDED_ART("extended art"), EXTENDED_ART("extended art"),
ALTERNATE_ART("alternate art"), ALTERNATE_ART("alternate art"),
ALTERNATE_FRAME("alternate frame"),
BUY_A_BOX("buy a box"), BUY_A_BOX("buy a box"),
PROMO("promo"), PROMO("promo"),
BUNDLE("bundle"),
BOX_TOPPER("box topper"); BOX_TOPPER("box topper");
private final String name; private final String name;
@@ -148,7 +150,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
} }
} }
public static class CardInSet { public static class CardInSet implements Comparable<CardInSet> {
public final CardRarity rarity; public final CardRarity rarity;
public final String collectorNumber; public final String collectorNumber;
public final String name; public final String name;
@@ -172,6 +174,56 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
sb.append(name); sb.append(name);
return sb.toString(); return sb.toString();
} }
/**
* This method implements the main strategy to allow for natural ordering of collectorNumber
* (i.e. "1" < "10"), overloading the default lexicographic order (i.e. "10" < "1").
* Any non-numerical parts in the input collectorNumber will be also accounted for, and attached to the
* resulting sorting key, accordingly.
*
* @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
* @return A 5-digits zero-padded collector number + any non-numerical parts attached.
*/
public static String getSortableCollectorNumber(final String collectorNumber){
String sortableCollNr = collectorNumber;
if (sortableCollNr == null || sortableCollNr.length() == 0)
sortableCollNr = "50000"; // very big number of 5 digits to have them in last positions
// Now, for proper sorting, let's zero-pad the collector number (if integer)
int collNr;
try {
collNr = Integer.parseInt(sortableCollNr);
sortableCollNr = String.format("%05d", collNr);
} catch (NumberFormatException ex) {
String nonNumeric = sortableCollNr.replaceAll("[0-9]", "");
String onlyNumeric = sortableCollNr.replaceAll("[^0-9]", "");
try {
collNr = Integer.parseInt(onlyNumeric);
} catch (NumberFormatException exon) {
collNr = 0; // this is the case of ONLY-letters collector numbers
}
if ((collNr > 0) && (sortableCollNr.startsWith(onlyNumeric))) // e.g. 12a, 37+, 2018f,
sortableCollNr = String.format("%05d", collNr) + nonNumeric;
else // e.g. WS6, S1
sortableCollNr = nonNumeric + String.format("%05d", collNr);
}
return sortableCollNr;
}
@Override
public int compareTo(CardInSet o) {
final int nameCmp = name.compareToIgnoreCase(o.name);
if (0 != nameCmp) {
return nameCmp;
}
String thisCollNr = getSortableCollectorNumber(collectorNumber);
String othrCollNr = getSortableCollectorNumber(o.collectorNumber);
final int collNrCmp = thisCollNr.compareTo(othrCollNr);
if (0 != collNrCmp) {
return collNrCmp;
}
return rarity.compareTo(o.rarity);
}
} }
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
@@ -206,6 +258,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
private String[] chaosDraftThemes = new String[0]; private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, CardInSet> cardMap; private final ListMultimap<String, CardInSet> cardMap;
private final List<CardInSet> cardsInSet;
private final Map<String, Integer> tokenNormalized; private final Map<String, Integer> tokenNormalized;
// custom print sheets that will be loaded lazily // custom print sheets that will be loaded lazily
private final Map<String, List<String>> customPrintSheetsToParse; private final Map<String, List<String>> customPrintSheetsToParse;
@@ -215,13 +268,18 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) { private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) {
this.cardMap = cardMap; this.cardMap = cardMap;
this.cardsInSet = new ArrayList<>(cardMap.values());
Collections.sort(cardsInSet);
this.tokenNormalized = tokens; this.tokenNormalized = tokens;
this.customPrintSheetsToParse = customPrintSheetsToParse; this.customPrintSheetsToParse = customPrintSheetsToParse;
} }
private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) { private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) {
List<CardInSet> cardsList = Arrays.asList(cards);
this.cardMap = ArrayListMultimap.create(); this.cardMap = ArrayListMultimap.create();
this.cardMap.replaceValues("cards", Arrays.asList(cards)); this.cardMap.replaceValues("cards", cardsList);
this.cardsInSet = new ArrayList<>(cardsList);
Collections.sort(cardsInSet);
this.tokenNormalized = tokens; this.tokenNormalized = tokens;
this.customPrintSheetsToParse = new HashMap<>(); this.customPrintSheetsToParse = new HashMap<>();
} }
@@ -256,7 +314,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
date = date + "-01"; date = date + "-01";
try { try {
return formatter.parse(date); return formatter.parse(date);
} catch (ParseException e) { } catch (Exception e) {
return new Date(); return new Date();
} }
} }
@@ -287,7 +345,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
public List<CardInSet> getCards() { return cardMap.get("cards"); } public List<CardInSet> getCards() { return cardMap.get("cards"); }
public List<CardInSet> getAllCardsInSet() { public List<CardInSet> getAllCardsInSet() {
return Lists.newArrayList(cardMap.values()); return cardsInSet;
} }
public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others
@@ -306,7 +364,10 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
if (o == null) { if (o == null) {
return 1; return 1;
} }
return date.compareTo(o.date); int dateComp = date.compareTo(o.date);
if (0 != dateComp)
return dateComp;
return name.compareTo(o.name);
} }
@Override @Override
@@ -340,7 +401,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
} }
public boolean isLargeSet() { public boolean isLargeSet() {
return getAllCardsInSet().size() > 200 && !smallSetOverride; return this.cardsInSet.size() > 200 && !smallSetOverride;
} }
public int getCntBoosterPictures() { public int getCntBoosterPictures() {
@@ -414,7 +475,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
* rarity - grouping #4 * rarity - grouping #4
* name - grouping #5 * name - grouping #5
*/ */
"(^([0-9]+.?) )?(([SCURML]) )?(.*)$" "(^([0-9A-Z]+.?) )?(([SCURML]) )?(.*)$"
); );
ListMultimap<String, CardInSet> cardMap = ArrayListMultimap.create(); ListMultimap<String, CardInSet> cardMap = ArrayListMultimap.create();

View File

@@ -84,6 +84,12 @@ public final class CardRules implements ICardCharacteristics {
boolean isReminder = false; boolean isReminder = false;
boolean isSymbol = false; boolean isSymbol = false;
String oracleText = face.getOracleText(); String oracleText = face.getOracleText();
// CR 903.4 colors defined by its characteristic-defining abilities
for (String staticAbility : face.getStaticAbilities()) {
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("SetColor$ All")) {
res |= MagicColor.ALL_COLORS;
}
}
int len = oracleText.length(); int len = oracleText.length();
for(int i = 0; i < len; i++) { for(int i = 0; i < len; i++) {
char c = oracleText.charAt(i); // This is to avoid needless allocations performed by toCharArray() char c = oracleText.charAt(i); // This is to avoid needless allocations performed by toCharArray()
@@ -388,7 +394,6 @@ public final class CardRules implements ICardCharacteristics {
this.removedFromNonCommanderDecks = "NonCommander".equalsIgnoreCase(value); this.removedFromNonCommanderDecks = "NonCommander".equalsIgnoreCase(value);
} }
} else if ("AlternateMode".equals(key)) { } else if ("AlternateMode".equals(key)) {
//System.out.println(faces[curFace].getName());
this.altMode = CardSplitType.smartValueOf(value); this.altMode = CardSplitType.smartValueOf(value);
} else if ("ALTERNATE".equals(key)) { } else if ("ALTERNATE".equals(key)) {
this.curFace = 1; this.curFace = 1;
@@ -539,7 +544,6 @@ public final class CardRules implements ICardCharacteristics {
@Override @Override
public final ManaCostShard next() { public final ManaCostShard next() {
final String unparsed = st.nextToken(); final String unparsed = st.nextToken();
// System.out.println(unparsed);
if (StringUtils.isNumeric(unparsed)) { if (StringUtils.isNumeric(unparsed)) {
this.genericCost += Integer.parseInt(unparsed); this.genericCost += Integer.parseInt(unparsed);
return null; return null;
@@ -555,7 +559,7 @@ public final class CardRules implements ICardCharacteristics {
*/ */
@Override @Override
public void remove() { public void remove() {
} // unsuported } // unsupported
} }
} }

View File

@@ -30,7 +30,6 @@ public class ManaCostParser implements IParserManaCost {
*/ */
public ManaCostParser(final String cost) { public ManaCostParser(final String cost) {
this.cost = cost.split(" "); this.cost = cost.split(" ");
// System.out.println(cost);
this.nextToken = 0; this.nextToken = 0;
this.genericCost = 0; this.genericCost = 0;
} }
@@ -66,7 +65,6 @@ public class ManaCostParser implements IParserManaCost {
@Override @Override
public final ManaCostShard next() { public final ManaCostShard next() {
final String unparsed = this.cost[this.nextToken++]; final String unparsed = this.cost[this.nextToken++];
// System.out.println(unparsed);
if (StringUtils.isNumeric(unparsed)) { if (StringUtils.isNumeric(unparsed)) {
this.genericCost += Integer.parseInt(unparsed); this.genericCost += Integer.parseInt(unparsed);
return null; return null;

View File

@@ -17,10 +17,11 @@ import forge.card.MagicColor;
import forge.util.PredicateCard; import forge.util.PredicateCard;
import forge.util.PredicateString; import forge.util.PredicateString;
//import forge.Card;
public interface IPaperCard extends InventoryItem, Serializable { public interface IPaperCard extends InventoryItem, Serializable {
String NO_COLLECTOR_NUMBER = "N.A."; // Placeholder for No-Collection number available
int DEFAULT_ART_INDEX = 1;
/** /**
* Number of filters based on CardPrinted values. * Number of filters based on CardPrinted values.
*/ */
@@ -228,6 +229,7 @@ public interface IPaperCard extends InventoryItem, Serializable {
String getName(); String getName();
String getEdition(); String getEdition();
String getCollectorNumber();
int getArtIndex(); int getArtIndex();
boolean isFoil(); boolean isFoil();
boolean isToken(); boolean isToken();

View File

@@ -26,6 +26,7 @@ import com.google.common.base.Function;
import forge.ImageKeys; import forge.ImageKeys;
import forge.StaticData; import forge.StaticData;
import forge.card.CardDb; import forge.card.CardDb;
import forge.card.CardEdition;
import forge.card.CardRarity; import forge.card.CardRarity;
import forge.card.CardRules; import forge.card.CardRules;
import forge.util.CardTranslation; import forge.util.CardTranslation;
@@ -48,12 +49,19 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
// These fields are kinda PK for PrintedCard // These fields are kinda PK for PrintedCard
private final String name; private final String name;
private final String edition; private final String edition;
/* [NEW] Attribute to store reference to CollectorNumber of each PaperCard.
By default the attribute is marked as "unset" so that it could be retrieved and set.
(see getCollectorNumber())
*/
private String collectorNumber = null;
private final int artIndex; private final int artIndex;
private final boolean foil; private final boolean foil;
private Boolean hasImage; private Boolean hasImage;
// Calculated fields are below: // Calculated fields are below:
private transient CardRarity rarity; // rarity is given in ctor when set is assigned private transient CardRarity rarity; // rarity is given in ctor when set is assigned
// Reference to a new instance of Self, but foiled!
private transient PaperCard foiledVersion = null;
@Override @Override
public String getName() { public String getName() {
@@ -65,6 +73,23 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
return edition; return edition;
} }
@Override
public String getCollectorNumber() {
/* The collectorNumber attribute is managed in a property-like fashion.
By default it is marked as "unset" (-1), which integrates with all constructors
invocations not including this as an extra parameter. In this way, the new
attribute could be added to the API with minimum disruption to code
throughout the other packages.
If "unset", the corresponding collectorNumber will be retrieved
from the corresponding CardEdition (see retrieveCollectorNumber)
* */
if (collectorNumber == null) {
collectorNumber = this.retrieveCollectorNumber();
}
return collectorNumber;
}
@Override @Override
public int getArtIndex() { public int getArtIndex() {
return artIndex; return artIndex;
@@ -90,6 +115,20 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
return rarity; return rarity;
} }
/* FIXME: At the moment, every card can get Foiled, with no restriction on the
corresponding Edition - so we could Foil even Alpha cards.
*/
public PaperCard getFoiled() {
if (this.foil)
return this;
if (this.foiledVersion == null) {
this.foiledVersion = new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, true, String.valueOf(collectorNumber));
}
return this.foiledVersion;
}
// @Override // @Override
// public String getImageKey() { // public String getImageKey() {
// return getImageLocator(getImageName(), getArtIndex(), true, false); // return getImageLocator(getImageName(), getArtIndex(), true, false);
@@ -124,11 +163,16 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
} }
}; };
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0) {
this(rules0, edition0, rarity0, IPaperCard.DEFAULT_ART_INDEX, false);
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0) { public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0) {
this(rules0, edition0, rarity0, artIndex0, false); this(rules0, edition0, rarity0, artIndex0, false);
} }
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0, final boolean foil0) {
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0,
final boolean foil0) {
if (rules0 == null || edition0 == null || rarity0 == null) { if (rules0 == null || edition0 == null || rarity0 == null) {
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity"); throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
} }
@@ -140,6 +184,17 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
rarity = rarity0; rarity = rarity0;
} }
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0,
final String collectorNumber0) {
this(rules0, edition0, rarity0, artIndex0, false, collectorNumber0);
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0) {
this(rules0, edition0, rarity0, artIndex0, foil0);
collectorNumber = collectorNumber0;
}
// Want this class to be a key for HashTable // Want this class to be a key for HashTable
@Override @Override
public boolean equals(final Object obj) { public boolean equals(final Object obj) {
@@ -160,11 +215,9 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
if (!edition.equals(other.edition)) { if (!edition.equals(other.edition)) {
return false; return false;
} }
if ((other.foil != foil) || (other.artIndex != artIndex)) { if (!getCollectorNumber().equals(other.getCollectorNumber()))
return false; return false;
} return (other.foil == foil) && (other.artIndex == artIndex);
return true;
} }
/* /*
@@ -174,18 +227,15 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
*/ */
@Override @Override
public int hashCode() { public int hashCode() {
final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) + (artIndex * 2); final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) +
(artIndex * 2) + (getCollectorNumber().hashCode() * 383);
if (foil) { if (foil) {
return code + 1; return code + 1;
} }
return code; return code;
} }
/* // FIXME: Check
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override @Override
public String toString() { public String toString() {
return CardTranslation.getTranslatedName(name); return CardTranslation.getTranslatedName(name);
@@ -194,21 +244,49 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
} }
/* /*
* (non-Javadoc) * This (utility) method transform a collectorNumber String into a key string for sorting.
* This method proxies the same strategy implemented in CardEdition.CardInSet class from which the
* collectorNumber of PaperCard instances are originally retrieved.
* This is also to centralise the criterion, whilst avoiding code duplication.
* *
* @see java.lang.Comparable#compareTo(java.lang.Object) * Note: The method has been made private as this is for internal API use **only**, to allow
* for generalised comparison with IPaperCard instances (see compareTo)
*
* The public API of PaperCard includes a method (i.e. getCollectorNumberSortingKey) which applies
* this method on instance's own collector number.
*
* @return a zero-padded 5-digits String + any non-numerical content in the input String, properly attached.
*/ */
private static String makeCollectorNumberSortingKey(final String collectorNumber0){
String collectorNumber = collectorNumber0;
if (collectorNumber.equals(NO_COLLECTOR_NUMBER))
collectorNumber = null;
return CardEdition.CardInSet.getSortableCollectorNumber(collectorNumber);
}
public String getCollectorNumberSortingKey(){
// Hardly the case, but just invoke getter rather than direct
// attribute to be sure that collectorNumber has been retrieved already!
return makeCollectorNumberSortingKey(getCollectorNumber());
}
@Override @Override
public int compareTo(final IPaperCard o) { public int compareTo(final IPaperCard o) {
final int nameCmp = getName().compareToIgnoreCase(o.getName()); final int nameCmp = name.compareToIgnoreCase(o.getName());
if (0 != nameCmp) { if (0 != nameCmp) {
return nameCmp; return nameCmp;
} }
// TODO compare sets properly //FIXME: compare sets properly
int setDiff = edition.compareTo(o.getEdition()); int setDiff = edition.compareTo(o.getEdition());
if ( 0 != setDiff ) if (0 != setDiff)
return setDiff; return setDiff;
String thisCollNrKey = getCollectorNumberSortingKey();
String othrCollNrKey = makeCollectorNumberSortingKey(o.getCollectorNumber());
final int collNrCmp = thisCollNrKey.compareTo(othrCollNrKey);
if (0 != collNrCmp) {
return collNrCmp;
}
return Integer.compare(artIndex, o.getArtIndex()); return Integer.compare(artIndex, o.getArtIndex());
} }
@@ -216,8 +294,7 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
// default deserialization // default deserialization
ois.defaultReadObject(); ois.defaultReadObject();
IPaperCard pc = null; IPaperCard pc = StaticData.instance().getCommonCards().getCard(name, edition, artIndex);
pc = StaticData.instance().getCommonCards().getCard(name, edition, artIndex);
if (pc == null) { if (pc == null) {
pc = StaticData.instance().getVariantCards().getCard(name, edition, artIndex); pc = StaticData.instance().getVariantCards().getCard(name, edition, artIndex);
if (pc == null) { if (pc == null) {
@@ -228,9 +305,35 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
rarity = pc.getRarity(); rarity = pc.getRarity();
} }
private String retrieveCollectorNumber() {
StaticData data = StaticData.instance();
CardEdition edition = data.getEditions().get(this.edition);
if (edition == null) {
edition = data.getCustomEditions().get(this.edition);
if (edition == null) // don't bother continuing - non-existing card!
return NO_COLLECTOR_NUMBER;
}
int artIndexCount = 0;
String collectorNumberInEdition = "";
for (CardEdition.CardInSet card : edition.getAllCardsInSet()) {
if (card.name.equalsIgnoreCase(this.name)) {
artIndexCount += 1;
if (artIndexCount == this.artIndex) {
collectorNumberInEdition = card.collectorNumber;
break;
}
}
}
// CardEdition stores collectorNumber as a String, which is null if there isn't any.
// In this case, the NO_COLLECTOR_NUMBER value (i.e. 0) is returned.
return ((collectorNumberInEdition != null) && (collectorNumberInEdition.length() > 0)) ?
collectorNumberInEdition : NO_COLLECTOR_NUMBER;
}
@Override @Override
public String getImageKey(boolean altState) { public String getImageKey(boolean altState) {
String imageKey = ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator + edition + CardDb.NameSetSeparator + artIndex; String imageKey = ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator
+ edition + CardDb.NameSetSeparator + artIndex;
if (altState) { if (altState) {
imageKey += ImageKeys.BACKFACE_POSTFIX; imageKey += ImageKeys.BACKFACE_POSTFIX;
} }

View File

@@ -132,6 +132,12 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override public String toString() { return name; } @Override public String toString() { return name; }
@Override public String getEdition() { return edition != null ? edition.getCode() : "???"; } @Override public String getEdition() { return edition != null ? edition.getCode() : "???"; }
@Override
public String getCollectorNumber() {
return IPaperCard.NO_COLLECTOR_NUMBER;
}
@Override public int getArtIndex() { return artIndex; } @Override public int getArtIndex() { return artIndex; }
@Override public boolean isFoil() { return false; } @Override public boolean isFoil() { return false; }
@Override public CardRules getRules() { return card; } @Override public CardRules getRules() { return card; }

View File

@@ -70,12 +70,14 @@ public class BoosterGenerator {
} }
private static PaperCard generateFoilCard(PrintSheet sheet) { private static PaperCard generateFoilCard(PrintSheet sheet) {
return StaticData.instance().getCommonCards().getFoiled(sheet.random(1, true).get(0)); PaperCard randomCard = sheet.random(1, true).get(0);
return randomCard.getFoiled();
} }
private static PaperCard generateFoilCard(List<PaperCard> cardList) { private static PaperCard generateFoilCard(List<PaperCard> cardList) {
Collections.shuffle(cardList, MyRandom.getRandom()); Collections.shuffle(cardList, MyRandom.getRandom());
return StaticData.instance().getCommonCards().getFoiled(cardList.get(0)); PaperCard randomCard = cardList.get(0);
return randomCard.getFoiled();
} }
public static List<PaperCard> getBoosterPack(SealedProduct.Template template) { public static List<PaperCard> getBoosterPack(SealedProduct.Template template) {
@@ -461,7 +463,7 @@ public class BoosterGenerator {
if (toReplace != null) { if (toReplace != null) {
// Keep the foil state // Keep the foil state
if (toReplace.isFoil()) { if (toReplace.isFoil()) {
toAdd = StaticData.instance().getCommonCards().getFoiled(toAdd); toAdd = toAdd.getFoiled();
} }
booster.remove(toReplace); booster.remove(toReplace);
booster.add(toAdd); booster.add(toAdd);

View File

@@ -21,7 +21,6 @@ import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -208,7 +207,7 @@ public final class FileUtil {
if ((file == null) || !file.exists()) { if ((file == null) || !file.exists()) {
return new ArrayList<>(); return new ArrayList<>();
} }
return FileUtil.readAllLines(new FileReader(file), false); return FileUtil.readAllLines(file, false);
} catch (final Exception ex) { } catch (final Exception ex) {
throw new RuntimeException("FileUtil : readFile() error, " + ex); throw new RuntimeException("FileUtil : readFile() error, " + ex);
} }
@@ -248,6 +247,31 @@ public final class FileUtil {
} }
return list; return list;
} }
/**
* Reads all lines from given file to a list of strings.
*
* @param file is the File to read.
* @param mayTrim defines whether to trim lines.
* @return list of strings
*/
public static List<String> readAllLines(final File file, final boolean mayTrim) {
final List<String> list = new ArrayList<>();
try {
final BufferedReader in = new BufferedReader(
new InputStreamReader(new FileInputStream(file), "UTF-8"));
String line;
while ((line = in.readLine()) != null) {
if (mayTrim) {
line = line.trim();
}
list.add(line);
}
in.close();
} catch (final IOException ex) {
throw new RuntimeException("FileUtil : readAllLines() error, " + ex);
}
return list;
}
// returns a list of <name, url> pairs. if the name is not in the file, it is synthesized from the url // returns a list of <name, url> pairs. if the name is not in the file, it is synthesized from the url
public static List<Pair<String, String>> readNameUrlFile(String nameUrlFile) { public static List<Pair<String, String>> readNameUrlFile(String nameUrlFile) {

View File

@@ -118,6 +118,27 @@ public class ImageUtil {
return getImageRelativePath(cp, backFace, true, true); return getImageRelativePath(cp, backFace, true, true);
} }
public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode){
return getScryfallDownloadUrl(cp, backFace, setCode, "en");
}
public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode, String langCode){
String editionCode;
if ((setCode != null) && (setCode.length() > 0))
editionCode = setCode;
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.replace("+", "");
String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (backFace ? "&face=back" : "&face=front");
}
return String.format("%s/%s/%s?format=image&version=normal%s", editionCode, cardCollectorNumber,
langCode, faceParam);
}
public static String toMWSFilename(String in) { public static String toMWSFilename(String in) {
final StringBuilder out = new StringBuilder(); final StringBuilder out = new StringBuilder();
char c; char c;

View File

@@ -58,6 +58,14 @@ public class Localizer {
return StandardCharsets.UTF_8.name(); return StandardCharsets.UTF_8.name();
} }
public String getMessageorUseDefault(final String key, final String defaultValue, final Object... messageArguments) {
try {
return getMessage(key, messageArguments);
} catch (Exception e) {
return defaultValue;
}
}
//FIXME: localizer should return default value from english locale or it will crash some GUI element like the NewGameMenu->NewGameScreen Popup when returned null...
public String getMessage(final String key, final Object... messageArguments) { public String getMessage(final String key, final Object... messageArguments) {
MessageFormat formatter = null; MessageFormat formatter = null;

View File

@@ -162,6 +162,9 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
* @return a boolean. * @return a boolean.
*/ */
public boolean matchesValid(final Object o, final String[] valids, final Card srcCard) { public boolean matchesValid(final Object o, final String[] valids, final Card srcCard) {
if (srcCard == null) {
return false;
}
return matchesValid(o, valids, srcCard, srcCard.getController()); return matchesValid(o, valids, srcCard, srcCard.getController());
} }
@@ -188,13 +191,17 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
return matchesValid(o, valids, getHostCard()); return matchesValid(o, valids, getHostCard());
} }
public boolean matchesValidParam(String param, final Object o) { public boolean matchesValidParam(String param, final Object o, final Card srcCard) {
if (hasParam(param) && !matchesValid(o, getParam(param).split(","))) { if (hasParam(param) && !matchesValid(o, getParam(param).split(","), srcCard)) {
return false; return false;
} }
return true; return true;
} }
public boolean matchesValidParam(String param, final Object o) {
return matchesValidParam(param, o, getHostCard());
}
/** /**
* Sets the suppressed. * Sets the suppressed.
* *
@@ -341,7 +348,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
} }
list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), this); list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), this);
final String rightString = presentCompare.substring(2); final String rightString = presentCompare.substring(2);
int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this); int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this);
final int left = list.size(); final int left = list.size();
@@ -434,7 +440,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
} }
if (params.containsKey("WerewolfTransformCondition")) { if (params.containsKey("WerewolfTransformCondition")) {
if (!CardUtil.getLastTurnCast("Card", this.getHostCard()).isEmpty()) { if (!CardUtil.getLastTurnCast("Card", this.getHostCard(), this).isEmpty()) {
return false; return false;
} }
} }

View File

@@ -756,7 +756,7 @@ public class Game {
// Rule 800.4 Losing a Multiplayer game // Rule 800.4 Losing a Multiplayer game
CardCollectionView cards = this.getCardsInGame(); CardCollectionView cards = this.getCardsInGame();
boolean planarControllerLost = false; boolean planarControllerLost = false;
boolean isMultiplayer = this.getPlayers().size() > 2; boolean isMultiplayer = getPlayers().size() > 2;
// 702.142f & 707.9 // 702.142f & 707.9
// If a player leaves the game, all face-down cards that player owns must be revealed to all players. // If a player leaves the game, all face-down cards that player owns must be revealed to all players.
@@ -772,7 +772,7 @@ public class Game {
for (Card c : cards) { for (Card c : cards) {
// CR 800.4d if card is controlled by opponent, LTB should trigger // CR 800.4d if card is controlled by opponent, LTB should trigger
if (c.getOwner().equals(p) && c.getController().equals(p)) { if (c.getOwner().equals(p) && c.getController().equals(p)) {
c.getCurrentState().clearTriggers(); c.getGame().getTriggerHandler().clearActiveTriggers(c, null);
} }
} }
@@ -793,16 +793,16 @@ public class Game {
} }
getAction().ceaseToExist(c, false); getAction().ceaseToExist(c, false);
// CR 603.2f owner of trigger source lost game // CR 603.2f owner of trigger source lost game
triggerHandler.clearDelayedTrigger(c); getTriggerHandler().clearDelayedTrigger(c);
} else { } else {
// return stolen permanents // return stolen permanents
if (c.getController().equals(p) && c.isInZone(ZoneType.Battlefield)) { if ((c.getController().equals(p) || c.getZone().getPlayer().equals(p)) && c.isInZone(ZoneType.Battlefield)) {
c.removeTempController(p); c.removeTempController(p);
getAction().controllerChangeZoneCorrection(c); getAction().controllerChangeZoneCorrection(c);
} }
c.removeTempController(p); c.removeTempController(p);
if (c.getController().equals(p)) { if (c.getController().equals(p)) {
this.getAction().exile(c, null); getAction().exile(c, null);
} }
} }
} else { } else {
@@ -830,7 +830,7 @@ public class Game {
} }
// Remove leftover items from // Remove leftover items from
this.getStack().removeInstancesControlledBy(p); getStack().removeInstancesControlledBy(p);
getTriggerHandler().onPlayerLost(p); getTriggerHandler().onPlayerLost(p);
@@ -1054,7 +1054,8 @@ public class Game {
public void onCleanupPhase() { public void onCleanupPhase() {
clearCounterAddedThisTurn(); clearCounterAddedThisTurn();
for (Player player : getPlayers()) { // some cards need this info updated even after a player lost, so don't skip them
for (Player player : getRegisteredPlayers()) {
player.onCleanupPhase(); player.onCleanupPhase();
} }
} }

View File

@@ -47,6 +47,7 @@ import forge.game.ability.effects.AttachEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardDamageMap;
import forge.game.card.CardFactory; import forge.game.card.CardFactory;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
@@ -61,6 +62,7 @@ import forge.game.event.GameEventCardTapped;
import forge.game.event.GameEventFlipCoin; import forge.game.event.GameEventFlipCoin;
import forge.game.event.GameEventGameStarted; import forge.game.event.GameEventGameStarted;
import forge.game.event.GameEventScry; import forge.game.event.GameEventScry;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.mulligan.MulliganService; import forge.game.mulligan.MulliganService;
import forge.game.player.GameLossReason; import forge.game.player.GameLossReason;
@@ -144,8 +146,8 @@ public class GameAction {
boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield); boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield);
boolean wasFacedown = c.isFaceDown(); boolean wasFacedown = c.isFaceDown();
//Rule 110.5g: A token that has left the battlefield can't move to another zone // Rule 111.8: A token that has left the battlefield can't move to another zone
if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Stack)) { if (!c.isSpell() && c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Stack)) {
return c; return c;
} }
@@ -241,7 +243,7 @@ public class GameAction {
copied = CardFactory.copyCard(c, false); copied = CardFactory.copyCard(c, false);
if (zoneTo.is(ZoneType.Stack)) { if (zoneTo.is(ZoneType.Stack)) {
// when moving to stack, copy changed card infomation // when moving to stack, copy changed card information
copied.setChangedCardColors(c.getChangedCardColors()); copied.setChangedCardColors(c.getChangedCardColors());
copied.setChangedCardKeywords(c.getChangedCardKeywords()); copied.setChangedCardKeywords(c.getChangedCardKeywords());
copied.setChangedCardTypes(c.getChangedCardTypesMap()); copied.setChangedCardTypes(c.getChangedCardTypesMap());
@@ -342,6 +344,7 @@ public class GameAction {
c.updateStateForView(); c.updateStateForView();
} }
} }
return c; return c;
} }
} }
@@ -371,7 +374,7 @@ public class GameAction {
if (saTargeting != null) { if (saTargeting != null) {
saTargeting.getTargets().replaceTargetCard(c, cards); saTargeting.getTargets().replaceTargetCard(c, cards);
} }
// Replace host rememberd cards // Replace host remembered cards
// But not replace RememberLKI, since it wants to refer to the last known info. // But not replace RememberLKI, since it wants to refer to the last known info.
Card hostCard = cause.getHostCard(); Card hostCard = cause.getHostCard();
if (!cause.hasParam("RememberLKI") && hostCard.isRemembered(c)) { if (!cause.hasParam("RememberLKI") && hostCard.isRemembered(c)) {
@@ -438,7 +441,7 @@ public class GameAction {
} }
if (mergedCards != null) { if (mergedCards != null) {
// Move components of merged permanet here // Move components of merged permanent here
// Also handle 721.3e and 903.9a // Also handle 721.3e and 903.9a
boolean wasToken = c.isToken(); boolean wasToken = c.isToken();
if (commanderEffect != null) { if (commanderEffect != null) {
@@ -938,7 +941,10 @@ public class GameAction {
game.getCombat().removeFromCombat(c); game.getCombat().removeFromCombat(c);
game.getCombat().saveLKI(lki); game.getCombat().saveLKI(lki);
} }
// again, make sure no triggers run from cards leaving controlled by loser
if (!lki.getController().equals(lki.getOwner())) {
game.getTriggerHandler().registerActiveLTBTrigger(lki); game.getTriggerHandler().registerActiveLTBTrigger(lki);
}
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.CardLKI, lki); runParams.put(AbilityKey.CardLKI, lki);
runParams.put(AbilityKey.Origin, c.getZone().getZoneType().name()); runParams.put(AbilityKey.Origin, c.getZone().getZoneType().name());
@@ -1352,8 +1358,7 @@ public class GameAction {
if (c.getCounters(CounterEnumType.LORE) < c.getFinalChapterNr()) { if (c.getCounters(CounterEnumType.LORE) < c.getFinalChapterNr()) {
return false; return false;
} }
if (!game.getStack().hasSimultaneousStackEntries() && if (!game.getStack().hasSourceOnStack(c, SpellAbilityPredicates.isChapter())) {
!game.getStack().hasSourceOnStack(c, SpellAbilityPredicates.isChapter())) {
sacrifice(c, null, table); sacrifice(c, null, table);
checkAgain = true; checkAgain = true;
} }
@@ -1680,6 +1685,8 @@ public class GameAction {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.Causer, activator); runParams.put(AbilityKey.Causer, activator);
game.getTriggerHandler().runTrigger(TriggerType.Destroyed, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.Destroyed, runParams, false);
// in case the destroyed card has such a trigger
game.getTriggerHandler().registerActiveLTBTrigger(c);
final Card sacrificed = sacrificeDestroy(c, sa, table); final Card sacrificed = sacrificeDestroy(c, sa, table);
return sacrificed != null; return sacrificed != null;
@@ -1695,7 +1702,7 @@ public class GameAction {
} }
final Card newCard = moveToGraveyard(c, cause, null); final Card newCard = moveToGraveyard(c, cause, null);
if (table != null) { if (table != null && newCard != null && newCard.getZone() != null) {
table.put(ZoneType.Battlefield, newCard.getZone().getZoneType(), newCard); table.put(ZoneType.Battlefield, newCard.getZone().getZoneType(), newCard);
} }
@@ -2121,4 +2128,44 @@ public class GameAction {
} }
} }
} }
public void dealDamage(final boolean isCombat, final CardDamageMap damageMap, final CardDamageMap preventMap,
final GameEntityCounterTable counterTable, final SpellAbility cause) {
// Clear assigned damage if is combat
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
final GameEntity ge = et.getKey();
if (isCombat && ge instanceof Card) {
((Card) ge).clearAssignedDamage();
}
}
// Run replacement effect for each entity dealt damage
game.getReplacementHandler().runReplaceDamage(isCombat, damageMap, preventMap, counterTable, cause);
// Actually deal damage according to replaced damage map
for (Map.Entry<Card, Map<GameEntity, Integer>> et : damageMap.rowMap().entrySet()) {
final Card sourceLKI = et.getKey();
int sum = 0;
for (Map.Entry<GameEntity, Integer> e : et.getValue().entrySet()) {
if (e.getValue() <= 0) {
continue;
}
sum += e.getValue();
e.getKey().addDamageAfterPrevention(e.getValue(), sourceLKI, isCombat, counterTable);
}
if (sourceLKI.hasKeyword(Keyword.LIFELINK)) {
sourceLKI.getController().gainLife(sum, sourceLKI, cause);
}
}
preventMap.triggerPreventDamage(isCombat);
preventMap.clear();
damageMap.triggerDamageDoneOnce(isCombat, game);
damageMap.clear();
counterTable.triggerCountersPutAll(game);
counterTable.clear();
}
} }

View File

@@ -23,11 +23,11 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardDamageMap;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
@@ -35,19 +35,17 @@ import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment; import forge.game.event.GameEventCardAttachment;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
public abstract class GameEntity extends GameObject implements IIdentifiable { public abstract class GameEntity extends GameObject implements IIdentifiable {
protected final int id; protected final int id;
private String name = ""; private String name = "";
private int preventNextDamage = 0;
protected CardCollection attachedCards = new CardCollection(); protected CardCollection attachedCards = new CardCollection();
private Map<Card, Map<String, String>> preventionShieldsWithEffects = Maps.newTreeMap();
protected Map<CounterType, Integer> counters = Maps.newHashMap(); protected Map<CounterType, Integer> counters = Maps.newHashMap();
protected GameEntity(int id0) { protected GameEntity(int id0) {
@@ -67,216 +65,61 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
getView().updateName(this); getView().updateName(this);
} }
public final int addDamage(final int damage, final Card source, boolean isCombat, boolean noPrevention, // This function handles damage after replacement and prevention effects are applied
final CardDamageMap damageMap, final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) { public abstract int addDamageAfterPrevention(final int damage, final Card source, final boolean isCombat, GameEntityCounterTable counterTable);
if (noPrevention) {
return addDamageWithoutPrevention(damage, source, damageMap, preventMap, counterTable, cause); // This should be also usable by the AI to forecast an effect (so it must
} else if (isCombat) { // not change the game state)
return addCombatDamage(damage, source, damageMap, preventMap, counterTable); public int staticDamagePrevention(final int damage, final int possiblePrevention, final Card source, final boolean isCombat) {
} else { if (damage <= 0) {
return addDamage(damage, source, damageMap, preventMap, counterTable, cause); return 0;
} }
} if (!source.canDamagePrevented(isCombat)) {
public int addDamage(final int damage, final Card source, final CardDamageMap damageMap,
final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) {
int damageToDo = damage;
damageToDo = replaceDamage(damageToDo, source, false, true, damageMap, preventMap, counterTable, cause);
damageToDo = preventDamage(damageToDo, source, false, preventMap, cause);
return addDamageAfterPrevention(damageToDo, source, false, damageMap, counterTable);
}
public final int addCombatDamage(final int damage, final Card source, final CardDamageMap damageMap,
final CardDamageMap preventMap, GameEntityCounterTable counterTable) {
int damageToDo = damage;
damageToDo = replaceDamage(damageToDo, source, true, true, damageMap, preventMap, counterTable, null);
damageToDo = preventDamage(damageToDo, source, true, preventMap, null);
if (damageToDo > 0) {
source.getDamageHistory().registerCombatDamage(this);
}
// damage prevention is already checked
return addCombatDamageBase(damageToDo, source, damageMap, counterTable);
}
protected int addCombatDamageBase(final int damage, final Card source, CardDamageMap damageMap, GameEntityCounterTable counterTable) {
return addDamageAfterPrevention(damage, source, true, damageMap, counterTable);
}
public int addDamageWithoutPrevention(final int damage, final Card source, final CardDamageMap damageMap,
final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) {
int damageToDo = replaceDamage(damage, source, false, false, damageMap, preventMap, counterTable, cause);
return addDamageAfterPrevention(damageToDo, source, false, damageMap, counterTable);
}
public int replaceDamage(final int damage, final Card source, final boolean isCombat, final boolean prevention,
final CardDamageMap damageMap, final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) {
// Replacement effects
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.DamageSource, source);
repParams.put(AbilityKey.DamageAmount, damage);
repParams.put(AbilityKey.IsCombat, isCombat);
repParams.put(AbilityKey.NoPreventDamage, !prevention);
repParams.put(AbilityKey.DamageMap, damageMap);
repParams.put(AbilityKey.PreventMap, preventMap);
repParams.put(AbilityKey.CounterTable, counterTable);
if (cause != null) {
repParams.put(AbilityKey.Cause, cause);
}
switch (getGame().getReplacementHandler().run(ReplacementType.DamageDone, repParams)) {
case NotReplaced:
return damage; return damage;
case Updated:
int newDamage = (int) repParams.get(AbilityKey.DamageAmount);
GameEntity newTarget = (GameEntity) repParams.get(AbilityKey.Affected);
// check if this is still the affected card or player
if (this.equals(newTarget)) {
return newDamage;
} else {
if (prevention) {
newDamage = newTarget.preventDamage(newDamage, source, isCombat, preventMap, cause);
} }
newTarget.addDamageAfterPrevention(newDamage, source, isCombat, damageMap, counterTable);
if (isCombat && getGame().getReplacementHandler().isPreventCombatDamageThisTurn()) {
return 0;
}
for (final Card ca : getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final ReplacementEffect re : ca.getReplacementEffects()) {
if (!re.getMode().equals(ReplacementType.DamageDone) ||
(!re.hasParam("PreventionEffect") && !re.hasParam("Prevent"))) {
continue;
}
if (!re.matchesValidParam("ValidSource", source)) {
continue;
}
if (!re.matchesValidParam("ValidTarget", this)) {
continue;
}
if (re.hasParam("IsCombat")) {
if (re.getParam("IsCombat").equals("True") != isCombat) {
continue;
}
}
if (re.hasParam("Prevent")) {
return 0;
} else if (re.getOverridingAbility() != null) {
SpellAbility repSA = re.getOverridingAbility();
if (repSA.getApi() == ApiType.ReplaceDamage) {
return Math.max(0, damage - AbilityUtils.calculateAmount(ca, repSA.getParam("Amount"), repSA));
}
} }
default:
return 0; return 0;
} }
} }
// This function handles damage after replacement and prevention effects are applied return Math.max(0, damage - possiblePrevention);
public abstract int addDamageAfterPrevention(final int damage, final Card source, final boolean isCombat, CardDamageMap damageMap, GameEntityCounterTable counterTable); }
// This should be also usable by the AI to forecast an effect (so it must
// not change the game state)
public abstract int staticDamagePrevention(final int damage, final Card source, final boolean isCombat, final boolean isTest);
// This should be also usable by the AI to forecast an effect (so it must // This should be also usable by the AI to forecast an effect (so it must
// not change the game state) // not change the game state)
public abstract int staticReplaceDamage(final int damage, final Card source, final boolean isCombat); public abstract int staticReplaceDamage(final int damage, final Card source, final boolean isCombat);
public final int preventDamage(
final int damage, final Card source, final boolean isCombat, CardDamageMap preventMap,
final SpellAbility cause) {
if (!source.canDamagePrevented(isCombat)) {
return damage;
}
int restDamage = damage;
// first try to replace the damage
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.DamageSource, source);
repParams.put(AbilityKey.DamageAmount, damage);
repParams.put(AbilityKey.IsCombat, isCombat);
repParams.put(AbilityKey.Prevention, true);
repParams.put(AbilityKey.PreventMap, preventMap);
if (cause != null) {
repParams.put(AbilityKey.Cause, cause);
}
switch (getGame().getReplacementHandler().run(ReplacementType.DamageDone, repParams)) {
case NotReplaced:
restDamage = damage;
break;
case Updated:
restDamage = (int) repParams.get(AbilityKey.DamageAmount);
break;
default:
restDamage = 0;
}
// then apply static Damage Prevention effects
restDamage = staticDamagePrevention(restDamage, source, isCombat, false);
// then apply ShieldEffects with Special Effect
restDamage = preventShieldEffect(restDamage);
// then do Shield with only number
if (restDamage <= 0) {
restDamage = 0;
} else if (restDamage >= getPreventNextDamage()) {
restDamage = restDamage - getPreventNextDamage();
setPreventNextDamage(0);
} else {
setPreventNextDamage(getPreventNextDamage() - restDamage);
restDamage = 0;
}
// if damage is greater than restDamage, damage was prevented
if (damage > restDamage) {
int prevent = damage - restDamage;
preventMap.put(source, this, damage - restDamage);
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.DamageTarget, this);
runParams.put(AbilityKey.DamageAmount, prevent);
runParams.put(AbilityKey.DamageSource, source);
runParams.put(AbilityKey.IsCombatDamage, isCombat);
getGame().getTriggerHandler().runTrigger(TriggerType.DamagePrevented, runParams, false);
}
return restDamage;
}
protected abstract int preventShieldEffect(final int damage);
public int getPreventNextDamage() {
return preventNextDamage;
}
public void setPreventNextDamage(final int n) {
preventNextDamage = n;
}
public void addPreventNextDamage(final int n) {
preventNextDamage += n;
}
public void subtractPreventNextDamage(final int n) {
preventNextDamage -= n;
}
public void resetPreventNextDamage() {
preventNextDamage = 0;
}
// PreventNextDamageWithEffect
public Map<Card, Map<String, String>> getPreventNextDamageWithEffect() {
return preventionShieldsWithEffects;
}
public int getPreventNextDamageTotalShields() { public int getPreventNextDamageTotalShields() {
int shields = preventNextDamage; return getGame().getReplacementHandler().getTotalPreventionShieldAmount(this);
for (final Map<String, String> value : preventionShieldsWithEffects.values()) {
shields += Integer.valueOf(value.get("ShieldAmount"));
}
return shields;
}
/**
* Adds a damage prevention shield with an effect that happens at time of prevention.
* @param shieldSource - The source card which generated the shield
* @param effectMap - A map of the effect occurring with the damage prevention
*/
public void addPreventNextDamageWithEffect(final Card shieldSource, Map<String, String> effectMap) {
if (preventionShieldsWithEffects.containsKey(shieldSource)) {
int currentShields = Integer.valueOf(preventionShieldsWithEffects.get(shieldSource).get("ShieldAmount"));
currentShields += Integer.valueOf(effectMap.get("ShieldAmount"));
effectMap.put("ShieldAmount", Integer.toString(currentShields));
preventionShieldsWithEffects.put(shieldSource, effectMap);
} else {
preventionShieldsWithEffects.put(shieldSource, effectMap);
}
}
public void subtractPreventNextDamageWithEffect(final Card shieldSource, final int n) {
int currentShields = Integer.valueOf(preventionShieldsWithEffects.get(shieldSource).get("ShieldAmount"));
if (currentShields > n) {
preventionShieldsWithEffects.get(shieldSource).put("ShieldAmount", String.valueOf(currentShields - n));
} else {
preventionShieldsWithEffects.remove(shieldSource);
}
}
public void resetPreventNextDamageWithEffect() {
preventionShieldsWithEffects.clear();
} }
public abstract boolean hasKeyword(final String keyword); public abstract boolean hasKeyword(final String keyword);

View File

@@ -20,6 +20,13 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
private Table<Optional<Player>, GameEntity, Map<CounterType, Integer>> dataMap = HashBasedTable.create(); private Table<Optional<Player>, GameEntity, Map<CounterType, Integer>> dataMap = HashBasedTable.create();
public GameEntityCounterTable() {
}
public GameEntityCounterTable(Table<Optional<Player>, GameEntity, Map<CounterType, Integer>> counterTable) {
putAll(counterTable);
}
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see com.google.common.collect.ForwardingTable#delegate() * @see com.google.common.collect.ForwardingTable#delegate()

View File

@@ -101,9 +101,10 @@ public class GameFormat implements Comparable<GameFormat> {
this.effectiveDate = effectiveDate; this.effectiveDate = effectiveDate;
if(sets != null) { if(sets != null) {
StaticData data = StaticData.instance();
Set<String> parsedSets = new HashSet<>(); Set<String> parsedSets = new HashSet<>();
for (String set : sets) { for (String set : sets) {
if (StaticData.instance().getEditions().get(set) == null) { if (data.getCardEdition(set) == null) {
System.out.println("Set " + set + " in format " + fName + " does not match any valid editions!"); System.out.println("Set " + set + " in format " + fName + " does not match any valid editions!");
continue; continue;
} }

View File

@@ -240,7 +240,7 @@ public class StaticEffect {
// remove keywords // remove keywords
// (Although nothing uses it at this time) // (Although nothing uses it at this time)
if (hasParam("AddKeyword") || hasParam("RemoveKeyword") if (hasParam("AddKeyword") || hasParam("RemoveKeyword")
|| hasParam("RemoveAllAbilities")) { || hasParam("RemoveAllAbilities") || hasParam("RemoveLandTypes")) {
affectedCard.removeChangedCardKeywords(getTimestamp()); affectedCard.removeChangedCardKeywords(getTimestamp());
} }

View File

@@ -57,6 +57,7 @@ public enum AbilityKey {
DefendingPlayer("DefendingPlayer"), DefendingPlayer("DefendingPlayer"),
Destination("Destination"), Destination("Destination"),
Devoured("Devoured"), Devoured("Devoured"),
DividedShieldAmount("DividedShieldAmount"),
EchoPaid("EchoPaid"), EchoPaid("EchoPaid"),
EffectOnly("EffectOnly"), EffectOnly("EffectOnly"),
Exploited("Exploited"), Exploited("Exploited"),
@@ -96,12 +97,14 @@ public enum AbilityKey {
PayingMana("PayingMana"), PayingMana("PayingMana"),
Phase("Phase"), Phase("Phase"),
Player("Player"), Player("Player"),
PreventedAmount("PreventedAmount"),
PreventMap("PreventMap"), PreventMap("PreventMap"),
Prevention("Prevention"), Prevention("Prevention"),
Produced("Produced"), Produced("Produced"),
Regeneration("Regeneration"), Regeneration("Regeneration"),
ReplacementEffect("ReplacementEffect"), ReplacementEffect("ReplacementEffect"),
ReplacementResult("ReplacementResult"), ReplacementResult("ReplacementResult"),
ReplacementResultMap("ReplacementResultMap"),
Result("Result"), Result("Result"),
Scheme("Scheme"), Scheme("Scheme"),
Source("Source"), Source("Source"),

File diff suppressed because it is too large Load Diff

View File

@@ -97,6 +97,7 @@ public enum ApiType {
LookAt (LookAtEffect.class), LookAt (LookAtEffect.class),
LoseLife (LifeLoseEffect.class), LoseLife (LifeLoseEffect.class),
LosesGame (GameLossEffect.class), LosesGame (GameLossEffect.class),
MakeCard (MakeCardEffect.class),
Mana (ManaEffect.class), Mana (ManaEffect.class),
ManaReflected (ManaReflectedEffect.class), ManaReflected (ManaReflectedEffect.class),
Manifest (ManifestEffect.class), Manifest (ManifestEffect.class),

View File

@@ -21,9 +21,10 @@ import forge.game.GameObject;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil; import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable; import forge.game.card.CardZoneTable;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
@@ -55,7 +56,7 @@ public abstract class SpellAbilityEffect {
public abstract void resolve(SpellAbility sa); public abstract void resolve(SpellAbility sa);
protected String getStackDescription(final SpellAbility sa) { protected String getStackDescription(final SpellAbility sa) {
// Unless overriden, let the spell description also be the stack description // Unless overridden, let the spell description also be the stack description
return sa.getDescription(); return sa.getDescription();
} }
@@ -123,7 +124,7 @@ public abstract class SpellAbilityEffect {
if (sa.hasParam("Announce")) { if (sa.hasParam("Announce")) {
String svar = sa.getParam("Announce"); String svar = sa.getParam("Announce");
int amount = CardFactoryUtil.xCount(sa.getHostCard(), sa.getSVar(svar)); int amount = AbilityUtils.calculateAmount(sa.getHostCard(), svar, sa);
sb.append(" "); sb.append(" ");
sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace(svar,"=",String.valueOf(amount)))); sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace(svar,"=",String.valueOf(amount))));
} else{ } else{
@@ -198,7 +199,7 @@ public abstract class SpellAbilityEffect {
// Players // Players
protected final static PlayerCollection getTargetPlayers(final SpellAbility sa) { return getPlayers(false, "Defined", sa); } protected final static PlayerCollection getTargetPlayers(final SpellAbility sa) { return getPlayers(false, "Defined", sa); }
protected final static PlayerCollection getTargetPlayers(final SpellAbility sa, final String definedParam) { return getPlayers(false, definedParam, sa); } protected final static PlayerCollection getTargetPlayers(final SpellAbility sa, final String definedParam) { return getPlayers(false, definedParam, sa); }
protected final static PlayerCollection getDefinedPlayersOrTargeted(final SpellAbility sa ) { return getPlayers(true, "Defined", sa); } protected final static PlayerCollection getDefinedPlayersOrTargeted(final SpellAbility sa) { return getPlayers(true, "Defined", sa); }
protected final static PlayerCollection getDefinedPlayersOrTargeted(final SpellAbility sa, final String definedParam) { return getPlayers(true, definedParam, sa); } protected final static PlayerCollection getDefinedPlayersOrTargeted(final SpellAbility sa, final String definedParam) { return getPlayers(true, definedParam, sa); }
private static PlayerCollection getPlayers(final boolean definedFirst, final String definedParam, final SpellAbility sa) { private static PlayerCollection getPlayers(final boolean definedFirst, final String definedParam, final SpellAbility sa) {
@@ -218,7 +219,6 @@ public abstract class SpellAbilityEffect {
: AbilityUtils.getDefinedSpellAbilities(sa.getHostCard(), sa.getParam(definedParam), sa); : AbilityUtils.getDefinedSpellAbilities(sa.getHostCard(), sa.getParam(definedParam), sa);
} }
// Targets of card or player type // Targets of card or player type
protected final static List<GameEntity> getTargetEntities(final SpellAbility sa) { return getEntities(false, "Defined", sa); } protected final static List<GameEntity> getTargetEntities(final SpellAbility sa) { return getEntities(false, "Defined", sa); }
protected final static List<GameEntity> getTargetEntities(final SpellAbility sa, final String definedParam) { return getEntities(false, definedParam, sa); } protected final static List<GameEntity> getTargetEntities(final SpellAbility sa, final String definedParam) { return getEntities(false, definedParam, sa); }
@@ -297,7 +297,7 @@ public abstract class SpellAbilityEffect {
} }
delTrig.append("| TriggerDescription$ ").append(desc); delTrig.append("| TriggerDescription$ ").append(desc);
final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), sa.getHostCard(), intrinsic); final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), CardUtil.getLKICopy(sa.getHostCard()), intrinsic);
for (final Card c : crds) { for (final Card c : crds) {
trig.addRemembered(c); trig.addRemembered(c);
@@ -576,6 +576,8 @@ public abstract class SpellAbilityEffect {
GameEntity defender = null; GameEntity defender = null;
FCollection<GameEntity> defs = null; FCollection<GameEntity> defs = null;
// important to update defenders here, maybe some PW got removed
combat.initConstraints();
if ("True".equalsIgnoreCase(attacking)) { if ("True".equalsIgnoreCase(attacking)) {
defs = (FCollection<GameEntity>) combat.getDefenders(); defs = (FCollection<GameEntity>) combat.getDefenders();
} else if (sa.hasParam("ChoosePlayerOrPlaneswalker")) { } else if (sa.hasParam("ChoosePlayerOrPlaneswalker")) {
@@ -697,4 +699,52 @@ public abstract class SpellAbilityEffect {
} }
} }
} }
protected static void addUntilCommand(final SpellAbility sa, GameCommand until) {
Card host = sa.getHostCard();
final Game game = host.getGame();
final String duration = sa.getParam("Duration");
// in case host was LKI
if (host.isLKI()) {
host = game.getCardState(host);
}
if ("UntilEndOfCombat".equals(duration)) {
game.getEndOfCombat().addUntil(until);
} else if ("UntilYourNextUpkeep".equals(duration)) {
game.getUpkeep().addUntil(sa.getActivatingPlayer(), until);
} else if ("UntilTheEndOfYourNextUpkeep".equals(duration)) {
if (game.getPhaseHandler().is(PhaseType.UPKEEP)) {
game.getUpkeep().registerUntilEnd(host.getController(), until);
} else {
game.getUpkeep().addUntilEnd(host.getController(), until);
}
} else if ("UntilTheEndOfYourNextTurn".equals(duration)) {
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
game.getEndOfTurn().registerUntilEnd(sa.getActivatingPlayer(), until);
} else {
game.getEndOfTurn().addUntilEnd(sa.getActivatingPlayer(), until);
}
} else if (duration != null && duration.startsWith("UntilAPlayerCastSpell")) {
game.getStack().addCastCommand(duration.split(" ")[1], until);
} else if ("UntilHostLeavesPlay".equals(duration)) {
host.addLeavesPlayCommand(until);
} else if ("UntilHostLeavesPlayOrEOT".equals(duration)) {
host.addLeavesPlayCommand(until);
game.getEndOfTurn().addUntil(until);
} else if ("UntilLoseControlOfHost".equals(duration)) {
host.addLeavesPlayCommand(until);
host.addChangeControllerCommand(until);
} else if ("UntilYourNextTurn".equals(duration)) {
game.getCleanup().addUntil(sa.getActivatingPlayer(), until);
} else if ("UntilUntaps".equals(duration)) {
host.addUntapCommand(until);
} else if ("UntilUnattached".equals(duration)) {
sa.getHostCard().addUnattachCommand(until);
} else if ("UntilFacedown".equals(duration)) {
sa.getHostCard().addFacedownCommand(until);
}else {
game.getEndOfTurn().addUntil(until);
}
}
} }

View File

@@ -16,13 +16,11 @@ public class AddTurnEffect extends SpellAbilityEffect {
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
final int numTurns = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumTurns"), sa); final int numTurns = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumTurns"), sa);
List<Player> tgtPlayers = getTargetPlayers(sa); List<Player> tgtPlayers = getTargetPlayers(sa);
for (final Player player : tgtPlayers) { for (final Player player : tgtPlayers) {
sb.append(player).append(" "); sb.append(player).append(" ");
} }

View File

@@ -43,7 +43,7 @@ public class AnimateAllEffect extends AnimateEffectBase {
// Every Animate event needs a unique time stamp // Every Animate event needs a unique time stamp
final long timestamp = game.getNextTimestamp(); final long timestamp = game.getNextTimestamp();
final boolean permanent = sa.hasParam("Permanent"); final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final CardType types = new CardType(true); final CardType types = new CardType(true);
if (sa.hasParam("Types")) { if (sa.hasParam("Types")) {
@@ -158,13 +158,7 @@ public class AnimateAllEffect extends AnimateEffectBase {
}; };
if (!permanent) { if (!permanent) {
if (sa.hasParam("UntilEndOfCombat")) { addUntilCommand(sa, unanimate);
game.getEndOfCombat().addUntil(unanimate);
} else if (sa.hasParam("UntilYourNextTurn")) {
game.getCleanup().addUntil(host.getController(), unanimate);
} else {
game.getEndOfTurn().addUntil(unanimate);
}
} }
} }
} // animateAllResolve } // animateAllResolve

View File

@@ -29,8 +29,8 @@ public class AnimateEffect extends AnimateEffectBase {
String animateImprinted = null; String animateImprinted = null;
//if host is not on the battlefield don't apply //if host is not on the battlefield don't apply
if ((sa.hasParam("UntilHostLeavesPlay") || sa.hasParam("UntilLoseControlOfHost")) if (("UntilHostLeavesPlay".equals(sa.getParam("Duration")) || "UntilLoseControlOfHost".equals(sa.getParam("Duration")))
&& !sa.getHostCard().isInPlay()) { && !source.isInPlay()) {
return; return;
} }
@@ -216,7 +216,7 @@ public class AnimateEffect extends AnimateEffectBase {
toughness = AbilityUtils.calculateAmount(host, sa.getParam("Toughness"), sa); toughness = AbilityUtils.calculateAmount(host, sa.getParam("Toughness"), sa);
} }
final boolean permanent = sa.hasParam("Permanent"); final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final List<String> types = Lists.newArrayList(); final List<String> types = Lists.newArrayList();
if (sa.hasParam("Types")) { if (sa.hasParam("Types")) {
types.addAll(Arrays.asList(sa.getParam("Types").split(","))); types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
@@ -296,15 +296,16 @@ public class AnimateEffect extends AnimateEffectBase {
// sb.append(abilities) // sb.append(abilities)
// sb.append(triggers) // sb.append(triggers)
if (!permanent) { if (!permanent) {
if (sa.hasParam("UntilEndOfCombat")) { final String duration = sa.getParam("Duration");
if ("UntilEndOfCombat".equals(duration)) {
sb.append(" until end of combat."); sb.append(" until end of combat.");
} else if (sa.hasParam("UntilHostLeavesPlay")) { } else if ("UntilHostLeavesPlay".equals(duration)) {
sb.append(" until ").append(host).append(" leaves the battlefield."); sb.append(" until ").append(host).append(" leaves the battlefield.");
} else if (sa.hasParam("UntilYourNextUpkeep")) { } else if ("UntilYourNextUpkeep".equals(duration)) {
sb.append(" until your next upkeep."); sb.append(" until your next upkeep.");
} else if (sa.hasParam("UntilYourNextTurn")) { } else if ("UntilYourNextTurn".equals(duration)) {
sb.append(" until your next turn."); sb.append(" until your next turn.");
} else if (sa.hasParam("UntilControllerNextUntap")) { } else if ("UntilControllerNextUntap".equals(duration)) {
sb.append(" until its controller's next untap step."); sb.append(" until its controller's next untap step.");
} else { } else {
sb.append(" until end of turn."); sb.append(" until end of turn.");
@@ -313,7 +314,6 @@ public class AnimateEffect extends AnimateEffectBase {
sb.append("."); sb.append(".");
} }
return sb.toString(); return sb.toString();
} }

View File

@@ -32,7 +32,6 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.event.GameEventCardStatsChanged; import forge.game.event.GameEventCardStatsChanged;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler; import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.AbilityStatic; import forge.game.spellability.AbilityStatic;
@@ -111,6 +110,10 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
removeEnchantmentTypes = true; removeEnchantmentTypes = true;
} }
if (sa.hasParam("RememberAnimated")) {
source.addRemembered(c);
}
if ((power != null) || (toughness != null)) { if ((power != null) || (toughness != null)) {
c.addNewPT(power, toughness, timestamp); c.addNewPT(power, toughness, timestamp);
} }
@@ -214,30 +217,11 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
addedStaticAbilities, removeAll, false, removeLandTypes, timestamp); addedStaticAbilities, removeAll, false, removeLandTypes, timestamp);
} }
if (!sa.hasParam("Permanent")) { if (!"Permanent".equals(sa.getParam("Duration"))) {
if (sa.hasParam("UntilEndOfCombat")) { if ("UntilControllerNextUntap".equals(sa.getParam("Duration"))) {
game.getEndOfCombat().addUntil(unanimate);
} else if (sa.hasParam("UntilHostLeavesPlay")) {
source.addLeavesPlayCommand(unanimate);
} else if (sa.hasParam("UntilLoseControlOfHost")) {
sa.getHostCard().addLeavesPlayCommand(unanimate);
sa.getHostCard().addChangeControllerCommand(unanimate);
} else if (sa.hasParam("UntilYourNextUpkeep")) {
game.getUpkeep().addUntil(source.getController(), unanimate);
} else if (sa.hasParam("UntilTheEndOfYourNextUpkeep")) {
if (game.getPhaseHandler().is(PhaseType.UPKEEP)) {
game.getUpkeep().registerUntilEnd(source.getController(), unanimate);
} else {
game.getUpkeep().addUntilEnd(source.getController(), unanimate);
}
} else if (sa.hasParam("UntilControllerNextUntap")) {
game.getUntap().addUntil(c.getController(), unanimate); game.getUntap().addUntil(c.getController(), unanimate);
} else if (sa.hasParam("UntilAPlayerCastSpell")) {
game.getStack().addCastCommand(sa.getParam("UntilAPlayerCastSpell"), unanimate);
} else if (sa.hasParam("UntilYourNextTurn")) {
game.getCleanup().addUntil(source.getController(), unanimate);
} else { } else {
game.getEndOfTurn().addUntil(unanimate); addUntilCommand(sa, unanimate);
} }
} }
} }

View File

@@ -44,7 +44,7 @@ public class AssignGroupEffect extends SpellAbilityEffect {
Player chooser = sa.getActivatingPlayer(); Player chooser = sa.getActivatingPlayer();
if (sa.hasParam("Chooser")) { if (sa.hasParam("Chooser")) {
final String choose = sa.getParam("Chooser"); final String choose = sa.getParam("Chooser");
chooser = AbilityUtils.getDefinedPlayers(sa.getHostCard(), choose, sa).get(0); chooser = AbilityUtils.getDefinedPlayers(host, choose, sa).get(0);
} }
Multimap<SpellAbility, GameObject> result = ArrayListMultimap.create(); Multimap<SpellAbility, GameObject> result = ArrayListMultimap.create();

View File

@@ -29,7 +29,7 @@ public class BlockEffect extends SpellAbilityEffect {
List<Card> attackers = new ArrayList<>(); List<Card> attackers = new ArrayList<>();
if (sa.hasParam("DefinedAttacker")) { if (sa.hasParam("DefinedAttacker")) {
for (final Card attacker : AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedAttacker"), sa)) { for (final Card attacker : AbilityUtils.getDefinedCards(host, sa.getParam("DefinedAttacker"), sa)) {
if (combat.isAttacking(attacker)) if (combat.isAttacking(attacker))
attackers.add(attacker); attackers.add(attacker);
} }
@@ -37,7 +37,7 @@ public class BlockEffect extends SpellAbilityEffect {
List<Card> blockers = new ArrayList<>(); List<Card> blockers = new ArrayList<>();
if (sa.hasParam("DefinedBlocker")) { if (sa.hasParam("DefinedBlocker")) {
for (final Card blocker : AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedBlocker"), sa)) { for (final Card blocker : AbilityUtils.getDefinedCards(host, sa.getParam("DefinedBlocker"), sa)) {
if (blocker.isCreature() && blocker.isInZone(ZoneType.Battlefield)) if (blocker.isCreature() && blocker.isInZone(ZoneType.Battlefield))
blockers.add(blocker); blockers.add(blocker);
} }

View File

@@ -31,9 +31,7 @@ public class CamouflageEffect extends SpellAbilityEffect {
} }
} }
if (attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") && if (blockers.size() < CombatUtil.getMinNumBlockersForAttacker(attacker, defender)) {
blockers.size() < defender.getCreaturesInPlay().size() ||
blockers.size() < CombatUtil.needsBlockers(attacker)) {
// If not enough remaining creatures to block, don't add them as blocker // If not enough remaining creatures to block, don't add them as blocker
continue; continue;
} }

View File

@@ -78,21 +78,17 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
TargetChoices newTargetBlock = oldTargetBlock.clone(); TargetChoices newTargetBlock = oldTargetBlock.clone();
// gets the divied value from old target // gets the divied value from old target
Integer div = oldTargetBlock.getDividedValue(oldTarget); Integer div = oldTargetBlock.getDividedValue(oldTarget);
newTargetBlock.remove(oldTarget);
replaceIn.updateTarget(newTargetBlock, sa.getHostCard());
// 3. test if updated choices would be correct. // 3. test if updated choices would be correct.
GameObject newTarget = Iterables.getFirst(getDefinedCardsOrTargeted(sa, "DefinedMagnet"), null); GameObject newTarget = Iterables.getFirst(getDefinedCardsOrTargeted(sa, "DefinedMagnet"), null);
if (replaceIn.getSpellAbility(true).canTarget(newTarget)) { if (replaceIn.getSpellAbility(true).canTarget(newTarget)) {
newTargetBlock.remove(oldTarget);
newTargetBlock.add(newTarget); newTargetBlock.add(newTarget);
if (div != null) { if (div != null) {
newTargetBlock.addDividedAllocation(newTarget, div); newTargetBlock.addDividedAllocation(newTarget, div);
} }
replaceIn.updateTarget(newTargetBlock, sa.getHostCard()); replaceIn.updateTarget(newTargetBlock, sa.getHostCard());
} }
else {
replaceIn.updateTarget(oldTargetBlock, sa.getHostCard());
}
} }
else { else {
while(changingTgtSI != null) { while(changingTgtSI != null) {
@@ -103,6 +99,14 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
int div = changingTgtSA.getTotalDividedValue(); int div = changingTgtSA.getTotalDividedValue();
changingTgtSA.resetTargets(); changingTgtSA.resetTargets();
List<GameEntity> candidates = changingTgtSA.getTargetRestrictions().getAllCandidates(changingTgtSA, true); List<GameEntity> candidates = changingTgtSA.getTargetRestrictions().getAllCandidates(changingTgtSA, true);
if (sa.hasParam("RandomTargetRestriction")) {
candidates.removeIf(new java.util.function.Predicate<GameEntity>() {
@Override
public boolean test(GameEntity c) {
return !c.isValid(sa.getParam("RandomTargetRestriction").split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
}
});
}
GameEntity choice = Aggregates.random(candidates); GameEntity choice = Aggregates.random(candidates);
changingTgtSA.getTargets().add(choice); changingTgtSA.getTargets().add(choice);
if (changingTgtSA.isDividedAsYouChoose()) { if (changingTgtSA.isDividedAsYouChoose()) {

View File

@@ -26,7 +26,7 @@ public class ChangeTextEffect extends SpellAbilityEffect {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Game game = source.getGame(); final Game game = source.getGame();
final Long timestamp = Long.valueOf(game.getNextTimestamp()); final Long timestamp = Long.valueOf(game.getNextTimestamp());
final boolean permanent = sa.hasParam("Permanent"); final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final String changedColorWordOriginal, changedColorWordNew; final String changedColorWordOriginal, changedColorWordNew;
if (sa.hasParam("ChangeColorWord")) { if (sa.hasParam("ChangeColorWord")) {
@@ -151,7 +151,7 @@ public class ChangeTextEffect extends SpellAbilityEffect {
changedTypeWordNew = null; changedTypeWordNew = null;
} }
final boolean permanent = sa.hasParam("Permanent"); final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
sb.append("Change the text of "); sb.append("Change the text of ");

View File

@@ -42,8 +42,10 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();
//if host is not on the battlefield don't apply //if host is not on the battlefield don't apply
if (sa.hasParam("UntilHostLeavesPlay") && !sa.getHostCard().isInPlay()) { if ("UntilHostLeavesPlay".equals(sa.getParam("Duration")) && !source.isInPlay()) {
return; return;
} }
@@ -53,7 +55,6 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
CardCollection cards; CardCollection cards;
List<Player> tgtPlayers = getTargetPlayers(sa); List<Player> tgtPlayers = getTargetPlayers(sa);
final Game game = sa.getActivatingPlayer().getGame(); final Game game = sa.getActivatingPlayer().getGame();
final Card source = sa.getHostCard();
if ((!sa.usesTargeting() && !sa.hasParam("Defined")) || sa.hasParam("UseAllOriginZones")) { if ((!sa.usesTargeting() && !sa.hasParam("Defined")) || sa.hasParam("UseAllOriginZones")) {
cards = new CardCollection(game.getCardsIn(origin)); cards = new CardCollection(game.getCardsIn(origin));
@@ -122,7 +123,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
} }
if (sa.hasParam("ForgetOtherRemembered")) { if (sa.hasParam("ForgetOtherRemembered")) {
sa.getHostCard().clearRemembered(); source.clearRemembered();
} }
final String remember = sa.getParam("RememberChanged"); final String remember = sa.getParam("RememberChanged");
@@ -203,9 +204,6 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
if (sa.hasParam("ExileFaceDown")) { if (sa.hasParam("ExileFaceDown")) {
movedCard.turnFaceDown(true); movedCard.turnFaceDown(true);
} }
if (sa.hasParam("Tapped")) {
movedCard.setTapped(true);
}
} }
if (remember != null) { if (remember != null) {
@@ -270,8 +268,8 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
triggerList.triggerChangesZoneAll(game, sa); triggerList.triggerChangesZoneAll(game, sa);
if (sa.hasParam("UntilHostLeavesPlay")) { if (sa.hasParam("Duration")) {
source.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, source)); addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, source));
} }
// if Shuffle parameter exists, and any amount of cards were owned by // if Shuffle parameter exists, and any amount of cards were owned by

View File

@@ -57,17 +57,9 @@ import forge.util.collect.FCollectionView;
public class ChangeZoneEffect extends SpellAbilityEffect { public class ChangeZoneEffect extends SpellAbilityEffect {
private boolean isHidden(SpellAbility sa) {
boolean hidden = sa.hasParam("Hidden");
if (!hidden && sa.hasParam("Origin")) {
hidden = ZoneType.isHidden(sa.getParam("Origin"));
}
return hidden;
}
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
if (isHidden(sa)) { if (sa.isHidden()) {
return changeHiddenOriginStackDescription(sa); return changeHiddenOriginStackDescription(sa);
} }
return changeKnownOriginStackDescription(sa); return changeKnownOriginStackDescription(sa);
@@ -97,13 +89,13 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// Player whose cards will change zones // Player whose cards will change zones
List<Player> fetchers = null; List<Player> fetchers = null;
if (sa.hasParam("DefinedPlayer")) { if (sa.hasParam("DefinedPlayer")) {
fetchers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("DefinedPlayer"), sa); fetchers = AbilityUtils.getDefinedPlayers(host, sa.getParam("DefinedPlayer"), sa);
} }
if (fetchers == null && sa.hasParam("ValidTgts") && sa.usesTargeting()) { if (fetchers == null && sa.hasParam("ValidTgts") && sa.usesTargeting()) {
fetchers = Lists.newArrayList(sa.getTargets().getTargetPlayers()); fetchers = Lists.newArrayList(sa.getTargets().getTargetPlayers());
} }
if (fetchers == null) { if (fetchers == null) {
fetchers = Lists.newArrayList(sa.getHostCard().getController()); fetchers = Lists.newArrayList(host.getController());
} }
final String fetcherNames = Lang.joinHomogenous(fetchers, Player.Accessors.FN_GET_NAME); final String fetcherNames = Lang.joinHomogenous(fetchers, Player.Accessors.FN_GET_NAME);
@@ -111,7 +103,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// Player who chooses the cards to move // Player who chooses the cards to move
List<Player> choosers = Lists.newArrayList(); List<Player> choosers = Lists.newArrayList();
if (sa.hasParam("Chooser")) { if (sa.hasParam("Chooser")) {
choosers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa); choosers = AbilityUtils.getDefinedPlayers(host, sa.getParam("Chooser"), sa);
} }
if (choosers.isEmpty()) { if (choosers.isEmpty()) {
choosers.add(sa.getActivatingPlayer()); choosers.add(sa.getActivatingPlayer());
@@ -420,11 +412,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
@Override @Override
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
//if host is not on the battlefield don't apply //if host is not on the battlefield don't apply
if (sa.hasParam("UntilHostLeavesPlay") && !sa.getHostCard().isInPlay()) { if ("UntilHostLeavesPlay".equals(sa.getParam("Duration")) && !sa.getHostCard().isInPlay()) {
return; return;
} }
if (isHidden(sa) && !sa.hasParam("Ninjutsu")) { if (sa.isHidden() && !sa.hasParam("Ninjutsu")) {
changeHiddenOriginResolve(sa); changeHiddenOriginResolve(sa);
} else { } else {
//else if (isKnown(origin) || sa.containsKey("Ninjutsu")) { //else if (isKnown(origin) || sa.containsKey("Ninjutsu")) {
@@ -502,7 +494,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
Player chooser = player; Player chooser = player;
if (sa.hasParam("Chooser")) { if (sa.hasParam("Chooser")) {
chooser = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa).get(0); chooser = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("Chooser"), sa).get(0);
} }
for (final Card tgtC : tgtCards) { for (final Card tgtC : tgtCards) {
@@ -510,7 +502,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// gameCard is LKI in that case, the card is not in game anymore // gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change // or the timestamp did change
// this should check Self too // this should check Self too
if (gameCard == null || !tgtC.equalsWithTimestamp(gameCard)) { if (gameCard == null || !tgtC.equalsWithTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue; continue;
} }
if (sa.usesTargeting() && !gameCard.canBeTargetedBy(sa)) { if (sa.usesTargeting() && !gameCard.canBeTargetedBy(sa)) {
@@ -636,8 +628,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
hostCard.removeRemembered(gameCard); hostCard.removeRemembered(gameCard);
} }
// Auras without Candidates stay in their current // Auras without Candidates stay in their current location
// location
if (gameCard.isAura()) { if (gameCard.isAura()) {
final SpellAbility saAura = gameCard.getFirstAttachSpell(); final SpellAbility saAura = gameCard.getFirstAttachSpell();
if (saAura != null) { if (saAura != null) {
@@ -679,12 +670,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
game.getCombat().getBandOfAttacker(movedCard).setBlocked(false); game.getCombat().getBandOfAttacker(movedCard).setBlocked(false);
combatChanged = true; combatChanged = true;
} }
if (sa.hasParam("Tapped") || sa.hasParam("Ninjutsu")) {
movedCard.setTapped(true);
}
if (sa.hasParam("Untapped")) {
movedCard.setTapped(false);
}
movedCard.setTimestamp(ts); movedCard.setTimestamp(ts);
} else { } else {
// might set before card is moved only for nontoken // might set before card is moved only for nontoken
@@ -812,12 +797,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
triggerList.triggerChangesZoneAll(game, sa); triggerList.triggerChangesZoneAll(game, sa);
counterTable.triggerCountersPutAll(game); counterTable.triggerCountersPutAll(game);
if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) { if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) {
registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards()); registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards());
} }
if (sa.hasParam("UntilHostLeavesPlay")) { if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
hostCard.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, hostCard)); addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, hostCard));
} }
// for things like Gaea's Blessing // for things like Gaea's Blessing
@@ -1292,11 +1276,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
setFaceDownState(c, sa); setFaceDownState(c, sa);
} }
movedCard = game.getAction().moveToPlay(c, c.getController(), sa, moveParams); movedCard = game.getAction().moveToPlay(c, c.getController(), sa, moveParams);
if (sa.hasParam("Tapped")) {
movedCard.setTapped(true);
} else if (sa.hasParam("Untapped")) {
c.setTapped(false);
}
movedCard.setTimestamp(ts); movedCard.setTimestamp(ts);
} }
@@ -1402,8 +1381,8 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
} }
triggerList.triggerChangesZoneAll(game, sa); triggerList.triggerChangesZoneAll(game, sa);
if (sa.hasParam("UntilHostLeavesPlay")) { if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
source.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, source)); addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, source));
} }
} }

View File

@@ -35,7 +35,7 @@ public class ChoosePlayerEffect extends SpellAbilityEffect {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final FCollectionView<Player> choices = sa.hasParam("Choices") ? AbilityUtils.getDefinedPlayers( final FCollectionView<Player> choices = sa.hasParam("Choices") ? AbilityUtils.getDefinedPlayers(
sa.getHostCard(), sa.getParam("Choices"), sa) : sa.getActivatingPlayer().getGame().getPlayersInTurnOrder(); card, sa.getParam("Choices"), sa) : sa.getActivatingPlayer().getGame().getPlayersInTurnOrder();
final String choiceDesc = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChoosePlayer"); final String choiceDesc = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChoosePlayer");
final boolean random = sa.hasParam("Random"); final boolean random = sa.hasParam("Random");

View File

@@ -5,11 +5,11 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -84,7 +84,6 @@ public class ChooseSourceEffect extends SpellAbilityEffect {
if (sa.hasParam("Choices")) { if (sa.hasParam("Choices")) {
permanentSources = CardLists.getValidCards(permanentSources, sa.getParam("Choices"), host.getController(), host, sa); permanentSources = CardLists.getValidCards(permanentSources, sa.getParam("Choices"), host.getController(), host, sa);
stackSources = CardLists.getValidCards(stackSources, sa.getParam("Choices"), host.getController(), host, sa); stackSources = CardLists.getValidCards(stackSources, sa.getParam("Choices"), host.getController(), host, sa);
referencedSources = CardLists.getValidCards(referencedSources, sa.getParam("Choices"), host.getController(), host, sa); referencedSources = CardLists.getValidCards(referencedSources, sa.getParam("Choices"), host.getController(), host, sa);
commandZoneSources = CardLists.getValidCards(commandZoneSources, sa.getParam("Choices"), host.getController(), host, sa); commandZoneSources = CardLists.getValidCards(commandZoneSources, sa.getParam("Choices"), host.getController(), host, sa);
@@ -127,7 +126,7 @@ public class ChooseSourceEffect extends SpellAbilityEffect {
} }
final String numericAmount = sa.getParamOrDefault("Amount", "1"); final String numericAmount = sa.getParamOrDefault("Amount", "1");
final int validAmount = StringUtils.isNumeric(numericAmount) ? Integer.parseInt(numericAmount) : CardFactoryUtil.xCount(host, host.getSVar(numericAmount)); final int validAmount = StringUtils.isNumeric(numericAmount) ? Integer.parseInt(numericAmount) : AbilityUtils.calculateAmount(host, numericAmount, sa);
for (final Player p : tgtPlayers) { for (final Player p : tgtPlayers) {
final CardCollection chosen = new CardCollection(); final CardCollection chosen = new CardCollection();
@@ -137,7 +136,7 @@ public class ChooseSourceEffect extends SpellAbilityEffect {
Card o = null; Card o = null;
do { do {
o = p.getController().chooseSingleEntityForEffect(sourcesToChooseFrom, sa, choiceTitle, null); o = p.getController().chooseSingleEntityForEffect(sourcesToChooseFrom, sa, choiceTitle, null);
} while (o == null); } while (o == null || o.getName().startsWith("--"));
chosen.add(o); chosen.add(o);
sourcesToChooseFrom.remove(o); sourcesToChooseFrom.remove(o);
} }

View File

@@ -51,10 +51,11 @@ public class ClashEffect extends SpellAbilityEffect {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, player); runParams.put(AbilityKey.Player, player);
runParams.put(AbilityKey.Won, player.equals(winner) ? "True" : "False"); runParams.put(AbilityKey.Won, player.equals(winner) ? "True" : "False");
sa.getHostCard().getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams, false); source.getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams, false);
runParams.put(AbilityKey.Player, opponent); final Map<AbilityKey, Object> runParams2 = AbilityKey.newMap();
runParams.put(AbilityKey.Won, opponent.equals(winner) ? "True" : "False"); runParams2.put(AbilityKey.Player, opponent);
sa.getHostCard().getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams, false); runParams2.put(AbilityKey.Won, opponent.equals(winner) ? "True" : "False");
source.getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams2, false);
} }
/** /**

View File

@@ -3,7 +3,6 @@ package forge.game.ability.effects;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@@ -143,6 +142,12 @@ public class CloneEffect extends SpellAbilityEffect {
tgtCard.updateStateForView(); tgtCard.updateStateForView();
// when clone is itself, cleanup from old abilities
if (host.equals(tgtCard)) {
tgtCard.clearImprintedCards();
tgtCard.clearRemembered();
}
// check if clone is now an Aura that needs to be attached // check if clone is now an Aura that needs to be attached
if (tgtCard.isAura() && !tgtCard.isInZone(ZoneType.Battlefield)) { if (tgtCard.isAura() && !tgtCard.isInZone(ZoneType.Battlefield)) {
AttachEffect.attachAuraOnIndirectEnterBattlefield(tgtCard); AttachEffect.attachAuraOnIndirectEnterBattlefield(tgtCard);
@@ -150,7 +155,7 @@ public class CloneEffect extends SpellAbilityEffect {
if (sa.hasParam("Duration")) { if (sa.hasParam("Duration")) {
final Card cloneCard = tgtCard; final Card cloneCard = tgtCard;
// if clone is temporary, target needs old values back after // if clone is temporary, target needs old values back after (keep Death-Mask Duplicant working)
final Iterable<Card> clonedImprinted = new CardCollection(tgtCard.getImprintedCards()); final Iterable<Card> clonedImprinted = new CardCollection(tgtCard.getImprintedCards());
final Iterable<Object> clonedRemembered = new FCollection<>(tgtCard.getRemembered()); final Iterable<Object> clonedRemembered = new FCollection<>(tgtCard.getRemembered());
@@ -164,7 +169,7 @@ public class CloneEffect extends SpellAbilityEffect {
cloneCard.clearImprintedCards(); cloneCard.clearImprintedCards();
cloneCard.clearRemembered(); cloneCard.clearRemembered();
// restore original Remembered and Imprinted, ignore cards from players who lost // restore original Remembered and Imprinted, ignore cards from players who lost
cloneCard.addImprintedCards(Iterables.filter(clonedImprinted, Predicates.not(CardPredicates.inZone(ZoneType.None)))); cloneCard.addImprintedCards(Iterables.filter(clonedImprinted, CardPredicates.ownerLives()));
cloneCard.addRemembered(Iterables.filter(clonedRemembered, Player.class)); cloneCard.addRemembered(Iterables.filter(clonedRemembered, Player.class));
cloneCard.addRemembered(Iterables.filter(Iterables.filter(clonedRemembered, Card.class), CardPredicates.ownerLives())); cloneCard.addRemembered(Iterables.filter(Iterables.filter(clonedRemembered, Card.class), CardPredicates.ownerLives()));
cloneCard.updateStateForView(); cloneCard.updateStateForView();
@@ -173,22 +178,10 @@ public class CloneEffect extends SpellAbilityEffect {
} }
}; };
final String duration = sa.getParam("Duration"); addUntilCommand(sa, unclone);
if (duration.equals("UntilEndOfTurn")) {
game.getEndOfTurn().addUntil(unclone);
}
else if (duration.equals("UntilYourNextTurn")) {
game.getCleanup().addUntil(host.getController(), unclone);
}
else if (duration.equals("UntilUnattached")) {
sa.getHostCard().addUnattachCommand(unclone);
}
else if (duration.equals("UntilFacedown")) {
sa.getHostCard().addFacedownCommand(unclone);
}
} }
//Clear Remembered and Imprint lists // now we can also cleanup in case target was another card
tgtCard.clearRemembered(); tgtCard.clearRemembered();
tgtCard.clearImprintedCards(); tgtCard.clearImprintedCards();

View File

@@ -99,8 +99,8 @@ public class ControlExchangeEffect extends SpellAbilityEffect {
object2.setController(player1, tStamp); object2.setController(player1, tStamp);
object1.setController(player2, tStamp); object1.setController(player2, tStamp);
if (sa.hasParam("RememberExchanged")) { if (sa.hasParam("RememberExchanged")) {
sa.getHostCard().addRemembered(object1); host.addRemembered(object1);
sa.getHostCard().addRemembered(object2); host.addRemembered(object2);
} }
} }

View File

@@ -24,7 +24,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
final List<Player> newController = getTargetPlayers(sa, "NewController"); final List<Player> newController = getDefinedPlayersOrTargeted(sa, "NewController");
if (newController.isEmpty()) { if (newController.isEmpty()) {
newController.add(sa.getActivatingPlayer()); newController.add(sa.getActivatingPlayer());
} }
@@ -108,8 +108,8 @@ public class ControlGainEffect extends SpellAbilityEffect {
continue; continue;
} }
if (!tgtC.equals(sa.getHostCard()) && !sa.getHostCard().getGainControlTargets().contains(tgtC)) { if (!tgtC.equals(source) && !source.getGainControlTargets().contains(tgtC)) {
sa.getHostCard().addGainControlTarget(tgtC); source.addGainControlTarget(tgtC);
} }
long tStamp = game.getNextTimestamp(); long tStamp = game.getNextTimestamp();
@@ -139,24 +139,24 @@ public class ControlGainEffect extends SpellAbilityEffect {
game.fireEvent(new GameEventCardStatsChanged(tgtC)); game.fireEvent(new GameEventCardStatsChanged(tgtC));
} }
if (remember && !sa.getHostCard().isRemembered(tgtC)) { if (remember && !source.isRemembered(tgtC)) {
sa.getHostCard().addRemembered(tgtC); source.addRemembered(tgtC);
} }
if (forget && sa.getHostCard().isRemembered(tgtC)) { if (forget && source.isRemembered(tgtC)) {
sa.getHostCard().removeRemembered(tgtC); source.removeRemembered(tgtC);
} }
if (lose != null) { if (lose != null) {
final GameCommand loseControl = getLoseControlCommand(tgtC, tStamp, bTapOnLose, source); final GameCommand loseControl = getLoseControlCommand(tgtC, tStamp, bTapOnLose, source);
if (lose.contains("LeavesPlay") && sa.getHostCard() != tgtC) { // Only return control if host and target are different cards if (lose.contains("LeavesPlay") && source != tgtC) { // Only return control if host and target are different cards
sa.getHostCard().addLeavesPlayCommand(loseControl); source.addLeavesPlayCommand(loseControl);
} }
if (lose.contains("Untap")) { if (lose.contains("Untap")) {
sa.getHostCard().addUntapCommand(loseControl); source.addUntapCommand(loseControl);
} }
if (lose.contains("LoseControl")) { if (lose.contains("LoseControl")) {
sa.getHostCard().addChangeControllerCommand(loseControl); source.addChangeControllerCommand(loseControl);
} }
if (lose.contains("EOT")) { if (lose.contains("EOT")) {
game.getEndOfTurn().addUntil(loseControl); game.getEndOfTurn().addUntil(loseControl);
@@ -169,7 +169,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
if (lose.contains("StaticCommandCheck")) { if (lose.contains("StaticCommandCheck")) {
String leftVar = sa.getSVar(sa.getParam("StaticCommandCheckSVar")); String leftVar = sa.getSVar(sa.getParam("StaticCommandCheckSVar"));
String rightVar = sa.getParam("StaticCommandSVarCompare"); String rightVar = sa.getParam("StaticCommandSVarCompare");
sa.getHostCard().addStaticCommandList(new Object[]{leftVar, rightVar, tgtC, loseControl}); source.addStaticCommandList(new Object[]{leftVar, rightVar, tgtC, loseControl});
} }
if (lose.contains("UntilTheEndOfYourNextTurn")) { if (lose.contains("UntilTheEndOfYourNextTurn")) {
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) { if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {

View File

@@ -3,6 +3,7 @@ package forge.game.ability.effects;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@@ -21,6 +22,7 @@ import forge.game.player.Player;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.util.CardTranslation; import forge.util.CardTranslation;
import forge.util.Localizer; import forge.util.Localizer;
@@ -162,6 +164,19 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
if (sa.hasParam("MayChooseTarget")) { if (sa.hasParam("MayChooseTarget")) {
copy.setMayChooseNewTargets(true); copy.setMayChooseNewTargets(true);
} }
if (sa.hasParam("RandomTarget")){
List<GameEntity> candidates = copy.getTargetRestrictions().getAllCandidates(chosenSA, true);
if (sa.hasParam("RandomTargetRestriction")) {
candidates.removeIf(new Predicate<GameEntity>() {
@Override
public boolean test(GameEntity c) {
return !c.isValid(sa.getParam("RandomTargetRestriction").split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
}
});
}
GameEntity choice = Aggregates.random(candidates);
resetFirstTargetOnCopy(copy, choice, chosenSA);
}
// extra case for Epic to remove the keyword and the last part of the SpellAbility // extra case for Epic to remove the keyword and the last part of the SpellAbility
if (sa.hasParam("Epic")) { if (sa.hasParam("Epic")) {

View File

@@ -41,7 +41,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final Player activator = sa.getActivatingPlayer(); final Player activator = sa.getActivatingPlayer();
final String type = sa.getParam("CounterType"); final String type = sa.getParam("CounterType");
final int counterAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa); final int counterAmount = AbilityUtils.calculateAmount(host, sa.getParam("CounterNum"), sa);
final String valid = sa.getParam("ValidCards"); final String valid = sa.getParam("ValidCards");
final ZoneType zone = sa.hasParam("ValidZone") ? ZoneType.smartValueOf(sa.getParam("ValidZone")) : ZoneType.Battlefield; final ZoneType zone = sa.hasParam("ValidZone") ? ZoneType.smartValueOf(sa.getParam("ValidZone")) : ZoneType.Battlefield;
final boolean etbcounter = sa.hasParam("ETB"); final boolean etbcounter = sa.hasParam("ETB");
@@ -52,7 +52,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
} }
CardCollectionView cards = game.getCardsIn(zone); CardCollectionView cards = game.getCardsIn(zone);
cards = CardLists.getValidCards(cards, valid, host.getController(), sa.getHostCard(), sa); cards = CardLists.getValidCards(cards, valid, host.getController(), host, sa);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
final Player pl = sa.getTargets().getFirstTargetedPlayer(); final Player pl = sa.getTargets().getFirstTargetedPlayer();

View File

@@ -155,16 +155,16 @@ public class CountersPutEffect extends SpellAbilityEffect {
} }
Player chooser = activator; Player chooser = activator;
if (sa.hasParam("Chooser")) { if (sa.hasParam("Chooser")) {
List<Player> choosers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa); List<Player> choosers = AbilityUtils.getDefinedPlayers(card, sa.getParam("Chooser"), sa);
if (choosers.isEmpty()) { if (choosers.isEmpty()) {
return; return;
} }
chooser = choosers.get(0); chooser = choosers.get(0);
} }
int n = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParamOrDefault("ChoiceAmount", int n = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("ChoiceAmount",
"1"), sa); "1"), sa);
int m = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParamOrDefault("MinChoiceAmount", int m = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("MinChoiceAmount",
sa.getParamOrDefault("ChoiceAmount", "1")), sa); sa.getParamOrDefault("ChoiceAmount", "1")), sa);
// no choices allowed // no choices allowed
@@ -405,10 +405,10 @@ public class CountersPutEffect extends SpellAbilityEffect {
Player placer = activator; Player placer = activator;
if (sa.hasParam("Placer")) { if (sa.hasParam("Placer")) {
final String pstr = sa.getParam("Placer"); final String pstr = sa.getParam("Placer");
placer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), pstr, sa).get(0); placer = AbilityUtils.getDefinedPlayers(card, pstr, sa).get(0);
} }
int counterAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amount, sa); int counterAmount = AbilityUtils.calculateAmount(card, amount, sa);
GameEntityCounterTable table = new GameEntityCounterTable(); GameEntityCounterTable table = new GameEntityCounterTable();
@@ -422,7 +422,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
List<String> keywords = Arrays.asList(sa.getParam("SharedKeywords").split(" & ")); List<String> keywords = Arrays.asList(sa.getParam("SharedKeywords").split(" & "));
List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone")); List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone"));
String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"}; String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"};
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard()); keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, card);
for (String k : keywords) { for (String k : keywords) {
resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table); resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table);
} }

View File

@@ -82,7 +82,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
int cntToRemove = 0; int cntToRemove = 0;
if (!num.equals("All") && !num.equals("Any")) { if (!num.equals("All") && !num.equals("Any")) {
cntToRemove = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa); cntToRemove = AbilityUtils.calculateAmount(card, num, sa);
} }
if (sa.hasParam("Optional")) { if (sa.hasParam("Optional")) {

Some files were not shown because too many files have changed in this diff Show More