diff --git a/.gitattributes b/.gitattributes index 6f989786377..47e63baca1c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java index 55ec882f20b..b0c29a10041 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackConstraints.java +++ b/forge-game/src/main/java/forge/game/combat/AttackConstraints.java @@ -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> legalAttackers = collectLegalAttackers(reqs, myMax); possible.putAll(Maps.asMap(legalAttackers.asSet(), FN_COUNT_VIOLATIONS)); - possible.put(Collections.emptyMap(), countViolations(Collections.emptyMap())); + if (countViolations(Collections.emptyMap()) != -1) { + possible.put(Collections.emptyMap(), countViolations(Collections.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 attackers) { - if (!globalRestrictions.isLegal(attackers)) { + if (!globalRestrictions.isLegal(attackers, possibleAttackers)) { return -1; } for (final Entry attacker : attackers.entrySet()) { diff --git a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java index d12ddae0183..eb619a8d1fb 100644 --- a/forge-game/src/main/java/forge/game/combat/AttackRequirement.java +++ b/forge-game/src/main/java/forge/game/combat/AttackRequirement.java @@ -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 defenderSpecific; + private final MapToAmount defenderOrPWSpecific; + private final Map> defenderSpecificAlternatives; private final MapToAmount causesToAttack; public AttackRequirement(final Card attacker, final MapToAmount causesToAttack, final FCollectionView possibleDefenders) { this.defenderSpecific = new LinkedHashMapToAmount(); + this.defenderOrPWSpecific = new LinkedHashMapToAmount(); + this.defenderSpecificAlternatives = new HashMap>(); + 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.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 toRemove = Lists.newArrayListWithCapacity(defenderSpecific.size()); - for (final GameEntity entity : defenderSpecific.keySet()) { + final MapToAmount combinedDefMap = new LinkedHashMapToAmount<>(); + combinedDefMap.putAll(defenderSpecific); + combinedDefMap.putAll(defenderOrPWSpecific); + + final List 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 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 toRemoveFromDefSpecific = Lists.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 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> getSortedRequirements() { final List> 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 def = result.get(i); diff --git a/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java b/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java index 6ca69a55066..8435d7d2313 100644 --- a/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java +++ b/forge-game/src/main/java/forge/game/combat/GlobalAttackRestrictions.java @@ -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 defenderMax; - private GlobalAttackRestrictions(final int max, final MapToAmount defenderMax) { + private final PlayerCollection mustBeAttackedByEachOpp; + private GlobalAttackRestrictions(final int max, final MapToAmount 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 attackers) { - return !getViolations(attackers, true).isViolated(); + public boolean isLegal(final Map attackers, final CardCollection possibleAttackers) { + return !getViolations(attackers, possibleAttackers,true).isViolated(); } - public GlobalAttackRestrictionViolations getViolations(final Map attackers) { - return getViolations(attackers, false); + public GlobalAttackRestrictionViolations getViolations(final Map attackers, final CardCollection possibleAttackers) { + return getViolations(attackers, possibleAttackers,false); } - private GlobalAttackRestrictionViolations getViolations(final Map attackers, final boolean returnQuickly) { + private GlobalAttackRestrictionViolations getViolations(final Map 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.emptyMap()); + return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.emptyMap(), MapToAmountUtil.emptyMap()); } final MapToAmount defenderTooMany = new LinkedHashMapToAmount(defenderMax.size()); @@ -73,18 +76,49 @@ public class GlobalAttackRestrictions { } } - return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany); + final MapToAmount defenderTooFew = new LinkedHashMapToAmount(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 defenderTooMany; + private final MapToAmount defenderTooFew; - public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount defenderTooMany) { - this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty(); + public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount defenderTooMany, final MapToAmount 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 getDefenderTooFew() { + return defenderTooFew; + } public MapToAmount getDefenderTooMany() { return defenderTooMany; } @@ -102,13 +139,14 @@ public class GlobalAttackRestrictions { * Get all global restrictions (applying to all creatures). *

* - * @param player + * @param attackingPlayer * the {@link Player} declaring attack. * @return a {@link GlobalAttackRestrictions} object. */ public static GlobalAttackRestrictions getGlobalRestrictions(final Player attackingPlayer, final FCollectionView possibleDefenders) { int max = -1; final MapToAmount defenderMax = new LinkedHashMapToAmount(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); } /** diff --git a/forge-gui/res/cardsfolder/upcoming/trove_of_temptation.txt b/forge-gui/res/cardsfolder/upcoming/trove_of_temptation.txt new file mode 100644 index 00000000000..ac04a299955 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/trove_of_temptation.txt @@ -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.