* AI: fixed game freezes with Karn Liberated in the game (#7922);

This commit is contained in:
Oleg Agafonov 2021-07-02 15:03:06 +04:00
parent d9e414db34
commit b929b28e43
7 changed files with 192 additions and 59 deletions

View file

@ -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
*/

View file

@ -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;
}
}

View file

@ -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();

View file

@ -209,5 +209,4 @@ public class CastCreaturesTest extends CardTestPlayerBaseAI {
assertPowerToughness(playerB, "Ammit Eternal", 4, 4);
}
}

View file

@ -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();
}
}

View file

@ -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());
}
/**

View file

@ -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;
}