mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-11 16:26:22 +00:00
- [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:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
11
forge-gui/res/cardsfolder/upcoming/trove_of_temptation.txt
Normal file
11
forge-gui/res/cardsfolder/upcoming/trove_of_temptation.txt
Normal 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.
|
||||
Reference in New Issue
Block a user