diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 509601aec744..e1fb1d6647da 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1534,13 +1534,6 @@ private boolean isSafeToHoldLandDropForMain2(Card landToPlay) { } private SpellAbility getSpellAbilityToPlay() { - if (skipped != null) { - //FIXME: this is for failed SA to skip temporarily, don't know why AI computation for mana fails, maybe due to auto mana compute? - for (SpellAbility sa : skipped) { - //System.out.println("Unskip: " + sa.toString() + " (" + sa.getHostCard().getName() + ")."); - sa.setSkip(false); - } - } CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player); cards = ComputerUtilCard.dedupeCards(cards); List saList = Lists.newArrayList(); @@ -1566,6 +1559,7 @@ private SpellAbility getSpellAbilityToPlay() { saList = ComputerUtilAbility.getSpellAbilities(cards, player); // get the SA list early to check for copy SAs if (ComputerUtilAbility.getFirstCopySASpell(saList) == null) { // Nothing to copy the spell with, so do nothing. + clearSkippedSpellAbilities(); return null; } } @@ -1589,22 +1583,38 @@ private SpellAbility getSpellAbilityToPlay() { // TODO allow when experimental profile? return spellAbility.isLandAbility() || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard())); }); - //removed skipped SA + // Keep failed SAs skipped for this priority sequence. Unskip only when passing so a later + // priority (new mana / board state) can retry — unskipping every choose caused equip loops. skipped = saList.stream().filter(SpellAbility::isSkip).collect(Collectors.toList()); - if (!skipped.isEmpty()) + if (!skipped.isEmpty()) { saList.removeAll(skipped); + } //update LivingEndPlayer useLivingEnd = IterableUtil.any(player.getZone(ZoneType.Library), CardPredicates.nameEquals("Living End")); SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true); if (topOwnedByAI && !mustRespond && chosenSa != ComputerUtilAbility.getFirstCopySASpell(saList)) { + clearSkippedSpellAbilities(); return null; // not planning to copy the spell and not marked as something the AI would respond to } + if (chosenSa == null) { + clearSkippedSpellAbilities(); + } return chosenSa; } + private void clearSkippedSpellAbilities() { + if (skipped == null || skipped.isEmpty()) { + return; + } + for (SpellAbility sa : skipped) { + sa.setSkip(false); + } + skipped.clear(); + } + private SpellAbility chooseSpellAbilityToPlayFromList(final List all, boolean skipCounter) { if (all == null || all.isEmpty()) return null; diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 659674c878a6..7ee0be6910df 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -837,11 +837,13 @@ public boolean playChosenSpellAbility(SpellAbility sa) { if (sa.isLandAbility()) { if (sa.canPlay()) { sa.resolve(); + return true; } - } else { - ComputerUtil.handlePlayingSpellAbility(player, sa, getDeferredTargetingPlayerAction(sa)); + return false; } - return true; + // Must return the real result: always-true made PhaseHandler treat failed equips/spells as + // successful plays, so the AI re-chose the same SA until "AI looped too much". + return ComputerUtil.handlePlayingSpellAbility(player, sa, getDeferredTargetingPlayerAction(sa)); } /**