server: improved server stability (#11285) and reworked triggers/playable logic (#8426):

* game: now all playable calculations done in game simulation, outside real game (no more freeze and ruined games by wrong Nyxbloom Ancient and other cards with wrong replacement dialog);
* game: fixed multiple problems with triggers (wrong order, duplicated calls or "too many mana" bugs, see #8426, #12087);
* tests: added data integrity checks for game's triggers (3 enabled and 3 disabled due current game engine logic);
This commit is contained in:
Oleg Agafonov 2024-04-16 23:10:04 +04:00
parent f68e435fc4
commit e8e2f23284
23 changed files with 362 additions and 120 deletions

View file

@ -53,9 +53,10 @@ public class TappedForManaFromMultipleEffects extends CardTestPlayerBase {
// cast nyx 2
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nyxbloom Ancient");
// TODO: TAPPED_FOR_MANA replace event called from checkTappedForManaReplacement and start to choose replace events (is that problem?)
// TODO: yes, it's a problem, cause playable calc must not use dialogs!!!
// use case (that test): comment one 1-2 choices to fail in 1-2 calls
setChoice(playerA, "Nyxbloom Ancient"); // getPlayable... checkTappedForManaReplacement... chooseReplacementEffect
setChoice(playerA, "Nyxbloom Ancient"); // playManaAbility... resolve... checkToFirePossibleEvents... chooseReplacementEffect
setChoice(playerA, "Nyxbloom Ancient"); // x2 replacement effects from x2 nyx
//setChoice(playerA, "Nyxbloom Ancient"); // wrongly choice from playable calc - no need after bug fix
// cast chloro
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Chlorophant");

View file

@ -11,15 +11,48 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions;
/**
*
* @author LevelX2
*/
public class SasayaOrochiAscendantTest extends CardTestPlayerBase {
@Test
public void test_SasayasEssence_SimpleManaCalculation() {
addCard(Zone.HAND, playerA, "Plains", 7);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
// Reveal your hand: If you have seven or more land cards in your hand, flip Sasaya, Orochi Ascendant.
// Sasaya's Essence: Legendary Enchantment
// Whenever a land you control is tapped for mana, for each other land you control with the same name, add one mana of any type that land produced.
addCard(Zone.BATTLEFIELD, playerA, "Sasaya, Orochi Ascendant", 1);
//
// Mana pools don't empty as steps and phases end.
addCard(Zone.HAND, playerA, "Upwelling", 1); // Enchantment {3}{G}
//
// At the beginning of your upkeep, you gain 1 life.
addCard(Zone.BATTLEFIELD, playerB, "Fountain of Renewal", 1);
// prepare Sasaya's Essence
checkPermanentCount("before prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sasaya's Essence", 0);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reveal your hand: If you have seven or more land cards in your hand, flip");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCount("after prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sasaya's Essence", 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
// possible error: additional triggers from neutral card can break mana triggers and will calc wrong mana
// reason: random triggers order on triggers iterator, can be fixed by linked map usage
// x3 forest + x2 for each other forest
ManaOptions manaOptions = playerA.getManaAvailable(currentGame);
assertManaOptions("{G}{G}{G}" + "{G}{G}" + "{G}{G}" + "{G}{G}", manaOptions);
}
@Test
public void testSasayasEssence() {
addCard(Zone.HAND, playerA, "Plains", 7);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.BATTLEFIELD, playerB, "Fountain of Renewal", 5);
// Reveal your hand: If you have seven or more land cards in your hand, flip Sasaya, Orochi Ascendant.
// Sasaya's Essence: Legendary Enchantment
@ -123,7 +156,7 @@ public class SasayaOrochiAscendantTest extends CardTestPlayerBase {
assertManaOptions("{R}{R}{R}{R}{G}{G}{G}{G}{G}", manaOptions);
assertManaOptions("{R}{R}{R}{G}{G}{G}{G}{G}{G}", manaOptions);
assertManaOptions("{R}{R}{G}{G}{G}{G}{G}{G}{G}", manaOptions);
assertManaOptions("{R}{G}{G}{G}{G}{G}{G}{G}{G}", manaOptions);
assertManaOptions("{R}{G}{G}{G}{G}{G}{G}{G}{G}", manaOptions);
}
}

View file

@ -46,7 +46,7 @@ public class ValkiGodOfLiesTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: Exile the top card of each player's library.");
playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears");
setChoice(playerA, "Emblem Tibalt");
//setChoice(playerA, "Emblem Tibalt"); // wrongly x2 replacement effects - no need after bug fix
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);

View file

@ -7,12 +7,12 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* 3BG Legendary Creature - Frog Horror Deathtouch
*
* <p>
* At the beginning of your upkeep, sacrifice The Gitrog Monster unless you
* sacrifice a land.
*
* <p>
* You may play an additional land on each of your turns.
*
* <p>
* Whenever one or more land cards are put into your graveyard from anywhere,
* draw a card.
*
@ -35,6 +35,7 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Armageddon");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.DRAW);
execute();
@ -58,10 +59,10 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
// on 3rd turn during upkeep opt to sacrifice a land
// TODO: I don't know how to get these choices to work, let the choices go automatically
// addTarget(playerA, "Swamp");
// setChoice(playerA, true);
setChoice(playerA, true); // sac land
setChoice(playerA, "Swamp"); // sac land
setStrictChooseMode(true);
setStopAt(3, PhaseStep.DRAW);
execute();
@ -88,6 +89,7 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Planar Outburst");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.DRAW);
execute();
@ -98,7 +100,7 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
/**
* NOTE: As of 05/05/2017 this test is failing due to a bug in code. See
* issue #3251
*
* <p>
* I took control of a Gitrog Monster, while the Gitrog Monster's owner
* controlled a Dryad Arbor and cast Toxic Deluge for 6.
*/
@ -125,10 +127,13 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
addCard(Zone.GRAVEYARD, playerB, "Rags // Riches", 1);
addCard(Zone.BATTLEFIELD, playerB, "Island", 7);
// first land
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp");
// cast gitrog and second land
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Dryad Arbor");
// change control to B, so no additional land for A
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Riches");
setChoice(playerA, "The Gitrog Monster");
@ -137,6 +142,7 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Toxic Deluge");
setChoice(playerA, "X=6");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();

View file

@ -27,6 +27,7 @@ import mage.player.ai.ComputerPlayerMCTS;
import mage.players.ManaPool;
import mage.players.Player;
import mage.server.game.GameSessionPlayer;
import mage.util.ThreadUtils;
import mage.utils.SystemUtil;
import mage.util.CardUtil;
import mage.view.GameView;
@ -236,6 +237,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
throw new IllegalStateException("Game is not initialized. Use load method to load a test case and initialize a game.");
}
ThreadUtils.ensureRunInGameThread();
// check stop command
int maxTurn = 1;
int maxPhase = 0;