Add more AI for TurboFog cards

This commit is contained in:
friarsol
2023-08-11 20:42:46 -04:00
committed by Chris H
parent efc3c2c159
commit 6a99aee1b1
13 changed files with 196 additions and 57 deletions

View File

@@ -1497,7 +1497,7 @@ public class ComputerUtil {
}
}
all.addAll(ai.getCardsActivableInExternalZones(true));
all.addAll(ai.getCardsActivatableInExternalZones(true));
all.addAll(ai.getCardsIn(ZoneType.Hand));
for (final Card c : all) {
@@ -1538,7 +1538,7 @@ public class ComputerUtil {
public static boolean hasAFogEffect(final Player defender, final Player ai, boolean checkingOther) {
final CardCollection all = new CardCollection(defender.getCardsIn(ZoneType.Battlefield));
all.addAll(defender.getCardsActivableInExternalZones(true));
all.addAll(defender.getCardsActivatableInExternalZones(true));
// TODO check if cards can be viewed instead
if (!checkingOther) {
all.addAll(defender.getCardsIn(ZoneType.Hand));
@@ -1590,7 +1590,7 @@ public class ComputerUtil {
public static int possibleNonCombatDamage(final Player ai, final Player enemy) {
int damage = 0;
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
all.addAll(ai.getCardsActivableInExternalZones(true));
all.addAll(ai.getCardsActivatableInExternalZones(true));
all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(Presets.PERMANENTS)));
for (final Card c : all) {

View File

@@ -1299,6 +1299,8 @@ public class PlayerControllerAi extends PlayerController {
name = ComputerUtilCard.getMostProminentCardName(cards);
} else if (logic.equals("CursedScroll")) {
name = SpecialCardAi.CursedScroll.chooseCard(player, sa);
} else if (logic.equals("PithingNeedle")) {
name = SpecialCardAi.PithingNeedle.chooseCard(player, sa);
}
if (!StringUtils.isBlank(name)) {

View File

@@ -293,6 +293,47 @@ public class SpecialCardAi {
}
}
public static class PithingNeedle {
public static String chooseCard(final Player ai, final SpellAbility sa) {
// TODO Remove names of cards already named by other Pithing Needles
Card best = null;
CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa);
if (!oppPerms.isEmpty()) {
return chooseCardFromList(oppPerms).getName();
}
CardCollection visibleZones = CardLists.getValidCards(ai.getOpponents().getCardsIn(Lists.newArrayList(ZoneType.Graveyard, ZoneType.Exile)), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa);
if (!visibleZones.isEmpty()) {
// If nothing on the battlefield has a nonmana ability choose something
return chooseCardFromList(visibleZones).getName();
}
return chooseNonBattlefieldName();
}
static public Card chooseCardFromList(CardCollection cardlist) {
Card best = ComputerUtilCard.getBestPlaneswalkerAI(cardlist);
if (best != null) {
// No planeswalkers choose something!
return best;
}
best = ComputerUtilCard.getBestCreatureAI(cardlist);
if (best == null) {
// If nothing on the battlefield has a nonmana ability choose something
Collections.shuffle(cardlist);
best = cardlist.getFirst();
}
return best;
}
static public String chooseNonBattlefieldName() {
return "Liliana of the Veil";
}
}
// Deathgorge Scavenger
public static class DeathgorgeScavenger {
public static boolean consider(final Player ai, final SpellAbility sa) {

View File

@@ -1068,7 +1068,7 @@ public class DamageDealAi extends DamageAiBase {
CardCollection cards = new CardCollection();
cards.addAll(ai.getCardsIn(ZoneType.Hand));
cards.addAll(ai.getCardsIn(ZoneType.Battlefield));
cards.addAll(ai.getCardsActivableInExternalZones(true));
cards.addAll(ai.getCardsActivatableInExternalZones(true));
for (Card c : cards) {
if (c.getZone().getPlayer() != null && c.getZone().getPlayer() != ai && c.mayPlay(ai).isEmpty()) {
continue;

View File

@@ -13,7 +13,6 @@ import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
@@ -82,21 +81,11 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true;
}
} else if (logic.equals("Fog")) {
if (phase.isPlayerTurn(sa.getActivatingPlayer())) {
return false;
}
if (!phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false;
}
if (!game.getStack().isEmpty()) {
return false;
}
if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false;
}
if (!ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
FogAi fogAi = new FogAi();
if (!fogAi.canPlayAI(ai, sa)) {
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
@@ -258,6 +247,21 @@ public class EffectAi extends SpellAbilityAi {
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
return false;
}
} else if (logic.equals("PeaceTalks")) {
Player nextPlayer = game.getNextPlayerAfter(ai);
// If opponent doesn't have creatures, preventing attacks don't mean as much
if (nextPlayer.getCreaturesInPlay().isEmpty()) {
return false;
}
// Only cast Peace Talks after you attack just in case you have creatures
if (!phase.is(PhaseType.MAIN2)) {
return false;
}
// Create a pseudo combat and see if my life is in danger
return randomReturn;
} else if (logic.equals("Bribe")) {
Card host = sa.getHostCard();
Combat combat = game.getCombat();

View File

@@ -10,13 +10,15 @@ import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardPredicates;
import forge.game.card.CardLists;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.game.zone.ZoneType;
public class FogAi extends SpellAbilityAi {
@@ -27,12 +29,70 @@ public class FogAi extends SpellAbilityAi {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
final Card hostCard = sa.getHostCard();
final Combat combat = game.getCombat();
// Don't cast it, if the effect is already in place
if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false;
}
// TODO Test if we can even Fog successfully
if (handleMemoryCheck(ai, sa)) {
return true;
}
// Only cast when Stack is empty, so Human uses spells/abilities first
if (!game.getStack().isEmpty()) {
return false;
}
// TODO Only cast outside of combat if I won't be able to cast inside of combat
if (combat == null) {
return false;
}
// AI should only activate this during Opponents Declare Blockers phase
if (!game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai) ||
!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// TODO Be careful of effects that don't let you cast spells during combat
return false;
}
int remainingLife = ComputerUtilCombat.lifeThatWouldRemain(ai, combat);
int dmg = ai.getLife() - remainingLife;
// Count the number of Fog spells in hand
int fogs = countAvailableFogs(ai);
if (fogs > 2 && dmg > 2) {
// Playing a fog deck. If you got them play them.
return true;
}
if (dmg > 2 &&
hostCard.hasKeyword(Keyword.BUYBACK) &&
CardLists.count(ai.getCardsIn(ZoneType.Battlefield), Card::isLand) > 3) {
// Constant mists sacrifices a land to buyback. But if AI is running it, they are probably ok sacrificing some lands
return true;
}
if ("SeriousDamage".equals(sa.getParam("AILogic"))) {
if (dmg > ai.getLife() / 4) {
return true;
} else if (dmg >= 5) {
return true;
} else if (ai.getLife() < ai.getStartingLife() / 3) {
return true;
}
}
// TODO Compare to poison counters?
// Cast it if life is in danger
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
}
private boolean handleMemoryCheck(Player ai, SpellAbility sa) {
Card hostCard = sa.getHostCard();
Game game = ai.getGame();
// if card would be destroyed, react and use immediately if it's not own turn
if ((AiCardMemory.isRememberedCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
&& (!game.getStack().isEmpty())
@@ -54,43 +114,32 @@ public class FogAi extends SpellAbilityAi {
AiCardMemory.rememberCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
}
}
return false;
}
// AI should only activate this during Human's Declare Blockers phase
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return false;
}
if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false;
}
// Only cast when Stack is empty, so Human uses spells/abilities first
if (!game.getStack().isEmpty()) {
return false;
}
if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) {
int dmg = 0;
for (Card atk : game.getCombat().getAttackersOf(ai)) {
if (game.getCombat().isUnblocked(atk)) {
dmg += atk.getNetCombatDamage();
} else if (atk.hasKeyword(Keyword.TRAMPLE)) {
dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness);
private int countAvailableFogs(Player ai) {
int fogs = 0;
for (Card c : ai.getCardsActivatableInExternalZones(false)) {
for (SpellAbility ability : c.getSpellAbilities()) {
if (ability.getApi().equals(ApiType.Fog)) {
fogs++;
break;
}
}
if (dmg > ai.getLife() / 4) {
return true;
} else if (dmg >= 5) {
return true;
} else if (ai.getLife() < ai.getStartingLife() / 3) {
return true;
}
}
// Cast it if life is in danger
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
for (Card c : ai.getCardsIn(ZoneType.Hand)) {
for (SpellAbility ability : c.getSpellAbilities()) {
if (ability.getApi().equals(ApiType.Fog)) {
fogs++;
break;
}
}
}
return fogs;
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI should only activate this during Human's turn

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.card.Card;
@@ -9,6 +10,7 @@ import forge.game.card.CardLists;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
/**
* AbilityFactory for Creature Spells.
@@ -18,7 +20,31 @@ public class PermanentNoncreatureAi extends PermanentAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
return !"Never".equals(aiLogic) && !"DontCast".equals(aiLogic);
if ("Never".equals(aiLogic) || "DontCast".equals(aiLogic)) {
return false;
}
Game game = ai.getGame();
if ("PithingNeedle".equals(aiLogic)) {
// Make sure theres something in play worth Needlings.
// Planeswalker or equipment or something
CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa);
if (oppPerms.isEmpty()) {
return false;
}
Card card = ComputerUtilCard.getBestPlaneswalkerAI(oppPerms);
if (card != null) {
return true;
}
// 5 percent chance to cast per opposing card with a non mana ability
return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size();
}
return true;
}
/**

View File

@@ -374,7 +374,7 @@ public class UntapAi extends SpellAbilityAi {
return false;
}
if (game.getPhaseHandler().getPhase().equals(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Blockers already set. Are there any dangerous unblocked creatures? Sort by creature that will deal the most damage?
Card card = ComputerUtilCombat.mostDangerousAttacker(list, ai, activeCombat, true);

View File

@@ -1091,6 +1091,18 @@ public class CardProperty {
if (!property.startsWith("without") && !card.hasStartOfUnHiddenKeyword(property.substring(4))) {
return false;
}
} else if (property.equals("hasNonmanaAbilities")) {
boolean hasAbilities = false;
for(SpellAbility sa : card.getSpellAbilities()) {
if (sa.isActivatedAbility() && !sa.isManaAbility()) {
hasAbilities = true;
break;
}
}
if (!hasAbilities) {
return false;
}
} else if (property.startsWith("activated")) {
if (!card.activatedThisTurn()) {
return false;

View File

@@ -1336,7 +1336,7 @@ public class Player extends GameEntity implements Comparable<Player> {
return cards;
}
else if (zoneType == ZoneType.Flashback) {
return getCardsActivableInExternalZones(true);
return getCardsActivatableInExternalZones(true);
}
PlayerZone zone = getZone(zoneType);
@@ -1379,7 +1379,7 @@ public class Player extends GameEntity implements Comparable<Player> {
return CardLists.filter(getCardsIn(zone), CardPredicates.nameEquals(cardName));
}
public CardCollectionView getCardsActivableInExternalZones(boolean includeCommandZone) {
public CardCollectionView getCardsActivatableInExternalZones(boolean includeCommandZone) {
final CardCollection cl = new CardCollection();
cl.addAll(getZone(ZoneType.Graveyard).getCardsPlayerCanActivate(this));

View File

@@ -1,7 +1,7 @@
Name:Peace Talks
ManaCost:1 W
Types:Sorcery
A:SP$ Effect | StaticAbilities$ STCantAttack,STCantTarget,STCantTargetPlayer | Duration$ ThisTurnAndNextTurn | SpellDescription$ This turn and next turn, creatures can't attack, and players and permanents can't be the targets of spells or activated abilities.
A:SP$ Effect | AILogic$ PeaceTalks | Stackable$ False | StaticAbilities$ STCantAttack,STCantTarget,STCantTargetPlayer | Duration$ ThisTurnAndNextTurn | SpellDescription$ This turn and next turn, creatures can't attack, and players and permanents can't be the targets of spells or activated abilities.
SVar:STCantAttack:Mode$ CantAttack | EffectZone$ Command | ValidCard$ Creature | Description$ Creatures can't attack.
SVar:STCantTarget:Mode$ CantTarget | ValidCard$ Permanent | EffectZone$ Command | ValidSA$ Spell,Activated | Description$ Permanents can't be the targets of spells or activated abilities.
SVar:STCantTargetPlayer:Mode$ CantTarget | ValidPlayer$ Player | EffectZone$ Command | ValidSA$ Spell,Activated | Description$ Players can't be the targets of spells or activated abilities.

View File

@@ -1,8 +1,9 @@
Name:Pithing Needle
ManaCost:1
Types:Artifact
A:SP$ PermanentNoncreature | AILogic$ PithingNeedle
K:ETBReplacement:Other:DBNameCard
SVar:DBNameCard:DB$ NameCard | Defined$ You | SpellDescription$ As CARDNAME enters the battlefield, choose a card name.
SVar:DBNameCard:DB$ NameCard | Defined$ You | AILogic$ PithingNeedle | SpellDescription$ As CARDNAME enters the battlefield, choose a card name.
S:Mode$ CantBeActivated | ValidCard$ Card.NamedCard | ValidSA$ Activated.nonManaAbility | Description$ Activated abilities of sources with the chosen name can't be activated unless they're mana abilities.
AI:RemoveDeck:Random
Oracle:As Pithing Needle enters the battlefield, choose a card name.\nActivated abilities of sources with the chosen name can't be activated unless they're mana abilities.

View File

@@ -223,7 +223,11 @@ public final class FModel {
magicDb.setBrawlPredicate(formats.get("Brawl").getFilterRules());
magicDb.setFilteredHandsEnabled(preferences.getPrefBoolean(FPref.FILTERED_HANDS));
magicDb.setMulliganRule(MulliganDefs.MulliganRule.valueOf(preferences.getPref(FPref.MULLIGAN_RULE)));
try {
magicDb.setMulliganRule(MulliganDefs.MulliganRule.valueOf(preferences.getPref(FPref.MULLIGAN_RULE)));
} catch(Exception e) {
magicDb.setMulliganRule(MulliganDefs.MulliganRule.London);
}
blocks = new StorageBase<>("Block definitions", new CardBlock.Reader(ForgeConstants.BLOCK_DATA_DIR + "blocks.txt", magicDb.getEditions()));
//setblockLands