mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
Rework drawing cards and associated replacement effects; implement [WHO] River Song (#12700)
* remove unused scoring system code * add test for Alms Collector replacement effect * flatten draw cards into single method in PlayerImpl * remove outdated MageAction framework * clarify game event for drawing two or more cards * clarify methods for getting cards from library * implement [WHO] River Song * fix error * adjust library methods * add lots of test cases for draw replacement effects * fix #12616 * track cards drawn this way through multi draw replacement as well * add test for River Song * remove redundant comment
This commit is contained in:
parent
34ae226130
commit
9fcbfdeac6
22 changed files with 645 additions and 345 deletions
|
|
@ -309,7 +309,7 @@ public class ComputerPlayerMCTS extends ComputerPlayer {
|
||||||
newPlayer.getHand().clear();
|
newPlayer.getHand().clear();
|
||||||
newPlayer.getLibrary().shuffle();
|
newPlayer.getLibrary().shuffle();
|
||||||
for (int i = 0; i < handSize; i++) {
|
for (int i = 0; i < handSize; i++) {
|
||||||
Card card = newPlayer.getLibrary().removeFromTop(mcts);
|
Card card = newPlayer.getLibrary().drawFromTop(mcts);
|
||||||
card.setZone(Zone.HAND, mcts);
|
card.setZone(Zone.HAND, mcts);
|
||||||
newPlayer.getHand().add(card);
|
newPlayer.getHand().add(card);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import mage.abilities.Ability;
|
import mage.abilities.Ability;
|
||||||
import mage.abilities.ActivatedAbility;
|
|
||||||
import mage.abilities.PlayLandAbility;
|
import mage.abilities.PlayLandAbility;
|
||||||
import mage.abilities.common.PassAbility;
|
import mage.abilities.common.PassAbility;
|
||||||
import mage.cards.Card;
|
import mage.cards.Card;
|
||||||
|
|
@ -270,7 +269,7 @@ public class MCTSNode {
|
||||||
player.getHand().clear();
|
player.getHand().clear();
|
||||||
player.getLibrary().shuffle();
|
player.getLibrary().shuffle();
|
||||||
for (int i = 0; i < handSize; i++) {
|
for (int i = 0; i < handSize; i++) {
|
||||||
Card card = player.getLibrary().removeFromTop(game);
|
Card card = player.getLibrary().drawFromTop(game);
|
||||||
card.setZone(Zone.HAND, game);
|
card.setZone(Zone.HAND, game);
|
||||||
player.getHand().add(card);
|
player.getHand().add(card);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ class AlmsCollectorReplacementEffect extends ReplacementEffectImpl {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean checksEventType(GameEvent event, Game game) {
|
public boolean checksEventType(GameEvent event, Game game) {
|
||||||
return event.getType() == GameEvent.EventType.DRAW_CARDS;
|
return event.getType() == GameEvent.EventType.DRAW_TWO_OR_MORE_CARDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -80,11 +80,7 @@ class BloodScrivenerReplacementEffect extends ReplacementEffectImpl {
|
||||||
public boolean applies(GameEvent event, Ability source, Game game) {
|
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||||
if (event.getPlayerId().equals(source.getControllerId())) {
|
if (event.getPlayerId().equals(source.getControllerId())) {
|
||||||
Player player = game.getPlayer(event.getPlayerId());
|
Player player = game.getPlayer(event.getPlayerId());
|
||||||
if(player != null) {
|
return player != null && player.getHand().isEmpty();
|
||||||
if (player.getHand().isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class CellarDoorEffect extends OneShotEffect {
|
||||||
public boolean apply(Game game, Ability source) {
|
public boolean apply(Game game, Ability source) {
|
||||||
Player player = game.getPlayer(source.getFirstTarget());
|
Player player = game.getPlayer(source.getFirstTarget());
|
||||||
if (player != null && player.getLibrary().hasCards()) {
|
if (player != null && player.getLibrary().hasCards()) {
|
||||||
Card card = player.getLibrary().removeFromBottom(game);
|
Card card = player.getLibrary().getFromBottom(game);
|
||||||
if (card != null) {
|
if (card != null) {
|
||||||
player.moveCards(card, Zone.GRAVEYARD, source, game);
|
player.moveCards(card, Zone.GRAVEYARD, source, game);
|
||||||
if (card.isCreature(game)) {
|
if (card.isCreature(game)) {
|
||||||
|
|
|
||||||
|
|
@ -89,4 +89,4 @@ class LidlessGazeEffect extends OneShotEffect {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
134
Mage.Sets/src/mage/cards/r/RiverSong.java
Normal file
134
Mage.Sets/src/mage/cards/r/RiverSong.java
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
package mage.cards.r;
|
||||||
|
|
||||||
|
import mage.MageInt;
|
||||||
|
import mage.abilities.Ability;
|
||||||
|
import mage.abilities.TriggeredAbility;
|
||||||
|
import mage.abilities.TriggeredAbilityImpl;
|
||||||
|
import mage.abilities.common.SimpleStaticAbility;
|
||||||
|
import mage.abilities.dynamicvalue.common.SourcePermanentPowerCount;
|
||||||
|
import mage.abilities.effects.Effect;
|
||||||
|
import mage.abilities.effects.ReplacementEffectImpl;
|
||||||
|
import mage.abilities.effects.common.DamageTargetEffect;
|
||||||
|
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
|
||||||
|
import mage.cards.CardImpl;
|
||||||
|
import mage.cards.CardSetInfo;
|
||||||
|
import mage.constants.*;
|
||||||
|
import mage.counters.CounterType;
|
||||||
|
import mage.game.Game;
|
||||||
|
import mage.game.events.DrawCardEvent;
|
||||||
|
import mage.game.events.GameEvent;
|
||||||
|
import mage.players.Player;
|
||||||
|
import mage.target.targetpointer.FixedTarget;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author xenohedron
|
||||||
|
*/
|
||||||
|
public final class RiverSong extends CardImpl {
|
||||||
|
|
||||||
|
public RiverSong(UUID ownerId, CardSetInfo setInfo) {
|
||||||
|
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}{R}");
|
||||||
|
|
||||||
|
this.supertype.add(SuperType.LEGENDARY);
|
||||||
|
this.subtype.add(SubType.HUMAN);
|
||||||
|
this.subtype.add(SubType.TIME_LORD);
|
||||||
|
this.subtype.add(SubType.ROGUE);
|
||||||
|
this.power = new MageInt(2);
|
||||||
|
this.toughness = new MageInt(2);
|
||||||
|
|
||||||
|
// Meet in Reverse -- You draw cards from the bottom of your library rather than the top.
|
||||||
|
this.addAbility(new SimpleStaticAbility(new RiverSongDrawFromBottomReplacementEffect())
|
||||||
|
.withFlavorWord("Meet in Reverse"));
|
||||||
|
|
||||||
|
// Spoilers -- Whenever an opponent scries, surveils, or searches their library, put a +1/+1 counter on River Song.
|
||||||
|
// Then River Song deals damage to that player equal to its power.
|
||||||
|
TriggeredAbility trigger = new RiverSongTriggeredAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()));
|
||||||
|
trigger.addEffect(new DamageTargetEffect(new SourcePermanentPowerCount(false))
|
||||||
|
.setText("Then {this} deals damage to that player equal to its power"));
|
||||||
|
this.addAbility(trigger.withFlavorWord("Spoilers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RiverSong(final RiverSong card) {
|
||||||
|
super(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RiverSong copy() {
|
||||||
|
return new RiverSong(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RiverSongDrawFromBottomReplacementEffect extends ReplacementEffectImpl {
|
||||||
|
|
||||||
|
RiverSongDrawFromBottomReplacementEffect() {
|
||||||
|
super(Duration.WhileOnBattlefield, Outcome.Neutral);
|
||||||
|
staticText = "You draw cards from the bottom of your library rather than the top";
|
||||||
|
}
|
||||||
|
|
||||||
|
private RiverSongDrawFromBottomReplacementEffect(final RiverSongDrawFromBottomReplacementEffect effect) {
|
||||||
|
super(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RiverSongDrawFromBottomReplacementEffect copy() {
|
||||||
|
return new RiverSongDrawFromBottomReplacementEffect(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
|
||||||
|
((DrawCardEvent) event).setFromBottom(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checksEventType(GameEvent event, Game game) {
|
||||||
|
return event.getType() == GameEvent.EventType.DRAW_CARD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||||
|
return source.getControllerId().equals(event.getPlayerId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RiverSongTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
|
|
||||||
|
RiverSongTriggeredAbility(Effect effect) {
|
||||||
|
super(Zone.BATTLEFIELD, effect);
|
||||||
|
setTriggerPhrase("Whenever an opponent scries, surveils, or searches their library, ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private RiverSongTriggeredAbility(final RiverSongTriggeredAbility ability) {
|
||||||
|
super(ability);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RiverSongTriggeredAbility copy() {
|
||||||
|
return new RiverSongTriggeredAbility(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkEventType(GameEvent event, Game game) {
|
||||||
|
switch (event.getType()) {
|
||||||
|
case SCRIED:
|
||||||
|
case SURVEILED:
|
||||||
|
case LIBRARY_SEARCHED:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkTrigger(GameEvent event, Game game) {
|
||||||
|
Player controller = game.getPlayer(getControllerId());
|
||||||
|
if (controller != null
|
||||||
|
&& controller.hasOpponent(event.getPlayerId(), game)
|
||||||
|
&& event.getPlayerId().equals(event.getTargetId())) { // searches own library
|
||||||
|
getEffects().setTargetPointer(new FixedTarget(event.getPlayerId()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -178,6 +178,7 @@ public final class DoctorWho extends ExpansionSet {
|
||||||
cards.add(new SetCardInfo("Return the Past", 92, Rarity.RARE, mage.cards.r.ReturnThePast.class));
|
cards.add(new SetCardInfo("Return the Past", 92, Rarity.RARE, mage.cards.r.ReturnThePast.class));
|
||||||
cards.add(new SetCardInfo("Return to Dust", 211, Rarity.UNCOMMON, mage.cards.r.ReturnToDust.class));
|
cards.add(new SetCardInfo("Return to Dust", 211, Rarity.UNCOMMON, mage.cards.r.ReturnToDust.class));
|
||||||
cards.add(new SetCardInfo("Reverse the Polarity", 54, Rarity.RARE, mage.cards.r.ReverseThePolarity.class));
|
cards.add(new SetCardInfo("Reverse the Polarity", 54, Rarity.RARE, mage.cards.r.ReverseThePolarity.class));
|
||||||
|
cards.add(new SetCardInfo("River Song", 152, Rarity.RARE, mage.cards.r.RiverSong.class));
|
||||||
cards.add(new SetCardInfo("River of Tears", 297, Rarity.RARE, mage.cards.r.RiverOfTears.class));
|
cards.add(new SetCardInfo("River of Tears", 297, Rarity.RARE, mage.cards.r.RiverOfTears.class));
|
||||||
cards.add(new SetCardInfo("Rockfall Vale", 298, Rarity.RARE, mage.cards.r.RockfallVale.class));
|
cards.add(new SetCardInfo("Rockfall Vale", 298, Rarity.RARE, mage.cards.r.RockfallVale.class));
|
||||||
cards.add(new SetCardInfo("Rogue's Passage", 299, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class));
|
cards.add(new SetCardInfo("Rogue's Passage", 299, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
package org.mage.test.cards.replacement;
|
package org.mage.test.cards.replacement;
|
||||||
|
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
|
|
@ -8,11 +7,343 @@ import org.junit.Test;
|
||||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @author LevelX2, xenohedron
|
||||||
* @author LevelX2
|
|
||||||
*/
|
*/
|
||||||
public class DrawEffectsTest extends CardTestPlayerBase {
|
public class DrawEffectsTest extends CardTestPlayerBase {
|
||||||
|
|
||||||
|
private static final String drawOne = "Radical Idea"; // 1U instant
|
||||||
|
private static final String drawTwo = "Quick Study"; // 2U instant
|
||||||
|
private static final String drawThree = "Jace's Ingenuity"; // 3UU instant
|
||||||
|
private static final String excavation = "Ancient Excavation"; // 2UB instant
|
||||||
|
// Draw cards equal to the number of cards in your hand, then discard a card for each card drawn this way.
|
||||||
|
|
||||||
|
private static final String reflection = "Thought Reflection";
|
||||||
|
// If you would draw a card, draw two cards instead.
|
||||||
|
private static final String scrivener = "Blood Scrivener";
|
||||||
|
// If you would draw a card while you have no cards in hand, instead you draw two cards and you lose 1 life.
|
||||||
|
private static final String notionThief = "Notion Thief";
|
||||||
|
// If an opponent would draw a card except the first one they draw in each of their draw steps,
|
||||||
|
// instead that player skips that draw and you draw a card.
|
||||||
|
private static final String asmodeus = "Asmodeus the Archfiend";
|
||||||
|
// If you would draw a card, exile the top card of your library face down instead.
|
||||||
|
private static final String almsCollector = "Alms Collector";
|
||||||
|
// If an opponent would draw two or more cards, instead you and that player each draw a card.
|
||||||
|
|
||||||
|
private void testBase(String cardDraw, int handPlayerA, int handPlayerB) {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Island", 5);
|
||||||
|
addCard(Zone.HAND, playerA, cardDraw);
|
||||||
|
addCard(Zone.HAND, playerB, cardDraw);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, cardDraw);
|
||||||
|
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, cardDraw);
|
||||||
|
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, cardDraw, 1);
|
||||||
|
assertGraveyardCount(playerB, cardDraw, 1);
|
||||||
|
assertHandCount(playerA, handPlayerA);
|
||||||
|
assertHandCount(playerB, handPlayerB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSingle(String cardDraw, int handPlayerA, int handPlayerB, String... choices) {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
|
||||||
|
addCard(Zone.HAND, playerA, cardDraw);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, cardDraw);
|
||||||
|
for (String choice : choices) {
|
||||||
|
setChoice(playerA, choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, cardDraw, 1);
|
||||||
|
assertHandCount(playerA, handPlayerA);
|
||||||
|
assertHandCount(playerB, handPlayerB);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflection1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testBase(drawOne, 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflection2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testBase(drawTwo, 4, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflection3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testBase(drawThree, 6, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testScrivener1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, scrivener);
|
||||||
|
testBase(drawOne, 2, 1);
|
||||||
|
assertLife(playerA, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testScrivener2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, scrivener);
|
||||||
|
testBase(drawTwo, 3, 2);
|
||||||
|
assertLife(playerA, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testScrivener3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, scrivener);
|
||||||
|
testBase(drawThree, 4, 3);
|
||||||
|
assertLife(playerA, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Each additional Blood Scrivener you control will effectively add one card and 1 life lost.
|
||||||
|
* Say you control two Blood Scriveners and would draw a card while you have no cards in hand.
|
||||||
|
* The effect of one Blood Scrivener will replace the event “draw a card” with “draw two cards and lose 1 life.”
|
||||||
|
* The effect of the other Blood Scrivener will replace the drawing of the first of those two cards with
|
||||||
|
* “draw two cards and lose 1 life.” You’ll draw two cards and lose 1 life,
|
||||||
|
* then draw another card and lose another 1 life. (2013-04-15)
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoubleScrivener1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, scrivener, 2);
|
||||||
|
testSingle(drawOne, 3, 0, scrivener);
|
||||||
|
assertLife(playerA, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoubleScrivener2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, scrivener, 2);
|
||||||
|
testSingle(drawTwo, 4, 0, scrivener);
|
||||||
|
assertLife(playerA, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoubleScrivener3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, scrivener, 2);
|
||||||
|
testSingle(drawThree, 5, 0, scrivener);
|
||||||
|
assertLife(playerA, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsmodeus1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
testBase(drawOne, 0, 1);
|
||||||
|
assertExileCount(playerA, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsmodeus2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
testBase(drawTwo, 0, 2);
|
||||||
|
assertExileCount(playerA, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsmodeus3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
testBase(drawThree, 0, 3);
|
||||||
|
assertExileCount(playerA, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflectionAsmodeus1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawOne, 0, 0, reflection);
|
||||||
|
assertExileCount(playerA, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflectionAsmodeus2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawTwo, 0, 0, reflection, reflection);
|
||||||
|
assertExileCount(playerA, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflectionAsmodeus3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawThree, 0, 0, reflection, reflection, reflection);
|
||||||
|
assertExileCount(playerA, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsmodeusReflection1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawOne, 0, 0, asmodeus);
|
||||||
|
assertExileCount(playerA, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsmodeusReflection2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawTwo, 0, 0, asmodeus, asmodeus);
|
||||||
|
assertExileCount(playerA, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAsmodeusReflection3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, asmodeus);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawThree, 0, 0, asmodeus, asmodeus, asmodeus);
|
||||||
|
assertExileCount(playerA, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlmsCollectorReflection1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, almsCollector);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawOne, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlmsCollectorReflection2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, almsCollector);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawTwo, 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlmsCollectorReflection3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, almsCollector);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawThree, 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotionThiefReflection1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, notionThief);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawOne, 0, 1, notionThief);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotionThiefReflection2() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, notionThief);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawTwo, 0, 2, notionThief, notionThief);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotionThiefReflection3() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, notionThief);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawThree, 0, 3, notionThief, notionThief, notionThief);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReflectionNotionThief1() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, notionThief);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
testSingle(drawOne, 0, 2, reflection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAncientExcavation() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 4);
|
||||||
|
addCard(Zone.HAND, playerA, excavation);
|
||||||
|
addCard(Zone.HAND, playerA, "Shock");
|
||||||
|
skipInitShuffling();
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Healing Salve");
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Giant Growth");
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, reflection);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, excavation);
|
||||||
|
// 1 card in hand, thought reflection -> draw 2, then discard two
|
||||||
|
setChoice(playerA, "Shock"); // to discard
|
||||||
|
setChoice(playerA, "Healing Salve"); // to discard
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, excavation, 1);
|
||||||
|
assertGraveyardCount(playerA, "Shock", 1);
|
||||||
|
assertGraveyardCount(playerA, "Healing Salve", 1);
|
||||||
|
assertHandCount(playerA, "Giant Growth", 1);
|
||||||
|
assertHandCount(playerA, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAncientExcavationNotionThief() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 4);
|
||||||
|
addCard(Zone.HAND, playerA, excavation);
|
||||||
|
addCard(Zone.HAND, playerA, "Shock");
|
||||||
|
addCard(Zone.HAND, playerA, "Dark Ritual");
|
||||||
|
addCard(Zone.HAND, playerA, "Ornithopter");
|
||||||
|
skipInitShuffling();
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Healing Salve");
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Giant Growth");
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, notionThief);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, excavation);
|
||||||
|
// 3 cards in hand, notion thief -> instead opponent draws three
|
||||||
|
// but cards were still drawn this way, so discard all three (no choice to make)
|
||||||
|
// if this turns out to be incorrect, modify the test accordingly
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, excavation, 1);
|
||||||
|
assertGraveyardCount(playerA, "Shock", 1);
|
||||||
|
assertGraveyardCount(playerA, "Dark Ritual", 1);
|
||||||
|
assertGraveyardCount(playerA, "Ornithopter", 1);
|
||||||
|
assertHandCount(playerA, 0);
|
||||||
|
assertLibraryCount(playerA, "Healing Salve", 1);
|
||||||
|
assertLibraryCount(playerA, "Giant Growth", 1);
|
||||||
|
assertHandCount(playerB, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAncientExcavationAlmsCollector() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 4);
|
||||||
|
addCard(Zone.HAND, playerA, excavation);
|
||||||
|
addCard(Zone.HAND, playerA, "Shock");
|
||||||
|
addCard(Zone.HAND, playerA, "Dark Ritual");
|
||||||
|
addCard(Zone.HAND, playerA, "Ornithopter");
|
||||||
|
skipInitShuffling();
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Healing Salve");
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Giant Growth");
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, almsCollector);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, excavation);
|
||||||
|
// 3 cards in hand, alms collector -> instead each player draws one
|
||||||
|
// interpret as two cards were drawn this way in total
|
||||||
|
// if this turns out to be incorrect, modify the test accordingly
|
||||||
|
setChoice(playerA, "Shock"); // to discard
|
||||||
|
setChoice(playerA, "Giant Growth"); // to discard
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, excavation, 1);
|
||||||
|
assertGraveyardCount(playerA, "Shock", 1);
|
||||||
|
assertGraveyardCount(playerA, "Giant Growth", 1);
|
||||||
|
assertHandCount(playerA, "Dark Ritual", 1);
|
||||||
|
assertHandCount(playerA, "Ornithopter", 1);
|
||||||
|
assertHandCount(playerA, 2);
|
||||||
|
assertLibraryCount(playerA, "Healing Salve", 1);
|
||||||
|
assertHandCount(playerB, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The effects of multiple Thought Reflections are cumulative. For example,
|
* The effects of multiple Thought Reflections are cumulative. For example,
|
||||||
* if you have three Thought Reflections on the battlefield, you'll draw
|
* if you have three Thought Reflections on the battlefield, you'll draw
|
||||||
|
|
@ -104,4 +435,49 @@ public class DrawEffectsTest extends CardTestPlayerBase {
|
||||||
assertPermanentCount(playerA, "Bear Token", 1);
|
assertPermanentCount(playerA, "Bear Token", 1);
|
||||||
assertHandCount(playerA, 1);
|
assertHandCount(playerA, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAlmsCollector() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Alms Collector");
|
||||||
|
// If an opponent would draw two or more cards, instead you and that player each draw a card.
|
||||||
|
|
||||||
|
// Draw two cards.
|
||||||
|
addCard(Zone.HAND, playerA, "Counsel of the Soratami", 1); // Sorcery {2}{U}
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Counsel of the Soratami");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, "Counsel of the Soratami", 1);
|
||||||
|
assertHandCount(playerA, 1);
|
||||||
|
assertHandCount(playerB, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRiverSong() {
|
||||||
|
skipInitShuffling();
|
||||||
|
removeAllCardsFromLibrary(playerA);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Healing Salve"); // bottom
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Giant Growth");
|
||||||
|
addCard(Zone.LIBRARY, playerA, "Shock"); // top
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "River Song");
|
||||||
|
// You draw cards from the bottom of your library rather than the top.
|
||||||
|
addCard(Zone.HAND, playerA, drawOne, 1);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, drawOne);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, drawOne, 1);
|
||||||
|
assertHandCount(playerA, 1);
|
||||||
|
assertHandCount(playerA, "Healing Salve", 1);
|
||||||
|
assertLibraryCount(playerA, "Shock", 1);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package mage.actions;
|
|
||||||
|
|
||||||
import mage.abilities.Ability;
|
|
||||||
import mage.actions.impl.MageAction;
|
|
||||||
import mage.actions.score.ArtificialScoringSystem;
|
|
||||||
import mage.cards.Card;
|
|
||||||
import mage.constants.Zone;
|
|
||||||
import mage.game.Game;
|
|
||||||
import mage.game.events.DrawCardEvent;
|
|
||||||
import mage.game.events.DrawCardsEvent;
|
|
||||||
import mage.game.events.DrewCardEvent;
|
|
||||||
import mage.game.events.GameEvent;
|
|
||||||
import mage.players.Player;
|
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action for drawing cards.
|
|
||||||
*
|
|
||||||
* @author ayrat
|
|
||||||
*/
|
|
||||||
public class MageDrawAction extends MageAction {
|
|
||||||
|
|
||||||
private static final int NEGATIVE_VALUE = -1000000;
|
|
||||||
|
|
||||||
private final Player player;
|
|
||||||
private final List<Card> drawnCards;
|
|
||||||
private final GameEvent originalDrawEvent; // for replace effects
|
|
||||||
|
|
||||||
private int amount;
|
|
||||||
|
|
||||||
public MageDrawAction(Player player, int amount, GameEvent originalDrawEvent) {
|
|
||||||
this.player = player;
|
|
||||||
this.amount = amount;
|
|
||||||
this.drawnCards = new ArrayList<>();
|
|
||||||
this.originalDrawEvent = originalDrawEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw and set action score.
|
|
||||||
*
|
|
||||||
* @param source
|
|
||||||
* @param game Game context.
|
|
||||||
* @return Number of cards drawn
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int doAction(Ability source, Game game) {
|
|
||||||
int numDrawn = 0;
|
|
||||||
int score = 0;
|
|
||||||
GameEvent event = new DrawCardsEvent(this.player.getId(), source, this.originalDrawEvent, this.amount);
|
|
||||||
// TODO: This needs a better description of how it works. Why "amount < 2"?
|
|
||||||
if (amount < 2 || !game.replaceEvent(event)) {
|
|
||||||
amount = event.getAmount();
|
|
||||||
for (int i = 0; i < amount; i++) {
|
|
||||||
int value = drawCard(source, this.originalDrawEvent, game);
|
|
||||||
if (value == NEGATIVE_VALUE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
numDrawn++;
|
|
||||||
score += value;
|
|
||||||
}
|
|
||||||
if (!player.isTopCardRevealed() && numDrawn > 0) {
|
|
||||||
game.fireInformEvent(player.getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : ""));
|
|
||||||
}
|
|
||||||
setScore(player, score);
|
|
||||||
}
|
|
||||||
return numDrawn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draw a card if possible (there is no replacement effect that prevent us
|
|
||||||
* from drawing). Fire event about card drawn.
|
|
||||||
*
|
|
||||||
* @param source
|
|
||||||
* @param originalDrawEvent original draw event for replacement effects, can be null for normal calls
|
|
||||||
* @param game
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected int drawCard(Ability source, GameEvent originalDrawEvent, Game game) {
|
|
||||||
GameEvent event = new DrawCardEvent(this.player.getId(), source, originalDrawEvent);
|
|
||||||
if (!game.replaceEvent(event)) {
|
|
||||||
Card card = player.getLibrary().removeFromTop(game);
|
|
||||||
if (card != null) {
|
|
||||||
drawnCards.add(card);
|
|
||||||
card.moveToZone(Zone.HAND, source, game, false); // if you want to use event.getSourceId() here then thinks x10 times
|
|
||||||
if (player.isTopCardRevealed()) {
|
|
||||||
game.fireInformEvent(player.getLogName() + " draws a revealed card (" + card.getLogName() + ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
game.fireEvent(new DrewCardEvent(card.getId(), player.getId(), source, originalDrawEvent));
|
|
||||||
return ArtificialScoringSystem.inst.getCardScore(card);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NEGATIVE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a card back to top.
|
|
||||||
*
|
|
||||||
* @param game Game context
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void undoAction(Game game) {
|
|
||||||
for (int index = drawnCards.size() - 1; index >= 0; index--) {
|
|
||||||
Card card = drawnCards.get(index);
|
|
||||||
player.getHand().remove(card);
|
|
||||||
player.getLibrary().putOnTop(card, game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
package mage.actions.impl;
|
|
||||||
|
|
||||||
import mage.abilities.Ability;
|
|
||||||
import mage.game.Game;
|
|
||||||
import mage.players.Player;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for mage actions.
|
|
||||||
*
|
|
||||||
* @author ayratn
|
|
||||||
*/
|
|
||||||
public abstract class MageAction {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link Player} we count score for.
|
|
||||||
*/
|
|
||||||
private Player scorePlayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current game score for the player.
|
|
||||||
*/
|
|
||||||
private int score = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set or change action score.
|
|
||||||
*
|
|
||||||
* @param scorePlayer Set player.
|
|
||||||
* @param score Set score value.
|
|
||||||
*/
|
|
||||||
protected void setScore(Player scorePlayer, int score) {
|
|
||||||
this.scorePlayer = scorePlayer;
|
|
||||||
this.score = score;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get game score for the {@link Player}. Value depends on the owner of this
|
|
||||||
* action. In case player and owner differ, negative value is returned.
|
|
||||||
*
|
|
||||||
* @param player
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public int getScore(final Player player) {
|
|
||||||
if (player == null || scorePlayer == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (player.getId().equals(scorePlayer.getId())) {
|
|
||||||
return score;
|
|
||||||
} else {
|
|
||||||
return -score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute action.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param source
|
|
||||||
* @param game Game context.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public abstract int doAction(Ability source, final Game game);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo action.
|
|
||||||
*
|
|
||||||
* @param game Game context
|
|
||||||
*/
|
|
||||||
public abstract void undoAction(final Game game);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package mage.actions.score;
|
|
||||||
|
|
||||||
import mage.cards.Card;
|
|
||||||
import mage.game.Game;
|
|
||||||
import org.apache.log4j.Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author ayratn
|
|
||||||
*/
|
|
||||||
public class ArtificialScoringSystem implements ScoringSystem {
|
|
||||||
|
|
||||||
public static ArtificialScoringSystem inst;
|
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(ArtificialScoringSystem.class);
|
|
||||||
|
|
||||||
static {
|
|
||||||
inst = new ArtificialScoringSystem();
|
|
||||||
log.debug("ArtificialScoringSystem has been instantiated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lose score is lowered in function of the turn and phase when it occurs.
|
|
||||||
* Encourages AI to win as fast as possible.
|
|
||||||
*
|
|
||||||
* @param game
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int getLoseGameScore(final Game game) {
|
|
||||||
if (game.getStep() == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return ScoringConstants.LOSE_GAME_SCORE + game.getTurnNum() * 2500 + game.getTurnStepType().getIndex() * 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCardScore(Card card) {
|
|
||||||
//TODO: implement
|
|
||||||
return ScoringConstants.UNKNOWN_CARD_SCORE;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package mage.actions.score;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants for scoring system.
|
|
||||||
*
|
|
||||||
* @author ayratn
|
|
||||||
*/
|
|
||||||
public final class ScoringConstants {
|
|
||||||
public static final int WIN_GAME_SCORE = 100000000;
|
|
||||||
public static final int LOSE_GAME_SCORE = -WIN_GAME_SCORE;
|
|
||||||
|
|
||||||
public static final int UNKNOWN_CARD_SCORE = 300;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package mage.actions.score;
|
|
||||||
|
|
||||||
import mage.cards.Card;
|
|
||||||
import mage.game.Game;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author ayratn
|
|
||||||
*/
|
|
||||||
public interface ScoringSystem {
|
|
||||||
|
|
||||||
int getLoseGameScore(final Game game);
|
|
||||||
int getCardScore(final Card card);
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ import mage.abilities.common.delayed.ReflexiveTriggeredAbility;
|
||||||
import mage.abilities.effects.ContinuousEffect;
|
import mage.abilities.effects.ContinuousEffect;
|
||||||
import mage.abilities.effects.ContinuousEffects;
|
import mage.abilities.effects.ContinuousEffects;
|
||||||
import mage.abilities.effects.PreventionEffectData;
|
import mage.abilities.effects.PreventionEffectData;
|
||||||
import mage.actions.impl.MageAction;
|
|
||||||
import mage.cards.Card;
|
import mage.cards.Card;
|
||||||
import mage.cards.Cards;
|
import mage.cards.Cards;
|
||||||
import mage.cards.MeldCard;
|
import mage.cards.MeldCard;
|
||||||
|
|
@ -552,8 +551,6 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
||||||
|
|
||||||
boolean endTurn(Ability source);
|
boolean endTurn(Ability source);
|
||||||
|
|
||||||
int doAction(Ability source, MageAction action);
|
|
||||||
|
|
||||||
//game transaction methods
|
//game transaction methods
|
||||||
void saveState(boolean bookmark);
|
void saveState(boolean bookmark);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import mage.abilities.effects.keyword.StunCounterEffect;
|
||||||
import mage.abilities.keyword.*;
|
import mage.abilities.keyword.*;
|
||||||
import mage.abilities.mana.DelayedTriggeredManaAbility;
|
import mage.abilities.mana.DelayedTriggeredManaAbility;
|
||||||
import mage.abilities.mana.TriggeredManaAbility;
|
import mage.abilities.mana.TriggeredManaAbility;
|
||||||
import mage.actions.impl.MageAction;
|
|
||||||
import mage.cards.*;
|
import mage.cards.*;
|
||||||
import mage.cards.decks.Deck;
|
import mage.cards.decks.Deck;
|
||||||
import mage.cards.decks.DeckCardInfo;
|
import mage.cards.decks.DeckCardInfo;
|
||||||
|
|
@ -3772,11 +3771,6 @@ public abstract class GameImpl implements Game {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int doAction(Ability source, MageAction action) {
|
|
||||||
return action.doAction(source, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Date getStartTime() {
|
public Date getStartTime() {
|
||||||
if (startTime == null) {
|
if (startTime == null) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ import java.util.UUID;
|
||||||
*/
|
*/
|
||||||
public class DrawCardEvent extends GameEvent {
|
public class DrawCardEvent extends GameEvent {
|
||||||
|
|
||||||
|
private boolean fromBottom = false; // for replacement effects that draw from bottom of library instead
|
||||||
|
|
||||||
|
private int cardsDrawn = 0; // for replacement effects to keep track for "cards drawn this way"
|
||||||
|
|
||||||
public DrawCardEvent(UUID playerId, Ability source, GameEvent originalDrawEvent) {
|
public DrawCardEvent(UUID playerId, Ability source, GameEvent originalDrawEvent) {
|
||||||
super(GameEvent.EventType.DRAW_CARD, playerId, null, playerId, 0, false);
|
super(GameEvent.EventType.DRAW_CARD, playerId, null, playerId, 0, false);
|
||||||
|
|
||||||
|
|
@ -22,4 +26,21 @@ public class DrawCardEvent extends GameEvent {
|
||||||
this.addAppliedEffects(originalDrawEvent.getAppliedEffects());
|
this.addAppliedEffects(originalDrawEvent.getAppliedEffects());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFromBottom(boolean fromBottom) {
|
||||||
|
this.fromBottom = fromBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFromBottom() {
|
||||||
|
return fromBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementCardsDrawn(int cardsDrawn) {
|
||||||
|
this.cardsDrawn += cardsDrawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCardsDrawn() {
|
||||||
|
return cardsDrawn;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ import java.util.UUID;
|
||||||
/**
|
/**
|
||||||
* @author JayDi85
|
* @author JayDi85
|
||||||
*/
|
*/
|
||||||
public class DrawCardsEvent extends GameEvent {
|
public class DrawTwoOrMoreCardsEvent extends GameEvent {
|
||||||
|
|
||||||
public DrawCardsEvent(UUID playerId, Ability source, GameEvent originalDrawEvent, int amount) {
|
private int cardsDrawn = 0; // for replacement effects to keep track for "cards drawn this way"
|
||||||
super(GameEvent.EventType.DRAW_CARDS, playerId, null, playerId, amount, false);
|
|
||||||
|
public DrawTwoOrMoreCardsEvent(UUID playerId, Ability source, GameEvent originalDrawEvent, int amount) {
|
||||||
|
super(GameEvent.EventType.DRAW_TWO_OR_MORE_CARDS, playerId, null, playerId, amount, false);
|
||||||
|
|
||||||
// source of draw events must be kept between replacements, example: UnpredictableCycloneTest
|
// source of draw events must be kept between replacements, example: UnpredictableCycloneTest
|
||||||
this.setSourceId(originalDrawEvent == null
|
this.setSourceId(originalDrawEvent == null
|
||||||
|
|
@ -22,4 +24,13 @@ public class DrawCardsEvent extends GameEvent {
|
||||||
this.addAppliedEffects(originalDrawEvent.getAppliedEffects());
|
this.addAppliedEffects(originalDrawEvent.getAppliedEffects());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void incrementCardsDrawn(int cardsDrawn) {
|
||||||
|
this.cardsDrawn += cardsDrawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCardsDrawn() {
|
||||||
|
return cardsDrawn;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +75,7 @@ public class GameEvent implements Serializable {
|
||||||
ZONE_CHANGE,
|
ZONE_CHANGE,
|
||||||
ZONE_CHANGE_GROUP, // between two specific zones only; TODO: rework all usages to ZONE_CHANGE_BATCH instead, see #11895
|
ZONE_CHANGE_GROUP, // between two specific zones only; TODO: rework all usages to ZONE_CHANGE_BATCH instead, see #11895
|
||||||
ZONE_CHANGE_BATCH, // all zone changes that occurred from a single effect
|
ZONE_CHANGE_BATCH, // all zone changes that occurred from a single effect
|
||||||
DRAW_CARDS, // event calls for multi draws only (if player draws 2+ cards at once)
|
DRAW_TWO_OR_MORE_CARDS, // event calls for multi draws only (if player draws 2+ cards at once)
|
||||||
DRAW_CARD, DREW_CARD,
|
DRAW_CARD, DREW_CARD,
|
||||||
EXPLORE, EXPLORED, // targetId is exploring permanent, playerId is its controller
|
EXPLORE, EXPLORED, // targetId is exploring permanent, playerId is its controller
|
||||||
ECHO_PAID,
|
ECHO_PAID,
|
||||||
|
|
|
||||||
|
|
@ -47,57 +47,51 @@ public class Library implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the top card of the Library and returns it
|
* Draws a card from the top of the library, removing it from the library.
|
||||||
*
|
* If library is empty, returns null and sets flag for drawing from an empty library.
|
||||||
* @param game
|
|
||||||
* @return Card
|
|
||||||
* @see Card
|
|
||||||
*/
|
*/
|
||||||
|
public Card drawFromTop(Game game) {
|
||||||
|
Card card = game.getCard(library.pollFirst());
|
||||||
|
if (card == null) {
|
||||||
|
emptyDraw = true;
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a card from the bottom of the library, removing it from the library.
|
||||||
|
* If library is empty, returns null and sets flag for drawing from an empty library.
|
||||||
|
*/
|
||||||
|
public Card drawFromBottom(Game game) {
|
||||||
|
Card card = game.getCard(library.pollLast());
|
||||||
|
if (card == null) {
|
||||||
|
emptyDraw = true;
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the top card from the Library and returns it (can be null if library is empty).
|
||||||
|
*/
|
||||||
|
@Deprecated // recommend refactoring methods that re-order library to not require this explicit removal
|
||||||
public Card removeFromTop(Game game) {
|
public Card removeFromTop(Game game) {
|
||||||
UUID cardId = library.pollFirst();
|
return game.getCard(library.pollFirst());
|
||||||
Card card = game.getCard(cardId);
|
|
||||||
if (card == null) {
|
|
||||||
emptyDraw = true;
|
|
||||||
}
|
|
||||||
return card;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the bottom card of the Library and returns it
|
* Returns the top card of the Library (can be null if library is empty).
|
||||||
*
|
* The card is still in the library, until/unless some zone-handling code moves it
|
||||||
* @param game
|
|
||||||
* @return Card
|
|
||||||
* @see Card
|
|
||||||
*/
|
|
||||||
public Card removeFromBottom(Game game) {
|
|
||||||
UUID cardId = library.pollLast();
|
|
||||||
Card card = game.getCard(cardId);
|
|
||||||
if (card == null) {
|
|
||||||
emptyDraw = true;
|
|
||||||
}
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the top card of the Library without removing it
|
|
||||||
*
|
|
||||||
* @param game
|
|
||||||
* @return Card
|
|
||||||
* @see Card
|
|
||||||
*/
|
*/
|
||||||
public Card getFromTop(Game game) {
|
public Card getFromTop(Game game) {
|
||||||
return game.getCard(library.peekFirst());
|
return game.getCard(library.peekFirst());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the bottommost card of the Library without removing it
|
* Returns the bottom card of the library (can be null if library is empty)
|
||||||
*
|
* The card is still in the library, until/unless some zone-handling code moves it
|
||||||
* @param game
|
|
||||||
* @return Card
|
|
||||||
* @see Card
|
|
||||||
*/
|
*/
|
||||||
public Card getFromBottom(Game game) {
|
public Card getFromBottom(Game game) {
|
||||||
return game.getCard(library.pollLast());
|
return game.getCard(library.peekLast());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putOnTop(Card card, Game game) {
|
public void putOnTop(Card card, Game game) {
|
||||||
|
|
@ -152,10 +146,7 @@ public class Library implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cards of the library in a list ordered from top to buttom
|
* Returns the cards of the library in a list ordered from top to bottom
|
||||||
*
|
|
||||||
* @param game
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public List<Card> getCards(Game game) {
|
public List<Card> getCards(Game game) {
|
||||||
return library.stream().map(game::getCard).filter(Objects::nonNull).collect(Collectors.toList());
|
return library.stream().map(game::getCard).filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
|
|
@ -235,9 +226,6 @@ public class Library implements Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests only -- find card position in library
|
* Tests only -- find card position in library
|
||||||
*
|
|
||||||
* @param cardId
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public int getCardPosition(UUID cardId) {
|
public int getCardPosition(UUID cardId) {
|
||||||
UUID[] list = library.toArray(new UUID[0]);
|
UUID[] list = library.toArray(new UUID[0]);
|
||||||
|
|
|
||||||
|
|
@ -407,25 +407,21 @@ public interface Player extends MageItem, Copyable<Player> {
|
||||||
void shuffleLibrary(Ability source, Game game);
|
void shuffleLibrary(Ability source, Game game);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw cards. If you call it in replace events then use method with event.appliedEffects param instead.
|
* Draw cards. If you call it in replace events then use method with event param instead (for appliedEffects)
|
||||||
* Returns 0 if replacement effect triggers on card draw.
|
|
||||||
*
|
*
|
||||||
* @param num
|
* @param num cards to draw
|
||||||
* @param source can be null for game default draws (non effects, example: start of the turn)
|
* @param source can be null for game default draws (non effects, example: start of the turn)
|
||||||
* @param game
|
* @return number of cards drawn, including as a result of replacement effects
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
int drawCards(int num, Ability source, Game game);
|
int drawCards(int num, Ability source, Game game);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw cards with applied effects, for replaceEvent
|
* Draw cards with applied effects, for replaceEvent
|
||||||
* Returns 0 if replacement effect triggers on card draw.
|
|
||||||
*
|
*
|
||||||
* @param num
|
* @param num cards to draw
|
||||||
* @param source can be null for game default draws (non effects, example: start of the turn)
|
* @param source can be null for game default draws (non effects, example: start of the turn)
|
||||||
* @param game
|
|
||||||
* @param event original draw event in replacement code
|
* @param event original draw event in replacement code
|
||||||
* @return
|
* @return number of cards drawn, including as a result of replacement effects
|
||||||
*/
|
*/
|
||||||
int drawCards(int num, Ability source, Game game, GameEvent event);
|
int drawCards(int num, Ability source, Game game, GameEvent event);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect;
|
||||||
import mage.abilities.keyword.*;
|
import mage.abilities.keyword.*;
|
||||||
import mage.abilities.mana.ActivatedManaAbilityImpl;
|
import mage.abilities.mana.ActivatedManaAbilityImpl;
|
||||||
import mage.abilities.mana.ManaOptions;
|
import mage.abilities.mana.ManaOptions;
|
||||||
import mage.actions.MageDrawAction;
|
|
||||||
import mage.cards.*;
|
import mage.cards.*;
|
||||||
import mage.cards.decks.Deck;
|
import mage.cards.decks.Deck;
|
||||||
import mage.choices.Choice;
|
import mage.choices.Choice;
|
||||||
|
|
@ -82,7 +81,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
/**
|
/**
|
||||||
* During some steps we can't play anything
|
* During some steps we can't play anything
|
||||||
*/
|
*/
|
||||||
final static Map<PhaseStep, Step.StepPart> SILENT_PHASES_STEPS = ImmutableMap.<PhaseStep, Step.StepPart>builder().
|
static final Map<PhaseStep, Step.StepPart> SILENT_PHASES_STEPS = ImmutableMap.<PhaseStep, Step.StepPart>builder().
|
||||||
put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build();
|
put(PhaseStep.DECLARE_ATTACKERS, Step.StepPart.PRE).build();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -737,15 +736,60 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int drawCards(int num, Ability source, Game game) {
|
public int drawCards(int num, Ability source, Game game) {
|
||||||
if (num > 0) {
|
return drawCards(num, source, game, null);
|
||||||
return game.doAction(source, new MageDrawAction(this, num, null));
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 614.11. Some effects replace card draws. These effects are applied even if no cards could be drawn because
|
||||||
|
* there are no cards in the affected player's library.
|
||||||
|
* 614.11a. If an effect replaces a draw within a sequence of card draws, all actions required by the replacement
|
||||||
|
* are completed, if possible, before resuming the sequence.
|
||||||
|
* 614.11b. If an effect would have a player both draw a card and perform an additional action on that card, and
|
||||||
|
* the draw is replaced, the additional action is not performed on any cards that are drawn as a result of that
|
||||||
|
* replacement effect.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int drawCards(int num, Ability source, Game game, GameEvent event) {
|
public int drawCards(int num, Ability source, Game game, GameEvent event) {
|
||||||
return game.doAction(source, new MageDrawAction(this, num, event));
|
if (num == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (num >= 2) {
|
||||||
|
// Event for replacement effects that only apply when two or more cards are drawn
|
||||||
|
DrawTwoOrMoreCardsEvent multiDrawEvent = new DrawTwoOrMoreCardsEvent(getId(), source, event, num);
|
||||||
|
if (game.replaceEvent(multiDrawEvent)) {
|
||||||
|
return multiDrawEvent.getCardsDrawn();
|
||||||
|
}
|
||||||
|
num = multiDrawEvent.getAmount();
|
||||||
|
}
|
||||||
|
int numDrawn = 0;
|
||||||
|
for (int i = 0; i < num; i++) {
|
||||||
|
DrawCardEvent drawCardEvent = new DrawCardEvent(getId(), source, event);
|
||||||
|
if (game.replaceEvent(drawCardEvent)) {
|
||||||
|
numDrawn += drawCardEvent.getCardsDrawn();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Card card = drawCardEvent.isFromBottom() ? getLibrary().drawFromBottom(game) : getLibrary().drawFromTop(game);
|
||||||
|
if (card != null) {
|
||||||
|
card.moveToZone(Zone.HAND, source, game, false); // if you want to use event.getSourceId() here then thinks x10 times
|
||||||
|
if (isTopCardRevealed()) {
|
||||||
|
game.fireInformEvent(getLogName() + " draws a revealed card (" + card.getLogName() + ')');
|
||||||
|
}
|
||||||
|
game.fireEvent(new DrewCardEvent(card.getId(), getId(), source, event));
|
||||||
|
numDrawn++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isTopCardRevealed() && numDrawn > 0) {
|
||||||
|
game.fireInformEvent(getLogName() + " draws " + CardUtil.numberToText(numDrawn, "a") + " card" + (numDrawn > 1 ? "s" : ""));
|
||||||
|
}
|
||||||
|
// if this method was called from a replacement event, pass the number of cards back through
|
||||||
|
// (uncomment conditions if correct ruling is to only count cards drawn by the same player)
|
||||||
|
if (event instanceof DrawCardEvent /* && event.getPlayerId().equals(getId()) */ ) {
|
||||||
|
((DrawCardEvent) event).incrementCardsDrawn(numDrawn);
|
||||||
|
}
|
||||||
|
if (event instanceof DrawTwoOrMoreCardsEvent /* && event.getPlayerId().equals(getId()) */ ) {
|
||||||
|
((DrawTwoOrMoreCardsEvent) event).incrementCardsDrawn(numDrawn);
|
||||||
|
}
|
||||||
|
return numDrawn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue