diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 58e2bd57142..4cff650ac63 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -27,19 +27,24 @@ import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; /** + * AI: mock player in simulated games (each player replaced by simulated) + * * @author BetaSteward_at_googlemail.com */ public class SimulatedPlayer2 extends ComputerPlayer { private static final Logger logger = Logger.getLogger(SimulatedPlayer2.class); private static final PassAbility pass = new PassAbility(); + private final boolean isSimulatedPlayer; private final List suggested; private transient ConcurrentLinkedQueue allActions; private boolean forced; + private final Player originalPlayer; // copy of the original player, source of choices/results in tests public SimulatedPlayer2(Player originalPlayer, boolean isSimulatedPlayer, List suggested) { super(originalPlayer.getId()); + this.originalPlayer = originalPlayer.copy(); pass.setControllerId(playerId); this.isSimulatedPlayer = isSimulatedPlayer; this.suggested = suggested; @@ -50,11 +55,9 @@ public class SimulatedPlayer2 extends ComputerPlayer { public SimulatedPlayer2(final SimulatedPlayer2 player) { super(player); this.isSimulatedPlayer = player.isSimulatedPlayer; - this.suggested = new ArrayList<>(); - for (String s : player.suggested) { - this.suggested.add(s); - } - + this.suggested = new ArrayList<>(player.suggested); + // this.allActions = player.allActions; // dynamic, no need to copy + this.originalPlayer = player.originalPlayer.copy(); } @Override @@ -448,4 +451,16 @@ public class SimulatedPlayer2 extends ComputerPlayer { pass(game); return false; } + + @Override + public boolean flipCoinResult(Game game) { + // same random results set up support in AI tests, see TestComputerPlayer for docs + return originalPlayer.flipCoinResult(game); + } + + @Override + public int rollDieResult(int sides, Game game) { + // same random results set up support in AI tests, see TestComputerPlayer for docs + return originalPlayer.rollDieResult(sides, game); + } } diff --git a/Mage.Sets/src/mage/cards/a/ArcaneEndeavor.java b/Mage.Sets/src/mage/cards/a/ArcaneEndeavor.java new file mode 100644 index 00000000000..70f26c348c8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/ArcaneEndeavor.java @@ -0,0 +1,88 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.cost.CastWithoutPayingManaCostEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.FilterCard; +import mage.filter.common.FilterInstantOrSorceryCard; +import mage.game.Game; +import mage.players.Player; + +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ArcaneEndeavor extends CardImpl { + + public ArcaneEndeavor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{5}{U}{U}"); + + // Roll two d8 and choose one result. Draw cards equal to that result. Then you may cast an instant or sorcery spell with mana value less than or equal to the other result from your hand without paying its mana cost. + this.getSpellAbility().addEffect(new ArcaneEndeavorEffect()); + } + + private ArcaneEndeavor(final ArcaneEndeavor card) { + super(card); + } + + @Override + public ArcaneEndeavor copy() { + return new ArcaneEndeavor(this); + } +} + +class ArcaneEndeavorEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterInstantOrSorceryCard( + "instant or sorcery card with mana value %mv or less from your hand" + ); + + ArcaneEndeavorEffect() { + super(Outcome.Benefit); + staticText = "roll two d8 and choose one result. Draw cards equal to that result. " + + "Then you may cast an instant or sorcery spell with mana value less than " + + "or equal to the other result from your hand without paying its mana cost"; + } + + private ArcaneEndeavorEffect(final ArcaneEndeavorEffect effect) { + super(effect); + } + + @Override + public ArcaneEndeavorEffect copy() { + return new ArcaneEndeavorEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + List results = player.rollDice(outcome, source, game, 8, 2, 0); + int firstResult = results.get(0); + int secondResult = results.get(1); + int first, second; + if (firstResult != secondResult && player.chooseUse( + outcome, "Choose a number of cards to draw", + "The other number will be the maximum mana value of the spell you cast", + "" + firstResult, "" + secondResult, source, game + )) { + first = firstResult; + second = secondResult; + } else { + first = secondResult; + second = firstResult; + } + player.drawCards(first, source, game); + new CastWithoutPayingManaCostEffect(StaticValue.get(second), filter).apply(game, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/a/AsLuckWouldHaveIt.java b/Mage.Sets/src/mage/cards/a/AsLuckWouldHaveIt.java index 970e79a553d..6afbc6c4822 100644 --- a/Mage.Sets/src/mage/cards/a/AsLuckWouldHaveIt.java +++ b/Mage.Sets/src/mage/cards/a/AsLuckWouldHaveIt.java @@ -1,25 +1,25 @@ - package mage.cards.a; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.HexproofAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.RollDieType; import mage.constants.Zone; import mage.counters.Counter; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** - * * @author spjspj */ public final class AsLuckWouldHaveIt extends CardImpl { @@ -63,15 +63,18 @@ class AsLuckWouldHaveItTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DICE_ROLLED; + return event.getType() == GameEvent.EventType.DIE_ROLLED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (this.isControlledBy(event.getPlayerId()) && event.getFlag()) { - for (Effect effect : this.getEffects()) { - effect.setValue("rolled", event.getAmount()); - } + DieRolledEvent drEvent = (DieRolledEvent) event; + // Any die roll with a numerical result will add luck counters to As Luck Would Have It. + // Rolling the planar die will not cause the second ability to trigger. + // (2018-01-19) + if (this.isControlledBy(event.getPlayerId()) && drEvent.getRollDieType() == RollDieType.NUMERICAL) { + // silver border card must look for "result" instead "natural result" + this.getEffects().setValue("rolled", drEvent.getResult()); return true; } return false; diff --git a/Mage.Sets/src/mage/cards/b/BagOfDevouring.java b/Mage.Sets/src/mage/cards/b/BagOfDevouring.java index 0c9ebe17a67..b223aa83345 100644 --- a/Mage.Sets/src/mage/cards/b/BagOfDevouring.java +++ b/Mage.Sets/src/mage/cards/b/BagOfDevouring.java @@ -107,7 +107,7 @@ class BagOfDevouringEffect extends OneShotEffect { if (player == null) { return false; } - int result = player.rollDice(source, game, 10); + int result = player.rollDice(Outcome.Benefit, source, game, 10); TargetCard target = new TargetCardInExile( 0, result, StaticFilters.FILTER_CARD, CardUtil.getExileZoneId(game, source) diff --git a/Mage.Sets/src/mage/cards/b/BagOfTricks.java b/Mage.Sets/src/mage/cards/b/BagOfTricks.java index 4d4fb098f30..18817fa6368 100644 --- a/Mage.Sets/src/mage/cards/b/BagOfTricks.java +++ b/Mage.Sets/src/mage/cards/b/BagOfTricks.java @@ -62,7 +62,7 @@ class BagOfTricksEffect extends OneShotEffect { if (player == null) { return false; } - int result = player.rollDice(source, game, 8); + int result = player.rollDice(outcome, source, game, 8); Cards cards = new CardsImpl(); for (Card card : player.getLibrary().getCards(game)) { cards.add(card); diff --git a/Mage.Sets/src/mage/cards/b/BarbarianClass.java b/Mage.Sets/src/mage/cards/b/BarbarianClass.java new file mode 100644 index 00000000000..5371390f800 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BarbarianClass.java @@ -0,0 +1,117 @@ +package mage.cards.b; + +import mage.abilities.Ability; +import mage.abilities.common.OneOrMoreDiceRolledTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.RollDiceEvent; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BarbarianClass extends CardImpl { + + public BarbarianClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{R}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // If you would roll one or more dice, instead roll that many dice plus one and ignore the lowest roll. + this.addAbility(new SimpleStaticAbility(new BarbarianClassEffect())); + + // {1}{R}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{1}{R}")); + + // Whenever you roll one or more dice, target creature you control gets +2/+0 and gains menace until end of turn. + Ability ability = new OneOrMoreDiceRolledTriggeredAbility( + new BoostTargetEffect(2, 0) + .setText("target creature you control gets +2/+0") + ); + ability.addEffect(new GainAbilityTargetEffect( + new MenaceAbility(), Duration.EndOfTurn + ).setText("and gains menace until end of turn")); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect(ability, 2))); + + // {2}{R}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{2}{R}")); + + // Creatures you control have haste. + this.addAbility(new SimpleStaticAbility( + new GainClassAbilitySourceEffect(new GainAbilityControlledEffect( + HasteAbility.getInstance(), Duration.WhileOnBattlefield, + StaticFilters.FILTER_PERMANENT_CREATURES + ), 3) + )); + } + + private BarbarianClass(final BarbarianClass card) { + super(card); + } + + @Override + public BarbarianClass copy() { + return new BarbarianClass(this); + } +} + +class BarbarianClassEffect extends ReplacementEffectImpl { + + BarbarianClassEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "if you would roll one or more dice, instead roll that many dice plus one and ignore the lowest roll"; + } + + private BarbarianClassEffect(final BarbarianClassEffect effect) { + super(effect); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + RollDiceEvent rdEvent = (RollDiceEvent) event; + rdEvent.incAmount(1); + rdEvent.incIgnoreLowestAmount(1); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ROLL_DICE + && ((RollDiceEvent) event).getRollDieType() == RollDieType.NUMERICAL; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return source.isControlledBy(event.getPlayerId()); + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public BarbarianClassEffect copy() { + return new BarbarianClassEffect(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BoxOfFreerangeGoblins.java b/Mage.Sets/src/mage/cards/b/BoxOfFreerangeGoblins.java index 48e72fd1e8a..f00201bfd99 100644 --- a/Mage.Sets/src/mage/cards/b/BoxOfFreerangeGoblins.java +++ b/Mage.Sets/src/mage/cards/b/BoxOfFreerangeGoblins.java @@ -56,7 +56,7 @@ class BoxOfFreerangeGoblinsEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); CreateTokenEffect effect = new CreateTokenEffect(new GoblinToken(), amount); effect.apply(game, source); return true; diff --git a/Mage.Sets/src/mage/cards/b/BrazenDwarf.java b/Mage.Sets/src/mage/cards/b/BrazenDwarf.java new file mode 100644 index 00000000000..6b5acff5248 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BrazenDwarf.java @@ -0,0 +1,39 @@ +package mage.cards.b; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.OneOrMoreDiceRolledTriggeredAbility; +import mage.abilities.effects.common.DamagePlayersEffect; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; + +/** + * + * @author weirddan455 + */ +public final class BrazenDwarf extends CardImpl { + + public BrazenDwarf(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}"); + + this.subtype.add(SubType.DWARF); + this.subtype.add(SubType.SHAMAN); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Whenever you roll one or more dice, Brazen Dwarf deals 1 damage to each opponent. + this.addAbility(new OneOrMoreDiceRolledTriggeredAbility(new DamagePlayersEffect(1, TargetController.OPPONENT))); + } + + private BrazenDwarf(final BrazenDwarf card) { + super(card); + } + + @Override + public BrazenDwarf copy() { + return new BrazenDwarf(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BucknardsEverfullPurse.java b/Mage.Sets/src/mage/cards/b/BucknardsEverfullPurse.java index fdd132921aa..aa28abec800 100644 --- a/Mage.Sets/src/mage/cards/b/BucknardsEverfullPurse.java +++ b/Mage.Sets/src/mage/cards/b/BucknardsEverfullPurse.java @@ -67,7 +67,7 @@ class BucknardsEverfullPurseEffect extends OneShotEffect { return false; } new TreasureToken().putOntoBattlefield( - player.rollDice(source, game, 4), + player.rollDice(outcome, source, game, 4), game, source, source.getControllerId() ); Permanent permanent = source.getSourcePermanentIfItStillExists(game); diff --git a/Mage.Sets/src/mage/cards/c/CantonicaCasino.java b/Mage.Sets/src/mage/cards/c/CantonicaCasino.java index 97d77800f01..9e494bbc09e 100644 --- a/Mage.Sets/src/mage/cards/c/CantonicaCasino.java +++ b/Mage.Sets/src/mage/cards/c/CantonicaCasino.java @@ -11,6 +11,7 @@ import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; +import java.util.List; import java.util.UUID; /** @@ -51,8 +52,9 @@ class CantonicaCasinoEffect extends OneShotEffect { Player you = game.getPlayer(source.getControllerId()); if (you != null) { // Roll two six-sided dice - int dice1 = you.rollDice(source, game, 6); - int dice2 = you.rollDice(source, game, 6); + List results = you.rollDice(outcome, source, game, 6, 2, 0); + int dice1 = results.get(0); + int dice2 = results.get(1); if (dice1 == dice2) { // If you roll doubles, gain 10 life diff --git a/Mage.Sets/src/mage/cards/c/ChaosDragon.java b/Mage.Sets/src/mage/cards/c/ChaosDragon.java index 535eb7a1c9c..2112f804ec1 100644 --- a/Mage.Sets/src/mage/cards/c/ChaosDragon.java +++ b/Mage.Sets/src/mage/cards/c/ChaosDragon.java @@ -79,7 +79,7 @@ class ChaosDragonEffect extends OneShotEffect { if (player == null) { return false; } - playerMap.computeIfAbsent(player.rollDice(source, game, 20), x -> new HashSet<>()).add(playerId); + playerMap.computeIfAbsent(player.rollDice(outcome, source, game, 20), x -> new HashSet<>()).add(playerId); } int max = playerMap.keySet().stream().mapToInt(x -> x).max().orElse(0); game.addEffect(new ChaosDragonRestrictionEffect(playerMap.get(max)), source); diff --git a/Mage.Sets/src/mage/cards/c/ChickenALaKing.java b/Mage.Sets/src/mage/cards/c/ChickenALaKing.java index a953eed669b..b44a0957270 100644 --- a/Mage.Sets/src/mage/cards/c/ChickenALaKing.java +++ b/Mage.Sets/src/mage/cards/c/ChickenALaKing.java @@ -17,6 +17,7 @@ import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.permanent.TappedPredicate; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; import mage.target.common.TargetControlledPermanent; @@ -82,20 +83,16 @@ class ChickenALaKingTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DICE_ROLLED; + return event.getType() == GameEvent.EventType.DIE_ROLLED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (this.isControlledBy(event.getPlayerId()) && event.getFlag()) { - // event.getData holds the num of sides of the die to roll - String data = event.getData(); - if (data != null) { - int numSides = Integer.parseInt(data); - return event.getAmount() == 6 && numSides == 6; - } - } - return false; + DieRolledEvent drEvent = (DieRolledEvent) event; + // silver border card must look for "result" instead "natural result" + return this.isControlledBy(drEvent.getPlayerId()) + && drEvent.getSides() == 6 + && drEvent.getResult() == 6; } @Override diff --git a/Mage.Sets/src/mage/cards/c/ChickenEgg.java b/Mage.Sets/src/mage/cards/c/ChickenEgg.java index 5489cdafd2f..7631083a5ad 100644 --- a/Mage.Sets/src/mage/cards/c/ChickenEgg.java +++ b/Mage.Sets/src/mage/cards/c/ChickenEgg.java @@ -60,7 +60,7 @@ class ChickenEggEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int result = controller.rollDice(source, game, 6); + int result = controller.rollDice(outcome, source, game, 6); if (result == 6) { new SacrificeSourceEffect().apply(game, source); return (new CreateTokenEffect(new GiantBirdToken(), 1)).apply(game, source); diff --git a/Mage.Sets/src/mage/cards/c/ChitteringDoom.java b/Mage.Sets/src/mage/cards/c/ChitteringDoom.java index 11e989835c9..cf805f0f967 100644 --- a/Mage.Sets/src/mage/cards/c/ChitteringDoom.java +++ b/Mage.Sets/src/mage/cards/c/ChitteringDoom.java @@ -1,7 +1,6 @@ package mage.cards.c; -import java.util.UUID; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; @@ -9,11 +8,13 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Zone; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; import mage.game.permanent.token.SquirrelToken; +import java.util.UUID; + /** - * * @author spjspj */ public final class ChitteringDoom extends CardImpl { @@ -52,17 +53,14 @@ class ChitteringDoomTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DICE_ROLLED; + return event.getType() == GameEvent.EventType.DIE_ROLLED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (this.isControlledBy(event.getPlayerId()) && event.getFlag()) { - if (event.getAmount() >= 4) { - return true; - } - } - return false; + DieRolledEvent drEvent = (DieRolledEvent) event; + // silver border card must look for "result" instead "natural result" + return this.isControlledBy(event.getPlayerId()) && drEvent.getResult() >= 4; } @Override diff --git a/Mage.Sets/src/mage/cards/c/ClamIAm.java b/Mage.Sets/src/mage/cards/c/ClamIAm.java index 03fc23f4901..6052dfa5293 100644 --- a/Mage.Sets/src/mage/cards/c/ClamIAm.java +++ b/Mage.Sets/src/mage/cards/c/ClamIAm.java @@ -1,24 +1,18 @@ - package mage.cards.c; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; import mage.game.events.GameEvent; -import mage.players.Player; + +import java.util.UUID; /** - * * @author L_J */ public final class ClamIAm extends CardImpl { @@ -56,28 +50,17 @@ class ClamIAmEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Player player = game.getPlayer(event.getPlayerId()); - if (player != null) { - String data = event.getData(); - int numSides = Integer.parseInt(data); - if (numSides == 6 && event.getAmount() == 3) { - if (player.chooseUse(outcome, "Reroll the die?", source, game)) { - game.informPlayers(player.getLogName() + " chose to reroll the die."); - event.setAmount(player.rollDice(source, game, 6)); - } - } - } - return false; + return true; } @Override public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ROLL_DICE; + return event.getType() == GameEvent.EventType.REPLACE_ROLLED_DIE; } @Override public boolean applies(GameEvent event, Ability source, Game game) { - return source.getControllerId().equals(event.getPlayerId()); + return source.isControlledBy(event.getPlayerId()); } @Override diff --git a/Mage.Sets/src/mage/cards/c/ClayGolem.java b/Mage.Sets/src/mage/cards/c/ClayGolem.java index 84ba183bfe8..3296da9ed43 100644 --- a/Mage.Sets/src/mage/cards/c/ClayGolem.java +++ b/Mage.Sets/src/mage/cards/c/ClayGolem.java @@ -82,7 +82,7 @@ class ClayGolemCost extends CostImpl { if (player == null) { return paid; } - lastRoll = player.rollDice(source, game, 8); + lastRoll = player.rollDice(Outcome.Benefit, source, game, 8); paid = true; return paid; } diff --git a/Mage.Sets/src/mage/cards/c/CriticalHit.java b/Mage.Sets/src/mage/cards/c/CriticalHit.java new file mode 100644 index 00000000000..eb9f4cce203 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CriticalHit.java @@ -0,0 +1,78 @@ +package mage.cards.c; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.ReturnSourceFromGraveyardToHandEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.DieRolledEvent; +import mage.game.events.GameEvent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CriticalHit extends CardImpl { + + public CriticalHit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{R}"); + + // Target creature gains double strike until end of turn. + this.getSpellAbility().addEffect(new GainAbilityTargetEffect( + DoubleStrikeAbility.getInstance(), Duration.EndOfTurn + )); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + + // When you roll a natural 20, return Critical Hit from your graveyard to your hand. + this.addAbility(new CriticalHitTriggeredAbility()); + } + + private CriticalHit(final CriticalHit card) { + super(card); + } + + @Override + public CriticalHit copy() { + return new CriticalHit(this); + } +} + +class CriticalHitTriggeredAbility extends TriggeredAbilityImpl { + + CriticalHitTriggeredAbility() { + super(Zone.GRAVEYARD, new ReturnSourceFromGraveyardToHandEffect()); + } + + private CriticalHitTriggeredAbility(final CriticalHitTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DIE_ROLLED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DieRolledEvent drEvent = (DieRolledEvent) event; + return isControlledBy(event.getPlayerId()) + && drEvent.getNaturalResult() == 20; + } + + @Override + public CriticalHitTriggeredAbility copy() { + return new CriticalHitTriggeredAbility(this); + } + + @Override + public String getRule() { + return "When you roll a natural 20, return {this} from your graveyard to your hand."; + } +} diff --git a/Mage.Sets/src/mage/cards/d/DanseMacabre.java b/Mage.Sets/src/mage/cards/d/DanseMacabre.java index bc06ebb0b0a..0730a11ad88 100644 --- a/Mage.Sets/src/mage/cards/d/DanseMacabre.java +++ b/Mage.Sets/src/mage/cards/d/DanseMacabre.java @@ -104,7 +104,7 @@ class DanseMacabreEffect extends OneShotEffect { cards.add(permanent); permanent.sacrifice(source, game); } - int result = controller.rollDice(source, game, 20) + toughness; + int result = controller.rollDice(outcome, source, game, 20) + toughness; cards.retainZone(Zone.GRAVEYARD, game); if (cards.isEmpty()) { return true; diff --git a/Mage.Sets/src/mage/cards/d/DelinaWildMage.java b/Mage.Sets/src/mage/cards/d/DelinaWildMage.java index 8925f6a0793..dfd92fdc4bf 100644 --- a/Mage.Sets/src/mage/cards/d/DelinaWildMage.java +++ b/Mage.Sets/src/mage/cards/d/DelinaWildMage.java @@ -86,7 +86,7 @@ class DelinaWildMageEffect extends OneShotEffect { )); effect.setTargetPointer(getTargetPointer()); while (true) { - int result = player.rollDice(source, game, 20); + int result = player.rollDice(outcome, source, game, 20); effect.apply(game, source); if (result < 15 || 20 < result || !player.chooseUse(outcome, "Roll again?", source, game)) { break; diff --git a/Mage.Sets/src/mage/cards/e/EbonyFly.java b/Mage.Sets/src/mage/cards/e/EbonyFly.java index 8949eb3277d..2453171c93d 100644 --- a/Mage.Sets/src/mage/cards/e/EbonyFly.java +++ b/Mage.Sets/src/mage/cards/e/EbonyFly.java @@ -91,7 +91,7 @@ class EbonyFlyEffect extends OneShotEffect { if (player == null) { return false; } - int result = player.rollDice(source, game, 6); + int result = player.rollDice(outcome, source, game, 6); Permanent permanent = source.getSourcePermanentIfItStillExists(game); if (permanent == null || !player.chooseUse( outcome, "Have " + permanent.getName() + " become a " diff --git a/Mage.Sets/src/mage/cards/e/ElvishImpersonators.java b/Mage.Sets/src/mage/cards/e/ElvishImpersonators.java index b3225c9aca0..71929b7d95d 100644 --- a/Mage.Sets/src/mage/cards/e/ElvishImpersonators.java +++ b/Mage.Sets/src/mage/cards/e/ElvishImpersonators.java @@ -1,7 +1,6 @@ package mage.cards.e; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AsEntersBattlefieldAbility; @@ -9,22 +8,20 @@ import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.continuous.SetPowerToughnessSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.SubLayer; +import mage.constants.*; import mage.game.Game; import mage.players.Player; +import java.util.List; +import java.util.UUID; + /** - * * @author L_J */ public final class ElvishImpersonators extends CardImpl { public ElvishImpersonators(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); this.subtype.add(SubType.ELVES); this.power = new MageInt(0); this.toughness = new MageInt(0); @@ -63,8 +60,9 @@ class ElvishImpersonatorsEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int firstRoll = controller.rollDice(source, game, 6); - int secondRoll = controller.rollDice(source, game, 6); + List results = controller.rollDice(outcome, source, game, 6, 2, 0); + int firstRoll = results.get(0); + int secondRoll = results.get(1); game.addEffect(new SetPowerToughnessSourceEffect(firstRoll, secondRoll, Duration.WhileOnBattlefield, SubLayer.SetPT_7b), source); return true; } diff --git a/Mage.Sets/src/mage/cards/f/FaridehDevilsChosen.java b/Mage.Sets/src/mage/cards/f/FaridehDevilsChosen.java new file mode 100644 index 00000000000..740c5d87da1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FaridehDevilsChosen.java @@ -0,0 +1,76 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.OneOrMoreDiceRolledTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.game.Game; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FaridehDevilsChosen extends CardImpl { + + public FaridehDevilsChosen(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.TIEFLING); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Dark One's Own Luck — Whenever you roll one or more dice, Farideh, Devil's Chosen gains flying and menace until end of turn. If any of those results was 10 or higher, draw a card. + Ability ability = new OneOrMoreDiceRolledTriggeredAbility( + new GainAbilitySourceEffect( + FlyingAbility.getInstance(), Duration.EndOfTurn + ).setText("{this} gains flying") + ); + ability.addEffect(new GainAbilitySourceEffect( + new MenaceAbility(), Duration.EndOfTurn + ).setText("and menace until end of turn")); + ability.addEffect(new ConditionalOneShotEffect( + new DrawCardSourceControllerEffect(1), FaridehDevilsChosenCondition.instance, + "If any of those results was 10 or higher, draw a card" + )); + this.addAbility(ability.withFlavorWord("Dark One's Own Luck")); + } + + private FaridehDevilsChosen(final FaridehDevilsChosen card) { + super(card); + } + + @Override + public FaridehDevilsChosen copy() { + return new FaridehDevilsChosen(this); + } +} + +enum FaridehDevilsChosenCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return source + .getEffects() + .stream() + .map(effect -> effect.getValue("maxDieRoll")) + .filter(Objects::nonNull) + .mapToInt(Integer.class::cast) + .anyMatch(x -> x >= 10); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FeywildTrickster.java b/Mage.Sets/src/mage/cards/f/FeywildTrickster.java new file mode 100644 index 00000000000..ea7bddb301b --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FeywildTrickster.java @@ -0,0 +1,39 @@ +package mage.cards.f; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.OneOrMoreDiceRolledTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.FaerieDragonToken; + +/** + * + * @author weirddan455 + */ +public final class FeywildTrickster extends CardImpl { + + public FeywildTrickster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}"); + + this.subtype.add(SubType.GNOME); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Whenever you roll one or more dice, create a 1/1 blue Faerie Dragon creature token with flying. + this.addAbility(new OneOrMoreDiceRolledTriggeredAbility(new CreateTokenEffect(new FaerieDragonToken()))); + } + + private FeywildTrickster(final FeywildTrickster card) { + super(card); + } + + @Override + public FeywildTrickster copy() { + return new FeywildTrickster(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FreeRangeChicken.java b/Mage.Sets/src/mage/cards/f/FreeRangeChicken.java index 26f44601950..1335a3090ab 100644 --- a/Mage.Sets/src/mage/cards/f/FreeRangeChicken.java +++ b/Mage.Sets/src/mage/cards/f/FreeRangeChicken.java @@ -16,6 +16,7 @@ import mage.players.Player; import mage.watchers.Watcher; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -64,8 +65,9 @@ class FreeRangeChickenEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int firstRoll = controller.rollDice(source, game, 6); - int secondRoll = controller.rollDice(source, game, 6); + List results = controller.rollDice(outcome, source, game, 6, 2, 0); + int firstRoll = results.get(0); + int secondRoll = results.get(1); if (firstRoll == secondRoll) { game.addEffect(new BoostSourceEffect(firstRoll, firstRoll, Duration.EndOfTurn), source); } diff --git a/Mage.Sets/src/mage/cards/g/GOTOJAIL.java b/Mage.Sets/src/mage/cards/g/GOTOJAIL.java index 1073bc7c626..8254ac8f733 100644 --- a/Mage.Sets/src/mage/cards/g/GOTOJAIL.java +++ b/Mage.Sets/src/mage/cards/g/GOTOJAIL.java @@ -1,7 +1,6 @@ package mage.cards.g; -import java.util.UUID; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; @@ -20,14 +19,15 @@ import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCreaturePermanent; import mage.util.CardUtil; +import java.util.List; +import java.util.UUID; + /** - * * @author spjspj */ public final class GOTOJAIL extends CardImpl { @@ -153,8 +153,9 @@ class GoToJailUpkeepEffect extends OneShotEffect { Player opponent = game.getPlayer(opponentId); if (opponent != null) { - int thisRoll = opponent.rollDice(source, game, 6); - int thatRoll = opponent.rollDice(source, game, 6); + List results = opponent.rollDice(outcome, source, game, 6, 2, 0); + int thisRoll = results.get(0); + int thatRoll = results.get(1); if (thisRoll == thatRoll) { return permanent.sacrifice(source, game); } diff --git a/Mage.Sets/src/mage/cards/g/GarbageElementalC.java b/Mage.Sets/src/mage/cards/g/GarbageElementalC.java index 7778157c27e..d33419c163b 100644 --- a/Mage.Sets/src/mage/cards/g/GarbageElementalC.java +++ b/Mage.Sets/src/mage/cards/g/GarbageElementalC.java @@ -1,7 +1,5 @@ - package mage.cards.g; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldAbility; @@ -17,8 +15,10 @@ import mage.game.permanent.token.GoblinToken; import mage.game.permanent.token.Token; import mage.players.Player; +import java.util.List; +import java.util.UUID; + /** - * * @author spjspj */ public final class GarbageElementalC extends CardImpl { @@ -71,8 +71,9 @@ class GarbageElementalCEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int thisRoll = controller.rollDice(source, game, 6); - int thatRoll = controller.rollDice(source, game, 6); + List results = controller.rollDice(outcome, source, game, 6, 2, 0); + int thisRoll = results.get(0); + int thatRoll = results.get(1); Token token = new GoblinToken(); return token.putOntoBattlefield(Math.abs(thatRoll - thisRoll), game, source, source.getControllerId()); diff --git a/Mage.Sets/src/mage/cards/g/GarbageElementalD.java b/Mage.Sets/src/mage/cards/g/GarbageElementalD.java index 0a48380c550..ce1d74b7171 100644 --- a/Mage.Sets/src/mage/cards/g/GarbageElementalD.java +++ b/Mage.Sets/src/mage/cards/g/GarbageElementalD.java @@ -72,7 +72,7 @@ class GarbageElementalDEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Player opponent = game.getPlayer(source.getFirstTarget()); if (controller != null && opponent != null) { - int damage = controller.rollDice(source, game, 6); + int damage = controller.rollDice(outcome, source, game, 6); return game.damagePlayerOrPlaneswalker(opponent.getId(), damage, source.getId(), source, game, false, true) > 0; } return false; diff --git a/Mage.Sets/src/mage/cards/g/GoblinBowlingTeam.java b/Mage.Sets/src/mage/cards/g/GoblinBowlingTeam.java index bd0a9c5041b..39b28de2b48 100644 --- a/Mage.Sets/src/mage/cards/g/GoblinBowlingTeam.java +++ b/Mage.Sets/src/mage/cards/g/GoblinBowlingTeam.java @@ -92,13 +92,13 @@ class GoblinBowlingTeamEffect extends ReplacementEffectImpl { if (damageEvent.getType() == GameEvent.EventType.DAMAGE_PLAYER) { Player targetPlayer = game.getPlayer(event.getTargetId()); if (targetPlayer != null) { - targetPlayer.damage(CardUtil.overflowInc(damageEvent.getAmount(), controller.rollDice(source, game, 6)), damageEvent.getSourceId(), source, game, damageEvent.isCombatDamage(), damageEvent.isPreventable(), event.getAppliedEffects()); + targetPlayer.damage(CardUtil.overflowInc(damageEvent.getAmount(), controller.rollDice(Outcome.Benefit, source, game, 6)), damageEvent.getSourceId(), source, game, damageEvent.isCombatDamage(), damageEvent.isPreventable(), event.getAppliedEffects()); return true; } } else { Permanent targetPermanent = game.getPermanent(event.getTargetId()); if (targetPermanent != null) { - targetPermanent.damage(CardUtil.overflowInc(damageEvent.getAmount(), controller.rollDice(source, game, 6)), damageEvent.getSourceId(), source, game, damageEvent.isCombatDamage(), damageEvent.isPreventable(), event.getAppliedEffects()); + targetPermanent.damage(CardUtil.overflowInc(damageEvent.getAmount(), controller.rollDice(Outcome.Benefit, source, game, 6)), damageEvent.getSourceId(), source, game, damageEvent.isCombatDamage(), damageEvent.isPreventable(), event.getAppliedEffects()); return true; } } diff --git a/Mage.Sets/src/mage/cards/g/GoblinTutor.java b/Mage.Sets/src/mage/cards/g/GoblinTutor.java index 8cb65ae0191..2754eeb0eab 100644 --- a/Mage.Sets/src/mage/cards/g/GoblinTutor.java +++ b/Mage.Sets/src/mage/cards/g/GoblinTutor.java @@ -66,7 +66,7 @@ class GoblinTutorEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); Effect effect = null; // 2 - A card named Goblin Tutor diff --git a/Mage.Sets/src/mage/cards/g/GroundPounder.java b/Mage.Sets/src/mage/cards/g/GroundPounder.java index 62a6edc664c..243e9d70134 100644 --- a/Mage.Sets/src/mage/cards/g/GroundPounder.java +++ b/Mage.Sets/src/mage/cards/g/GroundPounder.java @@ -1,7 +1,6 @@ package mage.cards.g; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; @@ -13,18 +12,16 @@ import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** - * * @author spjspj */ public final class GroundPounder extends CardImpl { @@ -38,7 +35,7 @@ public final class GroundPounder extends CardImpl { this.toughness = new MageInt(2); // 3G: Roll a six-sided die. Ground Pounder gets +X/+X until end of turn, where X is the result. - this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new GroundPounderEffect(), new ManaCostsImpl("{3}{G}"))); + this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new GroundPounderEffect(), new ManaCostsImpl<>("{3}{G}"))); // Whenever you roll a 5 or higher on a die, Ground Pounder gains trample until end of turn. this.addAbility(new GroundPounderTriggeredAbility()); @@ -75,7 +72,7 @@ class GroundPounderEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); game.addEffect(new BoostSourceEffect(amount, amount, Duration.EndOfTurn), source); return true; } @@ -100,17 +97,14 @@ class GroundPounderTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DICE_ROLLED; + return event.getType() == GameEvent.EventType.DIE_ROLLED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (this.isControlledBy(event.getPlayerId()) && event.getFlag()) { - if (event.getAmount() >= 5) { - return true; - } - } - return false; + DieRolledEvent drEvent = (DieRolledEvent) event; + // silver border card must look for "result" instead "natural result" + return this.isControlledBy(event.getPlayerId()) && drEvent.getResult() >= 5; } @Override diff --git a/Mage.Sets/src/mage/cards/g/GrowthSpurt.java b/Mage.Sets/src/mage/cards/g/GrowthSpurt.java index 9fd237648b3..ab05309bdac 100644 --- a/Mage.Sets/src/mage/cards/g/GrowthSpurt.java +++ b/Mage.Sets/src/mage/cards/g/GrowthSpurt.java @@ -56,7 +56,7 @@ class GrowthSpurtEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int result = controller.rollDice(source, game, 6); + int result = controller.rollDice(outcome, source, game, 6); Permanent permanent = game.getPermanent(source.getFirstTarget()); if (permanent != null) { ContinuousEffect effect = new BoostTargetEffect(result, result, Duration.EndOfTurn); diff --git a/Mage.Sets/src/mage/cards/h/HammerHelper.java b/Mage.Sets/src/mage/cards/h/HammerHelper.java index 8474bf228da..aed505c95c7 100644 --- a/Mage.Sets/src/mage/cards/h/HammerHelper.java +++ b/Mage.Sets/src/mage/cards/h/HammerHelper.java @@ -67,7 +67,7 @@ class HammerHelperEffect extends OneShotEffect { source.getEffects().get(0).setTargetPointer(new FixedTarget(targetCreature.getId())); game.addEffect(new GainControlTargetEffect(Duration.EndOfTurn), source); targetCreature.untap(game); - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); game.addEffect(new BoostTargetEffect(amount, 0, Duration.EndOfTurn), source); game.addEffect(new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.EndOfTurn), source); return true; diff --git a/Mage.Sets/src/mage/cards/h/HammerJammer.java b/Mage.Sets/src/mage/cards/h/HammerJammer.java index 24678de28ca..d5ec50aa8e0 100644 --- a/Mage.Sets/src/mage/cards/h/HammerJammer.java +++ b/Mage.Sets/src/mage/cards/h/HammerJammer.java @@ -1,14 +1,10 @@ package mage.cards.h; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; import mage.cards.CardImpl; @@ -20,12 +16,16 @@ import mage.constants.Zone; import mage.counters.Counter; import mage.counters.CounterType; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + /** - * * @author spjspj & L_J */ public final class HammerJammer extends CardImpl { @@ -39,7 +39,7 @@ public final class HammerJammer extends CardImpl { // As Hammer Jammer enters the battlefield, roll a six-sided die. Hammer Jammer enters the battlefield with a number of +1/+1 counters on it equal to the result. this.addAbility(new EntersBattlefieldAbility(new HammerJammerEntersEffect(CounterType.P1P1.createInstance()))); - + // Whenever you roll a die, remove all +1/+1 counters from Hammer Jammer, then put a number of +1/+1 counters on it equal to the result. this.addAbility(new HammerJammerTriggeredAbility()); @@ -70,7 +70,7 @@ class HammerJammerEntersEffect extends EntersBattlefieldWithXCountersEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanentEntering(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); List appliedEffects = (ArrayList) this.getValue("appliedEffects"); // the basic event is the EntersBattlefieldEvent, so use already applied replacement effects from that event permanent.addCounters(CounterType.P1P1.createInstance(amount), source.getControllerId(), source, game, appliedEffects); return super.apply(game, source); @@ -79,7 +79,7 @@ class HammerJammerEntersEffect extends EntersBattlefieldWithXCountersEffect { } @Override - public EntersBattlefieldWithXCountersEffect copy() { + public HammerJammerEntersEffect copy() { return new HammerJammerEntersEffect(this); } } @@ -101,15 +101,16 @@ class HammerJammerTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DICE_ROLLED; + return event.getType() == GameEvent.EventType.DIE_ROLLED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (this.getControllerId().equals(event.getPlayerId()) && event.getFlag()) { - for (Effect effect : this.getEffects()) { - effect.setValue("rolled", event.getAmount()); - } + DieRolledEvent drEvent = (DieRolledEvent) event; + // silver border card must look for "result" instead "natural result" + // planar die will trigger it with 0 amount + if (this.isControlledBy(drEvent.getPlayerId())) { + this.getEffects().setValue("rolled", drEvent.getResult()); return true; } return false; @@ -142,10 +143,12 @@ class HammerJammerEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); if (controller != null && permanent != null) { - if (getValue("rolled") != null) { - int amount = (Integer) getValue("rolled"); + Integer amount = (Integer) getValue("rolled"); + if (amount != null) { permanent.removeCounters(CounterType.P1P1.createInstance(permanent.getCounters(game).getCount(CounterType.P1P1)), source, game); - permanent.addCounters(CounterType.P1P1.createInstance(amount), source.getControllerId(), source, game); + if (amount > 0) { + permanent.addCounters(CounterType.P1P1.createInstance(amount), source.getControllerId(), source, game); + } return true; } } diff --git a/Mage.Sets/src/mage/cards/h/Hydradoodle.java b/Mage.Sets/src/mage/cards/h/Hydradoodle.java index d4e83303f03..3e14e87bfaa 100644 --- a/Mage.Sets/src/mage/cards/h/Hydradoodle.java +++ b/Mage.Sets/src/mage/cards/h/Hydradoodle.java @@ -91,12 +91,7 @@ class HydradoodleEffect extends OneShotEffect { && permanent.getZoneChangeCounter(game) == spellAbility.getSourceObjectZoneChangeCounter()) { int amount = spellAbility.getManaCostsToPay().getX(); if (amount > 0) { - int total = 0; - for (int roll = 0; roll < amount; roll++) { - int thisRoll = controller.rollDice(source, game, 6); - total += thisRoll; - } - + int total = controller.rollDice(outcome, source, game, 6, amount, 0).stream().mapToInt(x -> x).sum(); permanent.addCounters(CounterType.P1P1.createInstance(total), source.getControllerId(), source, game); } } diff --git a/Mage.Sets/src/mage/cards/i/Inhumaniac.java b/Mage.Sets/src/mage/cards/i/Inhumaniac.java index 52988213440..d8038a864c5 100644 --- a/Mage.Sets/src/mage/cards/i/Inhumaniac.java +++ b/Mage.Sets/src/mage/cards/i/Inhumaniac.java @@ -64,7 +64,7 @@ class InhumaniacEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); if (amount >= 3 && amount <= 4) { permanent.addCounters(CounterType.P1P1.createInstance(1), source.getControllerId(), source, game); } else if (amount >= 5) { diff --git a/Mage.Sets/src/mage/cards/j/JackInTheMox.java b/Mage.Sets/src/mage/cards/j/JackInTheMox.java index 89d239c2828..2780d404507 100644 --- a/Mage.Sets/src/mage/cards/j/JackInTheMox.java +++ b/Mage.Sets/src/mage/cards/j/JackInTheMox.java @@ -77,7 +77,7 @@ class JackInTheMoxManaEffect extends ManaEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); switch (amount) { case 1: permanent.sacrifice(source, game); diff --git a/Mage.Sets/src/mage/cards/j/JumboImp.java b/Mage.Sets/src/mage/cards/j/JumboImp.java index 82dd9576bcd..2cf061d908d 100644 --- a/Mage.Sets/src/mage/cards/j/JumboImp.java +++ b/Mage.Sets/src/mage/cards/j/JumboImp.java @@ -78,7 +78,7 @@ class JumboImpEffect extends EntersBattlefieldWithXCountersEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanentEntering(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); List appliedEffects = (ArrayList) this.getValue("appliedEffects"); // the basic event is the EntersBattlefieldEvent, so use already applied replacement effects from that event permanent.addCounters(CounterType.P1P1.createInstance(amount), source.getControllerId(), source, game, appliedEffects); return super.apply(game, source); @@ -114,7 +114,7 @@ class JumboImpAddCountersEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); permanent.addCounters(CounterType.P1P1.createInstance(amount), source.getControllerId(), source, game); return true; } @@ -143,7 +143,7 @@ class JumboImpRemoveCountersEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); permanent.removeCounters(CounterType.P1P1.createInstance(amount), source, game); return true; } diff --git a/Mage.Sets/src/mage/cards/k/KrarksOtherThumb.java b/Mage.Sets/src/mage/cards/k/KrarksOtherThumb.java index 1be63ea8baf..c2b19794405 100644 --- a/Mage.Sets/src/mage/cards/k/KrarksOtherThumb.java +++ b/Mage.Sets/src/mage/cards/k/KrarksOtherThumb.java @@ -1,7 +1,5 @@ - package mage.cards.k; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ReplacementEffectImpl; @@ -11,14 +9,13 @@ import mage.constants.CardType; import mage.constants.Duration; import mage.constants.Outcome; import mage.constants.SuperType; -import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; -import mage.players.Player; -import mage.util.RandomUtil; +import mage.game.events.RollDieEvent; + +import java.util.UUID; /** - * * @author spjspj */ public final class KrarksOtherThumb extends CardImpl { @@ -29,7 +26,7 @@ public final class KrarksOtherThumb extends CardImpl { addSuperType(SuperType.LEGENDARY); // If you would roll a die, instead roll two of those dice and ignore one of those results. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new KrarksOtherThumbEffect())); + this.addAbility(new SimpleStaticAbility(new KrarksOtherThumbEffect())); } private KrarksOtherThumb(final KrarksOtherThumb card) { @@ -46,7 +43,7 @@ class KrarksOtherThumbEffect extends ReplacementEffectImpl { KrarksOtherThumbEffect() { super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "If you would roll a die, instead roll two die and ignore one"; + staticText = "if you would roll a die, instead roll two of those dice and ignore one of those results"; } KrarksOtherThumbEffect(final KrarksOtherThumbEffect effect) { @@ -55,29 +52,15 @@ class KrarksOtherThumbEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Player player = game.getPlayer(event.getPlayerId()); - if (player != null) { - // event.getData holds the num of sides of the die to roll - String data = event.getData(); - int numSides = Integer.parseInt(data); - int secondDieRoll = RandomUtil.nextInt(numSides) + 1; - - if (!game.isSimulation()) { - game.informPlayers("[Roll a die] " + player.getLogName() + " rolled a " + secondDieRoll); - } - if (player.chooseUse(outcome, "Ignore the first die roll?", source, game)) { - event.setAmount(secondDieRoll); - game.informPlayers(player.getLogName() + " ignores the first die roll."); - } else { - game.informPlayers(player.getLogName() + " ignores the second die roll."); - } - } + // support any roll type + RollDieEvent rollDieEvent = (RollDieEvent) event; + rollDieEvent.doubleRollsAmount(); return false; } @Override public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ROLL_DICE; + return event.getType() == GameEvent.EventType.ROLL_DIE; } @Override diff --git a/Mage.Sets/src/mage/cards/k/KrazyKow.java b/Mage.Sets/src/mage/cards/k/KrazyKow.java index cb99f19991d..1d876882ecd 100644 --- a/Mage.Sets/src/mage/cards/k/KrazyKow.java +++ b/Mage.Sets/src/mage/cards/k/KrazyKow.java @@ -60,7 +60,7 @@ class KrazyKowEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int result = controller.rollDice(source, game, 6); + int result = controller.rollDice(outcome, source, game, 6); if (result == 1) { new SacrificeSourceEffect().apply(game, source); return new DamageEverythingEffect(3).apply(game, source); diff --git a/Mage.Sets/src/mage/cards/l/LobeLobber.java b/Mage.Sets/src/mage/cards/l/LobeLobber.java index 85947b0ecef..2812b6dfed4 100644 --- a/Mage.Sets/src/mage/cards/l/LobeLobber.java +++ b/Mage.Sets/src/mage/cards/l/LobeLobber.java @@ -73,7 +73,7 @@ class LobeLobberEffect extends OneShotEffect { if (controller != null && equipment != null && player != null) { player.damage(1, source.getSourceId(), source, game); - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); if (amount >= 5) { new UntapSourceEffect().apply(game, source); } diff --git a/Mage.Sets/src/mage/cards/m/MadScienceFairProject.java b/Mage.Sets/src/mage/cards/m/MadScienceFairProject.java index f3b11437b05..11f5102a96c 100644 --- a/Mage.Sets/src/mage/cards/m/MadScienceFairProject.java +++ b/Mage.Sets/src/mage/cards/m/MadScienceFairProject.java @@ -2,19 +2,18 @@ package mage.cards.m; import mage.Mana; import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.effects.mana.ManaEffect; -import mage.abilities.mana.SimpleManaAbility; +import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.choices.ManaChoice; import mage.constants.CardType; -import mage.constants.Zone; +import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; +import mage.target.TargetPlayer; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; /** @@ -26,7 +25,9 @@ public final class MadScienceFairProject extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); // {T}: Roll a six-sided die. On a 3 or lower, target player adds {C}. Otherwise, that player adds one mana of any color they choose. - this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD, new MadScienceFairManaEffect(), new TapSourceCost())); + Ability ability = new SimpleActivatedAbility(new MadScienceFairProjectEffect(), new TapSourceCost()); + ability.addTarget(new TargetPlayer()); + this.addAbility(ability); } private MadScienceFairProject(final MadScienceFairProject card) { @@ -39,40 +40,33 @@ public final class MadScienceFairProject extends CardImpl { } } -class MadScienceFairManaEffect extends ManaEffect { +class MadScienceFairProjectEffect extends OneShotEffect { - public MadScienceFairManaEffect() { - super(); - this.staticText = "Roll a six-sided die. On a 3 or lower, target player adds {C}. Otherwise, that player adds one mana of any color they choose"; + MadScienceFairProjectEffect() { + super(Outcome.Benefit); + staticText = "Roll a six-sided die. On a 3 or lower, target player adds {C}. " + + "Otherwise, that player adds one mana of any color they choose"; } - public MadScienceFairManaEffect(final MadScienceFairManaEffect effect) { + private MadScienceFairProjectEffect(final MadScienceFairProjectEffect effect) { super(effect); } @Override - public MadScienceFairManaEffect copy() { - return new MadScienceFairManaEffect(this); + public MadScienceFairProjectEffect copy() { + return new MadScienceFairProjectEffect(this); } @Override - public List getNetMana(Game game, Ability source) { - return new ArrayList<>(); - } - - @Override - public Mana produceMana(Game game, Ability source) { - if (game != null) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - int amount = controller.rollDice(source, game, 6); - if (amount <= 3) { - return Mana.ColorlessMana(1); - } else { - return ManaChoice.chooseAnyColor(controller, game, 1); - } - } + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player player = game.getPlayer(source.getFirstTarget()); + if (controller == null || player == null) { + return false; } - return new Mana(); + int amount = controller.rollDice(outcome, source, game, 6); + Mana mana = amount <= 3 ? Mana.ColorlessMana(1) : ManaChoice.chooseAnyColor(player, game, 1); + player.getManaPool().addMana(mana, game, source); + return true; } } diff --git a/Mage.Sets/src/mage/cards/m/MaddeningHex.java b/Mage.Sets/src/mage/cards/m/MaddeningHex.java index 90ec227dd91..fcb9610174a 100644 --- a/Mage.Sets/src/mage/cards/m/MaddeningHex.java +++ b/Mage.Sets/src/mage/cards/m/MaddeningHex.java @@ -118,7 +118,7 @@ class MaddeningHexEffect extends OneShotEffect { if (controller == null) { return false; } - int result = controller.rollDice(source, game, 6); + int result = controller.rollDice(outcome, source, game, 6); Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); if (player != null) { player.damage(result, source.getSourceId(), source, game); diff --git a/Mage.Sets/src/mage/cards/n/NetheresePuzzleWard.java b/Mage.Sets/src/mage/cards/n/NetheresePuzzleWard.java new file mode 100644 index 00000000000..92f35f4be5d --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NetheresePuzzleWard.java @@ -0,0 +1,108 @@ +package mage.cards.n; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.DieRolledEvent; +import mage.game.events.GameEvent; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NetheresePuzzleWard extends CardImpl { + + public NetheresePuzzleWard(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{U}"); + + // Focus Beam — At the beginning of your upkeep, roll a d4. Scry X, where X is the result. + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new NetheresePuzzleWardEffect(), TargetController.YOU, false + ).withFlavorWord("Focus Beam")); + + // Perfect Illumination — Whenever you roll a die's highest natural result, draw a card. + this.addAbility(new NetheresePuzzleWardTriggeredAbility()); + } + + private NetheresePuzzleWard(final NetheresePuzzleWard card) { + super(card); + } + + @Override + public NetheresePuzzleWard copy() { + return new NetheresePuzzleWard(this); + } +} + +class NetheresePuzzleWardEffect extends OneShotEffect { + + NetheresePuzzleWardEffect() { + super(Outcome.Benefit); + staticText = "roll a d4. Scry X, where X is the result"; + } + + private NetheresePuzzleWardEffect(final NetheresePuzzleWardEffect effect) { + super(effect); + } + + @Override + public NetheresePuzzleWardEffect copy() { + return new NetheresePuzzleWardEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + return player.scry(player.rollDice(outcome, source, game, 4), source, game); + } +} + +class NetheresePuzzleWardTriggeredAbility extends TriggeredAbilityImpl { + + NetheresePuzzleWardTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1)); + this.withFlavorWord("Perfect Illumination"); + } + + private NetheresePuzzleWardTriggeredAbility(final NetheresePuzzleWardTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DIE_ROLLED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DieRolledEvent drEvent = (DieRolledEvent) event; + return isControlledBy(drEvent.getPlayerId()) + && drEvent.getNaturalResult() == drEvent.getSides(); + } + + @Override + public NetheresePuzzleWardTriggeredAbility copy() { + return new NetheresePuzzleWardTriggeredAbility(this); + } + + @Override + public String getRule() { + return CardUtil.italicizeWithEmDash(flavorWord) + + "Whenever you roll a die's highest natural result, draw a card."; + } +} diff --git a/Mage.Sets/src/mage/cards/n/NeverwinterHydra.java b/Mage.Sets/src/mage/cards/n/NeverwinterHydra.java new file mode 100644 index 00000000000..3cbbd56d58b --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NeverwinterHydra.java @@ -0,0 +1,99 @@ +package mage.cards.n; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.EntersBattlefieldEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NeverwinterHydra extends CardImpl { + + public NeverwinterHydra(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{X}{X}{G}{G}"); + + this.subtype.add(SubType.HYDRA); + this.power = new MageInt(0); + this.toughness = new MageInt(0); + + // As Neverwinter Hydra enters the battlefield, roll X d6. It enters with a number of +1/+1 counters on it equal to the total of those results. + this.addAbility(new AsEntersBattlefieldAbility(new NeverwinterHydraEffect())); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Ward {4} + this.addAbility(new WardAbility(new ManaCostsImpl<>("{4}"))); + } + + private NeverwinterHydra(final NeverwinterHydra card) { + super(card); + } + + @Override + public NeverwinterHydra copy() { + return new NeverwinterHydra(this); + } +} + +class NeverwinterHydraEffect extends OneShotEffect { + + NeverwinterHydraEffect() { + super(Outcome.Benefit); + staticText = "roll X d6. It enters with a number of +1/+1 counters on it equal to the total of those results"; + } + + private NeverwinterHydraEffect(final NeverwinterHydraEffect effect) { + super(effect); + } + + @Override + public NeverwinterHydraEffect copy() { + return new NeverwinterHydraEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanentEntering(source.getSourceId()); + Player player = game.getPlayer(source.getControllerId()); + if (permanent == null || player == null) { + return true; + } + SpellAbility spellAbility = (SpellAbility) getValue(EntersBattlefieldEffect.SOURCE_CAST_SPELL_ABILITY); + if (spellAbility == null + || !spellAbility.getSourceId().equals(source.getSourceId()) + || permanent.getZoneChangeCounter(game) != spellAbility.getSourceObjectZoneChangeCounter()) { + return true; + } + if (!spellAbility.getSourceId().equals(source.getSourceId())) { + return true; + } // put into play by normal cast + int xValue = spellAbility.getManaCostsToPay().getX(); + if (xValue < 1) { + return false; + } + int amount = player.rollDice(outcome, source, game, 6, xValue, 0).stream().mapToInt(x -> x).sum(); + List appliedEffects = (ArrayList) this.getValue("appliedEffects"); + permanent.addCounters(CounterType.P1P1.createInstance(amount), source.getControllerId(), source, game, appliedEffects); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/p/Painiac.java b/Mage.Sets/src/mage/cards/p/Painiac.java index f755a08cfce..9aa82a2d9ba 100644 --- a/Mage.Sets/src/mage/cards/p/Painiac.java +++ b/Mage.Sets/src/mage/cards/p/Painiac.java @@ -66,7 +66,7 @@ class PainiacEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); game.addEffect(new BoostSourceEffect(amount, 0, Duration.EndOfTurn), source); return true; } diff --git a/Mage.Sets/src/mage/cards/p/PixieGuide.java b/Mage.Sets/src/mage/cards/p/PixieGuide.java new file mode 100644 index 00000000000..64f4e531f84 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PixieGuide.java @@ -0,0 +1,85 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.RollDiceEvent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class PixieGuide extends CardImpl { + + public PixieGuide(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.FAERIE); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Grant an Advantage — if you would roll one or more dice, instead roll that many dice plus one and ignore the lowest roll. + this.addAbility(new SimpleStaticAbility(new PixieGuideEffect()).withFlavorWord("Grant an Advantage")); + } + + private PixieGuide(final PixieGuide card) { + super(card); + } + + @Override + public PixieGuide copy() { + return new PixieGuide(this); + } +} + +class PixieGuideEffect extends ReplacementEffectImpl { + + PixieGuideEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "if you would roll one or more dice, instead roll that many dice plus one and ignore the lowest roll"; + } + + private PixieGuideEffect(final PixieGuideEffect effect) { + super(effect); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + RollDiceEvent rdEvent = (RollDiceEvent) event; + rdEvent.incAmount(1); + rdEvent.incIgnoreLowestAmount(1); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ROLL_DICE + && ((RollDiceEvent) event).getRollDieType() == RollDieType.NUMERICAL; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return source.isControlledBy(event.getPlayerId()); + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public PixieGuideEffect copy() { + return new PixieGuideEffect(this); + } +} diff --git a/Mage.Sets/src/mage/cards/p/Poultrygeist.java b/Mage.Sets/src/mage/cards/p/Poultrygeist.java index 177799e4903..96aa6570a08 100644 --- a/Mage.Sets/src/mage/cards/p/Poultrygeist.java +++ b/Mage.Sets/src/mage/cards/p/Poultrygeist.java @@ -64,7 +64,7 @@ class PoultrygeistEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int result = controller.rollDice(source, game, 6); + int result = controller.rollDice(outcome, source, game, 6); Permanent permanent = game.getPermanent(source.getSourceId()); if (permanent != null) { if (result == 1) { diff --git a/Mage.Sets/src/mage/cards/r/RecklessEndeavor.java b/Mage.Sets/src/mage/cards/r/RecklessEndeavor.java new file mode 100644 index 00000000000..369b6355f67 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RecklessEndeavor.java @@ -0,0 +1,86 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.TreasureToken; +import mage.players.Player; + +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RecklessEndeavor extends CardImpl { + + public RecklessEndeavor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{5}{R}{R}"); + + // Roll two d12 and choose one result. Reckless Endeavor deals damage equal to that result to each creature. Then create a number of Treasure tokens equal to the other result. + this.getSpellAbility().addEffect(new RecklessEndeavorEffect()); + } + + private RecklessEndeavor(final RecklessEndeavor card) { + super(card); + } + + @Override + public RecklessEndeavor copy() { + return new RecklessEndeavor(this); + } +} + +class RecklessEndeavorEffect extends OneShotEffect { + + RecklessEndeavorEffect() { + super(Outcome.Benefit); + staticText = "roll two d12 and choose one result. {this} deals damage equal to that result " + + "to each creature. Then create a number of Treasure tokens equal to the other result"; + } + + private RecklessEndeavorEffect(final RecklessEndeavorEffect effect) { + super(effect); + } + + @Override + public RecklessEndeavorEffect copy() { + return new RecklessEndeavorEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + List results = player.rollDice(outcome, source, game, 12, 2, 0); + int firstResult = results.get(0); + int secondResult = results.get(1); + int first, second; + if (firstResult != secondResult && player.chooseUse( + outcome, "Choose a number to deal damage to each creature", + "The other number will be the amount of treasures you create", + "" + firstResult, "" + secondResult, source, game + )) { + first = firstResult; + second = secondResult; + } else { + first = secondResult; + second = firstResult; + } + for (Permanent permanent : game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), game + )) { + permanent.damage(first, source.getSourceId(), source, game); + } + new TreasureToken().putOntoBattlefield(second, game, source, source.getControllerId()); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/r/Ricochet.java b/Mage.Sets/src/mage/cards/r/Ricochet.java index d34043ae288..365d5b78a39 100644 --- a/Mage.Sets/src/mage/cards/r/Ricochet.java +++ b/Mage.Sets/src/mage/cards/r/Ricochet.java @@ -116,7 +116,7 @@ class RicochetEffect extends OneShotEffect { } do { for (Player player : playerRolls.keySet()) { - playerRolls.put(player, player.rollDice(source, game, 6)); + playerRolls.put(player, player.rollDice(Outcome.Detriment, source, game, 6)); // bad outcome - ai must choose lowest value } int minValueInMap = Collections.min(playerRolls.values()); for (Map.Entry mapEntry : new HashSet<>(playerRolls.entrySet())) { diff --git a/Mage.Sets/src/mage/cards/s/SnickeringSquirrel.java b/Mage.Sets/src/mage/cards/s/SnickeringSquirrel.java index bf78c2b8df1..e636e34fc59 100644 --- a/Mage.Sets/src/mage/cards/s/SnickeringSquirrel.java +++ b/Mage.Sets/src/mage/cards/s/SnickeringSquirrel.java @@ -1,6 +1,5 @@ package mage.cards.s; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; @@ -10,11 +9,13 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.events.RollDieEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** - * * @author spjspj */ public final class SnickeringSquirrel extends CardImpl { @@ -55,26 +56,27 @@ class SnickeringSquirrelEffect extends ReplacementEffectImpl { public boolean replaceEvent(GameEvent event, Ability source, Game game) { Player controller = game.getPlayer(source.getControllerId()); Player dieRoller = game.getPlayer(event.getPlayerId()); - if (controller != null && dieRoller != null) { - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent != null && !permanent.isTapped()) { - if (controller.chooseUse(Outcome.AIDontUseIt, "Do you want to tap this to increase the result of a die (" - + event.getAmount() + ") " - + dieRoller.getName() + " rolled by 1?", null, "Yes", "No", source, game)) { - permanent.tap(source, game); - // ignore planar dies (dice roll amount of planar dies is equal to 0) - if (event.getAmount() > 0) { - event.setAmount(event.getAmount() + 1); - } - } - } + if (controller == null || dieRoller == null) { + return false; + } + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent == null || permanent.isTapped()) { + return false; + } + // TODO: allow AI for itself + // TODO: remove tap check on applies (no useless replace events)? + if (controller.chooseUse(Outcome.AIDontUseIt, "Tap this to increase the result of a die (" + + event.getAmount() + ") " + dieRoller.getName() + " rolled by 1?", source, game)) { + permanent.tap(source, game); + ((RollDieEvent) event).incResultModifier(1); } return false; } @Override public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ROLL_DICE; + return event.getType() == GameEvent.EventType.ROLL_DIE + && ((RollDieEvent) event).getRollDieType() == RollDieType.NUMERICAL; } @Override diff --git a/Mage.Sets/src/mage/cards/s/SongOfInspiration.java b/Mage.Sets/src/mage/cards/s/SongOfInspiration.java index 9ae653357bc..4c984a2679c 100644 --- a/Mage.Sets/src/mage/cards/s/SongOfInspiration.java +++ b/Mage.Sets/src/mage/cards/s/SongOfInspiration.java @@ -70,7 +70,7 @@ class SongOfInspirationEffect extends OneShotEffect { } Cards cards = new CardsImpl(getTargetPointer().getTargets(game, source)); int totalMv = cards.getCards(game).stream().mapToInt(MageObject::getManaValue).sum(); - int result = player.rollDice(source, game, 20); + int result = player.rollDice(outcome, source, game, 20); player.moveCards(cards, Zone.HAND, source, game); if (result + totalMv >= 15) { player.gainLife(totalMv, game, source); diff --git a/Mage.Sets/src/mage/cards/s/SparkFiend.java b/Mage.Sets/src/mage/cards/s/SparkFiend.java index c4bf555b697..800544b13d7 100644 --- a/Mage.Sets/src/mage/cards/s/SparkFiend.java +++ b/Mage.Sets/src/mage/cards/s/SparkFiend.java @@ -1,7 +1,5 @@ - package mage.cards.s; -import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; @@ -18,14 +16,16 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; + +import java.util.UUID; + /** - * * @author L_J */ public final class SparkFiend extends CardImpl { public SparkFiend(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}"); this.subtype.add(SubType.BEAST); this.power = new MageInt(5); this.toughness = new MageInt(6); @@ -67,7 +67,7 @@ class SparkFiendEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int roll = controller.rollDice(source, game, 6) + controller.rollDice(source, game, 6); + int roll = controller.rollDice(outcome, source, game, 6, 2, 0).stream().mapToInt(x -> x).sum(); MageObject mageObject = game.getObject(source.getSourceId()); if (mageObject instanceof Permanent) { Permanent sourcePermanent = (Permanent) mageObject; @@ -112,7 +112,7 @@ class SparkFiendUpkeepEffect extends OneShotEffect { if (controller != null) { if (game.getState().getValue("SparkFiend" + source.getSourceId().toString()) != null && (Integer) game.getState().getValue("SparkFiend" + source.getSourceId().toString()) != 0) { - int roll = controller.rollDice(source, game, 6) + controller.rollDice(source, game, 6); + int roll = controller.rollDice(outcome, source, game, 6, 2, 0).stream().mapToInt(x -> x).sum(); MageObject mageObject = game.getObject(source.getSourceId()); if (mageObject instanceof Permanent) { Permanent sourcePermanent = (Permanent) mageObject; diff --git a/Mage.Sets/src/mage/cards/s/SquirrelPoweredScheme.java b/Mage.Sets/src/mage/cards/s/SquirrelPoweredScheme.java index 664892c996e..c1b1b1d5641 100644 --- a/Mage.Sets/src/mage/cards/s/SquirrelPoweredScheme.java +++ b/Mage.Sets/src/mage/cards/s/SquirrelPoweredScheme.java @@ -1,21 +1,18 @@ - package mage.cards.s; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.events.RollDieEvent; + +import java.util.UUID; /** - * * @author spjspj */ public final class SquirrelPoweredScheme extends CardImpl { @@ -50,19 +47,19 @@ class SquirrelPoweredSchemeEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - event.setAmount(event.getAmount() + 2); + ((RollDieEvent) event).incResultModifier(2); return false; } @Override public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ROLL_DICE; + return event.getType() == GameEvent.EventType.ROLL_DIE + && ((RollDieEvent) event).getRollDieType() == RollDieType.NUMERICAL; } @Override public boolean applies(GameEvent event, Ability source, Game game) { - // ignore planar dies (dice roll amount of planar dies is equal to 0) - return event.getAmount() > 0 && source.isControlledBy(event.getPlayerId()); + return source.isControlledBy(event.getPlayerId()); } @Override diff --git a/Mage.Sets/src/mage/cards/s/SteelSquirrel.java b/Mage.Sets/src/mage/cards/s/SteelSquirrel.java index afbc2285aec..3a033058dd0 100644 --- a/Mage.Sets/src/mage/cards/s/SteelSquirrel.java +++ b/Mage.Sets/src/mage/cards/s/SteelSquirrel.java @@ -1,30 +1,26 @@ package mage.cards.s; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.RollDiceEffect; import mage.abilities.effects.common.continuous.BoostSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.constants.*; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** - * * @author spjspj */ public final class SteelSquirrel extends CardImpl { @@ -71,18 +67,16 @@ class SteelSquirrelTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DICE_ROLLED; + return event.getType() == GameEvent.EventType.DIE_ROLLED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (this.isControlledBy(event.getPlayerId()) && event.getFlag()) { - if (event.getAmount() >= 5) { - for (Effect effect : this.getEffects()) { - effect.setValue("rolled", event.getAmount()); - } - return true; - } + DieRolledEvent drEvent = (DieRolledEvent) event; + // silver border card must look for "result" instead "natural result" + if (this.isControlledBy(event.getPlayerId()) && drEvent.getResult() >= 5) { + this.getEffects().setValue("rolled", drEvent.getResult()); + return true; } return false; } @@ -113,12 +107,10 @@ class SteelSquirrelEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); Permanent permanent = game.getPermanent(source.getSourceId()); - if (controller != null && permanent != null) { - if (this.getValue("rolled") != null) { - int rolled = (Integer) this.getValue("rolled"); - game.addEffect(new BoostSourceEffect(rolled, rolled, Duration.EndOfTurn), source); - return true; - } + Integer amount = (Integer) this.getValue("rolled"); + if (controller != null && permanent != null && amount != null) { + game.addEffect(new BoostSourceEffect(amount, amount, Duration.EndOfTurn), source); + return true; } return false; } diff --git a/Mage.Sets/src/mage/cards/s/StrategySchmategy.java b/Mage.Sets/src/mage/cards/s/StrategySchmategy.java index d492729797d..25e9156cd87 100644 --- a/Mage.Sets/src/mage/cards/s/StrategySchmategy.java +++ b/Mage.Sets/src/mage/cards/s/StrategySchmategy.java @@ -70,7 +70,7 @@ class StrategySchmategyffect extends OneShotEffect { // 5 - Each player discards their hand and draws seven cards. // 6 - Repeat this process two more times while (numTimesToDo > 0) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(Outcome.Detriment, source, game, 6); // ai must try to choose min numTimesToDo--; if (amount == 2) { List artifactPermanents = game.getBattlefield().getActivePermanents(new FilterArtifactPermanent(), controller.getId(), game); diff --git a/Mage.Sets/src/mage/cards/s/SwordOfDungeonsAndDragons.java b/Mage.Sets/src/mage/cards/s/SwordOfDungeonsAndDragons.java index d728d071d3e..be9acd5555b 100644 --- a/Mage.Sets/src/mage/cards/s/SwordOfDungeonsAndDragons.java +++ b/Mage.Sets/src/mage/cards/s/SwordOfDungeonsAndDragons.java @@ -132,11 +132,11 @@ class SwordOfDungeonsAndDragonsEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { int count = 1; - int amount = controller.rollDice(source, game, 20); + int amount = controller.rollDice(outcome, source, game, 20); while (amount == 20) { count += 1; - amount = controller.rollDice(source, game, 20); + amount = controller.rollDice(outcome, source, game, 20); } return new CreateTokenEffect(new DragonTokenGold(), count).apply(game, source); } diff --git a/Mage.Sets/src/mage/cards/s/SwordOfHours.java b/Mage.Sets/src/mage/cards/s/SwordOfHours.java index df8efa9604d..992da06599f 100644 --- a/Mage.Sets/src/mage/cards/s/SwordOfHours.java +++ b/Mage.Sets/src/mage/cards/s/SwordOfHours.java @@ -73,7 +73,7 @@ class SwordOfHoursEffect extends OneShotEffect { if (player == null) { return false; } - int result = player.rollDice(source, game, 12); + int result = player.rollDice(outcome, source, game, 12); int damage = (Integer) getValue("damage"); if (result != 12 && damage <= result) { return true; diff --git a/Mage.Sets/src/mage/cards/t/TempOfTheDamned.java b/Mage.Sets/src/mage/cards/t/TempOfTheDamned.java index 2eedfae14c7..642fd50c0a0 100644 --- a/Mage.Sets/src/mage/cards/t/TempOfTheDamned.java +++ b/Mage.Sets/src/mage/cards/t/TempOfTheDamned.java @@ -68,7 +68,7 @@ class TempOfTheDamnedEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - return new AddCountersSourceEffect(CounterType.FUNK.createInstance(controller.rollDice(source, game, 6))).apply(game, source); + return new AddCountersSourceEffect(CounterType.FUNK.createInstance(controller.rollDice(Outcome.Benefit, source, game, 6))).apply(game, source); } return false; } diff --git a/Mage.Sets/src/mage/cards/t/TheBigIdea.java b/Mage.Sets/src/mage/cards/t/TheBigIdea.java index 8efb1900d88..06678e915bb 100644 --- a/Mage.Sets/src/mage/cards/t/TheBigIdea.java +++ b/Mage.Sets/src/mage/cards/t/TheBigIdea.java @@ -8,7 +8,6 @@ import mage.abilities.costs.common.TapTargetCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.ReplacementEffectImpl; -import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -16,7 +15,7 @@ import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.predicate.permanent.TappedPredicate; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; +import mage.game.events.RollDieEvent; import mage.game.permanent.token.BrainiacToken; import mage.players.Player; import mage.target.common.TargetControlledCreaturePermanent; @@ -43,7 +42,7 @@ public final class TheBigIdea extends CardImpl { this.power = new MageInt(4); this.toughness = new MageInt(4); - // 2{BR}{BR}, T: Roll a six-sided dice. Create a number of 1/1 red Brainiac creature tokens equal to the result. + // {2}{B/R}{B/R}, {T}: Roll a six-sided dice. Create a number of 1/1 red Brainiac creature tokens equal to the result. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new TheBigIdeaEffect(), new ManaCostsImpl("{2}{B/R}{B/R}")); ability.addCost(new TapSourceCost()); this.addAbility(ability); @@ -85,35 +84,20 @@ class TheBigIdeaReplacementEffect extends ReplacementEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Player controller = game.getPlayer(source.getControllerId()); - if (event.getData() != null) { - String data = event.getData(); - int numSides = Integer.parseInt(data); - if (numSides != 6) { - return false; - } - } - - if (controller != null) { - discard(); - int amount = controller.rollDice(source, game, 6); - event.setAmount(event.getAmount() + amount); - return true; - } - return false; + ((RollDieEvent) event).incBigIdeaRollsAmount(); + discard(); + return true; } @Override public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ROLL_DICE; + return event.getType() == GameEvent.EventType.ROLL_DIE + && ((RollDieEvent) event).getRollDieType() == RollDieType.NUMERICAL; } @Override public boolean applies(GameEvent event, Ability source, Game game) { - if (!this.used) { - return source.isControlledBy(event.getPlayerId()); - } - return false; + return !this.used && source.isControlledBy(event.getPlayerId()) && ((RollDieEvent) event).getSides() == 6; } } @@ -121,7 +105,7 @@ class TheBigIdeaEffect extends OneShotEffect { public TheBigIdeaEffect() { super(Outcome.PutCreatureInPlay); - this.staticText = "Roll a six-sided dice. Create a number of 1/1 red Brainiac creature tokens equal to the result"; + this.staticText = "Roll a six-sided die. Create a number of 1/1 red Brainiac creature tokens equal to the result"; } public TheBigIdeaEffect(final TheBigIdeaEffect effect) { @@ -136,11 +120,10 @@ class TheBigIdeaEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - Permanent permanent = game.getPermanent(source.getSourceId()); - if (controller != null && permanent != null) { - int amount = controller.rollDice(source, game, 6); - return new CreateTokenEffect(new BrainiacToken(), amount).apply(game, source); + if (controller == null) { + return false; } - return false; + int amount = controller.rollDice(outcome, source, game, 6); + return new BrainiacToken().putOntoBattlefield(amount, game, source, source.getControllerId()); } } diff --git a/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java b/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java index d5bbc7fe1b0..074542096b4 100644 --- a/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java +++ b/Mage.Sets/src/mage/cards/t/TheDeckOfManyThings.java @@ -80,7 +80,7 @@ class TheDeckOfManyThingsEffect extends RollDieWithResultTableEffect { if (player == null) { return false; } - int result = player.rollDice(source, game, sides) - player.getHand().size(); + int result = player.rollDice(outcome, source, game, sides) - player.getHand().size(); if (result <= 0) { player.discard(player.getHand(), false, source, game); } diff --git a/Mage.Sets/src/mage/cards/t/TimeOut.java b/Mage.Sets/src/mage/cards/t/TimeOut.java index ecdcafc626e..ccf868d8aa3 100644 --- a/Mage.Sets/src/mage/cards/t/TimeOut.java +++ b/Mage.Sets/src/mage/cards/t/TimeOut.java @@ -72,7 +72,7 @@ class TimeOutEffect extends OneShotEffect { if (owner == null) { return false; } - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); controller.putCardOnTopXOfLibrary(permanent, game, source, amount, true); return true; } diff --git a/Mage.Sets/src/mage/cards/u/UnderdarkRift.java b/Mage.Sets/src/mage/cards/u/UnderdarkRift.java index f1c63d0fb18..fe1e5db4fa5 100644 --- a/Mage.Sets/src/mage/cards/u/UnderdarkRift.java +++ b/Mage.Sets/src/mage/cards/u/UnderdarkRift.java @@ -83,7 +83,7 @@ class UnderdarkRiftEffect extends OneShotEffect { if (player == null || permanent == null) { return false; } - int result = player.rollDice(source, game, 10); + int result = player.rollDice(outcome, source, game, 10); player.putCardOnTopXOfLibrary(permanent, game, source, result + 1, true); return true; } diff --git a/Mage.Sets/src/mage/cards/u/UrzasScienceFairProject.java b/Mage.Sets/src/mage/cards/u/UrzasScienceFairProject.java index 6e77201a2ba..e84bf07addd 100644 --- a/Mage.Sets/src/mage/cards/u/UrzasScienceFairProject.java +++ b/Mage.Sets/src/mage/cards/u/UrzasScienceFairProject.java @@ -55,7 +55,7 @@ public final class UrzasScienceFairProject extends CardImpl { class UrzasScienceFairProjectEffect extends OneShotEffect { public UrzasScienceFairProjectEffect() { - super(Outcome.PutCreatureInPlay); + super(Outcome.Benefit); this.staticText = "Roll a six-sided die. {this} gets the indicated result. 1 - -2/-2 until end of turn. 2 - Prevent all combat damage it would deal this turn. 3 - gains vigilance until end of turn. 4 - gains first strike until end of turn. 5 - gains flying until end of turn. 6 - gets +2/+2 until end of turn"; } @@ -72,7 +72,7 @@ class UrzasScienceFairProjectEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); if (controller != null) { - int amount = controller.rollDice(source, game, 6); + int amount = controller.rollDice(outcome, source, game, 6); Effect effect = null; // 1 - -2/-2 until end of turn. diff --git a/Mage.Sets/src/mage/cards/v/VrondissRageOfAncients.java b/Mage.Sets/src/mage/cards/v/VrondissRageOfAncients.java new file mode 100644 index 00000000000..3309514180d --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VrondissRageOfAncients.java @@ -0,0 +1,50 @@ +package mage.cards.v; + +import mage.MageInt; +import mage.abilities.common.DealtDamageToSourceTriggeredAbility; +import mage.abilities.common.OneOrMoreDiceRolledTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DamageSelfEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.game.permanent.token.VrondissRageOfAncientsToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class VrondissRageOfAncients extends CardImpl { + + public VrondissRageOfAncients(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}{G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.DRAGON); + this.subtype.add(SubType.BARBARIAN); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Enrage — Whenever Vrondiss, Rage of Ancients is dealt damage, you may create a 5/4 red and green Dragon Spirit creature token with "When this creature deals damage, sacrifice it." + this.addAbility(new DealtDamageToSourceTriggeredAbility( + new CreateTokenEffect(new VrondissRageOfAncientsToken()), true, true + )); + + // Whenever you roll one or more dice, you may have Vrondiss, Rage of Ancients deal 1 damage to itself. + this.addAbility(new OneOrMoreDiceRolledTriggeredAbility( + new DamageSelfEffect(1).setText("{this} deal 1 damage to itself"), true + )); + } + + private VrondissRageOfAncients(final VrondissRageOfAncients card) { + super(card); + } + + @Override + public VrondissRageOfAncients copy() { + return new VrondissRageOfAncients(this); + } +} diff --git a/Mage.Sets/src/mage/cards/w/WillingTestSubject.java b/Mage.Sets/src/mage/cards/w/WillingTestSubject.java index e3b1826aff2..ee4127a083e 100644 --- a/Mage.Sets/src/mage/cards/w/WillingTestSubject.java +++ b/Mage.Sets/src/mage/cards/w/WillingTestSubject.java @@ -1,7 +1,6 @@ package mage.cards.w; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; @@ -17,10 +16,12 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.counters.CounterType; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; +import java.util.UUID; + /** - * * @author spjspj */ public final class WillingTestSubject extends CardImpl { @@ -59,7 +60,6 @@ class WillingTestSubjectTriggeredAbility extends TriggeredAbilityImpl { public WillingTestSubjectTriggeredAbility() { super(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance())); - } public WillingTestSubjectTriggeredAbility(final WillingTestSubjectTriggeredAbility ability) { @@ -73,17 +73,14 @@ class WillingTestSubjectTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DICE_ROLLED; + return event.getType() == GameEvent.EventType.DIE_ROLLED; } @Override public boolean checkTrigger(GameEvent event, Game game) { - if (this.isControlledBy(event.getPlayerId()) && event.getFlag()) { - if (event.getAmount() >= 4) { - return true; - } - } - return false; + DieRolledEvent drEvent = (DieRolledEvent) event; + // silver border card must look for "result" instead "natural result" + return this.isControlledBy(event.getPlayerId()) && drEvent.getResult() >= 4; } @Override diff --git a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java index 40be1d3e14e..78bc4fba4e2 100644 --- a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java +++ b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java @@ -45,10 +45,12 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Bag of Holding", 240, Rarity.UNCOMMON, mage.cards.b.BagOfHolding.class)); cards.add(new SetCardInfo("Baleful Beholder", 89, Rarity.COMMON, mage.cards.b.BalefulBeholder.class)); cards.add(new SetCardInfo("Bar the Gate", 47, Rarity.COMMON, mage.cards.b.BarTheGate.class)); + cards.add(new SetCardInfo("Barbarian Class", 131, Rarity.UNCOMMON, mage.cards.b.BarbarianClass.class)); cards.add(new SetCardInfo("Bard Class", 217, Rarity.RARE, mage.cards.b.BardClass.class)); cards.add(new SetCardInfo("Barrowin of Clan Undurr", 218, Rarity.UNCOMMON, mage.cards.b.BarrowinOfClanUndurr.class)); cards.add(new SetCardInfo("Battle Cry Goblin", 132, Rarity.UNCOMMON, mage.cards.b.BattleCryGoblin.class)); cards.add(new SetCardInfo("Black Dragon", 90, Rarity.UNCOMMON, mage.cards.b.BlackDragon.class)); + cards.add(new SetCardInfo("Brazen Dwarf", 134, Rarity.COMMON, mage.cards.b.BrazenDwarf.class)); cards.add(new SetCardInfo("Blink Dog", 3, Rarity.UNCOMMON, mage.cards.b.BlinkDog.class)); cards.add(new SetCardInfo("Blue Dragon", 49, Rarity.UNCOMMON, mage.cards.b.BlueDragon.class)); cards.add(new SetCardInfo("Boots of Speed", 133, Rarity.COMMON, mage.cards.b.BootsOfSpeed.class)); @@ -70,6 +72,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Cloister Gargoyle", 7, Rarity.UNCOMMON, mage.cards.c.CloisterGargoyle.class)); cards.add(new SetCardInfo("Compelled Duel", 178, Rarity.COMMON, mage.cards.c.CompelledDuel.class)); cards.add(new SetCardInfo("Contact Other Plane", 52, Rarity.COMMON, mage.cards.c.ContactOtherPlane.class)); + cards.add(new SetCardInfo("Critical Hit", 137, Rarity.UNCOMMON, mage.cards.c.CriticalHit.class)); cards.add(new SetCardInfo("Dancing Sword", 8, Rarity.RARE, mage.cards.d.DancingSword.class)); cards.add(new SetCardInfo("Dawnbringer Cleric", 9, Rarity.COMMON, mage.cards.d.DawnbringerCleric.class)); cards.add(new SetCardInfo("Deadly Dispute", 94, Rarity.COMMON, mage.cards.d.DeadlyDispute.class)); @@ -105,8 +108,10 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Eye of Vecna", 243, Rarity.RARE, mage.cards.e.EyeOfVecna.class)); cards.add(new SetCardInfo("Eyes of the Beholder", 101, Rarity.COMMON, mage.cards.e.EyesOfTheBeholder.class)); cards.add(new SetCardInfo("Farideh's Fireball", 142, Rarity.COMMON, mage.cards.f.FaridehsFireball.class)); + cards.add(new SetCardInfo("Farideh, Devil's Chosen", 221, Rarity.UNCOMMON, mage.cards.f.FaridehDevilsChosen.class)); cards.add(new SetCardInfo("Fates' Reversal", 102, Rarity.COMMON, mage.cards.f.FatesReversal.class)); cards.add(new SetCardInfo("Feign Death", 103, Rarity.COMMON, mage.cards.f.FeignDeath.class)); + cards.add(new SetCardInfo("Feywild Trickster", 58, Rarity.UNCOMMON, mage.cards.f.FeywildTrickster.class)); cards.add(new SetCardInfo("Fifty Feet of Rope", 244, Rarity.UNCOMMON, mage.cards.f.FiftyFeetOfRope.class)); cards.add(new SetCardInfo("Fighter Class", 222, Rarity.RARE, mage.cards.f.FighterClass.class)); cards.add(new SetCardInfo("Find the Path", 183, Rarity.COMMON, mage.cards.f.FindThePath.class)); @@ -191,6 +196,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Owlbear", 198, Rarity.COMMON, mage.cards.o.Owlbear.class)); cards.add(new SetCardInfo("Paladin Class", 29, Rarity.RARE, mage.cards.p.PaladinClass.class)); cards.add(new SetCardInfo("Paladin's Shield", 30, Rarity.COMMON, mage.cards.p.PaladinsShield.class)); + cards.add(new SetCardInfo("Pixie Guide", 66, Rarity.COMMON, mage.cards.p.PixieGuide.class)); cards.add(new SetCardInfo("Plains", 262, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Planar Ally", 31, Rarity.COMMON, mage.cards.p.PlanarAlly.class)); cards.add(new SetCardInfo("Plate Armor", 32, Rarity.UNCOMMON, mage.cards.p.PlateArmor.class)); diff --git a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java index 3106ed89741..514d965bc2c 100644 --- a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java +++ b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java @@ -25,6 +25,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Angelic Gift", 64, Rarity.COMMON, mage.cards.a.AngelicGift.class)); cards.add(new SetCardInfo("Anger", 113, Rarity.UNCOMMON, mage.cards.a.Anger.class)); cards.add(new SetCardInfo("Apex of Power", 114, Rarity.MYTHIC, mage.cards.a.ApexOfPower.class)); + cards.add(new SetCardInfo("Arcane Endeavor", 14, Rarity.RARE, mage.cards.a.ArcaneEndeavor.class)); cards.add(new SetCardInfo("Arcane Sanctum", 223, Rarity.UNCOMMON, mage.cards.a.ArcaneSanctum.class)); cards.add(new SetCardInfo("Arcane Signet", 197, Rarity.COMMON, mage.cards.a.ArcaneSignet.class)); cards.add(new SetCardInfo("Argentum Armor", 198, Rarity.RARE, mage.cards.a.ArgentumArmor.class)); @@ -168,8 +169,11 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Mulldrifter", 87, Rarity.UNCOMMON, mage.cards.m.Mulldrifter.class)); cards.add(new SetCardInfo("Murder of Crows", 88, Rarity.UNCOMMON, mage.cards.m.MurderOfCrows.class)); cards.add(new SetCardInfo("Nature's Lore", 164, Rarity.COMMON, mage.cards.n.NaturesLore.class)); + cards.add(new SetCardInfo("Netherese Puzzle-Ward", 17, Rarity.RARE, mage.cards.n.NetheresePuzzleWard.class)); + cards.add(new SetCardInfo("Reckless Endeavor", 33, Rarity.RARE, mage.cards.r.RecklessEndeavor.class)); cards.add(new SetCardInfo("Necromantic Selection", 103, Rarity.RARE, mage.cards.n.NecromanticSelection.class)); cards.add(new SetCardInfo("Necrotic Sliver", 188, Rarity.UNCOMMON, mage.cards.n.NecroticSliver.class)); + cards.add(new SetCardInfo("Neverwinter Hydra", 41, Rarity.RARE, mage.cards.n.NeverwinterHydra.class)); cards.add(new SetCardInfo("Nihiloor", 53, Rarity.MYTHIC, mage.cards.n.Nihiloor.class)); cards.add(new SetCardInfo("Nimbus Maze", 252, Rarity.RARE, mage.cards.n.NimbusMaze.class)); cards.add(new SetCardInfo("Obsessive Stitcher", 189, Rarity.UNCOMMON, mage.cards.o.ObsessiveStitcher.class)); @@ -268,6 +272,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Victimize", 112, Rarity.UNCOMMON, mage.cards.v.Victimize.class)); cards.add(new SetCardInfo("Viridian Longbow", 221, Rarity.COMMON, mage.cards.v.ViridianLongbow.class)); cards.add(new SetCardInfo("Vitu-Ghazi, the City-Tree", 272, Rarity.UNCOMMON, mage.cards.v.VituGhaziTheCityTree.class)); + cards.add(new SetCardInfo("Vrondiss, Rage of Ancients", 4, Rarity.MYTHIC, mage.cards.v.VrondissRageOfAncients.class)); cards.add(new SetCardInfo("Wall of Omens", 77, Rarity.UNCOMMON, mage.cards.w.WallOfOmens.class)); cards.add(new SetCardInfo("Wand of Orcus", 28, Rarity.RARE, mage.cards.w.WandOfOrcus.class)); cards.add(new SetCardInfo("Warstorm Surge", 149, Rarity.RARE, mage.cards.w.WarstormSurge.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/rolldice/RollDiceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/rolldice/RollDiceTest.java new file mode 100644 index 00000000000..a59c2d3798a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/rolldice/RollDiceTest.java @@ -0,0 +1,519 @@ +package org.mage.test.cards.rolldice; + +import mage.abilities.keyword.FlyingAbility; +import mage.constants.PhaseStep; +import mage.constants.Planes; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +import java.util.Arrays; + +/** + * @author TheElk801, JayDi85 + */ +public class RollDiceTest extends CardTestPlayerBaseWithAIHelps { + + private static final String goblins = "Swarming Goblins"; + private static final String guide = "Pixie Guide"; + private static final String thumb = "Krark's Other Thumb"; + private static final String gallery = "Mirror Gallery"; + private static final String farideh = "Farideh, Devil's Chosen"; + + @Test(expected = AssertionError.class) + public void test_StrictFailWithoutSetup() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.HAND, playerA, goblins); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblins); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + private void runGoblinTest(int roll, int goblinCount) { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.HAND, playerA, goblins); + + setDieRollResult(playerA, roll); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblins); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, goblins, 1); + assertPermanentCount(playerA, "Goblin", goblinCount); + } + + @Test + public void test_GoblinRoll_1() { + runGoblinTest(1, 1); + } + + @Test + public void test_GoblinRoll_9() { + runGoblinTest(9, 1); + } + + @Test + public void test_GoblinRoll_10() { + runGoblinTest(10, 2); + } + + @Test + public void test_GoblinRoll_19() { + runGoblinTest(19, 2); + } + + @Test + public void test_GoblinRoll_20() { + runGoblinTest(20, 3); + } + + @Test + public void test_PixieGuide_1() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, guide); + addCard(Zone.HAND, playerA, goblins); + + setDieRollResult(playerA, 9); + setDieRollResult(playerA, 10); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblins); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, goblins, 1); + assertPermanentCount(playerA, guide, 1); + assertPermanentCount(playerA, "Goblin", 2); + } + + @Test + public void test_PixieGuide_2() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, guide, 2); + addCard(Zone.HAND, playerA, goblins); + + setChoice(playerA, guide); // choose replacement effect + setDieRollResult(playerA, 9); + setDieRollResult(playerA, 9); + setDieRollResult(playerA, 10); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblins); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, goblins, 1); + assertPermanentCount(playerA, guide, 2); + assertPermanentCount(playerA, "Goblin", 2); + } + + private void runKrarksOtherThumbTest(int choice, int thumbCount, int goblinCount, int... rolls) { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, thumb, thumbCount); + addCard(Zone.BATTLEFIELD, playerA, gallery); + addCard(Zone.HAND, playerA, goblins); + + for (int i = 0; i < thumbCount - 1; i++) { + setChoice(playerA, thumb); // choose replacement effect + } + for (int roll : rolls) { + setDieRollResult(playerA, roll); + } + if (Arrays.stream(rolls).distinct().count() > 1) { + setChoice(playerA, "" + choice); + } + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblins); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, goblins, 1); + assertPermanentCount(playerA, gallery, 1); + assertPermanentCount(playerA, thumb, thumbCount); + assertPermanentCount(playerA, "Goblin", goblinCount); + } + + @Test(expected = AssertionError.class) + public void test_KrarksOtherThumb_1copy_MustFailOnWrongChoiceSetup() { + runKrarksOtherThumbTest(8, 1, 1, 9, 10); + } + + @Test + public void test_KrarksOtherThumb_1copy_ChooseLower() { + runKrarksOtherThumbTest(9, 1, 1, 9, 10); + } + + @Test + public void test_KrarksOtherThumb_1copy_ChooseHigher() { + runKrarksOtherThumbTest(10, 1, 2, 9, 10); + } + + @Test + public void test_KrarksOtherThumb_1copy_SameRoll() { + runKrarksOtherThumbTest(10, 1, 2, 10, 10); + } + + @Test + public void test_KrarksOtherThumb_2copies_ChooseLowest() { + runKrarksOtherThumbTest(8, 2, 1, 8, 9, 10, 11); + } + + @Test + public void test_KrarksOtherThumb_2copies_ChooseMedium() { + runKrarksOtherThumbTest(9, 2, 1, 8, 9, 10, 11); + } + + @Test + public void test_KrarksOtherThumb_2copies_ChooseHighest() { + runKrarksOtherThumbTest(11, 2, 2, 8, 9, 10, 11); + } + + @Test + public void test_KrarksOtherThumb_2copies_SameRoll() { + runKrarksOtherThumbTest(8, 2, 1, 8, 8, 8, 8); + } + + private void runFaridehTest(int goblinCount, int handCount, int roll) { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, farideh); + addCard(Zone.HAND, playerA, goblins); + + setDieRollResult(playerA, roll); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblins); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, goblins, 1); + assertPermanentCount(playerA, "Goblin", goblinCount); + assertAbility(playerA, farideh, FlyingAbility.getInstance(), true); + assertHandCount(playerA, handCount); + } + + @Test + public void test_FaridehDevilsChosen_NoDraw() { + runFaridehTest(1, 0, 9); + } + + @Test + public void test_FaridehDevilsChosen_Draw() { + runFaridehTest(2, 1, 10); + } + + @Test + public void test_PlanarDie_Single() { + // Active player can roll the planar die: Whenever you roll {CHAOS}, create a 7/7 colorless Eldrazi creature with annhilator 1 + addPlane(playerA, Planes.PLANE_HEDRON_FIELDS_OF_AGADEEM); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + // first chaos + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 1); // make chaos + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + // second chaos (with additional cost) + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 1); // make chaos + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Eldrazi", 2); + assertTappedCount("Mountain", true, 1); // cost for second planar die + } + + @Test + public void test_PlanarDice_OneOrMoreDieRollTriggersMustWork() { + // Active player can roll the planar die: Whenever you roll {CHAOS}, create a 7/7 colorless Eldrazi creature with annhilator 1 + addPlane(playerA, Planes.PLANE_HEDRON_FIELDS_OF_AGADEEM); + // + // Whenever you roll one or more dice, Farideh, Devil's Chosen gains flying and menace until end of turn. + // If any of those results was 10 or higher, draw a card. + addCard(Zone.BATTLEFIELD, playerA, "Farideh, Devil's Chosen"); + + checkAbility("no fly before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Farideh, Devil's Chosen", FlyingAbility.class, false); + + // roll planar die and trigger Farideh + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 1); // make chaos + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + checkAbility("must be fly after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Farideh, Devil's Chosen", FlyingAbility.class, true); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Eldrazi", 1); + } + + @Test + public void test_PlanarDice_DieRollTrigger_MustWorkAndSeeEmptyResult_1() { + // Active player can roll the planar die: Whenever you roll {CHAOS}, create a 7/7 colorless Eldrazi creature with annhilator 1 + addPlane(playerA, Planes.PLANE_HEDRON_FIELDS_OF_AGADEEM); + // + // As Hammer Jammer enters the battlefield, roll a six-sided die. Hammer Jammer enters the battlefield with a number of +1/+1 counters on it equal to the result. + // Whenever you roll a die, remove all +1/+1 counters from Hammer Jammer, then put a number of +1/+1 counters on it equal to the result. + addCard(Zone.HAND, playerA, "Hammer Jammer");// {3}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + // prepare 5/5 hammer + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hammer Jammer"); + setDieRollResult(playerA, 5); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPT("must have 5/5 hammer", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hammer Jammer", 5, 5); + + // roll planar die and trigger event with 0 result + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 1); // make chaos + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkGraveyardCount("hammer must die", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hammer Jammer", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Eldrazi", 1); + } + + @Test + public void test_PlanarDice_DieRollTrigger_MustWorkAndSeeEmptyResult_2() { + // Active player can roll the planar die: Whenever you roll {CHAOS}, create a 7/7 colorless Eldrazi creature with annhilator 1 + addPlane(playerA, Planes.PLANE_HEDRON_FIELDS_OF_AGADEEM); + // + // Whenever you roll a 5 or higher on a die, Steel Squirrel gets +X/+X until end of turn, where X is the result. + addCard(Zone.BATTLEFIELD, playerA, "Steel Squirrel", 1); // 1/1 + // + // Roll a six-sided die. Create a number of 1/1 red Goblin creature tokens equal to the result. + addCard(Zone.HAND, playerA, "Box of Free-Range Goblins", 1); // {4}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + checkPT("no boost before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Steel Squirrel", 1, 1); + + // roll planar die and trigger event with 0 result + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 7); // make blank + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPT("no boost after planar", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Steel Squirrel", 1, 1); + + // roll normal die and trigger with boost + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Box of Free-Range Goblins"); + setDieRollResult(playerA, 6); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPT("boost after normal", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Steel Squirrel", 1 + 6, 1 + 6); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Eldrazi", 0); + } + + @Test + public void test_AdditionalRoll_WithLowest() { + // If you would roll one or more dice, instead roll that many dice plus one and ignore the lowest roll. + addCard(Zone.BATTLEFIELD, playerA, "Barbarian Class", 2); + // + // Roll a six-sided die. Create a number of 1/1 red Goblin creature tokens equal to the result. + addCard(Zone.HAND, playerA, "Box of Free-Range Goblins", 1); // {4}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + // roll normal die and trigger 2x additional roll + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Box of Free-Range Goblins"); + setChoice(playerA, "Barbarian Class"); // replace events + setDieRollResult(playerA, 3); // normal roll + setDieRollResult(playerA, 6); // additional roll - will be selected + setDieRollResult(playerA, 5); // additional roll + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin", 6); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_AdditionalRoll_WithBigIdea() { + // {2}{B/R}{B/R}, {T}: Roll a six-sided dice. Create a number of 1/1 red Brainiac creature tokens equal to the result. + // Tap three untapped Brainiacs you control: The next time you would roll a six-sided die, + // instead roll two six-sided dice and use the total of those results. + addCard(Zone.BATTLEFIELD, playerA, "The Big Idea", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // + // Roll a six-sided die. Create a number of 1/1 red Goblin creature tokens equal to the result. + addCard(Zone.HAND, playerA, "Box of Free-Range Goblins", 1); // {4}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + // prepare idea cost + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B/R}{B/R}, {T}"); + setDieRollResult(playerA, 3); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brainiac", 3); + + // prepare idea effect + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tap three Brainiac"); + setChoice(playerA, "Brainiac", 3); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // roll and trigger idea replace event + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Box of Free-Range Goblins"); + setDieRollResult(playerA, 3); // normal roll + setDieRollResult(playerA, 6); // additional roll - will be sums + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin", 3 + 6); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_AdditionalRoll_WithChoose() { + // If you would roll a die, instead roll two of those dice and ignore one of those results. + addCard(Zone.BATTLEFIELD, playerA, "Krark's Other Thumb", 1); + // + // Roll a six-sided die. Create a number of 1/1 red Goblin creature tokens equal to the result. + addCard(Zone.HAND, playerA, "Box of Free-Range Goblins", 1); // {4}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + // roll normal die and trigger 2x additional roll + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Box of Free-Range Goblins"); + setDieRollResult(playerA, 3); // normal roll + setDieRollResult(playerA, 6); // additional roll + setChoice(playerA, "6"); // keep 6 as roll result + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin", 6); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_PlanarDice_AdditionalRoll_WithLowest_MustIgnore() { + // If you would roll one or more dice, instead roll that many dice plus one and ignore the lowest roll. + addCard(Zone.BATTLEFIELD, playerA, "Barbarian Class", 2); + // + // Active player can roll the planar die: Whenever you roll {CHAOS}, create a 7/7 colorless Eldrazi creature with annhilator 1 + addPlane(playerA, Planes.PLANE_HEDRON_FIELDS_OF_AGADEEM); + + // roll planar die, but no triggers with double roll - cause it works with numerical results (lowest) + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 1); // only one roll, chaos + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Eldrazi", 1); + } + + @Test + public void test_PlanarDice_AdditionalRoll_WithChoose_MustWork() { + // If you would roll a die, instead roll two of those dice and ignore one of those results. + addCard(Zone.BATTLEFIELD, playerA, "Krark's Other Thumb", 1); + // + // Active player can roll the planar die: Whenever you roll {CHAOS}, create a 7/7 colorless Eldrazi creature with annhilator 1 + addPlane(playerA, Planes.PLANE_HEDRON_FIELDS_OF_AGADEEM); + + // roll planar die, but no triggers with second roll - cause it works with numerical results (lowest) + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 4); // first roll as blank + setDieRollResult(playerA, 1); // second roll as chaos + setChoice(playerA, "Chaos Roll"); // must choose result + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Eldrazi", 1); + } + + @Test + public void test_PlanarDice_AdditionalRoll_WithBigIdea_MustIgnore() { + // see consts comments about planar die size + //Assert.assertEquals("Planar dice must be six sided", 6, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES); + + // {2}{B/R}{B/R}, {T}: Roll a six-sided dice. Create a number of 1/1 red Brainiac creature tokens equal to the result. + // Tap three untapped Brainiacs you control: The next time you would roll a six-sided die, + // instead roll two six-sided dice and use the total of those results. + addCard(Zone.BATTLEFIELD, playerA, "The Big Idea", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // + // Active player can roll the planar die: Whenever you roll {CHAOS}, create a 7/7 colorless Eldrazi creature with annhilator 1 + addPlane(playerA, Planes.PLANE_HEDRON_FIELDS_OF_AGADEEM); + + // prepare idea cost + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B/R}{B/R}, {T}"); + setDieRollResult(playerA, 3); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brainiac", 3); + + // prepare idea effect + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tap three Brainiac"); + setChoice(playerA, "Brainiac", 3); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // roll planar die, but no triggers with second roll - cause it works with numerical results (sum) + // or planar dice hasn't 6 sides + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}: Roll the planar"); + setDieRollResult(playerA, 1); // only one roll, chaos + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Eldrazi", 1); + } + + @Test + public void test_AI_AdditionalRollChoose() { + // If you would roll a die, instead roll two of those dice and ignore one of those results. + addCard(Zone.BATTLEFIELD, playerA, "Krark's Other Thumb", 1); + // + // Roll a six-sided die. Create a number of 1/1 red Goblin creature tokens equal to the result. + addCard(Zone.HAND, playerA, "Box of Free-Range Goblins", 1); // {4}{R}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + // roll normal die and trigger 2x additional roll + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Box of Free-Range Goblins"); + setDieRollResult(playerA, 3); // normal roll + setDieRollResult(playerA, 6); // additional roll + // AI must choose max value due good outcome + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin", 6); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mmq/MisdirectionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mmq/MisdirectionTest.java index ad91b2bea1e..5ca86a85543 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/mmq/MisdirectionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mmq/MisdirectionTest.java @@ -31,6 +31,7 @@ public class MisdirectionTest extends CardTestPlayerBase { checkHandCardCount("B haven't lions", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Silvercoat Lion", 0); checkHandCardCount("B have 5 bears", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Ashcoat Bear", 5); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); assertAllCommandsUsed(); @@ -83,12 +84,14 @@ public class MisdirectionTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rakshasa's Secret", playerB); // cast misdir, but it's not apply and taget will be same castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Misdirection", "Rakshasa's Secret", "Rakshasa's Secret"); + addTarget(playerB, playerB); // new target for rakhas will be same B // B must select cards to discard (2 lions, not bears) setChoice(playerB, "Silvercoat Lion"); // select target 1 setChoice(playerB, "Silvercoat Lion"); // select target 2 checkHandCardCount("B haven't lions", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Silvercoat Lion", 0); checkHandCardCount("B have 5 bears", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Ashcoat Bear", 5); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); assertAllCommandsUsed(); diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer.java index e3e1269dd6d..9908787ee2a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer.java @@ -1,5 +1,6 @@ package org.mage.test.player; +import mage.choices.Choice; import mage.constants.Outcome; import mage.constants.RangeOfInfluence; import mage.game.Game; @@ -13,16 +14,20 @@ import java.util.UUID; */ /** - * Mock class to override AI logic for test, cause PlayerImpl uses inner calls for other methods. If you - * want to override that methods for tests then call it here. + * Mock class to inject test player support in the inner choice calls, e.g. in PlayerImpl. If you + * want to set up inner choices then override it here. *

- * It's a workaround and can be bugged (if you catch overflow error with new method then TestPlayer - * class must re-implement full method code without computerPlayer calls). + * Works in strict mode only. + *

+ * If you catch overflow error with new method then check strict mode in it. *

* Example 1: TestPlayer's code uses outer computerPlayer call to discard but discard's inner code must call choose from TestPlayer * Example 2: TestPlayer's code uses outer computerPlayer call to flipCoin but flipCoin's inner code must call flipCoinResult from TestPlayer *

* Don't forget to add new methods in another classes like TestComputerPlayer7 or TestComputerPlayerMonteCarlo + *

+ * If you implement set up of random results for tests (die roll, flip coin, etc) and want to support AI tests + * (same random results in simulated games) then override same methods in SimulatedPlayer2 too */ public class TestComputerPlayer extends ComputerPlayer { @@ -39,12 +44,47 @@ public class TestComputerPlayer extends ComputerPlayer { @Override public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game) { - return testPlayerLink.choose(outcome, target, sourceId, game); + if (testPlayerLink.canChooseByComputer()) { + return super.choose(outcome, target, sourceId, game); + } else { + return testPlayerLink.choose(outcome, target, sourceId, game); + } + } + + @Override + public boolean choose(Outcome outcome, Choice choice, Game game) { + if (testPlayerLink.canChooseByComputer()) { + return super.choose(outcome, choice, game); + } else { + return testPlayerLink.choose(outcome, choice, game); + } } @Override public boolean flipCoinResult(Game game) { - return testPlayerLink.flipCoinResult(game); + if (testPlayerLink.canChooseByComputer()) { + return super.flipCoinResult(game); + } else { + return testPlayerLink.flipCoinResult(game); + } + } + + @Override + public int rollDieResult(int sides, Game game) { + if (testPlayerLink.canChooseByComputer()) { + return super.rollDieResult(sides, game); + } else { + return testPlayerLink.rollDieResult(sides, game); + } + } + + @Override + public boolean isComputer() { + if (testPlayerLink.canChooseByComputer()) { + return super.isComputer(); + } else { + return testPlayerLink.isComputer(); + } } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer7.java b/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer7.java index cd313a54973..0017004e43e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer7.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayer7.java @@ -1,5 +1,6 @@ package org.mage.test.player; +import mage.choices.Choice; import mage.constants.Outcome; import mage.constants.RangeOfInfluence; import mage.game.Game; @@ -9,7 +10,7 @@ import mage.target.Target; import java.util.UUID; /** - * Copy paste methods from TestComputerPlayer, see docs in there + * Copied-pasted methods from TestComputerPlayer, see docs in there * * @author JayDi85 */ @@ -28,11 +29,40 @@ public class TestComputerPlayer7 extends ComputerPlayer7 { @Override public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game) { - return testPlayerLink.choose(outcome, target, sourceId, game); + if (testPlayerLink.canChooseByComputer()) { + return super.choose(outcome, target, sourceId, game); + } else { + return testPlayerLink.choose(outcome, target, sourceId, game); + } + } + + @Override + public boolean choose(Outcome outcome, Choice choice, Game game) { + if (testPlayerLink.canChooseByComputer()) { + return super.choose(outcome, choice, game); + } else { + return testPlayerLink.choose(outcome, choice, game); + } } @Override public boolean flipCoinResult(Game game) { + // same random results must be same in any mode return testPlayerLink.flipCoinResult(game); } + + @Override + public int rollDieResult(int sides, Game game) { + // same random results must be same in any mode + return testPlayerLink.rollDieResult(sides, game); + } + + @Override + public boolean isComputer() { + if (testPlayerLink.canChooseByComputer()) { + return super.isComputer(); + } else { + return testPlayerLink.isComputer(); + } + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayerMonteCarlo.java b/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayerMonteCarlo.java index ee5d787f24b..e55d22749f1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayerMonteCarlo.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestComputerPlayerMonteCarlo.java @@ -1,5 +1,6 @@ package org.mage.test.player; +import mage.choices.Choice; import mage.constants.Outcome; import mage.constants.RangeOfInfluence; import mage.game.Game; @@ -9,7 +10,7 @@ import mage.target.Target; import java.util.UUID; /** - * Copy paste methods from TestComputerPlayer, see docs in there + * Copied-pasted methods from TestComputerPlayer, see docs in there * * @author JayDi85 */ @@ -28,11 +29,46 @@ public class TestComputerPlayerMonteCarlo extends ComputerPlayerMCTS { @Override public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game) { - return testPlayerLink.choose(outcome, target, sourceId, game); + if (testPlayerLink.canChooseByComputer()) { + return super.choose(outcome, target, sourceId, game); + } else { + return testPlayerLink.choose(outcome, target, sourceId, game); + } + } + + @Override + public boolean choose(Outcome outcome, Choice choice, Game game) { + if (testPlayerLink.canChooseByComputer()) { + return super.choose(outcome, choice, game); + } else { + return testPlayerLink.choose(outcome, choice, game); + } } @Override public boolean flipCoinResult(Game game) { - return testPlayerLink.flipCoinResult(game); + if (testPlayerLink.canChooseByComputer()) { + return super.flipCoinResult(game); + } else { + return testPlayerLink.flipCoinResult(game); + } + } + + @Override + public int rollDieResult(int sides, Game game) { + if (testPlayerLink.canChooseByComputer()) { + return super.rollDieResult(sides, game); + } else { + return testPlayerLink.rollDieResult(sides, game); + } + } + + @Override + public boolean isComputer() { + if (testPlayerLink.canChooseByComputer()) { + return super.isComputer(); + } else { + return testPlayerLink.isComputer(); + } } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index be12fae6908..a4e77c9a2ab 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -80,6 +80,7 @@ public class TestPlayer implements Player { public static final String NO_TARGET = "NO_TARGET"; // cast spell or activate ability without target defines public static final String FLIPCOIN_RESULT_TRUE = "[flipcoin_true]"; public static final String FLIPCOIN_RESULT_FALSE = "[flipcoin_false]"; + public static final String DIE_ROLL = "[die_roll]: "; private int maxCallsWithoutAction = 400; private int foundNoAction = 0; @@ -99,8 +100,13 @@ public class TestPlayer implements Player { private final Map aliases = new HashMap<>(); // aliases for game objects/players (use it for cards with same name to save and use) private final List modesSet = new ArrayList<>(); - private final ComputerPlayer computerPlayer; - private boolean strictChooseMode = false; // test will raise error on empty choice/target (e.g. devs must setup all choices/targets for all spells) + private final ComputerPlayer computerPlayer; // real player + + // Strict mode for all choose dialogs: + // - enable checks for wrong or missing choice commands (you must set up all choices by unit test) + // - enable inner choice dialogs accessable by set up choices + // (example: card call TestPlayer's choice, but it uses another choices, see docs in TestComputerPlayer) + private boolean strictChooseMode = false; private String[] groupsForTargetHandling = null; @@ -1926,7 +1932,7 @@ public class TestPlayer implements Player { } private void chooseStrictModeFailed(String choiceType, Game game, String reason, boolean printAbilities) { - if (strictChooseMode && !AIRealGameSimulation) { + if (!this.canChooseByComputer()) { if (printAbilities) { printStart("Available mana for " + computerPlayer.getName()); printMana(game, computerPlayer.getManaAvailable(game)); @@ -1996,6 +2002,7 @@ public class TestPlayer implements Player { @Override public boolean choose(Outcome outcome, Choice choice, Game game) { assertAliasSupportInChoices(false); + if (!choices.isEmpty()) { // skip choices @@ -3188,12 +3195,13 @@ public class TestPlayer implements Player { @Override public boolean isComputer() { - // all players in unit tests are computers, so you must use AIRealGameSimulation to test different logic (Human vs AI) + // all players in unit tests are computers, so it allows testing different logic (Human vs AI) if (isTestsMode()) { + // AIRealGameSimulation = true - full plyable AI + // AIRealGameSimulation = false - choose assisted AI (Human) return AIRealGameSimulation; } else { throw new IllegalStateException("Can't use test player outside of unit tests"); - //return !isHuman(); } } @@ -3513,11 +3521,6 @@ public class TestPlayer implements Player { return computerPlayer.flipCoin(source, game, true); } - @Override - public boolean flipCoin(Ability source, Game game, boolean winnable, List appliedEffects) { - return computerPlayer.flipCoin(source, game, true, appliedEffects); - } - @Override public boolean flipCoinResult(Game game) { assertAliasSupportInChoices(false); @@ -3531,20 +3534,31 @@ public class TestPlayer implements Player { return false; } } - this.chooseStrictModeFailed("flipcoin result", game, "Use setFlipCoinResult to setup it in unit tests"); + this.chooseStrictModeFailed("flip coin result", game, "Use setFlipCoinResult to set it up in unit tests"); // implementation from PlayerImpl: return RandomUtil.nextBoolean(); } @Override - public int rollDice(Ability source, Game game, int numSides) { - return computerPlayer.rollDice(source, game, numSides); + public List rollDice(Outcome outcome, Ability source, Game game, int numSides, int numDice, int ignoreLowestAmount) { + return computerPlayer.rollDice(outcome, source, game, numSides, numDice, ignoreLowestAmount); } @Override - public int rollDice(Ability source, Game game, List appliedEffects, int numSides) { - return computerPlayer.rollDice(source, game, appliedEffects, numSides); + public int rollDieResult(int sides, Game game) { + assertAliasSupportInChoices(false); + if (!choices.isEmpty()) { + String nextResult = choices.get(0); + if (nextResult.startsWith(DIE_ROLL)) { + choices.remove(0); + return Integer.parseInt(nextResult.substring(DIE_ROLL.length())); + } + } + this.chooseStrictModeFailed("die roll result", game, "Use setDieRollResult to set it up in unit tests"); + + // implementation from PlayerImpl: + return RandomUtil.nextInt(sides) + 1; } @Override @@ -4273,18 +4287,8 @@ public class TestPlayer implements Player { } @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects, int numberChaosSides, int numberPlanarSides) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int numberChaosSides, int numberPlanarSides) { + return computerPlayer.rollPlanarDie(outcome, source, game, numberChaosSides, numberPlanarSides); } @Override @@ -4362,4 +4366,18 @@ public class TestPlayer implements Player { public String toString() { return computerPlayer.toString(); } + + public boolean canChooseByComputer() { + // full playable AI can choose any time + if (this.AIRealGameSimulation) { + return true; + } + + // non-strict mode allows computer assisted choices (for old tests compatibility only) + if (!this.strictChooseMode) { + return true; + } + + return false; + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index f36a901309b..b15a0320844 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1508,11 +1508,21 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } public void assertChoicesCount(TestPlayer player, int count) throws AssertionError { - Assert.assertEquals("(Choices of " + player.getName() + ") Count are not equal (found " + player.getChoices() + ")", count, player.getChoices().size()); + String mes = String.format( + "(Choices of %s) Count are not equal (found %s). Some inner choose dialogs can be set up only in strict mode.", + player.getName(), + player.getChoices() + ); + Assert.assertEquals(mes, count, player.getChoices().size()); } public void assertTargetsCount(TestPlayer player, int count) throws AssertionError { - Assert.assertEquals("(Targets of " + player.getName() + ") Count are not equal (found " + player.getTargets() + ")", count, player.getTargets().size()); + String mes = String.format( + "(Targets of %s) Count are not equal (found %s). Some inner choose dialogs can be set up only in strict mode.", + player.getName(), + player.getTargets() + ); + Assert.assertEquals(mes, count, player.getTargets().size()); } /** @@ -1998,6 +2008,21 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement player.addChoice(result ? TestPlayer.FLIPCOIN_RESULT_TRUE : TestPlayer.FLIPCOIN_RESULT_FALSE); } + /** + * Set next result of next die roll (uses for both normal or planar rolls) + * + * For planar rolls: + * 1..2 - chaos + * 3..7 - blank + * 8..9 - planar + * + * @param player + * @param result + */ + public void setDieRollResult(TestPlayer player, int result) { + player.addChoice(TestPlayer.DIE_ROLL + result); + } + /** * Set target permanents * diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 7d172a07e88..8277b202bc8 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -635,23 +635,18 @@ public class PlayerStub implements Player { return false; } - @Override - public boolean flipCoin(Ability source, Game game, boolean winnable, List appliedEffects) { - return false; - } - @Override public boolean flipCoinResult(Game game) { return false; } @Override - public int rollDice(Ability source, Game game, int numSides) { - return 1; + public List rollDice(Outcome outcome, Ability source, Game game, int numSides, int numDice, int ignoreLowestAmount) { + return null; } @Override - public int rollDice(Ability source, Game game, List appliedEffects, int numSides) { + public int rollDieResult(int sides, Game game) { return 1; } @@ -1366,17 +1361,7 @@ public class PlayerStub implements Player { } @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects, int numberChaosSides, int numberPlanarSides) { + public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int numberChaosSides, int numberPlanarSides) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java index 733b539c423..9face45d4e4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/utils/RandomTest.java @@ -3,7 +3,8 @@ package org.mage.test.utils; import mage.cards.decks.Deck; import mage.cards.decks.DeckCardLists; import mage.constants.MultiplayerAttackOption; -import mage.constants.PlanarDieRoll; +import mage.constants.Outcome; +import mage.constants.PlanarDieRollResult; import mage.constants.RangeOfInfluence; import mage.game.Game; import mage.game.TwoPlayerDuel; @@ -101,7 +102,7 @@ public class RandomTest { for (int x = 0; x < weight; x++) { for (int y = 0; y < height; y++) { // roll dice - int diceVal = player.rollDice(null, game, 12); + int diceVal = player.rollDice(Outcome.Neutral, null, game, 12); int colorMult = Math.floorDiv(255, 12); image.setRGB(x, y, new Color(colorMult * diceVal, colorMult * diceVal, colorMult * diceVal).getRGB()); @@ -124,11 +125,11 @@ public class RandomTest { for (int x = 0; x < weight; x++) { for (int y = 0; y < height; y++) { // roll planar dice - PlanarDieRoll res = player.rollPlanarDie(null, game); + PlanarDieRollResult res = player.rollPlanarDie(Outcome.Neutral, null, game); image.setRGB(x, y, new Color( - res.equals(PlanarDieRoll.CHAOS_ROLL) ? 255 : 0, - res.equals(PlanarDieRoll.PLANAR_ROLL) ? 255 : 0, - res.equals(PlanarDieRoll.NIL_ROLL) ? 255 : 0 + res.equals(PlanarDieRollResult.CHAOS_ROLL) ? 255 : 0, + res.equals(PlanarDieRollResult.PLANAR_ROLL) ? 255 : 0, + res.equals(PlanarDieRollResult.BLANK_ROLL) ? 255 : 0 ).getRGB()); } } diff --git a/Mage/src/main/java/mage/abilities/common/OneOrMoreDiceRolledTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/OneOrMoreDiceRolledTriggeredAbility.java new file mode 100644 index 00000000000..53ba37e8903 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/OneOrMoreDiceRolledTriggeredAbility.java @@ -0,0 +1,63 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.DiceRolledEvent; +import mage.game.events.GameEvent; + +/** + * @author weirddan455 + */ +public class OneOrMoreDiceRolledTriggeredAbility extends TriggeredAbilityImpl { + + public OneOrMoreDiceRolledTriggeredAbility(Effect effect) { + this(effect, false); + } + + public OneOrMoreDiceRolledTriggeredAbility(Effect effect, boolean optional) { + super(Zone.BATTLEFIELD, effect, optional); + } + + private OneOrMoreDiceRolledTriggeredAbility(final OneOrMoreDiceRolledTriggeredAbility effect) { + super(effect); + } + + @Override + public OneOrMoreDiceRolledTriggeredAbility copy() { + return new OneOrMoreDiceRolledTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DICE_ROLLED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!isControlledBy(event.getPlayerId())) { + return false; + } + int maxRoll = ((DiceRolledEvent) event) + .getResults() + .stream() + .filter(Integer.class::isInstance) // only numerical die result can be masured + .map(Integer.class::cast) + .mapToInt(Integer::intValue) + .max() + .orElse(0); + this.getEffects().setValue("maxDieRoll", maxRoll); + return true; + } + + @Override + public String getTriggerPhrase() { + return "Whenever you roll one or more dice, "; + } + + @Override + public String getRule() { + return super.getRule(); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ReturnSourceFromGraveyardToHandEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnSourceFromGraveyardToHandEffect.java index 562382eabaa..cb54aa48929 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ReturnSourceFromGraveyardToHandEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnSourceFromGraveyardToHandEffect.java @@ -1,6 +1,6 @@ - package mage.abilities.effects.common; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; @@ -10,7 +10,6 @@ import mage.game.Game; import mage.players.Player; /** - * * @author BetaSteward_at_googlemail.com */ public class ReturnSourceFromGraveyardToHandEffect extends OneShotEffect { @@ -32,11 +31,9 @@ public class ReturnSourceFromGraveyardToHandEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - Card card = controller.getGraveyard().get(source.getSourceId(), game); - if (card != null) { - return controller.moveCards(card, Zone.HAND, source, game); - } - return false; + MageObject sourceObject = source.getSourceObjectIfItStillExists(game); + return controller != null + && sourceObject instanceof Card + && controller.moveCards((Card) sourceObject, Zone.HAND, source, game); } - } diff --git a/Mage/src/main/java/mage/abilities/effects/common/RollDiceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/RollDiceEffect.java index 62c1fe6a6dd..abd1ac95782 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/RollDiceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/RollDiceEffect.java @@ -1,4 +1,3 @@ - package mage.abilities.effects.common; import mage.MageObject; @@ -12,7 +11,6 @@ import mage.game.Game; import mage.players.Player; /** - * * @author spjspj */ public class RollDiceEffect extends OneShotEffect { @@ -47,7 +45,7 @@ public class RollDiceEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); MageObject mageObject = game.getObject(source.getSourceId()); if (controller != null && mageObject != null) { - controller.rollDice(source, game, numSides); + controller.rollDice(outcome, source, game, numSides); return true; } return false; @@ -58,8 +56,7 @@ public class RollDiceEffect extends OneShotEffect { if (!staticText.isEmpty()) { return staticText; } - StringBuilder sb = new StringBuilder("Roll a " + numSides + " sided dice"); - return sb.toString(); + return "Roll a " + numSides + " sided die"; } @Override diff --git a/Mage/src/main/java/mage/abilities/effects/common/RollDieWithResultTableEffect.java b/Mage/src/main/java/mage/abilities/effects/common/RollDieWithResultTableEffect.java index d2137e358c1..b198f7daf85 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/RollDieWithResultTableEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/RollDieWithResultTableEffect.java @@ -68,7 +68,7 @@ public class RollDieWithResultTableEffect extends OneShotEffect { if (player == null) { return false; } - int result = player.rollDice(source, game, sides) + modifier.calculate(game, source, this); + int result = player.rollDice(outcome, source, game, sides) + modifier.calculate(game, source, this); this.applyResult(result, game, source); return true; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/RollPlanarDieEffect.java b/Mage/src/main/java/mage/abilities/effects/common/RollPlanarDieEffect.java index 6385d094dff..836e5226a57 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/RollPlanarDieEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/RollPlanarDieEffect.java @@ -7,7 +7,7 @@ import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.constants.Outcome; -import mage.constants.PlanarDieRoll; +import mage.constants.PlanarDieRollResult; import mage.constants.Planes; import mage.game.Game; import mage.game.command.CommandObject; @@ -61,8 +61,8 @@ public class RollPlanarDieEffect extends OneShotEffect { Player controller = game.getPlayer(source.getControllerId()); MageObject mageObject = game.getObject(source.getSourceId()); if (controller != null && mageObject != null) { - PlanarDieRoll planarRoll = controller.rollPlanarDie(source, game); - if (planarRoll == PlanarDieRoll.CHAOS_ROLL && chaosEffects != null && chaosTargets != null) { + PlanarDieRollResult planarRoll = controller.rollPlanarDie(outcome, source, game); + if (planarRoll == PlanarDieRollResult.CHAOS_ROLL && chaosEffects != null && chaosTargets != null) { for (int i = 0; i < chaosTargets.size(); i++) { Target target = chaosTargets.get(i); if (target != null) { @@ -95,7 +95,7 @@ public class RollPlanarDieEffect extends OneShotEffect { done = true; } } - } else if (planarRoll == PlanarDieRoll.PLANAR_ROLL) { + } else if (planarRoll == PlanarDieRollResult.PLANAR_ROLL) { // Steps: 1) Remove the last plane and set its effects to discarded for (CommandObject cobject : game.getState().getCommand()) { if (cobject instanceof Plane) { diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java index 81d42fd7c8e..a25e98fd522 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/CastWithoutPayingManaCostEffect.java @@ -15,6 +15,7 @@ import mage.game.Game; import mage.players.Player; import mage.target.Target; import mage.target.common.TargetCardInHand; +import mage.util.CardUtil; import org.apache.log4j.Logger; /** @@ -24,10 +25,14 @@ import org.apache.log4j.Logger; * Allows player to choose to cast as card from hand without paying its mana * cost. *

+ * TODO: this doesn't work correctly with MDFCs or Adventures (see https://github.com/magefree/mage/issues/7742) */ public class CastWithoutPayingManaCostEffect extends OneShotEffect { private final DynamicValue manaCost; + private final FilterCard filter; + private static final FilterCard defaultFilter + = new FilterNonlandCard("card with mana value %mv or less from your hand"); /** * @param maxCost Maximum converted mana cost for this effect to apply to @@ -37,8 +42,13 @@ public class CastWithoutPayingManaCostEffect extends OneShotEffect { } public CastWithoutPayingManaCostEffect(DynamicValue maxCost) { + this(maxCost, defaultFilter); + } + + public CastWithoutPayingManaCostEffect(DynamicValue maxCost, FilterCard filter) { super(Outcome.PlayForFree); this.manaCost = maxCost; + this.filter = filter; this.staticText = "you may cast a spell with mana value " + maxCost + " or less from your hand without paying its mana cost"; } @@ -46,50 +56,54 @@ public class CastWithoutPayingManaCostEffect extends OneShotEffect { public CastWithoutPayingManaCostEffect(final CastWithoutPayingManaCostEffect effect) { super(effect); this.manaCost = effect.manaCost; + this.filter = effect.filter; } @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { return false; } int cmc = manaCost.calculate(game, source, this); - FilterCard filter = new FilterNonlandCard("card with mana value " - + cmc + " or less from your hand"); + FilterCard filter = this.filter.copy(); + filter.setMessage(filter.getMessage().replace("%mv", "" + cmc)); filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, cmc + 1)); - Target target = new TargetCardInHand(filter); - if (target.canChoose(source.getSourceId(), controller.getId(), game) - && controller.chooseUse(Outcome.PlayForFree, "Cast a card with mana value " + cmc - + " or less from your hand without paying its mana cost?", source, game)) { - Card cardToCast = null; - boolean cancel = false; - while (controller.canRespond() - && !cancel) { - if (controller.chooseTarget(Outcome.PlayForFree, target, source, game)) { - cardToCast = game.getCard(target.getFirstTarget()); - if (cardToCast != null) { - if (cardToCast.getSpellAbility() == null) { - Logger.getLogger(CastWithoutPayingManaCostEffect.class).fatal("Card: " - + cardToCast.getName() + " is no land and has no spell ability!"); - cancel = true; - } - if (cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) { - cancel = true; - } + if (!target.canChoose( + source.getSourceId(), controller.getId(), game + ) || !controller.chooseUse( + Outcome.PlayForFree, + "Cast " + CardUtil.addArticle(filter.getMessage()) + + " without paying its mana cost?", source, game + )) { + return true; + } + Card cardToCast = null; + boolean cancel = false; + while (controller.canRespond() + && !cancel) { + if (controller.chooseTarget(Outcome.PlayForFree, target, source, game)) { + cardToCast = game.getCard(target.getFirstTarget()); + if (cardToCast != null) { + if (cardToCast.getSpellAbility() == null) { + Logger.getLogger(CastWithoutPayingManaCostEffect.class).fatal("Card: " + + cardToCast.getName() + " is no land and has no spell ability!"); + cancel = true; + } + if (cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) { + cancel = true; } - } else { - cancel = true; } + } else { + cancel = true; } - if (cardToCast != null) { - game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), Boolean.TRUE); - controller.cast(controller.chooseAbilityForCast(cardToCast, game, true), - game, true, new ApprovingObject(source, game)); - game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), null); - } + } + if (cardToCast != null) { + game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), Boolean.TRUE); + controller.cast(controller.chooseAbilityForCast(cardToCast, game, true), + game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), null); } return true; } diff --git a/Mage/src/main/java/mage/choices/ChoiceImpl.java b/Mage/src/main/java/mage/choices/ChoiceImpl.java index 944f1ad5e2a..b3e837a16d3 100644 --- a/Mage/src/main/java/mage/choices/ChoiceImpl.java +++ b/Mage/src/main/java/mage/choices/ChoiceImpl.java @@ -255,8 +255,10 @@ public class ChoiceImpl implements Choice { for (String needChoice : answers) { for (Map.Entry currentChoice : this.getKeyChoices().entrySet()) { if (currentChoice.getKey().equals(needChoice)) { - this.setChoiceByKey(needChoice, false); - answers.remove(needChoice); + if (removeSelectAnswerFromList) { + this.setChoiceByKey(needChoice, false); + answers.remove(needChoice); + } return true; } @@ -266,8 +268,10 @@ public class ChoiceImpl implements Choice { for (String needChoice : answers) { for (Map.Entry currentChoice : this.getKeyChoices().entrySet()) { if (currentChoice.getValue().startsWith(needChoice)) { - this.setChoiceByKey(currentChoice.getKey(), false); - answers.remove(needChoice); + if (removeSelectAnswerFromList) { + this.setChoiceByKey(currentChoice.getKey(), false); + answers.remove(needChoice); + } return true; } } @@ -277,8 +281,10 @@ public class ChoiceImpl implements Choice { for (String needChoice : answers) { for (String currentChoice : this.getChoices()) { if (currentChoice.equals(needChoice)) { - this.setChoice(needChoice, false); - answers.remove(needChoice); + if (removeSelectAnswerFromList) { + this.setChoice(needChoice, false); + answers.remove(needChoice); + } return true; } } diff --git a/Mage/src/main/java/mage/constants/PlanarDieRoll.java b/Mage/src/main/java/mage/constants/PlanarDieRoll.java deleted file mode 100644 index db3ecfa274e..00000000000 --- a/Mage/src/main/java/mage/constants/PlanarDieRoll.java +++ /dev/null @@ -1,23 +0,0 @@ -package mage.constants; - -/** - * - * @author spjspj - */ -public enum PlanarDieRoll { - NIL_ROLL("Blank Roll"), - CHAOS_ROLL("Chaos Roll"), - PLANAR_ROLL("Planar Roll"); - - private final String text; - - PlanarDieRoll(String text) { - this.text = text; - } - - @Override - public String toString() { - return text; - } - -} diff --git a/Mage/src/main/java/mage/constants/PlanarDieRollResult.java b/Mage/src/main/java/mage/constants/PlanarDieRollResult.java new file mode 100644 index 00000000000..761c28e75f4 --- /dev/null +++ b/Mage/src/main/java/mage/constants/PlanarDieRollResult.java @@ -0,0 +1,29 @@ +package mage.constants; + +/** + * + * @author spjspj + */ +public enum PlanarDieRollResult { + + BLANK_ROLL("Blank Roll", 0), + CHAOS_ROLL("Chaos Roll", 2), + PLANAR_ROLL("Planar Roll", 1); + + private final String text; + private final int aiPriority; // priority for AI usage (0 - lower, 2 - higher) + + PlanarDieRollResult(String text, int aiPriority) { + this.text = text; + this.aiPriority = aiPriority; + } + + @Override + public String toString() { + return text; + } + + public int getAIPriority() { + return aiPriority; + } +} diff --git a/Mage/src/main/java/mage/constants/RollDieType.java b/Mage/src/main/java/mage/constants/RollDieType.java new file mode 100644 index 00000000000..bb8d808ecc5 --- /dev/null +++ b/Mage/src/main/java/mage/constants/RollDieType.java @@ -0,0 +1,11 @@ +package mage.constants; + +/** + * @author JayDi85 + */ +public enum RollDieType { + + NUMERICAL, + PLANAR + +} diff --git a/Mage/src/main/java/mage/game/GameOptions.java b/Mage/src/main/java/mage/game/GameOptions.java index f3e7c6d4752..918b05fd6c9 100644 --- a/Mage/src/main/java/mage/game/GameOptions.java +++ b/Mage/src/main/java/mage/game/GameOptions.java @@ -52,10 +52,14 @@ public class GameOptions implements Serializable, Copyable { */ public Set bannedUsers = Collections.emptySet(); - /** - * Use planechase variant - */ + + // PLANECHASE game mode public boolean planeChase = false; + // xmage uses increased by 1/3 chances (2/2/9) for chaos/planar result, see 1a9f12f5767ce0beeed26a8ff5c8a8f9490c9c47 + // if you need combo support with 6-sides rolls then it can be reset to original values + public static final int PLANECHASE_PLANAR_DIE_CHAOS_SIDES = 2; // original: 1 + public static final int PLANECHASE_PLANAR_DIE_PLANAR_SIDES = 2; // original: 1 + public static final int PLANECHASE_PLANAR_DIE_TOTAL_SIDES = 9; // original: 6 public GameOptions() { super(); diff --git a/Mage/src/main/java/mage/game/events/DiceRolledEvent.java b/Mage/src/main/java/mage/game/events/DiceRolledEvent.java new file mode 100644 index 00000000000..1c4a0b51fd1 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/DiceRolledEvent.java @@ -0,0 +1,29 @@ +package mage.game.events; + +import mage.abilities.Ability; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author TheElk801 + */ +public class DiceRolledEvent extends GameEvent { + + private final int sides; + private final List results = new ArrayList<>(); // Integer for numerical and PlanarDieRollResult for planar + + public DiceRolledEvent(int sides, List results, Ability source) { + super(EventType.DICE_ROLLED, source.getControllerId(), source, source.getControllerId()); + this.sides = sides; + this.results.addAll(results); + } + + public int getSides() { + return sides; + } + + public List getResults() { + return results; + } +} diff --git a/Mage/src/main/java/mage/game/events/DieRolledEvent.java b/Mage/src/main/java/mage/game/events/DieRolledEvent.java new file mode 100644 index 00000000000..d299ff4b9d5 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/DieRolledEvent.java @@ -0,0 +1,50 @@ +package mage.game.events; + +import mage.abilities.Ability; +import mage.constants.PlanarDieRollResult; +import mage.constants.RollDieType; + +/** + * @author TheElk801, JayDi85 + */ +public class DieRolledEvent extends GameEvent { + + // 706.2. + // After the roll, the number indicated on the top face of the die before any modifiers is + // the natural result. The instruction may include modifiers to the roll which add to or + // subtract from the natural result. Modifiers may also come from other sources. After + // considering all applicable modifiers, the final number is the result of the die roll. + + private final RollDieType rollDieType; + private final int sides; + private final int naturalResult; // planar die returns 0 values in result and natural result + private final PlanarDieRollResult planarResult; + + public DieRolledEvent(Ability source, RollDieType rollDieType, int sides, int naturalResult, int modifier, PlanarDieRollResult planarResult) { + super(EventType.DIE_ROLLED, source.getControllerId(), source, source.getControllerId(), naturalResult + modifier, false); + this.rollDieType = rollDieType; + this.sides = sides; + this.naturalResult = naturalResult; + this.planarResult = planarResult; + } + + public RollDieType getRollDieType() { + return rollDieType; + } + + public int getSides() { + return sides; + } + + public int getResult() { + return amount; + } + + public int getNaturalResult() { + return naturalResult; + } + + public PlanarDieRollResult getPlanarResult() { + return planarResult; + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 82a5db6b452..2c2554fe738 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -296,8 +296,9 @@ public class GameEvent implements Serializable { SURVEIL, SURVEILED, FATESEALED, FLIP_COIN, COIN_FLIPPED, + REPLACE_ROLLED_DIE, // for Clam-I-Am workaround only + ROLL_DIE, DIE_ROLLED, ROLL_DICE, DICE_ROLLED, - ROLL_PLANAR_DIE, PLANAR_DIE_ROLLED, PLANESWALK, PLANESWALKED, PAID_CUMULATIVE_UPKEEP, DIDNT_PAY_CUMULATIVE_UPKEEP, @@ -621,7 +622,7 @@ public class GameEvent implements Serializable { /** * used to store which replacement effects were already applied to an event - * or or any modified events that may replace it + * or any modified events that may replace it *

* 614.5. A replacement effect doesn't invoke itself repeatedly; it gets * only one opportunity to affect an event or any modified events that may diff --git a/Mage/src/main/java/mage/game/events/RollDiceEvent.java b/Mage/src/main/java/mage/game/events/RollDiceEvent.java new file mode 100644 index 00000000000..ce45e477ac3 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/RollDiceEvent.java @@ -0,0 +1,41 @@ +package mage.game.events; + +import mage.abilities.Ability; +import mage.constants.RollDieType; +import mage.util.CardUtil; + +/** + * @author TheElk801 + */ +public class RollDiceEvent extends GameEvent { + + private final int sides; + private int ignoreLowestAmount = 0; // ignore the lowest results + private final RollDieType rollDieType; + + public RollDiceEvent(Ability source, RollDieType rollDieType, int sides, int rollsAmount) { + super(EventType.ROLL_DICE, source.getControllerId(), source, source.getControllerId(), rollsAmount, false); + this.sides = sides; + this.rollDieType = rollDieType; + } + + public int getSides() { + return sides; + } + + public RollDieType getRollDieType() { + return rollDieType; + } + + public void incAmount(int additionalAmount) { + this.amount = CardUtil.overflowInc(this.amount, additionalAmount); + } + + public void incIgnoreLowestAmount(int additionalCount) { + this.ignoreLowestAmount = CardUtil.overflowInc(this.ignoreLowestAmount, additionalCount); + } + + public int getIgnoreLowestAmount() { + return ignoreLowestAmount; + } +} diff --git a/Mage/src/main/java/mage/game/events/RollDieEvent.java b/Mage/src/main/java/mage/game/events/RollDieEvent.java new file mode 100644 index 00000000000..8dee325adc1 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/RollDieEvent.java @@ -0,0 +1,56 @@ +package mage.game.events; + +import mage.abilities.Ability; +import mage.constants.RollDieType; +import mage.util.CardUtil; + +/** + * @author TheElk801 + */ +public class RollDieEvent extends GameEvent { + + private final RollDieType rollDieType; + private final int sides; + + private int resultModifier = 0; + private int rollsAmount = 1; // rolls X times and choose result from it + private int bigIdeaRollsAmount = 0; // rolls 2x and sum result + + public RollDieEvent(Ability source, RollDieType rollDieType, int sides) { + super(EventType.ROLL_DIE, source.getControllerId(), source, source.getControllerId()); + this.rollDieType = rollDieType; + this.sides = sides; + } + + public int getResultModifier() { + return resultModifier; + } + + public void incResultModifier(int modifier) { + this.resultModifier = CardUtil.overflowInc(this.resultModifier, modifier); + } + + public RollDieType getRollDieType() { + return rollDieType; + } + + public int getSides() { + return sides; + } + + public int getRollsAmount() { + return rollsAmount; + } + + public void doubleRollsAmount() { + this.rollsAmount = CardUtil.overflowMultiply(this.rollsAmount, 2); + } + + public int getBigIdeaRollsAmount() { + return bigIdeaRollsAmount; + } + + public void incBigIdeaRollsAmount() { + this.bigIdeaRollsAmount = CardUtil.overflowInc(this.bigIdeaRollsAmount, 1); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/token/FaerieDragonToken.java b/Mage/src/main/java/mage/game/permanent/token/FaerieDragonToken.java new file mode 100644 index 00000000000..379164ea695 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/FaerieDragonToken.java @@ -0,0 +1,33 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.keyword.FlyingAbility; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * + * @author weirddan455 + */ +public class FaerieDragonToken extends TokenImpl { + + public FaerieDragonToken() { + super("Faerie Dragon", "1/1 blue Faerie Dragon creature token with flying"); + cardType.add(CardType.CREATURE); + color.setBlue(true); + subtype.add(SubType.FAERIE); + subtype.add(SubType.DRAGON); + power = new MageInt(1); + toughness = new MageInt(1); + this.addAbility(FlyingAbility.getInstance()); + } + + private FaerieDragonToken(final FaerieDragonToken token) { + super(token); + } + + @Override + public FaerieDragonToken copy() { + return new FaerieDragonToken(this); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/token/VrondissRageOfAncientsToken.java b/Mage/src/main/java/mage/game/permanent/token/VrondissRageOfAncientsToken.java new file mode 100644 index 00000000000..04d71db2ee3 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/VrondissRageOfAncientsToken.java @@ -0,0 +1,65 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.SacrificeSourceEffect; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +public final class VrondissRageOfAncientsToken extends TokenImpl { + + public VrondissRageOfAncientsToken() { + super("Dragon Spirit", "5/4 red and green Dragon Spirit creature token with \"When this creature deals damage, sacrifice it.\""); + cardType.add(CardType.CREATURE); + color.setRed(true); + color.setGreen(true); + subtype.add(SubType.DRAGON); + subtype.add(SubType.SPIRIT); + power = new MageInt(5); + toughness = new MageInt(4); + this.addAbility(new VrondissRageOfAncientsTokenTriggeredAbility()); + } + + public VrondissRageOfAncientsToken(final VrondissRageOfAncientsToken token) { + super(token); + } + + public VrondissRageOfAncientsToken copy() { + return new VrondissRageOfAncientsToken(this); + } +} + +class VrondissRageOfAncientsTokenTriggeredAbility extends TriggeredAbilityImpl { + + public VrondissRageOfAncientsTokenTriggeredAbility() { + super(Zone.BATTLEFIELD, new SacrificeSourceEffect(), false); + } + + public VrondissRageOfAncientsTokenTriggeredAbility(final VrondissRageOfAncientsTokenTriggeredAbility ability) { + super(ability); + } + + @Override + public VrondissRageOfAncientsTokenTriggeredAbility copy() { + return new VrondissRageOfAncientsTokenTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER + || event.getType() == GameEvent.EventType.DAMAGED_PERMANENT; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getSourceId().equals(this.getSourceId()); + } + + @Override + public String getRule() { + return "When this creature deals damage, sacrifice it."; + } +} diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 08974e82a7c..35adef2c317 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -24,10 +24,7 @@ import mage.designations.DesignationType; import mage.filter.FilterCard; import mage.filter.FilterMana; import mage.filter.FilterPermanent; -import mage.game.Game; -import mage.game.GameState; -import mage.game.Graveyard; -import mage.game.Table; +import mage.game.*; import mage.game.combat.CombatGroup; import mage.game.draft.Draft; import mage.game.events.GameEvent; @@ -487,19 +484,21 @@ public interface Player extends MageItem, Copyable { boolean flipCoin(Ability source, Game game, boolean winnable); - boolean flipCoin(Ability source, Game game, boolean winnable, List appliedEffects); - boolean flipCoinResult(Game game); - int rollDice(Ability source, Game game, int numSides); + default int rollDice(Outcome outcome, Ability source, Game game, int numSides) { + return rollDice(outcome, source, game, numSides, 1, 0).stream().findFirst().orElse(0); + } - int rollDice(Ability source, Game game, List appliedEffects, int numSides); + List rollDice(Outcome outcome, Ability source, Game game, int numSides, int numDice, int ignoreLowestAmount); - PlanarDieRoll rollPlanarDie(Ability source, Game game); + int rollDieResult(int sides, Game game); - PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects); + default PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game) { + return rollPlanarDie(outcome, source, game, GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES, GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES); + } - PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects, int numberChaosSides, int numberPlanarSides); + PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int numberChaosSides, int numberPlanarSides); Card discardOne(boolean random, boolean payForCost, Ability source, Game game); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 6e1e60d2b3a..d1c9094341c 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -22,6 +22,8 @@ import mage.abilities.mana.ManaOptions; import mage.actions.MageDrawAction; import mage.cards.*; import mage.cards.decks.Deck; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; import mage.constants.*; import mage.counters.Counter; import mage.counters.CounterType; @@ -2438,7 +2440,7 @@ public abstract class PlayerImpl implements Player, Serializable { userData.resetRequestedHandPlayersList(game.getId()); // users can send request again break; } - logger.trace("PASS Priority: " + playerAction.toString()); + logger.trace("PASS Priority: " + playerAction); } @Override @@ -2786,21 +2788,15 @@ public abstract class PlayerImpl implements Player, Serializable { return casted; } - @Override - public boolean flipCoin(Ability source, Game game, boolean winnable) { - return this.flipCoin(source, game, winnable, null); - } - /** * @param source * @param game * @param winnable - * @param appliedEffects * @return if winnable, true if player won the toss, if not winnable, true * for heads and false for tails */ @Override - public boolean flipCoin(Ability source, Game game, boolean winnable, List appliedEffects) { + public boolean flipCoin(Ability source, Game game, boolean winnable) { boolean chosen = false; if (winnable) { chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game); @@ -2808,7 +2804,6 @@ public abstract class PlayerImpl implements Player, Serializable { } boolean result = this.flipCoinResult(game); FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable); - event.addAppliedEffects(appliedEffects); game.replaceEvent(event); game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult()) + CardUtil.getSourceLogName(game, source)); @@ -2835,7 +2830,6 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip" + CardUtil.getSourceLogName(game, source)); } - event.setAppliedEffects(appliedEffects); game.fireEvent(event.createFlippedEvent()); if (event.isWinnable()) { return event.getResult() == event.getChosen(); @@ -2853,85 +2847,334 @@ public abstract class PlayerImpl implements Player, Serializable { return RandomUtil.nextBoolean(); } + private static final class RollDieResult { + + // 706.2. + // After the roll, the number indicated on the top face of the die before any modifiers is + // the natural result. The instruction may include modifiers to the roll which add to or + // subtract from the natural result. Modifiers may also come from other sources. After + // considering all applicable modifiers, the final number is the result of the die roll. + private final int naturalResult; + private final int modifier; + private final PlanarDieRollResult planarResult; + + RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) { + this.naturalResult = naturalResult; + this.modifier = modifier; + this.planarResult = planarResult; + } + + public int getResult() { + return this.naturalResult + this.modifier; + } + + public PlanarDieRollResult getPlanarResult() { + return this.planarResult; + } + } + @Override - public int rollDice(Ability source, Game game, int numSides) { - return this.rollDice(source, game, null, numSides); + public int rollDieResult(int sides, Game game) { + return RandomUtil.nextInt(sides) + 1; + } + + /** + * Roll single die. Support both die types: planar and numerical. + * + * @param outcome + * @param game + * @param source + * @param rollDieType + * @param sidesAmount + * @param chaosSidesAmount + * @param planarSidesAmount + * @param rollsAmount + * @return + */ + private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) { + if (rollsAmount == 1) { + return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount); + } + Set choices = new HashSet<>(); + for (int j = 0; j < rollsAmount; j++) { + choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount)); + } + if (choices.size() == 1) { + return choices.stream().findFirst().orElse(0); + } + + // AI hint - use max/min values + if (this.isComputer()) { + if (rollDieType == RollDieType.NUMERICAL) { + // numerical + if (outcome.isGood()) { + return choices.stream() + .map(Integer.class::cast) + .max(Comparator.naturalOrder()) + .orElse(null); + } else { + return choices.stream() + .map(Integer.class::cast) + .min(Comparator.naturalOrder()) + .orElse(null); + } + } else { + // planar + // priority: chaos -> planar -> blank + return choices.stream() + .map(PlanarDieRollResult.class::cast) + .max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority)) + .orElse(null); + } + } + + Choice choice = new ChoiceImpl(true); + choice.setMessage("Choose which die roll result to keep (the rest will be ignored)"); + choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet())); + + this.choose(Outcome.Neutral, choice, game); + Object defaultChoice = choices.iterator().next(); + return choices.stream() + .filter(o -> o.toString().equals(choice.getChoice())) + .findFirst() + .orElse(defaultChoice); + } + + private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) { + switch (rollDieType) { + + case NUMERICAL: { + int result = rollDieResult(numSides, game); + // Clam-I-Am workaround: + // If you roll a 3 on a six-sided die, you may reroll that die. + if (numSides == 6 + && result == 3 + && game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId())) + && chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) { + result = rollDieResult(numSides, game); + } + return result; + } + + case PLANAR: { + if (numChaosSides + numPlanarSides > numSides) { + numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES; + numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES; + } + // for 9 sides: + // 1..2 - chaos + // 3..7 - blank + // 8..9 - planar + int result = this.rollDieResult(numSides, game); + PlanarDieRollResult roll; + if (result <= numChaosSides) { + roll = PlanarDieRollResult.CHAOS_ROLL; + } else if (result > numSides - numPlanarSides) { + roll = PlanarDieRollResult.PLANAR_ROLL; + } else { + roll = PlanarDieRollResult.BLANK_ROLL; + } + return roll; + } + + default: { + throw new IllegalArgumentException("Unknown roll die type " + rollDieType); + } + } + } + + /** + * @param outcome + * @param source + * @param game + * @param sidesAmount number of sides the dice has + * @param rollsAmount number of tries to roll the dice + * @param ignoreLowestAmount remove the lowest rolls from the results + * @return the number that the player rolled + */ + @Override + public List rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) { + return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount) + .stream() + .map(Integer.class::cast) + .collect(Collectors.toList()); + } + + /** + * Inner code to roll a dice. Support normal and planar types. + * + * @param outcome + * @param source + * @param game + * @param rollDieType die type to roll, e.g. planar or numerical + * @param sidesAmount sides per die + * @param chaosSidesAmount for planar die: chaos sides + * @param planarSidesAmount for planar die: planar sides + * @param rollsAmount rolls + * @param ignoreLowestAmount for numerical die: ignore multiple rolls with the lowest values + * @return + */ + private List rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType, + int sidesAmount, int chaosSidesAmount, int planarSidesAmount, + int rollsAmount, int ignoreLowestAmount) { + RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount); + if (ignoreLowestAmount > 0) { + rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount); + } + game.replaceEvent(rollDiceEvent); + + // 706.6. + // In a Planechase game, rolling the planar die will cause any ability that triggers whenever a + // player rolls one or more dice to trigger. However, any effect that refers to a numerical + // result of a die roll, including ones that compare the results of that roll to other rolls + // or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.” + + // ROLL MULTIPLE dies + // results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x) + List dieResults = new ArrayList<>(); + List dieRolls = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getAmount(); i++) { + // ROLL SINGLE die + RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides()); + game.replaceEvent(rollDieEvent); + + Object rollResult; + // big idea logic for numerical rolls only + if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) { + // rolls 2x + sum results + // The Big Idea: roll two six-sided dice and use the total of those results + int totalSum = 0; + for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) { + int singleResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount()); + totalSum += singleResult; + dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null)); + } + rollResult = totalSum; + } else { + // rolls 1x + switch (rollDieEvent.getRollDieType()) { + default: + case NUMERICAL: { + int naturalResult = (Integer) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null)); + rollResult = naturalResult; + break; + } + + case PLANAR: { + PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner( + outcome, + game, + source, + rollDieEvent.getRollDieType(), + rollDieEvent.getSides(), + chaosSidesAmount, + planarSidesAmount, + rollDieEvent.getRollsAmount() + ); + dieRolls.add(new RollDieResult(0, 0, planarResult)); + rollResult = planarResult; + break; + } + } + } + dieResults.add(rollResult); + } + + // ignore the lowest results + // planar dies: due to 706.6. planar die results must be fully ignored + // + // 706.5. + // If a player is instructed to roll two or more dice and ignore the lowest roll, the roll + // that yielded the lowest result is considered to have never happened. No abilities trigger + // because of the ignored roll, and no effects apply to that roll. If multiple results are tied + // for the lowest, the player chooses one of those rolls to be ignored. + if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) { + // find ignored values + List ignoredResults = new ArrayList<>(); + for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) { + int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0); + dieResults.remove(Integer.valueOf(min)); + ignoredResults.add(min); + } + // remove ignored rolls (they not exist anymore) + List newRolls = new ArrayList<>(); + for (RollDieResult rollDieResult : dieRolls) { + if (ignoredResults.contains(rollDieResult.getResult())) { + ignoredResults.remove((Integer) rollDieResult.getResult()); + } else { + newRolls.add(rollDieResult); + } + } + dieRolls.clear(); + dieRolls.addAll(newRolls); + } + + // raise affected roll events + for (RollDieResult result : dieRolls) { + game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult)); + } + game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source)); + + String message; + switch (rollDiceEvent.getRollDieType()) { + default: + case NUMERICAL: + // [Roll a die] user rolled 2x d6 and got [1, 4] (source: xxx) + message = String.format("[Roll a die] %s rolled %s %s and got [%s]%s", + getLogName(), + (dieResults.size() > 1 ? dieResults.size() + "x" : "a"), + "d" + rollDiceEvent.getSides(), + dieResults.stream().map(Object::toString).collect(Collectors.joining(", ")), + CardUtil.getSourceLogName(game, source)); + break; + case PLANAR: + // [Roll a planar die] user rolled CHAOS (source: xxx) + message = String.format("[Roll a planar die] %s rolled [%s]%s", + getLogName(), + dieResults.stream().map(Object::toString).collect(Collectors.joining(", ")), + CardUtil.getSourceLogName(game, source)); + break; + } + game.informPlayers(message); + return dieResults; } /** * @param source * @param game - * @param appliedEffects - * @param numSides Number of sides the dice has - * @return the number that the player rolled - */ - @Override - public int rollDice(Ability source, Game game, List appliedEffects, int numSides) { - int result = RandomUtil.nextInt(numSides) + 1; - if (!game.isSimulation()) { - game.informPlayers("[Roll a die] " + getLogName() + " rolled a " - + result + " on a " + numSides + " sided die" + CardUtil.getSourceLogName(game, source)); - } - GameEvent event = new GameEvent(GameEvent.EventType.ROLL_DICE, playerId, source, playerId, result, true); - event.setAppliedEffects(appliedEffects); - event.setAmount(result); - event.setData(numSides + ""); - if (!game.replaceEvent(event)) { - GameEvent ge = new GameEvent(GameEvent.EventType.DICE_ROLLED, playerId, source, playerId, event.getAmount(), event.getFlag()); - ge.setData(numSides + ""); - game.fireEvent(ge); - } - return event.getAmount(); - } - - @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game) { - return this.rollPlanarDie(source, game, null); - } - - @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects) { - return rollPlanarDie(source, game, appliedEffects, 2, 2); - } - - /** - * @param game - * @param appliedEffects - * @param numberChaosSides The number of chaos sides the planar die + * @param chaosSidesAmount The number of chaos sides the planar die * currently has (normally 1 but can be 5) - * @param numberPlanarSides The number of chaos sides the planar die + * @param planarSidesAmount The number of chaos sides the planar die * currently has (normally 1) * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll - * or NilRoll + * or BlankRoll */ @Override - public PlanarDieRoll rollPlanarDie(Ability source, Game game, List appliedEffects, int numberChaosSides, int numberPlanarSides) { - int result = RandomUtil.nextInt(9) + 1; - PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; - if (numberChaosSides + numberPlanarSides > 9) { - numberChaosSides = 2; - numberPlanarSides = 2; - } - if (result <= numberChaosSides) { - roll = PlanarDieRoll.CHAOS_ROLL; - } else if (result > 9 - numberPlanarSides) { - roll = PlanarDieRoll.PLANAR_ROLL; - } - if (!game.isSimulation()) { - game.informPlayers("[Roll the planar die] " + getLogName() - + " rolled a " + roll + " on the planar die" + CardUtil.getSourceLogName(game, source)); - } - GameEvent event = new GameEvent(GameEvent.EventType.ROLL_PLANAR_DIE, - playerId, source, playerId, result, true); - event.setAppliedEffects(appliedEffects); - event.setData(roll + ""); - if (!game.replaceEvent(event)) { - GameEvent ge = new GameEvent(GameEvent.EventType.PLANAR_DIE_ROLLED, - playerId, source, playerId, event.getAmount(), event.getFlag()); - ge.setData(roll + ""); - game.fireEvent(ge); - } - return roll; + public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) { + return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0) + .stream() + .map(o -> (PlanarDieRollResult) o) + .findFirst() + .orElse(PlanarDieRollResult.BLANK_ROLL); } @Override @@ -3539,15 +3782,12 @@ public abstract class PlayerImpl implements Player, Serializable { boolean canActivateAsHandZone = approvingObject != null || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); - boolean possibleToPlay = false; + boolean possibleToPlay = canActivateAsHandZone + && ability.getZone().match(Zone.HAND) + && (isPlaySpell || isPlayLand); // spell/hand abilities (play from all zones) // need permitingObject or canPlayCardsFromGraveyard - if (canActivateAsHandZone - && ability.getZone().match(Zone.HAND) - && (isPlaySpell || isPlayLand)) { - possibleToPlay = true; - } // zone's abilities (play from specific zone) // no need in permitingObject @@ -4275,7 +4515,7 @@ public abstract class PlayerImpl implements Player, Serializable { } break; default: - throw new UnsupportedOperationException("to Zone" + toZone.toString() + " not supported yet"); + throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet"); } return !successfulMovedCards.isEmpty(); } diff --git a/Mage/src/main/java/mage/watchers/common/PlanarRollWatcher.java b/Mage/src/main/java/mage/watchers/common/PlanarRollWatcher.java index 37919138b28..65714bfc3a4 100644 --- a/Mage/src/main/java/mage/watchers/common/PlanarRollWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/PlanarRollWatcher.java @@ -1,7 +1,9 @@ package mage.watchers.common; +import mage.constants.RollDieType; import mage.constants.WatcherScope; import mage.game.Game; +import mage.game.events.DieRolledEvent; import mage.game.events.GameEvent; import mage.watchers.Watcher; @@ -25,9 +27,10 @@ public class PlanarRollWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.PLANAR_DIE_ROLLED) { - UUID playerId = event.getPlayerId(); - if (playerId != null) { + if (event.getType() == GameEvent.EventType.DIE_ROLLED) { + DieRolledEvent drEvent = (DieRolledEvent) event; + UUID playerId = drEvent.getPlayerId(); + if (playerId != null && drEvent.getRollDieType() == RollDieType.PLANAR) { Integer amount = numberTimesPlanarDieRolled.get(playerId); if (amount == null) { amount = 1;