Improve applying attack requirements in multiplayer (#2582)

* Fix bad player position

* Recover clean up

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
This commit is contained in:
tool4ever
2023-02-28 05:48:08 +01:00
committed by GitHub
parent 5dcd91843b
commit 8482e5a4c8
10 changed files with 53 additions and 31 deletions

View File

@@ -18,6 +18,8 @@
package forge.ai; package forge.ai;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
@@ -815,14 +817,37 @@ public class AiAttackController {
} else { } else {
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) continue; if (combat.getAttackConstraints().getRequirements().get(attacker) == null) continue;
// check defenders in order of maximum requirements // check defenders in order of maximum requirements
for (Pair<GameEntity, Integer> e : combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements()) { List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements();
// TODO check if desired defender would also have the same amount final GameEntity def = defender;
Collections.sort(reqs, new Comparator<Pair<GameEntity, Integer>>() {
@Override
public int compare(Pair<GameEntity, Integer> r1, Pair<GameEntity, Integer> r2) {
if (r1.getValue() == r2.getValue()) {
// try to attack the designated defender
if (r1.getKey().equals(def) && !r2.getKey().equals(def)) {
return -1;
}
if (r2.getKey().equals(def) && !r1.getKey().equals(def)) {
return 1;
}
// otherwise PW
if (r1.getKey() instanceof Card && r2.getKey() instanceof Player) {
return -1;
}
if (r2.getKey() instanceof Card && r1.getKey() instanceof Player) {
return 1;
}
// or weakest player
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
}
}
return r2.getValue() - r1.getValue();
}
});
for (Pair<GameEntity, Integer> e : reqs) {
if (e.getRight() == 0) continue; if (e.getRight() == 0) continue;
GameEntity mustAttackDefMaybe = e.getLeft(); GameEntity mustAttackDefMaybe = e.getLeft();
// Gideon Jura returns LKI
if (mustAttackDefMaybe instanceof Card) {
mustAttackDefMaybe = ai.getGame().getCardState((Card) mustAttackDefMaybe);
}
if (canAttackWrapper(attacker, mustAttackDefMaybe) && CombatUtil.getAttackCost(ai.getGame(), attacker, mustAttackDefMaybe) == null) { if (canAttackWrapper(attacker, mustAttackDefMaybe) && CombatUtil.getAttackCost(ai.getGame(), attacker, mustAttackDefMaybe) == null) {
mustAttackDef = mustAttackDefMaybe; mustAttackDef = mustAttackDefMaybe;
break; break;

View File

@@ -1359,12 +1359,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
chance = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_CHANCE); chance = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_CHANCE);
} }
if (MyRandom.percentTrue(chance)) { if (MyRandom.percentTrue(chance)) {
Collections.sort(aiPlaneswalkers, new Comparator<Card>() { Collections.sort(aiPlaneswalkers, CardPredicates.compareByCounterType(CounterEnumType.LOYALTY));
@Override
public int compare(final Card a, final Card b) {
return a.getCounters(CounterEnumType.LOYALTY) - b.getCounters(CounterEnumType.LOYALTY);
}
});
for (Card pw : aiPlaneswalkers) { for (Card pw : aiPlaneswalkers) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY); int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
int freshLoyalty = Integer.valueOf(pw.getCurrentState().getBaseLoyalty()); int freshLoyalty = Integer.valueOf(pw.getCurrentState().getBaseLoyalty());

View File

@@ -15,7 +15,6 @@ import com.google.common.collect.Table;
import forge.GameCommand; import forge.GameCommand;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
@@ -220,11 +219,10 @@ public abstract class TokenEffectBase extends SpellAbilityEffect {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final Game game = host.getGame(); final Game game = host.getGame();
GameObject aTo = Iterables.getFirst( GameEntity aTo = Iterables.getFirst(
AbilityUtils.getDefinedObjects(host, sa.getParam("AttachedTo"), sa), null); AbilityUtils.getDefinedEntities(host, sa.getParam("AttachedTo"), sa), null);
if (aTo instanceof GameEntity) { if (aTo != null) {
GameEntity ge = (GameEntity)aTo;
// check what the token would be on the battlefield // check what the token would be on the battlefield
Card lki = CardUtil.getLKICopy(tok); Card lki = CardUtil.getLKICopy(tok);
@@ -237,7 +235,7 @@ public abstract class TokenEffectBase extends SpellAbilityEffect {
boolean canAttach = lki.isAttachment(); boolean canAttach = lki.isAttachment();
if (canAttach && !ge.canBeAttached(lki, sa)) { if (canAttach && !aTo.canBeAttached(lki, sa)) {
canAttach = false; canAttach = false;
} }
@@ -253,7 +251,7 @@ public abstract class TokenEffectBase extends SpellAbilityEffect {
return false; return false;
} }
tok.attachToEntity(ge, sa); tok.attachToEntity(aTo, sa);
return true; return true;
} }
// not a GameEntity, cant be attach // not a GameEntity, cant be attach

View File

@@ -2064,7 +2064,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n"); sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph") } else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")
|| keyword.startsWith("Escape") || keyword.startsWith("Foretell:") || keyword.startsWith("Escape") || keyword.startsWith("Foretell:")
|| keyword.startsWith("Madness:") || keyword.startsWith("Madness:")|| keyword.startsWith("Recover")
|| keyword.startsWith("Reconfigure") || keyword.startsWith("Squad") || keyword.startsWith("Reconfigure") || keyword.startsWith("Squad")
|| keyword.startsWith("Miracle") || keyword.startsWith("More Than Meets the Eye") || keyword.startsWith("Miracle") || keyword.startsWith("More Than Meets the Eye")
|| keyword.startsWith("Level up")) { || keyword.startsWith("Level up")) {
@@ -2072,11 +2072,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
sbLong.append(k[0]); sbLong.append(k[0]);
if (k.length > 1) { if (k.length > 1) {
final Cost mCost = new Cost(k[1], true); final Cost mCost = new Cost(k[1], true);
if (!mCost.isOnlyManaCost()) {
sbLong.append("");
}
if (mCost.isOnlyManaCost()) { if (mCost.isOnlyManaCost()) {
sbLong.append(" "); sbLong.append(" ");
} else {
sbLong.append("");
} }
sbLong.append(mCost.toString()); sbLong.append(mCost.toString());
if (!mCost.isOnlyManaCost()) { if (!mCost.isOnlyManaCost()) {

View File

@@ -1691,14 +1691,20 @@ public class CardFactoryUtil {
AbilitySub exileSA = (AbilitySub) AbilityFactory.getAbility(exileStr, card); AbilitySub exileSA = (AbilitySub) AbilityFactory.getAbility(exileStr, card);
changeSA.setSubAbility(exileSA); changeSA.setSubAbility(exileSA);
final Cost cost = new Cost(recoverCost, false);
String costDesc = cost.toSimpleString();
if (!cost.isOnlyManaCost()) {
costDesc = "" + costDesc;
}
String trigObject = card.isCreature() ? "Creature.Other+YouOwn" : "Creature.YouOwn"; String trigObject = card.isCreature() ? "Creature.Other+YouOwn" : "Creature.YouOwn";
String trigArticle = card.isCreature() ? "another" : "a"; String trigArticle = card.isCreature() ? "another" : "a";
String trigStr = "Mode$ ChangesZone | ValidCard$ " + trigObject String trigStr = "Mode$ ChangesZone | ValidCard$ " + trigObject
+ " | Origin$ Battlefield | Destination$ Graveyard | " + " | Origin$ Battlefield | Destination$ Graveyard | "
+ "TriggerZones$ Graveyard | Secondary$ True | " + "TriggerZones$ Graveyard | Secondary$ True | "
+ "TriggerDescription$ Recover " + recoverCost + " (When " + trigArticle + " creature is " + "TriggerDescription$ Recover " + costDesc + " (When " + trigArticle + " creature is "
+ "put into your graveyard from the battlefield, you " + "put into your graveyard from the battlefield, you "
+ "may pay " + recoverCost + ". If you do, return " + "may pay " + costDesc + ". If you do, return "
+ "CARDNAME from your graveyard to your hand. Otherwise," + "CARDNAME from your graveyard to your hand. Otherwise,"
+ " exile CARDNAME.)"; + " exile CARDNAME.)";
final Trigger myTrigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic); final Trigger myTrigger = TriggerHandler.parseTrigger(trigStr, card, intrinsic);

View File

@@ -3,7 +3,7 @@ ManaCost:2 B B
Types:Creature Zombie Dragon Types:Creature Zombie Dragon
PT:4/3 PT:4/3
K:Flying K:Flying
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Dragon.YouCtrl | IsPresent$ Card.StrictlySelf+inZoneGraveyard | PresentZone$ Graveyard | TriggerZones$ Graveyard | Execute$ TrigReturn | TriggerDescription$ Whenever a Dragon you control dies while Boneyard Scourge is in your graveyard, you may pay {1}{B}. If you do, return Boneyard Scourge from your graveyard to the battlefield. T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Dragon.YouCtrl | IsPresent$ Card.StrictlySelf+inZoneGraveyard | PresentZone$ Graveyard | TriggerZones$ Graveyard | Execute$ TrigReturn | TriggerDescription$ Whenever a Dragon you control dies while CARDNAME is in your graveyard, you may pay {1}{B}. If you do, return CARDNAME from your graveyard to the battlefield.
SVar:TrigReturn:AB$ ChangeZone | Cost$ 1 B | Defined$ Self | Origin$ Graveyard | Destination$ Battlefield SVar:TrigReturn:AB$ ChangeZone | Cost$ 1 B | Defined$ Self | Origin$ Graveyard | Destination$ Battlefield
SVar:SacMe:1 SVar:SacMe:1
SVar:DiscardMe:1 SVar:DiscardMe:1

View File

@@ -4,7 +4,6 @@ Types:Creature Phoenix
PT:2/2 PT:2/2
K:Flying K:Flying
K:Haste K:Haste
T:Mode$ DamageDone | ValidSource$ Spell.Instant+YouCtrl+Red,Spell.Sorcery+YouCtrl+Red | ValidTarget$ Opponent | TriggerZones$ Graveyard | Execute$ TrigReturn | TriggerDescription$ Whenever an opponent is dealt damage by a red instant or sorcery spell you control or by a red planeswalker you control, return Chandra's Phoenix from your graveyard to your hand. T:Mode$ DamageDone | ValidSource$ Spell.Instant+YouCtrl+Red,Spell.Sorcery+YouCtrl+Red,Planeswalker.YouCtrl+Red | ValidTarget$ Opponent | TriggerZones$ Graveyard | Execute$ TrigReturn | TriggerDescription$ Whenever an opponent is dealt damage by a red instant or sorcery spell you control or by a red planeswalker you control, return CARDNAME from your graveyard to your hand.
T:Mode$ DamageDone | ValidSource$ Planeswalker.YouCtrl+Red | ValidTarget$ Opponent | TriggerZones$ Graveyard | Execute$ TrigReturn | Secondary$ True | TriggerDescription$ Whenever an opponent is dealt damage by a red instant or sorcery spell you control or by a red planeswalker you control, return Chandra's Phoenix from your graveyard to your hand.
SVar:TrigReturn:DB$ ChangeZone | Defined$ Self | Origin$ Graveyard | Destination$ Hand SVar:TrigReturn:DB$ ChangeZone | Defined$ Self | Origin$ Graveyard | Destination$ Hand
Oracle:Flying\nHaste (This creature can attack and {T} as soon as it comes under your control.)\nWhenever an opponent is dealt damage by a red instant or sorcery spell you control or by a red planeswalker you control, return Chandra's Phoenix from your graveyard to your hand. Oracle:Flying\nHaste (This creature can attack and {T} as soon as it comes under your control.)\nWhenever an opponent is dealt damage by a red instant or sorcery spell you control or by a red planeswalker you control, return Chandra's Phoenix from your graveyard to your hand.

View File

@@ -5,7 +5,7 @@ A:SP$ Sacrifice | Cost$ 2 U B R | SacValid$ Creature,Planeswalker | SacMessage$
SVar:DBDiscard:DB$ Discard | NumCards$ 1 | Mode$ TgtChoose | Defined$ Player.Opponent | SubAbility$ DBRaiseDead SVar:DBDiscard:DB$ Discard | NumCards$ 1 | Mode$ TgtChoose | Defined$ Player.Opponent | SubAbility$ DBRaiseDead
SVar:DBRaiseDead:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | Hidden$ True | Mandatory$ True | ChangeType$ Creature.YouOwn,Planeswalker.YouOwn | SubAbility$ DBDraw | StackDescription$ You return a creature or planeswalker card from your graveyard to your hand SVar:DBRaiseDead:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | Hidden$ True | Mandatory$ True | ChangeType$ Creature.YouOwn,Planeswalker.YouOwn | SubAbility$ DBDraw | StackDescription$ You return a creature or planeswalker card from your graveyard to your hand
SVar:DBDraw:DB$ Draw | NumCards$ 1 | Defined$ You SVar:DBDraw:DB$ Draw | NumCards$ 1 | Defined$ You
T:Mode$ SpellCast | ValidCard$ Planeswalker.Bolas | TriggerZones$ Graveyard | Execute$ DBExileSelf | TriggerDescription$ When you cast a Bolas planeswalker spell, exile Dark Intimations from your graveyard. That planeswalker enters the battlefield with an additional loyalty counter on it. T:Mode$ SpellCast | ValidCard$ Planeswalker.Bolas | TriggerZones$ Graveyard | Execute$ DBExileSelf | TriggerDescription$ When you cast a Bolas planeswalker spell, exile CARDNAME from your graveyard. That planeswalker enters the battlefield with an additional loyalty counter on it.
SVar:DBExileSelf:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | Defined$ Self | SubAbility$ DBExtraLoyaltyEffect SVar:DBExileSelf:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | Defined$ Self | SubAbility$ DBExtraLoyaltyEffect
SVar:DBExtraLoyaltyEffect:DB$ Effect | ReplacementEffects$ DBBoostLoyalty | RememberObjects$ TriggeredCard | ExileOnMoved$ Stack SVar:DBExtraLoyaltyEffect:DB$ Effect | ReplacementEffects$ DBBoostLoyalty | RememberObjects$ TriggeredCard | ExileOnMoved$ Stack
SVar:DBBoostLoyalty:Event$ Moved | ReplacementResult$ Updated | ActiveZones$ Command | Destination$ Battlefield | ValidCard$ Card.IsRemembered | ReplaceWith$ AddExtraCounter | Description$ That planeswalker enters the battlefield with an additional loyalty counter on it. SVar:DBBoostLoyalty:Event$ Moved | ReplacementResult$ Updated | ActiveZones$ Command | Destination$ Battlefield | ValidCard$ Card.IsRemembered | ReplaceWith$ AddExtraCounter | Description$ That planeswalker enters the battlefield with an additional loyalty counter on it.

View File

@@ -2,7 +2,7 @@ Name:Garza's Assassin
ManaCost:B B B ManaCost:B B B
Types:Creature Human Assassin Types:Creature Human Assassin
PT:2/2 PT:2/2
K:Recover:PayLife<X> K:Recover:PayLife<X/half your life, rounded up>
A:AB$ Destroy | Cost$ Sac<1/CARDNAME> | ValidTgts$ Creature.nonBlack | TgtPrompt$ Select target nonblack creature | SpellDescription$ Destroy target nonblack creature. A:AB$ Destroy | Cost$ Sac<1/CARDNAME> | ValidTgts$ Creature.nonBlack | TgtPrompt$ Select target nonblack creature | SpellDescription$ Destroy target nonblack creature.
SVar:X:Count$YourLifeTotal/HalfUp SVar:X:Count$YourLifeTotal/HalfUp
Oracle:Sacrifice Garza's Assassin: Destroy target nonblack creature.\nRecover—Pay half your life, rounded up. (When another creature is put into your graveyard from the battlefield, you may pay half your life, rounded up. If you do, return this card from your graveyard to your hand. Otherwise, exile this card.) Oracle:Sacrifice Garza's Assassin: Destroy target nonblack creature.\nRecover—Pay half your life, rounded up. (When another creature is put into your graveyard from the battlefield, you may pay half your life, rounded up. If you do, return this card from your graveyard to your hand. Otherwise, exile this card.)

View File

@@ -4,7 +4,7 @@ Types:Creature Phoenix
PT:2/2 PT:2/2
K:Flying K:Flying
K:Haste K:Haste
T:Mode$ AttackersDeclared | CheckSVar$ X | SVarCompare$ GE3 | Execute$ TrigReturn | NoResolvingCheck$ True | TriggerZones$ Graveyard | AttackingPlayer$ You | TriggerDescription$ Whenever you attack with three or more creatures, you may pay {2}{R}. If you do, return Warcry Phoenix from your graveyard to the battlefield tapped and attacking. T:Mode$ AttackersDeclared | CheckSVar$ X | SVarCompare$ GE3 | Execute$ TrigReturn | NoResolvingCheck$ True | TriggerZones$ Graveyard | AttackingPlayer$ You | TriggerDescription$ Whenever you attack with three or more creatures, you may pay {2}{R}. If you do, return CARDNAME from your graveyard to the battlefield tapped and attacking.
SVar:TrigReturn:AB$ ChangeZone | Cost$ 2 R | Defined$ Self | Origin$ Graveyard | Destination$ Battlefield | Tapped$ True | Attacking$ True SVar:TrigReturn:AB$ ChangeZone | Cost$ 2 R | Defined$ Self | Origin$ Graveyard | Destination$ Battlefield | Tapped$ True | Attacking$ True
SVar:X:Count$Valid Creature.attacking SVar:X:Count$Valid Creature.attacking
Oracle:Flying, haste\nWhenever you attack with three or more creatures, you may pay {2}{R}. If you do, return Warcry Phoenix from your graveyard to the battlefield tapped and attacking. Oracle:Flying, haste\nWhenever you attack with three or more creatures, you may pay {2}{R}. If you do, return Warcry Phoenix from your graveyard to the battlefield tapped and attacking.