Commit 4f91b984 authored by Michael Kamensky's avatar Michael Kamensky

Merge branch 'ai-improvements' into 'master'

Various AI improvements.

See merge request core-developers/forge!1066
parents 1311415a 985599a3
......@@ -74,6 +74,8 @@ public enum AiProps { /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
AVOID_TARGETING_CREATS_THAT_WILL_DIE ("true"), /** */
DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION ("true"), /** */
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
USE_BERSERK_AGGRESSIVELY ("false"), /** */
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
......
......@@ -1770,7 +1770,68 @@ public class ComputerUtil {
Iterables.addAll(threatened, ComputerUtil.predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility()));
return threatened;
}
/**
* Returns true if the specified creature will die this turn either from lethal damage in combat
* or from a killing spell on stack.
* TODO: This currently does not account for the fact that spells on stack can be countered, can be improved.
*
* @param creature
* A creature to check
* @return true if the creature dies according to current board position.
*/
public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature) {
final Game game = creature.getGame();
// a creature will die as a result of combat
boolean willDieInCombat = game.getPhaseHandler().inCombat()
&& ComputerUtilCombat.combatantWouldBeDestroyed(creature.getController(), creature, game.getCombat());
// a creature will [hopefully] die from a spell on stack
boolean willDieFromSpell = false;
boolean noStackCheck = false;
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
for (SpellAbilityStackInstance si : game.getStack()) {
if (si.getSpellAbility(false).getApi() == ApiType.Counter) {
noStackCheck = true;
break;
}
}
}
willDieFromSpell = !noStackCheck && ComputerUtil.predictThreatenedObjects(creature.getController(), null).contains(creature);
return willDieInCombat || willDieFromSpell;
}
/**
* Returns a list of cards excluding any creatures that will die in active combat or from a spell on stack.
* Works only on AI profiles which have AVOID_TARGETING_CREATS_THAT_WILL_DIE enabled, otherwise returns
* the original list.
*
* @param ai
* The AI player performing this evaluation
* @param list
* The list of cards to work with
* @return a filtered list with no dying creatures in it
*/
public static CardCollection filterCreaturesThatWillDieThisTurn(final Player ai, final CardCollection list) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) {
// Try to avoid targeting creatures that are dead on board
List<Card> willBeKilled = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return card.isCreature() && ComputerUtil.predictCreatureWillDieThisTurn(ai, card);
}
});
list.removeAll(willBeKilled);
}
return list;
}
public static boolean playImmediately(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Zone zone = source.getZone();
......@@ -2779,6 +2840,10 @@ public class ComputerUtil {
if (ab.getApi() == null) {
// only API-based SAs are supported, other things may lead to a NPE (e.g. Ancestral Vision Suspend SA)
continue;
} else if (ab.getApi() == ApiType.Mana && "ManaRitual".equals(ab.getParam("AILogic"))) {
// Mana Ritual cards are too complex for the AI to consider casting through a spell effect and will
// lead to a stack overflow. Consider improving.
continue;
}
SpellAbility abTest = withoutPayingManaCost ? ab.copyWithNoManaCost() : ab.copy();
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
......
......@@ -275,7 +275,7 @@ public class DamageDealAi extends DamageAiBase {
final Game game = source.getGame();
List<Card> hPlay = getTargetableCards(ai, sa, pl, tgt, activator, source, game);
List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
CardCollection killables = CardLists.filter(hPlay, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getSVar("Targeting").equals("Dies")
......@@ -286,7 +286,10 @@ public class DamageDealAi extends DamageAiBase {
});
// Filter AI-specific targets if provided
killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true);
killables = ComputerUtil.filterAITgts(sa, ai, killables, true);
// Try not to target anything which will already be dead by the time the spell resolves
killables = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, killables);
Card targetCard = null;
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
......
......@@ -178,6 +178,8 @@ public class DestroyAi extends SpellAbilityAi {
});
}
// Try to avoid targeting creatures that are dead on board
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list);
if (list.isEmpty()) {
return false;
}
......@@ -313,6 +315,9 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
// Try to avoid targeting creatures that are dead on board
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list);
if (list.isEmpty() || list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
return false;
}
......
......@@ -175,6 +175,13 @@ public class PlayAi extends SpellAbilityAi {
spell = (Spell) spell.copyWithDefinedCost(abCost);
}
if( AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
// Before accepting, see if the spell has a valid number of targets (it should at this point).
// Proceeding past this point if the spell is not correctly targeted will result
// in "Failed to add to stack" error and the card disappearing from the game completely.
if (!spell.isTargetNumberValid()) {
return false;
}
return true;
}
}
......
......@@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135
# If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=true
# Options that allow the AI to attempt to optimize targeting for removal and damaging spells.
# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case
# this creature will die in current combat or to a spell which is currently on stack targeting it.
AVOID_TARGETING_CREATS_THAT_WILL_DIE=true
# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it,
# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not.
DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=true
# Only works when AI cheating is enabled in preferences, otherwise does nothing
CHEAT_WITH_MANA_ON_SHUFFLE=true
......
......@@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135
# If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=true
# Options that allow the AI to attempt to optimize targeting for removal and damaging spells.
# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case
# this creature will die in current combat or to a spell which is currently on stack targeting it.
AVOID_TARGETING_CREATS_THAT_WILL_DIE=true
# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it,
# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not.
DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=true
# Only works when AI cheating is enabled in preferences, otherwise does nothing
CHEAT_WITH_MANA_ON_SHUFFLE=true
......
......@@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135
# If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=false
# Options that allow the AI to attempt to optimize targeting for removal and damaging spells.
# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case
# this creature will die in current combat or to a spell which is currently on stack targeting it.
AVOID_TARGETING_CREATS_THAT_WILL_DIE=true
# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it,
# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not.
DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=false
# Only works when AI cheating is enabled in preferences, otherwise does nothing
CHEAT_WITH_MANA_ON_SHUFFLE=true
......@@ -219,4 +227,4 @@ AI_IN_DANGER_THRESHOLD=3
# for each evaluation, introducing some unpredictability.
AI_IN_DANGER_MAX_THRESHOLD=12
# <-- there are no options here at the moment -->
# <-- there are no other experimental options here at the moment -->
......@@ -66,6 +66,14 @@ THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER=135
# If enabled, the AI will not bother chump blocking to protect a planeswalker unless lethal damage is threatened to it
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL=true
# Options that allow the AI to attempt to optimize targeting for removal and damaging spells.
# If enabled, the AI will try not to target a creature with a damaging spell or spot removal in case
# this creature will die in current combat or to a spell which is currently on stack targeting it.
AVOID_TARGETING_CREATS_THAT_WILL_DIE=true
# If enabled, the AI will not evaluate the stack in case at least one counterspell is present on it,
# since the current AI is not smart enough to predict whether a kill spell on stack is countered or not.
DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION=true
# Only works when AI cheating is enabled in preferences, otherwise does nothing
CHEAT_WITH_MANA_ON_SHUFFLE=true
......
......@@ -9,6 +9,8 @@ SVar:DBPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | Condition
SVar:DBSac:DB$ Sacrifice | SacValid# Self | ConditionDefined$ Remembered | ConditionPresent$ Card.Creature | ConditionCompare$ EQ0 | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
A:AB$ PutCounter | Cost$ B G Discard<1/Creature> | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on CARDNAME.
SVar:NeedsToPlayVar:Z GE1
SVar:Z:Count$ValidGraveyard Creature.YouCtrl
AI:RemoveDeck:Random
DeckNeeds:Ability$Graveyard
DeckHas:Ability$Counters
......
......@@ -6,5 +6,6 @@ SVar:DBChoose2:DB$ ChooseCard | Defined$ You | Choices$ Creature.YouOwn+cmcEQ2 |
SVar:DBChoose3:DB$ ChooseCard | Defined$ You | Choices$ Creature.YouOwn+cmcEQ3 | ChoiceZone$ Graveyard | Amount$ 1 | SubAbility$ DBReturn | RememberChosen$ True | SpellDescription$ Choose a creature card with converted mana cost 3 in your graveyard.
SVar:DBReturn:DB$ ChangeZoneAll | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Card.IsRemembered | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:NeedsToPlay:Creature.YouCtrl+inZoneGraveyard+cmcLE3
SVar:NeedsToPlayVar:Z GE1
SVar:Z:Count$ValidGraveyard Creature.YouCtrl+cmcLE3
Oracle:Choose a creature card with converted mana cost 1 in your graveyard, then do the same for creature cards with converted mana costs 2 and 3. Return those cards to the battlefield.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment