Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 7 additions & 46 deletions forge-game/src/main/java/forge/game/combat/AttackConstraints.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,17 @@
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.google.common.collect.*;
import forge.util.IterableUtil;
import org.apache.commons.lang3.tuple.Pair;

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.CounterEnumType;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import forge.util.maps.LinkedHashMapToAmount;
Expand All @@ -41,52 +37,17 @@ public class AttackConstraints {
private final List<Set<GameEntity>> playerRequirements;

public AttackConstraints(final Combat combat) {
final Game game = combat.getAttackingPlayer().getGame();
possibleAttackers = combat.getAttackingPlayer().getCreaturesInPlay();
possibleDefenders = combat.getDefenders();
globalRestrictions = GlobalAttackRestrictions.getGlobalRestrictions(combat.getAttackingPlayer(), possibleDefenders);
playerRequirements = StaticAbilityMustAttack.mustAttackSpecific(combat.getAttackingPlayer(), possibleDefenders);

// Number of "must attack" constraints on each creature with a magnet counter (equal to the number of permanents requiring that constraint).
int nMagnetRequirements = 0;
final CardCollectionView magnetAttackers = CardLists.filter(possibleAttackers, CardPredicates.hasCounter(CounterEnumType.MAGNET));
// Only require if a creature with a magnet counter on it attacks.
if (!magnetAttackers.isEmpty()) {
nMagnetRequirements = CardLists.getAmountOfKeyword(
game.getCardsIn(ZoneType.Battlefield),
"If a creature with a magnet counter on it attacks, all creatures with magnet counters on them attack if able.");
}

final MapToAmount<Card> attacksIfOtherAttacks = new LinkedHashMapToAmount<>();
for (final Card possibleAttacker : possibleAttackers) {
attacksIfOtherAttacks.add(possibleAttacker, possibleAttacker.getAmountOfKeyword("If a creature you control attacks, CARDNAME also attacks if able."));
}

// TODO extend for "SharedTurnModes"
for (final Card possibleAttacker : possibleAttackers) {
restrictions.put(possibleAttacker, new AttackRestriction(possibleAttacker, possibleDefenders));

final MapToAmount<Card> causesToAttack = new LinkedHashMapToAmount<>();
for (final Entry<Card, Integer> entry : attacksIfOtherAttacks.entrySet()) {
if (entry.getKey() != possibleAttacker) {
causesToAttack.add(entry.getKey(), entry.getValue());
}
}

// Number of "all must attack" requirements on this attacker
final int nAllMustAttack = possibleAttacker.getAmountOfKeyword("If CARDNAME attacks, all creatures you control attack if able.");
for (final Card c : possibleAttackers) {
if (c != possibleAttacker) {
causesToAttack.add(c, nAllMustAttack);
}
}

if (possibleAttacker.getCounters(CounterEnumType.MAGNET) > 0) {
for (final Card c : magnetAttackers) {
if (c != possibleAttacker) {
causesToAttack.add(c, nMagnetRequirements);
}
}
}
final Multimap<Card, StaticAbility> causesToAttack = StaticAbilityMustAttack.getAttackRequirements(possibleAttacker,
possibleAttackers.stream().filter(p -> !p.equals(possibleAttacker)).collect(Collectors.toList()));

final AttackRequirement r = new AttackRequirement(possibleAttacker, causesToAttack, possibleDefenders);
requirements.put(possibleAttacker, r);
Expand Down Expand Up @@ -247,9 +208,9 @@ private List<Map<Card, GameEntity>> collectLegalAttackers(final Map<Card, GameEn

if (!requirement.getCausesToAttack().isEmpty()) {
final List<Attack> clonedReqs = deepClone(reqs);
for (final Entry<Card, Integer> causesToAttack : requirement.getCausesToAttack().entrySet()) {
for (final Entry<Card, Collection<StaticAbility>> causesToAttack : requirement.getCausesToAttack().asMap().entrySet()) {
for (final Attack a : findAll(reqs, causesToAttack.getKey())) {
a.requirements += causesToAttack.getValue();
a.requirements += causesToAttack.getValue().size();
}
}
// if maximum < no of possible attackers, try both with and without this creature
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package forge.game.combat;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMustAttack;
import org.apache.commons.lang3.tuple.Pair;

import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;

import forge.game.Game;
import forge.game.GameEntity;
Expand All @@ -21,10 +24,10 @@
public class AttackRequirement {

private final MapToAmount<GameEntity> defenderSpecific;
private final MapToAmount<Card> causesToAttack;
private final Multimap<Card, StaticAbility> causesToAttack;
private final Card attacker;

public AttackRequirement(final Card attacker, final MapToAmount<Card> causesToAttack, final FCollectionView<GameEntity> possibleDefenders) {
public AttackRequirement(final Card attacker, final Multimap<Card, StaticAbility> causesToAttack, final FCollectionView<GameEntity> possibleDefenders) {
this.defenderSpecific = new LinkedHashMapToAmount<>();
this.attacker = attacker;
this.causesToAttack = causesToAttack;
Expand Down Expand Up @@ -81,10 +84,10 @@ public Card getAttacker() {
}

public boolean hasRequirement() {
return defenderSpecific.countAll() > 0 || causesToAttack.countAll() > 0;
return defenderSpecific.countAll() > 0 || !causesToAttack.isEmpty();
}

public final MapToAmount<Card> getCausesToAttack() {
public final Multimap<Card, StaticAbility> getCausesToAttack() {
return causesToAttack;
}

Expand All @@ -101,13 +104,13 @@ public int countViolations(final GameEntity defender, final Map<Card, GameEntity

// check if a restriction will apply such that the requirement is no longer relevant
if (attackers.size() != 1 || !constraints.get(attackers.entrySet().iterator().next().getKey()).getTypes().contains(AttackRestrictionType.ONLY_ALONE)) {
for (final Map.Entry<Card, Integer> mustAttack : causesToAttack.entrySet()) {
for (final Map.Entry<Card, Collection<StaticAbility>> mustAttack : causesToAttack.asMap().entrySet()) {
if (constraints.get(mustAttack.getKey()).getTypes().contains(AttackRestrictionType.ONLY_ALONE)) continue;
int max = Objects.requireNonNullElse(GlobalAttackRestrictions.getGlobalRestrictions(mustAttack.getKey().getController(), combat.getDefenders()).getMax(), Integer.MAX_VALUE);

// only count violations if the forced creature can actually attack and has no cost incurred for doing so
if (attackers.size() < max && !attackers.containsKey(mustAttack.getKey()) && CombatUtil.canAttack(mustAttack.getKey()) && CombatUtil.getAttackCost(defender.getGame(), mustAttack.getKey(), defender) == null) {
violations += mustAttack.getValue();
violations += mustAttack.getValue().size();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ public enum StaticAbilityMode {
TurnReversed,
PhaseReversed,

// StaticAbilityAttackRequirement
AttackRequirement,

// StaticAbilityCountersRemain
CountersRemain,
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import java.util.List;
import java.util.Set;

import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;

public class StaticAbilityMustAttack {

public static List<GameEntity> entitiesMustAttack(final Card attacker) {
Expand Down Expand Up @@ -65,4 +68,26 @@ public static List<Set<GameEntity>> mustAttackSpecific(final Player attackingPla
}
return defToAtt;
}

public static Multimap<Card, StaticAbility> getAttackRequirements(final Card card, Iterable<Card> other) {
Multimap<Card, StaticAbility> result = MultimapBuilder.hashKeys().arrayListValues().build();
for (final Card ca : card.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions(StaticAbilityMode.AttackRequirement)) {
continue;
}

if (!stAb.matchesValidParam("ValidCard", card)) {
continue;
}
for (final Card co : other) {
if (stAb.matchesValidParam("ValidAttacker", co)) {
result.put(co, stAb);
}
}
}
}

return result;
}
}
4 changes: 2 additions & 2 deletions forge-gui/res/cardsfolder/e/ekundu_cyclops.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ Name:Ekundu Cyclops
ManaCost:3 R
Types:Creature Cyclops
PT:3/4
K:If a creature you control attacks, CARDNAME also attacks if able.
Oracle:If a creature you control attacks, Ekundu Cyclops also attacks if able.
S:Mode$ AttackRequirement | ValidCard$ Creature.YouCtrl | ValidAttacker$ Card.Self | Description$ If a creature you control attacks, this creature also attacks if able.
Oracle:If a creature you control attacks, this creature also attacks if able.
2 changes: 1 addition & 1 deletion forge-gui/res/cardsfolder/m/magnetic_web.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Name:Magnetic Web
ManaCost:2
Types:Artifact
K:If a creature with a magnet counter on it attacks, all creatures with magnet counters on them attack if able.
S:Mode$ AttackRequirement | ValidCard$ Card.counters_GE1_MAGNET | ValidAttacker$ Card.counters_GE1_MAGNET | Description$ If a creature with a magnet counter on it attacks, all creatures with magnet counters on them attack if able.
T:Mode$ Attacks | ValidCard$ Creature.counters_GE1_MAGNET | TriggerZones$ Battlefield | Execute$ TrigLure | TriggerDescription$ Whenever a creature with a magnet counter on it attacks, all creatures with magnet counters on them block that creature if able.
SVar:TrigLure:DB$ MustBlock | Defined$ Valid Creature.counters_GE1_MAGNET | DefinedAttacker$ TriggeredAttackerLKICopy
A:AB$ PutCounter | Cost$ 1 T | ValidTgts$ Creature | CounterType$ MAGNET | CounterNum$ 1 | SpellDescription$ Put a magnet counter on target creature.
Expand Down
4 changes: 2 additions & 2 deletions forge-gui/res/cardsfolder/v/viashino_bey.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ Name:Viashino Bey
ManaCost:2 R R
Types:Creature Lizard
PT:4/3
K:If CARDNAME attacks, all creatures you control attack if able.
Oracle:If Viashino Bey attacks, all creatures you control attack if able.
S:Mode$ AttackRequirement | ValidCard$ Card.Self | ValidAttacker$ Creature.YouCtrl | Description$ If this creature attacks, all creatures you control attack if able.
Oracle:If this creature attacks, all creatures you control attack if able.
3 changes: 2 additions & 1 deletion forge-gui/res/cardsfolder/w/wars_toll.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ManaCost:3 R
Types:Enchantment
T:Mode$ TapsForMana | ValidCard$ Land | Activator$ Opponent | TriggerZones$ Battlefield | Execute$ TrigTapAll | TriggerDescription$ Whenever an opponent taps a land for mana, tap all lands that player controls.
SVar:TrigTapAll:DB$ TapAll | Defined$ TriggeredActivator | ValidCards$ Land
S:Mode$ Continuous | Affected$ Creature.OppCtrl | AddKeyword$ If CARDNAME attacks, all creatures you control attack if able. | Description$ If a creature an opponent controls attacks, all creatures that player controls attack if able.
# TODO fix for shared turn modes like TwoHeadedGiant
S:Mode$ AttackRequirement | ValidCard$ Creature.OppCtrl | ValidAttacker$ Creature.OppCtrl | Description$ If a creature an opponent controls attacks, all creatures that player controls attack if able.
SVar:NonStackingEffect:True
Oracle:Whenever an opponent taps a land for mana, tap all lands that player controls.\nIf a creature an opponent controls attacks, all creatures that opponent controls attack if able.