[J21] Implemented Faceless Agent

This commit is contained in:
Evan Kranzler 2021-08-06 08:52:31 -04:00
parent b10b3da8f0
commit f5687acfad
8 changed files with 190 additions and 36 deletions

View file

@ -26,6 +26,7 @@ public class Historic extends Constructed {
// Additional sets // Additional sets
setCodes.add("JMP"); // Jumpstart (replacements below) setCodes.add("JMP"); // Jumpstart (replacements below)
setCodes.add("STA"); // Strixhaven Mystical Archive setCodes.add("STA"); // Strixhaven Mystical Archive
setCodes.add("J21"); // Jumpstart: Historic Horizons
// Regular ban list // Regular ban list
banned.add("Agent of Treachery"); banned.add("Agent of Treachery");

View file

@ -0,0 +1,122 @@
package mage.cards.f;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.ChangelingAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.SubTypeSet;
import mage.filter.FilterCard;
import mage.filter.common.FilterCreatureCard;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.players.Player;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public final class FacelessAgent extends CardImpl {
public FacelessAgent(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}");
this.subtype.add(SubType.SHAPESHIFTER);
this.power = new MageInt(2);
this.toughness = new MageInt(1);
// Changeling
this.addAbility(new ChangelingAbility());
// When Faceless Agent enters the battlefield, seek a creature card of the most prevalent creature type in your library.
this.addAbility(new EntersBattlefieldTriggeredAbility(new FacelessAgentEffect()));
}
private FacelessAgent(final FacelessAgent card) {
super(card);
}
@Override
public FacelessAgent copy() {
return new FacelessAgent(this);
}
}
class FacelessAgentEffect extends OneShotEffect {
private static final FilterCard filterAnyType = new FilterCreatureCard();
static {
filterAnyType.add(FacelessAgentPredicate.instance);
}
FacelessAgentEffect() {
super(Outcome.Benefit);
staticText = "seek a creature card of the most prevalent creature type in your library";
}
private FacelessAgentEffect(final FacelessAgentEffect effect) {
super(effect);
}
@Override
public FacelessAgentEffect copy() {
return new FacelessAgentEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null || player.getLibrary().count(filterAnyType, game) < 1) {
return false;
}
Map<SubType, Integer> typeMap = player
.getLibrary()
.getCards(game)
.stream()
.filter(card -> !card.isAllCreatureTypes(game))
.map(card -> card.getSubtype(game))
.flatMap(Collection::stream)
.filter(subType -> subType.getSubTypeSet() == SubTypeSet.CreatureType)
.collect(Collectors.toMap(Function.identity(), x -> 1, Integer::sum));
if (typeMap.isEmpty()) {
return player.seekCard(filterAnyType, source, game);
}
int max = typeMap.values().stream().mapToInt(x -> x).max().orElse(0);
FilterCard filter = new FilterCreatureCard();
filter.add(Predicates.or(
typeMap.entrySet()
.stream()
.filter(entry -> entry.getValue() == max)
.map(Map.Entry::getKey)
.map(SubType::getPredicate)
.collect(Collectors.toSet())
));
return player.seekCard(filter, source, game);
}
}
enum FacelessAgentPredicate implements Predicate<Card> {
instance;
@Override
public boolean apply(Card input, Game game) {
return input.isAllCreatureTypes(game)
|| input
.getSubtype(game)
.stream()
.anyMatch(subType -> subType.getSubTypeSet() == SubTypeSet.CreatureType);
}
}

View file

@ -1,6 +1,7 @@
package mage.sets; package mage.sets;
import mage.cards.ExpansionSet; import mage.cards.ExpansionSet;
import mage.constants.Rarity;
import mage.constants.SetType; import mage.constants.SetType;
/** /**
@ -15,8 +16,10 @@ public final class JumpstartHistoricHorizons extends ExpansionSet {
} }
private JumpstartHistoricHorizons() { private JumpstartHistoricHorizons() {
super("Historic Anthology 5", "HA5", ExpansionSet.buildDate(2021, 8, 12), SetType.MAGIC_ARENA); super("Jumpstart: Historic Horizons", "J21", ExpansionSet.buildDate(2021, 8, 12), SetType.MAGIC_ARENA);
this.hasBoosters = false; this.hasBoosters = false;
this.hasBasicLands = false; this.hasBasicLands = false;
cards.add(new SetCardInfo("Faceless Agent", 31, Rarity.COMMON, mage.cards.f.FacelessAgent.class));
} }
} }

View file

@ -23,10 +23,7 @@ import mage.counters.CounterType;
import mage.counters.Counters; import mage.counters.Counters;
import mage.designations.Designation; import mage.designations.Designation;
import mage.designations.DesignationType; import mage.designations.DesignationType;
import mage.filter.Filter; import mage.filter.*;
import mage.filter.FilterMana;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.common.*; import mage.filter.common.*;
import mage.filter.predicate.Predicates; import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.NamePredicate; import mage.filter.predicate.mageobject.NamePredicate;
@ -3447,6 +3444,11 @@ public class TestPlayer implements Player {
return computerPlayer.searchLibrary(target, source, game, targetPlayerId); return computerPlayer.searchLibrary(target, source, game, targetPlayerId);
} }
@Override
public boolean seekCard(FilterCard filter, Ability source, Game game) {
return computerPlayer.seekCard(filter, source, game);
}
@Override @Override
public void lookAtAllLibraries(Ability source, Game game) { public void lookAtAllLibraries(Ability source, Game game) {
computerPlayer.lookAtAllLibraries(source, game); computerPlayer.lookAtAllLibraries(source, game);

View file

@ -20,6 +20,7 @@ import mage.counters.Counter;
import mage.counters.Counters; import mage.counters.Counters;
import mage.designations.Designation; import mage.designations.Designation;
import mage.designations.DesignationType; import mage.designations.DesignationType;
import mage.filter.FilterCard;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.game.Game; import mage.game.Game;
@ -585,6 +586,11 @@ public class PlayerStub implements Player {
return false; return false;
} }
@Override
public boolean seekCard(FilterCard filter, Ability source, Game game) {
return false;
}
@Override @Override
public void lookAtAllLibraries(Ability source, Game game) { public void lookAtAllLibraries(Ability source, Game game) {
} }

View file

@ -21,6 +21,7 @@ import mage.counters.Counter;
import mage.counters.Counters; import mage.counters.Counters;
import mage.designations.Designation; import mage.designations.Designation;
import mage.designations.DesignationType; import mage.designations.DesignationType;
import mage.filter.FilterCard;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.game.Game; import mage.game.Game;
@ -430,6 +431,12 @@ public interface Player extends MageItem, Copyable<Player> {
boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId); boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId);
/**
* Gets a random card which matches the given filter and puts it into its owner's hand
* Doesn't reveal the card
*/
boolean seekCard(FilterCard filter, Ability source, Game game);
/** /**
* Reveals all players' libraries. Useful for abilities like Jace, Architect * Reveals all players' libraries. Useful for abilities like Jace, Architect
* of Thought's -8 that have effects that require information from all * of Thought's -8 that have effects that require information from all

View file

@ -1,10 +1,6 @@
package mage.players; package mage.players;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import mage.*; import mage.*;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.ActivatedAbility.ActivationStatus; import mage.abilities.ActivatedAbility.ActivationStatus;
@ -12,11 +8,7 @@ import mage.abilities.common.PassAbility;
import mage.abilities.common.PlayLandAsCommanderAbility; import mage.abilities.common.PlayLandAsCommanderAbility;
import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; import mage.abilities.common.WhileSearchingPlayFromLibraryAbility;
import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility;
import mage.abilities.costs.AlternativeCost2; import mage.abilities.costs.*;
import mage.abilities.costs.AlternativeSourceCosts;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.OptionalAdditionalSourceCosts;
import mage.abilities.costs.mana.AlternateManaPaymentAbility; import mage.abilities.costs.mana.AlternateManaPaymentAbility;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCosts;
@ -70,6 +62,11 @@ import mage.util.GameLog;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
public abstract class PlayerImpl implements Player, Serializable { public abstract class PlayerImpl implements Player, Serializable {
private static final Logger logger = Logger.getLogger(PlayerImpl.class); private static final Logger logger = Logger.getLogger(PlayerImpl.class);
@ -637,9 +634,9 @@ public abstract class PlayerImpl implements Player, Serializable {
&& this.hasOpponent(sourceControllerId, game) && this.hasOpponent(sourceControllerId, game)
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null
&& abilities.stream() && abilities.stream()
.filter(HexproofBaseAbility.class::isInstance) .filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast) .map(HexproofBaseAbility.class::cast)
.anyMatch(ability -> ability.checkObject(source, game))) { .anyMatch(ability -> ability.checkObject(source, game))) {
return false; return false;
} }
@ -679,7 +676,7 @@ public abstract class PlayerImpl implements Player, Serializable {
game.informPlayers(getLogName() + " discards down to " game.informPlayers(getLogName() + " discards down to "
+ this.maxHandSize + this.maxHandSize
+ (this.maxHandSize == 1 + (this.maxHandSize == 1
? " hand card" : " hand cards")); ? " hand card" : " hand cards"));
} }
discard(hand.size() - this.maxHandSize, false, false, null, game); discard(hand.size() - this.maxHandSize, false, false, null, game);
} }
@ -1147,7 +1144,7 @@ public abstract class PlayerImpl implements Player, Serializable {
/** /**
* @param originalAbility * @param originalAbility
* @param game * @param game
* @param noMana cast it without paying mana costs * @param noMana cast it without paying mana costs
* @param approvingObject which object approved the cast * @param approvingObject which object approved the cast
* @return * @return
*/ */
@ -2711,6 +2708,22 @@ public abstract class PlayerImpl implements Player, Serializable {
return true; return true;
} }
@Override
public boolean seekCard(FilterCard filter, Ability source, Game game) {
Set<Card> cards = this.getLibrary()
.getCards(game)
.stream()
.filter(card -> filter.match(card, source.getSourceId(), getId(), game))
.collect(Collectors.toSet());
Card card = RandomUtil.randomFromSet(cards);
if (card == null) {
return false;
}
game.informPlayers(this.getLogName() + " seeks a card from their library");
this.moveCards(card, Zone.HAND, source, game);
return true;
}
@Override @Override
public void lookAtAllLibraries(Ability source, Game game) { public void lookAtAllLibraries(Ability source, Game game) {
for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) { for (UUID playerId : game.getState().getPlayersInRange(this.getId(), game)) {
@ -2848,7 +2861,7 @@ public abstract class PlayerImpl implements Player, Serializable {
* @param source * @param source
* @param game * @param game
* @param appliedEffects * @param appliedEffects
* @param numSides Number of sides the dice has * @param numSides Number of sides the dice has
* @return the number that the player rolled * @return the number that the player rolled
*/ */
@Override @Override
@ -2883,10 +2896,10 @@ public abstract class PlayerImpl implements Player, Serializable {
/** /**
* @param game * @param game
* @param appliedEffects * @param appliedEffects
* @param numberChaosSides The number of chaos sides the planar die * @param numberChaosSides The number of chaos sides the planar die
* currently has (normally 1 but can be 5) * currently has (normally 1 but can be 5)
* @param numberPlanarSides The number of chaos sides the planar die * @param numberPlanarSides The number of chaos sides the planar die
* currently has (normally 1) * currently has (normally 1)
* @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll
* or NilRoll * or NilRoll
*/ */
@ -2965,7 +2978,7 @@ public abstract class PlayerImpl implements Player, Serializable {
for (Card card : getHand().getCards(game)) { for (Card card : getHand().getCards(game)) {
Abilities<ActivatedManaAbilityImpl> manaAbilities Abilities<ActivatedManaAbilityImpl> manaAbilities
= card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game); = card.getAbilities(game).getAvailableActivatedManaAbilities(Zone.HAND, playerId, game);
for (Iterator<ActivatedManaAbilityImpl> it = manaAbilities.iterator(); it.hasNext();) { for (Iterator<ActivatedManaAbilityImpl> it = manaAbilities.iterator(); it.hasNext(); ) {
ActivatedManaAbilityImpl ability = it.next(); ActivatedManaAbilityImpl ability = it.next();
Abilities<ActivatedManaAbilityImpl> noTapAbilities = new AbilitiesImpl<>(ability); Abilities<ActivatedManaAbilityImpl> noTapAbilities = new AbilitiesImpl<>(ability);
if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) { if (ability.getManaCosts().isEmpty() && !ability.isPoolDependant()) {
@ -2982,7 +2995,7 @@ public abstract class PlayerImpl implements Player, Serializable {
boolean useLater = false; // sources with mana costs or mana pool dependency boolean useLater = false; // sources with mana costs or mana pool dependency
Abilities<ActivatedManaAbilityImpl> manaAbilities Abilities<ActivatedManaAbilityImpl> manaAbilities
= permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true = permanent.getAbilities(game).getAvailableActivatedManaAbilities(Zone.BATTLEFIELD, playerId, game); // returns ability only if canActivate is true
for (Iterator<ActivatedManaAbilityImpl> it = manaAbilities.iterator(); it.hasNext();) { for (Iterator<ActivatedManaAbilityImpl> it = manaAbilities.iterator(); it.hasNext(); ) {
ActivatedManaAbilityImpl ability = it.next(); ActivatedManaAbilityImpl ability = it.next();
if (canUse == null) { if (canUse == null) {
canUse = permanent.canUseActivatedAbilities(game); canUse = permanent.canUseActivatedAbilities(game);
@ -3024,7 +3037,7 @@ public abstract class PlayerImpl implements Player, Serializable {
boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production boolean usePoolDependantAbilities = false; // use such abilities later than other if possible because it can maximize mana production
while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) { while (anAbilityWasUsed && !sourceWithCosts.isEmpty()) {
anAbilityWasUsed = false; anAbilityWasUsed = false;
for (Iterator<Abilities<ActivatedManaAbilityImpl>> iterator = sourceWithCosts.iterator(); iterator.hasNext();) { for (Iterator<Abilities<ActivatedManaAbilityImpl>> iterator = sourceWithCosts.iterator(); iterator.hasNext(); ) {
Abilities<ActivatedManaAbilityImpl> manaAbilities = iterator.next(); Abilities<ActivatedManaAbilityImpl> manaAbilities = iterator.next();
if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) { if (usePoolDependantAbilities || !manaAbilities.hasPoolDependantAbilities()) {
boolean used; boolean used;
@ -3060,7 +3073,7 @@ public abstract class PlayerImpl implements Player, Serializable {
* and cleared thereafter * and cleared thereafter
* *
* @param netManaAvailable the net mana produced by the triggered mana * @param netManaAvailable the net mana produced by the triggered mana
* abaility * abaility
*/ */
@Override @Override
public void addAvailableTriggeredMana(List<Mana> netManaAvailable public void addAvailableTriggeredMana(List<Mana> netManaAvailable
@ -3142,7 +3155,7 @@ public abstract class PlayerImpl implements Player, Serializable {
/** /**
* @param ability * @param ability
* @param availableMana if null, it won't be checked if enough mana is * @param availableMana if null, it won't be checked if enough mana is
* available * available
* @param sourceObject * @param sourceObject
* @param game * @param game
* @return * @return
@ -3291,7 +3304,7 @@ public abstract class PlayerImpl implements Player, Serializable {
ManaCostsImpl manaCosts = new ManaCostsImpl(); ManaCostsImpl manaCosts = new ManaCostsImpl();
for (Cost cost : alternateSourceCostsAbility.getCosts()) { for (Cost cost : alternateSourceCostsAbility.getCosts()) {
if (cost instanceof AlternativeCost2) { if (cost instanceof AlternativeCost2) {
if(((AlternativeCost2) cost).getCost() instanceof ManaCost) { if (((AlternativeCost2) cost).getCost() instanceof ManaCost) {
manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost()); manaCosts.add((ManaCost) ((AlternativeCost2) cost).getCost());
} }
} else { } else {
@ -3570,10 +3583,10 @@ public abstract class PlayerImpl implements Player, Serializable {
* currently cast/activate with his available resources * currently cast/activate with his available resources
* *
* @param game * @param game
* @param hidden also from hidden objects (e.g. turned face down cards ?) * @param hidden also from hidden objects (e.g. turned face down cards ?)
* @param fromZone of objects from which zone (ALL = from all zones) * @param fromZone of objects from which zone (ALL = from all zones)
* @param hideDuplicatedAbilities if equal abilities exist return only the * @param hideDuplicatedAbilities if equal abilities exist return only the
* first instance * first instance
* @return * @return
*/ */
public List<ActivatedAbility> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) { public List<ActivatedAbility> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) {
@ -4152,7 +4165,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCards(Set<? extends Card> cards, Zone toZone, public boolean moveCards(Set<? extends Card> cards, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return moveCards(cards, toZone, source, game, false, false, false, null); return moveCards(cards, toZone, source, game, false, false, false, null);
} }
@ -4308,7 +4321,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// identify cards from one owner // identify cards from one owner
Cards cards = new CardsImpl(); Cards cards = new CardsImpl();
UUID ownerId = null; UUID ownerId = null;
for (Iterator<? extends Card> it = allCards.iterator(); it.hasNext();) { for (Iterator<? extends Card> it = allCards.iterator(); it.hasNext(); ) {
Card card = it.next(); Card card = it.next();
if (cards.isEmpty()) { if (cards.isEmpty()) {
ownerId = card.getOwnerId(); ownerId = card.getOwnerId();
@ -4486,7 +4499,7 @@ public abstract class PlayerImpl implements Player, Serializable {
game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName()
+ (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' '
+ (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH)
+ ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId()));
} }
} }

View file

@ -10,7 +10,7 @@ Bloodthirst|number|
Bushido|number| Bushido|number|
Buyback|manaString| Buyback|manaString|
Cascade|new| Cascade|new|
Changeling|instance| Changeling|new|
Convoke|new| Convoke|new|
Crew|number| Crew|number|
Cumulative upkeep|cost| Cumulative upkeep|cost|