...
 
Commits (323)
......@@ -233,4 +233,6 @@ forge-gui/tools/oracleScript.log
/test-output
.settings
.classpath
.project
\ No newline at end of file
.project
.vscode/settings.json
.vscode/launch.json
......@@ -169,7 +169,7 @@ The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desk
# IntelliJ
TBD
Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/wikis/intellij-setup).
# Card Scripting
......
......@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.24-SNAPSHOT</version>
<version>1.6.26-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>
......
......@@ -190,7 +190,28 @@ public class AiAttackController {
if ((attacker.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, null, combat, true)) <= 0) {
return false;
}
// the attacker will die to a triggered ability (e.g. Sarkhan the Masterless)
for (Card c : ai.getOpponents().getCardsIn(ZoneType.Battlefield)) {
for (Trigger t : c.getTriggers()) {
if (t.getMode() == TriggerType.Attacks) {
SpellAbility sa = t.getOverridingAbility();
if (sa == null && t.hasParam("Execute")) {
sa = AbilityFactory.getAbility(c, t.getParam("Execute"));
}
if (sa != null && sa.getApi() == ApiType.EachDamage && "TriggeredAttacker".equals(sa.getParam("DefinedPlayers"))) {
List<Card> valid = CardLists.getValidCards(c.getController().getCreaturesInPlay(), sa.getParam("ValidCards"), c.getController(), c, sa);
// TODO: this assumes that 1 damage is dealt per creature. Improve this to check the parameter/X to determine
// how much damage is dealt by each of the creatures in the valid list.
if (attacker.getNetToughness() <= valid.size()) {
return false;
}
}
}
}
}
if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) {
return true;
}
......
......@@ -1010,7 +1010,7 @@ public class AiController {
p += 9;
}
// sort planeswalker abilities with most costly first
if (sa.getRestrictions().isPwAbility()) {
if (sa.isPwAbility()) {
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
if (cost instanceof CostRemoveCounter) {
p += cost.convertAmount() == null ? 1 : cost.convertAmount();
......
......@@ -129,7 +129,10 @@ public enum AiProps { /** */
FLASH_USE_BUFF_AURAS_AS_COMBAT_TRICKS("true"),
FLASH_BUFF_AURA_CHANCE_TO_CAST_EARLY("1"),
FLASH_BUFF_AURA_CHANCE_CAST_AT_EOT("5"),
FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"); /** */
FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"),
BLINK_RELOAD_PLANESWALKER_CHANCE("30"), /** */
BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY("2"), /** */
BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"); /** */
// Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
// <-- There are no experimental options here -->
......
......@@ -1255,7 +1255,7 @@ public class ComputerUtil {
// returns true if the AI should stop using the ability
public static boolean preventRunAwayActivations(final SpellAbility sa) {
int activations = sa.getRestrictions().getNumberTurnActivations();
int activations = sa.getActivationsThisTurn();
if (sa.isTemporary()) {
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
......
......@@ -1601,7 +1601,7 @@ public class ComputerUtilCard {
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
Set<CounterType> types = c.getCounters().keySet();
for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true);
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true, null);
}
//Copies tap-state and extra keywords (auras, equipment, etc.)
if (c.isTapped()) {
......
......@@ -481,7 +481,7 @@ public class ComputerUtilCost {
}
// Try not to lose Planeswalker if not threatened
if (sa.getRestrictions().isPwAbility()) {
if (sa.isPwAbility()) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
......
......@@ -1016,7 +1016,7 @@ public abstract class GameState {
String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2);
entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false);
entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
}
}
......@@ -1142,7 +1142,7 @@ public abstract class GameState {
} else if (info.startsWith("SummonSick")) {
c.setSickness(true);
} else if (info.startsWith("FaceDown")) {
c.setState(CardStateName.FaceDown, true);
c.turnFaceDown(true);
if (info.endsWith("Manifested")) {
c.setManifested(true);
}
......
......@@ -367,8 +367,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean apply(Card card) {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
return card.getName().equals("Volrath's Shapeshifter")
|| card.getStates().contains(CardStateName.OriginalText) && card.getState(CardStateName.OriginalText).getName().equals("Volrath's Shapeshifter");
return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter");
}
}).isEmpty()) {
int bestValue = 0;
......
......@@ -247,7 +247,7 @@ public abstract class SpellAbilityAi {
protected static boolean isSorcerySpeed(final SpellAbility sa) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.getRestrictions().isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
|| (sa.isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
}
/**
......@@ -276,7 +276,7 @@ public abstract class SpellAbilityAi {
return true;
}
if (sa.getRestrictions().isPwAbility() && phase.is(PhaseType.MAIN2)) {
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
return true;
}
if (sa.isSpell() && !sa.isBuyBackAbility()) {
......
......@@ -26,6 +26,7 @@ import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
......@@ -316,6 +317,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
for (final Player p : pDefined) {
CardCollectionView list = p.getCardsIn(origin);
// remove cards that won't be seen if library can't be searched
if (!ai.canSearchLibraryWith(sa, p)) {
list = CardLists.filter(list, Predicates.not(CardPredicates.inZone(ZoneType.Library)));
}
if (type != null && p == ai) {
// AI only "knows" about his information
list = CardLists.getValidCards(list, type, source.getController(), source);
......@@ -671,7 +677,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
// only use blink or bounce effects
if (!(destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone))
if (!(destination.equals(ZoneType.Exile)
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
&& !destination.equals(ZoneType.Hand)) {
return false;
}
......@@ -931,7 +938,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// if it's blink or bounce, try to save my about to die stuff
final boolean blink = (destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger
|| (subApi == ApiType.ChangeZone && subAffected.equals("Remembered"))));
|| "DelayedBlink".equals(sa.getParam("AILogic")) || (subApi == ApiType.ChangeZone && subAffected.equals("Remembered"))));
if ((destination.equals(ZoneType.Hand) || blink) && (tgt.getMinTargets(sa.getHostCard(), sa) <= 1)) {
// save my about to die stuff
Card tobounce = canBouncePermanent(ai, sa, list);
......@@ -1225,6 +1232,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// filter out untargetables
CardCollectionView aiPermanents = CardLists
.filterControlledBy(list, ai);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
// Felidar Guardian + Saheeli Rai combo support
if (sa.getHostCard().getName().equals("Felidar Guardian")) {
......@@ -1268,6 +1276,33 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
}
// Reload planeswalkers
else if (!aiPlaneswalkers.isEmpty() && (sa.getHostCard().isSorcery() || !game.getPhaseHandler().isPlayerTurn(ai))) {
int maxLoyaltyToConsider = 2;
int loyaltyDiff = 2;
int chance = 30;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
maxLoyaltyToConsider = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY);
loyaltyDiff = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF);
chance = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_CHANCE);
}
if (MyRandom.percentTrue(chance)) {
Collections.sort(aiPlaneswalkers, new Comparator<Card>() {
@Override
public int compare(final Card a, final Card b) {
return a.getCounters(CounterType.LOYALTY) - b.getCounters(CounterType.LOYALTY);
}
});
for (Card pw : aiPlaneswalkers) {
int curLoyalty = pw.getCounters(CounterType.LOYALTY);
int freshLoyalty = pw.getCurrentState().getBaseLoyalty();
if (freshLoyalty - curLoyalty >= loyaltyDiff && curLoyalty <= maxLoyaltyToConsider) {
return pw;
}
}
}
}
return null;
}
......
package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
......@@ -57,6 +58,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
CardCollectionView oppType = CardLists.filterControlledBy(game.getCardsIn(origin), ai.getOpponents());
CardCollectionView computerType = ai.getCardsIn(origin);
// remove cards that won't be seen in AI's own library if it can't be searched
if (!ai.canSearchLibraryWith(sa, ai)) {
computerType = CardLists.filter(computerType, Predicates.not(CardPredicates.inZone(ZoneType.Library)));
}
// Ugin check need to be done before filterListByType because of ChosenX
// Ugin AI: always try to sweep before considering +1
if (sourceName.equals("Ugin, the Spirit Dragon")) {
......
......@@ -12,7 +12,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.List;
......@@ -21,7 +20,6 @@ public class CloneAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Game game = source.getGame();
......@@ -39,27 +37,13 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?"
PhaseHandler phase = game.getPhaseHandler();
// don't use instant speed clone abilities outside computers
// Combat_Begin step
if (!phase.is(PhaseType.COMBAT_BEGIN)
&& phase.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false;
}
// don't use instant speed clone abilities outside humans
// Combat_Declare_Attackers_InstantAbility step
if (!phase.is(PhaseType.COMBAT_DECLARE_ATTACKERS) || phase.isPlayerTurn(ai) || game.getCombat().getAttackers().isEmpty()) {
return false;
}
// don't activate during main2 unless this effect is permanent
if (phase.is(PhaseType.MAIN2) && !sa.hasParam("Permanent")) {
return false;
}
PhaseHandler phase = game.getPhaseHandler();
if (null == tgt) {
if (!sa.usesTargeting()) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
......@@ -131,7 +115,7 @@ public class CloneAi extends SpellAbilityAi {
* <p>
* cloneTgtAI.
* </p>
*
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
......@@ -155,7 +139,7 @@ public class CloneAi extends SpellAbilityAi {
// a good target
return false;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
......@@ -178,7 +162,7 @@ public class CloneAi extends SpellAbilityAi {
/*
* (non-Javadoc)
*
*
* @see forge.ai.SpellAbilityAi#chooseSingleCard(forge.game.player.Player,
* forge.game.spellability.SpellAbility, java.lang.Iterable, boolean,
* forge.game.player.Player)
......@@ -186,9 +170,13 @@ public class CloneAi extends SpellAbilityAi {
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
Player targetedPlayer) {
final Card host = sa.getHostCard();
final Player ctrl = host.getController();
final Card cloneTarget = getCloneTarget(sa);
final boolean isOpp = cloneTarget.getController().isOpponentOf(sa.getActivatingPlayer());
final boolean isVesuva = "Vesuva".equals(host.getName());
final String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
......@@ -198,7 +186,8 @@ public class CloneAi extends SpellAbilityAi {
if (!newOptions.isEmpty()) {
options = newOptions;
}
Card choice = ComputerUtilCard.getBestAI(options);
Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);
if (isVesuva && "Vesuva".equals(choice.getName())) {
choice = null;
}
......@@ -206,4 +195,44 @@ public class CloneAi extends SpellAbilityAi {
return choice;
}
protected Card getCloneTarget(final SpellAbility sa) {
final Card host = sa.getHostCard();
Card tgtCard = host;
if (sa.hasParam("CloneTarget")) {
final List<Card> cloneTargets = AbilityUtils.getDefinedCards(host, sa.getParam("CloneTarget"), sa);
if (!cloneTargets.isEmpty()) {
tgtCard = cloneTargets.get(0);
}
} else if (sa.hasParam("Choices") && sa.usesTargeting()) {
tgtCard = sa.getTargets().getFirstTargetedCard();
}
return tgtCard;
}
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler)
*/
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
// don't use instant speed clone abilities outside computers
// Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false;
}
// don't use instant speed clone abilities outside humans
// Combat_Declare_Attackers_InstantAbility step
if (!ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS) || ph.isPlayerTurn(ai) || ph.getCombat().getAttackers().isEmpty()) {
return false;
}
// don't activate during main2 unless this effect is permanent
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent")) {
return false;
}
return true;
}
}
......@@ -6,6 +6,7 @@ import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
......@@ -354,7 +355,25 @@ public class CountersRemoveAi extends SpellAbilityAi {
*/
@Override
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
// TODO Auto-generated method stub
GameEntity target = (GameEntity) params.get("Target");
CounterType type = (CounterType) params.get("CounterType");
if (target instanceof Card) {
Card targetCard = (Card) target;
if (targetCard.getController().isOpponentOf(player)) {
return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
} else {
return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
}
} else if (target instanceof Player) {
Player targetPlayer = (Player) target;
if (targetPlayer.isOpponentOf(player)) {
return !type.equals(CounterType.POISON) ? max : min;
} else {
return type.equals(CounterType.POISON) ? max : min;
}
}
return super.chooseNumber(player, sa, min, max, params);
}
......@@ -370,30 +389,49 @@ public class CountersRemoveAi extends SpellAbilityAi {
return super.chooseCounterType(options, sa, params);
}
Player ai = sa.getActivatingPlayer();
Card target = (Card) params.get("Target");
if (target.getController().isOpponentOf(ai)) {
// if its a Planeswalker try to remove Loyality first
if (target.isPlaneswalker()) {
return CounterType.LOYALTY;
}
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, target)) {
return type;
GameEntity target = (GameEntity) params.get("Target");
if (target instanceof Card) {
Card targetCard = (Card) target;
if (targetCard.getController().isOpponentOf(ai)) {
// if its a Planeswalker try to remove Loyality first
if (targetCard.isPlaneswalker()) {
return CounterType.LOYALTY;
}
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
return type;
}
}
} else {
if (options.contains(CounterType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterType.M1M1;
} else if (options.contains(CounterType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterType.P1P1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, targetCard)) {
return type;
}
}
}
} else {
if (options.contains(CounterType.M1M1) && target.hasKeyword(Keyword.PERSIST)) {
return CounterType.M1M1;
} else if (options.contains(CounterType.P1P1) && target.hasKeyword(Keyword.UNDYING)) {
return CounterType.M1M1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, target)) {
return type;
} else if (target instanceof Player) {
Player targetPlayer = (Player) target;
if (targetPlayer.isOpponentOf(ai)) {
for (CounterType type : options) {
if (!type.equals(CounterType.POISON)) {
return type;
}
}
} else {
for (CounterType type : options) {
if (type.equals(CounterType.POISON)) {
return type;
}
}
}
}
return super.chooseCounterType(options, sa, params);
}
}
......@@ -75,7 +75,7 @@ public class DamageDealAi extends DamageAiBase {
// Set PayX here to maximum value.
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
}
......@@ -115,7 +115,7 @@ public class DamageDealAi extends DamageAiBase {
}
// Set PayX here to maximum value. It will be adjusted later depending on the target.
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
} else if (sa.getSVar(damage).contains("InYourHand") && source.getZone().is(ZoneType.Hand)) {
dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
......@@ -283,7 +283,7 @@ public class DamageDealAi extends DamageAiBase {
if (sourceName.equals("Crater's Claws") && ai.hasFerocious()) {
actualPay = actualPay > 2 ? actualPay - 2 : 0;
}
source.setSVar("PayX", Integer.toString(actualPay));
sa.setSVar("PayX", Integer.toString(actualPay));
}
}
......@@ -291,7 +291,7 @@ public class DamageDealAi extends DamageAiBase {
// Check to ensure that we have enough counters to remove per the defined PayX
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(source.getSVar("PayX"))) {
if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(sa.getSVar("PayX"))) {
return false;
}
break;
......@@ -717,8 +717,7 @@ public class DamageDealAi extends DamageAiBase {
}
if (phase.is(PhaseType.MAIN2) && sa.isAbility()) {
if (sa.getRestrictions().isPwAbility()
|| source.hasSVar("EndOfTurnLeavePlay"))
if (sa.isPwAbility() || source.hasSVar("EndOfTurnLeavePlay"))
freePing = true;
}
}
......@@ -942,7 +941,7 @@ public class DamageDealAi extends DamageAiBase {
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
// Set PayX here to maximum value.
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
......@@ -972,7 +971,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
source.setSVar("PayX", Integer.toString(actualPay));
sa.setSVar("PayX", Integer.toString(actualPay));
}
}
......@@ -1032,7 +1031,7 @@ public class DamageDealAi extends DamageAiBase {
saTgt.resetTargets();
saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent);
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
return true;
}
......
package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
......@@ -42,7 +46,9 @@ public class DigAi extends SpellAbilityAi {
if ("Never".equals(sa.getParam("AILogic"))) {
return false;
} else if ("AtOppEndOfTurn".equals(sa.getParam("AILogic"))) {
return game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN);
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
return false;
}
}
// don't deck yourself
......@@ -124,6 +130,25 @@ public class DigAi extends SpellAbilityAi {
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> valid, boolean isOptional, Player relatedPlayer) {
if ("DigForCreature".equals(sa.getParam("AILogic"))) {
Card bestChoice = ComputerUtilCard.getBestCreatureAI(valid);
if (bestChoice == null) {
// no creatures, but maybe there's a morphable card that can be played as a creature?
CardCollection morphs = CardLists.filter(valid, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return card.hasKeyword(Keyword.MORPH);
}
});
if (!morphs.isEmpty()) {
bestChoice = ComputerUtilCard.getBestAI(morphs);
}
}
// still nothing, so return the worst card since it'll be unplayable from exile (e.g. Vivien, Champion of the Wilds)
return bestChoice != null ? bestChoice : ComputerUtilCard.getWorstAI(valid);
}
if (sa.getActivatingPlayer().isOpponentOf(ai) && relatedPlayer.isOpponentOf(ai)) {
return ComputerUtilCard.getWorstPermanentAI(valid, false, true, false, false);
} else {
......
......@@ -211,9 +211,11 @@ public class DrawAi extends SpellAbilityAi {
private boolean targetAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
final Card source = sa.getHostCard();
final boolean drawback = sa.getParent() != null;
final Game game = ai.getGame();
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean considerPrimary = logic.equals("ConsiderPrimary");
final boolean drawback = (sa.getParent() != null) && !considerPrimary;
boolean assumeSafeX = false; // if true, the AI will assume that the X value has been set to a value that is safe to draw
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
......@@ -241,7 +243,12 @@ public class DrawAi extends SpellAbilityAi {
numCards = Integer.parseInt(source.getSVar("PayX"));
} else {
numCards = ComputerUtilMana.determineLeftoverMana(sa, ai);
// try not to overdraw
int safeDraw = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent
numCards = Math.min(numCards, safeDraw);
source.setSVar("PayX", Integer.toString(numCards));
assumeSafeX = true;
}
xPaid = true;
}
......@@ -492,7 +499,8 @@ public class DrawAi extends SpellAbilityAi {
if ((computerHandSize + numCards > computerMaxHandSize)
&& game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()) {
&& !sa.isTrigger()
&& !assumeSafeX) {
// Don't draw too many cards and then risk discarding cards at
// EOT
if (!drawback) {
......
......@@ -4,7 +4,6 @@ import com.google.common.collect.Maps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
......@@ -104,7 +103,7 @@ public class ManifestAi extends SpellAbilityAi {
// check to ensure that there are no replacement effects that prevent creatures ETBing from library
// (e.g. Grafdigger's Cage)
Card topCopy = CardUtil.getLKICopy(library.getFirst());
topCopy.setState(CardStateName.FaceDown, false);
topCopy.turnFaceDownNoUpdate();
topCopy.setManifested(true);
final Map<String, Object> repParams = Maps.newHashMap();
......
......@@ -20,7 +20,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityRestriction;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
......@@ -288,9 +287,8 @@ public class PumpAi extends PumpAiBase {
}
if (sa.hasParam("ActivationNumberSacrifice")) {
final SpellAbilityRestriction restrict = sa.getRestrictions();
final int sacActivations = Integer.parseInt(sa.getParam("ActivationNumberSacrifice").substring(2));
final int activations = restrict.getNumberTurnActivations();
final int activations = sa.getActivationsThisTurn();
// don't risk sacrificing a creature just to pump it
if (activations >= sacActivations - 1) {
return false;
......
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiPlayDecision;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.player.Player;
......@@ -51,6 +54,32 @@ public class RevealAi extends RevealAiBase {
return false;
}
if ("Kefnet".equals(sa.getParam("AILogic"))) {
final Card c = Iterables.getFirst(
AbilityUtils.getDefinedCards(sa.getHostCard(), "RevealDefined", sa), null
);
if (c == null || (!c.isInstant() && !c.isSorcery())) {
return false;
}
for (SpellAbility s : c.getBasicSpells()) {
Spell spell = (Spell) s.copy(ai);
// timing restrictions still apply
if (!s.getRestrictions().checkTimingRestrictions(c, s))
continue;
// use hard coded reduce cost
spell.getMapParams().put("ReduceCost", "2");
if (AiPlayDecision.WillPlay == ((PlayerControllerAi) ai.getController()).getAi()
.canPlayFromEffectAI(spell, false, false)) {
return true;
}
}
return false;
}
if (!revealHandTargetAI(ai, sa/*, false, mandatory*/)) {
return false;
}
......
......@@ -78,7 +78,7 @@ public class TokenAi extends SpellAbilityAi {
// Planeswalker-related flags
boolean pwMinus = false;
boolean pwPlus = false;
if (sa.getRestrictions().isPwAbility()) {
if (sa.isPwAbility()) {
/*
* Planeswalker token ability with loyalty costs should be played in
* Main1 or it might never be used due to other positive abilities.
......@@ -230,7 +230,7 @@ public class TokenAi extends SpellAbilityAi {
alwaysOnOppAttack = aic.getBooleanProperty(AiProps.TOKEN_GENERATION_ALWAYS_IF_OPP_ATTACKS);
}
if (sa.getRestrictions() != null && sa.getRestrictions().isPwAbility() && alwaysFromPW) {
if (sa.isPwAbility() && alwaysFromPW) {
return true;
} else if (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
......
......@@ -19,10 +19,7 @@ import forge.game.GameObjectMap;
import forge.game.GameRules;
import forge.game.Match;
import forge.game.StaticEffect;
import forge.game.card.Card;
import forge.game.card.CardFactory;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.keyword.KeywordInterface;
......@@ -267,6 +264,7 @@ public class GameCopier {
System.err.println(sa.toString());
}
}
return newCard;
}
......@@ -295,6 +293,8 @@ public class GameCopier {
newCard.setChangedCardTypes(c.getChangedCardTypesMap());
newCard.setChangedCardKeywords(c.getChangedCardKeywords());
newCard.setChangedCardNames(c.getChangedCardNames());
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such?
for (KeywordInterface kw : c.getHiddenExtrinsicKeywords())
newCard.addHiddenExtrinsicKeyword(kw);
......@@ -305,7 +305,7 @@ public class GameCopier {
if (c.isFaceDown()) {
boolean isCreature = newCard.isCreature();
boolean hasManaCost = !newCard.getManaCost().isNoCost();
newCard.setState(CardStateName.FaceDown, true);
newCard.turnFaceDown(true);
if (c.isManifested()) {
newCard.setManifested(true);
// TODO: Should be able to copy other abilities...
......@@ -324,17 +324,23 @@ public class GameCopier {
if (c.isPlaneswalker()) {
for (SpellAbility sa : c.getAllSpellAbilities()) {
SpellAbilityRestriction restrict = sa.getRestrictions();
if (restrict.isPwAbility() && restrict.getNumberTurnActivations() > 0) {
int active = sa.getActivationsThisTurn();
if (sa.isPwAbility() && active > 0) {
SpellAbility newSa = findSAInCard(sa, newCard);
if (newSa != null) {
for (int i = 0; i < restrict.getNumberTurnActivations(); i++) {
newSa.getRestrictions().abilityActivated();
for (int i = 0; i < active; i++) {
newCard.addAbilityActivated(newSa);
}
}
}
}
}
newCard.setFlipped(c.isFlipped());
for (Map.Entry<Long, CardCloneStates> e : c.getCloneStates().entrySet()) {
newCard.addCloneState(e.getValue().copy(newCard, true), e.getKey());
}
Map<CounterType, Integer> counters = c.getCounters();
if (!counters.isEmpty()) {
newCard.setCounters(Maps.newEnumMap(counters));
......
......@@ -194,6 +194,8 @@ public class GameSimulator {
System.out.println();
}
final SpellAbility playingSa = sa;
simGame.copyLastState();
ComputerUtil.handlePlayingSpellAbility(aiPlayer, sa, simGame, new Runnable() {
@Override
public void run() {
......
......@@ -148,7 +148,7 @@ public class SpellAbilityPicker {
if (sa.isSpell()) {
return !sa.getHostCard().isInstant() && !sa.getHostCard().withFlash(player);
}
if (sa.getRestrictions().isPwAbility()) {
if (sa.isPwAbility()) {
return !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed.");
}
return sa.isAbility() && sa.getRestrictions().isSorcerySpeed();
......
......@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.24-SNAPSHOT</version>
<version>1.6.26-SNAPSHOT</version>
</parent>
<artifactId>forge-core</artifactId>
......