Resolve "ZNR: Charm Effect tweaks"

This commit is contained in:
Hans Mackowiak
2020-10-01 05:02:41 +00:00
committed by Michael Kamensky
parent 2ce9f9a5b2
commit daf70f7e60
22 changed files with 237 additions and 143 deletions

View File

@@ -2103,9 +2103,9 @@ public class AiController {
return filterList(list, CardTraitPredicates.hasParam("AiLogic", logic)); return filterList(list, CardTraitPredicates.hasParam("AiLogic", logic));
} }
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {
if (simPicker != null) { if (simPicker != null) {
return simPicker.chooseModeForAbility(sa, min, num, allowRepeat); return simPicker.chooseModeForAbility(sa, possible, min, num, allowRepeat);
} }
return null; return null;
} }

View File

@@ -101,7 +101,9 @@ public class ComputerUtil {
sa = GameActionUtil.addExtraKeywordCost(sa); sa = GameActionUtil.addExtraKeywordCost(sa);
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa); if (!CharmEffect.makeChoices(sa)) {
return false;
}
} }
if (chooseTargets != null) { if (chooseTargets != null) {
chooseTargets.run(); chooseTargets.run();
@@ -261,7 +263,9 @@ public class ComputerUtil {
newSA.setHostCard(game.getAction().moveToStack(source, sa)); newSA.setHostCard(game.getAction().moveToStack(source, sa));
if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) { if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) {
CharmEffect.makeChoices(newSA); if (!CharmEffect.makeChoices(sa)) {
return false;
}
} }
} }

View File

@@ -800,8 +800,8 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {
List<AbilitySub> result = brains.chooseModeForAbility(sa, min, num, allowRepeat); List<AbilitySub> result = brains.chooseModeForAbility(sa, possible, min, num, allowRepeat);
if (result != null) { if (result != null) {
return result; return result;
} }

View File

@@ -2,7 +2,9 @@ package forge.ai.ability;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.*; import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -19,10 +21,13 @@ public class CharmAi extends SpellAbilityAi {
// sa is Entwined, no need for extra logic // sa is Entwined, no need for extra logic
if (sa.isEntwine()) { if (sa.isEntwine()) {
return true; return true;
} }
final Card source = sa.getHostCard();
final int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now? boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
// Reset the chosen list otherwise it will be locked in forever by earlier calls // Reset the chosen list otherwise it will be locked in forever by earlier calls

View File

@@ -9,7 +9,6 @@ import forge.ai.ability.ExploreAi;
import forge.ai.simulation.GameStateEvaluator.Score; import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -371,14 +370,12 @@ public class SpellAbilityPicker {
return bestScore; return bestScore;
} }
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> choices, int min, int num, boolean allowRepeat) {
if (interceptor != null) { if (interceptor != null) {
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
return interceptor.chooseModesForAbility(choices, min, num, allowRepeat); return interceptor.chooseModesForAbility(choices, min, num, allowRepeat);
} }
if (plan != null && plan.getSelectedDecision() != null && plan.getSelectedDecision().modes != null) { if (plan != null && plan.getSelectedDecision() != null && plan.getSelectedDecision().modes != null) {
Plan.Decision decision = plan.getSelectedDecision(); Plan.Decision decision = plan.getSelectedDecision();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
// TODO: Validate that there's no discrepancies between choices and modes? // TODO: Validate that there's no discrepancies between choices and modes?
List<AbilitySub> plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes); List<AbilitySub> plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes);
if (plan.getSelectedDecision().targets != null) { if (plan.getSelectedDecision().targets != null) {

View File

@@ -286,7 +286,7 @@ public final class AbilityFactory {
spellAbility.setDescription(sb.toString()); spellAbility.setDescription(sb.toString());
} else if (api == ApiType.Charm) { } else if (api == ApiType.Charm) {
spellAbility.setDescription(CharmEffect.makeSpellDescription(spellAbility)); spellAbility.setDescription(CharmEffect.makeFormatedDescription(spellAbility));
} else { } else {
spellAbility.setDescription(""); spellAbility.setDescription("");
} }

View File

@@ -374,7 +374,7 @@ public class AbilityUtils {
if (StringUtils.isBlank(amount)) { return 0; } if (StringUtils.isBlank(amount)) { return 0; }
if (card == null) { return 0; } if (card == null) { return 0; }
final Player player = card.getController(); final Player player = card.getController();
final Game game = player.getGame(); final Game game = player == null ? card.getGame() : player.getGame();
// Strip and save sign for calculations // Strip and save sign for calculations
final boolean startsWithPlus = amount.charAt(0) == '+'; final boolean startsWithPlus = amount.charAt(0) == '+';

View File

@@ -20,27 +20,24 @@ public class CharmEffect extends SpellAbilityEffect {
public static List<AbilitySub> makePossibleOptions(final SpellAbility sa) { public static List<AbilitySub> makePossibleOptions(final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
Iterable<Object> restriction = null; List<String> restriction = null;
if (sa.hasParam("ChoiceRestriction")) { if (sa.hasParam("ChoiceRestriction")) {
String rest = sa.getParam("ChoiceRestriction"); String rest = sa.getParam("ChoiceRestriction");
if (rest.equals("NotRemembered")) { if (rest.equals("ThisGame")) {
restriction = source.getRemembered(); restriction = source.getChosenModesGame(sa);
} else if (rest.equals("ThisTurn")) {
restriction = source.getChosenModesTurn(sa);
} }
} }
int indx = 0; int indx = 0;
List<AbilitySub> choices = Lists.newArrayList(sa.getAdditionalAbilityList("Choices")); List<AbilitySub> choices = Lists.newArrayList(sa.getAdditionalAbilityList("Choices"));
if (restriction != null) { if (restriction != null) {
List<AbilitySub> toRemove = Lists.newArrayList(); List<AbilitySub> toRemove = Lists.newArrayList();
for (Object o : restriction) { for (AbilitySub ch : choices) {
if (o instanceof AbilitySub) { if (restriction.contains(ch.getDescription())) {
String abText = ((AbilitySub)o).getDescription(); toRemove.add(ch);
for (AbilitySub ch : choices) {
if (ch.getDescription().equals(abText)) {
toRemove.add(ch);
}
}
} }
} }
choices.removeAll(toRemove); choices.removeAll(toRemove);
@@ -54,55 +51,24 @@ public class CharmEffect extends SpellAbilityEffect {
return choices; return choices;
} }
public static String makeSpellDescription(SpellAbility sa) {
int num = Integer.parseInt(sa.getParamOrDefault("CharmNum", "1"));
int min = Integer.parseInt(sa.getParamOrDefault("MinCharmNum", String.valueOf(num)));
boolean repeat = sa.hasParam("CanRepeatModes");
boolean random = sa.hasParam("Random");
boolean oppChooses = "Opponent".equals(sa.getParam("Chooser"));
List<AbilitySub> list = CharmEffect.makePossibleOptions(sa);
StringBuilder sb = new StringBuilder();
sb.append(sa.getCostDescription());
sb.append(oppChooses ? "An opponent chooses " : "Choose ");
if (num == min) {
sb.append(Lang.getNumeral(num));
} else if (min == 0) {
sb.append("up to ").append(Lang.getNumeral(num));
} else {
sb.append(Lang.getNumeral(min)).append(" or ").append(list.size() == 2 ? "both" : "more");
}
if (random) {
sb.append("at random.");
}
if (repeat) {
sb.append(". You may choose the same mode more than once.");
}
sb.append(" - ");
int i = 0;
for (AbilitySub sub : list) {
if (i > 0) {
sb.append("; ");
}
sb.append(sub.getParam("SpellDescription"));
++i;
}
return sb.toString();
}
public static String makeFormatedDescription(SpellAbility sa) { public static String makeFormatedDescription(SpellAbility sa) {
int num = Integer.parseInt(sa.getParamOrDefault("CharmNum", "1")); Card source = sa.getHostCard();
int min = Integer.parseInt(sa.getParamOrDefault("MinCharmNum", String.valueOf(num)));
List<AbilitySub> list = CharmEffect.makePossibleOptions(sa);
final int num;
// hotfix for Vindictive Lich when using getCardForUi
if (source.getController() == null && sa.getParamOrDefault("CharmNum", "1").contains("MaxUniqueOpponents")) {
// using getCardForUi game is not set, so can't guess max charm
num = Integer.MAX_VALUE;
} else {
num = Math.min(AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa), list.size());
}
final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
boolean repeat = sa.hasParam("CanRepeatModes"); boolean repeat = sa.hasParam("CanRepeatModes");
boolean random = sa.hasParam("Random"); boolean random = sa.hasParam("Random");
boolean oppChooses = "Opponent".equals(sa.getParam("Chooser")); boolean oppChooses = "Opponent".equals(sa.getParam("Chooser"));
List<AbilitySub> list = CharmEffect.makePossibleOptions(sa);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(sa.getCostDescription()); sb.append(sa.getCostDescription());
sb.append(oppChooses ? "An opponent chooses " : "Choose "); sb.append(oppChooses ? "An opponent chooses " : "Choose ");
@@ -117,8 +83,10 @@ public class CharmEffect extends SpellAbilityEffect {
if (sa.hasParam("ChoiceRestriction")) { if (sa.hasParam("ChoiceRestriction")) {
String rest = sa.getParam("ChoiceRestriction"); String rest = sa.getParam("ChoiceRestriction");
if (rest.equals("NotRemembered")) { if (rest.equals("ThisGame")) {
sb.append(" that hasn't been chosen"); sb.append(" that hasn't been chosen");
} else if (rest.equals("ThisTurn")) {
sb.append(" that hasn't been chosen this turn");
} }
} }
@@ -163,39 +131,52 @@ public class CharmEffect extends SpellAbilityEffect {
//this resets all previous choices //this resets all previous choices
sa.setSubAbility(null); sa.setSubAbility(null);
List<AbilitySub> choices = makePossibleOptions(sa);
// Entwine does use all Choices // Entwine does use all Choices
if (sa.isEntwine()) { if (sa.isEntwine()) {
chainAbilities(sa, makePossibleOptions(sa)); chainAbilities(sa, choices);
return true;
}
final int num = sa.hasParam("CharmNumOnResolve") ?
AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CharmNumOnResolve"), sa)
: Integer.parseInt(sa.getParamOrDefault("CharmNum", "1"));
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
if (sa.hasParam("Random")) {
chainAbilities(sa, Aggregates.random(makePossibleOptions(sa), num));
return true; return true;
} }
Card source = sa.getHostCard(); Card source = sa.getHostCard();
Player activator = sa.getActivatingPlayer(); Player activator = sa.getActivatingPlayer();
final int num = Math.min(AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa), choices.size());
final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
// if the amount of choices is smaller than min then they can't be chosen
if (min > choices.size()) {
return false;
}
if (sa.hasParam("Random")) {
chainAbilities(sa, Aggregates.random(choices, num));
return true;
}
Player chooser = sa.getActivatingPlayer(); Player chooser = sa.getActivatingPlayer();
if (sa.hasParam("Chooser")) { if (sa.hasParam("Chooser")) {
// Three modal cards require you to choose a player to make the modal choice' // Three modal cards require you to choose a player to make the modal choice'
// Two of these also reference the chosen player during the spell effect // Two of these also reference the chosen player during the spell effect
//String choosers = sa.getParam("Chooser"); //String choosers = sa.getParam("Chooser");
FCollection<Player> opponents = activator.getOpponents(); // all cards have Choser$ Opponent, so it's hardcoded here FCollection<Player> opponents = activator.getOpponents(); // all cards have Choser$ Opponent, so it's hardcoded here
chooser = activator.getController().chooseSingleEntityForEffect(opponents, sa, "Choose an opponent", null); chooser = activator.getController().chooseSingleEntityForEffect(opponents, sa, "Choose an opponent", null);
source.setChosenPlayer(chooser); source.setChosenPlayer(chooser);
} }
List<AbilitySub> chosen = chooser.getController().chooseModeForAbility(sa, min, num, sa.hasParam("CanRepeatModes")); List<AbilitySub> chosen = chooser.getController().chooseModeForAbility(sa, choices, min, num, sa.hasParam("CanRepeatModes"));
chainAbilities(sa, chosen); chainAbilities(sa, chosen);
return chosen != null && !chosen.isEmpty();
// trigger without chosen modes are removed from stack
if (sa.isTrigger()) {
return chosen != null && !chosen.isEmpty();
}
// for spells and activated abilities it is possible to chose zero if minCharmNum allows it
return true;
} }
private static void chainAbilities(SpellAbility sa, List<AbilitySub> chosen) { private static void chainAbilities(SpellAbility sa, List<AbilitySub> chosen) {
@@ -239,7 +220,7 @@ public class CharmEffect extends SpellAbilityEffect {
// add Clone to Tail of sa // add Clone to Tail of sa
sa.appendSubAbility(clone); sa.appendSubAbility(clone);
} }
} }

View File

@@ -34,7 +34,6 @@ import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostSacrifice; import forge.game.cost.CostSacrifice;
@@ -280,6 +279,11 @@ public class Card extends GameEntity implements Comparable<Card> {
private final Table<SpellAbility, StaticAbility, Integer> numberTurnActivationsStatic = HashBasedTable.create(); private final Table<SpellAbility, StaticAbility, Integer> numberTurnActivationsStatic = HashBasedTable.create();
private final Table<SpellAbility, StaticAbility, Integer> numberGameActivationsStatic = HashBasedTable.create(); private final Table<SpellAbility, StaticAbility, Integer> numberGameActivationsStatic = HashBasedTable.create();
private final Map<SpellAbility, List<String>> chosenModesTurn = Maps.newHashMap();
private final Map<SpellAbility, List<String>> chosenModesGame = Maps.newHashMap();
private final Table<SpellAbility, StaticAbility, List<String>> chosenModesTurnStatic = HashBasedTable.create();
private final Table<SpellAbility, StaticAbility, List<String>> chosenModesGameStatic = HashBasedTable.create();
// Enumeration for CMC request types // Enumeration for CMC request types
public enum SplitCMCMode { public enum SplitCMCMode {
@@ -2326,14 +2330,7 @@ public class Card extends GameEntity implements Comparable<Card> {
private String formatSpellAbility(final SpellAbility sa) { private String formatSpellAbility(final SpellAbility sa) {
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
final String elementText = sa.toString(); sb.append(sa.toString()).append("\r\n");
//Determine if a card has multiple choices, then format it in an easier to read list.
if (ApiType.Charm.equals(sa.getApi())) {
sb.append(CharmEffect.makeFormatedDescription(sa));
} else {
sb.append(elementText).append("\r\n");
}
return sb.toString(); return sb.toString();
} }
@@ -5916,6 +5913,7 @@ public class Card extends GameEntity implements Comparable<Card> {
clearBlockedThisTurn(); clearBlockedThisTurn();
resetMayPlayTurn(); resetMayPlayTurn();
resetExtertedThisTurn(); resetExtertedThisTurn();
resetChosenModeTurn();
} }
public boolean hasETBTrigger(final boolean drawbackOnly) { public boolean hasETBTrigger(final boolean drawbackOnly) {
@@ -6583,6 +6581,94 @@ public class Card extends GameEntity implements Comparable<Card> {
numberTurnActivationsStatic.clear(); numberTurnActivationsStatic.clear();
} }
public List<String> getChosenModesTurn(SpellAbility ability) {
SpellAbility original = null;
SpellAbility root = ability.getRootAbility();
// because trigger spell abilities are copied, try to get original one
if (root.isTrigger()) {
original = root.getTrigger().getOverridingAbility();
} else {
original = ability.getOriginalAbility();
if (original == null) {
original = ability;
}
}
if (ability.getGrantorStatic() != null) {
return chosenModesTurnStatic.get(original, ability.getGrantorStatic());
}
return chosenModesTurn.get(original);
}
public List<String> getChosenModesGame(SpellAbility ability) {
SpellAbility original = null;
SpellAbility root = ability.getRootAbility();
// because trigger spell abilities are copied, try to get original one
if (root.isTrigger()) {
original = root.getTrigger().getOverridingAbility();
} else {
original = ability.getOriginalAbility();
if (original == null) {
original = ability;
}
}
if (ability.getGrantorStatic() != null) {
return chosenModesGameStatic.get(original, ability.getGrantorStatic());
}
return chosenModesGame.get(original);
}
public void addChosenModes(SpellAbility ability, String mode) {
SpellAbility original = null;
SpellAbility root = ability.getRootAbility();
// because trigger spell abilities are copied, try to get original one
if (root.isTrigger()) {
original = root.getTrigger().getOverridingAbility();
} else {
original = ability.getOriginalAbility();
if (original == null) {
original = ability;
}
}
if (ability.getGrantorStatic() != null) {
List<String> result = chosenModesTurnStatic.get(original, ability.getGrantorStatic());
if (result == null) {
result = Lists.newArrayList();
chosenModesTurnStatic.put(original, ability.getGrantorStatic(), result);
}
result.add(mode);
result = chosenModesGameStatic.get(original, ability.getGrantorStatic());
if (result == null) {
result = Lists.newArrayList();
chosenModesGameStatic.put(original, ability.getGrantorStatic(), result);
}
result.add(mode);
} else {
List<String> result = chosenModesTurn.get(original);
if (result == null) {
result = Lists.newArrayList();
chosenModesTurn.put(original, result);
}
result.add(mode);
result = chosenModesGame.get(original);
if (result == null) {
result = Lists.newArrayList();
chosenModesGame.put(original, result);
}
result.add(mode);
}
}
public void resetChosenModeTurn() {
chosenModesTurn.clear();
chosenModesTurnStatic.clear();
}
public int getPlaneswalkerAbilityActivated() { public int getPlaneswalkerAbilityActivated() {
return planeswalkerAbilityActivated; return planeswalkerAbilityActivated;
} }

View File

@@ -213,7 +213,7 @@ public abstract class PlayerController {
public abstract boolean chooseFlipResult(SpellAbility sa, Player flipper, boolean[] results, boolean call); public abstract boolean chooseFlipResult(SpellAbility sa, Player flipper, boolean[] results, boolean call);
public abstract Card chooseProtectionShield(GameEntity entityBeingDamaged, List<String> options, Map<String, Card> choiceMap); public abstract Card chooseProtectionShield(GameEntity entityBeingDamaged, List<String> options, Map<String, Card> choiceMap);
public abstract List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat); public abstract List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat);
public abstract byte chooseColor(String message, SpellAbility sa, ColorSet colors); public abstract byte chooseColor(String message, SpellAbility sa, ColorSet colors);
public abstract byte chooseColorAllowColorless(String message, Card c, ColorSet colors); public abstract byte chooseColorAllowColorless(String message, Card c, ColorSet colors);

View File

@@ -575,6 +575,8 @@ public class TriggerHandler {
sa.setStackDescription(sa.toString()); sa.setStackDescription(sa.toString());
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
// need to be set for demonic pact to look for chosen modes
sa.setTrigger(regtrig);
if (!CharmEffect.makeChoices(sa)) { if (!CharmEffect.makeChoices(sa)) {
// 603.3c If no mode is chosen, the ability is removed from the stack. // 603.3c If no mode is chosen, the ability is removed from the stack.
return; return;

View File

@@ -246,9 +246,9 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
} }
} }
if (sp.getApi() == ApiType.Charm && sp.hasParam("RememberChoice")) { if (sp.getApi() == ApiType.Charm && sp.hasParam("ChoiceRestriction")) {
// Remember the Choice here for later handling // Remember the Choice here for later handling
source.addRemembered(sp.getSubAbility()); source.addChosenModes(sp, sp.getSubAbility().getDescription());
} }
//cancel auto-pass for all opponents of activating player //cancel auto-pass for all opponents of activating player

View File

@@ -446,7 +446,7 @@ public class PlayerControllerForTests extends PlayerController {
} }
@Override @Override
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {
throw new IllegalStateException("Erring on the side of caution here..."); throw new IllegalStateException("Erring on the side of caution here...");
} }

View File

@@ -5,14 +5,11 @@ R:Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | ReplaceWith$
SVar:DBChooseOpp:DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent | ChoiceTitle$ Choose an opponent to give control to: | AILogic$ Curse | SubAbility$ MoveToPlay SVar:DBChooseOpp:DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent | ChoiceTitle$ Choose an opponent to give control to: | AILogic$ Curse | SubAbility$ MoveToPlay
SVar:MoveToPlay:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Battlefield | Defined$ ReplacedCard | GainControl$ True | NewController$ ChosenPlayer | SubAbility$ ClearRemembered SVar:MoveToPlay:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Battlefield | Defined$ ReplacedCard | GainControl$ True | NewController$ ChosenPlayer | SubAbility$ ClearRemembered
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY
SVar:TrigCharm:DB$ Charm | Choices$ LifePact,DiscardPact,ZombiesPact | ChoiceRestriction$ NotRemembered | RememberChoice$ True | CharmNum$ 1 SVar:TrigCharm:DB$ Charm | Choices$ LifePact,DiscardPact,ZombiesPact | ChoiceRestriction$ ThisGame | CharmNum$ 1
SVar:LifePact:DB$ SetLife | Defined$ You | LifeAmount$ 4 | ChoiceName$ LifePact | SpellDescription$ Your life total becomes 4. SVar:LifePact:DB$ SetLife | Defined$ You | LifeAmount$ 4 | SpellDescription$ Your life total becomes 4.
SVar:DiscardPact:DB$ Discard | Defined$ You | Mode$ Hand | ChoiceName$ DiscardPact | SpellDescription$ Discard your hand. SVar:DiscardPact:DB$ Discard | Defined$ You | Mode$ Hand | SpellDescription$ Discard your hand.
SVar:ZombiesPact:DB$ RepeatEach | RepeatPlayers$ Player.Opponent | RepeatSubAbility$ MakeZombies | ChoiceName$ ZombiesPact | ChangeZoneTable$ True | SpellDescription$ Each opponent creates five 2/2 black Zombie creature tokens. SVar:ZombiesPact:DB$ RepeatEach | RepeatPlayers$ Player.Opponent | RepeatSubAbility$ MakeZombies | ChangeZoneTable$ True | SpellDescription$ Each opponent creates five 2/2 black Zombie creature tokens.
SVar:MakeZombies:DB$ Token | LegacyImage$ b 2 2 zombie rna | TokenAmount$ 5 | TokenScript$ b_2_2_zombie | TokenOwner$ Remembered | SpellDescription$ Each opponent creates five 2/2 black Zombie creature tokens. SVar:MakeZombies:DB$ Token | LegacyImage$ b 2 2 zombie rna | TokenAmount$ 5 | TokenScript$ b_2_2_zombie | TokenOwner$ Remembered | SpellDescription$ Each opponent creates five 2/2 black Zombie creature tokens.
# Clear RememberChoice just in case it's not getting cleared by Zone changes
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ ClearRemembered | Static$ True
SVar:ClearRemembered:DB$ Cleanup | ClearRemembered$ True | ClearChosenPlayer$ True
AI:RemoveDeck:Random AI:RemoveDeck:Random
DeckHas:Ability$Token DeckHas:Ability$Token
Oracle:Captive Audience enters the battlefield under the control of an opponent of your choice.\nAt the beginning of your upkeep, choose one that hasn't been chosen —\n• Your life total becomes 4.\n• Discard your hand.\n• Each opponent creates five 2/2 black Zombie creature tokens. Oracle:Captive Audience enters the battlefield under the control of an opponent of your choice.\nAt the beginning of your upkeep, choose one that hasn't been chosen —\n• Your life total becomes 4.\n• Discard your hand.\n• Each opponent creates five 2/2 black Zombie creature tokens.

View File

@@ -2,15 +2,11 @@ Name:Demonic Pact
ManaCost:2 B B ManaCost:2 B B
Types:Enchantment Types:Enchantment
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY
SVar:TrigCharm:DB$ Charm | Choices$ DrainPact,DiscardPact,DrawPact,DeathPact | ChoiceRestriction$ NotRemembered | RememberChoice$ True | CharmNum$ 1 SVar:TrigCharm:DB$ Charm | Choices$ DrainPact,DiscardPact,DrawPact,DeathPact | ChoiceRestriction$ ThisGame | CharmNum$ 1
SVar:DrainPact:DB$ DealDamage | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ 4 | SubAbility$ DBGainLife | ChoiceName$ DrainPact | SpellDescription$ CARDNAME deals 4 damage to any target and you gain 4 life. SVar:DrainPact:DB$ DealDamage | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ 4 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 4 damage to any target and you gain 4 life.
SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 4 SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 4
SVar:DiscardPact:DB$ Discard | ValidTgts$ Player | NumCards$ 2 | Mode$ TgtChoose | ChoiceName$ DiscardPact | SpellDescription$ Target player discards two cards. SVar:DiscardPact:DB$ Discard | ValidTgts$ Player | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ Target player discards two cards.
SVar:DrawPact:DB$ Draw | NumCards$ 2 | ChoiceName$ DrawPact | SpellDescription$ Draw two cards. SVar:DrawPact:DB$ Draw | NumCards$ 2 | SpellDescription$ Draw two cards.
SVar:DeathPact:DB$ LosesGame | Defined$ You | ChoiceName$ DeathPact | SpellDescription$ You lose the game. SVar:DeathPact:DB$ LosesGame | Defined$ You | SpellDescription$ You lose the game.
# Clear RememberChoice just in case it's not getting cleared by Zone changes
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ ClearRemembered | Static$ True
SVar:ClearRemembered:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All AI:RemoveDeck:All
SVar:Picture:http://www.wizards.com/global/images/magic/general/demonic_pact.jpg
Oracle:At the beginning of your upkeep, choose one that hasn't been chosen —\n• Demonic Pact deals 4 damage to any target and you gain 4 life.\n• Target opponent discards two cards.\n• Draw two cards.\n• You lose the game. Oracle:At the beginning of your upkeep, choose one that hasn't been chosen —\n• Demonic Pact deals 4 damage to any target and you gain 4 life.\n• Target opponent discards two cards.\n• Draw two cards.\n• You lose the game.

View File

@@ -0,0 +1,14 @@
Name:Inscription of Abundance
ManaCost:1 G
Types:Instant
K:Kicker:2 G
A:SP$ Charm | Cost$ 1 G | MinCharmNum$ X | CharmNum$ Y | References$ X,Y | Choices$ DBPutCounter,DBGainLife,DBPump | AdditionalDescription$ If this spell was kicked, choose any number instead.
SVar:DBPutCounter:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select target creature | CounterType$ P1P1 | CounterNum$ 2 | SpellDescription$ Put two +1/+1 counters on target creature.
SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player | LifeAmount$ Z | References$ Z | SpellDescription$ Target player gains X life, where X is the greatest power among creatures they control.
SVar:DBPump:DB$ Pump | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | AILogic$ Fight | SubAbility$ DBFight | SpellDescription$ Target creature you control fights target creature you don't control.
SVar:DBFight:DB$ Fight | Defined$ ParentTarget | ValidTgts$ Creature.YouDontCtrl | TgtPrompt$ Select target creature you don't control
SVar:X:Count$Kicked.0.1
SVar:Y:Count$Kicked.3.1
SVar:Z:Count$GreatestPower_Creature.TargetedPlayerCtrl
Oracle:Kicker {2}{G}\nChoose one. If this spell was kicked, choose any number instead.\n• Put two +1/+1 counters on target creature.\n• Target player gains X life, where X is the greatest power among creatures they control.\n• Target creature you control fights target creature you don't control.

View File

@@ -0,0 +1,14 @@
Name:Inscription of Insight
ManaCost:3 U
Types:Sorcery
K:Kicker:2 U U
A:SP$ Charm | Cost$ 3 U | MinCharmNum$ X | CharmNum$ Y | References$ X,Y | Choices$ DBReturn,DBScry,DBToken | AdditionalDescription$ If this spell was kicked, choose any number instead.
SVar:DBReturn:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | ValidTgts$ Creature | TgtPrompt$ Select up to two target creatures | Origin$ Battlefield | Destination$ Hand | SpellDescription$ Return up to two target creatures to their owners' hands.
SVar:DBScry:DB$ Scry | ScryNum$ 2 | SubAbility$ DBDraw | SpellDescription$ Scry 2, then draw two cards.
SVar:DBDraw:DB$ Draw | NumCards$ 2
SVar:DBToken:DB$ Token | ValidTgts$ Player | TgtPrompt$ Select target player | TokenAmount$ 1 | TokenScript$ u_x_x_illusion | TokenOwner$ TargetedPlayer | TokenPower$ Z | TokenToughness$ Z | References$ Z | SpellDescription$ Target player creates an X/X blue Illusion creature token, where X is the number of cards in their hand.
SVar:X:Count$Kicked.0.1
SVar:Y:Count$Kicked.3.1
SVar:Z:TargetedPlayer$CardsInHand
Oracle:Kicker {2}{U}{U}\nChoose one. If this spell was kicked, choose any number instead.\n• Return up to two target creatures to their owners hands.\n• Scry 2, then draw two cards.\n• Target player creates an X/X blue Illusion creature token, where X is the number of cards in their hand.

View File

@@ -0,0 +1,12 @@
Name:Inscription of Ruin
ManaCost:2 B
Types:Sorcery
K:Kicker:2 B B
A:SP$ Charm | Cost$ 2 B | MinCharmNum$ X | CharmNum$ Y | References$ X,Y | Choices$ DBDiscard,DBReturn,DBDestroy | AdditionalDescription$ If this spell was kicked, choose any number instead.
SVar:DBDiscard:DB$ Discard | ValidTgts$ Opponent | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ Target opponent discards two cards.
SVar:DBReturn:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Card.Creature+cmcLE2+YouOwn | TgtPrompt$ Select target creature card with converted mana cost 2 or less | SpellDescription$ Return target creature card with converted mana cost 2 or less from your graveyard to the battlefield.
SVar:DBDestroy:DB$ Destroy | ValidTgts$ Creature.cmcLE3 | TgtPrompt$ Select target creature with converted mana cost 3 or less | SpellDescription$ Destroy target creature with converted mana cost 3 or less.
SVar:X:Count$Kicked.0.1
SVar:Y:Count$Kicked.3.1
Oracle:Kicker {2}{B}{B}\nChoose one. If this spell was kicked, choose any number instead.\n• Target opponent discards two cards.\n• Return target creature card with converted mana cost 2 or less from your graveyard to the battlefield.\n• Destroy target creature with converted mana cost 3 or less.

View File

@@ -4,9 +4,7 @@ Types:Creature Human Warrior
PT:3/1 PT:3/1
S:Mode$ Continuous | Affected$ Creature.Coward | AddHiddenKeyword$ CantBlock Creature.Warrior:Warriors | Description$ Cowards can't block Warriors. S:Mode$ Continuous | Affected$ Creature.Coward | AddHiddenKeyword$ CantBlock Creature.Warrior:Warriors | Description$ Cowards can't block Warriors.
SVar:PlayMain1:TRUE SVar:PlayMain1:TRUE
T:Mode$ TurnBegin | ValidPlayer$ Player | Static$ True | TriggerZones$ Battlefield | Execute$ CharmReset A:AB$ Charm | Cost$ 1 | Choices$ Pump,Coward,Trample | ChoiceRestriction$ ThisTurn | CharmNum$ 1
SVar:CharmReset:DB$ Cleanup | ClearRemembered$ True
A:AB$ Charm | Cost$ 1 | Choices$ Pump,Coward,Trample | ChoiceRestriction$ NotRemembered | RememberChoice$ True | CharmNum$ 1
SVar:Pump:DB$ Pump | Defined$ Self | NumAtt$ 1 | NumDef$ 1 | SpellDescription$ CARDNAME gets +1/+1 until end of turn. SVar:Pump:DB$ Pump | Defined$ Self | NumAtt$ 1 | NumDef$ 1 | SpellDescription$ CARDNAME gets +1/+1 until end of turn.
SVar:Coward:DB$ Animate | ValidTgts$ Creature | TgtPrompt$ Select target creature | Types$ Coward | RemoveCreatureTypes$ True | SpellDescription$ Target creature becomes a Coward until end of turn. SVar:Coward:DB$ Animate | ValidTgts$ Creature | TgtPrompt$ Select target creature | Types$ Coward | RemoveCreatureTypes$ True | SpellDescription$ Target creature becomes a Coward until end of turn.
SVar:Trample:DB$ Pump | ValidTgts$ Warrior | TgtPrompt$ Select target Warrior | KW$ Trample | SpellDescription$ Target Warrior gains trample until end of turn. SVar:Trample:DB$ Pump | ValidTgts$ Warrior | TgtPrompt$ Select target Warrior | KW$ Trample | SpellDescription$ Target Warrior gains trample until end of turn.

View File

@@ -3,11 +3,9 @@ ManaCost:3 B
Types:Creature Zombie Wizard Types:Creature Zombie Wizard
PT:4/1 PT:4/1
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigCharm | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, ABILITY T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigCharm | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, ABILITY
SVar:TrigCharm:DB$ Charm | MinCharmNum$ 1 | CharmNum$ 3 | CharmNumOnResolve$ MaxUniqueOpponents | Choices$ SacCreature,DiscardCards,LoseLife | References$ MaxUniqueOpponents | AdditionalDescription$ Each mode must target a different player. SVar:TrigCharm:DB$ Charm | MinCharmNum$ 1 | CharmNum$ MaxUniqueOpponents | Choices$ SacCreature,DiscardCards,LoseLife | References$ MaxUniqueOpponents | AdditionalDescription$ Each mode must target a different player.
SVar:SacCreature:DB$ Sacrifice | ValidTgts$ Opponent | TargetUnique$ True | SacValid$ Creature | SacMessage$ Creature | SpellDescription$ Target opponent sacrifices a creature. SVar:SacCreature:DB$ Sacrifice | ValidTgts$ Opponent | TargetUnique$ True | SacValid$ Creature | SacMessage$ Creature | SpellDescription$ Target opponent sacrifices a creature.
SVar:DiscardCards:DB$ Discard | ValidTgts$ Opponent | TargetUnique$ True | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ Target opponent discards two cards. SVar:DiscardCards:DB$ Discard | ValidTgts$ Opponent | TargetUnique$ True | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ Target opponent discards two cards.
SVar:LoseLife:DB$ LoseLife | ValidTgts$ Opponent | TargetUnique$ True | LifeAmount$ 5 | SpellDescription$ Target opponent loses 5 life. SVar:LoseLife:DB$ LoseLife | ValidTgts$ Opponent | TargetUnique$ True | LifeAmount$ 5 | SpellDescription$ Target opponent loses 5 life.
SVar:MaxUniqueOpponents:PlayerCountOpponents$Amount SVar:MaxUniqueOpponents:PlayerCountOpponents$Amount
#TODO: The AI is able to target the same player with multiple modes, usually all three. This should not happen.
AI:RemoveDeck:All
Oracle:When Vindictive Lich dies, choose one or more. Each mode must target a different player.\n• Target opponent sacrifices a creature.\n• Target opponent discards two cards.\n• Target opponent loses 5 life. Oracle:When Vindictive Lich dies, choose one or more. Each mode must target a different player.\n• Target opponent sacrifices a creature.\n• Target opponent discards two cards.\n• Target opponent loses 5 life.

View File

@@ -85,7 +85,9 @@ public class HumanPlay {
} }
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa); if (!CharmEffect.makeChoices(sa)) {
return false;
}
} }
sa = AbilityUtils.addSpliceEffects(sa); sa = AbilityUtils.addSpliceEffects(sa);
@@ -150,7 +152,9 @@ public class HumanPlay {
if (!sa.isCopied()) { if (!sa.isCopied()) {
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa); if (!CharmEffect.makeChoices(sa)) {
return;
}
} }
sa = AbilityUtils.addSpliceEffects(sa); sa = AbilityUtils.addSpliceEffects(sa);
} }

View File

@@ -1,20 +1,8 @@
package forge.player; package forge.player;
import java.io.BufferedWriter; import java.io.*;
import java.io.File; import java.util.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.Range; import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -62,7 +50,6 @@ import forge.game.PlanarDice;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
@@ -1591,14 +1578,13 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
* spellability.SpellAbility, java.util.List, int, int) * spellability.SpellAbility, java.util.List, int, int)
*/ */
@Override @Override
public List<AbilitySub> chooseModeForAbility(final SpellAbility sa, final int min, final int num, public List<AbilitySub> chooseModeForAbility(final SpellAbility sa, List<AbilitySub> possible, final int min, final int num,
boolean allowRepeat) { boolean allowRepeat) {
boolean trackerFrozen = game.getTracker().isFrozen(); boolean trackerFrozen = game.getTracker().isFrozen();
if (trackerFrozen) { if (trackerFrozen) {
// The view tracker needs to be unfrozen to update the SpellAbilityViews at this point, or it may crash // The view tracker needs to be unfrozen to update the SpellAbilityViews at this point, or it may crash
game.getTracker().unfreeze(); game.getTracker().unfreeze();
} }
final List<AbilitySub> possible = CharmEffect.makePossibleOptions(sa);
Map<SpellAbilityView, AbilitySub> spellViewCache = SpellAbilityView.getMap(possible); Map<SpellAbilityView, AbilitySub> spellViewCache = SpellAbilityView.getMap(possible);
if (trackerFrozen) { if (trackerFrozen) {
game.getTracker().freeze(); // refreeze if the tracker was frozen prior to this update game.getTracker().freeze(); // refreeze if the tracker was frozen prior to this update