- [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/tishanas_wayfinder.txt -text
forge-gui/res/cardsfolder/upcoming/tocatli_honor_guard.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/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/unclaimed_territory.txt -text
forge-gui/res/cardsfolder/upcoming/unfriendly_fire.txt -text forge-gui/res/cardsfolder/upcoming/unfriendly_fire.txt -text
forge-gui/res/cardsfolder/upcoming/vanquishers_banner.txt -text forge-gui/res/cardsfolder/upcoming/vanquishers_banner.txt -text

View File

@@ -1,41 +1,24 @@
package forge.game.combat; 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.Function;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Collections2; import com.google.common.collect.*;
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.primitives.Ints; import com.google.common.primitives.Ints;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.card.Card; import forge.game.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.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import forge.util.maps.MapToAmountUtil;
import forge.util.maps.LinkedHashMapToAmount; import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount; 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 { public class AttackConstraints {
@@ -180,7 +163,9 @@ public class AttackConstraints {
// Now try all others (plus empty attack) and count their violations // Now try all others (plus empty attack) and count their violations
final FCollection<Map<Card, GameEntity>> legalAttackers = collectLegalAttackers(reqs, myMax); final FCollection<Map<Card, GameEntity>> legalAttackers = collectLegalAttackers(reqs, myMax);
possible.putAll(Maps.asMap(legalAttackers.asSet(), FN_COUNT_VIOLATIONS)); 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 // take the case with the fewest violations
return MapToAmountUtil.min(possible); return MapToAmountUtil.min(possible);
@@ -385,7 +370,7 @@ public class AttackConstraints {
* restriction is violated. * restriction is violated.
*/ */
public final int countViolations(final Map<Card, GameEntity> attackers) { public final int countViolations(final Map<Card, GameEntity> attackers) {
if (!globalRestrictions.isLegal(attackers)) { if (!globalRestrictions.isLegal(attackers, possibleAttackers)) {
return -1; return -1;
} }
for (final Entry<Card, GameEntity> attacker : attackers.entrySet()) { for (final Entry<Card, GameEntity> attacker : attackers.entrySet()) {

View File

@@ -1,31 +1,37 @@
package forge.game.combat; 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.base.Function;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import forge.util.maps.MapToAmountUtil;
import forge.util.maps.LinkedHashMapToAmount; import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount; 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 { public class AttackRequirement {
private final MapToAmount<GameEntity> defenderSpecific; private final MapToAmount<GameEntity> defenderSpecific;
private final MapToAmount<GameEntity> defenderOrPWSpecific;
private final Map<GameEntity, List<GameEntity>> defenderSpecificAlternatives;
private final MapToAmount<Card> causesToAttack; private final MapToAmount<Card> causesToAttack;
public AttackRequirement(final Card attacker, final MapToAmount<Card> causesToAttack, final FCollectionView<GameEntity> possibleDefenders) { public AttackRequirement(final Card attacker, final MapToAmount<Card> causesToAttack, final FCollectionView<GameEntity> possibleDefenders) {
this.defenderSpecific = new LinkedHashMapToAmount<GameEntity>(); this.defenderSpecific = new LinkedHashMapToAmount<GameEntity>();
this.defenderOrPWSpecific = new LinkedHashMapToAmount<GameEntity>();
this.defenderSpecificAlternatives = new HashMap<GameEntity, List<GameEntity>>();
this.causesToAttack = causesToAttack; this.causesToAttack = causesToAttack;
final GameEntity mustAttack = attacker.getController().getMustAttackEntity(); final GameEntity mustAttack = attacker.getController().getMustAttackEntity();
@@ -64,19 +70,43 @@ public class AttackRequirement {
} }
final Game game = attacker.getGame(); 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) { for (final GameEntity defender : possibleDefenders) {
if (CombatUtil.getAttackCost(game, attacker, defender) == null) { if (CombatUtil.getAttackCost(game, attacker, defender) == null) {
// use put here because we want to always put it, even if the value is 0 // 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)); defenderSpecific.put(defender, Integer.valueOf(defenderSpecific.count(defender) + nAttackAnything));
if (defenderOrPWSpecific.containsKey(defender)) {
defenderOrPWSpecific.put(defender, Integer.valueOf(defenderOrPWSpecific.count(defender) + nAttackAnything));
}
} else { } else {
defenderSpecific.remove(defender); defenderSpecific.remove(defender);
defenderOrPWSpecific.remove(defender);
} }
} }
// Remove GameEntities that are no longer on the battlefield or are // Remove GameEntities that are no longer on the battlefield or are
// related to Players who have lost the game // related to Players who have lost the game
final List<GameEntity> toRemove = Lists.newArrayListWithCapacity(defenderSpecific.size()); final MapToAmount<GameEntity> combinedDefMap = new LinkedHashMapToAmount<>();
for (final GameEntity entity : defenderSpecific.keySet()) { combinedDefMap.putAll(defenderSpecific);
combinedDefMap.putAll(defenderOrPWSpecific);
final List<GameEntity> toRemove = Lists.newArrayListWithCapacity(combinedDefMap.size());
for (final GameEntity entity : combinedDefMap.keySet()) {
boolean removeThis = false; boolean removeThis = false;
if (entity instanceof Player) { if (entity instanceof Player) {
if (((Player) entity).hasLost()) { if (((Player) entity).hasLost()) {
@@ -94,11 +124,12 @@ public class AttackRequirement {
} }
for (final GameEntity entity : toRemove) { for (final GameEntity entity : toRemove) {
defenderSpecific.remove(entity); defenderSpecific.remove(entity);
defenderOrPWSpecific.remove(entity);
} }
} }
public boolean hasRequirement() { public boolean hasRequirement() {
return !defenderSpecific.isEmpty() || !causesToAttack.isEmpty(); return !defenderSpecific.isEmpty() || !causesToAttack.isEmpty() || !defenderOrPWSpecific.isEmpty();
} }
public final MapToAmount<Card> getCausesToAttack() { public final MapToAmount<Card> getCausesToAttack() {
@@ -111,7 +142,36 @@ public class AttackRequirement {
} }
final boolean isAttacking = defender != null; 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) { if (isAttacking) {
for (final Map.Entry<Card, Integer> mustAttack : causesToAttack.entrySet()) { 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 // 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() { public List<Pair<GameEntity, Integer>> getSortedRequirements() {
final List<Pair<GameEntity, Integer>> result = Lists.newArrayListWithExpectedSize(defenderSpecific.size()); final List<Pair<GameEntity, Integer>> result = Lists.newArrayListWithExpectedSize(defenderSpecific.size());
result.addAll(MapToAmountUtil.sort(defenderSpecific)); result.addAll(MapToAmountUtil.sort(defenderSpecific));
result.addAll(MapToAmountUtil.sort(defenderOrPWSpecific));
for (int i = 0; i < result.size(); i++) { for (int i = 0; i < result.size(); i++) {
final Pair<GameEntity, Integer> def = result.get(i); final Pair<GameEntity, Integer> def = result.get(i);

View File

@@ -1,28 +1,31 @@
package forge.game.combat; package forge.game.combat;
import java.util.Map;
import java.util.Map.Entry;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GlobalRuleChange; import forge.game.GlobalRuleChange;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import forge.util.maps.MapToAmountUtil;
import forge.util.maps.LinkedHashMapToAmount; import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount; import forge.util.maps.MapToAmount;
import forge.util.maps.MapToAmountUtil;
import java.util.Map;
import java.util.Map.Entry;
public class GlobalAttackRestrictions { public class GlobalAttackRestrictions {
private final int max; private final int max;
private final MapToAmount<GameEntity> defenderMax; 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.max = max;
this.defenderMax = defenderMax; this.defenderMax = defenderMax;
this.mustBeAttackedByEachOpp = mustBeAttackedByEachOpp;
} }
public int getMax() { public int getMax() {
@@ -32,17 +35,17 @@ public class GlobalAttackRestrictions {
return defenderMax; return defenderMax;
} }
public boolean isLegal(final Map<Card, GameEntity> attackers) { public boolean isLegal(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) {
return !getViolations(attackers, true).isViolated(); return !getViolations(attackers, possibleAttackers,true).isViolated();
} }
public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers) { public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) {
return getViolations(attackers, false); 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; final int nTooMany = max < 0 ? 0 : attackers.size() - max;
if (returnQuickly && nTooMany > 0) { 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()); 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 { final class GlobalAttackRestrictionViolations {
private final boolean isViolated; private final boolean isViolated;
private final int globalTooMany; private final int globalTooMany;
private final MapToAmount<GameEntity> defenderTooMany; private final MapToAmount<GameEntity> defenderTooMany;
private final MapToAmount<GameEntity> defenderTooFew;
public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount<GameEntity> defenderTooMany) { public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount<GameEntity> defenderTooMany, final MapToAmount<GameEntity> defenderTooFew) {
this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty(); this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty() || !defenderTooFew.isEmpty();
this.globalTooMany = globalTooMany; this.globalTooMany = globalTooMany;
this.defenderTooMany = defenderTooMany; this.defenderTooMany = defenderTooMany;
this.defenderTooFew = defenderTooFew;
} }
public boolean isViolated() { public boolean isViolated() {
return isViolated; return isViolated;
@@ -92,6 +126,9 @@ public class GlobalAttackRestrictions {
public int getGlobalTooMany() { public int getGlobalTooMany() {
return globalTooMany; return globalTooMany;
} }
public MapToAmount<GameEntity> getDefenderTooFew() {
return defenderTooFew;
}
public MapToAmount<GameEntity> getDefenderTooMany() { public MapToAmount<GameEntity> getDefenderTooMany() {
return defenderTooMany; return defenderTooMany;
} }
@@ -102,13 +139,14 @@ public class GlobalAttackRestrictions {
* Get all global restrictions (applying to all creatures). * Get all global restrictions (applying to all creatures).
* </p> * </p>
* *
* @param player * @param attackingPlayer
* the {@link Player} declaring attack. * the {@link Player} declaring attack.
* @return a {@link GlobalAttackRestrictions} object. * @return a {@link GlobalAttackRestrictions} object.
*/ */
public static GlobalAttackRestrictions getGlobalRestrictions(final Player attackingPlayer, final FCollectionView<GameEntity> possibleDefenders) { public static GlobalAttackRestrictions getGlobalRestrictions(final Player attackingPlayer, final FCollectionView<GameEntity> possibleDefenders) {
int max = -1; int max = -1;
final MapToAmount<GameEntity> defenderMax = new LinkedHashMapToAmount<GameEntity>(possibleDefenders.size()); final MapToAmount<GameEntity> defenderMax = new LinkedHashMapToAmount<GameEntity>(possibleDefenders.size());
final PlayerCollection mustBeAttacked = new PlayerCollection();
final Game game = attackingPlayer.getGame(); final Game game = attackingPlayer.getGame();
if (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.onlyOneAttackerATurn)) { 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) { for (final GameEntity defender : possibleDefenders) {
final int defMax = getMaxAttackTo(defender); final int defMax = getMaxAttackTo(defender);
if (defMax != -1) { if (defMax != -1) {
@@ -142,7 +188,8 @@ public class GlobalAttackRestrictions {
// maximum on each defender, global maximum is sum of these // maximum on each defender, global maximum is sum of these
max = Ints.min(max, defenderMax.countAll()); 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.