From 3c6ede74070953997832d06e3858140f5c6d6d42 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Mon, 5 Jun 2017 12:27:59 +0200 Subject: [PATCH] * Improved game is infinite loop check to ask players only if some kind of iteration is recognized. --- .../mage/test/game/ends/GameIsADrawTest.java | 274 ++++++++++-------- Mage/src/main/java/mage/game/GameImpl.java | 59 ++-- 2 files changed, 185 insertions(+), 148 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/game/ends/GameIsADrawTest.java b/Mage.Tests/src/test/java/org/mage/test/game/ends/GameIsADrawTest.java index 4660ff92c8..5ffde49c85 100644 --- a/Mage.Tests/src/test/java/org/mage/test/game/ends/GameIsADrawTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/game/ends/GameIsADrawTest.java @@ -1,122 +1,152 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.mage.test.game.ends; - -import mage.constants.PhaseStep; -import mage.constants.Zone; -import mage.game.permanent.Permanent; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestPlayerBase; - -/** - * - * @author LevelX2 - */ -public class GameIsADrawTest extends CardTestPlayerBase { - - @Test - public void GameDrawByDamage() { - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); - // Flame Rift deals 4 damage to each player. - addCard(Zone.HAND, playerA, "Flame Rift", 3); // Sorcery {1}{R} - - addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); - // Flame Rift deals 4 damage to each player. - addCard(Zone.HAND, playerB, "Flame Rift", 2); // Sorcery - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flame Rift"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flame Rift"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flame Rift"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Rift"); - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Rift"); - - setStopAt(2, PhaseStep.BEGIN_COMBAT); - execute(); - - assertLife(playerA, 0); - assertLife(playerB, 0); - - Assert.assertFalse("Player A has not won.", playerA.hasWon()); - Assert.assertFalse("Player B has not won.", playerB.hasWon()); - - Assert.assertTrue("Game has ended.", currentGame.hasEnded()); - - Assert.assertTrue("Both players had 0 life, game has be de a draw.", currentGame.isADraw()); - - } - - @Test - public void GameDrawByDivineIntervention() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 8); - // Divine Intervention enters the battlefield with two intervention counters on it. - // At the beginning of your upkeep, remove an intervention counter from Divine Intervention. - // When you remove the last intervention counter from Divine Intervention, the game is a draw. - addCard(Zone.HAND, playerA, "Divine Intervention", 1); // Enchantment {6}{W}{W} - - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); - addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Divine Intervention"); - - setStopAt(5, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertLife(playerA, 20); - assertLife(playerB, 20); - - Assert.assertFalse("Player A has not won.", playerA.hasWon()); - Assert.assertFalse("Player B has not won.", playerB.hasWon()); - - Assert.assertTrue("Game has ended.", currentGame.hasEnded()); - - Assert.assertTrue("Both players had 0 life, game has be de a draw.", currentGame.isADraw()); - - } - - /** - * So here I made a simple infinite loop with Stuffy Doll and Pariah's - * Shield, which should make the game a draw. But instead, it just keeps - * going... - */ - @Test - public void GameDrawByInfiniteLoop() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); - - // All damage that would be dealt to you is dealt to equipped creature instead. - // Equip {3} - addCard(Zone.BATTLEFIELD, playerA, "Pariah's Shield", 1); // Artifact Equipment {5} - - // As Stuffy Doll enters the battlefield, choose a player. - // Stuffy Doll is indestructible. - // Whenever Stuffy Doll is dealt damage, it deals that much damage to the chosen player. - // {T}: Stuffy Doll deals 1 damage to itself. - addCard(Zone.HAND, playerA, "Stuffy Doll", 1); // Artifact Creature {5} 0/1 - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stuffy Doll"); - setChoice(playerA, "PlayerA"); - setChoice(playerA, "PlayerA"); - - activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Stuffy Doll"); - activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}"); - setStopAt(3, PhaseStep.END_TURN); - execute(); - - assertPermanentCount(playerA, "Stuffy Doll", 1); - Permanent shield = getPermanent("Pariah's Shield"); - Assert.assertTrue("Pariah's Shield is attached", shield.getAttachedTo() != null); - - Assert.assertFalse("Player A has not won.", playerA.hasWon()); - Assert.assertFalse("Player B has not won.", playerB.hasWon()); - - Assert.assertTrue("Game has ended.", currentGame.hasEnded()); - - Assert.assertTrue("Inifinite loop detected, game has be de a draw.", currentGame.isADraw()); - - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.game.ends; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * + * @author LevelX2 + */ +public class GameIsADrawTest extends CardTestPlayerBase { + + @Test + public void GameDrawByDamage() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + // Flame Rift deals 4 damage to each player. + addCard(Zone.HAND, playerA, "Flame Rift", 3); // Sorcery {1}{R} + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); + // Flame Rift deals 4 damage to each player. + addCard(Zone.HAND, playerB, "Flame Rift", 2); // Sorcery + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flame Rift"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flame Rift"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flame Rift"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Rift"); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Rift"); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 0); + assertLife(playerB, 0); + + Assert.assertFalse("Player A has not won.", playerA.hasWon()); + Assert.assertFalse("Player B has not won.", playerB.hasWon()); + + Assert.assertTrue("Game has ended.", currentGame.hasEnded()); + + Assert.assertTrue("Both players had 0 life, game has be de a draw.", currentGame.isADraw()); + + } + + @Test + public void GameDrawByDivineIntervention() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 8); + // Divine Intervention enters the battlefield with two intervention counters on it. + // At the beginning of your upkeep, remove an intervention counter from Divine Intervention. + // When you remove the last intervention counter from Divine Intervention, the game is a draw. + addCard(Zone.HAND, playerA, "Divine Intervention", 1); // Enchantment {6}{W}{W} + + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); + addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Divine Intervention"); + + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + Assert.assertFalse("Player A has not won.", playerA.hasWon()); + Assert.assertFalse("Player B has not won.", playerB.hasWon()); + + Assert.assertTrue("Game has ended.", currentGame.hasEnded()); + + Assert.assertTrue("Both players had 0 life, game has be de a draw.", currentGame.isADraw()); + + } + + /** + * So here I made a simple infinite loop with Stuffy Doll and Pariah's + * Shield, which should make the game a draw. But instead, it just keeps + * going... + */ + @Test + public void GameDrawByInfiniteLoop() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + + // All damage that would be dealt to you is dealt to equipped creature instead. + // Equip {3} + addCard(Zone.BATTLEFIELD, playerA, "Pariah's Shield", 1); // Artifact Equipment {5} + + // As Stuffy Doll enters the battlefield, choose a player. + // Stuffy Doll is indestructible. + // Whenever Stuffy Doll is dealt damage, it deals that much damage to the chosen player. + // {T}: Stuffy Doll deals 1 damage to itself. + addCard(Zone.HAND, playerA, "Stuffy Doll", 1); // Artifact Creature {5} 0/1 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stuffy Doll"); + setChoice(playerA, "PlayerA"); + setChoice(playerA, "PlayerA"); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Stuffy Doll"); + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}"); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Stuffy Doll", 1); + Permanent shield = getPermanent("Pariah's Shield"); + Assert.assertTrue("Pariah's Shield is attached", shield.getAttachedTo() != null); + + Assert.assertFalse("Player A has not won.", playerA.hasWon()); + Assert.assertFalse("Player B has not won.", playerB.hasWon()); + + Assert.assertTrue("Game has ended.", currentGame.hasEnded()); + + Assert.assertTrue("Inifinite loop detected, game has be de a draw.", currentGame.isADraw()); + + } + + /** + * Check that a simple triggered ability does not trigger the infinite loop + * request to players + */ + @Test + public void GameDrawByInfiniteLoopNot() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 43); + + // Whenever a creature enters the battlefield under your control, you gain life equal to its toughness. + addCard(Zone.BATTLEFIELD, playerA, "Angelic Chorus", 1); // Enchantment {5} + + // Create X 4/4 white Angel creature tokens with flying. + // Miracle (You may cast this card for its miracle cost when you draw it if it's the first card you drew this turn.) + addCard(Zone.HAND, playerA, "Entreat the Angels", 1); // Sorcery {X}{X}{W}{W}{W} + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Entreat the Angels"); + + setChoice(playerA, "X=20"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Angel", 20); + Assert.assertFalse("Game should not have ended.", currentGame.hasEnded()); + assertLife(playerA, 100); + + Assert.assertFalse("No inifinite loop detected, game has be no draw.", currentGame.isADraw()); + + } + +} diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index a7fe78783e..9c90a1466c 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -177,6 +177,8 @@ public abstract class GameImpl implements Game, Serializable { protected PlayerList playerList; private int infiniteLoopCounter; // used to check if the game is in an infinite loop + private int lastNumberOfAbilitiesOnTheStack; // used to check how long no new ability was put to stack + private final LinkedList stackObjectsCheck = new LinkedList<>(); // used to check if different sources used the stack // used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist) protected Map enterWithCounters = new HashMap<>(); @@ -1289,7 +1291,6 @@ public abstract class GameImpl implements Game, Serializable { } if (allPassed()) { if (!state.getStack().isEmpty()) { - checkInfiniteLoop(); //20091005 - 115.4 resolve(); applyEffects(); @@ -1298,7 +1299,6 @@ public abstract class GameImpl implements Game, Serializable { resetShortLivingLKI(); break; } else { - infiniteLoopCounter = 0; resetLKI(); return; } @@ -1346,6 +1346,7 @@ public abstract class GameImpl implements Game, Serializable { if (top != null) { state.getStack().remove(top); // seems partly redundant because move card from stack to grave is already done and the stack removed rememberLKI(top.getSourceId(), Zone.STACK, top); + checkInfiniteLoop(top.getSourceId()); if (!getTurn().isEndTurnRequested()) { while (state.hasSimultaneousEvents()) { state.handleSimultaneousEvent(this); @@ -1357,40 +1358,44 @@ public abstract class GameImpl implements Game, Serializable { /** * This checks if the stack gets filled iterated, without ever getting empty - * If the defined number of iterations is reached, the players in range of - * the stackObject get asked to confirm a draw. If they do, all confirming - * players get set to a draw. + * If the defined number of iterations with not more than 4 different + * sourceIds for the removed stack Objects is reached, the players in range + * of the stackObject get asked to confirm a draw. If they do, all + * confirming players get set to a draw. * - * Possible to improve: check that always the same set of stackObjects are - * again aand again on the stack + * @param removedStackObjectSourceId */ - protected void checkInfiniteLoop() { - if (getStack().size() < 4) { // to prevent that this also pops up, if e.g. 20 triggers resolve at once + protected void checkInfiniteLoop(UUID removedStackObjectSourceId) { + if (stackObjectsCheck.contains(removedStackObjectSourceId) + && getStack().size() >= lastNumberOfAbilitiesOnTheStack) { infiniteLoopCounter++; if (infiniteLoopCounter > 15) { - StackObject stackObject = getStack().getFirst(); - if (stackObject != null) { - Player controller = getPlayer(stackObject.getControllerId()); - if (controller != null) { - for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) { - Player player = getPlayer(playerId); - if (!player.chooseUse(Outcome.Detriment, "Draw game because of infinite looping?", null, this)) { - informPlayers(controller.getLogName() + " has NOT confirmed that the game is a draw because of infinite looping."); - infiniteLoopCounter = 0; - return; - } - informPlayers(controller.getLogName() + " has confirmed that the game is a draw because of infinite looping."); + Player controller = getPlayer(getControllerId(removedStackObjectSourceId)); + if (controller != null) { + for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) { + Player player = getPlayer(playerId); + if (!player.chooseUse(Outcome.Detriment, "Draw game because of infinite looping?", null, this)) { + informPlayers(controller.getLogName() + " has NOT confirmed that the game is a draw because of infinite looping."); + infiniteLoopCounter = 0; + return; } - for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) { - Player player = getPlayer(playerId); - if (player != null) { - player.drew(this); - } + informPlayers(controller.getLogName() + " has confirmed that the game is a draw because of infinite looping."); + } + for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) { + Player player = getPlayer(playerId); + if (player != null) { + player.drew(this); } } } } + } else { + stackObjectsCheck.add(removedStackObjectSourceId); + if (stackObjectsCheck.size() > 4) { + stackObjectsCheck.removeFirst(); + } } + lastNumberOfAbilitiesOnTheStack = getStack().size(); } protected boolean allPassed() { @@ -2618,6 +2623,8 @@ public abstract class GameImpl implements Game, Serializable { public void resetLKI() { lki.clear(); lkiExtended.clear(); + infiniteLoopCounter = 0; + stackObjectsCheck.clear(); } @Override