- [XLN] Added Trove of Temptation. This card is ugly, so certain side effects in corner cases are probably still possible. Currently implemented as a keyword-like ability similar to "no more than X creatures can attack each turn" etc., but maybe is better as a global rule, I'm not sure (and not sure how to properly convert it to a global rule either...). Assistance and improvements are welcome.

This commit is contained in:
Agetian
2017-09-13 08:59:35 +00:00
parent 85ece1ec63
commit a0abaf62b4
5 changed files with 159 additions and 54 deletions

1
.gitattributes vendored
View File

@@ -17335,6 +17335,7 @@ forge-gui/res/cardsfolder/upcoming/tishana_voice_of_thunder.txt -text
forge-gui/res/cardsfolder/upcoming/tishanas_wayfinder.txt -text
forge-gui/res/cardsfolder/upcoming/tocatli_honor_guard.txt -text
forge-gui/res/cardsfolder/upcoming/treasure_map_treasure_cove.txt -text
forge-gui/res/cardsfolder/upcoming/trove_of_temptation.txt -text
forge-gui/res/cardsfolder/upcoming/unclaimed_territory.txt -text
forge-gui/res/cardsfolder/upcoming/unfriendly_fire.txt -text
forge-gui/res/cardsfolder/upcoming/vanquishers_banner.txt -text

View File

@@ -1,41 +1,24 @@
package forge.game.combat;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.*;
import com.google.common.primitives.Ints;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import forge.util.maps.MapToAmountUtil;
import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount;
import forge.util.maps.MapToAmountUtil;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
public class AttackConstraints {
@@ -180,7 +163,9 @@ public class AttackConstraints {
// Now try all others (plus empty attack) and count their violations
final FCollection<Map<Card, GameEntity>> legalAttackers = collectLegalAttackers(reqs, myMax);
possible.putAll(Maps.asMap(legalAttackers.asSet(), FN_COUNT_VIOLATIONS));
possible.put(Collections.<Card, GameEntity>emptyMap(), countViolations(Collections.<Card, GameEntity>emptyMap()));
if (countViolations(Collections.<Card, GameEntity>emptyMap()) != -1) {
possible.put(Collections.<Card, GameEntity>emptyMap(), countViolations(Collections.<Card, GameEntity>emptyMap()));
}
// take the case with the fewest violations
return MapToAmountUtil.min(possible);
@@ -385,7 +370,7 @@ public class AttackConstraints {
* restriction is violated.
*/
public final int countViolations(final Map<Card, GameEntity> attackers) {
if (!globalRestrictions.isLegal(attackers)) {
if (!globalRestrictions.isLegal(attackers, possibleAttackers)) {
return -1;
}
for (final Entry<Card, GameEntity> attacker : attackers.entrySet()) {

View File

@@ -1,31 +1,37 @@
package forge.game.combat;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
import forge.util.maps.MapToAmountUtil;
import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount;
import forge.util.maps.MapToAmountUtil;
import org.apache.commons.lang3.tuple.Pair;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class AttackRequirement {
private final MapToAmount<GameEntity> defenderSpecific;
private final MapToAmount<GameEntity> defenderOrPWSpecific;
private final Map<GameEntity, List<GameEntity>> defenderSpecificAlternatives;
private final MapToAmount<Card> causesToAttack;
public AttackRequirement(final Card attacker, final MapToAmount<Card> causesToAttack, final FCollectionView<GameEntity> possibleDefenders) {
this.defenderSpecific = new LinkedHashMapToAmount<GameEntity>();
this.defenderOrPWSpecific = new LinkedHashMapToAmount<GameEntity>();
this.defenderSpecificAlternatives = new HashMap<GameEntity, List<GameEntity>>();
this.causesToAttack = causesToAttack;
final GameEntity mustAttack = attacker.getController().getMustAttackEntity();
@@ -64,19 +70,43 @@ public class AttackRequirement {
}
final Game game = attacker.getGame();
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
if (c.hasKeyword("Each opponent must attack you or a planeswalker you control with at least one creature each combat if able.")) {
if (attacker.getController().isOpponentOf(c.getController()) && !defenderOrPWSpecific.containsKey(c.getController())) {
defenderOrPWSpecific.put(c.getController(), 1);
for (Card pw : CardLists.filter(c.getController().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANEWALKERS)) {
// Add the attack alternatives that suffice (planeswalkers that can be attacked instead of the player)
if (!defenderSpecificAlternatives.containsKey(c.getController())) {
defenderSpecificAlternatives.put(c.getController(), Lists.<GameEntity>newArrayList());
}
defenderSpecificAlternatives.get(c.getController()).add(pw);
}
}
}
}
for (final GameEntity defender : possibleDefenders) {
if (CombatUtil.getAttackCost(game, attacker, defender) == null) {
// use put here because we want to always put it, even if the value is 0
defenderSpecific.put(defender, Integer.valueOf(defenderSpecific.count(defender) + nAttackAnything));
if (defenderOrPWSpecific.containsKey(defender)) {
defenderOrPWSpecific.put(defender, Integer.valueOf(defenderOrPWSpecific.count(defender) + nAttackAnything));
}
} else {
defenderSpecific.remove(defender);
defenderOrPWSpecific.remove(defender);
}
}
// Remove GameEntities that are no longer on the battlefield or are
// related to Players who have lost the game
final List<GameEntity> toRemove = Lists.newArrayListWithCapacity(defenderSpecific.size());
for (final GameEntity entity : defenderSpecific.keySet()) {
final MapToAmount<GameEntity> combinedDefMap = new LinkedHashMapToAmount<>();
combinedDefMap.putAll(defenderSpecific);
combinedDefMap.putAll(defenderOrPWSpecific);
final List<GameEntity> toRemove = Lists.newArrayListWithCapacity(combinedDefMap.size());
for (final GameEntity entity : combinedDefMap.keySet()) {
boolean removeThis = false;
if (entity instanceof Player) {
if (((Player) entity).hasLost()) {
@@ -94,11 +124,12 @@ public class AttackRequirement {
}
for (final GameEntity entity : toRemove) {
defenderSpecific.remove(entity);
defenderOrPWSpecific.remove(entity);
}
}
public boolean hasRequirement() {
return !defenderSpecific.isEmpty() || !causesToAttack.isEmpty();
return !defenderSpecific.isEmpty() || !causesToAttack.isEmpty() || !defenderOrPWSpecific.isEmpty();
}
public final MapToAmount<Card> getCausesToAttack() {
@@ -111,7 +142,36 @@ public class AttackRequirement {
}
final boolean isAttacking = defender != null;
int violations = defenderSpecific.countAll() - (isAttacking ? defenderSpecific.count(defender) : 0);
int violations = 0;
// first. check to see if "must attack X or Y with at least one creature" requirements are satisfied
List<GameEntity> toRemoveFromDefSpecific = Lists.<GameEntity>newArrayList();
if (!defenderOrPWSpecific.isEmpty()) {
for (GameEntity def : defenderOrPWSpecific.keySet()) {
if (defenderSpecificAlternatives.containsKey(def)) {
boolean isAttackingDefender = false;
outer: for (Card atk : attackers.keySet()) {
// is anyone attacking this defender or any of the alternative defenders?
if (attackers.get(atk).equals(def)) {
isAttackingDefender = true;
break;
}
for (GameEntity altDef : defenderSpecificAlternatives.get(def)) {
if (attackers.get(atk).equals(altDef)) {
isAttackingDefender = true;
break outer;
}
}
}
if (!isAttackingDefender) {
violations++; // no one is attacking that defender or any of his PWs
}
}
}
}
// now, count everything else
violations += defenderSpecific.countAll() - (isAttacking ? (defenderSpecific.count(defender)) : 0);
if (isAttacking) {
for (final Map.Entry<Card, Integer> mustAttack : causesToAttack.entrySet()) {
// only count violations if the forced creature can actually attack and has no cost incurred for doing so
@@ -126,6 +186,7 @@ public class AttackRequirement {
public List<Pair<GameEntity, Integer>> getSortedRequirements() {
final List<Pair<GameEntity, Integer>> result = Lists.newArrayListWithExpectedSize(defenderSpecific.size());
result.addAll(MapToAmountUtil.sort(defenderSpecific));
result.addAll(MapToAmountUtil.sort(defenderOrPWSpecific));
for (int i = 0; i < result.size(); i++) {
final Pair<GameEntity, Integer> def = result.get(i);

View File

@@ -1,28 +1,31 @@
package forge.game.combat;
import java.util.Map;
import java.util.Map.Entry;
import com.google.common.primitives.Ints;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
import forge.util.maps.MapToAmountUtil;
import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount;
import forge.util.maps.MapToAmountUtil;
import java.util.Map;
import java.util.Map.Entry;
public class GlobalAttackRestrictions {
private final int max;
private final MapToAmount<GameEntity> defenderMax;
private GlobalAttackRestrictions(final int max, final MapToAmount<GameEntity> defenderMax) {
private final PlayerCollection mustBeAttackedByEachOpp;
private GlobalAttackRestrictions(final int max, final MapToAmount<GameEntity> defenderMax, PlayerCollection mustBeAttackedByEachOpp) {
this.max = max;
this.defenderMax = defenderMax;
this.mustBeAttackedByEachOpp = mustBeAttackedByEachOpp;
}
public int getMax() {
@@ -32,17 +35,17 @@ public class GlobalAttackRestrictions {
return defenderMax;
}
public boolean isLegal(final Map<Card, GameEntity> attackers) {
return !getViolations(attackers, true).isViolated();
public boolean isLegal(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) {
return !getViolations(attackers, possibleAttackers,true).isViolated();
}
public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers) {
return getViolations(attackers, false);
public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) {
return getViolations(attackers, possibleAttackers,false);
}
private GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final boolean returnQuickly) {
private GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers, final boolean returnQuickly) {
final int nTooMany = max < 0 ? 0 : attackers.size() - max;
if (returnQuickly && nTooMany > 0) {
return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.<GameEntity>emptyMap());
return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.<GameEntity>emptyMap(), MapToAmountUtil.<GameEntity>emptyMap());
}
final MapToAmount<GameEntity> defenderTooMany = new LinkedHashMapToAmount<GameEntity>(defenderMax.size());
@@ -73,18 +76,49 @@ public class GlobalAttackRestrictions {
}
}
return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany);
final MapToAmount<GameEntity> defenderTooFew = new LinkedHashMapToAmount<GameEntity>(defenderMax.size());
for (final GameEntity mandatoryDef : mustBeAttackedByEachOpp) {
// check to ensure that this defender can even legally be attacked in the first place
boolean canAttackThisDef = false;
for (Card c : possibleAttackers) {
if (CombatUtil.canAttack(c, mandatoryDef) && null == CombatUtil.getAttackCost(c.getGame(), c, mandatoryDef)) {
canAttackThisDef = true;
break;
}
}
if (!canAttackThisDef) {
continue;
}
boolean isAttacked = false;
for (final GameEntity defender : attackers.values()) {
if (defender.equals(mandatoryDef)) {
isAttacked = true;
break;
} else if (defender instanceof Card && ((Card)defender).getController().equals(mandatoryDef)) {
isAttacked = true;
break;
}
}
if (!isAttacked) {
defenderTooFew.add(mandatoryDef);
}
}
return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany, defenderTooFew);
}
final class GlobalAttackRestrictionViolations {
private final boolean isViolated;
private final int globalTooMany;
private final MapToAmount<GameEntity> defenderTooMany;
private final MapToAmount<GameEntity> defenderTooFew;
public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount<GameEntity> defenderTooMany) {
this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty();
public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount<GameEntity> defenderTooMany, final MapToAmount<GameEntity> defenderTooFew) {
this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty() || !defenderTooFew.isEmpty();
this.globalTooMany = globalTooMany;
this.defenderTooMany = defenderTooMany;
this.defenderTooFew = defenderTooFew;
}
public boolean isViolated() {
return isViolated;
@@ -92,6 +126,9 @@ public class GlobalAttackRestrictions {
public int getGlobalTooMany() {
return globalTooMany;
}
public MapToAmount<GameEntity> getDefenderTooFew() {
return defenderTooFew;
}
public MapToAmount<GameEntity> getDefenderTooMany() {
return defenderTooMany;
}
@@ -102,13 +139,14 @@ public class GlobalAttackRestrictions {
* Get all global restrictions (applying to all creatures).
* </p>
*
* @param player
* @param attackingPlayer
* the {@link Player} declaring attack.
* @return a {@link GlobalAttackRestrictions} object.
*/
public static GlobalAttackRestrictions getGlobalRestrictions(final Player attackingPlayer, final FCollectionView<GameEntity> possibleDefenders) {
int max = -1;
final MapToAmount<GameEntity> defenderMax = new LinkedHashMapToAmount<GameEntity>(possibleDefenders.size());
final PlayerCollection mustBeAttacked = new PlayerCollection();
final Game game = attackingPlayer.getGame();
if (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.onlyOneAttackerATurn)) {
@@ -132,6 +170,14 @@ public class GlobalAttackRestrictions {
}
}
for (final Card card : game.getCardsIn(ZoneType.Battlefield)) {
if (card.hasKeyword("Each opponent must attack you or a planeswalker you control with at least one creature each combat if able.")) {
if (attackingPlayer.isOpponentOf(card.getController())) {
mustBeAttacked.add(card.getController());
}
}
}
for (final GameEntity defender : possibleDefenders) {
final int defMax = getMaxAttackTo(defender);
if (defMax != -1) {
@@ -142,7 +188,8 @@ public class GlobalAttackRestrictions {
// maximum on each defender, global maximum is sum of these
max = Ints.min(max, defenderMax.countAll());
}
return new GlobalAttackRestrictions(max, defenderMax);
return new GlobalAttackRestrictions(max, defenderMax, mustBeAttacked);
}
/**

View File

@@ -0,0 +1,11 @@
Name:Trove of Temptation
ManaCost:3 R
Types:Enchantment
K:Each opponent must attack you or a planeswalker you control with at least one creature each combat if able.
T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ DBTreasureToken | TriggerDescription$ At the beginning of your end step, create a colorless Treasure artifact token with "{T}, Sacrifice this artifact: Add one mana of any color to your mana pool.
# TODO: How many Treasure tokens with different art are there?
SVar:DBTreasureToken:DB$ Token | TokenAmount$ 1 | TokenName$ Treasure | TokenTypes$ Artifact,Treasure | TokenOwner$ You | TokenColors$ Colorless | TokenImage$ c treasure | TokenAbilities$ ABTreasureMana | TokenAltImages$ c_treasure2,c_treasure3
SVar:ABTreasureMana:AB$ Mana | Cost$ T Sac<1/CARDNAME> | Produced$ Any | Amount$ 1 | SpellDescription$ Add one mana of any color to your mana pool.
DeckHas:Ability$Token
SVar:Picture:http://www.wizards.com/global/images/magic/general/trove_of_temptation.jpg
Oracle:Each opponent must attack you or a planeswalker you control with at least one creature each combat if able.\nAt the beginning of your end step, create a colorless Treasure artifact token with "{T}, Sacrifice this artifact: Add one mana of any color to your mana pool.