mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 12:48:00 +00:00
Battle AI support + improve Battle mechanics support (#3107)
* - First draft of (very sketchy) Battle AI code. * - Imports fix. * - Slightly cleaner refreshCombatants. * - Update Combat to allow the protecting player to participate in declaring blocks to defend a battle. * - Update AiBlockController in order to allow the AI to participate in defending battles it's protecting. * Clean up * - Minor cleanup. * Fix checking backside * Add TODO * Fix missing combat removal * - Suggested minor cleanup. * - Fix imports. * - Improve support for battles in getAllPossibleDefenders. * - AI: prefer own Battles before choosing allied Battles. * Fix ClassCastException --------- Co-authored-by: TRT <>
This commit is contained in:
@@ -119,7 +119,11 @@ public class AiAttackController {
|
|||||||
} // overloaded constructor to evaluate single specified attacker
|
} // overloaded constructor to evaluate single specified attacker
|
||||||
|
|
||||||
private void refreshCombatants(GameEntity defender) {
|
private void refreshCombatants(GameEntity defender) {
|
||||||
this.oppList = getOpponentCreatures(defendingOpponent);
|
if (defender instanceof Card && ((Card) defender).isBattle()) {
|
||||||
|
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
|
||||||
|
} else {
|
||||||
|
this.oppList = getOpponentCreatures(defendingOpponent);
|
||||||
|
}
|
||||||
this.attackers = new ArrayList<>();
|
this.attackers = new ArrayList<>();
|
||||||
for (Card c : myList) {
|
for (Card c : myList) {
|
||||||
if (canAttackWrapper(c, defender)) {
|
if (canAttackWrapper(c, defender)) {
|
||||||
@@ -722,9 +726,14 @@ public class AiAttackController {
|
|||||||
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
|
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Card> battleDefending = c.getDefendingBattles();
|
// Get the preferred battle (prefer own battles, then ally battles)
|
||||||
if (!battleDefending.isEmpty()) {
|
final CardCollection defBattles = c.getDefendingBattles();
|
||||||
// TODO filter for team ones
|
List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
|
||||||
|
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
|
||||||
|
List<Card> prefBattleList = ownBattleDefending.isEmpty() ? allyBattleDefending : ownBattleDefending;
|
||||||
|
if (!prefBattleList.isEmpty()) {
|
||||||
|
// TODO try to be less predictable here, should really check if something would make the back uncastable
|
||||||
|
return Collections.min(prefBattleList, CardPredicates.compareByCounterType(CounterEnumType.DEFENSE));
|
||||||
}
|
}
|
||||||
|
|
||||||
return prefDefender;
|
return prefDefender;
|
||||||
@@ -756,7 +765,17 @@ public class AiAttackController {
|
|||||||
// decided to attack another defender so related lists need to be updated
|
// decided to attack another defender so related lists need to be updated
|
||||||
// (though usually rather try to avoid this situation for performance reasons)
|
// (though usually rather try to avoid this situation for performance reasons)
|
||||||
if (defender != defendingOpponent) {
|
if (defender != defendingOpponent) {
|
||||||
defendingOpponent = defender instanceof Player ? (Player) defender : ((Card)defender).getController();
|
if (defender instanceof Player) {
|
||||||
|
defendingOpponent = (Player) defender;
|
||||||
|
} else if (defender instanceof Card) {
|
||||||
|
Card defCard = (Card) defender;
|
||||||
|
if (defCard.isBattle()) {
|
||||||
|
defendingOpponent = defCard.getProtectingPlayer();
|
||||||
|
} else {
|
||||||
|
// TODO: assume Planeswalker for now, may need to be updated later if more unique mechanics appear like Battle
|
||||||
|
defendingOpponent = defCard.getController();
|
||||||
|
}
|
||||||
|
}
|
||||||
refreshCombatants(defender);
|
refreshCombatants(defender);
|
||||||
}
|
}
|
||||||
if (this.attackers.isEmpty()) {
|
if (this.attackers.isEmpty()) {
|
||||||
|
|||||||
@@ -165,10 +165,12 @@ public class AiBlockController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO Add creatures attacking Planeswalkers in order of which we want to protect
|
// TODO Add creatures attacking Planeswalkers in order of which we want to protect
|
||||||
// defend planeswalkers with more loyalty before planeswalkers with less loyalty
|
// defend planeswalkers with more loyalty before planeswalkers with less loyalty,
|
||||||
// if planeswalker will be too difficult to defend don't even bother
|
// defend battles with fewer defense counters before battles with more defense counters,
|
||||||
|
// if planeswalker/battle will be too difficult to defend don't even bother
|
||||||
for (GameEntity defender : defenders) {
|
for (GameEntity defender : defenders) {
|
||||||
if (defender instanceof Card && ((Card) defender).getController().equals(ai)) {
|
if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|
||||||
|
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
|
||||||
final CardCollection attackers = combat.getAttackersOf(defender);
|
final CardCollection attackers = combat.getAttackersOf(defender);
|
||||||
// Begin with the attackers that pose the biggest threat
|
// Begin with the attackers that pose the biggest threat
|
||||||
CardLists.sortByPowerDesc(attackers);
|
CardLists.sortByPowerDesc(attackers);
|
||||||
|
|||||||
@@ -139,17 +139,25 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options,
|
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options,
|
||||||
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||||
|
final CardStateName state;
|
||||||
|
if (sa.hasParam("CastTransformed")) {
|
||||||
|
state = CardStateName.Transformed;
|
||||||
|
options.forEach(c -> c.changeToState(CardStateName.Transformed));
|
||||||
|
} else {
|
||||||
|
state = CardStateName.Original;
|
||||||
|
}
|
||||||
|
|
||||||
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
// TODO needs to be aligned for MDFC along with getAbilityToPlay so the knowledge
|
// TODO needs to be aligned for MDFC along with getAbilityToPlay so the knowledge
|
||||||
// of which spell was the reason for the choice can be used there
|
// of which spell was the reason for the choice can be used there
|
||||||
for (SpellAbility s : c.getBasicSpells(c.getState(CardStateName.Original))) {
|
for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai, state)) {
|
||||||
|
if (!(s instanceof Spell)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Spell spell = (Spell) s;
|
Spell spell = (Spell) s;
|
||||||
s.setActivatingPlayer(ai, true);
|
s.setActivatingPlayer(ai, true);
|
||||||
// timing restrictions still apply
|
|
||||||
if (!s.getRestrictions().checkTimingRestrictions(c, s))
|
|
||||||
continue;
|
|
||||||
if (params != null && params.containsKey("CMCLimit")) {
|
if (params != null && params.containsKey("CMCLimit")) {
|
||||||
Integer cmcLimit = (Integer) params.get("CMCLimit");
|
Integer cmcLimit = (Integer) params.get("CMCLimit");
|
||||||
if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit)
|
if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit)
|
||||||
@@ -188,6 +196,11 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (sa.hasParam("CastTransformed")) {
|
||||||
|
options.forEach(c -> c.changeToState(CardStateName.Original));
|
||||||
|
}
|
||||||
|
|
||||||
final Card best = ComputerUtilCard.getBestAI(tgtCards);
|
final Card best = ComputerUtilCard.getBestAI(tgtCards);
|
||||||
if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
|
if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
|
||||||
sa.getTargets().add(best);
|
sa.getTargets().add(best);
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (zoneFrom != null) {
|
if (zoneFrom != null) {
|
||||||
if (fromBattlefield && c.isCreature() && game.getCombat() != null) {
|
if (fromBattlefield && game.getCombat() != null) {
|
||||||
if (!toBattlefield) {
|
if (!toBattlefield) {
|
||||||
game.getCombat().saveLKI(lastKnownInfo);
|
game.getCombat().saveLKI(lastKnownInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,50 +17,28 @@
|
|||||||
*/
|
*/
|
||||||
package forge.game.combat;
|
package forge.game.combat;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
|
|
||||||
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
|
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.*;
|
||||||
import com.google.common.collect.Iterables;
|
import forge.game.*;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import com.google.common.collect.Multimap;
|
|
||||||
import com.google.common.collect.Multimaps;
|
|
||||||
import com.google.common.collect.Table;
|
|
||||||
|
|
||||||
import forge.game.Game;
|
|
||||||
import forge.game.GameEntity;
|
|
||||||
import forge.game.GameEntityCounterTable;
|
|
||||||
import forge.game.GameLogEntryType;
|
|
||||||
import forge.game.GameObjectMap;
|
|
||||||
import forge.game.ability.AbilityKey;
|
import forge.game.ability.AbilityKey;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardCollectionView;
|
|
||||||
import forge.game.card.CardDamageMap;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.card.CardUtil;
|
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.replacement.ReplacementType;
|
import forge.game.replacement.ReplacementType;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
|
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.util.CardTranslation;
|
import forge.util.CardTranslation;
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -652,9 +630,7 @@ public class Combat {
|
|||||||
// iterate all attackers and remove illegal declarations
|
// iterate all attackers and remove illegal declarations
|
||||||
CardCollection missingCombatants = new CardCollection();
|
CardCollection missingCombatants = new CardCollection();
|
||||||
for (Entry<GameEntity, AttackingBand> ee : attackedByBands.entries()) {
|
for (Entry<GameEntity, AttackingBand> ee : attackedByBands.entries()) {
|
||||||
CardCollectionView atk = ee.getValue().getAttackers();
|
for (Card c : ee.getValue().getAttackers()) {
|
||||||
for (int i = atk.size() - 1; i >= 0; i--) { // might remove items from collection, so no iterators
|
|
||||||
Card c = atk.get(i);
|
|
||||||
if (!c.isInPlay() || !c.isCreature()) {
|
if (!c.isInPlay() || !c.isCreature()) {
|
||||||
missingCombatants.add(c);
|
missingCombatants.add(c);
|
||||||
}
|
}
|
||||||
@@ -967,7 +943,7 @@ public class Combat {
|
|||||||
public boolean isPlayerAttacked(Player who) {
|
public boolean isPlayerAttacked(Player who) {
|
||||||
for (GameEntity defender : attackedByBands.keySet()) {
|
for (GameEntity defender : attackedByBands.keySet()) {
|
||||||
Card defenderAsCard = defender instanceof Card ? (Card)defender : null;
|
Card defenderAsCard = defender instanceof Card ? (Card)defender : null;
|
||||||
if ((null != defenderAsCard && defenderAsCard.getController() != who) ||
|
if ((null != defenderAsCard && (defenderAsCard.getController() != who && defenderAsCard.getProtectingPlayer() != who)) ||
|
||||||
(null == defenderAsCard && defender != who)) {
|
(null == defenderAsCard && defender != who)) {
|
||||||
continue; // defender is not related to player 'who'
|
continue; // defender is not related to player 'who'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package forge.game.combat;
|
package forge.game.combat;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
@@ -36,12 +28,7 @@ import forge.game.Game;
|
|||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.ability.AbilityKey;
|
import forge.game.ability.AbilityKey;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardCollectionView;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.card.CardUtil;
|
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
@@ -59,6 +46,12 @@ import forge.util.TextUtil;
|
|||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
import forge.util.maps.MapToAmount;
|
import forge.util.maps.MapToAmount;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -75,15 +68,17 @@ public class CombatUtil {
|
|||||||
final FCollection<GameEntity> defenders = new FCollection<>();
|
final FCollection<GameEntity> defenders = new FCollection<>();
|
||||||
for (final Player defender : playerWhoAttacks.getOpponents()) {
|
for (final Player defender : playerWhoAttacks.getOpponents()) {
|
||||||
defenders.add(defender);
|
defenders.add(defender);
|
||||||
final CardCollection planeswalkers = defender.getPlaneswalkersInPlay();
|
defenders.addAll(defender.getPlaneswalkersInPlay());
|
||||||
defenders.addAll(planeswalkers);
|
}
|
||||||
for (Card battle : defender.getBattlesInPlay()) {
|
|
||||||
if (!playerWhoAttacks.equals(battle.getProtectingPlayer()) && battle.getType().hasSubtype("Siege")) {
|
// Relevant battles (protected by the attacking player's opponents)
|
||||||
defenders.add(battle);
|
final Game game = playerWhoAttacks.getGame();
|
||||||
}
|
final CardCollection battles = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.BATTLES);
|
||||||
|
for (Card battle : battles) {
|
||||||
|
if (battle.getType().hasSubtype("Siege") && battle.getProtectingPlayer().isOpponentOf(playerWhoAttacks)) {
|
||||||
|
defenders.add(battle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defenders.addAll(playerWhoAttacks.getBattlesInPlay());
|
|
||||||
|
|
||||||
return defenders;
|
return defenders;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user