CLB: Firkraag, Cunning Instigator and support (#1019)

* firkraag_cunning_instigator.txt

* CardProperty.cardHasProperty add "mustAttack"

* AttackRequirement.hasRequirement fix (thanks TRT)

* Add timestamp check

* Don't treat Trove of Temptation Requirement as Restriction

* Clean up

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
This commit is contained in:
Northmoc
2022-07-05 06:49:10 -04:00
committed by GitHub
parent 3decca4752
commit bf6d41672d
6 changed files with 62 additions and 84 deletions

View File

@@ -147,7 +147,7 @@ public class AbilityUtils {
} else if (defined.equals("TopOfGraveyard")) { } else if (defined.equals("TopOfGraveyard")) {
final CardCollectionView grave = hostCard.getController().getCardsIn(ZoneType.Graveyard); final CardCollectionView grave = hostCard.getController().getCardsIn(ZoneType.Graveyard);
if (grave.size() > 0) { // TopOfLibrary or BottomOfLibrary if (grave.size() > 0) {
c = grave.getLast(); c = grave.getLast();
} else { } else {
// we don't want this to fall through and return the "Self" // we don't want this to fall through and return the "Self"

View File

@@ -13,6 +13,7 @@ import forge.game.*;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.combat.AttackRequirement;
import forge.game.combat.AttackingBand; import forge.game.combat.AttackingBand;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
@@ -1689,6 +1690,11 @@ public class CardProperty {
if (band == null || !band.getAttackers().contains(card)) { if (band == null || !band.getAttackers().contains(card)) {
return false; return false;
} }
} else if (property.equals("hadToAttackThisCombat")) {
AttackRequirement e = game.getCombat().getAttackConstraints().getRequirements().get(card);
if (e == null || !e.hasCreatureRequirement() || !e.getAttacker().equalsWithTimestamp(card)) {
return false;
}
} else if (property.equals("couldAttackButNotAttacking")) { } else if (property.equals("couldAttackButNotAttacking")) {
if (!game.getPhaseHandler().isPlayerTurn(controller)) return false; if (!game.getPhaseHandler().isPlayerTurn(controller)) return false;
return CombatUtil.couldAttackButNotAttacking(combat, card); return CombatUtil.couldAttackButNotAttacking(combat, card);

View File

@@ -160,7 +160,7 @@ public class AttackConstraints {
} }
} }
} }
myPossibleAttackers.removeAll((Iterable<Card>) attackersToRemove); myPossibleAttackers.removeAll(attackersToRemove);
for (final Card toRemove : attackersToRemove) { for (final Card toRemove : attackersToRemove) {
reqs.removeAll(findAll(reqs, toRemove)); reqs.removeAll(findAll(reqs, toRemove));
} }
@@ -186,8 +186,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));
if (countViolations(Collections.emptyMap()) != -1) { int empty = countViolations(Collections.emptyMap());
possible.put(Collections.emptyMap(), countViolations(Collections.emptyMap())); if (empty != -1) {
possible.put(Collections.emptyMap(), empty);
} }
// take the case with the fewest violations // take the case with the fewest violations
@@ -392,7 +393,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, possibleAttackers)) { if (!globalRestrictions.isLegal(attackers)) {
return -1; return -1;
} }
for (final Entry<Card, GameEntity> attacker : attackers.entrySet()) { for (final Entry<Card, GameEntity> attacker : attackers.entrySet()) {

View File

@@ -107,8 +107,17 @@ public class AttackRequirement {
} }
} }
public Card getAttacker() {
return attacker;
}
public boolean hasRequirement() { public boolean hasRequirement() {
return !defenderSpecific.isEmpty() || !causesToAttack.isEmpty() || !defenderOrPWSpecific.isEmpty(); return defenderSpecific.countAll() > 0 || causesToAttack.countAll() > 0 || defenderOrPWSpecific.countAll() > 0;
}
// according to Firkraag ruling Trove of Temptation applies to players, not creatures
public boolean hasCreatureRequirement() {
return defenderSpecific.countAll() > 0 || causesToAttack.countAll() > 0;
} }
public final MapToAmount<Card> getCausesToAttack() { public final MapToAmount<Card> getCausesToAttack() {
@@ -124,29 +133,24 @@ public class AttackRequirement {
int violations = 0; int violations = 0;
// first. check to see if "must attack X or Y with at least one creature" requirements are satisfied // first. check to see if "must attack X or Y with at least one creature" requirements are satisfied
//List<GameEntity> toRemoveFromDefSpecific = Lists.newArrayList(); for (GameEntity def : defenderOrPWSpecific.keySet()) {
if (!defenderOrPWSpecific.isEmpty()) { boolean isAttackingDefender = false;
for (GameEntity def : defenderOrPWSpecific.keySet()) { outer: for (Card atk : attackers.keySet()) {
if (defenderSpecificAlternatives.containsKey(def)) { // is anyone attacking this defender or any of the alternative defenders?
boolean isAttackingDefender = false; if (attackers.get(atk).equals(def)) {
outer: for (Card atk : attackers.keySet()) { isAttackingDefender = true;
// is anyone attacking this defender or any of the alternative defenders? break;
if (attackers.get(atk).equals(def)) { }
isAttackingDefender = true; for (GameEntity altDef : defenderSpecificAlternatives.get(def)) {
break; if (attackers.get(atk).equals(altDef)) {
} isAttackingDefender = true;
for (GameEntity altDef : defenderSpecificAlternatives.get(def)) { break outer;
if (attackers.get(atk).equals(altDef)) {
isAttackingDefender = true;
break outer;
}
}
}
if (!isAttackingDefender && CombatUtil.getAttackCost(attacker.getGame(), attacker, def) == null) {
violations++; // no one is attacking that defender or any of his PWs
} }
} }
} }
if (!isAttackingDefender) {
violations++; // no one is attacking that defender or any of his PWs
}
} }
// now, count everything else // now, count everything else

View File

@@ -9,9 +9,7 @@ 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.LinkedHashMapToAmount; import forge.util.maps.LinkedHashMapToAmount;
@@ -22,11 +20,9 @@ public class GlobalAttackRestrictions {
private final int max; private final int max;
private final MapToAmount<GameEntity> defenderMax; private final MapToAmount<GameEntity> defenderMax;
private final PlayerCollection mustBeAttackedByEachOpp; private GlobalAttackRestrictions(final int max, final MapToAmount<GameEntity> defenderMax) {
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() {
@@ -36,17 +32,17 @@ public class GlobalAttackRestrictions {
return defenderMax; return defenderMax;
} }
public boolean isLegal(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) { public boolean isLegal(final Map<Card, GameEntity> attackers) {
return !getViolations(attackers, possibleAttackers, true).isViolated(); return !getViolations(attackers, true).isViolated();
} }
public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers) { public GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers) {
return getViolations(attackers, possibleAttackers, false); return getViolations(attackers, false);
} }
private GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, final CardCollection possibleAttackers, final boolean returnQuickly) { private GlobalAttackRestrictionViolations getViolations(final Map<Card, GameEntity> attackers, 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.emptyMap(), MapToAmountUtil.emptyMap()); return new GlobalAttackRestrictionViolations(nTooMany, MapToAmountUtil.emptyMap());
} }
final MapToAmount<GameEntity> defenderTooMany = new LinkedHashMapToAmount<>(defenderMax.size()); final MapToAmount<GameEntity> defenderTooMany = new LinkedHashMapToAmount<>(defenderMax.size());
@@ -77,49 +73,18 @@ public class GlobalAttackRestrictions {
} }
} }
final MapToAmount<GameEntity> defenderTooFew = new LinkedHashMapToAmount<>(defenderMax.size()); return new GlobalAttackRestrictionViolations(nTooMany, defenderTooMany);
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, final MapToAmount<GameEntity> defenderTooFew) { public GlobalAttackRestrictionViolations(final int globalTooMany, final MapToAmount<GameEntity> defenderTooMany) {
this.isViolated = globalTooMany > 0 || !defenderTooMany.isEmpty() || !defenderTooFew.isEmpty(); this.isViolated = globalTooMany > 0 || !defenderTooMany.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;
@@ -127,9 +92,6 @@ 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;
} }
@@ -147,7 +109,6 @@ public class GlobalAttackRestrictions {
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<>(possibleDefenders.size()); final MapToAmount<GameEntity> defenderMax = new LinkedHashMapToAmount<>(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)) {
@@ -171,14 +132,6 @@ 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) {
@@ -190,7 +143,7 @@ public class GlobalAttackRestrictions {
max = Ints.min(max, defenderMax.countAll()); max = Ints.min(max, defenderMax.countAll());
} }
return new GlobalAttackRestrictions(max, defenderMax, mustBeAttacked); return new GlobalAttackRestrictions(max, defenderMax);
} }
/** /**

View File

@@ -0,0 +1,14 @@
Name:Firkraag, Cunning Instigator
ManaCost:3 U R
Types:Legendary Creature Dragon
PT:3/3
K:Flying
K:Haste
T:Mode$ AttackersDeclaredOneTarget | ValidAttackers$ Dragon.YouCtrl | AttackedTarget$ Opponent | Execute$ TrigGoad | TriggerZones$ Battlefield | TriggerDescription$ Whenever one or more Dragons you control attack an opponent, goad target creature that player controls.
SVar:TrigGoad:DB$ Goad | ValidTgts$ Creature | TargetsWithDefinedController$ TriggeredAttackedTarget | TgtPrompt$ Select target creature that player controls
T:Mode$ DamageDone | ValidSource$ Creature.hadToAttackThisCombat | ValidTarget$ Opponent | CombatDamage$ True | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever a creature deals combat damage to one of your opponents, if that creature had to attack this combat, you put a +1/+1 counter on CARDNAME and you draw a card.
SVar:TrigPutCounter:DB$ PutCounter | CounterType$ P1P1 | SubAbility$ DBDraw
SVar:DBDraw:DB$ Draw
DeckHints:Type$Dragon
DeckHas:Ability$Counters
Oracle:Flying, haste\nWhenever one or more Dragons you control attack an opponent, goad target creature that player controls.\nWhenever a creature deals combat damage to one of your opponents, if that creature had to attack this combat, you put a +1/+1 counter on Firkraag, Cunning Instigator and you draw a card.