Adding dice rolling trigger/replacement (ready for review) (#7989)

* [AFR] Implemented DiceRolledTriggeredAbility
* [AFR] Implemented Brazen Dwarf
* [AFR] Implemented Feywild Trickster
* [AFC] Implemented Reckless Endeavor
* [AFR] Implemented Pixie Guide
* [AFR] Implemented Critical Hit
* [AFR] Implemented Netherese Puzzle Ward
* [AFC] Implemented Neverwinter Hydra
* [AFR] Implemented Farideh, Devil's Chosen
* [AFR] Implemented Barbarian Class
* [AFC] Implemented Vrondiss, Rage of Ancients
* [AFC] Implemented Arcane Endeavor
* Test framework: added planar die rolls support
* Test framework: added random results set up support in AI simulated games;
* AI: improved roll die results chooses in computer games;
* Roll die: improved combo support for planar die and roll die effects;

Co-authored-by: Daniel Bomar <dbdaniel42@gmail.com>
Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
Evan Kranzler 2021-08-26 06:06:10 -04:00 committed by GitHub
parent 12219cff01
commit f8d030bef4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
97 changed files with 2641 additions and 553 deletions

View file

@ -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<String> suggested;
private transient ConcurrentLinkedQueue<Ability> 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<String> 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);
}
}

View file

@ -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<Integer> 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;
}
}

View file

@ -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;

View file

@ -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)

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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<Integer> 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

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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.";
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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 "

View file

@ -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<Integer> 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;
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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<Integer> 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);
}

View file

@ -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<Integer> 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);
}

View file

@ -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<Integer> 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());

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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<UUID> appliedEffects = (ArrayList<UUID>) 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;
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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);

View file

@ -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<UUID> appliedEffects = (ArrayList<UUID>) 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;
}

View file

@ -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

View file

@ -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);

View file

@ -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);
}

View file

@ -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<Mana> 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;
}
}

View file

@ -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);

View file

@ -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.";
}
}

View file

@ -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<UUID> appliedEffects = (ArrayList<UUID>) this.getValue("appliedEffects");
permanent.addCounters(CounterType.P1P1.createInstance(amount), source.getControllerId(), source, game, appliedEffects);
return true;
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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<Integer> 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;
}
}

View file

@ -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<Player, Integer> mapEntry : new HashSet<>(playerRolls.entrySet())) {

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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;
}

View file

@ -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<Permanent> artifactPermanents = game.getBattlefield().getActivePermanents(new FilterArtifactPermanent(), controller.getId(), game);

View file

@ -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);
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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());
}
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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

View file

@ -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));

View file

@ -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));

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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.
* <p>
* 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.
* <p>
* If you catch overflow error with new method then check strict mode in it.
* <p>
* 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
* <p>
* Don't forget to add new methods in another classes like TestComputerPlayer7 or TestComputerPlayerMonteCarlo
* <p>
* 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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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<String, UUID> aliases = new HashMap<>(); // aliases for game objects/players (use it for cards with same name to save and use)
private final List<String> 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<UUID> 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<Integer> 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<UUID> 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<UUID> 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<UUID> 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;
}
}

View file

@ -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
*

View file

@ -635,23 +635,18 @@ public class PlayerStub implements Player {
return false;
}
@Override
public boolean flipCoin(Ability source, Game game, boolean winnable, List<UUID> 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<Integer> rollDice(Outcome outcome, Ability source, Game game, int numSides, int numDice, int ignoreLowestAmount) {
return null;
}
@Override
public int rollDice(Ability source, Game game, List<UUID> 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<UUID> 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<UUID> 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.
}

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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;
}

View file

@ -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) {

View file

@ -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.
* </p>
* 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;
}

View file

@ -255,8 +255,10 @@ public class ChoiceImpl implements Choice {
for (String needChoice : answers) {
for (Map.Entry<String, String> 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<String, String> 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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,11 @@
package mage.constants;
/**
* @author JayDi85
*/
public enum RollDieType {
NUMERICAL,
PLANAR
}

View file

@ -52,10 +52,14 @@ public class GameOptions implements Serializable, Copyable<GameOptions> {
*/
public Set<String> 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();

View file

@ -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<Object> results = new ArrayList<>(); // Integer for numerical and PlanarDieRollResult for planar
public DiceRolledEvent(int sides, List<Object> 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<Object> getResults() {
return results;
}
}

View file

@ -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;
}
}

View file

@ -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
* <p>
* 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

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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.";
}
}

View file

@ -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<Player> {
boolean flipCoin(Ability source, Game game, boolean winnable);
boolean flipCoin(Ability source, Game game, boolean winnable, List<UUID> 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<UUID> appliedEffects, int numSides);
List<Integer> 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<UUID> 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<UUID> 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);

View file

@ -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<UUID> 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<Object> 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<Integer> 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<Object> 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<Object> dieResults = new ArrayList<>();
List<RollDieResult> 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<Integer> 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<RollDieResult> 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<UUID> 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<UUID> 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<UUID> 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();
}

View file

@ -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;