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:
Agetian
2023-05-15 19:56:31 +03:00
committed by GitHub
parent 15a2ccb9f3
commit 38990aff32
6 changed files with 73 additions and 68 deletions

View File

@@ -119,7 +119,11 @@ public class AiAttackController {
} // overloaded constructor to evaluate single specified attacker
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<>();
for (Card c : myList) {
if (canAttackWrapper(c, defender)) {
@@ -722,9 +726,14 @@ public class AiAttackController {
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
}
List<Card> battleDefending = c.getDefendingBattles();
if (!battleDefending.isEmpty()) {
// TODO filter for team ones
// Get the preferred battle (prefer own battles, then ally battles)
final CardCollection defBattles = c.getDefendingBattles();
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;
@@ -756,7 +765,17 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons)
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);
}
if (this.attackers.isEmpty()) {

View File

@@ -165,10 +165,12 @@ public class AiBlockController {
}
// TODO Add creatures attacking Planeswalkers in order of which we want to protect
// defend planeswalkers with more loyalty before planeswalkers with less loyalty
// if planeswalker will be too difficult to defend don't even bother
// defend planeswalkers with more loyalty before planeswalkers with less loyalty,
// 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) {
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);
// Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers);

View File

@@ -139,17 +139,25 @@ public class PlayAi extends SpellAbilityAi {
@Override
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options,
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>() {
@Override
public boolean apply(final Card c) {
// 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
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;
s.setActivatingPlayer(ai, true);
// timing restrictions still apply
if (!s.getRestrictions().checkTimingRestrictions(c, s))
continue;
if (params != null && params.containsKey("CMCLimit")) {
Integer cmcLimit = (Integer) params.get("CMCLimit");
if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit)
@@ -188,6 +196,11 @@ public class PlayAi extends SpellAbilityAi {
return false;
}
});
if (sa.hasParam("CastTransformed")) {
options.forEach(c -> c.changeToState(CardStateName.Original));
}
final Card best = ComputerUtilCard.getBestAI(tgtCards);
if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
sa.getTargets().add(best);