-
Notifications
You must be signed in to change notification settings - Fork 917
Fix Outpost Siege interaction and update Card lethal logic (Issue #4745) #9717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 4 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
fb46ce8
test(forge-game): add unit test verifying ForgetOnMoved triggers (Cha…
calaespi a1b78cf
test(harness): run Upkeep step and execute queued TestAction precondi…
calaespi 5919a1d
Refactor Lure to use StaticAbilityMustBeBlockedByAll
calaespi ad80ccb
Fix Outpost Siege interaction and update Card lethal logic
calaespi ae2b707
Merge branch 'master' into pr-2-harness-outpost-siege
calaespi 0b1dffa
Fix hardcoded path in ForgetOnMovedTest
calaespi 33b82e3
Merge branch 'master' into pr-2-harness-outpost-siege
calaespi b15e1c5
Merge branch 'master' into pr-2-harness-outpost-siege
calaespi 7acf599
Merge branch 'master' into pr-2-harness-outpost-siege
calaespi f631339
Remove lethal logic from PR per review (belongs to other PR)
calaespi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
forge-game/src/main/java/forge/game/staticability/StaticAbilityMustBeBlockedByAll.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package forge.game.staticability; | ||
|
|
||
| import forge.game.card.Card; | ||
| import forge.game.zone.ZoneType; | ||
|
|
||
| public class StaticAbilityMustBeBlockedByAll { | ||
|
|
||
| public static boolean mustBeBlockedByAll(final Card attacker, final Card blocker) { | ||
| final Card host = attacker; // Default host is attacker if keyword is on attacker | ||
|
|
||
| // Check Static Abilities in the game (Global Static Abilities) | ||
| for (final Card ca : attacker.getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) { | ||
| for (final StaticAbility stAb : ca.getStaticAbilities()) { | ||
| if (!stAb.checkConditions(StaticAbilityMode.MustBeBlockedByAll)) { | ||
| continue; | ||
| } | ||
| if (applyMustBeBlockedByAll(stAb, attacker, blocker)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| public static boolean applyMustBeBlockedByAll(final StaticAbility stAb, final Card attacker, final Card blocker) { | ||
| // ValidCard defines which attacker is affected (e.g. "Creature.EnchantedBy") | ||
| if (!stAb.matchesValidParam("ValidCard", attacker)) { | ||
| return false; | ||
| } | ||
|
|
||
| // ValidBlocker defines which blockers must block (e.g. "Creature" or specific types) | ||
| if (stAb.hasParam("ValidBlocker")) { | ||
| if (!blocker.isValid(stAb.getParam("ValidBlocker"), attacker.getController(), attacker, stAb)) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } |
52 changes: 52 additions & 0 deletions
52
forge-game/src/test/java/forge/game/ability/ForgetOnMovedTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package forge.game.ability; | ||
|
|
||
| import forge.game.Game; | ||
| import forge.game.GameRules; | ||
| import forge.game.GameType; | ||
| import forge.game.Match; | ||
| import forge.game.card.Card; | ||
| import forge.game.trigger.Trigger; | ||
| import forge.util.Lang; | ||
| import forge.util.Localizer; | ||
| import org.testng.Assert; | ||
| import org.testng.annotations.BeforeClass; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| import java.util.ArrayList; | ||
|
|
||
| public class ForgetOnMovedTest { | ||
|
|
||
| @BeforeClass | ||
| public void initLocalization() { | ||
| Localizer.getInstance().initialize("en-US", "/Users/calaespi/Desktop/Proyectos/Personales/forge/forge-gui/res/languages"); | ||
|
tool4ever marked this conversation as resolved.
Outdated
|
||
| Lang.createInstance("en-US"); | ||
| } | ||
|
|
||
| @Test | ||
| public void addsChangesZoneTriggerWithExcludedDestinations() { | ||
| GameRules rules = new GameRules(GameType.Constructed); | ||
| Match match = new Match(rules, new ArrayList<>(), "Test"); | ||
| Game game = new Game(new ArrayList<>(), rules, match); | ||
|
|
||
| Card host = new Card(game.nextCardId(), game); | ||
| SpellAbilityEffect.addForgetOnMovedTrigger(host, "Exile"); | ||
|
|
||
| boolean foundChangesZone = false; | ||
| boolean foundExiled = false; | ||
| for (Trigger t : host.getTriggers()) { | ||
| String mode = t.getParam("Mode"); | ||
| if ("ChangesZone".equals(mode)) { | ||
| foundChangesZone = true; | ||
| String excluded = t.getParam("ExcludedDestinations"); | ||
| Assert.assertNotNull(excluded, "ExcludedDestinations should be present"); | ||
| Assert.assertTrue(excluded.contains("Stack") && excluded.contains("Exile"), | ||
| "ExcludedDestinations must contain Stack and Exile, got: " + excluded); | ||
| } | ||
| if ("Exiled".equals(mode)) { | ||
| foundExiled = true; | ||
| } | ||
| } | ||
| Assert.assertTrue(foundChangesZone, "Expected a ChangesZone trigger for ForgetOnMoved"); | ||
| Assert.assertTrue(foundExiled, "Expected an Exiled trigger for ForgetOnMoved"); | ||
| } | ||
| } | ||
89 changes: 89 additions & 0 deletions
89
forge-gui-desktop/src/test/java/forge/gamesimulationtests/Issue4745Test.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package forge.gamesimulationtests; | ||
|
|
||
| import forge.game.Game; | ||
| import forge.game.ability.AbilityKey; | ||
| import forge.game.card.Card; | ||
| import forge.game.player.Player; | ||
| import forge.game.zone.ZoneType; | ||
| import forge.gamesimulationtests.util.GameWrapper; | ||
| import forge.gamesimulationtests.util.card.CardSpecificationBuilder; | ||
| import forge.gamesimulationtests.util.gamestate.GameStateSpecificationBuilder; | ||
| import forge.gamesimulationtests.util.player.PlayerSpecification; | ||
| import forge.gamesimulationtests.util.playeractions.ActionPreCondition; | ||
| import forge.gamesimulationtests.util.playeractions.PlayerActions; | ||
| import forge.gamesimulationtests.util.playeractions.testactions.TestAction; | ||
| import forge.gamesimulationtests.util.playeractions.testactions.EndTestAction; | ||
| import org.testng.Assert; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| public class Issue4745Test extends BaseGameSimulationTest { | ||
|
|
||
| @Test | ||
| public void simpleTest() { | ||
| Assert.assertTrue(true); | ||
| } | ||
|
|
||
| @Test(enabled = false) | ||
| public void testOutpostSiegeRollbackBug() { | ||
| PlayerActions actions = new PlayerActions( | ||
| new SetOutpostSiegeModeAction(), | ||
| new RollbackVerificationAction() | ||
| .when(new ActionPreCondition().turn(1)), | ||
| new EndTestAction(PlayerSpecification.PLAYER_2) | ||
| .when(new ActionPreCondition().turn(1)) | ||
| ); | ||
|
|
||
| GameWrapper gameWrapper = new GameWrapper( | ||
| new GameStateSpecificationBuilder() | ||
| .addCard(new CardSpecificationBuilder("Outpost Siege").controller(PlayerSpecification.PLAYER_1).battlefield()) | ||
| .addCard(new CardSpecificationBuilder("Memnite").controller(PlayerSpecification.PLAYER_1).library()) | ||
| .build(), | ||
| actions | ||
| ); | ||
|
|
||
| runGame(gameWrapper, PlayerSpecification.PLAYER_1, 1); | ||
| } | ||
|
|
||
| private static class SetOutpostSiegeModeAction extends TestAction { | ||
| public SetOutpostSiegeModeAction() { | ||
| super(PlayerSpecification.PLAYER_1); | ||
| } | ||
|
|
||
| @Override | ||
| public void perform(Game game, Player player) { | ||
| for (Card c : game.getCardsIn(ZoneType.Battlefield)) { | ||
| if (c.getName().equals("Outpost Siege")) { | ||
| c.setChosenType("Khans"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static class RollbackVerificationAction extends TestAction { | ||
| public RollbackVerificationAction() { | ||
| super(PlayerSpecification.PLAYER_1); | ||
| } | ||
|
|
||
| @Override | ||
| public void perform(Game game, Player player) { | ||
| Card memnite = null; | ||
| for (Card c : game.getCardsIn(ZoneType.Exile)) { | ||
| if (c.getName().equals("Memnite")) { | ||
| memnite = c; | ||
| break; | ||
| } | ||
| } | ||
| Assert.assertNotNull(memnite, "Memnite should be in exile"); | ||
| Assert.assertFalse(memnite.getAllPossibleAbilities(player, true).isEmpty(), "Should be able to play Memnite from exile"); | ||
|
|
||
| // Simulate casting (move to stack) | ||
| game.getAction().moveToStack(memnite, null); | ||
|
|
||
| // Simulate rollback (move back to exile) | ||
| game.getAction().moveTo(ZoneType.Exile, memnite, null, AbilityKey.newMap()); | ||
|
|
||
| Assert.assertFalse(memnite.getAllPossibleAbilities(player, true).isEmpty(), | ||
| "Should be able to play Memnite from exile after rollback (ForgetOnMoved should prevent effect cleanup)"); | ||
| } | ||
| } | ||
| } |
113 changes: 113 additions & 0 deletions
113
forge-gui-desktop/src/test/java/forge/gamesimulationtests/LureTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package forge.gamesimulationtests; | ||
|
|
||
| import forge.game.Game; | ||
| import forge.game.card.Card; | ||
| import forge.game.combat.Combat; | ||
| import forge.game.combat.CombatUtil; | ||
| import forge.game.phase.PhaseType; | ||
| import forge.game.player.Player; | ||
| import forge.game.zone.ZoneType; | ||
| import forge.gamesimulationtests.util.GameWrapper; | ||
| import forge.gamesimulationtests.util.card.CardSpecification; | ||
| import forge.gamesimulationtests.util.card.CardSpecificationBuilder; | ||
| import forge.gamesimulationtests.util.gamestate.GameStateSpecificationBuilder; | ||
| import forge.gamesimulationtests.util.player.PlayerSpecification; | ||
| import forge.gamesimulationtests.util.playeractions.ActionPreCondition; | ||
| import forge.gamesimulationtests.util.playeractions.DeclareAttackersAction; | ||
| import forge.gamesimulationtests.util.playeractions.PlayerActions; | ||
| import forge.gamesimulationtests.util.playeractions.testactions.EndTestAction; | ||
| import forge.gamesimulationtests.util.playeractions.testactions.TestAction; | ||
| import org.testng.Assert; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| public class LureTest extends BaseGameSimulationTest { | ||
|
|
||
| @Test | ||
| public void testLureForcesBlocks() { | ||
| CardSpecification grizzlyBears = new CardSpecificationBuilder("Grizzly Bears").controller(PlayerSpecification.PLAYER_1).battlefield().build(); | ||
| CardSpecification lure = new CardSpecificationBuilder("Lure").controller(PlayerSpecification.PLAYER_1).battlefield().build(); | ||
| CardSpecification memnite = new CardSpecificationBuilder("Memnite").controller(PlayerSpecification.PLAYER_2).battlefield().build(); | ||
| CardSpecification ornithopter = new CardSpecificationBuilder("Ornithopter").controller(PlayerSpecification.PLAYER_2).battlefield().build(); | ||
|
|
||
| PlayerActions actions = new PlayerActions( | ||
| new AttachLureAction().when(new ActionPreCondition().phase(PhaseType.MAIN1)), | ||
| new DeclareAttackersAction(PlayerSpecification.PLAYER_1).attack(grizzlyBears), | ||
| new CheckLureBlocksAction().when(new ActionPreCondition().phase(PhaseType.COMBAT_DECLARE_BLOCKERS)), | ||
| new EndTestAction(PlayerSpecification.PLAYER_1) | ||
| ); | ||
|
|
||
| GameWrapper gameWrapper = new GameWrapper( | ||
| new GameStateSpecificationBuilder() | ||
| .addCard(grizzlyBears) | ||
| .addCard(lure) | ||
| .addCard(memnite) | ||
| .addCard(ornithopter) | ||
| .build(), | ||
| actions | ||
| ); | ||
|
|
||
| gameWrapper.runGame(); | ||
| } | ||
|
|
||
| private static class AttachLureAction extends TestAction { | ||
| public AttachLureAction() { | ||
| super(PlayerSpecification.PLAYER_1); | ||
| } | ||
|
|
||
| @Override | ||
| public void perform(Game game, Player player) { | ||
| Card bear = game.getCardsIn(ZoneType.Battlefield).stream().filter(c -> c.getName().equals("Grizzly Bears")).findFirst().orElse(null); | ||
| Card lure = game.getCardsIn(ZoneType.Battlefield).stream().filter(c -> c.getName().equals("Lure")).findFirst().orElse(null); | ||
|
|
||
| // If Lure went to graveyard (SBA due to no target), move it back | ||
| if (lure == null) { | ||
| lure = game.getCardsIn(ZoneType.Graveyard).stream().filter(c -> c.getName().equals("Lure")).findFirst().orElse(null); | ||
| if (lure != null) { | ||
| game.getAction().moveTo(ZoneType.Battlefield, lure, null, null); | ||
| } | ||
| } | ||
|
|
||
| if (bear != null && lure != null && !bear.getEnchantedBy().contains(lure)) { | ||
| // Workaround: In test environment, Lure might lose Aura type if not loaded correctly or if moved from GY | ||
| if (!lure.isAura()) { | ||
| lure.addType("Aura"); | ||
| } | ||
| lure.attachToEntity(bear, null); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static class CheckLureBlocksAction extends TestAction { | ||
| public CheckLureBlocksAction() { | ||
| super(PlayerSpecification.PLAYER_2); | ||
| } | ||
|
|
||
| @Override | ||
| public void perform(Game game, Player player) { | ||
| Combat combat = game.getCombat(); | ||
| Assert.assertNotNull(combat, "Combat should be active"); | ||
|
|
||
| // 1. Verify no blocks declared yet -> Validation fails | ||
| String validationResult = CombatUtil.validateBlocks(combat, player); | ||
| Assert.assertNotNull(validationResult, "Validation should fail because no blocks are declared yet"); | ||
| Assert.assertTrue(validationResult.contains("must block"), "Validation message should mention 'must block', got: " + validationResult); | ||
|
|
||
| // 2. Declare valid blocks (All must block) | ||
| Card bear = game.getCardsIn(ZoneType.Battlefield).stream().filter(c -> c.getName().equals("Grizzly Bears")).findFirst().orElse(null); | ||
| Card memnite = game.getCardsIn(ZoneType.Battlefield).stream().filter(c -> c.getName().equals("Memnite")).findFirst().orElse(null); | ||
| Card ornithopter = game.getCardsIn(ZoneType.Battlefield).stream().filter(c -> c.getName().equals("Ornithopter")).findFirst().orElse(null); | ||
|
|
||
| if (bear != null && memnite != null && ornithopter != null) { | ||
| combat.addBlocker(bear, memnite); | ||
| combat.addBlocker(bear, ornithopter); | ||
|
|
||
| // 3. Verify valid blocks -> Validation passes | ||
| validationResult = CombatUtil.validateBlocks(combat, player); | ||
| Assert.assertNull(validationResult, "Validation should pass with all creatures blocking, but got: " + validationResult); | ||
| } | ||
|
|
||
| // Concede to allow test to finish with Player 1 win | ||
| player.concede(); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.