From 973464f1c42cd8a5b2174f9ef18243d18880b939 Mon Sep 17 00:00:00 2001 From: Northmoc Date: Thu, 25 Mar 2021 10:56:40 -0400 Subject: [PATCH 1/8] professor_onyx.txt --- .../cardsfolder/upcoming/professor_onyx.txt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 forge-gui/res/cardsfolder/upcoming/professor_onyx.txt diff --git a/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt b/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt new file mode 100644 index 00000000000..9b9215874e9 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt @@ -0,0 +1,20 @@ +Name:Professor Onyx +ManaCost:4 B B +Types:Legendary Planeswalker Liliana +Loyalty:5 +T:Mode$ SpellCast | ValidCard$ Instant,Sorcery | ValidActivatingPlayer$ You | NoResolvingCheck$ True | Execute$ TrigDrain | TriggerZones$ Battlefield | TriggerDescription$ Magecraft — Whenever you cast or copy an instant or sorcery spell, each opponent loses 2 life and you gain 2 life. +T:Mode$ SpellCopy | ValidCard$ Instant,Sorcery | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigDrain | Secondary$ True | TriggerDescription$ Magecraft — Whenever you cast or copy an instant or sorcery spell, each opponent loses 2 life and you gain 2 life. +SVar:TrigDrain:DB$ LoseLife | Defined$ Player.Opponent | LifeAmount$ 2 | SubAbility$ DBGainLife +SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 2 +A:AB$ LoseLife | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | Defined$ You | LifeAmount$ 1 | SubAbility$ DBDig | SpellDescription$ You lose 1 life. Look at the top three cards of your library. Put one of them into your hand and the rest into your graveyard. +SVar:DBDig:DB$ Dig | DigNum$ 3 | ChangeNum$ 1 | DestinationZone2$ Graveyard | StackDescription$ {p:You} looks at the top three cards of their library. {p:You} puts one of them into their hand and the rest into their graveyard. +A:AB$ RepeatEach | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | RepeatPlayers$ Opponent | RepeatSubAbility$ DBChooseCard | SubAbility$ DBSac | SpellDescription$ Each opponent sacrifices a creature with the greatest power among creatures that player controls. +SVar:DBChooseCard:DB$ ChooseCard | Defined$ Player.IsRemembered | Choices$ Creature.greatestPowerControlledByRemembered | ChoiceTitle$ Choose a creature you control with the greatest power | Mandatory$ True | RememberChosen$ True +SVar:DBSac:DB$ SacrificeAll | ValidCards$ Card.IsRemembered | SubAbility$ DBCleanup | StackDescription$ Each opponent sacrifices a creature with the greatest power among creatures they control. +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearChosen$ True +A:AB$ Repeat | Cost$ SubCounter<8/LOYALTY> | Planeswalker$ True | Ultimate$ True | RepeatSubAbility$ DBRepeatEach | MaxRepeat$ 7 | StackDescription$ SpellDescription | SpellDescription$ Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times. +SVar:DBRepeatEach:DB$ RepeatEach | RepeatPlayers$ Opponent | RepeatSubAbility$ DiscardOrLose +SVar:DiscardOrLose:DB$ LoseLife | Defined$ Player.IsRemembered | LifeAmount$ 3 | UnlessCost$ Discard<1/Card> | UnlessPayer$ Player.IsRemembered +DeckHints:Type$Instant|Sorcery +DeckHas:Ability$Graveyard & Ability$LifeGain +Oracle:Magecraft — Whenever you cast or copy an instant or sorcery spell, each opponent loses 2 life and you gain 2 life.\n[+1]: You lose 1 life. Look at the top three cards of your library. Put one of them into your hand and the rest into your graveyard.\n[−3]: Each opponent sacrifices a creature with the greatest power among creatures that player controls.\n[−8]: Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times. From d03bc797fca3fe2e84882405fc8600ad53a490be Mon Sep 17 00:00:00 2001 From: Northmoc Date: Thu, 25 Mar 2021 18:13:10 -0400 Subject: [PATCH 2/8] DiscardEffect.java add "RememberDiscardingPlayers" param --- .../main/java/forge/game/ability/effects/DiscardEffect.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java index 77f4696e88a..99968f50479 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DiscardEffect.java @@ -349,6 +349,9 @@ public class DiscardEffect extends SpellAbilityEffect { runParams.put(AbilityKey.Cause, sa); runParams.put(AbilityKey.FirstTime, firstDiscard); game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false); + if (sa.hasParam("RememberDiscardingPlayers")) { + source.addRemembered(p); + } } } From 3ccc7c41e7ccbe5bb611aee2bca98cedd2f77724 Mon Sep 17 00:00:00 2001 From: Northmoc Date: Thu, 25 Mar 2021 18:13:33 -0400 Subject: [PATCH 3/8] professor_onyx.txt v2 --- forge-gui/res/cardsfolder/upcoming/professor_onyx.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt b/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt index 9b9215874e9..aaaf99e0b9e 100644 --- a/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt +++ b/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt @@ -12,9 +12,9 @@ A:AB$ RepeatEach | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | RepeatPlay SVar:DBChooseCard:DB$ ChooseCard | Defined$ Player.IsRemembered | Choices$ Creature.greatestPowerControlledByRemembered | ChoiceTitle$ Choose a creature you control with the greatest power | Mandatory$ True | RememberChosen$ True SVar:DBSac:DB$ SacrificeAll | ValidCards$ Card.IsRemembered | SubAbility$ DBCleanup | StackDescription$ Each opponent sacrifices a creature with the greatest power among creatures they control. SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearChosen$ True -A:AB$ Repeat | Cost$ SubCounter<8/LOYALTY> | Planeswalker$ True | Ultimate$ True | RepeatSubAbility$ DBRepeatEach | MaxRepeat$ 7 | StackDescription$ SpellDescription | SpellDescription$ Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times. -SVar:DBRepeatEach:DB$ RepeatEach | RepeatPlayers$ Opponent | RepeatSubAbility$ DiscardOrLose -SVar:DiscardOrLose:DB$ LoseLife | Defined$ Player.IsRemembered | LifeAmount$ 3 | UnlessCost$ Discard<1/Card> | UnlessPayer$ Player.IsRemembered +A:AB$ Repeat | Cost$ SubCounter<8/LOYALTY> | Planeswalker$ True | Ultimate$ True | RepeatSubAbility$ TrigDiscard | MaxRepeat$ 7 | StackDescription$ SpellDescription | SpellDescription$ Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times. +SVar:TrigDiscard:DB$ Discard | Defined$ Player.Opponent | Mode$ TgtChoose | Optional$ True | RememberDiscardingPlayers$ True | SubAbility$ DBLoseLife +SVar:DBLoseLife:DB$ LoseLife | Defined$ Opponent.IsNotRemembered | LifeAmount$ 3 | SubAbility$ DBCleanup DeckHints:Type$Instant|Sorcery DeckHas:Ability$Graveyard & Ability$LifeGain Oracle:Magecraft — Whenever you cast or copy an instant or sorcery spell, each opponent loses 2 life and you gain 2 life.\n[+1]: You lose 1 life. Look at the top three cards of your library. Put one of them into your hand and the rest into your graveyard.\n[−3]: Each opponent sacrifices a creature with the greatest power among creatures that player controls.\n[−8]: Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times. From 2a410b9099723c295299b922a20d3d09b0d9f656 Mon Sep 17 00:00:00 2001 From: Northmoc Date: Thu, 25 Mar 2021 20:14:22 -0400 Subject: [PATCH 4/8] snarls --- forge-gui/res/cardsfolder/upcoming/frostboil_snarl.txt | 8 ++++++++ forge-gui/res/cardsfolder/upcoming/furycalm_snarl.txt | 8 ++++++++ forge-gui/res/cardsfolder/upcoming/necroblossom_snarl.txt | 8 ++++++++ forge-gui/res/cardsfolder/upcoming/shineshadow_snarl.txt | 8 ++++++++ forge-gui/res/cardsfolder/upcoming/vineglimmer_snarl.txt | 8 ++++++++ 5 files changed, 40 insertions(+) create mode 100644 forge-gui/res/cardsfolder/upcoming/frostboil_snarl.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/furycalm_snarl.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/necroblossom_snarl.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/shineshadow_snarl.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/vineglimmer_snarl.txt diff --git a/forge-gui/res/cardsfolder/upcoming/frostboil_snarl.txt b/forge-gui/res/cardsfolder/upcoming/frostboil_snarl.txt new file mode 100644 index 00000000000..efb36638585 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/frostboil_snarl.txt @@ -0,0 +1,8 @@ +Name:Frostboil Snarl +ManaCost:no cost +Types:Land +K:ETBReplacement:Other:DBTap +SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Island,Mountain/Island or Mountain> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Island or Mountain card from your hand. If you don't, CARDNAME enters the battlefield tapped. +A:AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}. +A:AB$ Mana | Cost$ T | Produced$ R | SpellDescription$ Add {R}. +Oracle:As Frostboil Snarl enters the battlefield, you may reveal a Island or Mountain card from your hand. If you don't, Frostboil Snarl enters the battlefield tapped.\n{T}: Add {U} or {R}. diff --git a/forge-gui/res/cardsfolder/upcoming/furycalm_snarl.txt b/forge-gui/res/cardsfolder/upcoming/furycalm_snarl.txt new file mode 100644 index 00000000000..e6542fd3c41 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/furycalm_snarl.txt @@ -0,0 +1,8 @@ +Name:Furycalm Snarl +ManaCost:no cost +Types:Land +K:ETBReplacement:Other:DBTap +SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Mountain,Plains/Mountain or Plains> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Mountain or Plains card from your hand. If you don't, CARDNAME enters the battlefield tapped. +A:AB$ Mana | Cost$ T | Produced$ R | SpellDescription$ Add {R}. +A:AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}. +Oracle:As Furycalm Snarl enters the battlefield, you may reveal a Mountain or Plains card from your hand. If you don't, Furycalm Snarl enters the battlefield tapped.\n{T}: Add {R} or {W}. diff --git a/forge-gui/res/cardsfolder/upcoming/necroblossom_snarl.txt b/forge-gui/res/cardsfolder/upcoming/necroblossom_snarl.txt new file mode 100644 index 00000000000..edd55953b0c --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/necroblossom_snarl.txt @@ -0,0 +1,8 @@ +Name:Necroblossom Snarl +ManaCost:no cost +Types:Land +K:ETBReplacement:Other:DBTap +SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Swamp,Forest/Swamp or Forest> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Swamp or Forest card from your hand. If you don't, CARDNAME enters the battlefield tapped. +A:AB$ Mana | Cost$ T | Produced$ B | SpellDescription$ Add {B}. +A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}. +Oracle:As Necroblossom Snarl enters the battlefield, you may reveal a Swamp or Forest card from your hand. If you don't, Necroblossom Snarl enters the battlefield tapped.\n{T}: Add {B} or {G}. diff --git a/forge-gui/res/cardsfolder/upcoming/shineshadow_snarl.txt b/forge-gui/res/cardsfolder/upcoming/shineshadow_snarl.txt new file mode 100644 index 00000000000..9c4412e32fe --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/shineshadow_snarl.txt @@ -0,0 +1,8 @@ +Name:Shineshadow Snarl +ManaCost:no cost +Types:Land +K:ETBReplacement:Other:DBTap +SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Plains,Swamp/Plains or Swamp> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Plains or Swamp card from your hand. If you don't, CARDNAME enters the battlefield tapped. +A:AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}. +A:AB$ Mana | Cost$ T | Produced$ B | SpellDescription$ Add {B}. +Oracle:As Shineshadow Snarl enters the battlefield, you may reveal a Plains or Swamp card from your hand. If you don't, Shineshadow Snarl enters the battlefield tapped.\n{T}: Add {W} or {B}. diff --git a/forge-gui/res/cardsfolder/upcoming/vineglimmer_snarl.txt b/forge-gui/res/cardsfolder/upcoming/vineglimmer_snarl.txt new file mode 100644 index 00000000000..bda7eb70c05 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/vineglimmer_snarl.txt @@ -0,0 +1,8 @@ +Name:Vineglimmer Snarl +ManaCost:no cost +Types:Land +K:ETBReplacement:Other:DBTap +SVar:DBTap:DB$ Tap | ETB$ True | Defined$ Self | UnlessCost$ Reveal<1/Forest,Island/Forest or Island> | UnlessPayer$ You | StackDescription$ enters the battlefield tapped. | SpellDescription$ As CARDNAME enters the battlefield, you may reveal a Forest or Island card from your hand. If you don't, CARDNAME enters the battlefield tapped. +A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}. +A:AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}. +Oracle:As Vineglimmer Snarl enters the battlefield, you may reveal a Forest or Island card from your hand. If you don't, Vineglimmer Snarl enters the battlefield tapped.\n{T}: Add {G} or {U}. From ce4f12179cf2c3f1fdfded224e22014fe879fb45 Mon Sep 17 00:00:00 2001 From: Lyu Zong-Hong Date: Fri, 26 Mar 2021 13:05:04 +0900 Subject: [PATCH 5/8] Add Camouflage --- .../src/main/java/forge/ai/SpellApiToAi.java | 1 + .../main/java/forge/game/ability/ApiType.java | 1 + .../ability/effects/CamouflageEffect.java | 108 ++++++++++++++++++ .../java/forge/game/phase/PhaseHandler.java | 9 +- .../replacement/ReplaceDeclareBlocker.java | 29 +++++ .../game/replacement/ReplacementType.java | 1 + forge-gui/res/cardsfolder/c/camouflage.txt | 7 ++ forge-gui/res/languages/de-DE.properties | 3 + forge-gui/res/languages/en-US.properties | 3 + forge-gui/res/languages/es-ES.properties | 3 + forge-gui/res/languages/it-IT.properties | 3 + forge-gui/res/languages/ja-JP.properties | 3 + forge-gui/res/languages/zh-CN.properties | 3 + 13 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java create mode 100644 forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java create mode 100644 forge-gui/res/cardsfolder/c/camouflage.txt diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 6aad3f581c1..7f6c3b81469 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -34,6 +34,7 @@ public enum SpellApiToAi { .put(ApiType.BidLife, BidLifeAi.class) .put(ApiType.Bond, BondAi.class) .put(ApiType.Branch, AlwaysPlayAi.class) + .put(ApiType.Camouflage, ChooseCardAi.class) .put(ApiType.ChangeCombatants, ChangeCombatantsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeX, AlwaysPlayAi.class) diff --git a/forge-game/src/main/java/forge/game/ability/ApiType.java b/forge-game/src/main/java/forge/game/ability/ApiType.java index 024a2b35155..56aef8eab84 100644 --- a/forge-game/src/main/java/forge/game/ability/ApiType.java +++ b/forge-game/src/main/java/forge/game/ability/ApiType.java @@ -30,6 +30,7 @@ public enum ApiType { Block (BlockEffect.class), Bond (BondEffect.class), Branch (BranchEffect.class), + Camouflage (CamouflageEffect.class), ChangeCombatants (ChangeCombatantsEffect.class), ChangeTargets (ChangeTargetsEffect.class), ChangeText (ChangeTextEffect.class), diff --git a/forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java new file mode 100644 index 00000000000..481941f2f32 --- /dev/null +++ b/forge-game/src/main/java/forge/game/ability/effects/CamouflageEffect.java @@ -0,0 +1,108 @@ +package forge.game.ability.effects; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import forge.game.ability.AbilityUtils; +import forge.game.ability.SpellAbilityEffect; +import forge.game.card.Card; +import forge.game.card.CardCollection; +import forge.game.card.CardLists; +import forge.game.combat.Combat; +import forge.game.combat.CombatUtil; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.util.Localizer; + +public class CamouflageEffect extends SpellAbilityEffect { + + private void randomizeBlockers(SpellAbility sa, Combat combat, Player declarer, Player defender, List attackers, List blockerPiles) { + CardLists.shuffle(attackers); + for (int i = 0; i < attackers.size(); i++) { + final Card attacker = attackers.get(i); + CardCollection blockers = blockerPiles.get(i); + + // Remove all illegal blockers first + for (int j = blockers.size() - 1; j >= 0; j--) { + final Card blocker = blockers.get(j); + if (!CombatUtil.canBlock(attacker, blocker, combat)) { + blockers.remove(j); + } + } + + if (attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") && + blockers.size() < defender.getCreaturesInPlay().size() || + blockers.size() < CombatUtil.needsBlockers(attacker)) { + // If not enough remaining creatures to block, don't add them as blocker + continue; + } + + if (attacker.hasKeyword("CantBeBlockedByAmount GT1") && blockers.size() > 1) { + // If no more than one creature can block, order the player to choose one to block + Card chosen = declarer.getController().chooseCardsForEffect(blockers, sa, + Localizer.getInstance().getMessage("lblChooseBlockerForAttacker", attacker.toString()), 1, 1, false, null).get(0); + combat.addBlocker(attacker, chosen); + continue; + } + + // Add all remaning blockers normally + for (final Card blocker : blockers) { + combat.addBlocker(attacker, blocker); + } + } + } + + @Override + public void resolve(SpellAbility sa) { + Card hostCard = sa.getHostCard(); + Player declarer = getDefinedPlayersOrTargeted(sa).get(0); + Player defender = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("Defender"), sa).get(0); + Combat combat = hostCard.getGame().getCombat(); + List attackers = combat.getAttackers(); + List blockerPiles = new ArrayList<>(); + + if (declarer.isAI()) { + // For AI player, just let it declare blockers normally, then randomize it later. + declarer.getController().declareBlockers(defender, combat); + // Remove all blockers first + for (final Card attacker : attackers) { + CardCollection blockers = combat.getBlockers(attacker); + blockerPiles.add(blockers); + for (final Card blocker : blockers) { + combat.removeFromCombat(blocker); + } + } + } else { // Human player + CardCollection pool = new CardCollection(defender.getCreaturesInPlay()); + // remove all blockers that can't block + for (final Card blocker : pool) { + if (!CombatUtil.canBlock(blocker)) { + pool.remove(blocker); + } + } + List blockedSoFar = new ArrayList<>(Collections.nCopies(pool.size(), 0)); + + for (int i = 0; i < attackers.size(); i++) { + int size = pool.size(); + CardCollection blockers = new CardCollection(declarer.getController().chooseCardsForEffect( + pool, sa, Localizer.getInstance().getMessage("lblChooseBlockersForPile", String.valueOf(i + 1)), 0, size, false, null)); + blockerPiles.add(blockers); + // Remove chosen creatures, unless it can block additional attackers + for (final Card blocker : blockers) { + int index = pool.indexOf(blocker); + Integer blockedCount = blockedSoFar.get(index) + 1; + if (!blocker.canBlockAny() && blocker.canBlockAdditional() < blockedCount) { + pool.remove(index); + blockedSoFar.remove(index); + } else { + blockedSoFar.set(index, blockedCount); + } + } + } + } + + randomizeBlockers(sa, combat, declarer, defender, attackers, blockerPiles); + } + +} diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 51b04c6986b..61355f6b54e 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -653,7 +653,14 @@ public class PhaseHandler implements java.io.Serializable { } if (combat.isPlayerAttacked(p)) { if (CombatUtil.canBlock(p, combat)) { - whoDeclaresBlockers.getController().declareBlockers(p, combat); + // Replacement effects (for Camouflage) + final Map repRunParams = AbilityKey.mapFromAffected(p); + repRunParams.put(AbilityKey.Player, whoDeclaresBlockers); + ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.DeclareBlocker, repRunParams); + if (repres == ReplacementResult.NotReplaced) { + // If not replaced, run normal declare blockers + whoDeclaresBlockers.getController().declareBlockers(p, combat); + } } } else { continue; } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java b/forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java new file mode 100644 index 00000000000..dc0024c7797 --- /dev/null +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceDeclareBlocker.java @@ -0,0 +1,29 @@ +package forge.game.replacement; + +import java.util.Map; + +import forge.game.ability.AbilityKey; +import forge.game.card.Card; +import forge.game.spellability.SpellAbility; + +public class ReplaceDeclareBlocker extends ReplacementEffect { + + public ReplaceDeclareBlocker(final Map mapParams, final Card host, final boolean intrinsic) { + super(mapParams, host, intrinsic); + } + + @Override + public boolean canReplace(Map runParams) { + if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) { + return false; + } + return true; + } + + @Override + public void setReplacingObjects(Map runParams, SpellAbility sa) { + sa.setReplacingObject(AbilityKey.DefendingPlayer, runParams.get(AbilityKey.Affected)); + // Here the Player is the one who would declare blockers (may be changed by some Card's effect) + sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Player)); + } +} diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementType.java b/forge-game/src/main/java/forge/game/replacement/ReplacementType.java index da55335f69c..abc4c0605ec 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementType.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementType.java @@ -21,6 +21,7 @@ public enum ReplacementType { CreateToken(ReplaceToken.class), DamageDone(ReplaceDamage.class), DealtDamage(ReplaceDealtDamage.class), + DeclareBlocker(ReplaceDeclareBlocker.class), Destroy(ReplaceDestroy.class), Discard(ReplaceDiscard.class), Draw(ReplaceDraw.class), diff --git a/forge-gui/res/cardsfolder/c/camouflage.txt b/forge-gui/res/cardsfolder/c/camouflage.txt new file mode 100644 index 00000000000..d46d860dfd7 --- /dev/null +++ b/forge-gui/res/cardsfolder/c/camouflage.txt @@ -0,0 +1,7 @@ +Name:Camouflage +ManaCost:G +Types:Instant +A:SP$ Effect | Cost$ G | ReplacementEffects$ RDeclareBlocker | ActivationPhases$ Declare Attackers | PlayerTurn$ True | AILogic$ Evasion | SpellDescription$ Cast this spell only during your declare attackers step. This turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.) +SVar:RDeclareBlocker:Event$ DeclareBlocker | ValidPlayer$ Opponent | ReplaceWith$ DBCamouflage | Description$ This turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.) +SVar:DBCamouflage:DB$ Camouflage | Defined$ ReplacedPlayer | Defender$ ReplacedDefendingPlayer | AILogic$ BestBlocker +Oracle:Cast this spell only during your declare attackers step.\nThis turn, instead of declaring blockers, each defending player chooses any number of creatures they control and divides them into a number of piles equal to the number of attacking creatures for whom that player is the defending player. Creatures those players control that can block additional creatures may likewise be put into additional piles. Assign each pile to a different one of those attacking creatures at random. Each creature in a pile that can block the creature that pile is assigned to does so. (Piles can be empty.) diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index 360f21920e0..a78d4144714 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Möchtest du überbieten? Aktuelles Gebot: lblTopBidWithValueLife=hat mit {0} Leben überboten #BondEffect.java lblSelectACardPair=Wähle Karte zum Verbinden +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Welchen Verteidiger mit {0} angreifen? #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index a49a1f47510..c8d2811d194 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Do you want to top bid? Current Bid \= lblTopBidWithValueLife=topped bid with {0} life #BondEffect.java lblSelectACardPair=Select a card to pair with +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0} #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/es-ES.properties b/forge-gui/res/languages/es-ES.properties index 226f28075fd..425cd9f822b 100644 --- a/forge-gui/res/languages/es-ES.properties +++ b/forge-gui/res/languages/es-ES.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=¿Quieres hacer una puja máxima? Puja actual \= lblTopBidWithValueLife=puja más alta con {0} de vida #BondEffect.java lblSelectACardPair=Selecciona una carta para emparejarla con +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Elige con qué defensor atacar con {0} #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/it-IT.properties b/forge-gui/res/languages/it-IT.properties index c40b513a2ed..57f59cbe438 100644 --- a/forge-gui/res/languages/it-IT.properties +++ b/forge-gui/res/languages/it-IT.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=Do you want to top bid? Current Bid \= lblTopBidWithValueLife=topped bid with {0} life #BondEffect.java lblSelectACardPair=Select a card to pair with +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard=Choose which defender to attack with {0} #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/ja-JP.properties b/forge-gui/res/languages/ja-JP.properties index 219998afa3b..85845de8b18 100644 --- a/forge-gui/res/languages/ja-JP.properties +++ b/forge-gui/res/languages/ja-JP.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=競りの点数を上げますか? 現在の点数 \= lblTopBidWithValueLife={0}点のライフで競りの点数をつけた #BondEffect.java lblSelectACardPair=組にしたいカードを選ぶ +#CamouflageEffect.java +lblChooseBlockerForAttacker={0}をブロックするクリーチャーを選ぶ +lblChooseBlockersForPile={0}番の束に入れるクリーチャーを選ぶ(空にできる) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWithCard={0}が攻撃する対象を選ぶ #ChangeTargetsEffect.java diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index 1bb03642e6b..d24b2583448 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -1750,6 +1750,9 @@ lblDoYouWantTopBid=你想要喊更高的价? 现在价钱 \= lblTopBidWithValueLife=最高喊价为{0}生命 #BondEffect.java lblSelectACardPair=选择要组成搭档的牌 +#CamouflageEffect.java +lblChooseBlockerForAttacker=Choose a creature to block {0} +lblChooseBlockersForPile=Choose creatures to put in pile {0} (can be empty) #ChangeCombatantsEffect.java lblChooseDefenderToAttackWith=选择守军进行进攻 #ChangeTargetsEffect.java From d15d27defbfe6e924d92f529d54855299dddec27 Mon Sep 17 00:00:00 2001 From: CCTV-1 Date: Fri, 26 Mar 2021 18:40:04 +0800 Subject: [PATCH 6/8] update simplified chinese translation --- forge-gui/res/languages/zh-CN.properties | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/forge-gui/res/languages/zh-CN.properties b/forge-gui/res/languages/zh-CN.properties index 1bb03642e6b..b5ef9d56dfe 100644 --- a/forge-gui/res/languages/zh-CN.properties +++ b/forge-gui/res/languages/zh-CN.properties @@ -117,8 +117,8 @@ cbpCounterDisplayLocation=计数器显示区域 cbpGraveyardOrdering=允许指衍生物进入墓地 lblAltLifeDisplay=备用牌手布局(横向模式) nlAltLifeDisplay=启用备用牌手布局以显示玩家的生命以及中毒,能量和经验指示物。 -lblPreferredArt=Preferred Art -nlPreferredArt=Sets the preferred art for cards. +lblPreferredArt=首选卡图 +nlPreferredArt=设置牌张的首选卡图。 Troubleshooting=故障排除 GeneralConfiguration=常规配置 lblPlayerName=玩家名称 @@ -1933,9 +1933,9 @@ lblTapOrUntapTarget=横置还是重置{0}? #TwoPilesEffect.java lblSelectCardForFaceDownPile=选择一个面朝下的堆 lblDivideCardIntoTwoPiles=将牌分为两堆 -lblSelectCardForLeftPile=Select cards for the left pile -lblLeftPile=Left pile -lblRightPile=Right pile +lblSelectCardForLeftPile=选择左堆中的牌张 +lblLeftPile=左堆 +lblRightPile=右堆 lblChoosesPile=选择堆 lblEmptyPile=空堆 #UntapEffect.java @@ -1953,7 +1953,7 @@ lblViewAll=查看所有牌 lblSetupGame=设定游戏状态 lblDumpGame=转储游戏状态 lblTutor=导师牌 -lblRollbackPhase=Rollback Phase +lblRollbackPhase=回滚阶段 lblAddCounterPermanent=向牌添加指示物 lblSubCounterPermanent=从牌减少指示物 lblTapPermanent=横置永久物 From fd01dc1bb1d36d4e274112205da509f14159f1c0 Mon Sep 17 00:00:00 2001 From: Bug Hunter Date: Fri, 26 Mar 2021 10:58:22 +0000 Subject: [PATCH 7/8] AI Multiplayer improvements --- .../java/forge/ai/AiAttackController.java | 19 +- .../src/main/java/forge/ai/ComputerUtil.java | 132 +++++-------- .../main/java/forge/ai/ComputerUtilCard.java | 2 +- .../java/forge/ai/ComputerUtilCombat.java | 21 ++- .../forge/ai/ability/ActivateAbilityAi.java | 5 +- .../main/java/forge/ai/ability/BalanceAi.java | 15 +- .../main/java/forge/ai/ability/BidLifeAi.java | 3 +- .../java/forge/ai/ability/ChangeZoneAi.java | 23 +-- .../java/forge/ai/ability/ChooseCardAi.java | 3 +- .../forge/ai/ability/ChooseCardNameAi.java | 3 +- .../forge/ai/ability/ChooseEvenOddAi.java | 3 +- .../java/forge/ai/ability/ChooseNumberAi.java | 3 +- .../java/forge/ai/ability/ChooseSourceAi.java | 3 +- .../forge/ai/ability/ControlExchangeAi.java | 3 +- .../main/java/forge/ai/ability/DebuffAi.java | 17 +- .../java/forge/ai/ability/DestroyAllAi.java | 178 +++++++++--------- .../src/main/java/forge/ai/ability/DigAi.java | 5 +- .../java/forge/ai/ability/DigMultipleAi.java | 5 +- .../java/forge/ai/ability/DigUntilAi.java | 7 +- .../main/java/forge/ai/ability/DiscardAi.java | 9 +- .../java/forge/ai/ability/DrainManaAi.java | 3 +- .../java/forge/ai/ability/GameLossAi.java | 3 +- .../java/forge/ai/ability/LifeExchangeAi.java | 5 +- .../ai/ability/LifeExchangeVariantAi.java | 3 +- .../main/java/forge/ai/ability/LifeSetAi.java | 19 +- .../forge/ai/ability/PowerExchangeAi.java | 7 +- .../java/forge/ai/ability/PumpAiBase.java | 3 +- .../main/java/forge/ai/ability/PumpAllAi.java | 33 ++-- .../main/java/forge/ai/ability/RepeatAi.java | 6 +- .../java/forge/ai/ability/SacrificeAi.java | 15 +- .../java/forge/ai/ability/SacrificeAllAi.java | 52 ++--- .../src/main/java/forge/ai/ability/TapAi.java | 1 - .../main/java/forge/ai/ability/TapAiBase.java | 3 +- .../java/forge/ai/ability/TwoPilesAi.java | 3 +- .../java/forge/ai/ability/UnattachAllAi.java | 5 +- .../main/java/forge/ai/ability/UntapAi.java | 7 +- .../src/main/java/forge/game/card/Card.java | 11 -- .../main/java/forge/game/player/Player.java | 3 + .../res/cardsfolder/g/goblin_artisans.txt | 1 + .../res/cardsfolder/h/hellion_eruption.txt | 3 +- 40 files changed, 287 insertions(+), 358 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 5f2e126f872..c3ef8885a03 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -91,7 +91,7 @@ public class AiAttackController { public AiAttackController(final Player ai, boolean nextTurn) { this.ai = ai; - this.defendingOpponent = choosePreferredDefenderPlayer(); + this.defendingOpponent = choosePreferredDefenderPlayer(ai); this.oppList = getOpponentCreatures(this.defendingOpponent); this.myList = ai.getCreaturesInPlay(); this.attackers = new ArrayList<>(); @@ -107,7 +107,7 @@ public class AiAttackController { public AiAttackController(final Player ai, Card attacker) { this.ai = ai; - this.defendingOpponent = choosePreferredDefenderPlayer(); + this.defendingOpponent = choosePreferredDefenderPlayer(ai); this.oppList = getOpponentCreatures(this.defendingOpponent); this.myList = ai.getCreaturesInPlay(); this.attackers = new ArrayList<>(); @@ -156,13 +156,12 @@ public class AiAttackController { } /** Choose opponent for AI to attack here. Expand as necessary. */ - private Player choosePreferredDefenderPlayer() { - Player defender = ai.getWeakestOpponent(); //Gets opponent with the least life + public static Player choosePreferredDefenderPlayer(Player ai) { + Player defender = ai.getWeakestOpponent(); //Concentrate on opponent within easy kill range - if (defender.getLife() < 8) { //Concentrate on opponent within easy kill range - return defender; - } else { //Otherwise choose a random opponent to ensure no ganging up on players - defender = ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size())); + if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players + // TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked + return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size())); } return defender; } @@ -624,7 +623,7 @@ public class AiAttackController { int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage; int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp); - if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai) >= opp.getLife() + if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, opp) >= opp.getLife() && !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) { return true; } @@ -919,7 +918,7 @@ public class AiAttackController { // find the potential damage ratio the AI can cause double humanLifeToDamageRatio = 1000000; if (candidateUnblockedDamage > 0) { - humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai)) / candidateUnblockedDamage; + humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai, opp)) / candidateUnblockedDamage; } // determine if the ai outnumbers the player diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 03dceacc08e..17fb2007fab 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -188,7 +188,6 @@ public class ComputerUtil { final Card source = sa.getHostCard(); final TargetRestrictions tgt = sa.getTargetRestrictions(); - // Play higher costing spells first? final Cost cost = sa.getPayCosts(); @@ -213,7 +212,8 @@ public class ComputerUtil { if (unless != null && !unless.endsWith(">")) { final int amount = AbilityUtils.calculateAmount(source, unless, sa); - final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtil.getOpponentFor(ai), true).size(); + // this is enough as long as the AI is only smart enough to target top of stack + final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtilAbility.getTopSpellAbilityOnStack(ai.getGame(), sa).getActivatingPlayer(), true).size(); // If the Unless isn't enough, this should be less likely to be used if (amount > usableManaSources) { @@ -1068,9 +1068,6 @@ public class ComputerUtil { return true; } } - if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ComputerUtil.getOpponentFor(ai))) { - return true; - } if (card.isCreature()) { if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) { return true; @@ -1093,8 +1090,8 @@ public class ComputerUtil { } // BuffedBy - // get all cards the human controls with AntiBuffedBy - final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield); + // there's a good chance AI will attack weak target + final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield); for (Card buffedcard : antibuffed) { if (buffedcard.hasSVar("AntiBuffedBy")) { final String buffedby = buffedcard.getSVar("AntiBuffedBy"); @@ -1142,27 +1139,16 @@ public class ComputerUtil { * @return true if it's OK to cast this Card for less than the max targets */ public static boolean shouldCastLessThanMax(final Player ai, final Card source) { - boolean ret = true; - if (source.getManaCost().countX() > 0) { - // If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by mana available. - return ret; - } else { - // Otherwise, if life is possibly in danger, then this is fine. - Combat combat = new Combat(ComputerUtil.getOpponentFor(ai)); - CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay(); - for (Card att : attackers) { - if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { - combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController())); - } - } - AiBlockController aiBlock = new AiBlockController(ai); - aiBlock.assignBlockersForCombat(combat); - if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { - // Otherwise, return false. Do not play now. - ret = false; - } + if (source.getXManaCostPaid() > 0) { + // If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available. + return true; } - return ret; + if (aiLifeInDanger(ai, false, 0)) { + // Otherwise, if life is possibly in danger, then this is fine. + return true; + } + // do not play now. + return false; } /** @@ -1266,8 +1252,8 @@ public class ComputerUtil { } } - // get all cards the human controls with AntiBuffedBy - final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield); + // there's a good chance AI will attack weak target + final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield); for (Card buffedcard : antibuffed) { if (buffedcard.hasSVar("AntiBuffedBy")) { final String buffedby = buffedcard.getSVar("AntiBuffedBy"); @@ -1463,7 +1449,7 @@ public class ComputerUtil { return false; } - public static int possibleNonCombatDamage(Player ai) { + public static int possibleNonCombatDamage(Player ai, Player enemy) { int damage = 0; final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); all.addAll(ai.getCardsActivableInExternalZones(true)); @@ -1483,7 +1469,6 @@ public class ComputerUtil { if (tgt == null) { continue; } - final Player enemy = ComputerUtil.getOpponentFor(ai); if (!sa.canTarget(enemy)) { continue; } @@ -2346,7 +2331,7 @@ public class ComputerUtil { } } else if (logic.equals("ChosenLandwalk")) { - for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) { + for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) { for (String t : c.getType()) { if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) { chosen = t; @@ -2364,7 +2349,7 @@ public class ComputerUtil { else if (kindOfType.equals("Land")) { if (logic != null) { if (logic.equals("ChosenLandwalk")) { - for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) { + for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) { for (String t : c.getType().getLandTypes()) { if (!invalidTypes.contains(t)) { chosen = t; @@ -2399,23 +2384,26 @@ public class ComputerUtil { case "Torture": return "Torture"; case "GraceOrCondemnation": - return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace" - : "Condemnation"; + List graceZones = new ArrayList(); + graceZones.add(ZoneType.Battlefield); + graceZones.add(ZoneType.Graveyard); + CardCollection graceCreatures = CardLists.getType(sa.getHostCard().getGame().getCardsIn(graceZones), "Creature"); + int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size(); + int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size(); + return aiGrace > humanGrace ? "Grace" : "Condemnation"; case "CarnageOrHomage": - CardCollection cardsInPlay = CardLists - .getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land"); + CardCollection cardsInPlay = CardLists.getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land"); CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents()); - CardCollection computerlist = CardLists.filterControlledBy(cardsInPlay, ai); - return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard - .evaluatePermanentList(humanlist) ? "Carnage" : "Homage"; + CardCollection computerlist = ai.getCreaturesInPlay(); + return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage"; case "Judgment": if (votes.isEmpty()) { CardCollection list = new CardCollection(); for (Object o : options) { if (o instanceof Card) { list.add((Card) o); - } } + } return ComputerUtilCard.getBestAI(list); } else { return Iterables.getFirst(votes.keySet(), null); @@ -2934,23 +2922,6 @@ public class ComputerUtil { return true; } - - @Deprecated - public static final Player getOpponentFor(final Player player) { - // This method is deprecated and currently functions as a synonym for player.getWeakestOpponent - // until it can be replaced everywhere in the code. - - // Consider replacing calls to this method either with a multiplayer-friendly determination of - // opponent that contextually makes the most sense, or with a direct call to player.getWeakestOpponent - // where that is applicable and makes sense from the point of view of multiplayer AI logic. - Player opponent = player.getWeakestOpponent(); - if (opponent != null) { - return opponent; - } - - throw new IllegalStateException("No opponents left ingame for " + player); - } - public static int countUsefulCreatures(Player p) { CardCollection creats = p.getCreaturesInPlay(); int count = 0; @@ -3033,31 +3004,32 @@ public class ComputerUtil { // call this to determine if it's safe to use a life payment spell // or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell. public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) { - Player opponent = ComputerUtil.getOpponentFor(ai); - // test whether the human can kill the ai next turn - Combat combat = new Combat(opponent); - boolean containsAttacker = false; - for (Card att : opponent.getCreaturesInPlay()) { - if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { - combat.addAttacker(att, ai); - containsAttacker = true; + for (Player opponent: ai.getOpponents()) { + // test whether the human can kill the ai next turn + Combat combat = new Combat(opponent); + boolean containsAttacker = false; + for (Card att : opponent.getCreaturesInPlay()) { + if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { + combat.addAttacker(att, ai); + containsAttacker = true; + } } - } - if (!containsAttacker) { - return false; - } - AiBlockController block = new AiBlockController(ai); - block.assignBlockersForCombat(combat); + if (!containsAttacker) { + return false; + } + AiBlockController block = new AiBlockController(ai); + block.assignBlockersForCombat(combat); - // TODO predict other, noncombat sources of damage and add them to the "payment" variable. - // examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc - // If added, might need a parameter to define whether we want to check all threats or combat threats. + // TODO predict other, noncombat sources of damage and add them to the "payment" variable. + // examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc + // If added, might need a parameter to define whether we want to check all threats or combat threats. - if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) { - return true; - } - if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) { - return true; + if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) { + return true; + } + if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) { + return true; + } } return false; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 00433b71a17..19172312c04 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -550,7 +550,7 @@ public class ComputerUtilCard { */ public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) { AiBlockController aiBlk = new AiBlockController(ai); - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); Combat combat = new Combat(opp); //Use actual attackers if available, else consider all possible attackers Combat currentCombat = ai.getGame().getCombat(); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index d4b7d549f74..ebcde781c0e 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -97,34 +97,39 @@ public class ComputerUtilCombat { * canAttackNextTurn. *

* - * @param atacker + * @param attacker * a {@link forge.game.card.Card} object. * @param defender * the defending {@link GameEntity}. * @return a boolean. */ - public static boolean canAttackNextTurn(final Card atacker, final GameEntity defender) { - if (!atacker.isCreature()) { + public static boolean canAttackNextTurn(final Card attacker, final GameEntity defender) { + if (!attacker.isCreature()) { return false; } - if (!CombatUtil.canAttackNextTurn(atacker, defender)) { + if (!CombatUtil.canAttackNextTurn(attacker, defender)) { return false; } - for (final KeywordInterface inst : atacker.getKeywords()) { + for (final KeywordInterface inst : attacker.getKeywords()) { final String keyword = inst.getOriginal(); if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) { final String defined = keyword.split(":")[1]; - final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0); + final Player player = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0); if (!defender.equals(player)) { return false; } } } + // TODO this should be a factor but needs some alignment with AttachAi + //boolean leavesPlay = !ComputerUtilCard.hasActiveUndyingOrPersist(attacker) + // && ((attacker.hasKeyword(Keyword.VANISHING) && attacker.getCounters(CounterEnumType.TIME) == 1) + // || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0) + // || attacker.hasSVar("EndOfTurnLeavePlay")); // The creature won't untap next turn - return !atacker.isTapped() || Untap.canUntap(atacker); - } // canAttackNextTurn(Card, GameEntity) + return !attacker.isTapped() || Untap.canUntap(attacker); + } /** *

diff --git a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java index e261dab37db..3340761aff4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java @@ -22,7 +22,6 @@ public class ActivateAbilityAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); final Player opp = ai.getWeakestOpponent(); - boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); List list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card")); if (list.isEmpty()) { @@ -40,6 +39,7 @@ public class ActivateAbilityAi extends SpellAbilityAi { sa.getTargets().add(opp); } + boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); return randomReturn; } @@ -56,7 +56,8 @@ public class ActivateAbilityAi extends SpellAbilityAi { } else { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); - return defined.contains(opp); + // if at least two players are returned we can affect another opponent + return defined.size() > 1 || defined.contains(opp); } } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java b/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java index 9aba6e53fe9..902741c6da3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java @@ -13,18 +13,21 @@ public class BalanceAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { String logic = sa.getParam("AILogic"); - int diff = 0; - // TODO Add support for multiplayer logic - final Player opp = aiPlayer.getWeakestOpponent(); - final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield); + Player opp = aiPlayer.getWeakestOpponent(); final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield); + for (Player min : aiPlayer.getOpponents()) { + if (min.getCardsIn(ZoneType.Battlefield).size() < opp.getCardsIn(ZoneType.Battlefield).size()) { + opp = min; + } + } + final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield); if ("BalanceCreaturesAndLands".equals(logic)) { - // Copied over from hardcoded Balance. We should be checking value of the lands/creatures not just counting + // TODO Copied over from hardcoded Balance. We should be checking value of the lands/creatures for each opponent, not just counting diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() - CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size(); - diff += 1.5 * ( CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() - + diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() - CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size()); } else if ("BalancePermanents".equals(logic)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java index 634fabc1254..6f7b6e943d6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi { if (tgt != null) { sa.resetTargets(); if (tgt.canTgtCreature()) { - List list = CardLists.getTargetableCards(aiPlayer.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), sa); + List list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa); list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); if (list.isEmpty()) { return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index 58ac2740907..51c19811578 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -15,6 +15,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import forge.ai.AiAttackController; import forge.ai.AiBlockController; import forge.ai.AiCardMemory; import forge.ai.AiController; @@ -263,7 +264,7 @@ public class ChangeZoneAi extends SpellAbilityAi { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); ZoneType origin = null; - final Player opponent = ai.getWeakestOpponent(); + final Player opponent = AiAttackController.choosePreferredDefenderPlayer(ai); boolean activateForCost = ComputerUtil.activateForCost(sa, ai); if (sa.hasParam("Origin")) { @@ -471,7 +472,7 @@ public class ChangeZoneAi extends SpellAbilityAi { // if putting cards from hand to library and parent is drawing cards // make sure this will actually do something: final TargetRestrictions tgt = sa.getTargetRestrictions(); - final Player opp = aiPlayer.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); if (tgt != null && tgt.canTgtPlayer()) { boolean isCurse = sa.isCurse(); if (isCurse && sa.canTarget(opp)) { @@ -530,7 +531,7 @@ public class ChangeZoneAi extends SpellAbilityAi { Iterable pDefined; final TargetRestrictions tgt = sa.getTargetRestrictions(); if ((tgt != null) && tgt.canTgtPlayer()) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.isCurse()) { if (sa.canTarget(opp)) { sa.getTargets().add(opp); @@ -892,7 +893,6 @@ public class ChangeZoneAi extends SpellAbilityAi { // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? sa.setXManaCostPaid(xPay); - // TODO since change of PayX. the shouldCastLessThanMax logic might be faulty } CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); @@ -913,9 +913,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (sa.isSpell()) { list.remove(source); // spells can't target their own source, because it's actually in the stack zone } - //System.out.println("isPreferredTarget " + list); if (sa.hasParam("AttachedTo")) { - //System.out.println("isPreferredTarget att " + list); list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { @@ -927,7 +925,6 @@ public class ChangeZoneAi extends SpellAbilityAi { return false; } }); - //System.out.println("isPreferredTarget ok " + list); } if (list.size() < sa.getMinTargets()) { @@ -1482,9 +1479,11 @@ public class ChangeZoneAi extends SpellAbilityAi { if ("DeathgorgeScavenger".equals(logic)) { return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa); - } else if ("ExtraplanarLens".equals(logic)) { + } + if ("ExtraplanarLens".equals(logic)) { return SpecialCardAi.ExtraplanarLens.consider(ai, sa); - } else if ("ExileCombatThreat".equals(logic)) { + } + if ("ExileCombatThreat".equals(logic)) { return doExileCombatThreatLogic(ai, sa); } @@ -1984,11 +1983,7 @@ public class ChangeZoneAi extends SpellAbilityAi { toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); } - if (toPay == 0) { - canBeSaved.add(potentialTgt); - } - - if (toPay <= usableManaSources) { + if (toPay == 0 || toPay <= usableManaSources) { canBeSaved.add(potentialTgt); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java index a50ee8c6def..3dd7825cad3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -9,6 +9,7 @@ import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCombat; @@ -114,7 +115,7 @@ public class ChooseCardAi extends SpellAbilityAi { return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty(); } else if (aiLogic.equals("Duneblast")) { CardCollection aiCreatures = ai.getCreaturesInPlay(); - CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay(); + CardCollection oppCreatures = AiAttackController.choosePreferredDefenderPlayer(ai).getCreaturesInPlay(); aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE); oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE); diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java index 469a095fa4e..b12f37aecca 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java @@ -7,6 +7,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import forge.StaticData; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; import forge.ai.SpecialCardAi; @@ -44,7 +45,7 @@ public class ChooseCardNameAi extends SpellAbilityAi { if (tgt != null) { sa.resetTargets(); if (tgt.canOnlyTgtOpponent()) { - sa.getTargets().add(ai.getWeakestOpponent()); + sa.getTargets().add(AiAttackController.choosePreferredDefenderPlayer(ai)); } else { sa.getTargets().add(ai); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java index e5ac8a99d6d..2a6e465865a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -16,7 +17,7 @@ public class ChooseEvenOddAi extends SpellAbilityAi { TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); - Player opp = aiPlayer.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java index 46276414778..639baa92c40 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -16,7 +17,7 @@ public class ChooseNumberAi extends SpellAbilityAi { TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); - Player opp = aiPlayer.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java index 1396a1bd6dd..8a71e557d4d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java @@ -8,6 +8,7 @@ import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCombat; import forge.ai.SpellAbilityAi; @@ -54,7 +55,7 @@ public class ChooseSourceAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java index 57d56de1759..65f11c69b35 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java @@ -3,6 +3,7 @@ package forge.ai.ability; import com.google.common.base.Predicate; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; @@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi { sa.resetTargets(); CardCollection list = - CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); + CardLists.getValidCards(AiAttackController.choosePreferredDefenderPlayer(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); // AI won't try to grab cards that are filtered out of AI decks on // purpose list = CardLists.filter(list, new Predicate() { diff --git a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java index 4a28f540a1e..76263212db2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java @@ -8,6 +8,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; @@ -26,9 +27,6 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; public class DebuffAi extends SpellAbilityAi { - // ************************************************************************* - // ***************************** Debuff ************************************ - // ************************************************************************* @Override protected boolean canPlayAI(final Player ai, final SpellAbility sa) { @@ -140,7 +138,6 @@ public class DebuffAi extends SpellAbilityAi { while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) { Card t = null; - // boolean goodt = false; if (list.isEmpty()) { if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) { @@ -176,19 +173,18 @@ public class DebuffAi extends SpellAbilityAi { * @return a CardCollection. */ private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List kws) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa); if (!list.isEmpty()) { list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return c.hasAnyKeyword(kws); // don't add duplicate negative - // keywords + return c.hasAnyKeyword(kws); // don't add duplicate negative keywords } }); } return list; - } // getCurseCreatures() + } /** *

@@ -216,7 +212,7 @@ public class DebuffAi extends SpellAbilityAi { list.remove(c); } - final CardCollection pref = CardLists.filterControlledBy(list, ai.getWeakestOpponent()); + final CardCollection pref = CardLists.filterControlledBy(list, ai.getOpponents()); final CardCollection forced = CardLists.filterControlledBy(list, ai); final Card source = sa.getHostCard(); @@ -242,8 +238,7 @@ public class DebuffAi extends SpellAbilityAi { break; } - // TODO - if forced targeting, just pick something without the given - // keyword + // TODO - if forced targeting, just pick something without the given keyword Card c; if (CardLists.getNotType(forced, "Creature").size() == 0) { c = ComputerUtilCard.getWorstCreatureAI(forced); diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java index 13f1a975b53..79f88577204 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java @@ -70,12 +70,12 @@ public class DestroyAllAi extends SpellAbilityAi { return doMassRemovalLogic(ai, sa); } - public boolean doMassRemovalLogic(Player ai, SpellAbility sa) { + public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String logic = sa.getParamOrDefault("AILogic", ""); - Player opponent = ai.getWeakestOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()? - final int CREATURE_EVAL_THRESHOLD = 200; + // if we hit the whole board, the other opponents who are not the reason to cast this probably still suffer a bit too + final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1); if (logic.equals("Always")) { return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing @@ -93,99 +93,101 @@ public class DestroyAllAi extends SpellAbilityAi { valid = valid.replace("X", Integer.toString(xPay)); } - CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), - valid.split(","), source.getController(), source, sa); - CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), - source.getController(), source, sa); - - opplist = CardLists.filter(opplist, predicate); - ailist = CardLists.filter(ailist, predicate); - if (opplist.isEmpty()) { - return false; - } + // TODO should probably sort results when targeted to use on biggest threat instead of first match + for (Player opponent: ai.getOpponents()) { + CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); + CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); - if (sa.usesTargeting()) { - sa.resetTargets(); - if (sa.canTarget(opponent)) { - sa.getTargets().add(opponent); - ailist.clear(); - } else { + opplist = CardLists.filter(opplist, predicate); + ailist = CardLists.filter(ailist, predicate); + if (opplist.isEmpty()) { return false; } - } - // Special handling for Raiding Party - if (logic.equals("RaidingParty")) { - int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size()); - int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size()); - - return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave); - } - - // If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what! - if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) - && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) { - return true; - } - - // If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much - if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) - && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat())) - && ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) { - return true; - } - - // if only creatures are affected evaluate both lists and pass only if - // human creatures are more valuable - if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) { - if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) { - return true; - } - - if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { - return false; - } - - // test whether the human can kill the ai next turn - Combat combat = new Combat(opponent); - boolean containsAttacker = false; - for (Card att : opponent.getCreaturesInPlay()) { - if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { - combat.addAttacker(att, ai); - containsAttacker = containsAttacker | opplist.contains(att); - } - } - if (!containsAttacker) { - return false; - } - AiBlockController block = new AiBlockController(ai); - block.assignBlockersForCombat(combat); - - if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { - return true; - } - return false; - } // only lands involved - else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) { - if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds") && !opplist.isEmpty()) { - return true; - } - // evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes - CardCollection aiCreatures = ai.getCreaturesInPlay(); - CardCollection oppCreatures = opponent.getCreaturesInPlay(); - if (!oppCreatures.isEmpty()) { - if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) { + if (sa.usesTargeting()) { + sa.resetTargets(); + if (sa.canTarget(opponent)) { + sa.getTargets().add(opponent); + ailist.clear(); + } else { return false; } } - // check if the AI would lose more lands than the opponent would - if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) { + + // Special handling for Raiding Party + if (logic.equals("RaidingParty")) { + int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size()); + int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size()); + + return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave); + } + + // If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what! + if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) + && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) { + return true; + } + + // If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much + if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) + && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat())) + && ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) { + return true; + } + + // if only creatures are affected evaluate both lists and pass only if human creatures are more valuable + if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) { + if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) { + return true; + } + + if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { + return false; + } + + // test whether the human can kill the ai next turn + Combat combat = new Combat(opponent); + boolean containsAttacker = false; + for (Card att : opponent.getCreaturesInPlay()) { + if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { + combat.addAttacker(att, ai); + containsAttacker = containsAttacker | opplist.contains(att); + } + } + if (!containsAttacker) { + return false; + } + AiBlockController block = new AiBlockController(ai); + block.assignBlockersForCombat(combat); + + if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { + return true; + } + return false; + } // only lands involved + else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) { + if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) { + return true; + } + // evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes + CardCollection aiCreatures = ai.getCreaturesInPlay(); + CardCollection oppCreatures = opponent.getCreaturesInPlay(); + if (!oppCreatures.isEmpty()) { + if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) { + return false; + } + } + // check if the AI would lose more lands than the opponent would + if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) { + return false; + } + } // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable + else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) { return false; } - } // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable - else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) { - return false; + return true; } - return true; + return false; } -} + +} \ No newline at end of file diff --git a/forge-ai/src/main/java/forge/ai/ability/DigAi.java b/forge-ai/src/main/java/forge/ai/ability/DigAi.java index 4d7ffc1f458..33e3c315dc8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java @@ -5,6 +5,7 @@ import java.util.Map; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; @@ -32,7 +33,7 @@ public class DigAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Card host = sa.getHostCard(); Player libraryOwner = ai; @@ -120,7 +121,7 @@ public class DigAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final SpellAbility root = sa.getRootAbility(); - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { sa.resetTargets(); if (mandatory && sa.canTarget(opp)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java index 07796e6fba9..1e3b0350408 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -19,7 +20,7 @@ public class DigMultipleAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Card host = sa.getHostCard(); Player libraryOwner = ai; @@ -77,7 +78,7 @@ public class DigMultipleAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { sa.resetTargets(); if (mandatory && sa.canTarget(opp)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java index 449561f46bf..d421a67f54d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -31,10 +32,8 @@ public class DigUntilAi extends SpellAbilityAi { chance = 1; } - final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1); - Player libraryOwner = ai; - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if ("DontMillSelf".equals(logic)) { // A card that digs for specific things and puts everything revealed before it into graveyard @@ -92,12 +91,12 @@ public class DigUntilAi extends SpellAbilityAi { return false; } + final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1); return randomReturn; } @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - if (sa.usesTargeting()) { sa.resetTargets(); if (sa.isCurse()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java index dd1e0fc35d6..17d69dcdbc0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCost; @@ -60,11 +61,9 @@ public class DiscardAi extends SpellAbilityAi { if (players.get(0) == ai) { // the ai should only be using something like this if he has // few cards in hand, - // cards like this better have a good drawback to be in the - // AIs deck + // cards like this better have a good drawback to be in the AIs deck } else { - // defined to the human, so that's fine as long the human - // has cards + // defined to the human, so that's fine as long the human has cards if (!humanHasHand) { return false; } @@ -170,7 +169,7 @@ public class DiscardAi extends SpellAbilityAi { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (!discardTargetAI(ai, sa)) { if (mandatory && sa.canTarget(opp)) { sa.getTargets().add(opp); diff --git a/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java b/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java index 96310431fc3..870ae38bffd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java @@ -23,8 +23,7 @@ public class DrainManaAi extends SpellAbilityAi { if (tgt == null) { // assume we are looking to tap human's stuff - // TODO - check for things with untap abilities, and don't tap - // those. + // TODO - check for things with untap abilities, and don't tap those. final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); if (!defined.contains(opp)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java index 03b77143d99..8bdd71b1394 100644 --- a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java @@ -29,11 +29,10 @@ public class GameLossAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - // Phage the Untouchable // (Final Fortune would need to attach it's delayed trigger to a // specific turn, which can't be done yet) - Player opp = ai.getWeakestOpponent(); + Player opp = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard()); if (!mandatory && opp.cantLose()) { return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java index 8b387b7791a..2aba049fa22 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -19,7 +20,7 @@ public class LifeExchangeAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { final int myLife = aiPlayer.getLife(); - Player opponent = aiPlayer.getWeakestOpponent(); + Player opponent = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); final int hLife = opponent.getLife(); if (!aiPlayer.canGainLife()) { @@ -75,7 +76,7 @@ public class LifeExchangeAi extends SpellAbilityAi { final boolean mandatory) { final TargetRestrictions tgt = sa.getTargetRestrictions(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (tgt != null) { sa.resetTargets(); if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java index 0fd3e1e0702..7c3147c3dfd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.AiProps; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; @@ -154,7 +155,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { final boolean mandatory) { final TargetRestrictions tgt = sa.getTargetRestrictions(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (tgt != null) { sa.resetTargets(); if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java index 91e17c2da2f..cbf94b08d48 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java @@ -17,7 +17,7 @@ public class LifeSetAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final int myLife = ai.getLife(); - final Player opponent = ai.getWeakestOpponent(); + final Player opponent = ai.getStrongestOpponent(); final int hlife = opponent.getLife(); final String amountStr = sa.getParam("LifeAmount"); @@ -36,8 +36,7 @@ public class LifeSetAi extends SpellAbilityAi { return false; } - // TODO handle proper calculation of X values based on Cost and what - // would be paid + // TODO handle proper calculation of X values based on Cost and what would be paid int amount; // we shouldn't have to worry too much about PayX for SetLife if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { @@ -58,11 +57,9 @@ public class LifeSetAi extends SpellAbilityAi { if (tgt.canOnlyTgtOpponent()) { sa.getTargets().add(opponent); // if we can only target the human, and the Human's life - // would - // go up, don't play it. + // would go up, don't play it. // possibly add a combo here for Magister Sphinx and - // Higedetsu's - // (sp?) Second Rite + // Higedetsu's (sp?) Second Rite if ((amount > hlife) || !opponent.canLoseLife()) { return false; } @@ -81,8 +78,7 @@ public class LifeSetAi extends SpellAbilityAi { if (sa.getParam("Defined").equals("Player")) { if (amount == 0) { return false; - } else if (myLife > amount) { // will decrease computer's - // life + } else if (myLife > amount) { // will decrease computer's life if ((myLife < 5) || ((myLife - amount) > (hlife - amount))) { return false; } @@ -104,7 +100,7 @@ public class LifeSetAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final int myLife = ai.getLife(); - final Player opponent = ai.getWeakestOpponent(); + final Player opponent = ai.getStrongestOpponent(); final int hlife = opponent.getLife(); final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); @@ -133,8 +129,7 @@ public class LifeSetAi extends SpellAbilityAi { } // If the Target is gaining life, target self. - // if the Target is modifying how much life is gained, this needs to - // be handled better + // if the Target is modifying how much life is gained, this needs to be handled better final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); diff --git a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java index 9626b8f6f43..33d421d5626 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java @@ -30,13 +30,12 @@ public class PowerExchangeAi extends SpellAbilityAi { sa.resetTargets(); List list = - CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); - // AI won't try to grab cards that are filtered out of AI decks on - // purpose + CardLists.getValidCards(ai.getGame().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); + // AI won't try to grab cards that are filtered out of AI decks on purpose list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa); + return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa) && c.getController() != ai; } }); CardLists.sortByPowerAsc(list); diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java index aba87269e83..0fc65c64997 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -6,6 +6,7 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCombat; @@ -201,7 +202,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { final Game game = ai.getGame(); final Combat combat = game.getCombat(); final PhaseHandler ph = game.getPhaseHandler(); - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final int newPower = card.getNetCombatDamage() + attack; //int defense = getNumDefense(sa); if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java index 381147880bd..386d043770d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java @@ -48,12 +48,6 @@ public class PumpAllAi extends PumpAiBase { } } - final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa); - final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa); - final List keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>(); - - final PhaseType phase = game.getPhaseHandler().getPhase(); - if (ComputerUtil.preventRunAwayActivations(sa)) { return false; } @@ -63,16 +57,10 @@ public class PumpAllAi extends PumpAiBase { return false; } } - - if (sa.hasParam("ValidCards")) { - valid = sa.getParam("ValidCards"); - } - - final Player opp = ai.getWeakestOpponent(); - CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); - CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); - + final TargetRestrictions tgt = sa.getTargetRestrictions(); + final Player opp = ai.getStrongestOpponent(); + if (tgt != null && sa.canTarget(opp) && sa.hasParam("IsCurse")) { sa.resetTargets(); sa.getTargets().add(opp); @@ -85,6 +73,18 @@ public class PumpAllAi extends PumpAiBase { return true; } + final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa); + final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa); + final List keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>(); + final PhaseType phase = game.getPhaseHandler().getPhase(); + + if (sa.hasParam("ValidCards")) { + valid = sa.getParam("ValidCards"); + } + + CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); + CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); + if (!game.getStack().isEmpty() && !sa.isCurse()) { return pumpAgainstRemoval(ai, sa, comp); } @@ -139,8 +139,7 @@ public class PumpAllAi extends PumpAiBase { return true; } - // evaluate both lists and pass only if human creatures are more - // valuable + // evaluate both lists and pass only if human creatures are more valuable return (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human); } // end Curse diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java index 0f9c2a20ffe..310ea94efdf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.AiController; import forge.ai.ComputerUtilCost; import forge.ai.PlayerControllerAi; @@ -14,7 +15,7 @@ public class RepeatAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { if (!opp.canBeTargetedBy(sa)) { @@ -44,9 +45,8 @@ public class RepeatAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - if (sa.usesTargeting()) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.canTarget(opp)) { sa.resetTargets(); sa.getTargets().add(opp); diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java index dfd420dab29..36d0fa484c6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java @@ -20,9 +20,6 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; public class SacrificeAi extends SpellAbilityAi { - // ************************************************************** - // *************************** Sacrifice *********************** - // ************************************************************** @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { @@ -48,8 +45,7 @@ public class SacrificeAi extends SpellAbilityAi { } // Improve AI for triggers. If source is a creature with: - // When ETB, sacrifice a creature. Check to see if the AI has something - // to sacrifice + // When ETB, sacrifice a creature. Check to see if the AI has something to sacrifice // Eventually, we can call the trigger of ETB abilities with not // mandatory as part of the checks to cast something @@ -58,12 +54,11 @@ public class SacrificeAi extends SpellAbilityAi { } private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa) { - final Card source = sa.getHostCard(); final TargetRestrictions tgt = sa.getTargetRestrictions(); final boolean destroy = sa.hasParam("Destroy"); - Player opp = ai.getWeakestOpponent(); + Player opp = ai.getStrongestOpponent(); if (tgt != null) { sa.resetTargets(); @@ -109,8 +104,7 @@ public class SacrificeAi extends SpellAbilityAi { sa.setXManaCostPaid(Math.min(ComputerUtilCost.getMaxXValue(sa, ai), amount)); } - final int half = (amount / 2) + (amount % 2); // Half of amount - // rounded up + final int half = (amount / 2) + (amount % 2); // Half of amount rounded up // If the Human has at least half rounded up of the amount to be // sacrificed, cast the spell @@ -130,8 +124,7 @@ public class SacrificeAi extends SpellAbilityAi { // If Sacrifice hits both players: // Only cast it if Human has the full amount of valid // Only cast it if AI doesn't have the full amount of Valid - // TODO: Cast if the type is favorable: my "worst" valid is - // worse than his "worst" valid + // TODO: Cast if the type is favorable: my "worst" valid is worse than his "worst" valid final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1"; int amount = AbilityUtils.calculateAmount(source, num, sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java index 84f496ef24e..11ebac1c75c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java @@ -2,17 +2,12 @@ package forge.ai.ability; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; -import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; import forge.game.card.Card; -import forge.game.card.CardCollection; -import forge.game.card.CardLists; import forge.game.cost.Cost; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.zone.ZoneType; import forge.util.MyRandom; -import forge.util.TextUtil; public class SacrificeAllAi extends SpellAbilityAi { @@ -22,22 +17,7 @@ public class SacrificeAllAi extends SpellAbilityAi { // based on what the expected targets could be final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); - String valid = ""; - - if (sa.hasParam("ValidCards")) { - valid = sa.getParam("ValidCards"); - } - - if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { - // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay)); - } - - CardCollection humanlist = - CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); - CardCollection computerlist = - CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); + final String logic = sa.getParamOrDefault("AILogic", ""); if (abCost != null) { // AI currently disabled for some costs @@ -45,30 +25,20 @@ public class SacrificeAllAi extends SpellAbilityAi { return false; } } + + if (logic.equals("HellionEruption")) { + if (ai.getCreaturesInPlay().size() < 5 || ai.getCreaturesInPlay().size() * 150 < ComputerUtilCard.evaluateCreatureList(ai.getCreaturesInPlay())) { + return false; + } + } + + if (!DestroyAllAi.doMassRemovalLogic(ai, sa)) { + return false; + } // prevent run-away activations - first time will always return true boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - // if only creatures are affected evaluate both lists and pass only if - // human creatures are more valuable - if ((CardLists.getNotType(humanlist, "Creature").size() == 0) && (CardLists.getNotType(computerlist, "Creature").size() == 0)) { - if ((ComputerUtilCard.evaluateCreatureList(computerlist) + 200) >= ComputerUtilCard - .evaluateCreatureList(humanlist)) { - return false; - } - } // only lands involved - else if ((CardLists.getNotType(humanlist, "Land").size() == 0) && (CardLists.getNotType(computerlist, "Land").size() == 0)) { - if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 1) >= ComputerUtilCard - .evaluatePermanentList(humanlist)) { - return false; - } - } // otherwise evaluate both lists by CMC and pass only if human - // permanents are more valuable - else if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 3) >= ComputerUtilCard - .evaluatePermanentList(humanlist)) { - return false; - } - return ((MyRandom.getRandom().nextFloat() < .9667) && chance); } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java index cb6428ee193..7216345b1f8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java @@ -66,7 +66,6 @@ public class TapAi extends TapAiBase { // Set PayX here to maximum value. // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai)); - // TODO since change of PayX. the shouldCastLessThanMax logic might be faulty } sa.resetTargets(); diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java index fb27b86a4d6..a8f46f2a180 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java @@ -5,6 +5,7 @@ import java.util.List; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; @@ -109,7 +110,7 @@ public abstract class TapAiBase extends SpellAbilityAi { * @return a boolean. */ protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Game game = ai.getGame(); CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); tapList = CardLists.getTargetableCards(tapList, sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java index b871171de7b..d5b687f0774 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi { valid = sa.getParam("ValidCards"); } - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { diff --git a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java index 15b2c12a6d0..1e017a982d8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java @@ -56,7 +56,6 @@ public class UnattachAllAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Card card = sa.getHostCard(); - final Player opp = ai.getWeakestOpponent(); // Check if there are any valid targets List targets = new ArrayList<>(); final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -66,8 +65,8 @@ public class UnattachAllAi extends SpellAbilityAi { if (!mandatory && card.isEquipment() && !targets.isEmpty()) { Card newTarget = (Card) targets.get(0); - //don't equip human creatures - if (newTarget.getController().equals(opp)) { + //don't equip opponent creatures + if (!newTarget.getController().equals(ai)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java index 00c4837c82b..47c443f59b8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -6,6 +6,7 @@ import java.util.Map; import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; @@ -130,7 +131,8 @@ public class UntapAi extends SpellAbilityAi { Player targetController = ai; if (sa.isCurse()) { - targetController = ai.getWeakestOpponent(); + // TODO search through all opponents, may need to check if different controllers allowed + targetController = AiAttackController.choosePreferredDefenderPlayer(ai); } CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa); @@ -149,8 +151,7 @@ public class UntapAi extends SpellAbilityAi { } CardCollection untapList = targetUntapped ? list : CardLists.filter(list, Presets.TAPPED); - // filter out enchantments and planeswalkers, their tapped state doesn't - // matter. + // filter out enchantments and planeswalkers, their tapped state doesn't matter. final String[] tappablePermanents = {"Creature", "Land", "Artifact"}; untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa); diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 8a35eee03e2..9d4320867b1 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -1247,17 +1247,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { currentState.addTrigger(t); return t; } - @Deprecated - public final void removeTrigger(final Trigger t) { - currentState.removeTrigger(t); - } - @Deprecated - public final void removeTrigger(final Trigger t, final CardStateName state) { - getState(state).removeTrigger(t); - } - public final void clearTriggersNew() { - currentState.clearTriggers(); - } public final boolean hasTrigger(final Trigger t) { return currentState.hasTrigger(t); diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index c83ce6d79db..7954026a69c 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -421,6 +421,9 @@ public class Player extends GameEntity implements Comparable { public final Player getWeakestOpponent() { return getOpponents().min(PlayerPredicates.compareByLife()); } + public final Player getStrongestOpponent() { + return getOpponents().max(PlayerPredicates.compareByLife()); + } public boolean isOpponentOf(Player other) { return other != this && other != null && (other.teamNumber < 0 || other.teamNumber != teamNumber); diff --git a/forge-gui/res/cardsfolder/g/goblin_artisans.txt b/forge-gui/res/cardsfolder/g/goblin_artisans.txt index 342618a2a0c..e0f85b739a0 100644 --- a/forge-gui/res/cardsfolder/g/goblin_artisans.txt +++ b/forge-gui/res/cardsfolder/g/goblin_artisans.txt @@ -10,4 +10,5 @@ SVar:DBCleanup:DB$ Cleanup | ForgetDefined$ TriggeredCard A:AB$ FlipACoin | Cost$ T | TgtZone$ Stack | TargetType$ Spell | ValidTgts$ Artifact.YouCtrl+IsRemembered | WinSubAbility$ DBDraw | LoseSubAbility$ DBCounter | TgtPrompt$ Select target Artifact spell | SpellDescription$ Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans. SVar:DBDraw:DB$ Draw | NumCards$ 1 | Defined$ You SVar:DBCounter:DB$ Counter | Defined$ Targeted +AI:RemoveDeck:All Oracle:{T}: Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans. diff --git a/forge-gui/res/cardsfolder/h/hellion_eruption.txt b/forge-gui/res/cardsfolder/h/hellion_eruption.txt index 1d81e61d60b..b055e17e158 100644 --- a/forge-gui/res/cardsfolder/h/hellion_eruption.txt +++ b/forge-gui/res/cardsfolder/h/hellion_eruption.txt @@ -1,9 +1,8 @@ Name:Hellion Eruption ManaCost:5 R Types:Sorcery -A:SP$ SacrificeAll | Cost$ 5 R | ValidCards$ Creature.YouCtrl | RememberSacrificed$ True | SubAbility$ DBToken | SpellDescription$ Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens. +A:SP$ SacrificeAll | Cost$ 5 R | ValidCards$ Creature.YouCtrl | RememberSacrificed$ True | SubAbility$ DBToken | AILogic$ HellionEruption | SpellDescription$ Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens. SVar:DBToken:DB$Token | TokenAmount$ X | TokenScript$ r_4_4_hellion | TokenOwner$ You | LegacyImage$ r 4 4 hellion roe SVar:X:Remembered$Amount -AI:RemoveDeck:All SVar:Picture:http://www.wizards.com/global/images/magic/general/hellion_eruption.jpg Oracle:Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens. From 6d00a26530cec429df565a658b1f37acaa0c06b0 Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Fri, 26 Mar 2021 14:38:10 +0300 Subject: [PATCH 8/8] - Basic logic for Professor Onyx. --- forge-ai/src/main/java/forge/ai/AiController.java | 13 ++++++++++--- .../res/cardsfolder/upcoming/professor_onyx.txt | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 124a4a8e9c4..976989da429 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1102,6 +1102,8 @@ public class AiController { } public CardCollection getCardsToDiscard(int min, final int max, final CardCollection validCards, final SpellAbility sa) { + String logic = sa.getParamOrDefault("AILogic", ""); + if (validCards.size() < min) { return null; } @@ -1111,11 +1113,16 @@ public class AiController { int count = 0; if (sa != null) { sourceCard = sa.getHostCard(); - if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) { + if ("Always".equals(logic) && !validCards.isEmpty()) { min = 1; - } else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) { + } else if (logic.startsWith("UnlessAtLife.")) { + int threshold = AbilityUtils.calculateAmount(sourceCard, logic.substring(logic.indexOf(".") + 1), sa); + if (player.getLife() <= threshold) { + min = 1; + } + } else if ("VolrathsShapeshifter".equals(logic)) { return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa); - } else if ("DiscardCMCX".equals(sa.getParam("AILogic"))) { + } else if ("DiscardCMCX".equals(logic)) { final int cmc = sa.getXManaCostPaid(); CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc)); if (discards.isEmpty()) { diff --git a/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt b/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt index aaaf99e0b9e..a2d600935b2 100644 --- a/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt +++ b/forge-gui/res/cardsfolder/upcoming/professor_onyx.txt @@ -13,7 +13,7 @@ SVar:DBChooseCard:DB$ ChooseCard | Defined$ Player.IsRemembered | Choices$ Creat SVar:DBSac:DB$ SacrificeAll | ValidCards$ Card.IsRemembered | SubAbility$ DBCleanup | StackDescription$ Each opponent sacrifices a creature with the greatest power among creatures they control. SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearChosen$ True A:AB$ Repeat | Cost$ SubCounter<8/LOYALTY> | Planeswalker$ True | Ultimate$ True | RepeatSubAbility$ TrigDiscard | MaxRepeat$ 7 | StackDescription$ SpellDescription | SpellDescription$ Each opponent may discard a card. If they don't, they lose 3 life. Repeat this process six more times. -SVar:TrigDiscard:DB$ Discard | Defined$ Player.Opponent | Mode$ TgtChoose | Optional$ True | RememberDiscardingPlayers$ True | SubAbility$ DBLoseLife +SVar:TrigDiscard:DB$ Discard | Defined$ Player.Opponent | Mode$ TgtChoose | Optional$ True | RememberDiscardingPlayers$ True | AILogic$ UnlessAtLife.6 | SubAbility$ DBLoseLife SVar:DBLoseLife:DB$ LoseLife | Defined$ Opponent.IsNotRemembered | LifeAmount$ 3 | SubAbility$ DBCleanup DeckHints:Type$Instant|Sorcery DeckHas:Ability$Graveyard & Ability$LifeGain