Regenerate rework (#3385)

* CardFactoryUtil: remove false keyword

* CardFactoryUtil: remove wrong regenerate Keyword

* CantRegenerate as Static

* ~small fixes

* RegenerationAbility as SubAbility to the Regeneration Effect

* Card: use shieldCount instead of Collection

* remove deprecated trigger

* fix AiLogic vs AILogic

* EffectAi: start of logic for CantRegenerate

* EffectAi: try to do StackPeek for CantRegenerate

* ~ use wither damage

* AI prediction against Damage

* CantRegenerate: begin logic against Combat Damage

* AnimateAi: start logic for Bone Shaman

* fix Runesword
This commit is contained in:
Hans Mackowiak
2023-07-29 09:58:14 +02:00
committed by GitHub
parent 7bf4b5c126
commit c15542f949
74 changed files with 658 additions and 496 deletions

View File

@@ -2141,7 +2141,7 @@ public class AiController {
}
private <T extends CardTraitBase> List<T> filterListByAiLogic(List<T> list, final String logic) {
return filterList(list, CardTraitPredicates.hasParam("AiLogic", logic));
return filterList(list, CardTraitPredicates.hasParam("AILogic", logic));
}
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {

View File

@@ -3007,11 +3007,11 @@ public class ComputerUtil {
repParams,
ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
return false;
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LoseLife"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
return false;
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LichDraw"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
return false;
}
return true;
@@ -3036,13 +3036,13 @@ public class ComputerUtil {
ReplacementLayer.Other
);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
// no life gain is not negative
return false;
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LoseLife"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
// lose life is only negative is the player can lose life
return player.canLoseLife();
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LichDraw"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
// if it would draw more cards than player has, then its negative
return player.getCardsIn(ZoneType.Library).size() <= n;
}

View File

@@ -1,10 +1,12 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
import forge.card.CardType;
import forge.card.ColorSet;
import forge.game.CardTraitPredicates;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -20,6 +22,7 @@ import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import java.util.Arrays;
import java.util.List;
@@ -54,6 +57,23 @@ public class AnimateAi extends SpellAbilityAi {
if ("EOT".equals(aiLogic) && ph.getPhase().isBefore(PhaseType.MAIN2)) {
return false;
}
if ("BoneManCantRegenerate".equals(aiLogic)) {
Card host = sa.getHostCard();
String svar = AbilityUtils.getSVar(sa, sa.getParam("staticAbilities"));
if (svar == null) {
return false;
}
Map<String, String> map = FileSection.parseToMap(svar, FileSection.DOLLAR_SIGN_KV_SEPARATOR);
if (!map.containsKey("Description")) {
return false;
}
// check for duplicate static ability
if (Iterables.any(host.getStaticAbilities(), CardTraitPredicates.hasParam("Description", map.get("Description")))) {
return false;
}
// TODO check if Bone Man would deal damage to something that otherwise would regenerate
}
return super.checkAiLogic(ai, sa, aiLogic);
}

View File

@@ -475,8 +475,8 @@ public class CountersMoveAi extends SpellAbilityAi {
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
Player targetedPlayer, Map<String, Object> params) {
if (sa.hasParam("AiLogic")) {
String logic = sa.getParam("AiLogic");
if (sa.hasParam("AILogic")) {
String logic = sa.getParam("AILogic");
if ("ToValid".equals(logic)) {
// cards like Forgotten Ancient

View File

@@ -1,6 +1,9 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.Nullable;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
@@ -15,7 +18,10 @@ import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.game.CardTraitPredicates;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -28,11 +34,17 @@ import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
public class EffectAi extends SpellAbilityAi {
@Override
@@ -256,6 +268,59 @@ public class EffectAi extends SpellAbilityAi {
return true;
}
return false;
} else if (logic.equals("CantRegenerate")) {
if (sa.usesTargeting()) {
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.filter(list, CardPredicates.Presets.CAN_BE_DESTROYED, new Predicate<Card>() {
@Override
public boolean apply(@Nullable Card input) {
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(input);
runParams.put(AbilityKey.Regeneration, true);
List<ReplacementEffect> repDestoryList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
if (repDestoryList.isEmpty() || Iterables.any(repDestoryList, Predicates.not(CardTraitPredicates.hasParam("Regeneration")))) {
return false;
}
if (cantRegenerateCheckCombat(input) || cantRegenerateCheckStack(input)) {
return true;
}
return false;
}
});
if (list.isEmpty()) {
return false;
}
// TODO check Stack for Effects that would destroy the selected card?
sa.getTargets().add(ComputerUtilCard.getBestAI(list));
return true;
} else if (sa.getParent() != null) {
// sub ability should be okay
return true;
} else if ("Self".equals(sa.getParam("RememberObjects"))) {
// the ones affecting itself are Nimbus cards, were opponent can activate this effect
Card host = sa.getHostCard();
if (!host.canBeDestroyed()) {
return false;
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
runParams.put(AbilityKey.Regeneration, true);
List<ReplacementEffect> repDestoryList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
if (repDestoryList.isEmpty() || Iterables.any(repDestoryList, Predicates.not(CardTraitPredicates.hasParam("Regeneration")))) {
return false;
}
if (cantRegenerateCheckCombat(host) || cantRegenerateCheckStack(host)) {
return true;
}
return false;
}
}
} else { //no AILogic
return false;
@@ -319,4 +384,193 @@ public class EffectAi extends SpellAbilityAi {
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}
protected boolean cantRegenerateCheckCombat(Card host) {
final Game game = host.getGame();
if (!game.getPhaseHandler().inCombat()) {
return false;
}
if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DAMAGE)) {
return false;
}
Combat combat = game.getCombat();
if (game.getPhaseHandler().isPlayerTurn(host.getController())) {
// attacking player
if (!combat.isAttacking(host)) {
return false;
}
// TODO predict lethal combat damage
return combat.isBlocked(host);
} else {
// TODO predict lethal combat damage
return combat.isBlocking(host);
}
}
protected boolean cantRegenerateCheckStack(Card host) {
final Game game = host.getGame();
// do this only in reaction to a threatening spell on directly on the stack
MagicStack stack = game.getStack();
if (stack.isEmpty()) {
return false;
}
// TODO check Stack for Effects that would destroy host, either direct or indirect
SpellAbility stackSa = stack.peekAbility();
if (stackSa == null) {
return false;
}
// regenerate is a replace destroy, meaning either destroyed by effect
// or destroyed by state based action, when dying by lethal damage
SpellAbility subAbility = stackSa;
while (subAbility != null) {
ApiType apiType = subAbility.getApi();
if (apiType == null) {
continue;
}
if (ApiType.DestroyAll == apiType) {
// or skip to sub abilities?
if (subAbility.hasParam("NoRegen")) {
return false;
}
if (subAbility.usesTargeting() && !Iterables.contains(subAbility.getTargets().getTargetPlayers(), host.getController())) {
return false;
}
String valid = subAbility.getParamOrDefault("ValidCards", "");
// Ugh. If calculateAmount needs to be called with DestroyAll it _needs_
// to use the X variable
// We really need a better solution to this
if (valid.contains("X")) {
valid = TextUtil.fastReplace(valid,
"X", Integer.toString(AbilityUtils.calculateAmount(subAbility.getHostCard(), "X", subAbility)));
}
// host card is valid
if (host.isValid(valid.split(","), subAbility.getActivatingPlayer(), subAbility.getHostCard(), subAbility)) {
return true;
}
// failed to check via valid, need to pass through the filterList method
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
if (subAbility.usesTargeting()) {
list = CardLists.filterControlledBy(list, new PlayerCollection(subAbility.getTargets().getTargetPlayers()));
}
list = AbilityUtils.filterListByType(list, valid, subAbility);
if (list.contains(host)) {
return true;
}
// check for defined
} else if (ApiType.Destroy == apiType) {
if (subAbility.hasParam("NoRegen")) {
return false;
}
if (subAbility.hasParam("Sacrifice")) {
return false;
}
// simulate getTargetCards
if (subAbility.usesTargeting()) {
// isTargeting checks parents, i think that might be wrong
if (subAbility.getTargets().contains(host)) {
return true;
}
} else {
if (AbilityUtils.getDefinedObjects(subAbility.getHostCard(), subAbility.getParam("Defined"), subAbility).contains(host)) {
return true;
}
}
if (CardUtil.getRadiance(subAbility).contains(host)) {
return true;
}
// check for target or indirect target
} else if (ApiType.DamageAll == apiType) {
if (!subAbility.hasParam("ValidCards")) {
continue;
}
String valid = subAbility.getParamOrDefault("ValidCards", "");
if (valid.isEmpty()) {
continue;
}
Card source = game.getChangeZoneLKIInfo(subAbility.getHostCard());
if (source.isWitherDamage()) {
return false;
}
// host card is valid
if (host.isValid(valid.split(","), subAbility.getActivatingPlayer(), subAbility.getHostCard(), subAbility)) {
// TODO check if damage would be lethal
return true;
}
// failed to check via valid, need to pass through the filterList method
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
if (subAbility.usesTargeting()) {
list = CardLists.filterControlledBy(list, new PlayerCollection(subAbility.getTargets().getTargetPlayers()));
}
list = AbilityUtils.filterListByType(list, valid, subAbility);
if (list.contains(host)) {
// TODO check if damage would be lethal
return true;
}
} else if (ApiType.DealDamage == apiType) {
// skip choices
if (subAbility.hasParam("CardChoices") || subAbility.hasParam("PlayerChoices")) {
continue;
}
final List<Card> definedSources = AbilityUtils.getDefinedCards(subAbility.getHostCard(), subAbility.getParam("DamageSource"), subAbility);
if (definedSources == null || definedSources.isEmpty()) {
continue;
}
boolean targeting = false;
// simulate getTargetCards
if (subAbility.usesTargeting()) {
// isTargeting checks parents, i think that might be wrong
if (subAbility.getTargets().contains(host)) {
targeting = true;
}
} else {
if (AbilityUtils.getDefinedObjects(subAbility.getHostCard(), subAbility.getParam("Defined"), subAbility).contains(host)) {
targeting = true;
}
}
for (Card source : definedSources) {
final Card sourceLKI = game.getChangeZoneLKIInfo(source);
if (sourceLKI.isWitherDamage()) {
return false;
}
if (subAbility.hasParam("RelativeTarget")) {
targeting = false;
if (AbilityUtils.getDefinedEntities(subAbility.getHostCard(), subAbility.getParam("Defined"), subAbility).contains(host)) {
targeting = true;
}
}
// TODO predict damage
if (targeting) {
return true;
}
}
if (CardUtil.getRadiance(subAbility).contains(host)) {
return true;
}
}
subAbility = subAbility.getSubAbility();
}
return false;
}
}

View File

@@ -166,12 +166,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
return ph.isPlayerTurn(ai) || (combat != null && combat.isAttacking(card) && card.getNetCombatDamage() > 0);
} else if (keyword.endsWith("CARDNAME can't be regenerated.")) {
if (card.getShieldCount() > 0) {
return true;
}
return card.hasKeyword("If CARDNAME would be destroyed, regenerate it.") && combat != null
&& (combat.isBlocked(card) || combat.isBlocking(card));
} else return !keyword.endsWith("CARDNAME's activated abilities can't be activated."); //too complex
}