Compare commits

..

6 Commits

Author SHA1 Message Date
Hans Mackowiak
c32b84b40c Update EffectAi.java 2025-07-30 11:19:02 +02:00
Hans Mackowiak
6962c2cf99 EffectAi: first try of generic cantAttack and cantBlock 2025-07-30 11:14:11 +02:00
Hans Mackowiak
b7ec60863a ~ start of DetainAi 2025-07-30 11:14:11 +02:00
Hans Mackowiak
239cf3ece7 more hidden keywords to effects 2025-07-30 11:14:11 +02:00
Hans Mackowiak
06781fb6ff moved another part to EffectEffect 2025-07-30 11:14:11 +02:00
Hans Mackowiak
a85f00043b Add DetainEffect 2025-07-30 11:14:11 +02:00
1597 changed files with 31603 additions and 42560 deletions

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: ['17', '21']
java: [ '17' ]
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3

7
.gitignore vendored
View File

@@ -66,9 +66,6 @@ forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat
# Generated changelog file
forge-gui/release-files/CHANGES.txt
forge-gui/res/PerSetTrackingResults
forge-gui/res/decks
forge-gui/res/layouts
@@ -90,7 +87,3 @@ forge-gui/tools/PerSetTrackingResults
*.tiled-session
/forge-gui/res/adventure/*.tiled-project
/forge-gui/res/adventure/*.tiled-session
# Ignore python temporaries
__pycache__
*.pyc

View File

@@ -0,0 +1,33 @@
Summary
(Summarize the bug encountered concisely)
Steps to reproduce
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
What is the current bug behavior?
(What actually happens)
What is the expected correct behavior?
(What you should see instead)
Relevant logs and/or screenshots
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
Possible fixes
(If you can, link to the line of code that might be responsible for the problem)
/label ~needs-investigation

View File

@@ -0,0 +1,15 @@
Summary
(Summarize the feature you wish concisely)
Example screenshots
(If this is a UI change, please provide an example screenshot of how this feature might work)
Feature type
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
/label ~feature request

View File

@@ -15,7 +15,7 @@ public class Main {
public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
GuiBase.setDeviceInfo("", "", 0, 0);
new EditorMainWindow(Config.instance());
}
}

View File

@@ -59,6 +59,7 @@ public class AiCardMemory {
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
BOUNCED_THIS_TURN, // These cards were bounced this turn
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part

View File

@@ -66,11 +66,13 @@ import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import java.util.*;
import java.util.concurrent.FutureTask;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -481,7 +483,7 @@ public class AiController {
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.Backside).getType().isLand())) {
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
return false;
}
@@ -1664,7 +1666,8 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
final ExecutorService executor = Executors.newSingleThreadExecutor();
Future<SpellAbility> future = executor.submit(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1742,18 +1745,11 @@ public class AiController {
return null;
});
Thread t = new Thread(future);
t.start();
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
try {
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
try {
t.stop();
} catch (UnsupportedOperationException ex) {
// Android and Java 20 dropped support to stop so sadly thread will keep running
future.cancel(true);
}
future.cancel(true);
return null;
}
}

View File

@@ -563,7 +563,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
removed += thisRemove;
table.put(null, prefCard, cType, thisRemove);
table.put(null, prefCard, CounterType.get(cType), thisRemove);
}
}
}
@@ -573,7 +573,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
if (c <= 0) {
return null;
@@ -716,7 +716,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) {
toRemove += over;
table.put(null, crd, CounterEnumType.QUEST, over);
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
}
}
}

View File

@@ -19,7 +19,7 @@ public enum AiPlayDecision {
StackNotEmpty,
AnotherTime,
// Don't play decision reasons
// Don't play decision reasons,
CantPlaySa,
CantPlayAi,
CantAfford,

View File

@@ -767,7 +767,7 @@ public class ComputerUtil {
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
if (untap) {
typeList.remove(activate);
@@ -1074,80 +1074,6 @@ public class ComputerUtil {
return prevented;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
@@ -1319,6 +1245,80 @@ public class ComputerUtil {
return false;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final SpellAbility sub = sa.getSubAbility();
@@ -1327,6 +1327,7 @@ public class ComputerUtil {
return true;
}
// Cipher spells
if (sub != null) {
final ApiType api = sub.getApi();
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
@@ -1621,6 +1622,7 @@ public class ComputerUtil {
damage = dmg;
}
// Triggered abilities
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
for (final Trigger t : c.getTriggers()) {
if (TriggerType.Attacks.equals(t.getMode())) {
@@ -2542,7 +2544,7 @@ public class ComputerUtil {
boolean opponent = controller.isOpponentOf(ai);
final CounterType p1p1Type = CounterEnumType.P1P1;
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
if (!sa.hasParam("AILogic")) {
return Aggregates.random(options);
@@ -2551,7 +2553,7 @@ public class ComputerUtil {
String logic = sa.getParam("AILogic");
switch (logic) {
case "Torture":
return options.get(1);
return "Torture";
case "GraceOrCondemnation":
List<ZoneType> graceZones = new ArrayList<ZoneType>();
graceZones.add(ZoneType.Battlefield);
@@ -2559,12 +2561,12 @@ public class ComputerUtil {
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
return options.get(aiGrace > humanGrace ? 0 : 1);
return aiGrace > humanGrace ? "Grace" : "Condemnation";
case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
CardCollection computerlist = ai.getCreaturesInPlay();
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
case "Judgment":
if (votes.isEmpty()) {
CardCollection list = new CardCollection();
@@ -2578,71 +2580,68 @@ public class ComputerUtil {
return Iterables.getFirst(votes.keySet(), null);
case "Protection":
if (votes.isEmpty()) {
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
List<String> restrictedToColors = Lists.newArrayList();
for (Object o : options) {
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
restrictedToColors.put(sp.getOriginalDescription(), sp);
if (o instanceof String) {
restrictedToColors.add((String) o);
}
}
}
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
}
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
SpellAbility feather = (SpellAbility)options.get(0);
SpellAbility quill = (SpellAbility)options.get(1);
// try to mill opponent with Quill vote
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
int numQuill = votes.get(quill).size();
int numQuill = votes.get("Quill").size();
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? feather : quill;
return opponent ? "Feather" : "Quill";
}
// if source is not on the battlefield anymore, choose +1/+1 ones
if (!game.getCardState(source).isInPlay()) {
return opponent ? feather : quill;
return opponent ? "Feather" : "Quill";
}
// if no hand cards, try to mill opponent
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
return opponent ? quill : feather;
return opponent ? "Quill" : "Feather";
}
// AI has something to discard
if (ai.equals(controller)) {
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
return quill;
return "Quill";
}
}
// default card draw and discard are better than +1/+1 counter
return opponent ? feather : quill;
return opponent ? "Feather" : "Quill";
case "StrengthOrNumbers":
SpellAbility strength = (SpellAbility)options.get(0);
SpellAbility numbers = (SpellAbility)options.get(1);
// similar to fabricate choose +1/+1 or Token
int numStrength = votes.get(strength).size();
int numNumbers = votes.get(numbers).size();
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
int numStrength = votes.get("Strength").size();
int numNumbers = votes.get("Numbers").size();
Card token = TokenAi.spawnToken(controller, numbers);
Card token = TokenAi.spawnToken(controller, saToken);
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? strength : numbers;
return opponent ? "Strength" : "Numbers";
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? strength : numbers;
return opponent ? "Strength" : "Numbers";
}
// token would not survive
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
return opponent ? numbers : strength;
return opponent ? "Numbers" : "Strength";
}
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
@@ -2663,40 +2662,35 @@ public class ComputerUtil {
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
case "SproutOrHarvest":
SpellAbility sprout = (SpellAbility)options.get(0);
SpellAbility harvest = (SpellAbility)options.get(1);
// lifegain would hurt or has no effect
if (opponent) {
if (lifegainNegative(controller, source)) {
return harvest;
return "Harvest";
}
} else {
if (lifegainNegative(controller, source)) {
return sprout;
return "Sprout";
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? sprout : harvest;
return opponent ? "Sprout" : "Harvest";
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? sprout : harvest;
return opponent ? "Sprout" : "Harvest";
}
// TODO add Lifegain to +1/+1 counters trigger
// for now +1/+1 counters are better
return opponent ? harvest : sprout;
return opponent ? "Harvest" : "Sprout";
case "DeathOrTaxes":
SpellAbility death = (SpellAbility)options.get(0);
SpellAbility taxes = (SpellAbility)options.get(1);
int numDeath = votes.get(death).size();
int numTaxes = votes.get(taxes).size();
int numDeath = votes.get("Death").size();
int numTaxes = votes.get("Taxes").size();
if (opponent) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
@@ -2704,29 +2698,29 @@ public class ComputerUtil {
// would need to sacrifice more creatures than AI has
// sacrifice even more
if (aiCreatures.size() <= numDeath) {
return death;
return "Death";
}
// would need to discard more cards than it has
if (aiCardsInHand.size() <= numTaxes) {
return taxes;
return "Taxes";
}
// has cards with SacMe or Token
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
return death;
return "Death";
}
// has cards with DiscardMe
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
return taxes;
return "Taxes";
}
// discard is probably less worse than sacrifice
return taxes;
return "Taxes";
} else {
// ai is first voter or ally of controller
// both are not affected, but if opponents control creatures, sacrifice is worse
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
}
default:
return Iterables.getFirst(options, null);
@@ -2743,7 +2737,7 @@ public class ComputerUtil {
return safeCards;
}
public static Card getKilledByTargeting(final SpellAbility sa, CardCollectionView validCards) {
public static Card getKilledByTargeting(final SpellAbility sa, Iterable<Card> validCards) {
CardCollection killables = CardLists.filter(validCards, c -> c.getController() != sa.getActivatingPlayer() && c.getSVar("Targeting").equals("Dies"));
return ComputerUtilCard.getBestCreatureAI(killables);
}

View File

@@ -919,14 +919,14 @@ public class ComputerUtilCard {
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
}
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
for (byte c : MagicColor.WUBRG) {
if ((colors & c) != 0) {
return MagicColor.toLongString(c);
}
}
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
return restrictedToColors.get(0); // no difference, there was no prominent color
}
public static List<String> getColorByProminence(final List<Card> list) {

View File

@@ -974,13 +974,17 @@ public class ComputerUtilCombat {
continue;
}
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) {
continue;
}
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -994,11 +998,12 @@ public class ComputerUtilCombat {
continue;
}
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
power += pBonus;
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
}
}
@@ -1102,13 +1107,17 @@ public class ComputerUtilCombat {
continue;
}
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) {
continue;
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -1122,11 +1131,12 @@ public class ComputerUtilCombat {
continue;
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
toughness += tBonus;
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
}
}
return toughness;
@@ -1295,7 +1305,6 @@ public class ComputerUtilCombat {
continue;
}
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) {
continue;
@@ -1305,8 +1314,11 @@ public class ComputerUtilCombat {
continue;
}
if (!ability.getPayCosts().hasTapCost()) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
@@ -1321,14 +1333,13 @@ public class ComputerUtilCombat {
continue;
}
if (!ability.getPayCosts().hasTapCost()) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
}
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
power += pBonus;
}
}
return power;
}
@@ -1519,14 +1530,16 @@ public class ComputerUtilCombat {
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
continue;
}
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
continue;
}
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) {
continue;
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -1540,11 +1553,10 @@ public class ComputerUtilCombat {
continue;
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
toughness += tBonus;
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
}
return toughness;

View File

@@ -291,12 +291,6 @@ public class ComputerUtilMana {
continue;
}
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
if (amount <= 0) {
// wrong gamestate for variable amount
continue;
}
if (sa.getApi() == ApiType.Animate) {
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined"))
@@ -449,6 +443,7 @@ public class ComputerUtilMana {
manaProduced = manaProduced.replace(s, color);
}
} else if (saMana.hasParam("ReplaceColor")) {
// replace color
String color = saMana.getParam("ReplaceColor");
if ("Chosen".equals(color)) {
if (card.hasChosenColor()) {
@@ -740,8 +735,7 @@ public class ComputerUtilMana {
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
// not a good idea to sac a card that you're targeting with the SA you're paying for
saExcludeList.add(saPayment);
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
continue;
}
}
@@ -1502,7 +1496,7 @@ public class ComputerUtilMana {
}
if (!cost.isReusuableResource()) {
for (CostPart part : cost.getCostParts()) {
for(CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
unpreferredCost = true;
}
@@ -1593,8 +1587,10 @@ public class ComputerUtilMana {
// don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
}
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list

View File

@@ -264,14 +264,12 @@ public abstract class GameState {
}
if (c.hasMergedCard()) {
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
// we have to go by the current top card name here
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
} else {
String suffix = c.hasPaperFoil() ? "+" : "";
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
.append("|Art:").append(c.getPaperCard().getArtIndex());
}
}
@@ -321,21 +319,18 @@ public abstract class GameState {
newText.append(":Cloaked");
}
}
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
if (c.getMeldedWith() != null) {
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
newText.append(":");
newText.append(c.getMeldedWith().getName()).append(suffix);
}
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
if (c.isModal()) {
newText.append("|Modal");
} else {
newText.append("|Transformed");
newText.append(c.getMeldedWith().getName());
}
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
if (c.getPlayerAttachedTo() != null) {
@@ -1316,8 +1311,8 @@ public abstract class GameState {
if (info.endsWith("Cloaked")) {
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
}
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
c.setState(CardStateName.Backside, true);
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
c.setBackSide(true);
} else if (info.startsWith("Flipped")) {
c.setState(CardStateName.Flipped, true);
@@ -1335,6 +1330,9 @@ public abstract class GameState {
}
c.setState(CardStateName.Meld, true);
c.setBackSide(true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
c.setBackSide(true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";

View File

@@ -1347,11 +1347,6 @@ public class PlayerControllerAi extends PlayerController {
// Ai won't understand that anyway
}
@Override
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Ai won't understand that anyway
}
@Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto

View File

@@ -171,7 +171,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}

View File

@@ -144,7 +144,7 @@ public abstract class SpellAbilityAi {
return false;
}
if (!"Once".equals(aiLogic)) {
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
return !AiCardMemory.isRememberedCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
return true;
}
@@ -153,7 +153,7 @@ public abstract class SpellAbilityAi {
* The rest of the logic not covered by the canPlayAI template is defined here
*/
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
if (MyRandom.getRandom().nextFloat() < .8f) {
// 80% chance to play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -77,6 +77,7 @@ public enum SpellApiToAi {
.put(ApiType.DelayedTrigger, DelayedTriggerAi.class)
.put(ApiType.Destroy, DestroyAi.class)
.put(ApiType.DestroyAll, DestroyAllAi.class)
.put(ApiType.Detain, DetainAi.class)
.put(ApiType.Dig, DigAi.class)
.put(ApiType.DigMultiple, DigMultipleAi.class)
.put(ApiType.DigUntil, DigUntilAi.class)

View File

@@ -83,7 +83,7 @@ public class AddTurnAi extends SpellAbilityAi {
*/
@Override
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return doTriggerNoCost(aiPlayer, sa, false);
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -159,78 +159,81 @@ public class AnimateAi extends SpellAbilityAi {
}
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
sa.setXManaCostPaid(xPay);
}
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
}
if (!sa.usesTargeting()) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped()
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
&& !c.isEquipping();
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped()
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
&& !c.isEquipping();
// for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
if (!c.hasKeyword(keyword)) {
bFlag = true;
// for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
if (!c.hasKeyword(keyword)) {
bFlag = true;
}
}
}
}
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
// also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, source)) {
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
// also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
}
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else {
sa.resetTargets();
return animateTgtAI(sa);
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, source);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
@@ -252,7 +255,8 @@ public class AnimateAi extends SpellAbilityAi {
return decision;
} else if (!mandatory) {
return decision;
} else {
}
else {
// fallback if animate is mandatory
sa.resetTargets();
List<Card> list = CardUtil.getValidCardsToTarget(sa);
@@ -273,13 +277,8 @@ public class AnimateAi extends SpellAbilityAi {
}
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
if (sa.getMaxTargets() == 0) {
// this happens if an optional cost is skipped, e.g. Brave the Wilds
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
}
final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean alwaysActivatePWAbility = sa.isPwAbility()
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
@@ -291,8 +290,10 @@ public class AnimateAi extends SpellAbilityAi {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
final Game game = ai.getGame();
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
// list is empty, no possible targets
@@ -397,8 +398,7 @@ public class AnimateAi extends SpellAbilityAi {
}
if (logic.equals("SetPT")) {
// TODO: 1. Teach the AI to use this to save the creature from direct damage;
// 2. Determine the best target in a smarter way?
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
Card buffed = becomeAnimated(worst, sa);
@@ -435,9 +435,11 @@ public class AnimateAi extends SpellAbilityAi {
if (sa.hasParam("AITgts") && !list.isEmpty()) {
//No logic, but we do have preferences. Pick the best among those?
Card best = ComputerUtilCard.getBestAI(list);
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
if(best != null) {
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or

View File

@@ -988,7 +988,6 @@ public class AttachAi extends SpellAbilityAi {
}
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);

View File

@@ -289,7 +289,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean.
*/
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -453,7 +455,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
final AbilitySub subAb = sa.getSubAbility();
if (subAb == null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
}
/**
@@ -499,6 +506,19 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
if (sa.hasParam("AILogic")) {
if (sa.getParam("AILogic").equals("Never")) {
/*
* Hack to stop AI from using Aviary Mechanic's "may bounce" trigger.
* Ideally it should look for a good bounce target like "Pacifism"-victims
* but there is no simple way to check that. It is preferable for the AI
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
* (bounce useful permanent).
*/
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
List<ZoneType> origin = new ArrayList<>();
if (sa.hasParam("Origin")) {
origin = ZoneType.listValueOf(sa.getParam("Origin"));
@@ -753,7 +773,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
final AbilitySub subAb = sa.getSubAbility();
if (subAb == null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
}
/*
@@ -1594,7 +1619,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList);
} else if (logic.equals("BounceOwnTrigger")) {
return doBounceOwnTriggerLogic(decider, sa, fetchList);
return doBounceOwnTriggerLogic(decider, fetchList);
}
}
if (fetchList.isEmpty()) {
@@ -2158,18 +2183,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
}
private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardCollection choices) {
private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
// TODO check for threatened cards
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
if (!prefChoices.isEmpty()) {
return ComputerUtilCard.getBestAI(prefChoices);
}
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
// some extra benefit like First Responder
} else if (!unprefChoices.isEmpty()) {
return ComputerUtilCard.getWorstAI(unprefChoices);
} else {
return null;
}
return null;
}
@Override

View File

@@ -286,6 +286,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
// Change Zone All, can be any type moving from one zone to another
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);

View File

@@ -9,6 +9,7 @@ import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.Collections;
@@ -88,7 +89,12 @@ public class CharmAi extends SpellAbilityAi {
CharmEffect.chainAbilities(sa, chosenList);
}
return super.checkApiLogic(ai, sa);
// prevent run-away activations - first time will always return true
if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {

View File

@@ -78,8 +78,10 @@ public class ChooseCardNameAi extends SpellAbilityAi {
}
if (mandatory) {
// If mandatory, then we will play it.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If not mandatory, then we won't play it.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -63,7 +63,8 @@ public class ChooseNumberAi extends SpellAbilityAi {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} {
return canPlay(ai, sa);
}
return canPlay(ai, sa);
}
}

View File

@@ -20,7 +20,7 @@ import java.util.Map;
public class CloneAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
@@ -38,6 +38,10 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?"
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
}
PhaseHandler phase = game.getPhaseHandler();
if (!sa.usesTargeting()) {
@@ -73,7 +77,7 @@ public class CloneAi extends SpellAbilityAi {
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} // end cloneCanPlayAI()
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {

View File

@@ -42,7 +42,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (!MyRandom.percentTrue(chance)
&& !"Always".equals(logic)
&& !"AlwaysIfViable".equals(logic)
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@@ -93,6 +93,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (decision == AiPlayDecision.WillPlay) {
sa.getTargets().add(top);
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, decision);
@@ -114,6 +115,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (logic.contains("Always")) {
// If the logic is "Always" or "AlwaysIfViable", we will always play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@@ -124,11 +126,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
}
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
}
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (!decision.willingToPlay()) {
if (sa.isMandatory()) {

View File

@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
} else if (type.equals("DIVINITY")) {
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
} else if (CounterType.getType(type).isKeywordCounter()) {
} else if (CounterType.get(type).isKeywordCounter()) {
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
} else {
// The AI really should put counters on cards that can use it.

View File

@@ -20,40 +20,41 @@ public class CountersMultiplyAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
return setTargets(ai, sa);
}
final CounterType counterType = getCounterType(sa);
// defined are mostly Self or Creatures you control
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
list = CardLists.filter(list, c -> {
if (!c.hasCounters()) {
return false;
}
if (!sa.usesTargeting()) {
// defined are mostly Self or Creatures you control
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (counterType != null) {
if (c.getCounters(counterType) <= 0) {
list = CardLists.filter(list, c -> {
if (!c.hasCounters()) {
return false;
}
if (!c.canReceiveCounters(counterType)) {
return false;
}
} else {
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
// has negative counter it would double
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
if (counterType != null) {
if (c.getCounters(counterType) <= 0) {
return false;
}
if (!c.canReceiveCounters(counterType)) {
return false;
}
} else {
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
// has negative counter it would double
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
return false;
}
}
}
return true;
});
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return true;
});
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} else {
return setTargets(ai, sa);
}
return super.checkApiLogic(ai, sa);
@@ -154,7 +155,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}
if (counterType == null || counterType.is(type)) {
addTargetsByCounterType(ai, sa, aiList, type);
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
}
}
}
@@ -163,7 +164,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!oppList.isEmpty()) {
// not enough targets
if (sa.canAddMoreTarget()) {
final CounterType type = CounterEnumType.M1M1;
final CounterType type = CounterType.get(CounterEnumType.M1M1);
if (counterType == null || counterType == type) {
addTargetsByCounterType(ai, sa, oppList, type);
}

View File

@@ -110,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Proliferate is always optional for all, no need to select best
final CounterType poison = CounterEnumType.POISON;
final CounterType poison = CounterType.get(CounterEnumType.POISON);
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
// because countertype can't be chosen anymore, only look for poison counters

View File

@@ -170,7 +170,7 @@ public class CountersPutAi extends CountersAi {
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1));
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
if (best != null) {
@@ -247,9 +247,10 @@ public class CountersPutAi extends CountersAi {
} else if (sa.getSubAbility() != null
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
// Bristling Hydra: save from death using a ping activation
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
@@ -336,7 +337,7 @@ public class CountersPutAi extends CountersAi {
Game game = ai.getGame();
Combat combat = game.getCombat();
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return doCombatAdaptLogic(source, amount, combat);
@@ -442,9 +443,11 @@ public class CountersPutAi extends CountersAi {
}
sa.addDividedAllocation(c, amount);
return decision;
} else if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
} else {
if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
}
}
}
}
@@ -606,21 +609,7 @@ public class CountersPutAi extends CountersAi {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
// adding counters would cause counter amount to overflow
if (Integer.MAX_VALUE - currCounters <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (type.equals("P1P1")) {
if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
// each non +1/+1 counter on the card is a 10% chance of not
// activating this ability.
@@ -635,7 +624,7 @@ public class CountersPutAi extends CountersAi {
}
// Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -682,12 +671,14 @@ public class CountersPutAi extends CountersAi {
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (sa.usesTargeting()) {
CardCollection list;
CardCollection list = null;
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
}
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty() && isMandatoryTrigger) {
@@ -703,8 +694,9 @@ public class CountersPutAi extends CountersAi {
|| sa.getTargets().isEmpty()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
break;
}
break;
}
if (sa.isCurse()) {
@@ -747,6 +739,9 @@ public class CountersPutAi extends CountersAi {
final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// boolean chance = true;
boolean preferred = true;
CardCollection list;
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose();
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
@@ -767,12 +762,26 @@ public class CountersPutAi extends CountersAi {
if ("ChargeToBestCMC".equals(aiLogic)) {
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
if (decision.willingToPlay()) {
// If the AI logic is to charge to best CMC, we can return true
// if the logic was successfully applied or if it's mandatory.
return decision;
}
if (mandatory) {
} else if (mandatory) {
// If the logic was not applied and it's mandatory, we return false.
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
// If the logic was not applied and it's not mandatory, we return false.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
AiAbilityDecision decision = doChargeToOppCtrlCMCLogic(ai, sa);
if (decision.willingToPlay()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (mandatory) {
// If the logic was not applied and it's mandatory, we return false.
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (!sa.usesTargeting()) {
@@ -823,19 +832,19 @@ public class CountersPutAi extends CountersAi {
}
}
sa.resetTargets();
Iterable<Card> filteredField;
if (sa.isCurse()) {
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
filteredField = ai.getCardsIn(ZoneType.Battlefield);
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
}
CardCollection list = CardLists.getTargetableCards(filteredField, sa);
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
boolean preferred = true;
list = CardLists.getTargetableCards(list, sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
int totalTargets = list.size();
sa.resetTargets();
while (sa.canAddMoreTarget()) {
if (mandatory) {
// When things are mandatory, gotta handle a little differently
@@ -860,7 +869,9 @@ public class CountersPutAi extends CountersAi {
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
Card choice = null;
@@ -872,21 +883,27 @@ public class CountersPutAi extends CountersAi {
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else if (type.equals("M1M1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
if (type.equals("M1M1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
}
}
} else if (preferred) {
list = ComputerUtil.getSafeTargets(ai, sa, list);
choice = chooseBoonTarget(list, type);
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else if (type.equals("P1P1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
if (preferred) {
list = ComputerUtil.getSafeTargets(ai, sa, list);
choice = chooseBoonTarget(list, type);
if (choice == null && mandatory) {
choice = Aggregates.random(list);
}
} else {
if (type.equals("P1P1")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = Aggregates.random(list);
}
}
}
if (choice != null && divided) {
int alloc = Math.max(amount / totalTargets, 1);
@@ -962,8 +979,8 @@ public class CountersPutAi extends CountersAi {
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Bolster does use this
// TODO need more or less logic there?
final CounterType m1m1 = CounterEnumType.M1M1;
final CounterType p1p1 = CounterEnumType.P1P1;
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
// no logic if there is no options or no to choice
if (!isOptional && Iterables.size(options) <= 1) {
@@ -1081,10 +1098,11 @@ public class CountersPutAi extends CountersAi {
Player ai = sa.getActivatingPlayer();
GameEntity e = (GameEntity) params.get("Target");
// for Card try to select not useless counter
if (e instanceof Card c) {
if (e instanceof Card) {
Card c = (Card) e;
if (c.getController().isOpponentOf(ai)) {
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.M1M1;
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.M1M1);
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, c)) {
@@ -1098,14 +1116,15 @@ public class CountersPutAi extends CountersAi {
}
}
}
} else if (e instanceof Player p) {
} else if (e instanceof Player) {
Player p = (Player) e;
if (p.isOpponentOf(ai)) {
if (options.contains(CounterEnumType.POISON)) {
return CounterEnumType.POISON;
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
return CounterType.get(CounterEnumType.POISON);
}
} else {
if (options.contains(CounterEnumType.EXPERIENCE)) {
return CounterEnumType.EXPERIENCE;
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
return CounterType.get(CounterEnumType.EXPERIENCE);
}
}
@@ -1209,8 +1228,10 @@ public class CountersPutAi extends CountersAi {
}
}
if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -1232,8 +1253,9 @@ public class CountersPutAi extends CountersAi {
if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -218,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
Card tgt = (Card) params.get("Target");
// planeswalker has high priority for loyalty counters
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) {
return CounterEnumType.LOYALTY;
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
return CounterType.get(CounterEnumType.LOYALTY);
}
if (tgt.getController().isOpponentOf(ai)) {
// creatures with BaseToughness below or equal zero might be
// killed if their counters are removed
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
if (options.contains(CounterEnumType.P1P1)) {
return CounterEnumType.P1P1;
} else if (options.contains(CounterEnumType.M1M1)) {
return CounterEnumType.M1M1;
if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterType.get(CounterEnumType.P1P1);
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterType.get(CounterEnumType.M1M1);
}
}
@@ -241,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
} else {
// this counters are treat first to be removed
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) {
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
if (maritEmpty) {
return CounterEnumType.ICE;
return CounterType.get(CounterEnumType.ICE);
}
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) {
return CounterEnumType.P1P1;
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) {
return CounterEnumType.M1M1;
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterType.get(CounterEnumType.P1P1);
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterType.get(CounterEnumType.M1M1);
}
// fallback logic, select positive counter to add more

View File

@@ -384,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (targetCard.getController().isOpponentOf(ai)) {
// if its a Planeswalker try to remove Loyality first
if (targetCard.isPlaneswalker()) {
return CounterEnumType.LOYALTY;
return CounterType.get(CounterEnumType.LOYALTY);
}
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
@@ -392,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
} else {
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterEnumType.M1M1;
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.P1P1;
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterType.get(CounterEnumType.M1M1);
} else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.P1P1);
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, targetCard)) {

View File

@@ -5,6 +5,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -20,6 +21,14 @@ public class DamageAllAi extends SpellAbilityAi {
// based on what the expected targets could be
final Card source = sa.getHostCard();
// abCost stuff that should probably be centralized...
final Cost abCost = sa.getPayCosts();
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
}
// wait until stack is empty (prevents duplicate kills)
if (!ai.getGame().getStack().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
@@ -133,9 +142,9 @@ public class DamageAllAi extends SpellAbilityAi {
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
// When using Pestilence to hurt players, do it at
// the end of the opponent's turn only
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
// Need further improvement : if able to kill immediately with repeated activations, do not wait
// for phases! Will also need to implement considering repeated activations for killed creatures!
// || (ai.sa.getPayCosts(). ??? )

View File

@@ -110,7 +110,15 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list;
// Targeting
if (sa.usesTargeting()) {
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
sa.getRootAbility().setXManaCostPaid(xPay);
}
// Assume there where already enough targets chosen by AI Logic Above
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
@@ -120,10 +128,7 @@ public class DestroyAi extends SpellAbilityAi {
sa.resetTargets();
int maxTargets;
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if (sa.getRootAbility().costHasManaX() ||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
if (sa.getRootAbility().costHasManaX()) {
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
// need to set XPaid to get the right number for

View File

@@ -0,0 +1,119 @@
package forge.ai.ability;
import java.util.List;
import java.util.function.Predicate;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class DetainAi extends SpellAbilityAi {
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
};
protected boolean prefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
final Game game = ai.getGame();
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.filter(list, CREATURE_OR_TAP_ABILITY);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (list.isEmpty()) {
return false;
}
while (sa.canAddMoreTarget()) {
Card choice = null;
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
return false;
}
}
PhaseHandler phase = game.getPhaseHandler();
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, list);
if (primeTarget != null) {
choice = primeTarget;
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Tap creatures possible blockers before combat during AI's turn.
List<Card> attackers;
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
//Combat has already started
attackers = game.getCombat().getAttackers();
} else {
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
attackers.remove(source);
}
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
// TODO check if own creature would be forced to attack and we want to keep it alive
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
} else if (phase.isPlayerTurn(opp)
&& phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
// Tap creatures possible blockers before combat during AI's turn.
if (list.anyMatch(CardPredicates.CREATURES)) {
List<Card> creatureList = CardLists.filter(list, c -> c.isCreature() && CombatUtil.canAttack(c, opp));
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else { // no creatures available
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
} else {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
return false;
} else {
if (!ComputerUtil.shouldCastLessThanMax(ai, source)) {
return false;
}
break;
}
}
list.remove(choice);
sa.getTargets().add(choice);
}
return true;
}
}

View File

@@ -26,6 +26,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -369,7 +370,7 @@ public class DrawAi extends SpellAbilityAi {
// try to make opponent lose to poison
// currently only Caress of Phyrexia
if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) {
if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (oppA.getPoisonCounters() + numCards > 9) {
sa.getTargets().add(oppA);
return true;
@@ -413,7 +414,7 @@ public class DrawAi extends SpellAbilityAi {
}
}
if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) {
if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (numCards + ai.getPoisonCounters() >= 8) {
aiTarget = false;
}
@@ -471,7 +472,7 @@ public class DrawAi extends SpellAbilityAi {
}
// ally would lose because of poison
if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) {
if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) {
continue;
}

View File

@@ -23,8 +23,10 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
@@ -37,17 +39,22 @@ public class EffectAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
final PhaseHandler phase = game.getPhaseHandler();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
String logic = "";
if (sa.hasParam("AILogic")) {
logic = sa.getParam("AILogic");
final PhaseHandler phase = game.getPhaseHandler();
if (logic.equals("BeginningOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("EndOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
for (Player opp : ai.getOpponents()) {
boolean worthHolding = false;
@@ -289,7 +296,6 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("YawgmothsWill")) {
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.startsWith("NeedCreatures")) {
// TODO convert to AiCheckSVar
if (ai.getCreaturesInPlay().isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@@ -324,8 +330,8 @@ public class EffectAi extends SpellAbilityAi {
Combat combat = game.getCombat();
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
// ideally needs once per combat or something
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
@@ -379,6 +385,118 @@ public class EffectAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else if (sa.hasParam("RememberObjects")) { //generic
boolean cantAttack = false;
boolean cantBlock = false;
boolean cantActivate = false;
String duraction = sa.getParam("Duration");
String matchStr = "Card.IsRemembered";
for (String st : sa.getParam("StaticAbilities").split(",")) {
Map<String, String> params = FileSection.parseToMap(sa.getSVar(st), FileSection.DOLLAR_SIGN_KV_SEPARATOR);
List<StaticAbilityMode> modes = StaticAbilityMode.listValueOf(params.get("Mode"));
if (modes.contains(StaticAbilityMode.CantAttack) && matchStr.equals(params.get("ValidCard"))) {
cantAttack = true;
}
if (modes.contains(StaticAbilityMode.CantBlock) && matchStr.equals(params.get("ValidCard"))) {
cantBlock = true;
}
if (modes.contains(StaticAbilityMode.CantBlockBy) && matchStr.equals(params.get("ValidBlocker"))) {
cantBlock = true;
}
if (modes.contains(StaticAbilityMode.CantBeActivated) && matchStr.equals(params.get("ValidCard"))) {
cantActivate = true;
}
}
// TODO add more cases later
if (!cantAttack && !cantBlock && !cantActivate) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (cantBlock && duraction == null && phase.isPlayerTurn(ai) && !phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.usesTargeting()) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa));
list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (list.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
List<Card> oppCreatures = CardLists.filterAsList(list, c -> {
return c.isCreature() && c.getController().isOpponentOf(ai);
});
List<Card> oppWithAbilities = CardLists.filterAsList(list, c -> {
return !c.isCreature() && c.getController().isOpponentOf(ai) && c.getSpellAbilities().anyMatch(SpellAbility::isActivatedAbility);
});
if (cantAttack || cantBlock) {
if (oppCreatures.isEmpty()) {
if (!cantActivate || oppWithAbilities.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
while (sa.canAddMoreTarget()) {
Card choice = null;
if (cantAttack && cantBlock && !oppCreatures.isEmpty()) {
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, oppCreatures);
if (primeTarget != null) {
choice = primeTarget;
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Tap creatures possible blockers before combat during AI's turn.
List<Card> attackers;
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
//Combat has already started
attackers = game.getCombat().getAttackers();
} else {
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
}
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
// TODO check if own creature would be forced to attack and we want to keep it alive
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
}
} // TODO add logic to tap non creatures with activated abilities if cantActivate is true
if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else {
if (!ComputerUtil.shouldCastLessThanMax(ai, sa.getHostCard())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
break;
}
}
list.remove(choice);
oppCreatures.remove(choice);
sa.getTargets().add(choice);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else { //no AILogic
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}

View File

@@ -1,15 +1,17 @@
package forge.ai.ability;
import com.google.common.collect.Sets;
import forge.ai.*;
import forge.game.Game;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.Game;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayLife;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
@@ -34,26 +36,6 @@ public class EndureAi extends SpellAbilityAi {
sa.getTargets().add(bestCreature);
}
// Card-specific logic
final String num = sa.getParamOrDefault("Num", "1");
if ("X".equals(num) && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
if (!aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
int curLife = aiPlayer.getLife();
int dangerLife = (((PlayerControllerAi) aiPlayer.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
if (curLife <= dangerLife) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
int availableMana = ComputerUtilMana.getAvailableManaEstimate(aiPlayer) - 1;
int maxEndureX = Math.min(availableMana, curLife - dangerLife);
if (maxEndureX > 0) {
sa.setXManaCostPaid(maxEndureX);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;

View File

@@ -76,8 +76,7 @@ public class ManaAi extends SpellAbilityAi {
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
}
if ("AtOppEOT".equals(logic)) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai
&& (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife());
return (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
@@ -159,7 +158,7 @@ public class ManaAi extends SpellAbilityAi {
int numCounters = 0;
int manaSurplus = 0;
if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
CounterType ctrType = CounterEnumType.KI; // Petalmane Baku
CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;
@@ -278,4 +277,10 @@ public class ManaAi extends SpellAbilityAi {
}
return !lose;
}
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
final String logic = sa.getParamOrDefault("AILogic", "");
return checkApiLogic(ai, sa);
}
}

View File

@@ -23,8 +23,10 @@ public class MeldAi extends SpellAbilityAi {
boolean hasPrimaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(primaryMeld).and(CardPredicates.isOwner(aiPlayer)));
boolean hasSecondaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(secondaryMeld).and(CardPredicates.isOwner(aiPlayer)));
if (hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld)) {
// If the primary meld card is on the battlefield and both meld cards are owned by the AI, play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the secondary meld card is on the battlefield and it is the one being activated, play the ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -29,8 +29,9 @@ public class MillAi extends SpellAbilityAi {
if (aiLogic.equals("Main1")) {
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|| ComputerUtil.castSpellInMain1(ai, sa);
} else if (aiLogic.equals("EndOfOppTurn")) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
} else if (aiLogic.equals("LilianaMill")) {
// TODO convert to AICheckSVar
// Only mill if a "Raise Dead" target is available, in case of control decks with few creatures
return CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES).size() >= 1;
}

View File

@@ -27,8 +27,7 @@ public class MustBlockAi extends SpellAbilityAi {
if (combat == null || !combat.isAttacking(source)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
} else if (AiCardMemory.isRememberedCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
// The AI can meaningfully do it only to one creature per card yet, trying to do it to multiple cards
// may result in overextending and losing the attacker
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
@@ -42,6 +41,7 @@ public class MustBlockAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(blocker);
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import forge.ai.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -28,6 +29,11 @@ public class PeekAndRevealAi extends SpellAbilityAi {
if (aiPlayer.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("EndOfOppTurn".equals(logic)) {
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
if (!(ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// So far this only appears on Triggers, but will expand
// once things get converted from Dig + NoMove

View File

@@ -78,7 +78,7 @@ public class PermanentCreatureAi extends PermanentAi {
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
&& ai.getManaPool().totalMana() <= 0
&& (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
&& !card.hasETBTrigger(true) && !card.hasSVar("AmbushAI")
&& (!card.hasETBTrigger(true) && !card.hasSVar("AmbushAI"))
&& game.getStack().isEmpty()
&& !ComputerUtil.castPermanentInMain1(ai, sa)) {
// AiPlayDecision.AnotherTime;

View File

@@ -153,8 +153,8 @@ public class PlayAi extends SpellAbilityAi {
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
final CardStateName state;
if (sa.hasParam("CastTransformed")) {
state = CardStateName.Backside;
options.forEach(c -> c.changeToState(CardStateName.Backside));
state = CardStateName.Transformed;
options.forEach(c -> c.changeToState(CardStateName.Transformed));
} else {
state = CardStateName.Original;
}

View File

@@ -6,6 +6,7 @@ import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.GameLossReason;
@@ -64,7 +65,7 @@ public class PoisonAi extends SpellAbilityAi {
boolean result;
if (sa.usesTargeting()) {
result = tgtPlayer(ai, sa, mandatory);
} else if (mandatory || !ai.canReceiveCounters(CounterEnumType.POISON)) {
} else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
// mandatory or ai is uneffected
result = true;
} else {
@@ -89,7 +90,7 @@ public class PoisonAi extends SpellAbilityAi {
PlayerCollection betterTgts = tgts.filter(input -> {
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return false;
} else if (!input.canReceiveCounters(CounterEnumType.POISON)) {
} else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
return false;
}
return true;
@@ -108,7 +109,7 @@ public class PoisonAi extends SpellAbilityAi {
if (tgts.isEmpty()) {
if (mandatory) {
// AI is uneffected
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterEnumType.POISON)) {
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
sa.getTargets().add(ai);
return true;
}
@@ -120,7 +121,7 @@ public class PoisonAi extends SpellAbilityAi {
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
return true;
}
return !input.canReceiveCounters(CounterEnumType.POISON);
return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON));
});
if (!betterAllies.isEmpty()) {
allies = betterAllies;

View File

@@ -7,6 +7,8 @@ import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostTapType;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -21,6 +23,13 @@ import java.util.*;
public class PumpAi extends PumpAiBase {
private static boolean hasTapCost(final Cost cost, final Card source) {
if (cost == null) {
return true;
}
return cost.hasSpecificCostType(CostTapType.class);
}
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("MoveCounter".equals(aiLogic)) {
@@ -87,7 +96,7 @@ public class PumpAi extends PumpAiBase {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Game game = ai.getGame();
boolean main1Preferred = "Main1IfAble".equals(sa.getParam("AILogic")) && ph.is(PhaseType.MAIN1, ai);
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
if (game.getStack().isEmpty() && hasTapCost(sa.getPayCosts(), sa.getHostCard())) {
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
return false;
}
@@ -338,13 +347,13 @@ public class PumpAi extends PumpAiBase {
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
//Targeted
if (!pumpTgtAI(ai, sa, defense, attack, false, false)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} // pumpPlayAI()
private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory,
boolean immediately) {
@@ -478,28 +487,18 @@ public class PumpAi extends PumpAiBase {
}
}
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().isPlayerTurn(ai)) {
list.remove(source);
}
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
list.remove(source);
}
}
// Detain target nonland permanent: don't target noncreature permanents that don't have
// any activated abilities.
if ("DetainNonLand".equals(sa.getParam("AILogic"))) {
list = CardLists.filter(list, CardPredicates.CREATURES.or(card -> {
for (SpellAbility sa1 : card.getSpellAbilities()) {
if (sa1.isActivatedAbility()) {
return true;
}
if (game.getStack().isEmpty()) {
// If the cost is tapping, don't activate before declare attack/block
if (sa.getPayCosts().hasTapCost()) {
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().isPlayerTurn(ai)) {
list.remove(sa.getHostCard());
}
return false;
}));
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
list.remove(sa.getHostCard());
}
}
}
// Filter AI-specific targets if provided

View File

@@ -147,7 +147,7 @@ public class PumpAllAi extends PumpAiBase {
// important to call canPlay first so targets are added if needed
AiAbilityDecision decision = canPlay(ai, sa);
if (mandatory && !decision.decision().willingToPlay()) {
if (mandatory && decision.decision().willingToPlay()) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return decision;

View File

@@ -37,12 +37,13 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
}
// Do it once per turn, generally (may be improved later)
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
if (AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (sa.usesTargeting()) {
// ability is targeted
sa.resetTargets();
PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
@@ -61,6 +62,14 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // could not find a valid target
}
if (!canTgtHuman || !canTgtAI) {
// can't target another player anyway, remember for no second activation this turn
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
} else {
// if it's just defined, no big deal
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
@@ -71,6 +80,8 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
// Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
AiAbilityDecision decision = canPlay(ai, sa);
if (decision.willingToPlay()) {
return decision;

View File

@@ -88,9 +88,9 @@ public class ScryAi extends SpellAbilityAi {
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
// even if there's no mana cost.
if (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || sa.getHostCard().isCreature())
&& !isSorcerySpeed(sa, ai)) {
if (logic.equals("AtOppEOT") || (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
&& !isSorcerySpeed(sa, ai))) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}

View File

@@ -21,6 +21,10 @@ public class SurveilAi extends SpellAbilityAi {
*/
@Override
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
sa.resetTargets();
sa.getTargets().add(ai);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@@ -88,6 +92,7 @@ public class SurveilAi extends SpellAbilityAi {
}
if (randomReturn) {
AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}

View File

@@ -20,6 +20,19 @@ import java.util.function.Predicate;
public abstract class TapAiBase extends SpellAbilityAi {
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
};
/**
* <p>
* tapTargetList.
@@ -102,34 +115,12 @@ public abstract class TapAiBase extends SpellAbilityAi {
final Game game = ai.getGame();
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
tapList = CardLists.filter(tapList, CardPredicates.CAN_TAP);
tapList = CardLists.filter(tapList, c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa1 : c.getSpellAbilities()) {
if (sa1.isAbility() && sa1.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
});
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
//use broader approach when the cost is a positive thing
if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) {
tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
tapList = CardLists.filter(tapList, c -> {
if (c.isCreature()) {
return true;
}
for (final SpellAbility sa12 : c.getSpellAbilities()) {
if (sa12.isAbility() && sa12.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
});
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
}
//try to exclude things that will already be tapped due to something on stack or because something is

View File

@@ -8,6 +8,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController;
@@ -39,7 +40,7 @@ public class TimeTravelAi extends SpellAbilityAi {
// so removing them is good; stuff on the battlefield is usually stuff like Vanishing or As Foretold, which favors adding Time
// counters for better effect, but exceptions should be added here).
Card target = (Card)params.get("Target");
return !ComputerUtil.isNegativeCounter(CounterEnumType.TIME, target);
return !ComputerUtil.isNegativeCounter(CounterType.get(CounterEnumType.TIME), target);
}
@Override

View File

@@ -26,6 +26,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
@@ -66,15 +67,17 @@ public class TokenAi extends SpellAbilityAi {
}
}
}
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
Card actualToken = spawnToken(ai, sa);
if (actualToken == null || (actualToken.isCreature() && actualToken.getNetToughness() < 1)) {
// planeswalker plus ability or sub-ability is useful
return pwPlus || sa.getSubAbility() != null;
final AbilitySub sub = sa.getSubAbility();
// useful
// no token created
return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkDrawback(sub, ai).willingToPlay()); // planeswalker plus ability or sub-ability is
}
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString());
String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString());
@@ -131,6 +134,9 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
/*
* readParameters() is called in checkPhaseRestrictions
*/
final Game game = ai.getGame();
final Player opp = ai.getWeakestOpponent();

View File

@@ -31,8 +31,6 @@ public final class ImageKeys {
public static final String MONARCH_IMAGE = "monarch";
public static final String THE_RING_IMAGE = "the_ring";
public static final String RADIATION_IMAGE = "radiation";
public static final String SPEED_IMAGE = "speed";
public static final String MAX_SPEED_IMAGE = "max_speed";
public static final String BACKFACE_POSTFIX = "$alt";
public static final String SPECFACE_W = "$wspec";

View File

@@ -18,9 +18,9 @@ import java.util.*;
import java.util.function.Predicate;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* The class holding game invariants, such as cards, editions, game formats. All that data, which is not supposed to be changed by player
*
@@ -29,6 +29,8 @@ import java.util.stream.Collectors;
public class StaticData {
private final CardStorageReader cardReader;
private final CardStorageReader tokenReader;
private final CardStorageReader customCardReader;
private final String blockDataFolder;
private final CardDb commonCards;
private final CardDb variantCards;
@@ -77,6 +79,7 @@ public class StaticData {
this.tokenReader = tokenReader;
this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder)));
this.blockDataFolder = blockDataFolder;
this.customCardReader = customCardReader;
this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance;
this.enableSmartCardArtSelection = enableSmartCardArtSelection;
this.loadNonLegalCards = loadNonLegalCards;
@@ -781,7 +784,6 @@ public class StaticData {
Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>();
boolean nifHeader = false;
boolean cniHeader = false;
final Pattern funnyCardCollectorNumberPattern = Pattern.compile("^F\\d+");
for (CardEdition e : editions) {
if (CardEdition.Type.FUNNY.equals(e.getType()))
continue;
@@ -789,13 +791,11 @@ public class StaticData {
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
List<CompletableFuture<?>> futures = new ArrayList<>();
for (CardEdition.EditionEntry c : e.getObtainableCards()) {
int amount = 1;
if (cardCount.containsKey(c.name())) {
amount = cardCount.get(c.name()).getRight() + 1;
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), cardCount.get(c.name()).getRight() + 1));
} else {
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), 1));
}
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && funnyCardCollectorNumberPattern.matcher(c.collectorNumber()).matches(), amount));
}
// loop through the cards in this edition, considering art variations...

View File

@@ -21,7 +21,6 @@ import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.CardEdition.EditionEntry;
import forge.card.CardEdition.Type;
@@ -45,6 +44,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static char NameSetSeparator = '|';
public final static String FlagPrefix = "#";
public static final String FlagSeparator = "\t";
private final String exlcudedCardName = "Concentrate";
private final String exlcudedCardSet = "DS0";
// need this to obtain cardReference by name+set+artindex
private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
@@ -199,7 +200,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
private static boolean isArtIndex(String s) {
return StringUtils.isNumeric(s) && s.length() <= 2; // only artIndex between 1-99
return StringUtils.isNumeric(s) && s.length() <= 2 ; // only artIndex between 1-99
}
private static boolean isSetCode(String s) {
@@ -240,8 +241,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
setCode = info[index];
index++;
}
if(info.length > index && isArtIndex(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""))) {
artIndex = Integer.parseInt(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""));
if(info.length > index && isArtIndex(info[index])) {
artIndex = Integer.parseInt(info[index]);
index++;
}
if(info.length > index && isCollectorNumber(info[index])) {
@@ -301,7 +302,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// create faces list from rules
for (final CardRules rule : rules.values()) {
if (filteredCards.contains(rule.getName()))
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
continue;
for (ICardFace face : rule.getAllFaces()) {
addFaceToDbNames(face);
@@ -499,9 +500,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
public void addCard(PaperCard paperCard) {
if (filtered.contains(paperCard.getName())) {
if (excludeCard(paperCard.getName(), paperCard.getEdition()))
return;
}
allCardsByName.put(paperCard.getName(), paperCard);
@@ -522,6 +522,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
}
private boolean excludeCard(String cardName, String cardEdition) {
if (filtered.isEmpty())
return false;
if (filtered.contains(cardName)) {
if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName))
return true;
else return !exlcudedCardName.equalsIgnoreCase(cardName);
}
return false;
}
private void reIndex() {
uniqueCardsByName.clear();
for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) {

View File

@@ -52,14 +52,6 @@ import java.util.stream.Collectors;
*/
public final class CardEdition implements Comparable<CardEdition> {
public DraftOptions getDraftOptions() {
return draftOptions;
}
public void setDraftOptions(DraftOptions draftOptions) {
this.draftOptions = draftOptions;
}
// immutable
public enum Type {
UNKNOWN,
@@ -283,22 +275,18 @@ public final class CardEdition implements Comparable<CardEdition> {
// Booster/draft info
private List<BoosterSlot> boosterSlots = null;
private boolean smallSetOverride = false;
private String additionalUnlockSet = "";
private FoilType foilType = FoilType.NOT_SUPPORTED;
// Replace all of these things with booster slots
private boolean foilAlwaysInCommonSlot = false;
private FoilType foilType = FoilType.NOT_SUPPORTED;
private double foilChanceInBooster = 0;
private double chanceReplaceCommonWith = 0;
private String slotReplaceCommonWith = "Common";
private String additionalSheetForFoils = "";
private String additionalUnlockSet = "";
private String boosterMustContain = "";
private String boosterReplaceSlotFromPrintSheet = "";
private String sheetReplaceCardFromSheet = "";
private String sheetReplaceCardFromSheet2 = "";
// Draft options
private DraftOptions draftOptions = null;
private String doublePickDuringDraft = "";
private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, EditionEntry> cardMap;
@@ -385,6 +373,7 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
public String getAdditionalUnlockSet() { return additionalUnlockSet; }
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
public String getBoosterMustContain() { return boosterMustContain; }
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; }
@@ -563,16 +552,26 @@ public final class CardEdition implements Comparable<CardEdition> {
public List<PrintSheet> getPrintSheetsBySection() {
final CardDb cardDb = StaticData.instance().getCommonCards();
Map<String, Integer> cardToIndex = new HashMap<>();
List<PrintSheet> sheets = Lists.newArrayList();
for (Map.Entry<String, java.util.Collection<EditionEntry>> section : cardMap.asMap().entrySet()) {
if (section.getKey().equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
for (String sectionName : cardMap.keySet()) {
if (sectionName.equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
continue;
}
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), section.getKey()));
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
for (EditionEntry card : section.getValue()) {
sheet.add(cardDb.getCard(card.name, this.getCode(), card.collectorNumber));
List<EditionEntry> cards = cardMap.get(sectionName);
for (EditionEntry card : cards) {
int index = 1;
if (cardToIndex.containsKey(card.name)) {
index = cardToIndex.get(card.name) + 1;
}
cardToIndex.put(card.name, index);
PaperCard pCard = cardDb.getCard(card.name, this.getCode(), index);
sheet.add(pCard);
}
sheets.add(sheet);
@@ -630,7 +629,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* functional variant name - grouping #9
*/
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
"(^(.?[0-9A-Z-]+\\S*[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
);
final Pattern tokenPattern = Pattern.compile(
@@ -639,7 +638,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* name - grouping #3
* artist name - grouping #5
*/
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
);
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
@@ -660,37 +659,31 @@ public final class CardEdition implements Comparable<CardEdition> {
continue;
}
if (sectionName.endsWith("Types")) {
CardType.Helper.parseTypes(sectionName, contents.get(sectionName));
} else {
// Parse cards
// parse sections of the format "<collector number> <rarity> <name>"
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
for(String line : contents.get(sectionName)) {
Matcher matcher = pattern.matcher(line);
// parse sections of the format "<collector number> <rarity> <name>"
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
for(String line : contents.get(sectionName)) {
Matcher matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
String collectorNumber = matcher.group(2);
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
String cardName = matcher.group(5);
String artistName = matcher.group(7);
String functionalVariantName = matcher.group(9);
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
cardMap.put(sectionName, cis);
if (!matcher.matches()) {
continue;
}
} else if (boosterSlotsToParse.contains(sectionName)) {
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
} else {
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
String collectorNumber = matcher.group(2);
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
String cardName = matcher.group(5);
String artistName = matcher.group(7);
String functionalVariantName = matcher.group(9);
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
cardMap.put(sectionName, cis);
}
} else if (boosterSlotsToParse.contains(sectionName)) {
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
} else {
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
}
}
@@ -819,6 +812,7 @@ public final class CardEdition implements Comparable<CardEdition> {
res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral
res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon)
res.doublePickDuringDraft = metadata.get("DoublePick", ""); // "FirstPick" or "Always"
res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature
res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card
@@ -826,23 +820,6 @@ public final class CardEdition implements Comparable<CardEdition> {
res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", "");
res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names
// Draft options
String doublePick = metadata.get("DoublePick", "Never");
int maxPodSize = metadata.getInt("MaxPodSize", 8);
int recommendedPodSize = metadata.getInt("RecommendedPodSize", 8);
int maxMatchPlayers = metadata.getInt("MaxMatchPlayers", 2);
String deckType = metadata.get("DeckType", "Normal");
String freeCommander = metadata.get("FreeCommander", "");
res.draftOptions = new DraftOptions(
doublePick,
maxPodSize,
recommendedPodSize,
maxMatchPlayers,
deckType,
freeCommander
);
return res;
}
@@ -873,7 +850,7 @@ public final class CardEdition implements Comparable<CardEdition> {
@Override
public void add(CardEdition item) { //Even though we want it to be read only, make an exception for custom content.
if(lock) throw new UnsupportedOperationException("This is a read-only storage");
else map.put(item.getCode(), item);
else map.put(item.getName(), item);
}
public void append(CardEdition.Collection C){ //Append custom editions
if (lock) throw new UnsupportedOperationException("This is a read-only storage");

View File

@@ -53,7 +53,6 @@ public final class CardRules implements ICardCharacteristics {
private boolean addsWildCardColor;
private int setColorID;
private boolean custom;
private boolean unsupported;
private String path;
public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
@@ -167,24 +166,6 @@ public final class CardRules implements ICardCharacteristics {
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
}
public boolean isTransformable() {
if (CardSplitType.Transform == getSplitType()) {
return true;
}
if (CardSplitType.Modal != getSplitType()) {
return false;
}
for (ICardFace face : getAllFaces()) {
for (String spell : face.getAbilities()) {
if (spell.contains("AB$ SetState") && spell.contains("Mode$ Transform")) {
return true;
}
}
// TODO check keywords if needed
}
return false;
}
public ICardFace getWSpecialize() {
return specializedParts.get(CardStateName.SpecializeW);
}
@@ -221,9 +202,7 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean isCustom() { return custom; }
public void setCustom() { custom = true; }
public boolean isUnsupported() { return unsupported; }
public void setCustom() { custom = true; }
@Override
public CardType getType() {
@@ -364,21 +343,16 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean isDoctor() {
Set<String> subtypes = new HashSet<>();
for (String type : mainPart.getType().getSubtypes()) {
subtypes.add(type);
if (!type.equals("Time Lord") && !type.equals("Doctor")) {
return false;
}
}
return subtypes.size() == 2 &&
subtypes.contains("Time Lord") &&
subtypes.contains("Doctor");
return true;
}
public boolean canBeOathbreaker() {
CardType type = mainPart.getType();
if (mainPart.getOracleText().contains("can be your commander")) {
return true;
}
return type.isPlaneswalker();
}
@@ -831,8 +805,6 @@ public final class CardRules implements ICardCharacteristics {
faces[0].assignMissingFields();
final CardRules result = new CardRules(faces, CardSplitType.None, cah);
result.unsupported = true;
return result;
}

View File

@@ -189,38 +189,6 @@ public final class CardRulesPredicates {
return card -> card.getType().hasSupertype(type);
}
/**
* @return a Predicate that matches cards that are of the split type.
*/
public static Predicate<CardRules> isSplitType(final CardSplitType type) {
return card -> card.getSplitType().equals(type);
}
/**
* @return a Predicate that matches cards that are vanilla.
*/
public static Predicate<CardRules> isVanilla() {
return card -> {
if (!(card.getType().isCreature() || card.getType().isLand()) ||
card.getSplitType() != CardSplitType.None ||
card.hasFunctionalVariants()) {
return false;
}
ICardFace mainPart = card.getMainPart();
boolean hasAny =
mainPart.getKeywords().iterator().hasNext() ||
mainPart.getAbilities().iterator().hasNext() ||
mainPart.getStaticAbilities().iterator().hasNext() ||
mainPart.getTriggers().iterator().hasNext() ||
(mainPart.getDraftActions() != null && mainPart.getDraftActions().iterator().hasNext()) ||
mainPart.getReplacements().iterator().hasNext();
return !hasAny;
};
}
/**
* Checks for color.
*

View File

@@ -7,13 +7,13 @@ import java.util.EnumSet;
public enum CardSplitType
{
None(FaceSelectionMethod.USE_PRIMARY_FACE, null),
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Transformed),
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
public static final EnumSet<CardSplitType> DUAL_FACED_CARDS = EnumSet.of(

View File

@@ -5,11 +5,12 @@ public enum CardStateName {
Original,
FaceDown,
Flipped,
Backside,
Transformed,
Meld,
LeftSplit,
RightSplit,
Secondary,
Modal,
EmptyRoom,
SpecializeW,
SpecializeU,
@@ -41,7 +42,7 @@ public enum CardStateName {
return CardStateName.Flipped;
}
if ("DoubleFaced".equalsIgnoreCase(value)) {
return CardStateName.Backside;
return CardStateName.Transformed;
}
throw new IllegalArgumentException("No element named " + value + " in enum CardCharactersticName");

View File

@@ -1066,74 +1066,4 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return type;
}
public static class Helper {
public static final void parseTypes(String sectionName, List<String> content) {
Set<String> addToSection = null;
switch (sectionName) {
case "BasicTypes":
addToSection = CardType.Constant.BASIC_TYPES;
break;
case "LandTypes":
addToSection = CardType.Constant.LAND_TYPES;
break;
case "CreatureTypes":
addToSection = CardType.Constant.CREATURE_TYPES;
break;
case "SpellTypes":
addToSection = CardType.Constant.SPELL_TYPES;
break;
case "EnchantmentTypes":
addToSection = CardType.Constant.ENCHANTMENT_TYPES;
break;
case "ArtifactTypes":
addToSection = CardType.Constant.ARTIFACT_TYPES;
break;
case "WalkerTypes":
addToSection = CardType.Constant.WALKER_TYPES;
break;
case "DungeonTypes":
addToSection = CardType.Constant.DUNGEON_TYPES;
break;
case "BattleTypes":
addToSection = CardType.Constant.BATTLE_TYPES;
break;
case "PlanarTypes":
addToSection = CardType.Constant.PLANAR_TYPES;
break;
}
if (addToSection == null) {
return;
}
for(String line : content) {
if (line.length() == 0) continue;
if (line.contains(":")) {
String[] k = line.split(":");
if (addToSection.contains(k[0])) {
continue;
}
addToSection.add(k[0]);
CardType.Constant.pluralTypes.put(k[0], k[1]);
if (k[0].contains(" ")) {
CardType.Constant.MultiwordTypes.add(k[0]);
}
} else {
if (addToSection.contains(line)) {
continue;
}
addToSection.add(line);
if (line.contains(" ")) {
CardType.Constant.MultiwordTypes.add(line);
}
}
}
}
}
}

View File

@@ -1,75 +0,0 @@
package forge.card;
public class DraftOptions {
public enum DoublePick {
NEVER,
FIRST_PICK, // only first pick each pack
WHEN_POD_SIZE_IS_4, // only when pod size is 4, so you can pick two cards each time
ALWAYS // each time you receive a pack, you can pick two cards
};
public enum DeckType {
Normal, // Standard deck, usually 40 cards
Commander // Special deck type for Commander format. Important for selection/construction
}
private DoublePick doublePick = DoublePick.NEVER;
private final int maxPodSize; // Usually 8, but could be smaller for cubes. I guess it could be larger too
private final int recommendedPodSize; // Usually 8, but is 4 for new double pick
private final int maxMatchPlayers; // Usually 2, but 4 for things like Commander or Conspiracy
private final DeckType deckType; // Normal or Commander
private final String freeCommander;
public DraftOptions(String doublePickOption, int maxPodSize, int recommendedPodSize, int maxMatchPlayers, String deckType, String freeCommander) {
this.maxPodSize = maxPodSize;
this.recommendedPodSize = recommendedPodSize;
this.maxMatchPlayers = maxMatchPlayers;
this.deckType = DeckType.valueOf(deckType);
this.freeCommander = freeCommander;
if (doublePickOption != null) {
switch (doublePickOption.toLowerCase()) {
case "firstpick":
doublePick = DoublePick.FIRST_PICK;
break;
case "always":
doublePick = DoublePick.ALWAYS;
break;
case "whenpodsizeis4":
doublePick = DoublePick.WHEN_POD_SIZE_IS_4;
break;
}
}
}
public int getMaxPodSize() {
return maxPodSize;
}
public int getRecommendedPodSize() {
return recommendedPodSize;
}
public DoublePick getDoublePick() {
return doublePick;
}
public DoublePick isDoublePick(int podSize) {
if (doublePick == DoublePick.WHEN_POD_SIZE_IS_4) {
if (podSize != 4) {
return DoublePick.NEVER;
}
// only when pod size is 4, so you can pick two cards each time
return DoublePick.ALWAYS;
}
return doublePick;
}
public int getMaxMatchPlayers() {
return maxMatchPlayers;
}
public DeckType getDeckType() {
return deckType;
}
public String getFreeCommander() {
return freeCommander;
}
}

View File

@@ -13,7 +13,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Predicate;
/**
* TODO: Write javadoc for this type.
@@ -69,13 +68,6 @@ public class PrintSheet {
cardsWithWeights.remove(card);
}
public boolean contains(PaperCard pc) {
return cardsWithWeights.contains(pc);
}
public PaperCard find(Predicate<PaperCard> filter) {
return cardsWithWeights.find(filter);
}
private PaperCard fetchRoulette(int start, int roulette, Collection<PaperCard> toSkip) {
int sum = start;
boolean isSecondRun = start > 0;
@@ -93,6 +85,15 @@ public class PrintSheet {
return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip
}
public List<PaperCard> all() {
List<PaperCard> result = new ArrayList<>();
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
for (int i = 0; i < kv.getValue(); i++) {
result.add(kv.getKey());
}
}
return result;
}
public boolean containsCardNamed(String name,int atLeast) {
int count=0;
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
@@ -143,7 +144,7 @@ public class PrintSheet {
return cardsWithWeights.isEmpty();
}
public List<PaperCard> toFlatList() {
public Iterable<PaperCard> toFlatList() {
return cardsWithWeights.toFlatList();
}

View File

@@ -37,6 +37,7 @@ import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CardPool extends ItemPool<PaperCard> {
private static final long serialVersionUID = -5379091255613968393L;
@@ -77,20 +78,12 @@ public class CardPool extends ItemPool<PaperCard> {
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
CardDb db = entry.getValue();
PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags);
if (paperCard != null) {
this.add(paperCard, amount);
return;
}
}
// Try to get non-Alchemy version if it cannot find it.
if (cardName.startsWith("A-")) {
System.out.println("Alchemy card not found for '" + cardName + "'. Trying to get its non-Alchemy equivalent.");
cardName = cardName.replaceFirst("A-", "");
}
//Failed to find it. Fall back accordingly?
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
}
@@ -426,12 +419,6 @@ public class CardPool extends ItemPool<PaperCard> {
return pool;
}
public static CardPool fromSingleCardRequest(String cardRequest) {
if(StringUtils.isBlank(cardRequest))
return new CardPool();
return fromCardList(List.of(cardRequest));
}
public static List<Pair<String, Integer>> processCardList(final Iterable<String> lines) {
List<Pair<String, Integer>> cardRequests = new ArrayList<>();
if (lines == null)
@@ -481,7 +468,6 @@ public class CardPool extends ItemPool<PaperCard> {
* @param predicate the Predicate to apply to this CardPool
* @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate
*/
@Override
public CardPool getFilteredPool(Predicate<PaperCard> predicate) {
CardPool filteredPool = new CardPool();
for (PaperCard c : this.items.keySet()) {

View File

@@ -28,8 +28,6 @@ import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.util.*;
import java.util.Map.Entry;
@@ -115,20 +113,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return parts.get(DeckSection.Main);
}
public Pair<Deck, List<PaperCard>> getValid() {
List<PaperCard> unsupported = new ArrayList<>();
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool pool = kv.getValue();
for (Entry<PaperCard, Integer> pc : pool) {
if (pc.getKey().getRules() != null && pc.getKey().getRules().isUnsupported()) {
unsupported.add(pc.getKey());
pool.remove(pc.getKey());
}
}
}
return Pair.of(this, unsupported);
}
public List<PaperCard> getCommanders() {
List<PaperCard> result = Lists.newArrayList();
final CardPool cp = get(DeckSection.Commander);
@@ -224,19 +208,14 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
super.cloneFieldsTo(clone);
final Deck result = (Deck) clone;
loadDeferredSections();
// parts shouldn't be null
if (parts != null) {
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool cp = new CardPool();
result.parts.put(kv.getKey(), cp);
cp.addAll(kv.getValue());
}
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
CardPool cp = new CardPool();
result.parts.put(kv.getKey(), cp);
cp.addAll(kv.getValue());
}
result.setAiHints(StringUtils.join(aiHints, " | "));
result.setDraftNotes(draftNotes);
//noinspection ConstantValue
if(tags != null) //Can happen deserializing old Decks.
result.tags.addAll(this.tags);
tags.addAll(result.getTags());
}
/*
@@ -542,17 +521,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return sum;
}
/**
* Counts the number of copies of this exact card print across all deck sections.
*/
public int count(PaperCard card) {
int sum = 0;
for (Entry<DeckSection, CardPool> section : this) {
sum += section.getValue().count(card);
}
return sum;
}
public void setAiHints(String aiHintsInfo) {
if (aiHintsInfo == null || aiHintsInfo.trim().isEmpty()) {
return;
@@ -646,14 +614,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
return this;
}
@Serial
private Object readResolve() throws ObjectStreamException {
//If we deserialized an old deck that doesn't have tags, fix it here.
if(this.tags == null)
return new Deck(this, this.getName() == null ? "" : this.getName());
return this;
}
/** {@inheritDoc} */
@Override
public boolean equals(final Object o) {
@@ -703,4 +663,4 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
}
return totalCount == 0 ? 0 : Math.round(totalCMC / totalCount);
}
}
}

View File

@@ -32,8 +32,11 @@ import forge.util.TextUtil;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;
/**
@@ -57,13 +60,6 @@ public enum DeckFormat {
//Limited contraption decks have no restrictions.
return null;
}
@Override
public int getExtraSectionMaxCopies(DeckSection section) {
if(section == DeckSection.Attractions || section == DeckSection.Contraptions)
return Integer.MAX_VALUE;
return super.getExtraSectionMaxCopies(section);
}
},
Commander ( Range.is(99), Range.of(0, 10), 1, null,
card -> StaticData.instance().getCommanderPredicate().test(card)
@@ -112,13 +108,7 @@ public enum DeckFormat {
}
},
PlanarConquest ( Range.of(40, Integer.MAX_VALUE), Range.is(0), 1),
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, Integer.MAX_VALUE), 4) {
@Override
public boolean allowCustomCards() {
//If the player has them, may as well allow them.
return true;
}
},
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, 15), 4),
Vanguard ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
Planechase ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
Archenemy ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
@@ -201,57 +191,12 @@ public enum DeckFormat {
}
/**
* @return the default maximum copies of a card in this format.
* @return the maxCardCopies
*/
public int getMaxCardCopies() {
return maxCardCopies;
}
/**
* @return the maximum copies of the specified card allowed in this format. This does not include ban or restricted lists.
*/
public int getMaxCardCopies(PaperCard card) {
if(canHaveSpecificNumberInDeck(card) != null)
return canHaveSpecificNumberInDeck(card);
else if (canHaveAnyNumberOf(card))
return Integer.MAX_VALUE;
else if (card.getRules().isVariant()) {
DeckSection section = DeckSection.matchingSection(card);
if(section == DeckSection.Planes && card.getRules().getType().isPhenomenon())
return 2; //These are two-of.
return getExtraSectionMaxCopies(section);
}
else
return this.getMaxCardCopies();
}
public int getExtraSectionMaxCopies(DeckSection section) {
return switch (section) {
case Avatar, Commander, Planes, Dungeon, Attractions, Contraptions -> 1;
case Schemes -> 2;
case Conspiracy -> Integer.MAX_VALUE;
default -> maxCardCopies;
};
}
/**
* @return the deck sections used by most decks in this format.
*/
public EnumSet<DeckSection> getPrimaryDeckSections() {
if(this == Planechase)
return EnumSet.of(DeckSection.Planes);
if(this == Archenemy)
return EnumSet.of(DeckSection.Schemes);
if(this == Vanguard)
return EnumSet.of(DeckSection.Avatar);
EnumSet<DeckSection> out = EnumSet.of(DeckSection.Main);
if(sideRange == null || sideRange.getMaximum() > 0)
out.add(DeckSection.Sideboard);
if(hasCommander())
out.add(DeckSection.Commander);
return out;
}
public String getDeckConformanceProblem(Deck deck) {
if (deck == null) {
return "is not selected";
@@ -408,7 +353,7 @@ public enum DeckFormat {
// Should group all cards by name, so that different editions of same card are really counted as the same card
for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, pc -> StaticData.instance().getCommonCards().getName(pc.getName(), true))) {
IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey());
if (simpleCard != null && simpleCard.getRules().isCustom() && !allowCustomCards())
if (simpleCard != null && simpleCard.getRules().isCustom() && !StaticData.instance().allowCustomCardsInDecksConformance())
return TextUtil.concatWithSpace("contains a Custom Card:", cp.getKey(), "\nPlease Enable Custom Cards in Forge Preferences to use this deck.");
// Might cause issues since it ignores "Special" Cards
if (simpleCard == null) {
@@ -539,10 +484,6 @@ public enum DeckFormat {
// Not needed by default
}
public boolean allowCustomCards() {
return StaticData.instance().allowCustomCardsInDecksConformance();
}
public boolean isLegalCard(PaperCard pc) {
if (cardPoolFilter == null) {
if (paperCardPoolFilter == null) {
@@ -557,13 +498,13 @@ public enum DeckFormat {
if (cardPoolFilter != null && !cardPoolFilter.test(rules)) {
return false;
}
if (this == DeckFormat.Oathbreaker) {
if (this.equals(DeckFormat.Oathbreaker)) {
return rules.canBeOathbreaker();
}
if (this == DeckFormat.Brawl) {
if (this.equals(DeckFormat.Brawl)) {
return rules.canBeBrawlCommander();
}
if (this == DeckFormat.TinyLeaders) {
if (this.equals(DeckFormat.TinyLeaders)) {
return rules.canBeTinyLeadersCommander();
}
return rules.canBeCommander();
@@ -612,8 +553,6 @@ public enum DeckFormat {
for (final PaperCard p : commanders) {
cmdCI |= p.getRules().getColorIdentity().getColor();
}
if(cmdCI == MagicColor.ALL_COLORS)
return x -> true;
Predicate<CardRules> predicate = CardRulesPredicates.hasColorIdentity(cmdCI);
if (commanders.size() == 1 && commanders.get(0).getRules().canBePartnerCommander()) {
// Also show available partners a commander can have a partner.

View File

@@ -46,7 +46,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
// These fields are kinda PK for PrintedCard
private final String name;
private 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())
@@ -154,31 +154,6 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
return this.noSellVersion;
}
public PaperCard getMeldBaseCard() {
if (getRules().getSplitType() != CardSplitType.Meld) {
return null;
}
// This is the base part of the meld duo
if (getRules().getOtherPart() == null) {
return this;
}
String meldWith = getRules().getMeldWith();
if (meldWith == null) {
return null;
}
List<PrintSheet> sheets = StaticData.instance().getCardEdition(this.edition).getPrintSheetsBySection();
for (PrintSheet sheet : sheets) {
if (sheet.contains(this)) {
return sheet.find(PaperCardPredicates.name(meldWith));
}
}
return null;
}
public PaperCard copyWithoutFlags() {
if(this.flaglessVersion == null) {
if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
@@ -250,7 +225,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
this.foil = foil;
this.rarity = rarity;
this.artist = artist;
this.artist = TextUtil.normalizeText(artist);
this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER;
// If the user changes the language this will make cards sort by the old language until they restart the game.
// This is a good tradeoff
@@ -375,8 +350,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex);
pc = readObjectAlternate(name, edition);
if (pc == null) {
pc = StaticData.instance().getCommonCards().createUnsupportedCard(name);
//throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
}
System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex());
}

View File

@@ -50,13 +50,6 @@ public abstract class PaperCardPredicates {
return new PredicateNames(what);
}
/**
* Filters on a card foil status
*/
public static Predicate<PaperCard> isFoil(final boolean isFoil) {
return new PredicateFoil(isFoil);
}
private static final class PredicatePrintedWithRarity implements Predicate<PaperCard> {
private final CardRarity matchingRarity;
@@ -100,17 +93,6 @@ public abstract class PaperCardPredicates {
}
}
private static final class PredicateFoil implements Predicate<PaperCard> {
private final boolean operand;
@Override
public boolean test(final PaperCard card) { return card.isFoil() == operand; }
private PredicateFoil(final boolean isFoil) {
this.operand = isFoil;
}
}
private static final class PredicateRarity implements Predicate<PaperCard> {
private final CardRarity operand;

View File

@@ -19,11 +19,6 @@ public class SealedTemplate {
Pair.of(BoosterSlots.RARE_MYTHIC, 1), Pair.of(BoosterSlots.BASIC_LAND, 1)
));
// This is a generic cube booster. 15 cards, no rarity slots.
public final static SealedTemplate genericNoSlotBooster = new SealedTemplate(null, Lists.newArrayList(
Pair.of(BoosterSlots.ANY, 15)
));
protected final List<Pair<String, Integer>> slots;
protected final String name;

View File

@@ -254,7 +254,7 @@ public class BoosterGenerator {
if (sheetKey.startsWith("wholeSheet")) {
PrintSheet ps = getPrintSheet(sheetKey);
result.addAll(ps.toFlatList());
result.addAll(ps.all());
continue;
}
@@ -384,7 +384,7 @@ public class BoosterGenerator {
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
List<PaperCard> candidates = Lists.newArrayList();
for (PaperCard p : result) {
if (replaceThis.contains(p)) {
if (replaceThis.all().contains(p)) {
candidates.add(candidates.size(), p);
}
}
@@ -398,7 +398,7 @@ public class BoosterGenerator {
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
List<PaperCard> candidates = Lists.newArrayList();
for (PaperCard p : result) {
if (replaceThis.contains(p)) {
if (replaceThis.all().contains(p)) {
candidates.add(candidates.size(), p);
}
}
@@ -462,8 +462,9 @@ public class BoosterGenerator {
} else {
paperCards.addAll(ps.random(numCardsToGenerate, true));
}
result.addAll(paperCards);
}
result.addAll(paperCards);
}
return result;

View File

@@ -199,14 +199,19 @@ public class ImageUtil {
return getImageRelativePath(cp, face, true, true);
}
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false);
}
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
String editionCode;
if (setCode != null && !setCode.isEmpty())
editionCode = setCode;
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.replace("+", "");
// override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode
if (cardCollectorNumber.startsWith("OHOP")) {
editionCode = "ohop";
@@ -217,42 +222,29 @@ public class ImageUtil {
} else if (cardCollectorNumber.startsWith("OPC2")) {
editionCode = "opc2";
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
} else if (hyphenateAlchemy) {
if (!cardCollectorNumber.startsWith("A")) {
return null;
}
cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
}
String versionParam = useArtCrop ? "art_crop" : "normal";
String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
} else if (cp.getRules().getSplitType() == CardSplitType.Meld
&& !cardCollectorNumber.endsWith("a")
&& !cardCollectorNumber.endsWith("b")) {
if (cp.getRules().getSplitType() == CardSplitType.Meld) {
if (face.equals("back")) {
PaperCard meldBasePc = cp.getMeldBaseCard();
cardCollectorNumber = meldBasePc.getCollectorNumber();
String collectorNumberSuffix = "";
if (cardCollectorNumber.endsWith("a")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
} else if (cardCollectorNumber.endsWith("as")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
collectorNumberSuffix = "s";
} else if (cardCollectorNumber.endsWith("ap")) {
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
collectorNumberSuffix = "p";
} else if (cp.getCollectorNumber().endsWith("a")) {
// SIR
cardCollectorNumber = cp.getCollectorNumber().substring(0, cp.getCollectorNumber().length() - 1);
// Only the bottom half of a meld card shares a collector number.
// Hanweir Garrison EMN already has a appended.
// Exception: The front facing card doesn't use a in FIN
if (face.equals("back")) {
cardCollectorNumber += "b";
} else if (!editionCode.equals("fin")) {
cardCollectorNumber += "a";
}
cardCollectorNumber += "b" + collectorNumberSuffix;
}
faceParam = "&face=front";
} else if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") && cp.getRules().getSplitType() != CardSplitType.Flip
? "&face=back"
: "&face=front");
}
if (cardCollectorNumber.endsWith("")) {
faceParam = "&face=back";
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
}
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber),
@@ -264,10 +256,6 @@ public class ImageUtil {
if (!faceParam.isEmpty()) {
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
}
if (collectorNumber.endsWith("")) {
faceParam = "&face=back";
collectorNumber = collectorNumber.substring(0, collectorNumber.length() - 1);
}
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, encodeUtf8(collectorNumber),
langCode, versionParam, faceParam);
}
@@ -288,7 +276,8 @@ public class ImageUtil {
char c;
for (int i = 0; i < in.length(); i++) {
c = in.charAt(i);
if ((c != '"') && (c != '/') && (c != ':') && (c != '?')) {
if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) {
} else {
out.append(c);
}
}

View File

@@ -269,16 +269,11 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
// need not set out-of-sync: either remove did set, or nothing was removed
}
public void removeIf(Predicate<T> filter) {
items.keySet().removeIf(filter);
}
public void retainIf(Predicate<T> filter) {
items.keySet().removeIf(filter.negate());
}
public T find(Predicate<T> filter) {
return items.keySet().stream().filter(filter).findFirst().orElse(null);
public void removeIf(Predicate<T> test) {
for (final T item : items.keySet()) {
if (test.test(item))
remove(item);
}
}
public void clear() {
@@ -290,19 +285,4 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
return (obj instanceof ItemPool ip) &&
(this.items.equals(ip.items));
}
/**
* Applies a predicate to this ItemPool's entries.
*
* @param predicate the Predicate to apply to this ItemPool
* @return a new ItemPool made from this ItemPool with only the items that agree with the provided Predicate
*/
public ItemPool<T> getFilteredPool(Predicate<T> predicate) {
ItemPool<T> filteredPool = new ItemPool<>(myClass);
for (T c : this.items.keySet()) {
if (predicate.test(c))
filteredPool.add(c, this.items.get(c));
}
return filteredPool;
}
}

View File

@@ -32,7 +32,7 @@
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-logback</artifactId>
<version>8.19.1</version>
<version>7.15.0</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>

View File

@@ -195,7 +195,7 @@ public class ForgeScript {
return sa.isKeyword(Keyword.SADDLE);
} else if (property.equals("Station")) {
return sa.isKeyword(Keyword.STATION);
} else if (property.equals("Cycling")) {
}else if (property.equals("Cycling")) {
return sa.isCycling();
} else if (property.equals("Dash")) {
return sa.isDash();
@@ -237,8 +237,6 @@ public class ForgeScript {
return sa.isBoast();
} else if (property.equals("Exhaust")) {
return sa.isExhaust();
} else if (property.equals("Mayhem")) {
return sa.isMayhem();
} else if (property.equals("Mutate")) {
return sa.isMutate();
} else if (property.equals("Ninjutsu")) {

View File

@@ -414,6 +414,19 @@ public class Game {
return players;
}
/**
* Gets the nonactive players who are still fighting to win, in turn order.
*/
public final PlayerCollection getNonactivePlayers() {
// Don't use getPlayersInTurnOrder to prevent copying the player collection twice
final PlayerCollection players = new PlayerCollection(ingamePlayers);
players.remove(phaseHandler.getPlayerTurn());
if (!getTurnOrder().isDefaultDirection()) {
Collections.reverse(players);
}
return players;
}
/**
* Gets the players who participated in match (regardless of outcome).
* <i>Use this in UI and after match calculations</i>
@@ -731,7 +744,7 @@ public class Game {
if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) {
return;
}
if (!visitor.visitAll(player.getCardsIn(ZoneType.PART_OF_COMMAND_ZONE))) {
if (!visitor.visitAll(player.getZone(ZoneType.Command).getCards())) {
return;
}
if (withSideboard && !visitor.visitAll(player.getZone(ZoneType.Sideboard).getCards())) {

View File

@@ -1822,8 +1822,8 @@ public class GameAction {
private boolean stateBasedAction704_5q(Card c) {
boolean checkAgain = false;
final CounterType p1p1 = CounterEnumType.P1P1;
final CounterType m1m1 = CounterEnumType.M1M1;
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
int plusOneCounters = c.getCounters(p1p1);
int minusOneCounters = c.getCounters(m1m1);
if (plusOneCounters > 0 && minusOneCounters > 0) {
@@ -1843,7 +1843,7 @@ public class GameAction {
return checkAgain;
}
private boolean stateBasedAction704_5r(Card c) {
final CounterType dreamType = CounterEnumType.DREAM;
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
int old = c.getCounters(dreamType);
if (old <= 0) {
@@ -2222,13 +2222,6 @@ public class GameAction {
}
}
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Notify players
for (Player p : game.getPlayers()) {
p.getController().revealUnsupported(unsupported);
}
}
/** Delivers a message to all players. (use reveal to show Cards) */
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
if (saSource != null) {

View File

@@ -125,22 +125,10 @@ public final class GameActionUtil {
// need to be done there before static abilities does reset the card
// These Keywords depend on the Mana Cost of for Split Cards
if (sa.isBasicSpell()) {
if (sa.isBasicSpell() && !sa.isLandAbility()) {
for (final KeywordInterface inst : source.getKeywords()) {
final String keyword = inst.getOriginal();
if (keyword.startsWith("Mayhem")) {
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
}
if (sa.isLandAbility()) {
continue;
}
if (keyword.startsWith("Escape")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
@@ -177,7 +165,26 @@ public final class GameActionUtil {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback));
SpellAbility flashback = null;
// there is a flashback cost (and not the cards cost)
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
final String[] k = keyword.split(":");
flashback = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
String extraParams = k.length > 2 ? k[2] : "";
if (!extraParams.isEmpty()) {
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
flashback.putParam(param.getKey(), param.getValue());
}
}
} else { // same cost as original (e.g. Otaria plane)
flashback = sa.copy(activator);
}
flashback.setAlternativeCost(AlternativeCost.Flashback);
flashback.getRestrictions().setZone(ZoneType.Graveyard);
flashback.setKeyword(inst);
flashback.setIntrinsic(inst.isIntrinsic());
alternatives.add(flashback);
} else if (keyword.startsWith("Harmonize")) {
if (!source.isInZone(ZoneType.Graveyard)) {
continue;
@@ -187,7 +194,25 @@ public final class GameActionUtil {
continue;
}
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Harmonize));
SpellAbility harmonize = null;
if (keyword.contains(":")) {
final String[] k = keyword.split(":");
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
String extraParams = k.length > 2 ? k[2] : "";
if (!extraParams.isEmpty()) {
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
harmonize.putParam(param.getKey(), param.getValue());
}
}
} else {
harmonize = sa.copy(activator);
}
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
harmonize.setKeyword(inst);
harmonize.setIntrinsic(inst.isIntrinsic());
alternatives.add(harmonize);
} else if (keyword.startsWith("Foretell")) {
// Foretell cast only from Exile
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
@@ -242,7 +267,6 @@ public final class GameActionUtil {
}
stackCopy.setLastKnownZone(game.getStackZone());
stackCopy.setCastFrom(oldZone);
stackCopy.setCastSA(sa);
lkicheck = true;
stackCopy.clearStaticChangedCardKeywords(false);
@@ -293,30 +317,6 @@ public final class GameActionUtil {
return alternatives;
}
public static SpellAbility getGraveyardSpellByKeyword(KeywordInterface inst, SpellAbility sa, Player activator, AlternativeCost altCost) {
String keyword = inst.getOriginal();
SpellAbility newSA = null;
// there is a flashback cost (and not the cards cost)
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
final String[] k = keyword.split(":");
newSA = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
String extraParams = k.length > 2 ? k[2] : "";
if (!extraParams.isEmpty()) {
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
newSA.putParam(param.getKey(), param.getValue());
}
}
} else { // same cost as original (e.g. Otaria plane)
newSA = sa.copy(activator);
}
newSA.setAlternativeCost(altCost);
newSA.getRestrictions().setZone(ZoneType.Graveyard);
newSA.setKeyword(inst);
newSA.setIntrinsic(inst.isIntrinsic());
return newSA;
}
public static List<SpellAbility> getMayPlaySpellOptions(final SpellAbility sa, final Card source, final Player activator, boolean altCostOnly) {
final List<SpellAbility> alternatives = Lists.newArrayList();

View File

@@ -33,6 +33,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment;
import forge.game.keyword.Keyword;
@@ -304,6 +305,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
Integer value = counters.get(counterName);
return value == null ? 0 : value;
}
public final int getCounters(final CounterEnumType counterType) {
return getCounters(CounterType.get(counterType));
}
public void setCounters(final CounterType counterType, final Integer num) {
if (num <= 0) {
@@ -312,6 +316,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
counters.put(counterType, num);
}
}
public void setCounters(final CounterEnumType counterType, final Integer num) {
setCounters(CounterType.get(counterType), num);
}
abstract public void setCounters(final Map<CounterType, Integer> allCounters);
@@ -321,6 +328,10 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
abstract public int subtractCounter(final CounterType counterName, final int n, final Player remover);
abstract public void clearCounters();
public boolean canReceiveCounters(final CounterEnumType type) {
return canReceiveCounters(CounterType.get(type));
}
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
if (n <= 0 || !canReceiveCounters(counterType)) {
// As per rule 107.1b
@@ -340,7 +351,18 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
table.put(source, this, counterType, n);
}
public final void addCounter(final CounterEnumType counterType, final int n, final Player source, GameEntityCounterTable table) {
addCounter(CounterType.get(counterType), n, source, table);
}
public int subtractCounter(final CounterEnumType counterName, final int n, final Player remover) {
return subtractCounter(CounterType.get(counterName), n, remover);
}
abstract public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params);
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
}
public Integer getCounterMax(final CounterType counterType) {
return null;
}

View File

@@ -6,7 +6,6 @@ public enum GameLogEntryType {
TURN("Turn"),
MULLIGAN("Mulligan"),
ANTE("Ante"),
DRAFT("Draft"),
ZONE_CHANGE("Zone Change"),
PLAYER_CONTROL("Player control"),
COMBAT("Combat"),

View File

@@ -29,25 +29,25 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventGameOutcome ev) {
// Turn number counted from the starting player
int lastTurn = (int)Math.ceil((float)ev.result().getLastTurnNumber() / 2.0);
int lastTurn = (int)Math.ceil((float)ev.result.getLastTurnNumber() / 2.0);
log.add(GameLogEntryType.GAME_OUTCOME, localizer.getMessage("lblTurn") + " " + lastTurn);
for (String outcome : ev.result().getOutcomeStrings()) {
for (String outcome : ev.result.getOutcomeStrings()) {
log.add(GameLogEntryType.GAME_OUTCOME, outcome);
}
return generateSummary(ev.history());
return generateSummary(ev.history);
}
@Override
public GameLogEntry visit(GameEventScry ev) {
String scryOutcome = "";
if (ev.toTop() > 0 && ev.toBottom() > 0) {
scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop())).replace("%bottom", String.valueOf(ev.toBottom()));
} else if (ev.toBottom() == 0) {
scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player().toString()).replace("%top", String.valueOf(ev.toTop()));
if (ev.toTop > 0 && ev.toBottom > 0) {
scryOutcome = localizer.getMessage("lblLogScryTopBottomLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop)).replace("%bottom", String.valueOf(ev.toBottom));
} else if (ev.toBottom == 0) {
scryOutcome = localizer.getMessage("lblLogScryTopLibrary").replace("%s", ev.player.toString()).replace("%top", String.valueOf(ev.toTop));
} else {
scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player().toString()).replace("%bottom", String.valueOf(ev.toBottom()));
scryOutcome = localizer.getMessage("lblLogScryBottomLibrary").replace("%s", ev.player.toString()).replace("%bottom", String.valueOf(ev.toBottom));
}
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome);
@@ -57,12 +57,12 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
public GameLogEntry visit(GameEventSurveil ev) {
String surveilOutcome = "";
if (ev.toLibrary() > 0 && ev.toGraveyard() > 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player().toString(), String.valueOf(ev.toLibrary()), String.valueOf(ev.toGraveyard()));
} else if (ev.toGraveyard() == 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player().toString(), String.valueOf(ev.toLibrary()));
if (ev.toLibrary > 0 && ev.toGraveyard > 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibraryGraveyard", ev.player.toString(), String.valueOf(ev.toLibrary), String.valueOf(ev.toGraveyard));
} else if (ev.toGraveyard == 0) {
surveilOutcome = localizer.getMessage("lblLogSurveiledToLibrary", ev.player.toString(), String.valueOf(ev.toLibrary));
} else {
surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player().toString(), String.valueOf(ev.toGraveyard()));
surveilOutcome = localizer.getMessage("lblLogSurveiledToGraveyard", ev.player.toString(), String.valueOf(ev.toGraveyard));
}
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, surveilOutcome);
@@ -70,26 +70,26 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventSpellResolved ev) {
String messageForLog = ev.hasFizzled() ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell().getHostCard().toString()) : ev.spell().getStackDescription();
String messageForLog = ev.hasFizzled ? localizer.getMessage("lblLogCardAbilityFizzles", ev.spell.getHostCard().toString()) : ev.spell.getStackDescription();
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, messageForLog);
}
@Override
public GameLogEntry visit(GameEventSpellAbilityCast event) {
String player = event.sa().getActivatingPlayer().getName();
String action = event.sa().isSpell() ? localizer.getMessage("lblCast")
: event.sa().isTrigger() ? localizer.getMessage("lblTriggered")
String player = event.sa.getActivatingPlayer().getName();
String action = event.sa.isSpell() ? localizer.getMessage("lblCast")
: event.sa.isTrigger() ? localizer.getMessage("lblTriggered")
: localizer.getMessage("lblActivated");
String object = event.si().getStackDescription().startsWith("Morph ")
String object = event.si.getStackDescription().startsWith("Morph ")
? localizer.getMessage("lblMorph")
: event.sa().getHostCard().toString();
: event.sa.getHostCard().toString();
String messageForLog = "";
if (event.sa().getTargetRestrictions() != null) {
if (event.sa.getTargetRestrictions() != null) {
StringBuilder sb = new StringBuilder();
for (TargetChoices ch : event.sa().getAllTargetChoices()) {
for (TargetChoices ch : event.sa.getAllTargetChoices()) {
if (null != ch) {
sb.append(ch);
}
@@ -104,18 +104,18 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventCardModeChosen ev) {
if (!ev.log()) {
if (!ev.log) {
return null;
}
String modeChoiceOutcome;
if (ev.random()) {
modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName(), ev.mode());
if (ev.random) {
modeChoiceOutcome = localizer.getMessage("lblLogRandomMode", ev.cardName, ev.mode);
} else {
modeChoiceOutcome = localizer.getMessage("lblLogPlayerChosenModeForCard",
ev.player().toString(), ev.mode(), ev.cardName());
ev.player.toString(), ev.mode, ev.cardName);
}
String name = CardTranslation.getTranslatedName(ev.cardName());
String name = CardTranslation.getTranslatedName(ev.cardName);
modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "CARDNAME", name);
modeChoiceOutcome = TextUtil.fastReplace(modeChoiceOutcome, "NICKNAME",
Lang.getInstance().getNickName(name));
@@ -124,7 +124,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventRandomLog ev) {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message());
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.message);
}
private static GameLogEntry generateSummary(final Collection<GameOutcome> gamesPlayed) {
@@ -152,8 +152,8 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(final GameEventPlayerControl event) {
final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer();
final Player p = event.player();
final LobbyPlayer newLobbyPlayer = event.newLobbyPlayer;
final Player p = event.player;
final String message;
if (newLobbyPlayer == null) {
@@ -166,23 +166,23 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventTurnPhase ev) {
Player p = ev.playerTurn();
return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc() + Lang.getInstance().getPossessedObject(p.getName(), ev.phase().nameForUi));
Player p = ev.playerTurn;
return new GameLogEntry(GameLogEntryType.PHASE, ev.phaseDesc + Lang.getInstance().getPossessedObject(p.getName(), ev.phase.nameForUi));
}
@Override
public GameLogEntry visit(GameEventCardDamaged event) {
String additionalLog = "";
if (event.type() == DamageType.Deathtouch) {
if (event.type == DamageType.Deathtouch) {
additionalLog = localizer.getMessage("lblDeathtouch");
}
if (event.type() == DamageType.M1M1Counters) {
if (event.type == DamageType.M1M1Counters) {
additionalLog = localizer.getMessage("lblAsM1M1Counters");
}
if (event.type() == DamageType.LoyaltyLoss) {
additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount()));
if (event.type == DamageType.LoyaltyLoss) {
additionalLog = localizer.getMessage("lblRemovingNLoyaltyCounter", String.valueOf(event.amount));
}
String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source().toString(), String.valueOf(event.amount()), additionalLog, event.card().toString());
String message = localizer.getMessage("lblSourceDealsNDamageToDest", event.source.toString(), String.valueOf(event.amount), additionalLog, event.card.toString());
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@@ -191,43 +191,43 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
*/
@Override
public GameLogEntry visit(GameEventLandPlayed ev) {
String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player().toString(), ev.land().toString());
String message = localizer.getMessage("lblLogPlayerPlayedLand", ev.player.toString(), ev.land.toString());
return new GameLogEntry(GameLogEntryType.LAND, message);
}
@Override
public GameLogEntry visit(GameEventTurnBegan event) {
String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber()), event.turnOwner().toString());
String message = localizer.getMessage("lblLogTurnNOwnerByPlayer", String.valueOf(event.turnNumber), event.turnOwner.toString());
return new GameLogEntry(GameLogEntryType.TURN, message);
}
@Override
public GameLogEntry visit(GameEventPlayerDamaged ev) {
String extra = ev.infect() ? localizer.getMessage("lblLogAsPoisonCounters") : "";
String damageType = ev.combat() ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat");
String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source().toString(),
String.valueOf(ev.amount()), damageType, ev.target().toString(), extra);
String extra = ev.infect ? localizer.getMessage("lblLogAsPoisonCounters") : "";
String damageType = ev.combat ? localizer.getMessage("lblCombat") : localizer.getMessage("lblNonCombat");
String message = localizer.getMessage("lblLogSourceDealsNDamageOfTypeToDest", ev.source.toString(),
String.valueOf(ev.amount), damageType, ev.target.toString(), extra);
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@Override
public GameLogEntry visit(GameEventPlayerPoisoned ev) {
String message = localizer.getMessage("lblLogPlayerReceivesNPosionCounterFrom",
ev.receiver().toString(), String.valueOf(ev.amount()), ev.source().toString());
ev.receiver.toString(), String.valueOf(ev.amount), ev.source.toString());
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@Override
public GameLogEntry visit(GameEventPlayerRadiation ev) {
String message;
final int change = ev.change();
final int change = ev.change;
String radCtr = CounterEnumType.RAD.getName().toLowerCase() + " " +
Localizer.getInstance().getMessage("lblCounter").toLowerCase();
if (change >= 0) message = localizer.getMessage("lblLogPlayerRadiation",
ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr),
ev.source().toString());
ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(change), radCtr),
ev.source.toString());
else message = localizer.getMessage("lblLogPlayerRadRemove",
ev.receiver().toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr));
ev.receiver.toString(), Lang.nounWithNumeralExceptOne(String.valueOf(Math.abs(change)), radCtr));
return new GameLogEntry(GameLogEntryType.DAMAGE, message);
}
@@ -239,16 +239,16 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
// Append Defending Player/Planeswalker
// Not a big fan of the triple nested loop here
for (GameEntity k : ev.attackersMap().keySet()) {
Collection<Card> attackers = ev.attackersMap().get(k);
for (GameEntity k : ev.attackersMap.keySet()) {
Collection<Card> attackers = ev.attackersMap.get(k);
if (attackers == null || attackers.isEmpty()) {
continue;
}
if (sb.length() > 0) sb.append("\n");
sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player(), Lang.joinHomogenous(attackers), k));
sb.append(localizer.getMessage("lblLogPlayerAssignedAttackerToAttackTarget", ev.player, Lang.joinHomogenous(attackers), k));
}
if (sb.length() == 0) {
sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player().toString()));
sb.append(localizer.getMessage("lblPlayerDidntAttackThisTurn").replace("%s", ev.player.toString()));
}
return new GameLogEntry(GameLogEntryType.COMBAT, sb.toString());
}
@@ -262,7 +262,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
Collection<Card> blockers = null;
for (Entry<GameEntity, MapOfLists<Card, Card>> kv : ev.blockers().entrySet()) {
for (Entry<GameEntity, MapOfLists<Card, Card>> kv : ev.blockers.entrySet()) {
GameEntity defender = kv.getKey();
MapOfLists<Card, Card> attackers = kv.getValue();
if (attackers == null || attackers.isEmpty()) {
@@ -298,7 +298,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
@Override
public GameLogEntry visit(GameEventMulligan ev) {
String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player().getZone(ZoneType.Hand).size())).replace("%s", ev.player().toString());
String message = localizer.getMessage("lblPlayerHasMulliganedDownToNCards").replace("%d", String.valueOf(ev.player.getZone(ZoneType.Hand).size())).replace("%s", ev.player.toString());
return new GameLogEntry(GameLogEntryType.MULLIGAN, message);
}

View File

@@ -15,7 +15,6 @@ public class GameRules {
private boolean AISideboardingEnabled = false;
private boolean sideboardForAI = false;
private final Set<GameType> appliedVariants = EnumSet.noneOf(GameType.class);
private int simTimeout = 120;
// it's a preference, not rule... but I could hardly find a better place for it
private boolean useGrayText;
@@ -125,12 +124,4 @@ public class GameRules {
public void setWarnAboutAICards(final boolean warnAboutAICards) {
this.warnAboutAICards = warnAboutAICards;
}
public int getSimTimeout() {
return this.simTimeout;
}
public void setSimTimeout(final int duration) {
this.simTimeout = duration;
}
}

View File

@@ -293,12 +293,6 @@ public class GameSnapshot {
Card newCard = toGame.findById(fromCard.getId());
Player toPlayer = findBy(toGame, fromCard.getController());
ZoneType fromType = fromCard.getZone().getZoneType();
int zonePosition = 0;
if (ZoneType.ORDERED_ZONES.contains(fromType)) {
// If the card is in an ordered zone, we need to find its position in the zone
// and set it in the new game.
zonePosition = fromCard.getZone().getCards().indexOf(fromCard);
}
if (newCard == null) {
// Storing a game uses this path...
@@ -315,10 +309,10 @@ public class GameSnapshot {
}
if (fromType.equals(ZoneType.Stack)) {
toGame.getStackZone().add(newCard, zonePosition);
toGame.getStackZone().add(newCard);
newCard.setZone(toGame.getStackZone());
} else {
toPlayer.getZone(fromType).add(newCard, zonePosition);
toPlayer.getZone(fromType).add(newCard);
newCard.setZone(toPlayer.getZone(fromType));
}

View File

@@ -24,7 +24,6 @@ public enum GameType {
Tournament (DeckFormat.Constructed, false, true, true, "lblTournament", ""),
CommanderGauntlet (DeckFormat.Commander, false, false, false, "lblCommanderGauntlet", "lblCommanderDesc"),
Quest (DeckFormat.QuestDeck, true, true, false, "lblQuest", ""),
QuestCommander (DeckFormat.Commander, true, true, false, "lblQuestCommander", ""),
QuestDraft (DeckFormat.Limited, true, true, true, "lblQuestDraft", ""),
PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "lblPlanarConquest", ""),
Adventure (DeckFormat.Adventure, true, false, false, "lblAdventure", ""),
@@ -72,8 +71,6 @@ public enum GameType {
return deck;
});
private static final EnumSet<GameType> DRAFT_FORMATS = EnumSet.of(Draft, QuestDraft, AdventureEvent);
private final DeckFormat deckFormat;
private final boolean isCardPoolLimited, canSideboard, addWonCardsMidGame;
private final String name, englishName, description;
@@ -90,7 +87,7 @@ public enum GameType {
addWonCardsMidGame = addWonCardsMidgame0;
name = localizer.getMessage(name0);
englishName = localizer.getEnglishMessage(name0);
if (!description0.isEmpty()) {
if (description0.length()>0) {
description0 = localizer.getMessage(description0);
}
description = description0;
@@ -130,8 +127,19 @@ public enum GameType {
return addWonCardsMidGame;
}
public boolean isDraft() {
return DRAFT_FORMATS.contains(this);
public boolean isCommandZoneNeeded() {
return true; //TODO: Figure out way to move command zone into field so it can be hidden when empty
/*switch (this) {
case Archenemy:
case Commander:
case Oathbreaker:
case TinyLeaders:
case Planechase:
case Vanguard:
return true;
default:
return false;
}*/
}
public String toString() {
@@ -145,27 +153,6 @@ public enum GameType {
return description;
}
/**
* @return the deck sections used by most decks in this game type.
*/
public EnumSet<DeckSection> getPrimaryDeckSections() {
return deckFormat.getPrimaryDeckSections();
}
/**
* @return the set of variant card sections that decks for this game type can include.
*/
public EnumSet<DeckSection> getSupplimentalDeckSections() {
if(!deckFormat.getPrimaryDeckSections().contains(DeckSection.Main))
return EnumSet.noneOf(DeckSection.class); //Already an extra deck, like a dedicated Scheme or Planar deck.
if(deckFormat == DeckFormat.Limited)
return EnumSet.of(DeckSection.Conspiracy, DeckSection.Contraptions, DeckSection.Attractions);
if(this == Constructed || this == Commander)
return EnumSet.of(DeckSection.Avatar, DeckSection.Schemes, DeckSection.Planes, DeckSection.Conspiracy,
DeckSection.Attractions, DeckSection.Contraptions);
return EnumSet.of(DeckSection.Attractions, DeckSection.Contraptions);
}
public static GameType smartValueOf(String name) {
return Enums.getIfPresent(GameType.class, name).orNull();
}

View File

@@ -215,7 +215,6 @@ public class GameView extends TrackableObject {
}
public void setDependencies(Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
if (dependencies.isEmpty()) {
set(TrackableProperty.Dependencies, "");
return;
}
StringBuilder sb = new StringBuilder();

View File

@@ -23,7 +23,6 @@ import forge.item.PaperCard;
import forge.util.Localizer;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
@@ -225,7 +224,6 @@ public class Match {
// friendliness
Map<Player, Map<DeckSection, List<? extends PaperCard>>> rAICards = new HashMap<>();
Multimap<Player, PaperCard> removedAnteCards = ArrayListMultimap.create();
Map<Player, List<PaperCard>> unsupported = new HashMap<>();
final FCollectionView<Player> players = game.getPlayers();
final List<RegisteredPlayer> playersConditions = game.getMatch().getPlayers();
@@ -290,32 +288,22 @@ public class Match {
}
}
Deck toCheck = psc.getDeck();
if (toCheck == null) {
try {
System.err.println(psc.getPlayer().getName() + " Deck is NULL...");
int val = rules.getGameType().getDeckFormat().getMainRange().getMinimum();
toCheck = new Deck("NULL");
if (val > 0)
toCheck.getMain().add("Wastes", val);
} catch (Exception ignored) {}
}
Pair<Deck, List<PaperCard>> myDeck = toCheck.getValid();
player.setDraftNotes(myDeck.getLeft().getDraftNotes());
Deck myDeck = psc.getDeck();
player.setDraftNotes(myDeck.getDraftNotes());
Set<PaperCard> myRemovedAnteCards = null;
if (!rules.useAnte()) {
myRemovedAnteCards = getRemovedAnteCards(myDeck.getLeft());
myRemovedAnteCards = getRemovedAnteCards(myDeck);
for (PaperCard cp: myRemovedAnteCards) {
for (Entry<DeckSection, CardPool> ds : myDeck.getLeft()) {
for (Entry<DeckSection, CardPool> ds : myDeck) {
ds.getValue().removeAll(cp);
}
}
}
preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil());
if (myDeck.getLeft().has(DeckSection.Sideboard)) {
preparePlayerZone(player, ZoneType.Sideboard, myDeck.getLeft().get(DeckSection.Sideboard), psc.useRandomFoil());
preparePlayerZone(player, ZoneType.Library, myDeck.getMain(), psc.useRandomFoil());
if (myDeck.has(DeckSection.Sideboard)) {
preparePlayerZone(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil());
// Assign Companion
Card companion = player.assignCompanion(game, person);
@@ -334,7 +322,7 @@ public class Match {
player.shuffle(null);
if (isFirstGame) {
Map<DeckSection, List<? extends PaperCard>> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck.getLeft());
Map<DeckSection, List<? extends PaperCard>> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck);
if (cardsComplained != null && !cardsComplained.isEmpty()) {
rAICards.put(player, cardsComplained);
}
@@ -349,7 +337,6 @@ public class Match {
if (myRemovedAnteCards != null && !myRemovedAnteCards.isEmpty()) {
removedAnteCards.putAll(player, myRemovedAnteCards);
}
unsupported.put(player, myDeck.getRight());
}
final Localizer localizer = Localizer.getInstance();
@@ -360,10 +347,6 @@ public class Match {
if (!removedAnteCards.isEmpty()) {
game.getAction().revealAnte(localizer.getMessage("lblAnteCardsRemoved"), removedAnteCards);
}
if (!unsupported.isEmpty()) {
game.getAction().revealUnsupported(unsupported);
}
}
private void executeAnte(Game lastGame) {

View File

@@ -3,6 +3,7 @@ package forge.game.ability;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityActivated;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.TargetRestrictions;
import java.util.Map;
@@ -17,7 +18,14 @@ public class AbilityApiBased extends AbilityActivated {
api = api0;
effect = api.getSpellEffect();
effect.buildSpellAbility(this);
if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
this.setUndoable(true); // will try at least
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
}
@Override

View File

@@ -63,9 +63,7 @@ public final class AbilityFactory {
"CantChooseSubAbility", // Can't choose a player via ChoosePlayer
"RegenerationAbility", // for Regeneration Effect
"ReturnAbility", // for Delayed Trigger on Magpie
"GiftAbility", // for Promise Gift
"VoteSubAbility", // for Vote with VoteCard
"VoteTiedAbility" // for fallback to Choices
"GiftAbility" // for Promise Gift
);
public enum AbilityRecordType {
@@ -202,6 +200,15 @@ public final class AbilityFactory {
final Card hostCard = state.getCard();
TargetRestrictions abTgt = mapParams.containsKey("ValidTgts") ? readTarget(mapParams) : null;
if (api == ApiType.CopySpellAbility || api == ApiType.Counter || api == ApiType.ChangeTargets || api == ApiType.ControlSpell) {
// Since all "CopySpell" ABs copy things on the Stack no need for it to be everywhere
// Since all "Counter" or "ChangeTargets" abilities only target the Stack Zone
// No need to have each of those scripts have that info
if (abTgt != null) {
abTgt.setZone(ZoneType.Stack);
}
}
if (abCost == null) {
abCost = parseAbilityCost(state, mapParams, type);
}
@@ -261,7 +268,7 @@ public final class AbilityFactory {
}
}
if (api == ApiType.Charm || api == ApiType.GenericChoice || api == ApiType.AssignGroup || api == ApiType.VillainousChoice || api == ApiType.Vote) {
if (api == ApiType.Charm || api == ApiType.GenericChoice || api == ApiType.AssignGroup || api == ApiType.VillainousChoice) {
final String key = "Choices";
if (mapParams.containsKey(key)) {
List<String> names = Lists.newArrayList(mapParams.get(key).split(","));
@@ -383,9 +390,6 @@ public final class AbilityFactory {
if (mapParams.containsKey("TargetsWithDifferentCMC")) {
abTgt.setDifferentCMC(true);
}
if (mapParams.containsKey("TargetsWithDifferentNames")) {
abTgt.setDifferentNames(true);
}
if (mapParams.containsKey("TargetsWithEqualToughness")) {
abTgt.setEqualToughness(true);
}

View File

@@ -113,6 +113,13 @@ public class AbilityUtils {
}
} else if (defined.equals("Enchanted")) {
c = hostCard.getEnchantingCard();
if (c == null && sa instanceof SpellAbility) {
SpellAbility root = ((SpellAbility)sa).getRootAbility();
CardCollection sacrificed = root.getPaidList("Sacrificed", true);
if (sacrificed != null && !sacrificed.isEmpty()) {
c = sacrificed.getFirst().getEnchantingCard();
}
}
} else if (defined.equals("TopOfGraveyard")) {
final CardCollectionView grave = player.getCardsIn(ZoneType.Graveyard);
@@ -2338,9 +2345,6 @@ public class AbilityUtils {
if (sq[0].equals("YourSpeed")) {
return doXMath(player.getSpeed(), expr, c, ctb);
}
if (sq[0].equals("AllFourBend")) {
return doXMath(calculateAmount(c, sq[player.hasAllElementBend() ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("Night")) {
return doXMath(calculateAmount(c, sq[game.isNight() ? 1 : 2], ctb), expr, c, ctb);
@@ -2985,14 +2989,14 @@ public class AbilityUtils {
if (tgtCard.isFaceDown()) {
collectSpellsForPlayEffect(list, original, controller, withAltCost);
} else {
if (state == CardStateName.Backside && !tgtCard.isModal() && tgtCard.isPermanent() && !tgtCard.isAura()) {
if (state == CardStateName.Transformed && tgtCard.isPermanent() && !tgtCard.isAura()) {
// casting defeated battle
Spell sp = new SpellPermanent(tgtCard, original);
sp.setCardState(original);
list.add(sp);
}
if (tgtCard.isModal() && tgtCard.hasState(CardStateName.Backside)) {
collectSpellsForPlayEffect(list, tgtCard.getState(CardStateName.Backside), controller, withAltCost);
if (tgtCard.isModal() && tgtCard.hasState(CardStateName.Modal)) {
collectSpellsForPlayEffect(list, tgtCard.getState(CardStateName.Modal), controller, withAltCost);
}
}

View File

@@ -19,7 +19,6 @@ public enum ApiType {
AddPhase (AddPhaseEffect.class),
AddTurn (AddTurnEffect.class),
AdvanceCrank (AdvanceCrankEffect.class),
Airbend (AirbendEffect.class),
AlterAttribute (AlterAttributeEffect.class),
Amass (AmassEffect.class),
Animate (AnimateEffect.class),
@@ -68,6 +67,7 @@ public enum ApiType {
Counter (CounterEffect.class),
DamageAll (DamageAllEffect.class),
DealDamage (DamageDealEffect.class),
Detain (DetainEffect.class),
DayTime (DayTimeEffect.class),
Debuff (DebuffEffect.class),
DelayedTrigger (DelayedTriggerEffect.class),
@@ -82,7 +82,6 @@ public enum ApiType {
Draft (DraftEffect.class),
Draw (DrawEffect.class),
EachDamage (DamageEachEffect.class),
Earthbend (EarthbendEffect.class),
Effect (EffectEffect.class),
Encode (EncodeEffect.class),
EndCombatPhase (EndCombatPhaseEffect.class),

View File

@@ -49,8 +49,6 @@ public abstract class SpellAbilityEffect {
return sa.getDescription();
}
public void buildSpellAbility(final SpellAbility sa) {}
/**
* Returns this effect description with needed prelude and epilogue.
* @param params
@@ -1069,8 +1067,6 @@ public abstract class SpellAbilityEffect {
// if ability was granted use that source so they can be kept apart later
if (cause.isCopiedTrait()) {
exilingSource = cause.getOriginalHost();
} else if (cause.getKeyword() != null && cause.getKeyword().getStatic() != null) {
exilingSource = cause.getKeyword().getStatic().getOriginalHost();
}
movedCard.setExiledWith(exilingSource);
Player exiler = cause.hasParam("DefinedExiler") ?

View File

@@ -4,6 +4,7 @@ import java.util.Map;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.Spell;
import forge.game.spellability.TargetRestrictions;
@@ -23,7 +24,13 @@ public class SpellApiBased extends Spell {
// A spell is always intrinsic
this.setIntrinsic(true);
effect.buildSpellAbility(this);
if (api.equals(ApiType.Mana) || api.equals(ApiType.ManaReflected)) {
this.setManaPart(new AbilityManaPart(this, mapParams));
}
if (api.equals(ApiType.ChangeZone) || api.equals(ApiType.ChangeZoneAll)) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
}
@Override

View File

@@ -2,6 +2,8 @@ package forge.game.ability;
import java.util.Map;
import forge.game.ability.effects.ChangeZoneAllEffect;
import forge.game.ability.effects.ChangeZoneEffect;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.spellability.AbilityStatic;
@@ -18,7 +20,9 @@ public class StaticAbilityApiBased extends AbilityStatic {
api = api0;
effect = api.getSpellEffect();
effect.buildSpellAbility(this);
if (effect instanceof ChangeZoneEffect || effect instanceof ChangeZoneAllEffect) {
AbilityFactory.adjustChangeZoneTarget(mapParams, this);
}
}
@Override

View File

@@ -1,93 +0,0 @@
package forge.game.ability.effects;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardZoneTable;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Lang;
public class AirbendEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Airbend ");
Iterable<Card> tgts;
if (sa.usesTargeting()) {
tgts = getCardsfromTargets(sa);
} else { // otherwise add self to list and go from there
tgts = sa.knownDetermineDefined(sa.getParam("Defined"));
}
sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts)));
sb.append(".");
if (Iterables.size(tgts) > 1) {
sb.append(" (Exile them. While each one is exiled, its owner may cast it for {2} rather than its mana cost.)");
} else {
sb.append(" (Exile it. While its exiled, its owner may cast it for {2} rather than its mana cost.)");
}
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final Player pl = sa.getActivatingPlayer();
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
for (Card c : getTargetCards(sa)) {
final Card gameCard = game.getCardState(c, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
// this should check Self too
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue;
}
if (!gameCard.canExiledBy(sa, true)) {
continue;
}
handleExiledWith(gameCard, sa);
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, triggerList);
Card movedCard = game.getAction().exile(gameCard, sa, moveParams);
if (movedCard == null || !movedCard.isInZone(ZoneType.Exile)) {
continue;
}
// Effect to cast for 2 from exile
Card eff = createEffect(sa, movedCard.getOwner(), "Airbend" + movedCard, hostCard.getImageKey());
eff.addRemembered(movedCard);
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | MayPlayAltManaCost$ 2 | EffectZone$ Command | Affected$ Card.IsRemembered+nonLand");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
eff.addStaticAbility(sbPlay.toString());
addForgetOnMovedTrigger(eff, "Exile");
addForgetOnCastTrigger(eff, "Card.IsRemembered");
game.getAction().moveToCommand(eff, sa);
}
triggerList.triggerChangesZoneAll(game, sa);
handleExiledWith(triggerList.allCards(), sa);
pl.triggerElementalBend(TriggerType.Airbend);
}
}

View File

@@ -46,9 +46,6 @@ public class AlterAttributeEffect extends SpellAbilityEffect {
boolean altered = false;
switch (attr.trim()) {
case "Harnessed":
altered = gameCard.setHarnessed(activate);
break;
case "Plotted":
altered = gameCard.setPlotted(activate);

View File

@@ -17,6 +17,7 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardZoneTable;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo;
import forge.game.event.GameEventCombatChanged;
import forge.game.event.GameEventTokenCreated;
@@ -85,7 +86,7 @@ public class AmassEffect extends TokenEffectBase {
}
Map<String, Object> params = Maps.newHashMap();
params.put("CounterType", CounterEnumType.P1P1);
params.put("CounterType", CounterType.get(CounterEnumType.P1P1));
params.put("Amount", amount);
Card tgt = activator.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblChooseAnArmy"), false, params);

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