[CMR] implemented Opposition Agent and other changes:

* You may play cards and you may spend mana of any color - refactored cards to use same code;
* Library search event allows to change searching controller (gives full game control for another player);
* Library searched event allows to remove founded cards from result;
* Improved library searching effects with Panglacial Wurm's effects;
* Little changes to test framework;
This commit is contained in:
Oleg Agafonov 2020-11-24 23:49:19 +04:00
parent 13fa98ec44
commit c2a636e2b2
22 changed files with 806 additions and 721 deletions

View file

@ -0,0 +1,53 @@
package mage.abilities.effects.common.asthought;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import java.util.UUID;
/**
* Play card from current zone. Will be discarded on any card movements or blinks.
* <p>
* Recommends to use combo effects from CardUtil.makeCardPlayableAndSpendManaAsAnyColor instead signle effect
*
* @author JayDi85
*/
public class CanPlayCardControllerEffect extends AsThoughEffectImpl {
protected final MageObjectReference mor;
public CanPlayCardControllerEffect(Game game, UUID cardId, int cardZCC, Duration duration) {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, duration, Outcome.Benefit);
this.staticText = "You may play those card";
this.mor = new MageObjectReference(cardId, cardZCC, game);
}
public CanPlayCardControllerEffect(final CanPlayCardControllerEffect effect) {
super(effect);
this.mor = effect.mor;
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public CanPlayCardControllerEffect copy() {
return new CanPlayCardControllerEffect(this);
}
@Override
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
if (mor.getCard(game) == null) {
discard();
return false;
}
return mor.refersTo(sourceId, game) && source.isControlledBy(affectedControllerId);
}
}

View file

@ -0,0 +1,55 @@
package mage.abilities.effects.common.asthought;
import mage.abilities.Ability;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.AsThoughManaEffect;
import mage.constants.*;
import mage.game.Game;
import mage.players.ManaPoolItem;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.UUID;
/**
* Spend mana as any color to cast targeted card. Will not affected after any card movements or blinks.
*
* @author JayDi85
*/
public class YouMaySpendManaAsAnyColorToCastTargetEffect extends AsThoughEffectImpl implements AsThoughManaEffect {
public YouMaySpendManaAsAnyColorToCastTargetEffect(Duration duration) {
super(AsThoughEffectType.SPEND_OTHER_MANA, duration, Outcome.Benefit);
this.staticText = "You may spend mana as though it were mana of any color to cast it";
}
public YouMaySpendManaAsAnyColorToCastTargetEffect(final YouMaySpendManaAsAnyColorToCastTargetEffect effect) {
super(effect);
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public YouMaySpendManaAsAnyColorToCastTargetEffect copy() {
return new YouMaySpendManaAsAnyColorToCastTargetEffect(this);
}
@Override
public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
objectId = CardUtil.getMainCardId(game, objectId); // for split cards
FixedTarget fixedTarget = ((FixedTarget) getTargetPointer());
return source.isControlledBy(affectedControllerId)
&& Objects.equals(objectId, fixedTarget.getTarget())
&& game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1
&& (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED);
}
@Override
public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) {
return mana.getFirstAvailable();
}
}

View file

@ -0,0 +1,48 @@
package mage.abilities.effects.common.replacement;
import mage.abilities.Ability;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.SearchLibraryEvent;
import mage.players.Player;
/**
* @author JayDi85
*/
public class YouControlYourOpponentsWhileSearchingReplacementEffect extends ReplacementEffectImpl {
public YouControlYourOpponentsWhileSearchingReplacementEffect() {
super(Duration.WhileOnBattlefield, Outcome.Benefit);
staticText = "You control your opponents while theyre searching their libraries";
}
YouControlYourOpponentsWhileSearchingReplacementEffect(final YouControlYourOpponentsWhileSearchingReplacementEffect effect) {
super(effect);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
SearchLibraryEvent se = (SearchLibraryEvent) event;
se.setSearchingControllerId(source.getControllerId());
return false;
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SEARCH_LIBRARY;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
Player controller = game.getPlayer(source.getControllerId());
return controller != null && game.isOpponent(controller, event.getPlayerId());
}
@Override
public YouControlYourOpponentsWhileSearchingReplacementEffect copy() {
return new YouControlYourOpponentsWhileSearchingReplacementEffect(this);
}
}

View file

@ -0,0 +1,30 @@
package mage.game.events;
import mage.target.Target;
import java.util.UUID;
/**
* @author JayDi85
*/
public class LibrarySearchedEvent extends GameEvent {
protected Target searchedTarget;
/**
* Searched library event (after library searching finished). Return false on replaceEvent to
*
* @param targetPlayerId whose library searched
* @param sourceId source of the searching effect
* @param playerId who must search the library
* @param searchedTarget founded cards (targets list can be changed by replace events, see Opposition Agent)
*/
public LibrarySearchedEvent(UUID targetPlayerId, UUID sourceId, UUID playerId, Target searchedTarget) {
super(EventType.LIBRARY_SEARCHED, targetPlayerId, sourceId, playerId, searchedTarget.getTargets().size(), false);
this.searchedTarget = searchedTarget;
}
public Target getSearchedTarget() {
return this.searchedTarget;
}
}

View file

@ -0,0 +1,32 @@
package mage.game.events;
import java.util.UUID;
/**
* @author JayDi85
*/
public class SearchLibraryEvent extends GameEvent {
protected UUID searchingControllerId; // who controls the searching process, see Opposition Agent
/**
* Searching library event
*
* @param targetPlayerId whose library will be searched
* @param sourceId source of the searching effect
* @param playerId who must search the library (also see searchingControllerId)
* @param amount cards amount to search
*/
public SearchLibraryEvent(UUID targetPlayerId, UUID sourceId, UUID playerId, int amount) {
super(GameEvent.EventType.SEARCH_LIBRARY, targetPlayerId, sourceId, playerId, amount, false);
this.searchingControllerId = playerId;
}
public UUID getSearchingControllerId() {
return this.searchingControllerId;
}
public void setSearchingControllerId(UUID searchingControllerId) {
this.searchingControllerId = searchingControllerId;
}
}

View file

@ -1,10 +1,7 @@
package mage.players;
import com.google.common.collect.ImmutableMap;
import mage.ApprovingObject;
import mage.ConditionalMana;
import mage.MageObject;
import mage.Mana;
import mage.*;
import mage.abilities.*;
import mage.abilities.ActivatedAbility.ActivationStatus;
import mage.abilities.common.PassAbility;
@ -28,7 +25,6 @@ import mage.abilities.mana.ManaOptions;
import mage.actions.MageDrawAction;
import mage.cards.*;
import mage.cards.decks.Deck;
import mage.choices.ChoiceImpl;
import mage.constants.*;
import mage.counters.Counter;
import mage.counters.CounterType;
@ -38,6 +34,7 @@ import mage.designations.DesignationType;
import mage.filter.FilterCard;
import mage.filter.FilterMana;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.common.FilterCreatureForCombat;
import mage.filter.common.FilterCreatureForCombatBlock;
@ -2609,73 +2606,97 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override
public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) {
//20091005 - 701.14c
Library searchedLibrary = null;
String searchInfo = null;
if (targetPlayerId.equals(playerId)) {
searchInfo = getLogName() + " searches their library";
searchedLibrary = library;
// searching control can be intercepted by another player, see Opposition Agent
SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source.getSourceId(), playerId, Integer.MAX_VALUE);
if (game.replaceEvent(searchEvent)) {
return false;
}
Player targetPlayer = game.getPlayer(targetPlayerId);
Player searchingPlayer = this;
Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId());
if (targetPlayer == null || searchingController == null) {
return false;
}
String searchInfo = searchingPlayer.getLogName();
if (!searchingPlayer.getId().equals(searchingController.getId())) {
searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName();
}
if (targetPlayer.getId().equals(searchingPlayer.getId())) {
searchInfo = searchInfo + " searches their library";
} else {
Player targetPlayer = game.getPlayer(targetPlayerId);
if (targetPlayer != null) {
searchInfo = getLogName() + " searches the library of " + targetPlayer.getLogName();
searchedLibrary = targetPlayer.getLibrary();
}
}
if (searchedLibrary == null) {
return false;
}
GameEvent event = GameEvent.getEvent(GameEvent.EventType.SEARCH_LIBRARY,
targetPlayerId, source.getSourceId(), playerId, Integer.MAX_VALUE);
if (game.replaceEvent(event)) {
return false;
searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName();
}
if (!game.isSimulation()) {
game.informPlayers(searchInfo);
}
// https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/
// You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect:
// * While youre searching your library, you may cast Panglacial Wurm from your library.
// So use here same code as Word of Command
// P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest
boolean takeControl = false;
if (!searchingPlayer.getId().equals(searchingController.getId())) {
CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true);
takeControl = true;
}
Library searchingLibrary = targetPlayer.getLibrary();
TargetCardInLibrary newTarget = target.copy();
int count;
int librarySearchLimit = event.getAmount();
int librarySearchLimit = searchEvent.getAmount();
List<Card> cardsFromTop = null;
do {
// TODO: prevent shuffling from moving the visualized cards
if (librarySearchLimit == Integer.MAX_VALUE) {
count = searchedLibrary.count(target.getFilter(), game);
count = searchingLibrary.count(target.getFilter(), game);
} else {
Player targetPlayer = game.getPlayer(targetPlayerId);
if (targetPlayer == null) {
return false;
}
if (cardsFromTop == null) {
cardsFromTop = new ArrayList<>(targetPlayer.getLibrary().getTopCards(game, librarySearchLimit));
cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit));
} else {
cardsFromTop.retainAll(targetPlayer.getLibrary().getCards(game));
cardsFromTop.retainAll(searchingLibrary.getCards(game));
}
newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size()));
count = Math.min(searchedLibrary.count(target.getFilter(), game), librarySearchLimit);
count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit);
}
if (count < target.getNumberOfTargets()) {
newTarget.setMinNumberOfTargets(count);
}
if (newTarget.choose(Outcome.Neutral, playerId, targetPlayerId, game)) {
if (targetPlayerId.equals(playerId) && handleLibraryCastableCards(library,
game, targetPlayerId)) { // for handling Panglacial Wurm
// handling Panglacial Wurm - cast cards while searching from own library
if (targetPlayer.getId().equals(searchingPlayer.getId())) {
if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) {
// clear all choices to start from scratch (casted cards must be removed from library)
newTarget.clearChosen();
continue;
}
}
if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) {
target.getTargets().clear();
for (UUID targetId : newTarget.getTargets()) {
target.add(targetId, game);
}
} else if (targetPlayerId.equals(playerId) && handleLibraryCastableCards(library,
game, targetPlayerId)) { // for handling Panglacial Wurm
newTarget.clearChosen();
continue;
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SEARCHED, targetPlayerId, playerId));
// END SEARCH
if (takeControl) {
CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer);
game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back");
}
LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source.getSourceId(), searchingPlayer.getId(), target);
if (game.replaceEvent(searchedEvent)) {
return false;
}
break;
} while (true);
return true;
}
@ -2690,58 +2711,53 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
private boolean handleLibraryCastableCards(Library library, Game game, UUID targetPlayerId) {
// for handling Panglacial Wurm
boolean alreadyChosenUse = false;
Map<UUID, String> libraryCastableCardTracker = new HashMap<>();
searchForCards:
do {
for (Card card : library.getCards(game)) {
for (Ability ability : card.getAbilities()) {
if (ability.getClass() == WhileSearchingPlayFromLibraryAbility.class) {
libraryCastableCardTracker.put(card.getId(), card.getIdName());
}
}
private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) {
// must return true after cast try (to restart searching process without casted cards)
// uses for handling Panglacial Wurm:
// * While you're searching your library, you may cast Panglacial Wurm from your library.
List<UUID> castableCards = library.getCards(game).stream()
.filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class))
.map(MageItem::getId)
.collect(Collectors.toList());
if (castableCards.size() == 0) {
return false;
}
// only humans can use it
if (!targetPlayer.isHuman() && !targetPlayer.isTestMode()) {
return false;
}
if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "Library have " + castableCards.size() + " castable cards on searching. Do you want to cast it?", null, game)) {
return false;
}
boolean casted = false;
TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD);
targetCard.setTargetName("card to cast from library");
targetCard.setNotTarget(true);
while (castableCards.size() > 0) {
targetCard.clearChosen();
if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) {
break;
}
if (!libraryCastableCardTracker.isEmpty()) {
Player player = game.getPlayer(targetPlayerId);
if (player != null) {
if (player.isHuman() && (alreadyChosenUse || player.chooseUse(Outcome.AIDontUseIt,
"Cast a creature card from your library? (choose \"No\" to finish search)", null, game))) {
ChoiceImpl chooseCard = new ChoiceImpl();
chooseCard.setMessage("Which creature do you wish to cast from your library?");
Set<String> choice = new LinkedHashSet<>();
for (Entry<UUID, String> entry : libraryCastableCardTracker.entrySet()) {
choice.add(new AbstractMap.SimpleEntry<>(entry).getValue());
}
chooseCard.setChoices(choice);
while (!choice.isEmpty()) {
if (player.choose(Outcome.AIDontUseIt, chooseCard, game)) {
String chosenCard = chooseCard.getChoice();
for (Entry<UUID, String> entry : libraryCastableCardTracker.entrySet()) {
if (chosenCard.equals(entry.getValue())) {
Card card = game.getCard(entry.getKey());
if (card != null) {
// TODO: fix costs (why is Panglacial Wurm automatically accepting payment?)
player.cast(card.getSpellAbility(), game, false, null);
}
chooseCard.clearChoice();
libraryCastableCardTracker.clear();
alreadyChosenUse = true;
continue searchForCards;
}
}
continue;
}
break;
}
return true;
}
}
Card card = game.getCard(targetCard.getFirstTarget());
if (card == null) {
break;
}
break;
} while (alreadyChosenUse);
return alreadyChosenUse;
// AI NOTICE: if you want AI implement here then remove selected card from castable after each
// choice (otherwise you catch infinite freeze on uncastable use case)
// casting selected card
// TODO: fix costs (why is Panglacial Wurm automatically accepting payment?)
targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null);
castableCards.remove(card.getId());
casted = true;
}
return casted;
}
@Override

View file

@ -10,6 +10,8 @@ import mage.abilities.SpellAbility;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.*;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.common.asthought.CanPlayCardControllerEffect;
import mage.abilities.effects.common.asthought.YouMaySpendManaAsAnyColorToCastTargetEffect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.HintUtils;
import mage.cards.Card;
@ -28,6 +30,7 @@ import mage.game.permanent.token.Token;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.Target;
import mage.target.targetpointer.FixedTarget;
import mage.util.functions.CopyTokenFunction;
import org.apache.log4j.Logger;
@ -955,4 +958,56 @@ public final class CardUtil {
}
return RULES_ERROR_INFO;
}
/**
* Take control under another player, use it in inner effects like Word of Commands. Don't forget to end it in same code.
*
* @param game
* @param controller
* @param targetPlayer
* @param givePauseForResponse if you want to give controller time to watch opponent's hand (if you remove control effect in the end of code)
*/
public static void takeControlUnderPlayerStart(Game game, Player controller, Player targetPlayer, boolean givePauseForResponse) {
controller.controlPlayersTurn(game, targetPlayer.getId());
if (givePauseForResponse) {
while (controller.canRespond()) {
if (controller.chooseUse(Outcome.Benefit, "You got control of " + targetPlayer.getLogName()
+ ". Use switch hands button to view opponent's hand.", null,
"Continue", "Wait", null, game)) {
break;
}
}
}
}
/**
* Return control under another player, use it in inner effects like Word of Commands.
*
* @param game
* @param controller
* @param targetPlayer
*/
public static void takeControlUnderPlayerEnd(Game game, Player controller, Player targetPlayer) {
targetPlayer.setGameUnderYourControl(true, false);
if (!targetPlayer.getTurnControlledBy().equals(controller.getId())) {
controller.getPlayersUnderYourControl().remove(targetPlayer.getId());
}
}
/**
* Add effects to game that allows to play/cast card from current zone and spend mana as any type for it.
* Effects will be discarded/ignored on any card movements or blinks (after ZCC change)
*
* @param game
* @param card
* @param duration
*/
public static void makeCardPlayableAndSpendManaAsAnyColor(Game game, Ability source, Card card, Duration duration) {
// Effect can be used for cards in zones and permanents on battlefield
// So there is a workaround to get real ZCC (PermanentCard's ZCC is static, but it must be from Card's ZCC)
// Example: Hostage Taker
int zcc = game.getState().getZoneChangeCounter(card.getId());
game.addEffect(new CanPlayCardControllerEffect(game, card.getId(), zcc, duration), source);
game.addEffect(new YouMaySpendManaAsAnyColorToCastTargetEffect(duration).setTargetPointer(new FixedTarget(card.getId(), zcc)), source);
}
}