From 1898605123a0f4309c29f1296d85a319a32a593d Mon Sep 17 00:00:00 2001 From: Guilherme Manso Date: Fri, 27 Mar 2026 16:34:54 +0000 Subject: [PATCH] Fix #9973: AI never uses mutagen tokens Even if the AI has a sure fire way to win, it'd never use the mutagen tokens to give itself a better position. Now, the AI recognizes that it can indeed use the tokens for its' advantage and win. --- .../main/java/forge/ai/ComputerUtilCost.java | 4 +- .../java/forge/ai/ability/CountersPutAi.java | 2 +- .../src/main/java/forge/GuiDesktop.java | 15 ++++-- .../src/test/java/forge/ai/AITest.java | 4 +- .../forge/ai/ability/CountersPutAiTest.java | 49 +++++++++++++++++++ .../java/forge/error/ExceptionHandler.java | 20 +++++++- 6 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 forge-gui-desktop/src/test/java/forge/ai/ability/CountersPutAiTest.java diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index 7dfb7f68434..4264ca3f473 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -353,7 +353,9 @@ public static boolean checkSacrificeCost(final Player ai, final Cost cost, final for (final CostPart part : cost.getCostParts()) { if (part instanceof CostSacrifice sac) { if (sac.payCostFromSource()) { - if (!important) { + // Allow self-sacrifice for tokens (they're designed to be sacrificed for effect) + // even if not in immediate danger + if (!important && !source.isToken()) { return false; } if (!CardLists.filterControlledBy(source.getEnchantedBy(), source.getController()).isEmpty()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 605b106688c..b8190e0cb73 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -467,7 +467,7 @@ protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) { // don't put the counter on the dead creature if (sacSelf && c.equals(source)) { return false; - } else if (hasSacCost && !ComputerUtil.shouldSacrificeThreatenedCard(ai, c, sa)) { + } else if (hasSacCost && !sacSelf && !ComputerUtil.shouldSacrificeThreatenedCard(ai, c, sa)) { return false; } if ("NoCounterOfType".equals(sa.getParam("AILogic"))) { diff --git a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java index 223bf6caf82..45ab524addd 100644 --- a/forge-gui-desktop/src/main/java/forge/GuiDesktop.java +++ b/forge-gui-desktop/src/main/java/forge/GuiDesktop.java @@ -360,11 +360,16 @@ public void preventSystemSleep(boolean preventSleep) { } private static float initializeScreenScale() { - GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); - AffineTransform at = gc.getDefaultTransform(); - double scaleX = at.getScaleX(); - double scaleY = at.getScaleY(); - return (float) Math.min(scaleX, scaleY); + try { + GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + AffineTransform at = gc.getDefaultTransform(); + double scaleX = at.getScaleX(); + double scaleY = at.getScaleY(); + return (float) Math.min(scaleX, scaleY); + } catch (java.awt.HeadlessException e) { + // Running in headless mode (e.g., in tests or CI). Use default scale. + return 1.0f; + } } static float screenScale = initializeScreenScale(); diff --git a/forge-gui-desktop/src/test/java/forge/ai/AITest.java b/forge-gui-desktop/src/test/java/forge/ai/AITest.java index a494237494b..b53d2a9e6aa 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/AITest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/AITest.java @@ -5,7 +5,6 @@ import com.google.common.collect.Lists; -import forge.GuiDesktop; import forge.StaticData; import forge.deck.Deck; import forge.game.Game; @@ -26,6 +25,7 @@ import forge.item.PaperToken; import forge.localinstance.properties.ForgePreferences.FPref; import forge.model.FModel; +import forge.net.HeadlessGuiDesktop; public class AITest { private static boolean initialized = false; @@ -50,7 +50,7 @@ public Game resetGame() { protected Game initAndCreateGame() { if (!initialized) { - GuiBase.setInterface(new GuiDesktop()); + GuiBase.setInterface(new HeadlessGuiDesktop()); FModel.initialize(null, preferences -> { preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false); preferences.setPref(FPref.UI_LANGUAGE, "en-US"); diff --git a/forge-gui-desktop/src/test/java/forge/ai/ability/CountersPutAiTest.java b/forge-gui-desktop/src/test/java/forge/ai/ability/CountersPutAiTest.java new file mode 100644 index 00000000000..20b38d21c67 --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/ai/ability/CountersPutAiTest.java @@ -0,0 +1,49 @@ +package forge.ai.ability; + +import forge.ai.AITest; +import forge.ai.AiPlayDecision; +import forge.ai.SpellAbilityAi; +import forge.ai.SpellApiToAi; +import forge.game.Game; +import forge.game.ability.ApiType; +import forge.game.card.Card; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import org.testng.annotations.Test; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; + +public class CountersPutAiTest extends AITest { + + @Test + public void testSelfSacrificePutCounterAbilityIsPlayable() { + Game game = initAndCreateGame(); + Player ai = game.getPlayers().get(1); + + // Mutagen token activation needs one generic mana and a valid creature target. + Card mutagenToken = addToken("c_a_mutagen_sac", ai); + addCard("Plains", ai); + addCard("Runeclaw Bear", ai); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, ai); + game.getAction().checkStateEffects(true); + + // Find the PutCounter ability (with the activation cost) + SpellAbility putCounterSa = null; + for (SpellAbility sa : mutagenToken.getSpellAbilities()) { + if (sa.getDescription().contains("Sacrifice this token: Put a +1/+1")) { + putCounterSa = sa; + break; + } + } + assertNotNull("Could not find PutCounter ability on mutagen token", putCounterSa); + + SpellAbilityAi aiLogic = SpellApiToAi.Converter.get(ApiType.PutCounter); + AiPlayDecision decision = aiLogic.canPlayWithSubs(ai, putCounterSa).decision(); + + assertEquals("AI should activate self-sacrifice mutagen PutCounter ability", + AiPlayDecision.WillPlay, decision); + } +} diff --git a/forge-gui/src/main/java/forge/error/ExceptionHandler.java b/forge-gui/src/main/java/forge/error/ExceptionHandler.java index a84a7e026e5..48068a78e6a 100644 --- a/forge-gui/src/main/java/forge/error/ExceptionHandler.java +++ b/forge-gui/src/main/java/forge/error/ExceptionHandler.java @@ -99,7 +99,15 @@ public static void unregisterErrorHandling() throws IOException { /** {@inheritDoc} */ @Override public final void uncaughtException(final Thread t, final Throwable ex) { - BugReporter.reportException(ex); + try { + BugReporter.reportException(ex); + } catch (Throwable handlerFailure) { + System.err.println("Fatal error while reporting uncaught exception."); + System.err.println("Original uncaught exception:"); + ex.printStackTrace(); + System.err.println("Exception while handling uncaught exception:"); + handlerFailure.printStackTrace(); + } } /** @@ -110,6 +118,14 @@ public final void uncaughtException(final Thread t, final Throwable ex) { * a {@link java.lang.Throwable} object. */ public final void handle(final Throwable ex) { - BugReporter.reportException(ex); + try { + BugReporter.reportException(ex); + } catch (Throwable handlerFailure) { + System.err.println("Fatal error while reporting AWT exception."); + System.err.println("Original AWT exception:"); + ex.printStackTrace(); + System.err.println("Exception while handling AWT exception:"); + handlerFailure.printStackTrace(); + } } }