mirror of
https://github.com/correl/mage.git
synced 2024-12-24 11:50:45 +00:00
* Improved game is infinite loop check to ask players only if some kind of iteration is recognized.
This commit is contained in:
parent
2728a274ca
commit
3c6ede7407
2 changed files with 185 additions and 148 deletions
|
@ -1,122 +1,152 @@
|
||||||
/*
|
/*
|
||||||
* To change this license header, choose License Headers in Project Properties.
|
* To change this license header, choose License Headers in Project Properties.
|
||||||
* To change this template file, choose Tools | Templates
|
* To change this template file, choose Tools | Templates
|
||||||
* and open the template in the editor.
|
* and open the template in the editor.
|
||||||
*/
|
*/
|
||||||
package org.mage.test.game.ends;
|
package org.mage.test.game.ends;
|
||||||
|
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author LevelX2
|
* @author LevelX2
|
||||||
*/
|
*/
|
||||||
public class GameIsADrawTest extends CardTestPlayerBase {
|
public class GameIsADrawTest extends CardTestPlayerBase {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void GameDrawByDamage() {
|
public void GameDrawByDamage() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
|
||||||
// Flame Rift deals 4 damage to each player.
|
// Flame Rift deals 4 damage to each player.
|
||||||
addCard(Zone.HAND, playerA, "Flame Rift", 3); // Sorcery {1}{R}
|
addCard(Zone.HAND, playerA, "Flame Rift", 3); // Sorcery {1}{R}
|
||||||
|
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4);
|
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4);
|
||||||
// Flame Rift deals 4 damage to each player.
|
// Flame Rift deals 4 damage to each player.
|
||||||
addCard(Zone.HAND, playerB, "Flame Rift", 2); // Sorcery
|
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(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");
|
||||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Rift");
|
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Rift");
|
||||||
|
|
||||||
setStopAt(2, PhaseStep.BEGIN_COMBAT);
|
setStopAt(2, PhaseStep.BEGIN_COMBAT);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
assertLife(playerA, 0);
|
assertLife(playerA, 0);
|
||||||
assertLife(playerB, 0);
|
assertLife(playerB, 0);
|
||||||
|
|
||||||
Assert.assertFalse("Player A has not won.", playerA.hasWon());
|
Assert.assertFalse("Player A has not won.", playerA.hasWon());
|
||||||
Assert.assertFalse("Player B has not won.", playerB.hasWon());
|
Assert.assertFalse("Player B has not won.", playerB.hasWon());
|
||||||
|
|
||||||
Assert.assertTrue("Game has ended.", currentGame.hasEnded());
|
Assert.assertTrue("Game has ended.", currentGame.hasEnded());
|
||||||
|
|
||||||
Assert.assertTrue("Both players had 0 life, game has be de a draw.", currentGame.isADraw());
|
Assert.assertTrue("Both players had 0 life, game has be de a draw.", currentGame.isADraw());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void GameDrawByDivineIntervention() {
|
public void GameDrawByDivineIntervention() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 8);
|
addCard(Zone.BATTLEFIELD, playerA, "Plains", 8);
|
||||||
// Divine Intervention enters the battlefield with two intervention counters on it.
|
// Divine Intervention enters the battlefield with two intervention counters on it.
|
||||||
// At the beginning of your upkeep, remove an intervention counter from Divine Intervention.
|
// 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.
|
// 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.HAND, playerA, "Divine Intervention", 1); // Enchantment {6}{W}{W}
|
||||||
|
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
|
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1);
|
addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 1);
|
||||||
|
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Divine Intervention");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Divine Intervention");
|
||||||
|
|
||||||
setStopAt(5, PhaseStep.PRECOMBAT_MAIN);
|
setStopAt(5, PhaseStep.PRECOMBAT_MAIN);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
assertLife(playerA, 20);
|
assertLife(playerA, 20);
|
||||||
assertLife(playerB, 20);
|
assertLife(playerB, 20);
|
||||||
|
|
||||||
Assert.assertFalse("Player A has not won.", playerA.hasWon());
|
Assert.assertFalse("Player A has not won.", playerA.hasWon());
|
||||||
Assert.assertFalse("Player B has not won.", playerB.hasWon());
|
Assert.assertFalse("Player B has not won.", playerB.hasWon());
|
||||||
|
|
||||||
Assert.assertTrue("Game has ended.", currentGame.hasEnded());
|
Assert.assertTrue("Game has ended.", currentGame.hasEnded());
|
||||||
|
|
||||||
Assert.assertTrue("Both players had 0 life, game has be de a draw.", currentGame.isADraw());
|
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
|
* 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
|
* Shield, which should make the game a draw. But instead, it just keeps
|
||||||
* going...
|
* going...
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void GameDrawByInfiniteLoop() {
|
public void GameDrawByInfiniteLoop() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
|
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
|
||||||
|
|
||||||
// All damage that would be dealt to you is dealt to equipped creature instead.
|
// All damage that would be dealt to you is dealt to equipped creature instead.
|
||||||
// Equip {3}
|
// Equip {3}
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Pariah's Shield", 1); // Artifact Equipment {5}
|
addCard(Zone.BATTLEFIELD, playerA, "Pariah's Shield", 1); // Artifact Equipment {5}
|
||||||
|
|
||||||
// As Stuffy Doll enters the battlefield, choose a player.
|
// As Stuffy Doll enters the battlefield, choose a player.
|
||||||
// Stuffy Doll is indestructible.
|
// Stuffy Doll is indestructible.
|
||||||
// Whenever Stuffy Doll is dealt damage, it deals that much damage to the chosen player.
|
// Whenever Stuffy Doll is dealt damage, it deals that much damage to the chosen player.
|
||||||
// {T}: Stuffy Doll deals 1 damage to itself.
|
// {T}: Stuffy Doll deals 1 damage to itself.
|
||||||
addCard(Zone.HAND, playerA, "Stuffy Doll", 1); // Artifact Creature {5} 0/1
|
addCard(Zone.HAND, playerA, "Stuffy Doll", 1); // Artifact Creature {5} 0/1
|
||||||
|
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stuffy Doll");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stuffy Doll");
|
||||||
setChoice(playerA, "PlayerA");
|
setChoice(playerA, "PlayerA");
|
||||||
setChoice(playerA, "PlayerA");
|
setChoice(playerA, "PlayerA");
|
||||||
|
|
||||||
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Stuffy Doll");
|
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Stuffy Doll");
|
||||||
activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}");
|
activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}");
|
||||||
setStopAt(3, PhaseStep.END_TURN);
|
setStopAt(3, PhaseStep.END_TURN);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
assertPermanentCount(playerA, "Stuffy Doll", 1);
|
assertPermanentCount(playerA, "Stuffy Doll", 1);
|
||||||
Permanent shield = getPermanent("Pariah's Shield");
|
Permanent shield = getPermanent("Pariah's Shield");
|
||||||
Assert.assertTrue("Pariah's Shield is attached", shield.getAttachedTo() != null);
|
Assert.assertTrue("Pariah's Shield is attached", shield.getAttachedTo() != null);
|
||||||
|
|
||||||
Assert.assertFalse("Player A has not won.", playerA.hasWon());
|
Assert.assertFalse("Player A has not won.", playerA.hasWon());
|
||||||
Assert.assertFalse("Player B has not won.", playerB.hasWon());
|
Assert.assertFalse("Player B has not won.", playerB.hasWon());
|
||||||
|
|
||||||
Assert.assertTrue("Game has ended.", currentGame.hasEnded());
|
Assert.assertTrue("Game has ended.", currentGame.hasEnded());
|
||||||
|
|
||||||
Assert.assertTrue("Inifinite loop detected, game has be de a draw.", currentGame.isADraw());
|
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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -177,6 +177,8 @@ public abstract class GameImpl implements Game, Serializable {
|
||||||
protected PlayerList playerList;
|
protected PlayerList playerList;
|
||||||
|
|
||||||
private int infiniteLoopCounter; // used to check if the game is in an infinite loop
|
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<UUID> 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)
|
// used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist)
|
||||||
protected Map<UUID, Counters> enterWithCounters = new HashMap<>();
|
protected Map<UUID, Counters> enterWithCounters = new HashMap<>();
|
||||||
|
|
||||||
|
@ -1289,7 +1291,6 @@ public abstract class GameImpl implements Game, Serializable {
|
||||||
}
|
}
|
||||||
if (allPassed()) {
|
if (allPassed()) {
|
||||||
if (!state.getStack().isEmpty()) {
|
if (!state.getStack().isEmpty()) {
|
||||||
checkInfiniteLoop();
|
|
||||||
//20091005 - 115.4
|
//20091005 - 115.4
|
||||||
resolve();
|
resolve();
|
||||||
applyEffects();
|
applyEffects();
|
||||||
|
@ -1298,7 +1299,6 @@ public abstract class GameImpl implements Game, Serializable {
|
||||||
resetShortLivingLKI();
|
resetShortLivingLKI();
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
infiniteLoopCounter = 0;
|
|
||||||
resetLKI();
|
resetLKI();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1346,6 +1346,7 @@ public abstract class GameImpl implements Game, Serializable {
|
||||||
if (top != null) {
|
if (top != null) {
|
||||||
state.getStack().remove(top); // seems partly redundant because move card from stack to grave is already done and the stack removed
|
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);
|
rememberLKI(top.getSourceId(), Zone.STACK, top);
|
||||||
|
checkInfiniteLoop(top.getSourceId());
|
||||||
if (!getTurn().isEndTurnRequested()) {
|
if (!getTurn().isEndTurnRequested()) {
|
||||||
while (state.hasSimultaneousEvents()) {
|
while (state.hasSimultaneousEvents()) {
|
||||||
state.handleSimultaneousEvent(this);
|
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
|
* 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
|
* If the defined number of iterations with not more than 4 different
|
||||||
* the stackObject get asked to confirm a draw. If they do, all confirming
|
* sourceIds for the removed stack Objects is reached, the players in range
|
||||||
* players get set to a draw.
|
* 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
|
* @param removedStackObjectSourceId
|
||||||
* again aand again on the stack
|
|
||||||
*/
|
*/
|
||||||
protected void checkInfiniteLoop() {
|
protected void checkInfiniteLoop(UUID removedStackObjectSourceId) {
|
||||||
if (getStack().size() < 4) { // to prevent that this also pops up, if e.g. 20 triggers resolve at once
|
if (stackObjectsCheck.contains(removedStackObjectSourceId)
|
||||||
|
&& getStack().size() >= lastNumberOfAbilitiesOnTheStack) {
|
||||||
infiniteLoopCounter++;
|
infiniteLoopCounter++;
|
||||||
if (infiniteLoopCounter > 15) {
|
if (infiniteLoopCounter > 15) {
|
||||||
StackObject stackObject = getStack().getFirst();
|
Player controller = getPlayer(getControllerId(removedStackObjectSourceId));
|
||||||
if (stackObject != null) {
|
if (controller != null) {
|
||||||
Player controller = getPlayer(stackObject.getControllerId());
|
for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) {
|
||||||
if (controller != null) {
|
Player player = getPlayer(playerId);
|
||||||
for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) {
|
if (!player.chooseUse(Outcome.Detriment, "Draw game because of infinite looping?", null, this)) {
|
||||||
Player player = getPlayer(playerId);
|
informPlayers(controller.getLogName() + " has NOT confirmed that the game is a draw because of infinite looping.");
|
||||||
if (!player.chooseUse(Outcome.Detriment, "Draw game because of infinite looping?", null, this)) {
|
infiniteLoopCounter = 0;
|
||||||
informPlayers(controller.getLogName() + " has NOT confirmed that the game is a draw because of infinite looping.");
|
return;
|
||||||
infiniteLoopCounter = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
informPlayers(controller.getLogName() + " has confirmed that the game is a draw because of infinite looping.");
|
|
||||||
}
|
}
|
||||||
for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) {
|
informPlayers(controller.getLogName() + " has confirmed that the game is a draw because of infinite looping.");
|
||||||
Player player = getPlayer(playerId);
|
}
|
||||||
if (player != null) {
|
for (UUID playerId : getState().getPlayersInRange(controller.getId(), this)) {
|
||||||
player.drew(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() {
|
protected boolean allPassed() {
|
||||||
|
@ -2618,6 +2623,8 @@ public abstract class GameImpl implements Game, Serializable {
|
||||||
public void resetLKI() {
|
public void resetLKI() {
|
||||||
lki.clear();
|
lki.clear();
|
||||||
lkiExtended.clear();
|
lkiExtended.clear();
|
||||||
|
infiniteLoopCounter = 0;
|
||||||
|
stackObjectsCheck.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in a new issue