refactor: simplified extra turn code, fixed NPE error on usage (Alchemist's Gambit)

This commit is contained in:
Oleg Agafonov 2023-06-30 06:26:48 +04:00
parent ad461498b0
commit 6529ead72f
9 changed files with 100 additions and 107 deletions

View file

@ -1003,6 +1003,8 @@ public class HumanPlayer extends PlayerImpl {
} }
} }
// TODO: check that all skips and stops used from real controlling player
// like holdingPriority (is it a bug here?)
if (getJustActivatedType() != null && !holdingPriority) { if (getJustActivatedType() != null && !holdingPriority) {
if (controllingPlayer.getUserData().isPassPriorityCast() if (controllingPlayer.getUserData().isPassPriorityCast()
&& getJustActivatedType() == AbilityType.SPELL) { && getJustActivatedType() == AbilityType.SPELL) {

View file

@ -1212,8 +1212,8 @@ public class GameController implements GameCallback {
sb.append(state.getStepNum()); sb.append(state.getStepNum());
sb.append("<br>getTurn: "); sb.append("<br>getTurn: ");
sb.append(state.getTurn()); sb.append(state.getTurn());
sb.append("<br>getTurnId: "); sb.append("<br>getExtraTurnId: ");
sb.append(state.getTurnId()); sb.append(state.getExtraTurnId());
sb.append("<br>getTurnMods: "); sb.append("<br>getTurnMods: ");
sb.append(state.getTurnMods()); sb.append(state.getTurnMods());
sb.append("<br>getTurnNum: "); sb.append("<br>getTurnNum: ");

View file

@ -93,6 +93,6 @@ class AlchemistsGambitEffect extends ReplacementEffectImpl {
@Override @Override
public boolean applies(GameEvent event, Ability source, Game game) { public boolean applies(GameEvent event, Ability source, Game game) {
return game.getState().getTurnId().equals(turnId); return this.turnId != null && turnId.equals(game.getState().getExtraTurnId());
} }
} }

View file

@ -2,104 +2,92 @@ package org.mage.test.turnmod;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.players.Player;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
/** /**
* * @author LevelX2, JayDi85
* @author LevelX2
*/ */
public class ExtraTurnsTest extends CardTestPlayerBase { public class ExtraTurnsTest extends CardTestPlayerBase {
/** private void checkTurnControl(int turn, TestPlayer needTurnController, boolean isExtraTurn) {
* Emrakul, the Promised End not giving an extra turn when cast in the runCode("checking turn " + turn, turn, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> {
* opponent's turn Player defaultTurnController = game.getPlayer(game.getActivePlayerId());
*/ Player realTurnController = defaultTurnController.getTurnControlledBy() == null ? defaultTurnController : game.getPlayer(defaultTurnController.getTurnControlledBy());
@Test Assert.assertEquals(String.format("turn %d must be controlled by %s", turn, needTurnController.getName()),
public void testEmrakulCastOnOpponentsTurnCheckTurn3() { needTurnController.getName(), realTurnController.getName());
addCard(Zone.BATTLEFIELD, playerA, "Island", 12); Assert.assertEquals(String.format("turn %d must be %s", turn, (isExtraTurn ? "extra turn" : "normal turn")),
addCard(Zone.GRAVEYARD, playerA, "Island", 1); isExtraTurn, game.getState().isExtraTurn());
// Emrakul, the Promised End costs {1} less to cast for each card type among cards in your graveyard. });
// When you cast Emrakul, you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn.
// Flying
// Trample
// Protection from instants
addCard(Zone.HAND, playerA, "Emrakul, the Promised End", 1); // {13}
// Flash (You may cast this spell any time you could cast an instant.)
// Creature cards you own that aren't on the battlefield have flash.
// Each opponent can cast spells only any time they could cast a sorcery.
addCard(Zone.BATTLEFIELD, playerA, "Teferi, Mage of Zhalfir", 1);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Promised End");
// Turn 4 is the next turn of opponent (player B) that player A controls
// So Turn 5 is the extra turn for player B after Turn 4
setStopAt(3, PhaseStep.DRAW);
execute();
assertPermanentCount(playerA, "Emrakul, the Promised End", 1);
Assert.assertTrue("Turn 3 is no extra turn ", !currentGame.getState().isExtraTurn());
Assert.assertEquals("For turn " + currentGame.getTurnNum() + ", playerA has to be the active player but active player is: "
+ currentGame.getPlayer(currentGame.getActivePlayerId()).getName(), currentGame.getActivePlayerId(), playerA.getId());
} }
@Test @Test
public void testEmrakulCastOnOpponentsTurnCheckTurn4() { public void test_EmrakulMustGiveExtraTurn_OnOwnTurn() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 12);
addCard(Zone.GRAVEYARD, playerA, "Island", 1);
// Emrakul, the Promised End costs {1} less to cast for each card type among cards in your graveyard. // Emrakul, the Promised End costs {1} less to cast for each card type among cards in your graveyard.
// When you cast Emrakul, you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn. // When you cast Emrakul, you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn.
// Flying
// Trample
// Protection from instants
addCard(Zone.HAND, playerA, "Emrakul, the Promised End", 1); // {13} addCard(Zone.HAND, playerA, "Emrakul, the Promised End", 1); // {13}
// Flash (You may cast this spell any time you could cast an instant.) addCard(Zone.BATTLEFIELD, playerA, "Island", 12);
// Creature cards you own that aren't on the battlefield have flash. addCard(Zone.GRAVEYARD, playerA, "Island", 1);
// Each opponent can cast spells only any time they could cast a sorcery.
addCard(Zone.BATTLEFIELD, playerA, "Teferi, Mage of Zhalfir", 1);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Promised End"); // cast emrakul on own turn and take extra turn
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Promised End");
addTarget(playerA, playerB);
// checks for all turns
// Turn 4 is the next turn of opponent (player B) that player A controls // Turn 4 is the next turn of opponent (player B) that player A controls
// So Turn 5 is the extra turn for player B after Turn 4 // So Turn 5 is the extra turn for player B after Turn 4
setStopAt(4, PhaseStep.DRAW); checkTurnControl(1, playerA, false);
execute(); checkTurnControl(2, playerB, false);
checkTurnControl(3, playerA, false);
checkTurnControl(4, playerA, false); // A take control of B's turn
checkTurnControl(5, playerB, true); // B take extra turn
checkTurnControl(6, playerA, false);
checkTurnControl(7, playerB, false);
assertPermanentCount(playerA, "Emrakul, the Promised End", 1); setStrictChooseMode(true);
Assert.assertTrue("Turn 4 is a controlled turn ", !playerB.isGameUnderControl()); setStopAt(8, PhaseStep.END_TURN);
Assert.assertEquals("For turn " + currentGame.getTurnNum() + ", playerB has to be the active player but active player is: " execute();
+ currentGame.getPlayer(currentGame.getActivePlayerId()).getName(), currentGame.getActivePlayerId(), playerB.getId());
} }
@Test @Test
public void testEmrakulCastOnOpponentsTurnCheckTurn5() { public void test_EmrakulMustGiveExtraTurn_OnOpponentTurn() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 12);
addCard(Zone.GRAVEYARD, playerA, "Island", 1);
// Emrakul, the Promised End costs {1} less to cast for each card type among cards in your graveyard. // Emrakul, the Promised End costs {1} less to cast for each card type among cards in your graveyard.
// When you cast Emrakul, you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn. // When you cast Emrakul, you gain control of target opponent during that player's next turn. After that turn, that player takes an extra turn.
// Flying
// Trample
// Protection from instants
addCard(Zone.HAND, playerA, "Emrakul, the Promised End", 1); // {13} addCard(Zone.HAND, playerA, "Emrakul, the Promised End", 1); // {13}
addCard(Zone.BATTLEFIELD, playerA, "Island", 12);
addCard(Zone.GRAVEYARD, playerA, "Island", 1);
//
// Flash (You may cast this spell any time you could cast an instant.) // Flash (You may cast this spell any time you could cast an instant.)
// Creature cards you own that aren't on the battlefield have flash. // Creature cards you own that aren't on the battlefield have flash.
// Each opponent can cast spells only any time they could cast a sorcery. // Each opponent can cast spells only any time they could cast a sorcery.
addCard(Zone.BATTLEFIELD, playerA, "Teferi, Mage of Zhalfir", 1); addCard(Zone.BATTLEFIELD, playerA, "Teferi, Mage of Zhalfir", 1);
// cast emrakul on opponent's turn and take extra turn
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Promised End"); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Emrakul, the Promised End");
addTarget(playerA, playerB);
// checks for all turns
// Turn 4 is the next turn of opponent (player B) that player A controls // Turn 4 is the next turn of opponent (player B) that player A controls
// So Turn 5 is the extra turn for player B after Turn 4 // So Turn 5 is the extra turn for player B after Turn 4
setStopOnTurn(5); checkTurnControl(1, playerA, false);
execute(); checkTurnControl(2, playerB, false);
checkTurnControl(3, playerA, false);
checkTurnControl(4, playerA, false); // A take control of B's turn
checkTurnControl(5, playerB, true); // B take extra turn
checkTurnControl(6, playerA, false);
checkTurnControl(7, playerB, false);
assertPermanentCount(playerA, "Emrakul, the Promised End", 1); setStrictChooseMode(true);
Assert.assertTrue("Turn 5 is an extra turn ", currentGame.getState().isExtraTurn()); setStopAt(8, PhaseStep.END_TURN);
Assert.assertEquals("For turn " + currentGame.getTurnNum() + ", playerB has to be the active player but active player is: " execute();
+ currentGame.getPlayer(currentGame.getActivePlayerId()).getName(), currentGame.getActivePlayerId(), playerB.getId());
} }
/** /**
* https://github.com/magefree/mage/issues/6824 * https://github.com/magefree/mage/issues/6824
* * <p>
* When you cast miracled Temporal Mastery with God-Eternal Kefnet on the * When you cast miracled Temporal Mastery with God-Eternal Kefnet on the
* battlefield and copy it with it's ability you get only 1 extra turn. It * battlefield and copy it with it's ability you get only 1 extra turn. It
* should be 2, since you cast Temporal Mastery with it's miracle ability + * should be 2, since you cast Temporal Mastery with it's miracle ability +
@ -107,9 +95,7 @@ public class ExtraTurnsTest extends CardTestPlayerBase {
* proceeds to next player. * proceeds to next player.
*/ */
@Test @Test
public void testCopyMiracledTemporalMastery4TwoExtraTurns() { public void test_MiracledTemporalMastery_MustGives2ExtraTurnsWithCopy() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerB, "Island", 7); addCard(Zone.BATTLEFIELD, playerB, "Island", 7);
// Flying // Flying
// You may reveal the first card you draw each turn as you draw it. Whenever you reveal an instant or sorcery card this way, // You may reveal the first card you draw each turn as you draw it. Whenever you reveal an instant or sorcery card this way,
@ -129,15 +115,18 @@ public class ExtraTurnsTest extends CardTestPlayerBase {
setChoice(playerB, false); // Would you like to reveal first drawn card? (Turn 3) setChoice(playerB, false); // Would you like to reveal first drawn card? (Turn 3)
setChoice(playerB, false); // Would you like to reveal first drawn card? (Turn 4) setChoice(playerB, false); // Would you like to reveal first drawn card? (Turn 4)
// turn checks
// Turn 3 + 4 are extra turns // Turn 3 + 4 are extra turns
setStopAt(4, PhaseStep.PRECOMBAT_MAIN); checkTurnControl(1, playerA, false);
checkTurnControl(2, playerB, false);
checkTurnControl(3, playerB, true); // extra turn for B
checkTurnControl(4, playerB, true); // extra turn for B
checkTurnControl(5, playerA, false);
setStrictChooseMode(true);
setStopAt(5, PhaseStep.END_TURN);
execute(); execute();
assertExileCount(playerB, "Temporal Mastery", 1); assertExileCount(playerB, "Temporal Mastery", 1);
Assert.assertTrue("Turn 4 is an extra turn ", currentGame.getState().isExtraTurn());
Assert.assertEquals("For turn " + currentGame.getTurnNum() + ", playerB has to be the active player but active player is: "
+ currentGame.getPlayer(currentGame.getActivePlayerId()).getName(), currentGame.getActivePlayerId(), playerB.getId());
} }
} }

View file

@ -82,16 +82,16 @@ public class AddExtraTurnControllerEffect extends OneShotEffect {
class LoseGameDelayedTriggeredAbility extends DelayedTriggeredAbility { class LoseGameDelayedTriggeredAbility extends DelayedTriggeredAbility {
private final UUID connectedTurnMod; private final UUID turnId;
public LoseGameDelayedTriggeredAbility(UUID connectedTurnMod) { public LoseGameDelayedTriggeredAbility(UUID turnId) {
super(new LoseGameSourceControllerEffect(), Duration.EndOfGame); super(new LoseGameSourceControllerEffect(), Duration.EndOfGame);
this.connectedTurnMod = connectedTurnMod; this.turnId = turnId;
} }
public LoseGameDelayedTriggeredAbility(final LoseGameDelayedTriggeredAbility ability) { public LoseGameDelayedTriggeredAbility(final LoseGameDelayedTriggeredAbility ability) {
super(ability); super(ability);
this.connectedTurnMod = ability.connectedTurnMod; this.turnId = ability.turnId;
} }
@Override @Override
@ -106,7 +106,7 @@ class LoseGameDelayedTriggeredAbility extends DelayedTriggeredAbility {
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
return connectedTurnMod != null && connectedTurnMod.equals(game.getState().getTurnId()); return this.turnId != null && this.turnId.equals(game.getState().getExtraTurnId());
} }
@Override @Override

View file

@ -206,6 +206,8 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
/** /**
* Id of the player the current turn it is. * Id of the player the current turn it is.
* *
* Player can be under control of another player, so search a real GUI's controller by Player->getTurnControlledBy
*
* @return * @return
*/ */
UUID getActivePlayerId(); UUID getActivePlayerId();

View file

@ -1033,13 +1033,13 @@ public abstract class GameImpl implements Game {
private boolean playExtraTurns() { private boolean playExtraTurns() {
//20091005 - 500.7 //20091005 - 500.7
TurnMod extraTurn = getNextExtraTurn(); TurnMod extraTurn = getNextExtraTurn();
try {
while (extraTurn != null) { while (extraTurn != null) {
GameEvent event = new GameEvent(GameEvent.EventType.PLAY_TURN, null, null, extraTurn.getPlayerId()); GameEvent event = new GameEvent(GameEvent.EventType.PLAY_TURN, null, null, extraTurn.getPlayerId());
if (!replaceEvent(event)) { if (!replaceEvent(event)) {
Player extraPlayer = this.getPlayer(extraTurn.getPlayerId()); Player extraPlayer = this.getPlayer(extraTurn.getPlayerId());
if (extraPlayer != null && extraPlayer.canRespond()) { if (extraPlayer != null && extraPlayer.canRespond()) {
state.setExtraTurn(true); state.setExtraTurnId(extraTurn.getId());
state.setTurnId(extraTurn.getId());
if (!this.isSimulation()) { if (!this.isSimulation()) {
informPlayers(extraPlayer.getLogName() + " takes an extra turn"); informPlayers(extraPlayer.getLogName() + " takes an extra turn");
} }
@ -1050,8 +1050,9 @@ public abstract class GameImpl implements Game {
} }
extraTurn = getNextExtraTurn(); extraTurn = getNextExtraTurn();
} }
state.setTurnId(null); } finally {
state.setExtraTurn(false); state.setExtraTurnId(null);
}
return true; return true;
} }

View file

@ -91,8 +91,7 @@ public class GameState implements Serializable, Copyable<GameState> {
private Battlefield battlefield; private Battlefield battlefield;
private int turnNum = 1; private int turnNum = 1;
private int stepNum = 0; private int stepNum = 0;
private UUID turnId = null; private UUID extraTurnId = null; // id of the current extra turn (null on normal turn or after game stopped)
private boolean extraTurn = false;
private boolean gameOver; private boolean gameOver;
private boolean paused; private boolean paused;
private ContinuousEffects effects; private ContinuousEffects effects;
@ -162,7 +161,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.battlefield = state.battlefield.copy(); this.battlefield = state.battlefield.copy();
this.turnNum = state.turnNum; this.turnNum = state.turnNum;
this.stepNum = state.stepNum; this.stepNum = state.stepNum;
this.extraTurn = state.extraTurn; this.extraTurnId = state.extraTurnId;
this.effects = state.effects.copy(); this.effects = state.effects.copy();
for (TriggeredAbility trigger : state.triggered) { for (TriggeredAbility trigger : state.triggered) {
this.triggered.add(trigger.copy()); this.triggered.add(trigger.copy());
@ -229,7 +228,7 @@ public class GameState implements Serializable, Copyable<GameState> {
companion.clear(); companion.clear();
turnNum = 1; turnNum = 1;
stepNum = 0; stepNum = 0;
extraTurn = false; extraTurnId = null;
gameOver = false; gameOver = false;
specialActions.clear(); specialActions.clear();
cardState.clear(); cardState.clear();
@ -265,7 +264,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.battlefield = state.battlefield; this.battlefield = state.battlefield;
this.turnNum = state.turnNum; this.turnNum = state.turnNum;
this.stepNum = state.stepNum; this.stepNum = state.stepNum;
this.extraTurn = state.extraTurn; this.extraTurnId = state.extraTurnId;
this.effects = state.effects; this.effects = state.effects;
this.triggered = state.triggered; this.triggered = state.triggered;
this.triggers = state.triggers; this.triggers = state.triggers;
@ -616,20 +615,16 @@ public class GameState implements Serializable, Copyable<GameState> {
this.turnNum = turnNum; this.turnNum = turnNum;
} }
public UUID getTurnId() { public UUID getExtraTurnId() {
return this.turnId; return this.extraTurnId;
} }
public void setTurnId(UUID turnId) { public void setExtraTurnId(UUID extraTurnId) {
this.turnId = turnId; this.extraTurnId = extraTurnId;
} }
public boolean isExtraTurn() { public boolean isExtraTurn() {
return extraTurn; return this.extraTurnId != null;
}
public void setExtraTurn(boolean extraTurn) {
this.extraTurn = extraTurn;
} }
public boolean isGameOver() { public boolean isGameOver() {

View file

@ -9,6 +9,10 @@ import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType; import mage.game.events.GameEvent.EventType;
/** /**
* Game's step
*
* Warning, don't use a changeable data in step's implementations
* TODO: implement copyable<> interface and copy usage in GameState
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */