mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
* 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:
parent
f68e435fc4
commit
e8e2f23284
23 changed files with 362 additions and 120 deletions
|
|
@ -414,7 +414,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
Target target = effect.getTarget();
|
Target target = effect.getTarget();
|
||||||
if (!target.doneChoosing()) {
|
if (!target.doneChoosing()) {
|
||||||
for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) {
|
for (UUID targetId : target.possibleTargets(stackObject.getControllerId(), stackObject.getStackAbility(), game)) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
StackAbility newAbility = (StackAbility) stackObject.copy();
|
StackAbility newAbility = (StackAbility) stackObject.copy();
|
||||||
SearchEffect newEffect = getSearchEffect(newAbility);
|
SearchEffect newEffect = getSearchEffect(newAbility);
|
||||||
newEffect.getTarget().addTarget(targetId, newAbility, sim);
|
newEffect.getTarget().addTarget(targetId, newAbility, sim);
|
||||||
|
|
@ -514,8 +514,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
logger.info("Sim Prio [" + depth + "] -- interrupted");
|
logger.info("Sim Prio [" + depth + "] -- interrupted");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
sim.setSimulation(true);
|
|
||||||
if (!(action instanceof StaticAbility) //for MorphAbility, etc
|
if (!(action instanceof StaticAbility) //for MorphAbility, etc
|
||||||
&& sim.getPlayer(currentPlayer.getId()).activateAbility((ActivatedAbility) action.copy(), sim)) {
|
&& sim.getPlayer(currentPlayer.getId()).activateAbility((ActivatedAbility) action.copy(), sim)) {
|
||||||
sim.applyEffects();
|
sim.applyEffects();
|
||||||
|
|
@ -1067,8 +1066,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
|
||||||
* @return a new game object with simulated players
|
* @return a new game object with simulated players
|
||||||
*/
|
*/
|
||||||
protected Game createSimulation(Game game) {
|
protected Game createSimulation(Game game) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
sim.setSimulation(true);
|
|
||||||
for (Player oldPlayer : sim.getState().getPlayers().values()) {
|
for (Player oldPlayer : sim.getState().getPlayers().values()) {
|
||||||
// replace original player by simulated player and find result (execute/resolve current action)
|
// replace original player by simulated player and find result (execute/resolve current action)
|
||||||
Player origPlayer = game.getState().getPlayers().get(oldPlayer.getId()).copy();
|
Player origPlayer = game.getState().getPlayers().get(oldPlayer.getId()).copy();
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
|
|
||||||
public List<Ability> simulatePriority(Game game) {
|
public List<Ability> simulatePriority(Game game) {
|
||||||
allActions = new ConcurrentLinkedQueue<>();
|
allActions = new ConcurrentLinkedQueue<>();
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
sim.setSimulation(true);
|
|
||||||
forced = false;
|
forced = false;
|
||||||
simulateOptions(sim);
|
simulateOptions(sim);
|
||||||
|
|
||||||
|
|
@ -170,17 +169,6 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// protected void simulateAction(Game game, SimulatedAction previousActions, Ability action) {
|
|
||||||
// List<Ability> actions = new ArrayList<Ability>(previousActions.getAbilities());
|
|
||||||
// actions.add(action);
|
|
||||||
// Game sim = game.copy();
|
|
||||||
// if (sim.getPlayer(playerId).activateAbility((ActivatedAbility) action.copy(), sim)) {
|
|
||||||
// sim.applyEffects();
|
|
||||||
// sim.getPlayers().resetPassed();
|
|
||||||
// allActions.add(new SimulatedAction(sim, actions));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if suggested abilities exist, return only those from playables
|
* if suggested abilities exist, return only those from playables
|
||||||
*
|
*
|
||||||
|
|
@ -322,7 +310,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
int powerElements = (int) Math.pow(2, attackersList.size());
|
int powerElements = (int) Math.pow(2, attackersList.size());
|
||||||
StringBuilder binary = new StringBuilder();
|
StringBuilder binary = new StringBuilder();
|
||||||
for (int i = powerElements - 1; i >= 0; i--) {
|
for (int i = powerElements - 1; i >= 0; i--) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
binary.setLength(0);
|
binary.setLength(0);
|
||||||
binary.append(Integer.toBinaryString(i));
|
binary.append(Integer.toBinaryString(i));
|
||||||
while (binary.length() < attackersList.size()) {
|
while (binary.length() < attackersList.size()) {
|
||||||
|
|
@ -360,7 +348,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
//add a node with no blockers
|
//add a node with no blockers
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
engagements.put(sim.getCombat().getValue().hashCode(), sim.getCombat());
|
engagements.put(sim.getCombat().getValue().hashCode(), sim.getCombat());
|
||||||
sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, playerId, playerId));
|
sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, playerId, playerId));
|
||||||
|
|
||||||
|
|
@ -381,7 +369,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
List<Permanent> remaining = remove(blockers, blocker);
|
List<Permanent> remaining = remove(blockers, blocker);
|
||||||
for (int i = 0; i < numGroups; i++) {
|
for (int i = 0; i < numGroups; i++) {
|
||||||
if (game.getCombat().getGroups().get(i).canBlock(blocker, game)) {
|
if (game.getCombat().getGroups().get(i).canBlock(blocker, game)) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
sim.getCombat().getGroups().get(i).addBlocker(blocker.getId(), playerId, sim);
|
sim.getCombat().getGroups().get(i).addBlocker(blocker.getId(), playerId, sim);
|
||||||
if (engagements.put(sim.getCombat().getValue().hashCode(), sim.getCombat()) != null) {
|
if (engagements.put(sim.getCombat().getValue().hashCode(), sim.getCombat()) != null) {
|
||||||
logger.debug("simulating -- found redundant block combination");
|
logger.debug("simulating -- found redundant block combination");
|
||||||
|
|
@ -419,7 +407,7 @@ public final class SimulatedPlayer2 extends ComputerPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addAbilityNode(SimulationNode2 parent, Ability ability, int depth, Game game) {
|
protected void addAbilityNode(SimulationNode2 parent, Ability ability, int depth, Game game) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
sim.getStack().push(new StackAbility(ability, playerId));
|
sim.getStack().push(new StackAbility(ability, playerId));
|
||||||
if (ability.activate(sim, false) && ability.isUsesStack()) {
|
if (ability.activate(sim, false) && ability.isUsesStack()) {
|
||||||
game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId()));
|
game.fireEvent(new GameEvent(GameEvent.EventType.TRIGGERED_ABILITY, ability.getId(), ability, ability.getControllerId()));
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ public final class CombatUtil {
|
||||||
* @deprecated TODO: unused, can be deleted?
|
* @deprecated TODO: unused, can be deleted?
|
||||||
*/
|
*/
|
||||||
public static SurviveInfo willItSurvive(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
public static SurviveInfo willItSurvive(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
|
|
||||||
// TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?)
|
// TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?)
|
||||||
Combat combat = sim.getCombat();
|
Combat combat = sim.getCombat();
|
||||||
|
|
@ -307,7 +307,7 @@ public final class CombatUtil {
|
||||||
|
|
||||||
public static SurviveInfo willItSurvive2(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
public static SurviveInfo willItSurvive2(Game game, UUID attackingPlayerId, UUID defendingPlayerId, Permanent attacker, Permanent blocker) {
|
||||||
|
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
|
|
||||||
// TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?)
|
// TODO: bugged, miss combat.clear code (possible bugs - wrong blocker declare by AI on multiple options?)
|
||||||
Combat combat = sim.getCombat();
|
Combat combat = sim.getCombat();
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ public class ComputerPlayerMCTS extends ComputerPlayer {
|
||||||
* @return a new game object with simulated players
|
* @return a new game object with simulated players
|
||||||
*/
|
*/
|
||||||
protected Game createMCTSGame(Game game) {
|
protected Game createMCTSGame(Game game) {
|
||||||
Game mcts = game.copy();
|
Game mcts = game.createSimulationForAI();
|
||||||
|
|
||||||
for (Player copyPlayer : mcts.getState().getPlayers().values()) {
|
for (Player copyPlayer : mcts.getState().getPlayers().values()) {
|
||||||
Player origPlayer = game.getState().getPlayers().get(copyPlayer.getId());
|
Player origPlayer = game.getState().getPlayers().get(copyPlayer.getId());
|
||||||
|
|
@ -295,7 +295,6 @@ public class ComputerPlayerMCTS extends ComputerPlayer {
|
||||||
}
|
}
|
||||||
mcts.getState().getPlayers().put(copyPlayer.getId(), newPlayer);
|
mcts.getState().getPlayers().put(copyPlayer.getId(), newPlayer);
|
||||||
}
|
}
|
||||||
mcts.setSimulation(true);
|
|
||||||
mcts.resume();
|
mcts.resume();
|
||||||
return mcts;
|
return mcts;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ public class MCTSNode {
|
||||||
* @return a new game object with simulated players
|
* @return a new game object with simulated players
|
||||||
*/
|
*/
|
||||||
protected Game createSimulation(Game game, UUID playerId) {
|
protected Game createSimulation(Game game, UUID playerId) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
|
|
||||||
for (Player oldPlayer: sim.getState().getPlayers().values()) {
|
for (Player oldPlayer: sim.getState().getPlayers().values()) {
|
||||||
Player origPlayer = game.getState().getPlayers().get(oldPlayer.getId()).copy();
|
Player origPlayer = game.getState().getPlayers().get(oldPlayer.getId()).copy();
|
||||||
|
|
@ -254,7 +254,6 @@ public class MCTSNode {
|
||||||
sim.getState().getPlayers().put(oldPlayer.getId(), newPlayer);
|
sim.getState().getPlayers().put(oldPlayer.getId(), newPlayer);
|
||||||
}
|
}
|
||||||
randomizePlayers(sim, playerId);
|
randomizePlayers(sim, playerId);
|
||||||
sim.setSimulation(true);
|
|
||||||
return sim;
|
return sim;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public class PriorityNextAction implements MCTSNodeNextAction{
|
||||||
else
|
else
|
||||||
abilities = MCTSNode.getPlayables(player, fullStateValue, game);
|
abilities = MCTSNode.getPlayables(player, fullStateValue, game);
|
||||||
for (Ability ability: abilities) {
|
for (Ability ability: abilities) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId());
|
MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId());
|
||||||
simPlayer.activateAbility((ActivatedAbility)ability, sim);
|
simPlayer.activateAbility((ActivatedAbility)ability, sim);
|
||||||
sim.resume();
|
sim.resume();
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public class SelectAttackersNextAction implements MCTSNodeNextAction{
|
||||||
attacks = getAttacks(player, fullStateValue, game);
|
attacks = getAttacks(player, fullStateValue, game);
|
||||||
UUID defenderId = game.getOpponents(player.getId()).iterator().next();
|
UUID defenderId = game.getOpponents(player.getId()).iterator().next();
|
||||||
for (List<UUID> attack: attacks) {
|
for (List<UUID> attack: attacks) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId());
|
MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId());
|
||||||
for (UUID attackerId: attack) {
|
for (UUID attackerId: attack) {
|
||||||
simPlayer.declareAttacker(attackerId, defenderId, sim, false);
|
simPlayer.declareAttacker(attackerId, defenderId, sim, false);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public class SelectBlockersNextAction implements MCTSNodeNextAction{
|
||||||
else
|
else
|
||||||
blocks = getBlocks(player, fullStateValue, game);
|
blocks = getBlocks(player, fullStateValue, game);
|
||||||
for (List<List<UUID>> block : blocks) {
|
for (List<List<UUID>> block : blocks) {
|
||||||
Game sim = game.copy();
|
Game sim = game.createSimulationForAI();
|
||||||
MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId());
|
MCTSPlayer simPlayer = (MCTSPlayer) sim.getPlayer(player.getId());
|
||||||
List<CombatGroup> groups = sim.getCombat().getGroups();
|
List<CombatGroup> groups = sim.getCombat().getGroups();
|
||||||
for (int i = 0; i < groups.size(); i++) {
|
for (int i = 0; i < groups.size(); i++) {
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,8 @@ public class GameSessionWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameView getGameView() {
|
public GameView getGameView() {
|
||||||
// game view calculation can take some time, so use copy for thread save (protection from ConcurrentModificationException)
|
// game view calculation can take some time and can be called from non-game thread,
|
||||||
|
// so use copy for thread save (protection from ConcurrentModificationException)
|
||||||
Game sourceGame = game.copy();
|
Game sourceGame = game.copy();
|
||||||
|
|
||||||
GameView gameView = new GameView(sourceGame.getState(), sourceGame, null, userId);
|
GameView gameView = new GameView(sourceGame.getState(), sourceGame, null, userId);
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,10 @@ public class TappedForManaFromMultipleEffects extends CardTestPlayerBase {
|
||||||
// cast nyx 2
|
// cast nyx 2
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nyxbloom Ancient");
|
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: 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
|
// 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"); // x2 replacement effects from x2 nyx
|
||||||
setChoice(playerA, "Nyxbloom Ancient"); // playManaAbility... resolve... checkToFirePossibleEvents... chooseReplacementEffect
|
//setChoice(playerA, "Nyxbloom Ancient"); // wrongly choice from playable calc - no need after bug fix
|
||||||
|
|
||||||
// cast chloro
|
// cast chloro
|
||||||
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Chlorophant");
|
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Chlorophant");
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,48 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||||
import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions;
|
import static org.mage.test.utils.ManaOptionsTestUtils.assertManaOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author LevelX2
|
* @author LevelX2
|
||||||
*/
|
*/
|
||||||
public class SasayaOrochiAscendantTest extends CardTestPlayerBase {
|
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
|
@Test
|
||||||
public void testSasayasEssence() {
|
public void testSasayasEssence() {
|
||||||
addCard(Zone.HAND, playerA, "Plains", 7);
|
addCard(Zone.HAND, playerA, "Plains", 7);
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
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.
|
// Reveal your hand: If you have seven or more land cards in your hand, flip Sasaya, Orochi Ascendant.
|
||||||
// Sasaya's Essence: Legendary Enchantment
|
// Sasaya's Essence: Legendary Enchantment
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+2: Exile the top card of each player's library.");
|
||||||
playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains");
|
playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains");
|
||||||
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Ephemerate", "Grizzly Bears");
|
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);
|
setStrictChooseMode(true);
|
||||||
setStopAt(1, PhaseStep.END_TURN);
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3BG Legendary Creature - Frog Horror Deathtouch
|
* 3BG Legendary Creature - Frog Horror Deathtouch
|
||||||
*
|
* <p>
|
||||||
* At the beginning of your upkeep, sacrifice The Gitrog Monster unless you
|
* At the beginning of your upkeep, sacrifice The Gitrog Monster unless you
|
||||||
* sacrifice a land.
|
* sacrifice a land.
|
||||||
*
|
* <p>
|
||||||
* You may play an additional land on each of your turns.
|
* 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,
|
* Whenever one or more land cards are put into your graveyard from anywhere,
|
||||||
* draw a card.
|
* draw a card.
|
||||||
*
|
*
|
||||||
|
|
@ -35,6 +35,7 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
||||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Armageddon");
|
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Armageddon");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(3, PhaseStep.DRAW);
|
setStopAt(3, PhaseStep.DRAW);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
|
|
@ -58,10 +59,10 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
|
||||||
|
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
||||||
// on 3rd turn during upkeep opt to sacrifice a land
|
// 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
|
setChoice(playerA, true); // sac land
|
||||||
// addTarget(playerA, "Swamp");
|
setChoice(playerA, "Swamp"); // sac land
|
||||||
// setChoice(playerA, true);
|
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(3, PhaseStep.DRAW);
|
setStopAt(3, PhaseStep.DRAW);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
|
|
@ -88,6 +89,7 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
||||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Planar Outburst");
|
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Planar Outburst");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(3, PhaseStep.DRAW);
|
setStopAt(3, PhaseStep.DRAW);
|
||||||
execute();
|
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
|
* NOTE: As of 05/05/2017 this test is failing due to a bug in code. See
|
||||||
* issue #3251
|
* issue #3251
|
||||||
*
|
* <p>
|
||||||
* I took control of a Gitrog Monster, while the Gitrog Monster's owner
|
* I took control of a Gitrog Monster, while the Gitrog Monster's owner
|
||||||
* controlled a Dryad Arbor and cast Toxic Deluge for 6.
|
* 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.GRAVEYARD, playerB, "Rags // Riches", 1);
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 7);
|
addCard(Zone.BATTLEFIELD, playerB, "Island", 7);
|
||||||
|
|
||||||
|
// first land
|
||||||
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp");
|
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swamp");
|
||||||
|
// cast gitrog and second land
|
||||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "The Gitrog Monster");
|
||||||
playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Dryad Arbor");
|
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");
|
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Riches");
|
||||||
setChoice(playerA, "The Gitrog Monster");
|
setChoice(playerA, "The Gitrog Monster");
|
||||||
|
|
||||||
|
|
@ -137,6 +142,7 @@ public class TheGitrogMonsterTest extends CardTestPlayerBase {
|
||||||
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Toxic Deluge");
|
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Toxic Deluge");
|
||||||
setChoice(playerA, "X=6");
|
setChoice(playerA, "X=6");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(3, PhaseStep.BEGIN_COMBAT);
|
setStopAt(3, PhaseStep.BEGIN_COMBAT);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import mage.player.ai.ComputerPlayerMCTS;
|
||||||
import mage.players.ManaPool;
|
import mage.players.ManaPool;
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
import mage.server.game.GameSessionPlayer;
|
import mage.server.game.GameSessionPlayer;
|
||||||
|
import mage.util.ThreadUtils;
|
||||||
import mage.utils.SystemUtil;
|
import mage.utils.SystemUtil;
|
||||||
import mage.util.CardUtil;
|
import mage.util.CardUtil;
|
||||||
import mage.view.GameView;
|
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.");
|
throw new IllegalStateException("Game is not initialized. Use load method to load a test case and initialize a game.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThreadUtils.ensureRunInGameThread();
|
||||||
|
|
||||||
// check stop command
|
// check stop command
|
||||||
int maxTurn = 1;
|
int maxTurn = 1;
|
||||||
int maxPhase = 0;
|
int maxPhase = 0;
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ public abstract class AbilityImpl implements Ability {
|
||||||
if (checkIfClause(game)) {
|
if (checkIfClause(game)) {
|
||||||
// Ability has started resolving. Fire event.
|
// Ability has started resolving. Fire event.
|
||||||
// Used for abilities counting the number of resolutions like Ashling the Pilgrim.
|
// Used for abilities counting the number of resolutions like Ashling the Pilgrim.
|
||||||
|
// TODO: called for mana abilities too, must be removed to safe place someday (see old place like StackAbility::resolve)
|
||||||
game.fireEvent(new GameEvent(GameEvent.EventType.RESOLVING_ABILITY, this.getOriginalId(), this, this.getControllerId()));
|
game.fireEvent(new GameEvent(GameEvent.EventType.RESOLVING_ABILITY, this.getOriginalId(), this, this.getControllerId()));
|
||||||
if (this instanceof TriggeredAbility) {
|
if (this instanceof TriggeredAbility) {
|
||||||
for (UUID modeId : this.getModes().getSelectedModes()) {
|
for (UUID modeId : this.getModes().getSelectedModes()) {
|
||||||
|
|
|
||||||
|
|
@ -24,23 +24,22 @@ public class DelayedTriggeredAbilities extends AbilitiesImpl<DelayedTriggeredAbi
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkTriggers(GameEvent event, Game game) {
|
public void checkTriggers(GameEvent event, Game game) {
|
||||||
if (this.size() > 0) {
|
// TODO: add same integrity checks as TriggeredAbilities?!
|
||||||
for (Iterator<DelayedTriggeredAbility> it = this.iterator(); it.hasNext(); ) {
|
for (Iterator<DelayedTriggeredAbility> it = this.iterator(); it.hasNext(); ) {
|
||||||
DelayedTriggeredAbility ability = it.next();
|
DelayedTriggeredAbility ability = it.next();
|
||||||
if (ability.getDuration() == Duration.Custom) {
|
if (ability.getDuration() == Duration.Custom) {
|
||||||
if (ability.isInactive(game)) {
|
if (ability.isInactive(game)) {
|
||||||
it.remove();
|
it.remove();
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ability.checkEventType(event, game)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ability.checkTrigger(event, game)) {
|
}
|
||||||
ability.trigger(game, ability.controllerId, event);
|
if (!ability.checkEventType(event, game)) {
|
||||||
if (ability.getTriggerOnlyOnce()) {
|
continue;
|
||||||
it.remove();
|
}
|
||||||
}
|
if (ability.checkTrigger(event, game)) {
|
||||||
|
ability.trigger(game, ability.controllerId, event);
|
||||||
|
if (ability.getTriggerOnlyOnce()) {
|
||||||
|
it.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import mage.constants.AbilityType;
|
||||||
import mage.constants.AsThoughEffectType;
|
import mage.constants.AsThoughEffectType;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import mage.game.Game;
|
import mage.game.Game;
|
||||||
|
import mage.players.Player;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,56 +8,203 @@ import mage.game.events.NumberOfTriggersEvent;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.game.stack.Spell;
|
import mage.game.stack.Spell;
|
||||||
import mage.util.CardUtil;
|
import mage.util.CardUtil;
|
||||||
|
import mage.util.Copyable;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author BetaSteward_at_googlemail.com
|
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||||
* <p>
|
|
||||||
* This class uses ConcurrentHashMap to avoid ConcurrentModificationExceptions.
|
|
||||||
* See ticket https://github.com/magefree/mage/issues/966 and
|
|
||||||
* https://github.com/magefree/mage/issues/473
|
|
||||||
*/
|
*/
|
||||||
public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbility> {
|
public class TriggeredAbilities extends LinkedHashMap<String, TriggeredAbility> implements Copyable<TriggeredAbilities> {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(TriggeredAbilities.class);
|
private static final Logger logger = Logger.getLogger(TriggeredAbilities.class);
|
||||||
|
|
||||||
private final Map<String, List<UUID>> sources = new HashMap<>();
|
private final Map<String, List<UUID>> sources = new HashMap<>();
|
||||||
|
|
||||||
|
// data integrity check for triggers
|
||||||
|
// reason: game engine can generate additional events and triggers while checking another one,
|
||||||
|
// it can generate multiple bugs, freeze, etc, see https://github.com/magefree/mage/issues/8426
|
||||||
|
// all checks can be catches by existing tests
|
||||||
|
private boolean enableIntegrityCheck1_MustKeepSameTriggersOrder = true; // good
|
||||||
|
private boolean enableIntegrityCheck2_MustKeepSameTriggersList = false; // bad, impossible to fix due dynamic triggers gen
|
||||||
|
private boolean enableIntegrityCheck3_CantStartEventProcessingBeforeFinishPrev = false; // bad, impossible to fix due dynamic triggers gen
|
||||||
|
private boolean enableIntegrityCheck4_EventMustProcessAllOldTriggers = true; // good
|
||||||
|
private boolean enableIntegrityCheck5_EventMustProcessInSameOrder = true; // good
|
||||||
|
private boolean enableIntegrityCheck6_EventMustNotProcessNewTriggers = false; // bad, impossible to fix due dynamic triggers gen
|
||||||
|
private boolean enableIntegrityLogs = false; // debug only
|
||||||
|
private boolean processingStarted = false;
|
||||||
|
private GameEvent.EventType processingStartedEvent = null; // null for game state triggers
|
||||||
|
private List<TriggeredAbility> processingNeed = new ArrayList<>();
|
||||||
|
private List<TriggeredAbility> processingDone = new ArrayList<>();
|
||||||
|
|
||||||
public TriggeredAbilities() {
|
public TriggeredAbilities() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected TriggeredAbilities(final TriggeredAbilities abilities) {
|
protected TriggeredAbilities(final TriggeredAbilities abilities) {
|
||||||
|
makeSureNotProcessing(null);
|
||||||
|
|
||||||
for (Map.Entry<String, TriggeredAbility> entry : abilities.entrySet()) {
|
for (Map.Entry<String, TriggeredAbility> entry : abilities.entrySet()) {
|
||||||
this.put(entry.getKey(), entry.getValue().copy());
|
this.put(entry.getKey(), entry.getValue().copy());
|
||||||
}
|
}
|
||||||
for (Map.Entry<String, List<UUID>> entry : abilities.sources.entrySet()) {
|
for (Map.Entry<String, List<UUID>> entry : abilities.sources.entrySet()) {
|
||||||
sources.put(entry.getKey(), entry.getValue());
|
sources.put(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.enableIntegrityCheck1_MustKeepSameTriggersOrder = abilities.enableIntegrityCheck1_MustKeepSameTriggersOrder;
|
||||||
|
this.enableIntegrityCheck2_MustKeepSameTriggersList = abilities.enableIntegrityCheck2_MustKeepSameTriggersList;
|
||||||
|
this.enableIntegrityCheck3_CantStartEventProcessingBeforeFinishPrev = abilities.enableIntegrityCheck3_CantStartEventProcessingBeforeFinishPrev;
|
||||||
|
this.enableIntegrityCheck4_EventMustProcessAllOldTriggers = abilities.enableIntegrityCheck4_EventMustProcessAllOldTriggers;
|
||||||
|
this.enableIntegrityCheck5_EventMustProcessInSameOrder = abilities.enableIntegrityCheck5_EventMustProcessInSameOrder;
|
||||||
|
this.enableIntegrityCheck6_EventMustNotProcessNewTriggers = abilities.enableIntegrityCheck6_EventMustNotProcessNewTriggers;
|
||||||
|
|
||||||
|
this.enableIntegrityLogs = abilities.enableIntegrityLogs;
|
||||||
|
this.processingStarted = abilities.processingStarted;
|
||||||
|
this.processingStartedEvent = abilities.processingStartedEvent;
|
||||||
|
this.processingNeed = CardUtil.deepCopyObject(abilities.processingNeed);
|
||||||
|
this.processingDone = CardUtil.deepCopyObject(abilities.processingDone);
|
||||||
|
|
||||||
|
// runtime check: triggers order (not required by paper rules, by required by xmage to make same result for all game instances)
|
||||||
|
if (this.enableIntegrityCheck1_MustKeepSameTriggersOrder) {
|
||||||
|
if (!Objects.equals(this.values().stream().findFirst().orElse(null) + "",
|
||||||
|
abilities.values().stream().findFirst().orElse(null) + "")) {
|
||||||
|
// how-to fix: use LinkedHashMap instead HashMap/ConcurrentHashMap
|
||||||
|
throw new IllegalStateException("Triggers integrity failed: triggers order changed");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkStateTriggers(Game game) {
|
public void checkStateTriggers(Game game) {
|
||||||
for (Iterator<TriggeredAbility> it = this.values().iterator(); it.hasNext(); ) {
|
makeSureNotProcessing(null);
|
||||||
TriggeredAbility ability = it.next();
|
|
||||||
if (ability instanceof StateTriggeredAbility && ((StateTriggeredAbility) ability).canTrigger(game)) {
|
processingStart(null);
|
||||||
checkTrigger(ability, null, game);
|
boolean needErrorChecksOnEnd = true;
|
||||||
|
try {
|
||||||
|
for (Iterator<TriggeredAbility> it = this.values().iterator(); it.hasNext(); ) {
|
||||||
|
TriggeredAbility ability = it.next();
|
||||||
|
if (ability instanceof StateTriggeredAbility && ((StateTriggeredAbility) ability).canTrigger(game)) {
|
||||||
|
checkTrigger(ability, null, game);
|
||||||
|
}
|
||||||
|
this.processingDone(ability);
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// need additional catch to show inner errors
|
||||||
|
needErrorChecksOnEnd = false;
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
processingEnd(needErrorChecksOnEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkTriggers(GameEvent event, Game game) {
|
public void checkTriggers(GameEvent event, Game game) {
|
||||||
for (Iterator<TriggeredAbility> it = this.values().iterator(); it.hasNext(); ) {
|
processingStart(event);
|
||||||
TriggeredAbility ability = it.next();
|
boolean needErrorChecksOnEnd = true;
|
||||||
if (ability.checkEventType(event, game)) {
|
// must keep real object refs (not copies), cause check trigger code can change trigger's and effect's data like targets
|
||||||
checkTrigger(ability, event, game);
|
ArrayList<TriggeredAbility> currentTriggers = new ArrayList<>(this.values());
|
||||||
|
try {
|
||||||
|
for (TriggeredAbility ability : currentTriggers) {
|
||||||
|
if (ability.checkEventType(event, game)) {
|
||||||
|
checkTrigger(ability, event, game);
|
||||||
|
}
|
||||||
|
this.processingDone(ability);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// need additional catch to show inner errors
|
||||||
|
needErrorChecksOnEnd = false;
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
processingEnd(needErrorChecksOnEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void makeSureNotProcessing(GameEvent newEvent) {
|
||||||
|
if (this.enableIntegrityCheck2_MustKeepSameTriggersList
|
||||||
|
&& this.processingStarted) {
|
||||||
|
List<String> info = new ArrayList<>();
|
||||||
|
info.add("old event: " + this.processingStartedEvent);
|
||||||
|
info.add("new event: " + newEvent.getType());
|
||||||
|
// how-to fix: impossible until mana events/triggers rework cause one mana event can generate additional events/triggers
|
||||||
|
throw new IllegalArgumentException("Triggers integrity failed: triggers can't be modified while processing - "
|
||||||
|
+ String.join(", ", info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processingStart(GameEvent newEvent) {
|
||||||
|
makeSureNotProcessing(newEvent);
|
||||||
|
|
||||||
|
this.processingStarted = true;
|
||||||
|
this.processingStartedEvent = newEvent == null ? null : newEvent.getType();
|
||||||
|
this.processingNeed.clear();
|
||||||
|
this.processingNeed.addAll(this.values());
|
||||||
|
this.processingDone.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processingDone(TriggeredAbility trigger) {
|
||||||
|
this.processingDone.add(trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processingEnd(boolean needErrorChecks) {
|
||||||
|
if (needErrorChecks) {
|
||||||
|
if (this.enableIntegrityCheck3_CantStartEventProcessingBeforeFinishPrev
|
||||||
|
&& !this.processingStarted) {
|
||||||
|
throw new IllegalArgumentException("Triggers integrity failed: can't finish event before start");
|
||||||
|
}
|
||||||
|
|
||||||
|
// must use ability's id to check equal (rules can be diff due usage of dynamic values - alternative to card hints)
|
||||||
|
List<UUID> needIds = new ArrayList<>();
|
||||||
|
String needInfo = this.processingNeed.stream()
|
||||||
|
.peek(a -> needIds.add(a.getId()))
|
||||||
|
.map(t -> "- " + t)
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
List<UUID> doneIds = new ArrayList<>();
|
||||||
|
String doneInfo = this.processingDone.stream()
|
||||||
|
.peek(a -> doneIds.add(a.getId()))
|
||||||
|
.map(t -> "- " + t)
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
String errorInfo = ""
|
||||||
|
+ "\n" + "Need: "
|
||||||
|
+ "\n" + (needInfo.isEmpty() ? "-" : needInfo)
|
||||||
|
+ "\n" + "Done: "
|
||||||
|
+ "\n" + (doneInfo.isEmpty() ? "-" : doneInfo);
|
||||||
|
|
||||||
|
if (this.enableIntegrityCheck4_EventMustProcessAllOldTriggers
|
||||||
|
&& this.processingDone.size() < this.processingNeed.size()) {
|
||||||
|
throw new IllegalArgumentException("Triggers integrity failed: event processing miss some triggers" + errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enableIntegrityCheck5_EventMustProcessInSameOrder
|
||||||
|
&& this.processingDone.size() > 0
|
||||||
|
&& this.processingDone.size() == this.processingNeed.size()
|
||||||
|
&& !needIds.toString().equals(doneIds.toString())) {
|
||||||
|
throw new IllegalArgumentException("Triggers integrity failed: event processing used wrong order" + errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enableIntegrityCheck6_EventMustNotProcessNewTriggers
|
||||||
|
&& this.processingDone.size() > this.processingNeed.size()) {
|
||||||
|
throw new IllegalArgumentException("Triggers integrity failed: event processing must not process new triggers" + errorInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processingStarted = false;
|
||||||
|
this.processingStartedEvent = null;
|
||||||
|
this.processingNeed.clear();
|
||||||
|
this.processingDone.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkTrigger(TriggeredAbility ability, GameEvent event, Game game) {
|
private void checkTrigger(TriggeredAbility ability, GameEvent event, Game game) {
|
||||||
// for effects like when leaves battlefield or destroyed use ShortLKI to check if permanent was in the correct zone before (e.g. Oblivion Ring or Karmic Justice)
|
// for effects like when leaves battlefield or destroyed use ShortLKI to check if permanent was in the correct zone before (e.g. Oblivion Ring or Karmic Justice)
|
||||||
|
if (this.enableIntegrityLogs) {
|
||||||
|
logger.info("---");
|
||||||
|
logger.info("checking trigger: " + ability);
|
||||||
|
logger.info("playable state: " + game.inCheckPlayableState());
|
||||||
|
logger.info(game);
|
||||||
|
logger.info("battlefield:" + "\n" + game.getBattlefield().getAllPermanents().stream()
|
||||||
|
.map(p -> "- " + p.toString())
|
||||||
|
.collect(Collectors.joining("\n")) + "\n");
|
||||||
|
}
|
||||||
MageObject object = game.getObject(ability.getSourceId());
|
MageObject object = game.getObject(ability.getSourceId());
|
||||||
if (ability.isInUseableZone(game, object, event)) {
|
if (ability.isInUseableZone(game, object, event)) {
|
||||||
if (event == null || !game.getContinuousEffects().preventedByRuleModification(event, ability, game, false)) {
|
if (event == null || !game.getContinuousEffects().preventedByRuleModification(event, ability, game, false)) {
|
||||||
|
|
@ -99,6 +246,9 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
|
||||||
if (event == null || !game.replaceEvent(numberOfTriggersEvent, ability)) {
|
if (event == null || !game.replaceEvent(numberOfTriggersEvent, ability)) {
|
||||||
int numTriggers = ability.getTriggersOnceEachTurn() ? 1 : numberOfTriggersEvent.getAmount();
|
int numTriggers = ability.getTriggersOnceEachTurn() ? 1 : numberOfTriggersEvent.getAmount();
|
||||||
for (int i = 0; i < numTriggers; i++) {
|
for (int i = 0; i < numTriggers; i++) {
|
||||||
|
if (this.enableIntegrityLogs) {
|
||||||
|
logger.info("trigger will be USED: " + ability);
|
||||||
|
}
|
||||||
ability.trigger(game, ability.getControllerId(), event);
|
ability.trigger(game, ability.getControllerId(), event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +265,7 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
|
||||||
* @param attachedTo - the object that gained the ability
|
* @param attachedTo - the object that gained the ability
|
||||||
*/
|
*/
|
||||||
public void add(TriggeredAbility ability, UUID sourceId, MageObject attachedTo) {
|
public void add(TriggeredAbility ability, UUID sourceId, MageObject attachedTo) {
|
||||||
|
makeSureNotProcessing(null);
|
||||||
if (sourceId == null) {
|
if (sourceId == null) {
|
||||||
add(ability, attachedTo);
|
add(ability, attachedTo);
|
||||||
} else if (attachedTo == null) {
|
} else if (attachedTo == null) {
|
||||||
|
|
@ -130,6 +281,7 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(TriggeredAbility ability, MageObject attachedTo) {
|
public void add(TriggeredAbility ability, MageObject attachedTo) {
|
||||||
|
makeSureNotProcessing(null);
|
||||||
this.put(getKey(ability, attachedTo), ability);
|
this.put(getKey(ability, attachedTo), ability);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,8 +314,8 @@ public class TriggeredAbilities extends ConcurrentHashMap<String, TriggeredAbili
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public TriggeredAbilities copy() {
|
public TriggeredAbilities copy() {
|
||||||
return new TriggeredAbilities(this);
|
return new TriggeredAbilities(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,12 +269,18 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
||||||
|
|
||||||
boolean isSimulation();
|
boolean isSimulation();
|
||||||
|
|
||||||
void setSimulation(boolean checkPlayableState);
|
/**
|
||||||
|
* Prepare game for any simulations like AI or effects calc
|
||||||
|
*/
|
||||||
|
Game createSimulationForAI();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare game for any playable calc (available mana/abilities)
|
||||||
|
*/
|
||||||
|
Game createSimulationForPlayableCalc();
|
||||||
|
|
||||||
boolean inCheckPlayableState();
|
boolean inCheckPlayableState();
|
||||||
|
|
||||||
void setCheckPlayableState(boolean checkPlayableState);
|
|
||||||
|
|
||||||
MageObject getLastKnownInformation(UUID objectId, Zone zone);
|
MageObject getLastKnownInformation(UUID objectId, Zone zone);
|
||||||
|
|
||||||
CardState getLastKnownInformationCard(UUID objectId, Zone zone);
|
CardState getLastKnownInformationCard(UUID objectId, Zone zone);
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,10 @@ public abstract class GameImpl implements Game {
|
||||||
|
|
||||||
private transient Object customData; // temporary data, used in AI simulations
|
private transient Object customData; // temporary data, used in AI simulations
|
||||||
private transient Player losingPlayer; // temporary data, used in AI simulations
|
private transient Player losingPlayer; // temporary data, used in AI simulations
|
||||||
protected boolean simulation = false;
|
|
||||||
protected boolean checkPlayableState = false;
|
protected boolean simulation = false; // for inner simulations (game without user messages)
|
||||||
|
protected boolean aiGame = false; // for inner simulations (ai game, debug only)
|
||||||
|
protected boolean checkPlayableState = false; // for inner playable calculations (game without user dialogs)
|
||||||
|
|
||||||
protected AtomicInteger totalErrorsCount = new AtomicInteger(); // for debug only: error stats
|
protected AtomicInteger totalErrorsCount = new AtomicInteger(); // for debug only: error stats
|
||||||
|
|
||||||
|
|
@ -180,6 +182,7 @@ public abstract class GameImpl implements Game {
|
||||||
protected GameImpl(final GameImpl game) {
|
protected GameImpl(final GameImpl game) {
|
||||||
//this.customData = game.customData; // temporary data, no need on game copy
|
//this.customData = game.customData; // temporary data, no need on game copy
|
||||||
//this.losingPlayer = game.losingPlayer; // temporary data, no need on game copy
|
//this.losingPlayer = game.losingPlayer; // temporary data, no need on game copy
|
||||||
|
this.aiGame = game.aiGame;
|
||||||
this.simulation = game.simulation;
|
this.simulation = game.simulation;
|
||||||
this.checkPlayableState = game.checkPlayableState;
|
this.checkPlayableState = game.checkPlayableState;
|
||||||
|
|
||||||
|
|
@ -248,13 +251,19 @@ public abstract class GameImpl implements Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setSimulation(boolean simulation) {
|
public Game createSimulationForAI() {
|
||||||
this.simulation = simulation;
|
Game res = this.copy();
|
||||||
|
((GameImpl) res).simulation = true;
|
||||||
|
((GameImpl) res).aiGame = true;
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setCheckPlayableState(boolean checkPlayableState) {
|
public Game createSimulationForPlayableCalc() {
|
||||||
this.checkPlayableState = checkPlayableState;
|
Game res = this.copy();
|
||||||
|
((GameImpl) res).simulation = true;
|
||||||
|
((GameImpl) res).checkPlayableState = true;
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1602,6 +1611,10 @@ public abstract class GameImpl implements Game {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playPriority(UUID activePlayerId, boolean resuming) {
|
public void playPriority(UUID activePlayerId, boolean resuming) {
|
||||||
|
if (!this.isSimulation() && this.inCheckPlayableState()) {
|
||||||
|
throw new IllegalStateException("Wrong code usage. Only simulation games can be in CheckPlayableState");
|
||||||
|
}
|
||||||
|
|
||||||
int priorityErrorsCount = 0;
|
int priorityErrorsCount = 0;
|
||||||
infiniteLoopCounter = 0;
|
infiniteLoopCounter = 0;
|
||||||
int rollbackBookmarkOnPriorityStart = 0;
|
int rollbackBookmarkOnPriorityStart = 0;
|
||||||
|
|
@ -1711,7 +1724,7 @@ public abstract class GameImpl implements Game {
|
||||||
throw new MageException(UNIT_TESTS_ERROR_TEXT);
|
throw new MageException(UNIT_TESTS_ERROR_TEXT);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setCheckPlayableState(false);
|
//setCheckPlayableState(false); // TODO: delete
|
||||||
}
|
}
|
||||||
state.getPlayerList().getNext();
|
state.getPlayerList().getNext();
|
||||||
}
|
}
|
||||||
|
|
@ -1730,7 +1743,7 @@ public abstract class GameImpl implements Game {
|
||||||
} finally {
|
} finally {
|
||||||
resetLKI();
|
resetLKI();
|
||||||
clearAllBookmarks();
|
clearAllBookmarks();
|
||||||
setCheckPlayableState(false);
|
//setCheckPlayableState(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4045,8 +4058,24 @@ public abstract class GameImpl implements Game {
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
Player activePayer = this.getPlayer(this.getActivePlayerId());
|
Player activePayer = this.getPlayer(this.getActivePlayerId());
|
||||||
|
|
||||||
|
// show non-standard game state (not part of the real game, e.g. AI or mana calculation)
|
||||||
|
List<String> simInfo = new ArrayList<>();
|
||||||
|
if (this.simulation) {
|
||||||
|
simInfo.add("SIMULATION");
|
||||||
|
}
|
||||||
|
if (this.aiGame) {
|
||||||
|
simInfo.add("AI");
|
||||||
|
}
|
||||||
|
if (this.checkPlayableState) {
|
||||||
|
simInfo.add("PLAYABLE CALC");
|
||||||
|
}
|
||||||
|
if (!ThreadUtils.isRunGameThread()) {
|
||||||
|
simInfo.add("NOT GAME THREAD");
|
||||||
|
}
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder()
|
StringBuilder sb = new StringBuilder()
|
||||||
.append(this.isSimulation() ? "!!!SIMULATION!!! " : "")
|
.append(!simInfo.isEmpty() ? "!!!" + String.join(", ", simInfo) + "!!! " : "")
|
||||||
.append(this.getGameType().toString())
|
.append(this.getGameType().toString())
|
||||||
.append("; ").append(CardUtil.getTurnInfo(this))
|
.append("; ").append(CardUtil.getTurnInfo(this))
|
||||||
.append("; active: ").append((activePayer == null ? "none" : activePayer.getName()))
|
.append("; active: ").append((activePayer == null ? "none" : activePayer.getName()))
|
||||||
|
|
|
||||||
|
|
@ -820,7 +820,7 @@ public interface Player extends MageItem, Copyable<Player> {
|
||||||
|
|
||||||
void updateRange(Game game);
|
void updateRange(Game game);
|
||||||
|
|
||||||
ManaOptions getManaAvailable(Game game);
|
ManaOptions getManaAvailable(Game originalGame);
|
||||||
|
|
||||||
void addAvailableTriggeredMana(List<Mana> netManaAvailable);
|
void addAvailableTriggeredMana(List<Mana> netManaAvailable);
|
||||||
|
|
||||||
|
|
@ -832,7 +832,7 @@ public interface Player extends MageItem, Copyable<Player> {
|
||||||
|
|
||||||
PlayableObjectsList getPlayableObjects(Game game, Zone zone);
|
PlayableObjectsList getPlayableObjects(Game game, Zone zone);
|
||||||
|
|
||||||
Map<UUID, ActivatedAbility> getPlayableActivatedAbilities(MageObject object, Zone zone, Game game);
|
Map<UUID, ActivatedAbility> getPlayableActivatedAbilities(MageObject object, Zone zone, Game originalGame);
|
||||||
|
|
||||||
boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game);
|
boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
//
|
//
|
||||||
// A card may be able to cast multiple way with multiple methods.
|
// A card may be able to cast multiple way with multiple methods.
|
||||||
// The specific MageIdentifier should be checked, before checking null as a fallback.
|
// The specific MageIdentifier should be checked, before checking null as a fallback.
|
||||||
|
// TODO: must rework playable methods to static
|
||||||
protected Map<UUID, Set<MageIdentifier>> castSourceIdWithAlternateMana = new HashMap<>();
|
protected Map<UUID, Set<MageIdentifier>> castSourceIdWithAlternateMana = new HashMap<>();
|
||||||
protected Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> castSourceIdManaCosts = new HashMap<>();
|
protected Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> castSourceIdManaCosts = new HashMap<>();
|
||||||
protected Map<UUID, Map<MageIdentifier, Costs<Cost>>> castSourceIdCosts = new HashMap<>();
|
protected Map<UUID, Map<MageIdentifier, Costs<Cost>>> castSourceIdCosts = new HashMap<>();
|
||||||
|
|
@ -1755,23 +1756,19 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
if (object instanceof StackAbility || object == null) {
|
if (object instanceof StackAbility || object == null) {
|
||||||
return useable;
|
return useable;
|
||||||
}
|
}
|
||||||
boolean previousState = game.inCheckPlayableState();
|
|
||||||
game.setCheckPlayableState(true);
|
|
||||||
try {
|
|
||||||
// collect and filter playable activated abilities
|
|
||||||
// GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right)
|
|
||||||
Set<UUID> needIds = CardUtil.getObjectParts(object);
|
|
||||||
|
|
||||||
// workaround to find all abilities first and filter it for one object
|
// collect and filter playable activated abilities
|
||||||
List<ActivatedAbility> allPlayable = getPlayable(game, true, zone, false);
|
// GUI: user clicks on card, but it must activate ability from ANY card's parts (main, left, right)
|
||||||
for (ActivatedAbility ability : allPlayable) {
|
Set<UUID> needIds = CardUtil.getObjectParts(object);
|
||||||
if (needIds.contains(ability.getSourceId())) {
|
|
||||||
useable.putIfAbsent(ability.getId(), ability);
|
// workaround to find all abilities first and filter it for one object
|
||||||
}
|
List<ActivatedAbility> allPlayable = getPlayable(game, true, zone, false);
|
||||||
|
for (ActivatedAbility ability : allPlayable) {
|
||||||
|
if (needIds.contains(ability.getSourceId())) {
|
||||||
|
useable.putIfAbsent(ability.getId(), ability);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
game.setCheckPlayableState(previousState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return useable;
|
return useable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3376,13 +3373,13 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
* combinations of mana are available to cast spells or activate abilities
|
* combinations of mana are available to cast spells or activate abilities
|
||||||
* etc.
|
* etc.
|
||||||
*
|
*
|
||||||
* @param game
|
* @param originalGame
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ManaOptions getManaAvailable(Game game) {
|
public ManaOptions getManaAvailable(Game originalGame) {
|
||||||
boolean oldState = game.inCheckPlayableState();
|
// workaround to fix a triggers list modification bug (game must be immutable on playable calculations)
|
||||||
game.setCheckPlayableState(true);
|
Game game = originalGame.createSimulationForPlayableCalc();
|
||||||
|
|
||||||
ManaOptions availableMana = new ManaOptions();
|
ManaOptions availableMana = new ManaOptions();
|
||||||
availableMana.addMana(manaPool.getMana());
|
availableMana.addMana(manaPool.getMana());
|
||||||
|
|
@ -3477,8 +3474,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
|
|
||||||
availableMana.removeFullyIncludedVariations();
|
availableMana.removeFullyIncludedVariations();
|
||||||
availableMana.remove(new Mana()); // Remove any empty mana that was left over from the way the code is written
|
availableMana.remove(new Mana()); // Remove any empty mana that was left over from the way the code is written
|
||||||
game.setCheckPlayableState(oldState);
|
|
||||||
return availableMana;
|
// make sure it independent of sim game
|
||||||
|
return availableMana.copy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3596,6 +3594,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ALTERNATIVE COST FROM dynamic effects
|
// ALTERNATIVE COST FROM dynamic effects
|
||||||
|
|
||||||
for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) {
|
for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) {
|
||||||
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier);
|
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier);
|
||||||
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier);
|
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier);
|
||||||
|
|
@ -4001,6 +4000,14 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
approvingObjects = game.getContinuousEffects().asThough(object.getId(),
|
approvingObjects = game.getContinuousEffects().asThough(object.getId(),
|
||||||
AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game);
|
AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: warning, PLAY_FROM_NOT_OWN_HAND_ZONE save some playable info in player's castSourceXXX fields
|
||||||
|
// it must be reworked (available/playable methods must be static, all play info must be stored in GameState)
|
||||||
|
// Current workaround to sync sim info with real game, remove here and from regexp search: asThough\(.+AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE
|
||||||
|
Player simPlayer = game.getPlayer(this.getId());
|
||||||
|
this.castSourceIdCosts = new HashMap<>(simPlayer.getCastSourceIdCosts());
|
||||||
|
this.castSourceIdManaCosts = new HashMap<>(simPlayer.getCastSourceIdManaCosts());
|
||||||
|
this.castSourceIdWithAlternateMana = new HashMap<>(simPlayer.getCastSourceIdWithAlternateMana());
|
||||||
} else {
|
} else {
|
||||||
// other abilities from direct zones
|
// other abilities from direct zones
|
||||||
approvingObjects = new HashSet<>();
|
approvingObjects = new HashSet<>();
|
||||||
|
|
@ -4057,21 +4064,20 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
* currently cast/activate with his available resources.
|
* currently cast/activate with his available resources.
|
||||||
* Without target validation.
|
* Without target validation.
|
||||||
*
|
*
|
||||||
* @param game
|
* @param originalGame
|
||||||
* @param hidden also from hidden objects (e.g. turned face down cards ?)
|
* @param hidden also from hidden objects (e.g. turned face down cards ?)
|
||||||
* @param fromZone of objects from which zone (ALL = from all zones)
|
* @param fromZone of objects from which zone (ALL = from all zones)
|
||||||
* @param hideDuplicatedAbilities if equal abilities exist return only the
|
* @param hideDuplicatedAbilities if equal abilities exist return only the
|
||||||
* first instance
|
* first instance
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public List<ActivatedAbility> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) {
|
public List<ActivatedAbility> getPlayable(Game originalGame, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) {
|
||||||
List<ActivatedAbility> playable = new ArrayList<>();
|
List<ActivatedAbility> playable = new ArrayList<>();
|
||||||
if (shouldSkipGettingPlayable(game)) {
|
if (shouldSkipGettingPlayable(originalGame)) {
|
||||||
return playable;
|
return playable;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean previousState = game.inCheckPlayableState();
|
Game game = originalGame.createSimulationForPlayableCalc();
|
||||||
game.setCheckPlayableState(true);
|
|
||||||
try {
|
try {
|
||||||
ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition))
|
ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition))
|
||||||
boolean fromAll = fromZone.equals(Zone.ALL);
|
boolean fromAll = fromZone.equals(Zone.ALL);
|
||||||
|
|
@ -4241,10 +4247,13 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
playable.addAll(activatedAll);
|
playable.addAll(activatedAll);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
game.setCheckPlayableState(previousState);
|
//game.setCheckPlayableState(previousState); // TODO: delete
|
||||||
}
|
}
|
||||||
|
|
||||||
return playable;
|
// make sure it independent of sim game
|
||||||
|
return playable.stream()
|
||||||
|
.map(ActivatedAbility::copy)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -4306,7 +4315,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
||||||
* @param game
|
* @param game
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private boolean shouldSkipGettingPlayable(Game game) {
|
static private boolean shouldSkipGettingPlayable(Game game) {
|
||||||
if (game.getStep() == null) { // happens at the start of the game
|
if (game.getStep() == null) { // happens at the start of the game
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,27 @@ public final class ThreadUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ensureRunInGameThread() {
|
public static void ensureRunInGameThread() {
|
||||||
String name = Thread.currentThread().getName();
|
if (!isRunGameThread()) {
|
||||||
if (!name.startsWith("GAME")) {
|
// for real games
|
||||||
// how-to fix: use signal logic to inform a game about new command to execute instead direct execute (see example with WantConcede)
|
// how-to fix: use signal logic to inform a game about new command to execute instead direct execute (see example with WantConcede)
|
||||||
// reason: user responses/commands are received by network/call thread, but must be processed by game thread
|
// reason: user responses/commands are received by network/call thread, but must be processed by game thread
|
||||||
throw new IllegalArgumentException("Wrong code usage: game related code must run in GAME thread, but it used in " + name, new Throwable());
|
//
|
||||||
|
// for unit tests
|
||||||
|
// how-to fix: if your test runner uses a diff thread name to run tests then add it to isRunGameThread
|
||||||
|
throw new IllegalArgumentException("Wrong code usage: game related code must run in GAME thread, but it used in " + Thread.currentThread().getName(), new Throwable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isRunGameThread() {
|
||||||
|
String name = Thread.currentThread().getName();
|
||||||
|
if (name.startsWith("GAME ")) {
|
||||||
|
// server game
|
||||||
|
return true;
|
||||||
|
} else if (name.equals("main")) {
|
||||||
|
// unit test
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue