Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions forge-ai/src/main/java/forge/ai/AiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.LearnAi;
import forge.ai.simulation.GameStateEvaluator;
import forge.ai.simulation.OnePlaySafetyChecker;
import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName;
import forge.card.CardType;
Expand Down Expand Up @@ -95,6 +96,7 @@ public class AiController {
private Combat predictedCombatNextTurn;
private boolean useSimulation;
private SpellAbilityPicker simPicker;
private OnePlaySafetyChecker safetyChecker;
private int lastAttackAggression;
private boolean useLivingEnd;
private List<SpellAbility> skipped;
Expand All @@ -105,6 +107,7 @@ public AiController(final Player computerPlayer, final Game game0) {
game = game0;
memory = new AiCardMemory();
simPicker = new SpellAbilityPicker(game, player);
safetyChecker = new OnePlaySafetyChecker(player);
}

public boolean usesSimulation() {
Expand All @@ -131,6 +134,14 @@ public Player getPlayer() {
return player;
}

public int getSafetyThreatBonus(Card card) {
return safetyChecker.getThreatAssessmentBonus(card);
}

public AiPlayDecision checkOnePlaySafety(SpellAbility sa) {
return safetyChecker.check(sa);
}

public AiCardMemory getCardMemory() {
return memory;
}
Expand Down Expand Up @@ -879,6 +890,13 @@ private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
return AiPlayDecision.CantAfford;
}

if (!useSimulation && !OnePlaySafetyChecker.isChecking()) {
AiPlayDecision safety = safetyChecker.checkDuringSpellSelection(sa);
if (safety != AiPlayDecision.WillPlay) {
return safety;
}
}

return AiPlayDecision.WillPlay;
}

Expand Down Expand Up @@ -1274,6 +1292,13 @@ public boolean getBoolProperty(AiProps propName) {
}

public AiPlayDecision canPlayFromEffectAI(Spell spell, boolean mandatory, boolean withoutPayingManaCost) {
if (!mandatory && !OnePlaySafetyChecker.isChecking()) {
AiPlayDecision safety = safetyChecker.checkStatic(spell);
if (safety != AiPlayDecision.WillPlay) {
return safety;
}
}

if (spell instanceof SpellApiBased) {
boolean chance;
if (withoutPayingManaCost) {
Expand Down
33 changes: 33 additions & 0 deletions forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ public static Card getMostExpensivePermanentAI(final CardCollectionView list, fi
if (targeted) {
all = CardLists.filter(all, c -> c.canBeTargetedBy(spell));
}
Card safetyThreat = getKnownSafetyThreatToRemove(spell == null ? null : spell.getActivatingPlayer(), all);
if (safetyThreat != null) {
return safetyThreat;
}
return getMostExpensivePermanentAI(all);
}

Expand Down Expand Up @@ -1221,6 +1225,11 @@ public static boolean useRemovalNow(final SpellAbility sa, final Card c, final i
return true;
}

final int safetyThreatBonus = getSafetyThreatBonus(ai, c);
if (safetyThreatBonus >= 150) {
return true;
}

//Check for cards that profit from spells - for example Prowess or Threshold
if (phaseType == PhaseType.MAIN1 && ComputerUtil.castSpellInMain1(ai, sa)) {
return true;
Expand Down Expand Up @@ -1413,6 +1422,7 @@ public static boolean useRemovalNow(final SpellAbility sa, final Card c, final i
}
//TODO:add threat from triggers and other abilities (ie. Bident of Thassa)
}
threat += safetyThreatBonus / 100.0f;
if (!c.getManaAbilities().isEmpty()) {
threat += 0.5f * costTarget / opp.getLandsInPlay().size(); //set back opponent's mana
}
Expand All @@ -1425,6 +1435,29 @@ public static boolean useRemovalNow(final SpellAbility sa, final Card c, final i
return chance < valueNow;
}

public static Card getKnownSafetyThreatToRemove(final Player ai, final Iterable<Card> cards) {
if (ai == null) {
return null;
}
Card best = null;
int bestBonus = 0;
for (Card card : cards) {
int bonus = getSafetyThreatBonus(ai, card);
if (bonus > bestBonus) {
best = card;
bestBonus = bonus;
}
}
return best;
}

private static int getSafetyThreatBonus(final Player ai, final Card card) {
if (!(ai.getController() instanceof PlayerControllerAi)) {
return 0;
}
return ((PlayerControllerAi) ai.getController()).getAi().getSafetyThreatBonus(card);
}

/**
* Decides if the "pump" is worthwhile
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {

if ("LivingDeath".equals(aiLogic)) {
return SpecialCardAi.LivingDeath.consider(ai, sa);
} else if ("Timetwister".equals(aiLogic)) {
} else if ("Timetwister".equalsIgnoreCase(aiLogic)) {
Comment thread
Madwand99 marked this conversation as resolved.
return SpecialCardAi.Timetwister.consider(ai, sa);
} else if ("RetDiscardedThisTurn".equals(aiLogic)) {
boolean result = !ai.getDiscardedThisTurn().isEmpty() && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
Expand Down
8 changes: 4 additions & 4 deletions forge-ai/src/main/java/forge/ai/ability/DestroyAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,17 @@ protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa
}
}

Card choice = null;
Card choice = ComputerUtilCard.getKnownSafetyThreatToRemove(ai, list);
// If the targets are only of one type, take the best
if (CardLists.getNotType(list, "Creature").isEmpty()) {
if (choice == null && CardLists.getNotType(list, "Creature").isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(list);
if ("OppDestroyYours".equals(logic)) {
Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay());
if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else if (CardLists.getNotType(list, "Land").isEmpty()) {
} else if (choice == null && CardLists.getNotType(list, "Land").isEmpty()) {
choice = ComputerUtilCard.getBestLandToRemoveAI(ai, list, sa);

if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) {
Expand All @@ -235,7 +235,7 @@ protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
} else if (choice == null) {
// TODO look for "exiled until leaves" of own stuff
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
}
Expand Down
25 changes: 21 additions & 4 deletions forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
package forge.ai.ability;

import forge.ai.*;
import java.util.function.Predicate;

import forge.ai.AiAbilityDecision;
import forge.ai.AiBlockController;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.simulation.SwingyPlaySimulationEvaluator;
import forge.card.MagicColor;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostDamage;
Expand All @@ -13,8 +27,6 @@
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;

import java.util.function.Predicate;

public class DestroyAllAi extends SpellAbilityAi {

private static final Predicate<Card> predicate = c -> !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getCounters(CounterEnumType.SHIELD) > 0 || c.hasSVar("SacMe"));
Expand Down Expand Up @@ -66,6 +78,11 @@ public static AiAbilityDecision doMassRemovalLogic(Player ai, SpellAbility sa) {
ComputerUtilCost.setMaxXValue(sa, ai, sa.isTrigger());
}

AiAbilityDecision simulatedDecision = SwingyPlaySimulationEvaluator.judge(ai, sa);
if (simulatedDecision != null) {
return simulatedDecision;
}

// 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, source.getController(), source, sa);
Expand Down
2 changes: 1 addition & 1 deletion forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) {
// TODO update thisTurnCast

if (advanceToPhase != null) {
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase, () -> GameSimulator.resolveStack(newGame, aiPlayer.getWeakestOpponent()));
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase, () -> GameSimulator.resolveStack(newGame, aiPlayer));
}

return newGame;
Expand Down
33 changes: 23 additions & 10 deletions forge-ai/src/main/java/forge/ai/simulation/GameSimulator.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public GameSimulator(SimulationController controller, Game origGame, Player orig
debugLines = origLines;
Game copyOrigGame = copier.makeCopy();
Player copyOrigAiPlayer = copyOrigGame.getPlayers().get(1);
resolveStack(copyOrigGame, copyOrigGame.getPlayers().get(0));
resolveStack(copyOrigGame, copyOrigAiPlayer);
origScore = eval.getScoreForGameState(copyOrigGame, copyOrigAiPlayer);
}

Expand Down Expand Up @@ -225,9 +225,7 @@ public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval,
}

if (resolve) {
// TODO: Support multiple opponents.
Player opponent = aiPlayer.getWeakestOpponent();
resolveStack(simGame, opponent);
resolveStack(simGame, aiPlayer);
}

// TODO: If this is during combat, before blockers are declared,
Expand Down Expand Up @@ -260,11 +258,9 @@ public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval,
return score;
}

public static void resolveStack(final Game game, final Player opponent) {
// TODO: This needs to set an AI controller for all opponents, in case of multiplayer.
PlayerControllerAi sim = new PlayerControllerAi(game, opponent, opponent.getLobbyPlayer());
sim.setUseSimulation(true);
opponent.runWithController(() -> {
public static void resolveStack(final Game game, final Player preservedPlayer) {
List<Player> players = new ArrayList<>(game.getPlayers());
runWithSimulationControllers(game, preservedPlayer, players, 0, () -> {
final Set<Card> allAffectedCards = new HashSet<>();
game.getAction().checkStateEffects(false, allAffectedCards);
game.getStack().addAllTriggeredAbilitiesToStack();
Expand All @@ -288,7 +284,24 @@ public static void resolveStack(final Game game, final Player opponent) {

// Continue until stack is empty.
}
}, sim);
});
}

private static void runWithSimulationControllers(final Game game, final Player preservedPlayer,
final List<Player> players, final int index, final Runnable proc) {
if (index >= players.size()) {
proc.run();
return;
}

Player player = players.get(index);
if (player.equals(preservedPlayer)) {
runWithSimulationControllers(game, preservedPlayer, players, index + 1, proc);
return;
}
PlayerControllerAi sim = new PlayerControllerAi(game, player, player.getLobbyPlayer());
sim.setUseSimulation(true);
player.runWithController(() -> runWithSimulationControllers(game, preservedPlayer, players, index + 1, proc), sim);
}

public Game getSimulatedGameState() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame, fina
gameCopy = copier.makeCopy(null, aiPlayer);
}

gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE, () -> GameSimulator.resolveStack(gameCopy, aiPlayer.getWeakestOpponent()));
gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE, () -> GameSimulator.resolveStack(gameCopy, aiPlayer));
CombatSimResult result = new CombatSimResult();
result.copier = copier;
result.gameCopy = gameCopy;
Expand Down
Loading