From 3f10e6ea833005f3bc793b0869e15a2e57ec6a30 Mon Sep 17 00:00:00 2001 From: marthinwurer Date: Sat, 27 Jul 2024 22:00:29 -0600 Subject: [PATCH 1/6] initial feature flag --- forge-game/src/main/java/forge/game/Game.java | 3 ++- .../src/main/java/forge/game/phase/PhaseHandler.java | 7 +++++++ .../forge/screens/home/settings/CSubmenuPreferences.java | 1 + .../forge/screens/home/settings/VSubmenuPreferences.java | 8 ++++++++ .../src/forge/screens/settings/SettingsPage.java | 4 ++++ forge-gui/res/languages/en-US.properties | 2 ++ .../src/main/java/forge/gamemodes/match/HostedMatch.java | 1 + .../forge/localinstance/properties/ForgePreferences.java | 1 + 8 files changed, 26 insertions(+), 1 deletion(-) diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index c32cf0cca3e..d7862638edc 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -90,8 +90,9 @@ public class Game { private final Zone stackZone = new Zone(ZoneType.Stack, this); + // While these are false here, they're really set by the Match/Preferences public boolean EXPERIMENTAL_RESTORE_SNAPSHOT = false; - // While this is false here, its really set by the Match/Preferences + public boolean EXPERIMENTAL_LOOP = false; // If this merges with LKI In the future, it will need to change forms private GameSnapshot previousGameState = null; diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 7e51cac2c97..b849209c91c 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -989,6 +989,13 @@ public void startFirstTurn(Player goesFirst) { startFirstTurn(goesFirst, null); } public void startFirstTurn(Player goesFirst, Runnable startGameHook) { + + if (this.game.EXPERIMENTAL_LOOP) { + System.out.println("Hit feature flag for experimental loop"); + } else { + System.out.println("No feature flag for experimental loop"); + } + StopWatch sw = new StopWatch(); if (phase != null) { diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java index b270cfe0d82..70d1b95414f 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java @@ -127,6 +127,7 @@ public void itemStateChanged(final ItemEvent arg0) { lstControls.add(Pair.of(view.getCbEnforceDeckLegality(), FPref.ENFORCE_DECK_LEGALITY)); lstControls.add(Pair.of(view.getCbPerformanceMode(), FPref.PERFORMANCE_MODE)); lstControls.add(Pair.of(view.getCbExperimentalRestore(), FPref.MATCH_EXPERIMENTAL_RESTORE)); + lstControls.add(Pair.of(view.getCbExperimentalMainLoop(), FPref.MATCH_EXPERIMENTAL_LOOP)); lstControls.add(Pair.of(view.getCbFilteredHands(), FPref.FILTERED_HANDS)); lstControls.add(Pair.of(view.getCbCloneImgSource(), FPref.UI_CLONE_MODE_SOURCE)); lstControls.add(Pair.of(view.getCbRemoveSmall(), FPref.DECKGEN_NOSMALL)); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java index 1bc10b0a959..b75e92a45cc 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java @@ -76,6 +76,7 @@ public enum VSubmenuPreferences implements IVSubmenu { private final JCheckBox cbWorkshopSyntax = new OptionsCheckBox(localizer.getMessage("cbWorkshopSyntax")); private final JCheckBox cbEnforceDeckLegality = new OptionsCheckBox(localizer.getMessage("cbEnforceDeckLegality")); private final JCheckBox cbExperimentalRestore = new OptionsCheckBox(localizer.getMessage("cbExperimentalRestore")); + private final JCheckBox cbExperimentalMainLoop = new OptionsCheckBox(localizer.getMessage("cbExperimentalMainLoop")); private final JCheckBox cbPerformanceMode = new OptionsCheckBox(localizer.getMessage("cbPerformanceMode")); private final JCheckBox cbSROptimize = new OptionsCheckBox(localizer.getMessage("cbSROptimize")); private final JCheckBox cbFilteredHands = new OptionsCheckBox(localizer.getMessage("cbFilteredHands")); @@ -237,6 +238,9 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(cbExperimentalRestore, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlExperimentalRestore")), descriptionConstraints); + pnlPrefs.add(cbExperimentalMainLoop, titleConstraints); + pnlPrefs.add(new NoteLabel(localizer.getMessage("nlExperimentalMainLoop")), descriptionConstraints); + pnlPrefs.add(cbFilteredHands, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlFilteredHands")), descriptionConstraints); @@ -849,6 +853,10 @@ public JCheckBox getCbExperimentalRestore() { return cbExperimentalRestore; } + public JCheckBox getCbExperimentalMainLoop() { + return cbExperimentalMainLoop; + } + /** @return {@link javax.swing.JCheckBox} */ public JCheckBox getCbFilteredHands() { return cbFilteredHands; diff --git a/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java b/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java index 6311f4ee6fb..5b0c209231b 100644 --- a/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java +++ b/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java @@ -240,6 +240,10 @@ public void valueChanged(String newValue) { Forge.getLocalizer().getMessage("cbExperimentalRestore"), Forge.getLocalizer().getMessage("nlExperimentalRestore")), 1); + lstSettings.addItem(new BooleanSetting(FPref.MATCH_EXPERIMENTAL_LOOP, + Forge.getLocalizer().getMessage("cbExperimentalLoop"), + Forge.getLocalizer().getMessage("nlExperimentaLoop")), + 1); lstSettings.addItem(new BooleanSetting(FPref.FILTERED_HANDS, Forge.getLocalizer().getMessage("cbFilteredHands"), Forge.getLocalizer().getMessage("nlFilteredHands")), diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 57f2f6b9e70..d49a3108fc6 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -72,6 +72,7 @@ cbWorkshopSyntax=Workshop Syntax Checker cbEnforceDeckLegality=Deck Conformance cbpAiSideboardingMode=AI Sideboarding Mode cbExperimentalRestore=EXPERIMENTAL Undo restore +cbExperimentalMainLoop=EXPERIMENTAL Discrete Main Game Loop cbPerformanceMode=Performance Mode cbFilteredHands=Filtered Hands cbImageFetcher=Automatically Download Missing Card Art @@ -159,6 +160,7 @@ nlManaLostPrompt=When enabled, you get a warning if passing priority would cause nlEnforceDeckLegality=Enforces deck legality relevant to each environment (minimum deck sizes, max card count etc). nlpAiSideboardingMode=Choose the way the AI sideboards: Off (the AI doesn't sideboard), AI (the AI sideboards for itself, currently mostly random), Human For AI (the human player sideboards for the AI in Constructed formats). nlExperimentalRestore=EXPERIMENTAL - Stores a snapshot to be used for undoing spells or abilities +nlExperimentalLoop=EXPERIMENTAL - Uses a new discrete main game loop implementation nlPerformanceMode=Disables additional static abilities checks to speed up the game engine. (Warning: breaks some ''as if had flash'' scenarios when casting cards owned by opponents). nlFilteredHands=Generates two starting hands and keeps the one with the closest to average land count for the deck. (REQUIRES RESTART) nlCloneImgSource=When enabled clones will use their original art instead of the cloned card''s art. diff --git a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java index 8326e13b2ac..1dd4e2e86f9 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java @@ -172,6 +172,7 @@ public void startGame() { game = match.createGame(); game.EXPERIMENTAL_RESTORE_SNAPSHOT = FModel.getPreferences().getPrefBoolean(FPref.MATCH_EXPERIMENTAL_RESTORE); + game.EXPERIMENTAL_LOOP = FModel.getPreferences().getPrefBoolean(FPref.MATCH_EXPERIMENTAL_LOOP); StaticData.instance().setSourceImageForClone(FModel.getPreferences().getPrefBoolean(FPref.UI_CLONE_MODE_SOURCE)); diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java index c398fa71096..49a5ecd52aa 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java @@ -207,6 +207,7 @@ public enum FPref { MATCH_AI_SIDEBOARDING_MODE("Human For AI"), MATCH_EXPERIMENTAL_RESTORE("false"), + MATCH_EXPERIMENTAL_LOOP("true"), ENFORCE_DECK_LEGALITY ("true"), PERFORMANCE_MODE ("false"), FILTERED_HANDS ("false"), From 1d640fca3b4b16f9041205190b78cb252e52ea9c Mon Sep 17 00:00:00 2001 From: marthinwurer Date: Sat, 27 Jul 2024 22:55:08 -0600 Subject: [PATCH 2/6] initial split out --- .../java/forge/game/phase/PhaseHandler.java | 157 ++++ .../src/test/java/forge/ai/AITest.java | 152 +++ .../java/forge/ai/SpellAbilityPickerTest.java | 877 ++++++++++++++++++ 3 files changed, 1186 insertions(+) create mode 100644 forge-gui-desktop/src/test/java/forge/ai/AITest.java create mode 100644 forge-gui-desktop/src/test/java/forge/ai/SpellAbilityPickerTest.java diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index b849209c91c..91dbf0c0a60 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -985,6 +985,161 @@ public final boolean beforeFirstPostCombatMainEnd() { private final static boolean DEBUG_PHASES = false; + public void discreteStartFirstTurn(Player goesFirst, Runnable startGameHook) { + + if (phase != null) { + throw new IllegalStateException("Turns already started, call this only once per game"); + } + + setPlayerTurn(goesFirst); + advanceToNextPhase(); + onPhaseBegin(); + + // don't even offer priority, because it's untap of 1st turn now + givePriorityToPlayer = false; + + if (startGameHook != null) { + startGameHook.run(); + givePriorityToPlayer = true; + } + + mainGameLoop(); + } + + public void mainGameLoop() { + while (!game.isGameOver()) { + mainLoopStep(); + } + } + + public void mainLoopStep() { + if (givePriorityToPlayer) { + StopWatch sw = new StopWatch(); + if (DEBUG_PHASES) { + sw.start(); + } + + game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer())); + List chosenSa = null; + + int loopCount = 0; + do { + if (checkStateBasedEffects()) { + // state-based effects check could lead to game over + return; + } + game.stashGameState(); + + chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay(); + + // this needs to come after chosenSa so it sees you conceding on own turn + if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) { + // If the active player has lost, and they have priority, set the next player to have priority + System.out.println("Active player is no longer in the game..."); + pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer()); + pFirstPriority = pPlayerPriority; + } + + if (chosenSa == null) { + break; // that means 'I pass' + } + if (DEBUG_PHASES) { + System.out.print("... " + pPlayerPriority + " plays " + chosenSa); + } + + boolean rollback = false; + for (SpellAbility sa : chosenSa) { + Card saHost = sa.getHostCard(); + final Zone originZone = saHost.getZone(); + + if (pPlayerPriority.getController().playChosenSpellAbility(sa)) { + // 117.3c If a player has priority when they cast a spell, activate an ability, [play a land] + // that player receives priority afterward. + pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve + } else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) { + rollback = true; + } + + saHost = game.getCardState(saHost); + final Zone currentZone = saHost.getZone(); + + // Need to check if Zone did change + if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa instanceof LandAbility)) { + // currently there can be only one Spell put on the Stack at once, or Land Abilities be played + final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard()); + triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost); + triggerList.triggerChangesZoneAll(game, sa); + } + } + // Don't copy last state if we're in the middle of rolling back a spell... + if (!rollback) { + game.copyLastState(); + } + loopCount++; + } while (loopCount < 999 || !pPlayerPriority.getController().isAI()); + + if (loopCount >= 999 && pPlayerPriority.getController().isAI()) { + System.out.print("AI looped too much with: " + chosenSa); + } + + if (DEBUG_PHASES) { + sw.stop(); + System.out.print("... passed in " + sw.getTime()/1000f + " s\n"); + System.out.println("\t\tStack: " + game.getStack()); + sw.reset(); + } + } + else if (DEBUG_PHASES) { + System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n"); + } + + // actingPlayer is the player who may act + // the firstAction is the player who gained Priority First in this segment + // of Priority + Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer()); + + if (game.isGameOver() || nextPlayer == null) { return; } // conceded? + + if (DEBUG_PHASES) { + System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString())); + } + if (pFirstPriority == nextPlayer) { + if (game.getStack().isEmpty()) { + if (playerTurn.hasLost()) { + setPriority(game.getNextPlayerAfter(playerTurn)); + } else { + setPriority(playerTurn); + } + + // end phase + givePriorityToPlayer = true; + onPhaseEnd(); + advanceToNextPhase(); + onPhaseBegin(); + } + else if (!game.getStack().hasSimultaneousStackEntries()) { + game.getStack().resolveStack(); + } + } else { + // pass the priority to other player + pPlayerPriority = nextPlayer; + } + + // If ever the karn's ultimate resolved + if (game.getAge() == GameStage.RestartedByKarn) { + setPhase(null); + game.updatePhaseForView(); + game.fireEvent(new GameEventGameRestarted(playerTurn)); + return; + } + + // update Priority for all players + for (final Player p : game.getPlayers()) { + p.setHasPriority(getPriorityPlayer() == p); + } + + } + public void startFirstTurn(Player goesFirst) { startFirstTurn(goesFirst, null); } @@ -992,6 +1147,8 @@ public void startFirstTurn(Player goesFirst, Runnable startGameHook) { if (this.game.EXPERIMENTAL_LOOP) { System.out.println("Hit feature flag for experimental loop"); + discreteStartFirstTurn(goesFirst, startGameHook); + return; } else { System.out.println("No feature flag for experimental loop"); } diff --git a/forge-gui-desktop/src/test/java/forge/ai/AITest.java b/forge-gui-desktop/src/test/java/forge/ai/AITest.java new file mode 100644 index 00000000000..d9c7e4efad4 --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/ai/AITest.java @@ -0,0 +1,152 @@ +package forge.ai; + +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import forge.GuiDesktop; +import forge.StaticData; +import forge.deck.Deck; +import forge.game.Game; +import forge.game.GameRules; +import forge.game.GameStage; +import forge.game.GameType; +import forge.game.Match; +import forge.game.card.Card; +import forge.game.card.CardCollectionView; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.RegisteredPlayer; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import forge.gui.GuiBase; +import forge.item.IPaperCard; +import forge.localinstance.properties.ForgePreferences; +import forge.localinstance.properties.ForgePreferences.FPref; +import forge.model.FModel; + +public class AITest { + private static boolean initialized = false; + + public Game resetGame() { + // need to be done after FModel.initialize, or the Localizer isn't loaded yet + List players = Lists.newArrayList(); + Deck d1 = new Deck(); + players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p2", null))); + players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p1", null))); + GameRules rules = new GameRules(GameType.Constructed); + Match match = new Match(rules, players, "Test"); + Game game = new Game(players, rules, match); + Player p = game.getPlayers().get(1); + game.setAge(GameStage.Play); + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getPhaseHandler().onStackResolved(); + // game.getAction().checkStateEffects(true); + + return game; + } + + protected Game initAndCreateGame() { + if (!initialized) { + GuiBase.setInterface(new GuiDesktop()); + FModel.initialize(null, new Function() { + @Override + public Void apply(ForgePreferences preferences) { + preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false); + preferences.setPref(FPref.UI_LANGUAGE, "en-US"); + return null; + } + }); + initialized = true; + } + + return resetGame(); + } + + protected int countCardsWithName(Game game, String name) { + int i = 0; + for (Card c : game.getCardsIn(ZoneType.Battlefield)) { + if (c.getName().equals(name)) { + i++; + } + } + return i; + } + + protected Card findCardWithName(Game game, String name) { + for (Card c : game.getCardsIn(ZoneType.Battlefield)) { + if (c.getName().equals(name)) { + return c; + } + } + return null; + } + + protected String gameStateToString(Game game) { + StringBuilder sb = new StringBuilder(); + for (ZoneType zone : ZoneType.values()) { + CardCollectionView cards = game.getCardsIn(zone); + if (!cards.isEmpty()) { + sb.append("Zone ").append(zone.name()).append(":\n"); + for (Card c : game.getCardsIn(zone)) { + sb.append(" ").append(c); + if (c.isTapped()) { + sb.append(" (T)"); + } + sb.append("\n"); + } + } + } + return sb.toString(); + } + + protected SpellAbility findSAWithPrefix(Card c, String prefix) { + return findSAWithPrefix(c.getSpellAbilities(), prefix); + } + + protected SpellAbility findSAWithPrefix(Iterable abilities, String prefix) { + for (SpellAbility sa : abilities) { + if (sa.getDescription().startsWith(prefix)) { + return sa; + } + } + return null; + } + + protected Card createCard(String name, Player p) { + IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name); + if (paperCard == null) { + StaticData.instance().attemptToLoadCard(name); + paperCard = FModel.getMagicDb().getCommonCards().getCard(name); + } + return Card.fromPaperCard(paperCard, p); + } + + protected Card addCardToZone(String name, Player p, ZoneType zone) { + Card c = createCard(name, p); + // card need a new Timestamp otherwise Static Abilities might collide + c.setGameTimestamp(p.getGame().getNextTimestamp()); + p.getZone(zone).add(c); + return c; + } + + protected Card addCard(String name, Player p) { + return addCardToZone(name, p, ZoneType.Battlefield); + } + + protected List addCards(String name, int count, Player p) { + List cards = Lists.newArrayList(); + for (int i = 0; i < count; i++) { + cards.add(addCard(name, p)); + } + return cards; + } + + void playUntilStackClear(Game game) { + do { + game.getPhaseHandler().mainLoopStep(); + } while (!game.isGameOver() && !game.getStack().isEmpty()); + return; + } +} diff --git a/forge-gui-desktop/src/test/java/forge/ai/SpellAbilityPickerTest.java b/forge-gui-desktop/src/test/java/forge/ai/SpellAbilityPickerTest.java new file mode 100644 index 00000000000..e6a7e2d847b --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/ai/SpellAbilityPickerTest.java @@ -0,0 +1,877 @@ +package forge.ai; + +import forge.ai.simulation.GameStateEvaluator; +import forge.game.spellability.LandAbility; + +import java.util.ArrayList; +import java.util.List; + +import forge.item.PaperCard; +import forge.model.FModel; +import org.testng.AssertJUnit; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +import forge.game.Game; +import forge.game.card.Card; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; + +public class SpellAbilityPickerTest extends AITest { + @Test + public void testPickingLethalDamage() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + p.setTeam(0); + + addCard("Mountain", p); + addCardToZone("Shock", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + opponent.setTeam(1); + + addCard("Runeclaw Bear", opponent); + opponent.setLife(2, null); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + List chosenSa = p.getController().chooseSpellAbilityToPlay(); + System.out.println(chosenSa); + SpellAbility sa = chosenSa.get(0); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertNull(sa.getTargetCard()); + AssertJUnit.assertEquals(opponent, sa.getTargets().getFirstTargetedPlayer()); + } + + @Ignore + @Test + public void testPickingKillingCreature() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCard("Mountain", p); + addCardToZone("Shock", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + Card bearCard = addCard("Runeclaw Bear", opponent); + opponent.setLife(20, null); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + List chosenSa = p.getController().chooseSpellAbilityToPlay(); + System.out.println(chosenSa); + System.out.println(gameStateToString(game)); + SpellAbility sa = chosenSa.get(0); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertEquals(bearCard, sa.getTargetCard()); + AssertJUnit.assertNull(sa.getTargets().getFirstTargetedPlayer()); + } + + @Ignore + @Test + public void testSequenceStartingWithPlayingLand() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + Card mountain = addCardToZone("Mountain", p, ZoneType.Hand); + addCardToZone("Shock", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + addCard("Runeclaw Bear", opponent); + opponent.setLife(20, null); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + List chosenSa = p.getController().chooseSpellAbilityToPlay(); + System.out.println(chosenSa); + System.out.println(gameStateToString(game)); + SpellAbility sa = chosenSa.get(0); + AssertJUnit.assertTrue(sa instanceof LandAbility); + AssertJUnit.assertEquals(mountain, sa.getHostCard()); + + playUntilStackClear(game); + game.getPhaseHandler().mainLoopStep(); + System.out.println(gameStateToString(game)); + sa = p.getController().chooseSpellAbilityToPlay().get(0); + + AssertJUnit.assertEquals("Shock deals 2 damage to any target.", sa.toString()); + // AssertJUnit.assertTrue(plan.getDecisions().get(1).targets.toString().contains("Runeclaw Bear")); + } + + @Ignore + @Test + public void testPlayingLandAfterSpell() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCards("Island", 2, p); + addCards("Forest", 3, p); + + Card tatyova = addCardToZone("Tatyova, Benthic Druid", p, ZoneType.Hand); + addCardToZone("Forest", p, ZoneType.Hand); + addCardToZone("Forest", p, ZoneType.Library); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals(tatyova, sa.getHostCard()); + + // The plan should involve playing Tatyova first and then playing a land, to benefit from + // the landfall trigger. + playUntilStackClear(game); + sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals("Play land", sa.toString()); +// Plan plan = picker.getPlan(); +// AssertJUnit.assertEquals(2, plan.getDecisions().size()); +// AssertJUnit.assertEquals("Tatyova, Benthic Druid - Creature 3 / 3", plan.getDecisions().get(0).saRef.toString()); +// AssertJUnit.assertEquals("Play land", plan.getDecisions().get(1).saRef.toString()); + } + + @Ignore + @Test + public void testModeSelection() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCard("Plains", p); + addCard("Island", p); + addCard("Swamp", p); + Card spell = addCardToZone("Dromar's Charm", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + addCard("Runeclaw Bear", opponent); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + // Expected: All creatures get -2/-2 to kill the bear. + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); +// SpellAbilityPicker picker = new SpellAbilityPicker(game, p); +// SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(spell.getSpellAbilities().get(0), sa); + AssertJUnit.assertEquals("Dromar's Charm -> Target creature gets -2/-2 until end of turn.", + sa.toString()); + } + + @Ignore + @Test + public void testModeSelection2() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCard("Plains", p); + addCard("Island", p); + addCard("Swamp", p); + Card spell = addCardToZone("Dromar's Charm", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + // Expected: Gain 5 life, since other modes aren't helpful. + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); +// SpellAbilityPicker picker = new SpellAbilityPicker(game, p); +// SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(spell.getSpellAbilities().get(0), sa); + AssertJUnit.assertEquals("Dromar's Charm -> You gain 5 life.", sa.toString()); + } + + /* + @Test + public void testMultipleModes() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCards("Mountain", 4, p); + Card spell = addCardToZone("Fiery Confluence", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + addCard("Runeclaw Bear", opponent); + opponent.setLife(20, null); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + // Expected: 2x 1 damage to each creature, 1x 2 damage to each opponent. + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(spell.getSpellAbilities().get(0), sa); + + String dmgCreaturesStr = "Fiery Confluence deals 1 damage to each creature."; + String dmgOppStr = "Fiery Confluence deals 2 damage to each opponent."; + String expected = "Fiery Confluence -> " + dmgCreaturesStr + " " + dmgCreaturesStr + " " + dmgOppStr; + AssertJUnit.assertEquals(expected, picker.getPlan().getDecisions().get(0).modesStr); + } + + @Test + public void testMultipleModes2() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCards("Mountain", 4, p); + Card spell = addCardToZone("Fiery Confluence", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + addCard("Runeclaw Bear", opponent); + opponent.setLife(6, null); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + // Expected: 3x 2 damage to each opponent. + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(spell.getSpellAbilities().get(0), sa); + + String dmgOppStr = "Fiery Confluence deals 2 damage to each opponent."; + String expected = "Fiery Confluence -> " + dmgOppStr + " " + dmgOppStr + " " + dmgOppStr; + AssertJUnit.assertEquals(expected, picker.getPlan().getDecisions().get(0).modesStr); + } + + @Test + public void testMultipleTargets() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCards("Mountain", 2, p); + Card spell = addCardToZone("Arc Trail", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + Card bear = addCard("Runeclaw Bear", opponent); + Card men = addCard("Flying Men", opponent); + opponent.setLife(20, null); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(spell.getSpellAbilities().get(0), sa); + AssertJUnit.assertEquals(bear, sa.getTargetCard()); + AssertJUnit.assertEquals("2", sa.getParam("NumDmg")); + SpellAbility subSa = sa.getSubAbility(); + AssertJUnit.assertEquals(men, subSa.getTargetCard()); + AssertJUnit.assertEquals("1", subSa.getParam("NumDmg")); + } + + @Test + public void testLandSearchForCombo() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCard("Forest", p); + addCard("Thespian's Stage", p); + Card darkDepths = addCard("Dark Depths", p); + + Card cropRotation = addCardToZone("Crop Rotation", p, ZoneType.Hand); + + addCardToZone("Forest", p, ZoneType.Library); + addCardToZone("Urborg, Tomb of Yawgmoth", p, ZoneType.Library); + addCardToZone("Swamp", p, ZoneType.Library); + + darkDepths.setCounters(CounterEnumType.ICE, 10); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + AssertJUnit.assertEquals(10, darkDepths.getCounters(CounterEnumType.ICE)); + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(cropRotation.getSpellAbilities().get(0), sa); + // Expected: Sac a Forest to get an Urborg. + List choices = picker.getPlan().getDecisions().get(0).choices; + AssertJUnit.assertEquals(2, choices.size()); + AssertJUnit.assertEquals("Forest", choices.get(0)); + AssertJUnit.assertEquals("Urborg, Tomb of Yawgmoth", choices.get(1)); + // Next, expected to use Thespian's Stage to copy Dark Depths. + Plan.Decision d2 = picker.getPlan().getDecisions().get(1); + String expected = "{2}, {T}: Thespian's Stage becomes a copy of target land, except it has this ability."; + AssertJUnit.assertEquals(expected, d2.saRef.toString()); + AssertJUnit.assertTrue(d2.targets.toString().contains("Dark Depths")); + } + */ + + @Ignore + @Test + public void playTaplandIfNoPlays() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + // start with a hand with a basic, a tapland, and a card that can't be cast + addCard("Forest", p); + addCardToZone("Forest", p, ZoneType.Hand); + Card desired = addCardToZone("Simic Guildgate", p, ZoneType.Hand); + addCardToZone("Centaur Courser", p, ZoneType.Hand); + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + // ensure that the tapland is paid + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals(desired, sa.getHostCard()); + } + + @Ignore + @Test + public void playBouncelandIfNoPlays() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + // start with a hand with a basic, a bounceland, and a card that can't be cast + addCard("Forest", p); + addCardToZone("Forest", p, ZoneType.Hand); + Card desired = addCardToZone("Simic Growth Chamber", p, ZoneType.Hand); + addCardToZone("Centaur Courser", p, ZoneType.Hand); + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + // ensure that the tapland is played + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals(desired, sa.getHostCard()); + } + + @Ignore + @Test + public void playTronOverBasic() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + // start with a hand with a basic, a Tron land, and a card that can't be cast + addCard("Urza's Tower", p); + addCard("Urza's Mine", p); + addCardToZone("Forest", p, ZoneType.Hand); + Card desired = addCardToZone("Urza's Power Plant", p, ZoneType.Hand); + addCardToZone("Opt", p, ZoneType.Hand); + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + // ensure that the tron land is played + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals(desired, sa.getHostCard()); + } + + @Test + public void playManalessLands() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + // start with a hand with a land that can't produce mana. + Card desired = addCardToZone("Maze of Ith", p, ZoneType.Hand); + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + // ensure that the land is played + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals(desired, sa.getHostCard()); + } + + @Test + public void playBasicOverUtility() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + // start with a hand with a colorless utility land and a basic + addCardToZone("Rogue's Passage", p, ZoneType.Hand); + Card desired = addCardToZone("Forest", p, ZoneType.Hand); + + // make sure that there is a card in the library with G mana cost + addCardToZone("Grizzly Bears", p, ZoneType.Library); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + // ensure that the basic land is played + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals(desired, sa.getHostCard()); + } + + @Ignore + @Test + public void targetUtilityLandOverRainbow() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(20, null); + + // start with the opponent having a basic land, a dual, and a rainbow + addCard("Forest", opponent); + addCard("Breeding Pool", opponent); + addCard("Mana Confluence", opponent); + Card desired = addCard("Mutavault", opponent); + addCard("Strip Mine", p); + + // It doesn't want to use strip mine in main + game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_BLOCKERS, p); + game.getAction().checkStateEffects(true); + + // ensure that the land is played + SpellAbility sa = p.getController().chooseSpellAbilityToPlay().get(0); + AssertJUnit.assertEquals(desired, sa.getTargetCard()); + } + + @Ignore + @Test + public void ensureAllLandsArePlayable() { + initAndCreateGame(); + + System.out.println("Adding lands to hand"); + + // add every land to the player's hand + List funky = new ArrayList<>(); + String previous = ""; + for (PaperCard c : FModel.getMagicDb().getCommonCards().getAllCards()) { + // Only test one version of a card + if (c.getName().equals(previous)) { + continue; + } + previous = c.getName(); + + // skip nonland cards + if (!c.getRules().getType().isLand()) { + continue; + } + +// System.out.println(c.getName()); + + // Skip glacial chasm, it's really weird. + if (c.getName().equals("Glacial Chasm")) { + System.out.println("Skipping " + c.getName()); + continue; + } + + // reset the game + Game game = resetGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(20, null); + + // add one of each basic to the battlefield so that bouncelands and similar work + addCard("Plains", p); + addCard("Island", p); + addCard("Swamp", p); + addCard("Mountain", p); + addCard("Forest", p); + // Add basics to library to ensure fetches work + addCardToZone("Plains", p, ZoneType.Library); + addCardToZone("Island", p, ZoneType.Library); + addCardToZone("Swamp", p, ZoneType.Library); + addCardToZone("Mountain", p, ZoneType.Library); + addCardToZone("Forest", p, ZoneType.Library); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + // Add the target card to the hand and test it + Card desired = addCardToZone(c.getName(), p, ZoneType.Hand); + + List choices = p.getController().chooseSpellAbilityToPlay(); + if (choices == null) { + funky.add(desired); + continue; + } + SpellAbility sa = choices.get(0); + if (sa == null || sa.getHostCard() != desired) { + funky.add(desired); + continue; + } +// AssertJUnit.assertEquals(desired, sa.getTargetCard()); +// GameStateEvaluator.Score s = new GameStateEvaluator().getScoreForGameState(game, p); +// System.out.println("Starting score: " + s); +// SpellAbilityPicker picker = new SpellAbilityPicker(game, p); +// List candidateSAs = picker.getCandidateSpellsAndAbilities(); +// for (int i = 0; i < candidateSAs.size(); i++) { +// SpellAbility sa = candidateSAs.get(i); +// if (sa.isActivatedAbility()) { +// continue; +// } +// GameStateEvaluator.Score value = picker.evaluateSa(new SimulationController(s), game.getPhaseHandler().getPhase(), candidateSAs, i); +// System.out.println("sa: " + sa.getHostCard() + ", value: " + value); +// if (!(value.value > s.value)) { +// funky.add(sa.getHostCard()); +// } +// } + } + + // ensure that every land play has a higher evaluation than doing nothing + System.out.println(funky); + for (Card c : funky) { + GameStateEvaluator gse = new GameStateEvaluator(); + Game game = resetGame(); + System.out.println(c.getName() + ": " + gse.evalCard(game, game.getStartingPlayer(), c)); + } + AssertJUnit.assertEquals(0, funky.size()); + } + + + /* + @Test + public void testPlayRememberedCardsLand() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCards("Mountain", 2, p); + Card abbot = addCardToZone("Abbot of Keral Keep", p, ZoneType.Hand); + addCardToZone("Lightning Bolt", p, ZoneType.Hand); + // Note: This assumes the top of library is revealed. If the AI is made + // smarter to not assume that, then this test can be updated to have + // something that reveals top of library active - e.g. Lens of Clarity. + addCardToZone("Mountain", p, ZoneType.Library); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + // Expected plan: + // 1. Play Abbot. + // 2. Play land exiled by Abbot. + // 3. Play Bolt targeting opponent. + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(abbot.getSpellAbilities().get(0), sa); + Plan plan = picker.getPlan(); + AssertJUnit.assertEquals(3, plan.getDecisions().size()); + AssertJUnit.assertEquals("Mountain (5) -> Play land by Abbot of Keral Keep (3)", + plan.getDecisions().get(1).saRef.toString(true)); + AssertJUnit.assertEquals("Lightning Bolt deals 3 damage to any target.", + plan.getDecisions().get(2).saRef.toString()); + } + + @Test + public void testPlayRememberedCardsSpell() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCards("Mountain", 3, p); + Card abbot = addCardToZone("Abbot of Keral Keep", p, ZoneType.Hand); + // Note: This assumes the top of library is revealed. If the AI is made + // smarter to not assume that, then this test can be updated to have + // something that reveals top of library active - e.g. Lens of Clarity. + addCardToZone("Lightning Bolt", p, ZoneType.Library); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + // Expected plan: + // 1. Play Abbot. + // 3. Play Bolt exiled by Abbot. + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertEquals(abbot.getSpellAbilities().get(0), sa); + Plan plan = picker.getPlan(); + AssertJUnit.assertEquals(2, plan.getDecisions().size()); + String saDesc = plan.getDecisions().get(1).saRef.toString(); + AssertJUnit.assertTrue(saDesc, saDesc.startsWith("Lightning Bolt deals 3 damage to any target.")); + } + + @Test + public void testPlayingPumpSpellsAfterBlocks() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(2, null); + + Card blocker = addCard("Fugitive Wizard", opponent); + Card attacker1 = addCard("Dwarven Trader", p); + attacker1.setSickness(false); + Card attacker2 = addCard("Dwarven Trader", p); + attacker2.setSickness(false); + addCard("Mountain", p); + addCardToZone("Brute Force", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + AssertJUnit.assertNull(picker.chooseSpellAbilityToPlay(null)); + + game.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_BEGIN); + game.getAction().checkStateEffects(true); + AssertJUnit.assertNull(picker.chooseSpellAbilityToPlay(null)); + + game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, p); + Combat combat = new Combat(p); + combat.addAttacker(attacker1, opponent); + combat.addAttacker(attacker2, opponent); + game.getPhaseHandler().setCombat(combat); + game.getAction().checkStateEffects(true); + AssertJUnit.assertNull(picker.chooseSpellAbilityToPlay(null)); + + game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_BLOCKERS, p, false); + game.getAction().checkStateEffects(true); + combat.addBlocker(attacker1, blocker); + combat.getBandOfAttacker(attacker1).setBlocked(true); + combat.getBandOfAttacker(attacker2).setBlocked(false); + combat.orderBlockersForDamageAssignment(); + combat.orderAttackersForDamageAssignment(); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertEquals("Target creature gets +3/+3 until end of turn.", sa.toString()); + AssertJUnit.assertEquals(attacker2, sa.getTargetCard()); + } + + @Test + public void testPlayingSorceryPumpSpellsBeforeBlocks() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(2, null); + + addCard("Fugitive Wizard", opponent); + Card attacker1 = addCard("Dwarven Trader", p); + attacker1.setSickness(false); + Card attacker2 = addCard("Kird Ape", p); + attacker2.setSickness(false); + addCard("Mountain", p); + Card furor = addCardToZone("Furor of the Bitten", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertEquals(furor.getSpellAbilities().get(0), sa); + AssertJUnit.assertEquals(attacker1, sa.getTargetCard()); + } + + @Test + public void testPlayingRemovalBeforeBlocks() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + opponent.setLife(2, null); + + Card blocker = addCard("Fugitive Wizard", opponent); + Card attacker1 = addCard("Dwarven Trader", p); + attacker1.setSickness(false); + addCards("Swamp", 2, p); + addCardToZone("Doom Blade", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertEquals("Destroy target nonblack creature.", sa.toString()); + AssertJUnit.assertEquals(blocker, sa.getTargetCard()); + } + + // Run the test 100 times to ensure there's no flakiness. + @Test(invocationCount = 100) + public void testChoicesResultingFromRandomEffects() { + // Sometimes, the effect of a spell can be random, and as a result of that, new choices + // could be selected during simulation. This test verifies that this doesn't cause problems. + // + // Note: The current implementation works around the issue by setting a consistent random + // seed during choice evaluation, although in the future, it may make sense to handle it + // some other way. + + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + + addCardToZone("Chaos Warp", p, ZoneType.Hand); + addCards("Mountain", 3, p); + + addCard("Plains", opponent); + addCard("Mountain", opponent); + addCard("Forest", opponent); + // Use a card that is worthwhile to target even if the shuffle ends up choosing it + // again. In this case, life loss on ETB and leaving. + Card expectedTarget = addCard("Raving Oni-Slave", opponent); + + addCardToZone("Chaos Warp", opponent, ZoneType.Library); + addCardToZone("Island", opponent, ZoneType.Library); + addCardToZone("Swamp", opponent, ZoneType.Library); + // The presence of Pilgrim's Eye in the library is important for this test, as this + // will result in sub-choices (which land to pick) if this card ends up being the top + // of the library during simulation. + addCardToZone("Pilgrim's Eye", opponent, ZoneType.Library); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertEquals("Chaos Warp", sa.getHostCard().getName()); + AssertJUnit.assertEquals(expectedTarget, sa.getTargetCard()); + } + + @Test + public void testNoSimulationsWhenNoTargets() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCards("Forest", 2, p); + addCardToZone("Counterspell", p, ZoneType.Hand); + addCardToZone("Unsummon", p, ZoneType.Hand); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNull(sa); + AssertJUnit.assertEquals(0, picker.getNumSimulations()); + } + + @Test + public void testSpellCantTargetSelf() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + + addCardToZone("Unsubstantiate", p, ZoneType.Hand); + addCard("Forest", p); + addCard("Island", p); + Card expectedTarget = addCard("Flying Men", opponent); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + AssertJUnit.assertEquals(expectedTarget, sa.getTargetCard()); + // Only a single simulation expected (no target self). + AssertJUnit.assertEquals(1, picker.getNumSimulations()); + } + + @Test + public void testModalSpellCantTargetSelf() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + + addCardToZone("Decisive Denial", p, ZoneType.Hand); + addCard("Forest", p); + addCard("Island", p); + addCard("Runeclaw Bear", p); + addCard("Flying Men", opponent); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + // Expected: Runeclaw Bear fights Flying Men + // Only a single simulation expected (no target self). + AssertJUnit.assertEquals(1, picker.getNumSimulations()); + } + + @Test + public void testModalSpellNoTargetsForModeWithSubAbility() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCardToZone("Temur Charm", p, ZoneType.Hand); + addCard("Forest", p); + addCard("Island", p); + addCard("Mountain", p); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + picker.chooseSpellAbilityToPlay(null); + // Only mode "Creatures with power 3 or less can’t block this turn" should be simulated. + AssertJUnit.assertEquals(1, picker.getNumSimulations()); + } + + @Test + public void testModalSpellNoTargetsForAnyModes() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + + addCardToZone("Drown in the Loch", p, ZoneType.Hand); + addCard("Swamp", p); + addCard("Island", p); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + picker.chooseSpellAbilityToPlay(null); + // TODO: Ideally, this would be 0 simulations, but we currently only determine there are no + // valid modes in SpellAbilityChoicesIterator, which runs already when we're simulating. + // Still, this test case exercises the code path and ensures we don't crash in this case. + AssertJUnit.assertEquals(1, picker.getNumSimulations()); + } + + @Test + public void threeDistinctTargetSpell() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + + addCardToZone("Incremental Growth", p, ZoneType.Hand); + addCards("Forest", 5, p); + addCard("Forest Bear", p); + addCard("Flying Men", opponent); + addCard("Runeclaw Bear", p); + addCard("Water Elemental", opponent); + addCard("Grizzly Bears", p); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + MultiTargetSelector.Targets targets = picker.getPlan().getSelectedDecision().targets; + AssertJUnit.assertEquals(3, targets.size()); + AssertJUnit.assertTrue(targets.toString().contains("Forest Bear")); + AssertJUnit.assertTrue(targets.toString().contains("Runeclaw Bear")); + AssertJUnit.assertTrue(targets.toString().contains("Grizzly Bear")); + // Expected 5*4*3=60 iterations (5 choices for first target, 4 for next, 3 for last.) + AssertJUnit.assertEquals(60, picker.getNumSimulations()); + } + + @Test + public void threeDistinctTargetSpellCantBeCast() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + + addCardToZone("Incremental Growth", p, ZoneType.Hand); + addCards("Forest", 5, p); + addCard("Forest Bear", p); + addCard("Flying Men", opponent); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNull(sa); + } + + @Test + public void correctTargetChoicesWithTwoTargetSpell() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + Player opponent = game.getPlayers().get(0); + + addCardToZone("Rites of Reaping", p, ZoneType.Hand); + addCard("Swamp", p); + addCards("Forest", 5, p); + addCard("Flying Men", opponent); + addCard("Forest Bear", p); + addCard("Water Elemental", opponent); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + SpellAbilityPicker picker = new SpellAbilityPicker(game, p); + SpellAbility sa = picker.chooseSpellAbilityToPlay(null); + AssertJUnit.assertNotNull(sa); + MultiTargetSelector.Targets targets = picker.getPlan().getSelectedDecision().targets; + AssertJUnit.assertEquals(2, targets.size()); + AssertJUnit.assertTrue(targets.toString().contains("Forest Bear")); + AssertJUnit.assertTrue(targets.toString().contains("Flying Men")); + } + */ +} From 1bb5217fdc4159b1d9e2fede94dcfb5f0ce5e87b Mon Sep 17 00:00:00 2001 From: marthinwurer Date: Sun, 28 Jul 2024 01:02:36 -0600 Subject: [PATCH 3/6] fixed karn and startup error --- forge-game/src/main/java/forge/game/phase/PhaseHandler.java | 2 +- forge-gui/res/languages/en-US.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 91dbf0c0a60..a1ec37d9777 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -1007,7 +1007,7 @@ public void discreteStartFirstTurn(Player goesFirst, Runnable startGameHook) { } public void mainGameLoop() { - while (!game.isGameOver()) { + while (!game.isGameOver() && !(game.getAge() == GameStage.RestartedByKarn)) { mainLoopStep(); } } diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index d49a3108fc6..03b563a9d38 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -160,7 +160,7 @@ nlManaLostPrompt=When enabled, you get a warning if passing priority would cause nlEnforceDeckLegality=Enforces deck legality relevant to each environment (minimum deck sizes, max card count etc). nlpAiSideboardingMode=Choose the way the AI sideboards: Off (the AI doesn't sideboard), AI (the AI sideboards for itself, currently mostly random), Human For AI (the human player sideboards for the AI in Constructed formats). nlExperimentalRestore=EXPERIMENTAL - Stores a snapshot to be used for undoing spells or abilities -nlExperimentalLoop=EXPERIMENTAL - Uses a new discrete main game loop implementation +nlExperimentalMainLoop=EXPERIMENTAL - Uses a new discrete main game loop implementation nlPerformanceMode=Disables additional static abilities checks to speed up the game engine. (Warning: breaks some ''as if had flash'' scenarios when casting cards owned by opponents). nlFilteredHands=Generates two starting hands and keeps the one with the closest to average land count for the deck. (REQUIRES RESTART) nlCloneImgSource=When enabled clones will use their original art instead of the cloned card''s art. From 994ba07b1d112f319dd90afe2b56f779fa73ca7b Mon Sep 17 00:00:00 2001 From: marthinwurer Date: Mon, 12 Aug 2024 22:17:39 -0600 Subject: [PATCH 4/6] new tests and a further broken out loop --- .../src/main/java/forge/StaticData.java | 4 + forge-core/src/main/java/forge/deck/Deck.java | 8 ++ .../java/forge/game/phase/PhaseHandler.java | 6 +- .../main/java/forge/view/SimulateMatch.java | 11 +- .../src/test/java/forge/GameFuzzingTest.java | 60 ++++++++ .../src/test/java/forge/ai/AITest.java | 83 +++++++---- .../src/test/java/forge/ai/CombatTests.java | 55 ++++++++ .../forge/ai/simulation/SimulationTest.java | 132 +----------------- 8 files changed, 200 insertions(+), 159 deletions(-) create mode 100644 forge-gui-desktop/src/test/java/forge/GameFuzzingTest.java create mode 100644 forge-gui-desktop/src/test/java/forge/ai/CombatTests.java diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java index d2d681d2eb5..6999818b653 100644 --- a/forge-core/src/main/java/forge/StaticData.java +++ b/forge-core/src/main/java/forge/StaticData.java @@ -388,6 +388,10 @@ public IStorage getPrintSheets() { return printSheets; } + /** + * Get a database of all non-variant cards + * @return + */ public CardDb getCommonCards() { return commonCards; } diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java index 8cbbcbcc3b9..1035b0c0623 100644 --- a/forge-core/src/main/java/forge/deck/Deck.java +++ b/forge-core/src/main/java/forge/deck/Deck.java @@ -69,6 +69,14 @@ public Deck(final String name0) { getOrCreate(DeckSection.Main); } + /** + * Create a new deck from a cardpool + */ + public Deck(final String name0, CardPool main) { + super(name0); + getOrCreate(DeckSection.Main).add(main.toFlatList()); + } + /** * Copy constructor. * diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index f6d1210deeb..61dcda1d1a8 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -988,8 +988,7 @@ public final boolean skippedDeclareBlockers() { private final static boolean DEBUG_PHASES = false; - public void discreteStartFirstTurn(Player goesFirst, Runnable startGameHook) { - + public void setupFirstTurn(Player goesFirst, Runnable startGameHook) { if (phase != null) { throw new IllegalStateException("Turns already started, call this only once per game"); } @@ -1005,7 +1004,10 @@ public void discreteStartFirstTurn(Player goesFirst, Runnable startGameHook) { startGameHook.run(); givePriorityToPlayer = true; } + } + public void discreteStartFirstTurn(Player goesFirst, Runnable startGameHook) { + setupFirstTurn(goesFirst, startGameHook); mainGameLoop(); } diff --git a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java index 4fd38e12023..25c65cf984a 100644 --- a/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java +++ b/forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java @@ -171,7 +171,7 @@ private static void argumentHelp() { System.out.println("\tq - Quiet flag. Output just the game result, not the entire game log."); } - public static void simulateSingleMatch(final Match mc, int iGame, boolean outputGamelog) { + public static Game simulateSingleGameOfMatch(final Match mc, int timeout) { final StopWatch sw = new StopWatch(); sw.start(); @@ -181,7 +181,7 @@ public static void simulateSingleMatch(final Match mc, int iGame, boolean output TimeLimitedCodeBlock.runWithTimeout(() -> { mc.startGame(g1); sw.stop(); - }, 120, TimeUnit.SECONDS); + }, timeout, TimeUnit.SECONDS); } catch (TimeoutException e) { System.out.println("Stopping slow match as draw"); } catch (Exception | StackOverflowError e) { @@ -195,6 +195,13 @@ public static void simulateSingleMatch(final Match mc, int iGame, boolean output } } + return g1; + } + + public static void simulateSingleMatch(final Match mc, int iGame, boolean outputGamelog) { + final StopWatch sw = new StopWatch(); + sw.start(); + final Game g1 = simulateSingleGameOfMatch(mc, 120); List log; if (outputGamelog) { log = g1.getGameLog().getLogEntries(null); diff --git a/forge-gui-desktop/src/test/java/forge/GameFuzzingTest.java b/forge-gui-desktop/src/test/java/forge/GameFuzzingTest.java new file mode 100644 index 00000000000..b5acf4470ba --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/GameFuzzingTest.java @@ -0,0 +1,60 @@ + +package forge; + +import com.google.common.base.Function; +import forge.ai.AIOption; +import forge.ai.LobbyPlayerAi; +import forge.card.CardDb; +import forge.deck.Deck; +import forge.deck.DeckFormat; +import forge.deck.generation.DeckGenerator5Color; +import forge.game.GameRules; +import forge.game.GameType; +import forge.game.Match; +import forge.game.player.RegisteredPlayer; +import forge.gui.GuiBase; +import forge.localinstance.properties.ForgePreferences; +import forge.model.FModel; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static forge.view.SimulateMatch.simulateSingleGameOfMatch; + +public class GameFuzzingTest { + @Ignore + @Test + public void PlayGameWithRandomDecks() { + GuiBase.setInterface(new GuiDesktop()); + FModel.initialize(null, new Function() { + @Override + public Void apply(ForgePreferences preferences) { + preferences.setPref(ForgePreferences.FPref.LOAD_CARD_SCRIPTS_LAZILY, false); + preferences.setPref(ForgePreferences.FPref.UI_LANGUAGE, "en-US"); + return null; + } + }); + + // first deck + CardDb cardDb = FModel.getMagicDb().getCommonCards(); + final DeckGenerator5Color gen = new DeckGenerator5Color(cardDb, DeckFormat.Constructed); + final Deck first_deck = new Deck("first", gen.getDeck(60, false)); + final Deck second_deck = new Deck("second", gen.getDeck(60, false)); + + final RegisteredPlayer p1 = new RegisteredPlayer(first_deck); + final RegisteredPlayer p2 = new RegisteredPlayer(second_deck); + + Set options = new HashSet<>(); + // options.add(AIOption.USE_SIMULATION); + p1.setPlayer(new LobbyPlayerAi("p1", options)); + p2.setPlayer(new LobbyPlayerAi("p2", options)); + GameRules rules = new GameRules(GameType.Constructed); + // need game rules, players, and title + Match m = new Match(rules, Arrays.asList(p1, p2), "test"); + + simulateSingleGameOfMatch(m, 120); + } +} \ No newline at end of file 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 d9c7e4efad4..3e4c3508df6 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/AITest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/AITest.java @@ -1,5 +1,6 @@ package forge.ai; +import java.util.ArrayList; import java.util.List; import com.google.common.base.Function; @@ -15,6 +16,7 @@ import forge.game.Match; import forge.game.card.Card; import forge.game.card.CardCollectionView; +import forge.game.card.CardFactory; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.RegisteredPlayer; @@ -22,6 +24,7 @@ import forge.game.zone.ZoneType; import forge.gui.GuiBase; import forge.item.IPaperCard; +import forge.item.PaperToken; import forge.localinstance.properties.ForgePreferences; import forge.localinstance.properties.ForgePreferences.FPref; import forge.model.FModel; @@ -50,13 +53,10 @@ public Game resetGame() { protected Game initAndCreateGame() { if (!initialized) { GuiBase.setInterface(new GuiDesktop()); - FModel.initialize(null, new Function() { - @Override - public Void apply(ForgePreferences preferences) { - preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false); - preferences.setPref(FPref.UI_LANGUAGE, "en-US"); - return null; - } + FModel.initialize(null, preferences -> { + preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false); + preferences.setPref(FPref.UI_LANGUAGE, "en-US"); + return null; }); initialized = true; } @@ -83,23 +83,6 @@ protected Card findCardWithName(Game game, String name) { return null; } - protected String gameStateToString(Game game) { - StringBuilder sb = new StringBuilder(); - for (ZoneType zone : ZoneType.values()) { - CardCollectionView cards = game.getCardsIn(zone); - if (!cards.isEmpty()) { - sb.append("Zone ").append(zone.name()).append(":\n"); - for (Card c : game.getCardsIn(zone)) { - sb.append(" ").append(c); - if (c.isTapped()) { - sb.append(" (T)"); - } - sb.append("\n"); - } - } - } - return sb.toString(); - } protected SpellAbility findSAWithPrefix(Card c, String prefix) { return findSAWithPrefix(c.getSpellAbilities(), prefix); @@ -143,10 +126,60 @@ protected List addCards(String name, int count, Player p) { return cards; } + protected Card createToken(String name, Player p) { + PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name); + if (token == null) { + System.out.println("Failed to find token name " + name); + return null; + } + return CardFactory.getCard(token, p, p.getGame()); + } + + protected List addTokens(String name, int amount, Player p) { + List cards = new ArrayList<>(); + + for(int i = 0; i < amount; i++) { + cards.add(addToken(name, p)); + } + + return cards; + } + + protected Card addToken(String name, Player p) { + Card c = createToken(name, p); + // card need a new Timestamp otherwise Static Abilities might collide + c.setGameTimestamp(p.getGame().getNextTimestamp()); + p.getZone(ZoneType.Battlefield).add(c); + return c; + } + void playUntilStackClear(Game game) { do { game.getPhaseHandler().mainLoopStep(); } while (!game.isGameOver() && !game.getStack().isEmpty()); - return; + } + + void playUntilPhase(Game game, PhaseType phase) { + do { + game.getPhaseHandler().mainLoopStep(); + } while (!game.isGameOver() && !game.getPhaseHandler().is(phase)); + } + + protected String gameStateToString(Game game) { + StringBuilder sb = new StringBuilder(); + for (ZoneType zone : ZoneType.values()) { + CardCollectionView cards = game.getCardsIn(zone); + if (!cards.isEmpty()) { + sb.append("Zone ").append(zone.name()).append(":\n"); + for (Card c : game.getCardsIn(zone)) { + sb.append(" ").append(c); + if (c.isTapped()) { + sb.append(" (T)"); + } + sb.append("\n"); + } + } + } + return sb.toString(); } } diff --git a/forge-gui-desktop/src/test/java/forge/ai/CombatTests.java b/forge-gui-desktop/src/test/java/forge/ai/CombatTests.java new file mode 100644 index 00000000000..11ef9a45021 --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/ai/CombatTests.java @@ -0,0 +1,55 @@ +package forge.ai; + +import forge.game.Game; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import org.testng.AssertJUnit; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +public class CombatTests extends AITest { + + @Test + public void testSwingForLethal() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + p.setTeam(0); + addCard("Nest Robber", p); + addCard("Nest Robber", p); + + Player opponent = game.getPlayers().get(0); + opponent.setTeam(1); + + addCard("Runeclaw Bear", opponent); + opponent.setLife(2, null); + + this.playUntilPhase(game, PhaseType.END_OF_TURN); + + AssertJUnit.assertTrue(game.isGameOver()); + } + + @Ignore + @Test + public void testClearForLethal() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(1); + p.setTeam(0); + addCard("Brazen Scourge", p); + addCard("Brazen Scourge", p); + addCard("Mountain", p); + addCardToZone("Shock", p, ZoneType.Hand); + + Player opponent = game.getPlayers().get(0); + opponent.setTeam(1); + + addCard("Runeclaw Bear", opponent); + opponent.setLife(6, null); + + this.playUntilPhase(game, PhaseType.END_OF_TURN); + System.out.println(this.gameStateToString(game)); + + AssertJUnit.assertTrue(game.isGameOver()); + } + +} diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java index 8f8e41cc47a..2c57d3f738a 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/SimulationTest.java @@ -1,15 +1,13 @@ package forge.ai.simulation; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import com.google.common.collect.Lists; -import forge.GuiDesktop; -import forge.StaticData; import forge.ai.AIOption; +import forge.ai.AITest; import forge.ai.LobbyPlayerAi; import forge.ai.simulation.GameStateEvaluator.Score; import forge.deck.Deck; @@ -18,21 +16,10 @@ import forge.game.GameStage; import forge.game.GameType; import forge.game.Match; -import forge.game.card.Card; -import forge.game.card.CardCollectionView; -import forge.game.card.CardFactory; import forge.game.player.Player; import forge.game.player.RegisteredPlayer; -import forge.game.spellability.SpellAbility; -import forge.game.zone.ZoneType; -import forge.gui.GuiBase; -import forge.item.IPaperCard; -import forge.item.PaperToken; -import forge.localinstance.properties.ForgePreferences.FPref; -import forge.model.FModel; -public class SimulationTest { - private static boolean initialized = false; +public class SimulationTest extends AITest { public Game resetGame() { // need to be done after FModel.initialize, or the Localizer isn't loaded yet @@ -51,19 +38,6 @@ public Game resetGame() { return game; } - protected Game initAndCreateGame() { - if (!initialized) { - GuiBase.setInterface(new GuiDesktop()); - FModel.initialize(null, preferences -> { - preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false); - preferences.setPref(FPref.UI_LANGUAGE, "en-US"); - return null; - }); - initialized = true; - } - - return resetGame(); - } protected GameSimulator createSimulator(Game game, Player p) { return new GameSimulator(new SimulationController(new Score(0)) { @@ -73,106 +47,4 @@ public boolean shouldRecurse() { } }, game, p, null); } - - protected int countCardsWithName(Game game, String name) { - int i = 0; - for (Card c : game.getCardsIn(ZoneType.Battlefield)) { - if (c.getName().equals(name)) { - i++; - } - } - return i; - } - - protected Card findCardWithName(Game game, String name) { - for (Card c : game.getCardsIn(ZoneType.Battlefield)) { - if (c.getName().equals(name)) { - return c; - } - } - return null; - } - - protected String gameStateToString(Game game) { - StringBuilder sb = new StringBuilder(); - for (ZoneType zone : ZoneType.values()) { - CardCollectionView cards = game.getCardsIn(zone); - if (!cards.isEmpty()) { - sb.append("Zone ").append(zone.name()).append(":\n"); - for (Card c : game.getCardsIn(zone)) { - sb.append(" ").append(c).append("\n"); - } - } - } - return sb.toString(); - } - - protected SpellAbility findSAWithPrefix(Card c, String prefix) { - return findSAWithPrefix(c.getSpellAbilities(), prefix); - } - - protected SpellAbility findSAWithPrefix(Iterable abilities, String prefix) { - for (SpellAbility sa : abilities) { - if (sa.getDescription().startsWith(prefix)) { - return sa; - } - } - return null; - } - - protected Card createCard(String name, Player p) { - IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name); - if (paperCard == null) { - StaticData.instance().attemptToLoadCard(name); - paperCard = FModel.getMagicDb().getCommonCards().getCard(name); - } - return Card.fromPaperCard(paperCard, p); - } - - protected Card addCardToZone(String name, Player p, ZoneType zone) { - Card c = createCard(name, p); - // card need a new Timestamp otherwise Static Abilities might collide - c.setGameTimestamp(p.getGame().getNextTimestamp()); - p.getZone(zone).add(c); - return c; - } - - protected Card addCard(String name, Player p) { - return addCardToZone(name, p, ZoneType.Battlefield); - } - - protected List addCards(String name, int count, Player p) { - List cards = Lists.newArrayList(); - for (int i = 0; i < count; i++) { - cards.add(addCard(name, p)); - } - return cards; - } - - protected Card createToken(String name, Player p) { - PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name); - if (token == null) { - System.out.println("Failed to find token name " + name); - return null; - } - return CardFactory.getCard(token, p, p.getGame()); - } - - protected List addTokens(String name, int amount, Player p) { - List cards = new ArrayList<>(); - - for(int i = 0; i < amount; i++) { - cards.add(addToken(name, p)); - } - - return cards; - } - - protected Card addToken(String name, Player p) { - Card c = createToken(name, p); - // card need a new Timestamp otherwise Static Abilities might collide - c.setGameTimestamp(p.getGame().getNextTimestamp()); - p.getZone(ZoneType.Battlefield).add(c); - return c; - } } From 4c256f5cfc85205a2123c5ab67239c5dcc6bdae0 Mon Sep 17 00:00:00 2001 From: marthinwurer Date: Mon, 12 Aug 2024 22:34:56 -0600 Subject: [PATCH 5/6] cleanup --- forge-gui-desktop/src/test/java/forge/ai/AITest.java | 2 -- 1 file changed, 2 deletions(-) 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 3e4c3508df6..d087a06d195 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/AITest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/AITest.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; -import com.google.common.base.Function; import com.google.common.collect.Lists; import forge.GuiDesktop; @@ -25,7 +24,6 @@ import forge.gui.GuiBase; import forge.item.IPaperCard; import forge.item.PaperToken; -import forge.localinstance.properties.ForgePreferences; import forge.localinstance.properties.ForgePreferences.FPref; import forge.model.FModel; From 9bd9badc21883f94a050e2a0d80a09ba20d9b78a Mon Sep 17 00:00:00 2001 From: marthinwurer Date: Sun, 18 Aug 2024 15:57:27 -0600 Subject: [PATCH 6/6] updated combat test --- .../src/test/java/forge/ai/CombatTests.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/forge-gui-desktop/src/test/java/forge/ai/CombatTests.java b/forge-gui-desktop/src/test/java/forge/ai/CombatTests.java index 11ef9a45021..27e3a087d94 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/CombatTests.java +++ b/forge-gui-desktop/src/test/java/forge/ai/CombatTests.java @@ -1,6 +1,7 @@ package forge.ai; import forge.game.Game; +import forge.game.card.Card; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.zone.ZoneType; @@ -38,18 +39,18 @@ public void testClearForLethal() { addCard("Brazen Scourge", p); addCard("Brazen Scourge", p); addCard("Mountain", p); - addCardToZone("Shock", p, ZoneType.Hand); + addCardToZone("Pillar of Flame", p, ZoneType.Hand); Player opponent = game.getPlayers().get(0); opponent.setTeam(1); - addCard("Runeclaw Bear", opponent); + Card bearCard = addCard("Runeclaw Bear", opponent); opponent.setLife(6, null); - this.playUntilPhase(game, PhaseType.END_OF_TURN); + this.playUntilStackClear(game); System.out.println(this.gameStateToString(game)); - AssertJUnit.assertTrue(game.isGameOver()); + AssertJUnit.assertEquals(bearCard.getZone().getZoneType(), ZoneType.Exile); } }