mirror of
https://github.com/correl/mage.git
synced 2024-11-14 19:19:32 +00:00
* AI: fixed game freezes with Karn Liberated in the game (#7922);
This commit is contained in:
parent
d9e414db34
commit
b929b28e43
7 changed files with 192 additions and 59 deletions
|
@ -15,6 +15,8 @@ import mage.choices.Choice;
|
|||
import mage.constants.AbilityType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.RangeOfInfluence;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.combat.Combat;
|
||||
import mage.game.events.GameEvent;
|
||||
|
@ -40,8 +42,6 @@ import java.io.File;
|
|||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.StaticFilters;
|
||||
|
||||
/**
|
||||
* @author nantuko
|
||||
|
@ -215,7 +215,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
logger.trace("Add Action [" + depth + "] " + node.getAbilities().toString() + " a: " + alpha + " b: " + beta);
|
||||
}
|
||||
Game game = node.getGame();
|
||||
if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
|
||||
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
|
||||
&& Thread.interrupted()) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.debug("interrupted");
|
||||
|
@ -435,7 +435,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
pool.execute(task);
|
||||
try {
|
||||
int maxSeconds = maxThink;
|
||||
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS) {
|
||||
if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS) {
|
||||
maxSeconds = 3600;
|
||||
}
|
||||
logger.debug("maxThink: " + maxSeconds + " seconds ");
|
||||
|
@ -460,7 +460,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
}
|
||||
|
||||
protected int simulatePriority(SimulationNode2 node, Game game, int depth, int alpha, int beta) {
|
||||
if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
|
||||
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
|
||||
&& Thread.interrupted()) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.info("interrupted");
|
||||
|
@ -480,7 +480,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
int bestValSubNodes = Integer.MIN_VALUE;
|
||||
for (Ability action : allActions) {
|
||||
counter++;
|
||||
if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
|
||||
if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS
|
||||
&& Thread.interrupted()) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.info("Sim Prio [" + depth + "] -- interrupted");
|
||||
|
@ -492,7 +492,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
&& sim.getPlayer(currentPlayer.getId()).activateAbility((ActivatedAbility) action.copy(), sim)) {
|
||||
sim.applyEffects();
|
||||
if (checkForRepeatedAction(sim, node, action, currentPlayer.getId())) {
|
||||
logger.debug("Sim Prio [" + depth + "] -- repeated action: " + action.toString());
|
||||
logger.debug("Sim Prio [" + depth + "] -- repeated action: " + action);
|
||||
continue;
|
||||
}
|
||||
if (!sim.checkIfGameIsOver()
|
||||
|
@ -513,7 +513,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
} else {
|
||||
val = addActions(newNode, depth - 1, alpha, beta);
|
||||
}
|
||||
logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + counter + " <" + val + "> - (" + action.toString() + ") ");
|
||||
logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + counter + " <" + val + "> - (" + action + ") ");
|
||||
if (logger.isInfoEnabled()
|
||||
&& depth >= maxDepth) {
|
||||
StringBuilder sb = new StringBuilder("Sim Prio [").append(depth).append("] #").append(counter)
|
||||
|
@ -979,14 +979,15 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
protected Game createSimulation(Game game) {
|
||||
Game sim = game.copy();
|
||||
sim.setSimulation(true);
|
||||
for (Player copyPlayer : sim.getState().getPlayers().values()) {
|
||||
Player origPlayer = game.getState().getPlayers().get(copyPlayer.getId()).copy();
|
||||
for (Player oldPlayer : sim.getState().getPlayers().values()) {
|
||||
// replace original player by simulated player and find result (execute/resolve current action)
|
||||
Player origPlayer = game.getState().getPlayers().get(oldPlayer.getId()).copy();
|
||||
if (!suggestedActions.isEmpty()) {
|
||||
logger.debug(origPlayer.getName() + " suggested: " + suggestedActions);
|
||||
}
|
||||
SimulatedPlayer2 newPlayer = new SimulatedPlayer2(copyPlayer.getId(), copyPlayer.getId().equals(playerId), suggestedActions);
|
||||
newPlayer.restore(origPlayer);
|
||||
sim.getState().getPlayers().put(copyPlayer.getId(), newPlayer);
|
||||
SimulatedPlayer2 simPlayer = new SimulatedPlayer2(oldPlayer, oldPlayer.getId().equals(playerId), suggestedActions);
|
||||
simPlayer.restore(origPlayer);
|
||||
sim.getState().getPlayers().put(oldPlayer.getId(), simPlayer);
|
||||
}
|
||||
return sim;
|
||||
}
|
||||
|
@ -1069,7 +1070,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
|
|||
*
|
||||
* @param game
|
||||
* @param targets
|
||||
* @param format example: my %s in data
|
||||
* @param format example: my %s in data
|
||||
* @param emptyText default text for empty targets list
|
||||
* @return
|
||||
*/
|
||||
|
|
|
@ -31,14 +31,14 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
|||
public class SimulatedPlayer2 extends ComputerPlayer {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(SimulatedPlayer2.class);
|
||||
private static PassAbility pass = new PassAbility();
|
||||
private static final PassAbility pass = new PassAbility();
|
||||
private final boolean isSimulatedPlayer;
|
||||
private final List<String> suggested;
|
||||
private transient ConcurrentLinkedQueue<Ability> allActions;
|
||||
private boolean forced;
|
||||
|
||||
public SimulatedPlayer2(UUID id, boolean isSimulatedPlayer, List<String> suggested) {
|
||||
super(id);
|
||||
public SimulatedPlayer2(Player originalPlayer, boolean isSimulatedPlayer, List<String> suggested) {
|
||||
super(originalPlayer.getId());
|
||||
pass.setControllerId(playerId);
|
||||
this.isSimulatedPlayer = isSimulatedPlayer;
|
||||
this.suggested = suggested;
|
||||
|
@ -435,8 +435,15 @@ public class SimulatedPlayer2 extends ComputerPlayer {
|
|||
|
||||
@Override
|
||||
public boolean priority(Game game) {
|
||||
//should never get here
|
||||
// simulated player do nothing - it must pass until stack resolve to see final game score after action apply
|
||||
|
||||
// it's a workaround for Karn Liberated restart ability (see CommandersGameRestartTest)
|
||||
// reason: restarted game is broken (miss clear code of some game/player data?) and ai can't simulate it
|
||||
// so game is freezes on non empty stack (last part of karn's restart ability)
|
||||
if (game.getStack().isEmpty()) {
|
||||
game.pause();
|
||||
}
|
||||
pass(game);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -92,7 +92,11 @@ class KarnLiberatedEffect extends OneShotEffect {
|
|||
}
|
||||
}
|
||||
}
|
||||
game.getState().clear();
|
||||
|
||||
// dirty hack for game restart, can cause bugs due strange clear code (some data like ZCC keeping on new game)
|
||||
// see testCommanderRestoredToBattlefieldAfterKarnUltimate for more details
|
||||
|
||||
game.getState().clearOnGameRestart();
|
||||
// default watchers init, TODO: remove all restart/init code to game
|
||||
((GameImpl) game).initGameDefaultWatchers();
|
||||
|
||||
|
|
|
@ -209,5 +209,4 @@ public class CastCreaturesTest extends CardTestPlayerBaseAI {
|
|||
|
||||
assertPowerToughness(playerB, "Ammit Eternal", 4, 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
package org.mage.test.cards.continuous;
|
||||
|
||||
import mage.constants.CommanderCardType;
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.watchers.common.CommanderPlaysCountWatcher;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestCommander4PlayersWithAIHelps;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class CommandersGameRestartTest extends CardTestCommander4PlayersWithAIHelps {
|
||||
|
||||
@Test
|
||||
public void test_KarnLiberated_Manual() {
|
||||
// Player order: A -> D -> C -> B
|
||||
|
||||
addCard(Zone.COMMAND, playerA, "Balduvian Bears", 1); // {1}{G}, 2/2, commander
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
//
|
||||
// -14: Restart the game, leaving in exile all non-Aura permanent cards exiled with Karn Liberated.
|
||||
// Then put those cards onto the battlefield under your control.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Karn Liberated", 1);
|
||||
|
||||
// prepare commander
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1);
|
||||
|
||||
// prepare karn
|
||||
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Karn Liberated", CounterType.LOYALTY, 20);
|
||||
|
||||
// check watcher before restart
|
||||
runCode("before restart", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
|
||||
UUID commanderId = game.getCommandersIds(player, CommanderCardType.ANY, false).stream().findFirst().orElse(null);
|
||||
CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class);
|
||||
Assert.assertEquals("commander tax must be x1", 1, watcher.getPlaysCount(commanderId));
|
||||
});
|
||||
|
||||
// game restart
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-14: ");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
// check watcher after restart
|
||||
UUID commanderId = currentGame.getCommandersIds(playerA, CommanderCardType.ANY, false).stream().findFirst().orElse(null);
|
||||
CommanderPlaysCountWatcher watcher = currentGame.getState().getWatcher(CommanderPlaysCountWatcher.class);
|
||||
Assert.assertEquals("commander tax must be x0", 0, watcher.getPlaysCount(commanderId));
|
||||
|
||||
assertPermanentCount(playerA, 0); // no cards on battle after game restart
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_KarnLiberated_AI() {
|
||||
// Player order: A -> D -> C -> B
|
||||
|
||||
addCard(Zone.COMMAND, playerA, "Balduvian Bears", 1); // {1}{G}, 2/2, commander
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
//
|
||||
// -14: Restart the game, leaving in exile all non-Aura permanent cards exiled with Karn Liberated.
|
||||
// Then put those cards onto the battlefield under your control.
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Karn Liberated", 1);
|
||||
//
|
||||
addCard(Zone.HAND, playerB, "Balduvian Bears", 5);
|
||||
addCard(Zone.HAND, playerC, "Balduvian Bears", 5);
|
||||
addCard(Zone.HAND, playerD, "Balduvian Bears", 5);
|
||||
|
||||
// prepare commander
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1);
|
||||
|
||||
// prepare karn
|
||||
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Karn Liberated", CounterType.LOYALTY, 50);
|
||||
|
||||
// check watcher before restart
|
||||
runCode("before restart", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
|
||||
UUID commanderId = game.getCommandersIds(player, CommanderCardType.ANY, false).stream().findFirst().orElse(null);
|
||||
CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class);
|
||||
Assert.assertEquals("commander tax must be x1", 1, watcher.getPlaysCount(commanderId));
|
||||
});
|
||||
|
||||
// possible bug: ai can use restart in one of the simulations, so it can freeze the game (if bugged)
|
||||
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerB);
|
||||
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerC);
|
||||
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerD);
|
||||
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package org.mage.test.commander.duel;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
|
@ -10,8 +9,9 @@ import org.junit.Assert;
|
|||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestCommanderDuelBase;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public class CastBRGCommanderTest extends CardTestCommanderDuelBase {
|
||||
|
@ -97,22 +97,36 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase {
|
|||
addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 1);
|
||||
|
||||
// exile from hand 1
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA);
|
||||
addTarget(playerA, "Silvercoat Lion");
|
||||
|
||||
// prepare commander
|
||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Daxos of Meletis");
|
||||
|
||||
// exile from hand 2
|
||||
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA);
|
||||
addTarget(playerA, "Silvercoat Lion");
|
||||
|
||||
// attack and get commander damage
|
||||
attack(4, playerB, "Daxos of Meletis");
|
||||
|
||||
// exile commander
|
||||
activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "-3: Exile target permanent", "Daxos of Meletis");
|
||||
setChoice(playerB, "No"); // Move commander NOT to command zone
|
||||
|
||||
// exile from hand 3
|
||||
activateAbility(7, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA);
|
||||
addTarget(playerA, "Silvercoat Lion");
|
||||
|
||||
// restart game and return to battlefield 1x commander and 3x lions
|
||||
activateAbility(9, PhaseStep.PRECOMBAT_MAIN, playerA, "-14: Restart");
|
||||
// warning:
|
||||
// - karn restart code can clear some game data
|
||||
// - current version ignores a card's ZCC
|
||||
// - so ZCC are same after game restart and SBA can't react on commander new move
|
||||
// - logic can be changed in the future, so game can ask commander move again here
|
||||
//setChoice(playerB, "No"); // Move commander NOT to command zone
|
||||
|
||||
setStopAt(9, PhaseStep.BEGIN_COMBAT);
|
||||
|
||||
|
@ -127,8 +141,7 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase {
|
|||
assertPermanentCount(playerA, "Daxos of Meletis", 1); // Karn brings back the cards under the control of Karn's controller
|
||||
|
||||
CommanderInfoWatcher watcher = currentGame.getState().getWatcher(CommanderInfoWatcher.class, playerB.getCommandersIds().iterator().next());
|
||||
Assert.assertEquals("Watcher is reset to 0 commander damage", 0, (int) watcher.getDamageToPlayer().size());
|
||||
|
||||
Assert.assertEquals("Watcher is reset to 0 commander damage", 0, watcher.getDamageToPlayer().size());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -193,6 +193,45 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
this.commandersToStay.addAll(state.commandersToStay);
|
||||
}
|
||||
|
||||
public void clearOnGameRestart() {
|
||||
// special code for Karn Liberated
|
||||
// must clear game data on restart, but also must keep some info (wtf, why?)
|
||||
// if you catch freezes or bugs with Karn then research here
|
||||
// test example: testCommanderRestoredToBattlefieldAfterKarnUltimate
|
||||
// TODO: must be implemented as full data clear?
|
||||
|
||||
battlefield.clear();
|
||||
effects.clear();
|
||||
triggers.clear();
|
||||
delayed.clear();
|
||||
triggered.clear();
|
||||
stack.clear();
|
||||
exile.clear();
|
||||
command.clear();
|
||||
designations.clear();
|
||||
seenPlanes.clear();
|
||||
isPlaneChase = false;
|
||||
revealed.clear();
|
||||
lookedAt.clear();
|
||||
companion.clear();
|
||||
turnNum = 1;
|
||||
stepNum = 0;
|
||||
extraTurn = false;
|
||||
legendaryRuleActive = true;
|
||||
gameOver = false;
|
||||
specialActions.clear();
|
||||
cardState.clear();
|
||||
combat.clear();
|
||||
turnMods.clear();
|
||||
watchers.clear();
|
||||
values.clear();
|
||||
zones.clear();
|
||||
simultaneousEvents.clear();
|
||||
copiedCards.clear();
|
||||
usePowerInsteadOfToughnessForDamageLethalityFilters.clear();
|
||||
permanentOrderNumber = 0;
|
||||
}
|
||||
|
||||
public void restoreForRollBack(GameState state) {
|
||||
restore(state);
|
||||
this.turn = state.turn;
|
||||
|
@ -1124,7 +1163,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
if (attachedTo instanceof PermanentCard) {
|
||||
throw new IllegalArgumentException("Error, wrong code usage. If you want to add new ability to the "
|
||||
+ "permanent then use a permanent.addAbility(a, source, game): "
|
||||
+ ability.getClass().getCanonicalName() + " - " + ability.toString());
|
||||
+ ability.getClass().getCanonicalName() + " - " + ability);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1153,39 +1192,6 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
this.setManaBurn(false);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
battlefield.clear();
|
||||
effects.clear();
|
||||
triggers.clear();
|
||||
delayed.clear();
|
||||
triggered.clear();
|
||||
stack.clear();
|
||||
exile.clear();
|
||||
command.clear();
|
||||
designations.clear();
|
||||
seenPlanes.clear();
|
||||
isPlaneChase = false;
|
||||
revealed.clear();
|
||||
lookedAt.clear();
|
||||
companion.clear();
|
||||
turnNum = 0;
|
||||
stepNum = 0;
|
||||
extraTurn = false;
|
||||
legendaryRuleActive = true;
|
||||
gameOver = false;
|
||||
specialActions.clear();
|
||||
cardState.clear();
|
||||
combat.clear();
|
||||
turnMods.clear();
|
||||
watchers.clear();
|
||||
values.clear();
|
||||
zones.clear();
|
||||
simultaneousEvents.clear();
|
||||
copiedCards.clear();
|
||||
usePowerInsteadOfToughnessForDamageLethalityFilters.clear();
|
||||
permanentOrderNumber = 0;
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
this.paused = true;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue