- Explicitly marked cards that use a re-entry mechanism for testing playability via canPlaySa such that they don't recursively call each other and thus risk getting into an infinite loop (fixes potential check combinations of e.g. various mana rituals + Yawgmoth's Will in AI's hand and other things like that, which would otherwise cause a stack overflow).

- Some simplification in the AICardMemory interface, allowing to use static methods to simplify calls.
This commit is contained in:
Agetian
2017-01-26 06:40:46 +00:00
parent 402f1ce884
commit 94f53e05e6
21 changed files with 58 additions and 33 deletions

View File

@@ -585,9 +585,8 @@ public class AiAttackController {
// Cards that are remembered to attack anyway (e.g. temporarily stolen creatures)
if (ai.getController() instanceof PlayerControllerAi) {
// Only do this if |ai| is actually an AI - as we could be trying to predict how the human will attack.
AiCardMemory aiMemory = ((PlayerControllerAi) ai.getController()).getAi().getCardMemory();
for (Card attacker : this.attackers) {
if (aiMemory.isRememberedCard(attacker, AiCardMemory.MemorySet.MANDATORY_ATTACKERS)) {
if (AiCardMemory.isRememberedCard(ai, attacker, AiCardMemory.MemorySet.MANDATORY_ATTACKERS)) {
combat.addAttacker(attacker, defender);
attackersLeft.remove(attacker);
}

View File

@@ -44,12 +44,14 @@ public class AiCardMemory {
private final Set<Card> memHeldManaSources;
private final Set<Card> memAttachedThisTurn;
private final Set<Card> memAnimatedThisTurn;
private final Set<Card> memTestedCanPlayThisTurn;
public AiCardMemory() {
this.memMandatoryAttackers = new HashSet<>();
this.memHeldManaSources = new HashSet<>();
this.memAttachedThisTurn = new HashSet<>();
this.memAnimatedThisTurn = new HashSet<>();
this.memTestedCanPlayThisTurn = new HashSet<>();
}
/**
@@ -62,6 +64,7 @@ public class AiCardMemory {
HELD_MANA_SOURCES,
ATTACHED_THIS_TURN,
ANIMATED_THIS_TURN,
TESTED_CANPLAY_THIS_PHASE,
//REVEALED_CARDS // stub, not linked to AI code yet
}
@@ -75,6 +78,8 @@ public class AiCardMemory {
return memAttachedThisTurn;
case ANIMATED_THIS_TURN:
return memAnimatedThisTurn;
case TESTED_CANPLAY_THIS_PHASE:
return memTestedCanPlayThisTurn;
//case REVEALED_CARDS:
// return memRevealedCards;
default:
@@ -247,5 +252,23 @@ public class AiCardMemory {
clearMemorySet(MemorySet.HELD_MANA_SOURCES);
clearMemorySet(MemorySet.ATTACHED_THIS_TURN);
clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
clearMemorySet(MemorySet.TESTED_CANPLAY_THIS_PHASE);
}
// Static functions to simplify access to AI card memory of a given AI player.
public static void rememberCard(Player ai, Card c, MemorySet set) {
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().rememberCard(c, set);
}
public static void forgetCard(Player ai, Card c, MemorySet set) {
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().forgetCard(c, set);
}
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set);
}
public static void clearMemorySet(Player ai, MemorySet set) {
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
}
public static boolean isMemorySetEmpty(Player ai, MemorySet set) {
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isMemorySetEmpty(set);
}
}

View File

@@ -607,7 +607,7 @@ public class AiController {
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
for (Card c : manaSources) {
((PlayerControllerAi)player.getController()).getAi().getCardMemory().rememberCard(c, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
AiCardMemory.rememberCard(player, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
}
}

View File

@@ -800,10 +800,10 @@ public class ComputerUtilMana {
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
if (curPhase == PhaseType.MAIN2 || curPhase == PhaseType.CLEANUP) {
aic.getCardMemory().clearMemorySet(AiCardMemory.MemorySet.HELD_MANA_SOURCES);
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
}
else {
if (aic.getCardMemory().isRememberedCard(sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
// This mana source is held elsewhere for a Main Phase 2 spell.
return true;
}

View File

@@ -359,8 +359,8 @@ public class SpecialCardAi {
continue;
}
if (testSa.getHostCard().getName().equals(sa.getHostCard().getName())) {
// prevent infinitely recursing own ability when testing AI play decision
if (testSa.getHostCard().getName().equals(sa.getHostCard().getName()) || testSa.hasParam("AIRecursiveTestAbility")) {
// prevent infinitely recursing abilities that are susceptible to reentry
continue;
}
@@ -544,8 +544,9 @@ public class SpecialCardAi {
// cut short considering to play counterspells via Yawgmoth's Will
continue;
}
if (ab.getHostCard().getName().equals(sa.getHostCard().getName())) {
// prevent infinitely recursing own ability when testing AI play decision
if (ab.getHostCard().getName().equals(sa.getHostCard().getName()) || ab.hasParam("AIRecursiveTestAbility")) {
// prevent infinitely recursing abilities that are susceptible to reentry
continue;
}

View File

@@ -610,12 +610,10 @@ public class AnimateAi extends SpellAbilityAi {
}
private void rememberAnimatedThisTurn(Player ai, Card c) {
AiCardMemory mem = ((PlayerControllerAi)ai.getController()).getAi().getCardMemory();
mem.rememberCard(c, AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
}
public static boolean isAnimatedThisTurn(Player ai, Card c) {
AiCardMemory mem = ((PlayerControllerAi)ai.getController()).getAi().getCardMemory();
return mem.isRememberedCard(c, AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
}
}

View File

@@ -1095,7 +1095,7 @@ public class AttachAi extends SpellAbilityAi {
// make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around
boolean decideMoveFromUseless = uselessCreature && aic.getBooleanProperty(AiProps.PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS);
if (!decideMoveFromUseless && aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
if (!decideMoveFromUseless && AiCardMemory.isMemorySetEmpty(aiPlayer, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Attach);
if (futureSpell != null && futureSpell.getHostCard() != null) {
aic.reserveManaSourcesForMain2(futureSpell);
@@ -1103,12 +1103,12 @@ public class AttachAi extends SpellAbilityAi {
}
// avoid randomly moving the equipment back and forth between several creatures in one turn
if (aic.getCardMemory().isRememberedCard(sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN)) {
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN)) {
return null;
}
}
aic.getCardMemory().rememberCard(sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN);
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN);
if (c == null && mandatory) {
CardLists.shuffle(list);

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import com.google.common.base.Predicates;
import forge.ai.AiCardMemory;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
@@ -10,6 +11,7 @@ import forge.ai.SpellAbilityAi;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -63,7 +65,7 @@ public class ManaEffectAi extends SpellAbilityAi {
*/
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if ("ManaRitual".equals(logic)) {
if (logic.startsWith("ManaRitual")) {
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
@@ -110,7 +112,9 @@ public class ManaEffectAi extends SpellAbilityAi {
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true);
int numManaSrcs = manaSources.size();
int manaReceived = AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa);
int manaReceived = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1;
manaReceived *= sa.getParam("Produced").split(" ").length;
int selfCost = sa.getPayCosts().getCostMana() != null ? sa.getPayCosts().getCostMana().getMana().getCMC() : 0;
byte producedColor = MagicColor.fromName(sa.getParam("Produced"));
int searchCMC = numManaSrcs - selfCost + manaReceived;
@@ -139,8 +143,8 @@ public class ManaEffectAi extends SpellAbilityAi {
continue;
}
if (testSa.getHostCard().getName().equals(host.getName())) {
// prevent infinitely recursing own ability when testing AI play decision
if (testSa.getHostCard().equals(host) || testSa.hasParam("AIRecursiveTestAbility")) {
// prevent infinitely recursing mana ritual and other abilities with reentry
continue;
}