Merge branch 'simulation' into 'master'

Simulation fixes!

See merge request core-developers/forge!5890
This commit is contained in:
Michael Kamensky
2021-11-29 03:56:52 +00:00
23 changed files with 240 additions and 206 deletions

View File

@@ -358,7 +358,7 @@ public class AiAttackController {
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return ComputerUtilCombat.canAttackNextTurn(c) && c.getNetCombatDamage() > 0;
return c.getNetCombatDamage() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
}
});
for (final Card c : this.myList) {
@@ -422,7 +422,7 @@ public class AiAttackController {
int humanBasePower = ComputerUtilCombat.getAttack(this.oppList.get(0)) + humanExaltedBonus;
if (finestHour) {
// For Finest Hour, one creature could attack and get the bonus TWICE
humanBasePower = humanBasePower + humanExaltedBonus;
humanBasePower += humanExaltedBonus;
}
final int totalExaltedAttack = opp.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower
: humanBasePower;
@@ -470,7 +470,7 @@ public class AiAttackController {
if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) {
return true;
}
return ai.canReceiveCounters(CounterEnumType.POISON) && ai.getPoisonCounters() + totalPoison > 9;
return ai.getPoisonCounters() + totalPoison > 9 && ai.canReceiveCounters(CounterEnumType.POISON);
}
private boolean doAssault(final Player ai) {
@@ -672,9 +672,9 @@ public class AiAttackController {
*
* @return a {@link forge.game.combat.Combat} object.
*/
public final void declareAttackers(final Combat combat) {
public final int declareAttackers(final Combat combat) {
if (this.attackers.isEmpty()) {
return;
return aiAggression;
}
// Aggro options
@@ -686,6 +686,7 @@ public class AiAttackController {
boolean predictEvasion = false;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
if (!aic.usesSimulation()) {
playAggro = aic.getBooleanProperty(AiProps.PLAY_AGGRO);
chanceToAttackToTrade = aic.getIntProperty(AiProps.CHANCE_TO_ATTACK_INTO_TRADE);
tradeIfTappedOut = aic.getBooleanProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
@@ -693,6 +694,7 @@ public class AiAttackController {
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
predictEvasion = aic.getBooleanProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION);
}
}
final boolean bAssault = doAssault(ai);
// TODO: detect Lightmine Field by presence of a card with a specific trigger
@@ -714,7 +716,7 @@ public class AiAttackController {
if (attackMax == 0) {
// can't attack anymore
return;
return aiAggression;
}
// Attackers that don't really have a choice
@@ -751,7 +753,7 @@ public class AiAttackController {
}
}
if (attackersLeft.isEmpty()) {
return;
return aiAggression;
}
}
@@ -761,7 +763,7 @@ public class AiAttackController {
}
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily
if (!doRevengeOfRavensAttackLogic(ai, defender, attackersLeft, numForcedAttackers, attackMax)) {
return;
return aiAggression;
}
if (bAssault && defender == this.defendingOpponent) { // in case we are forced to attack someone else
@@ -771,14 +773,14 @@ public class AiAttackController {
for (Card attacker : attackersLeft) {
// reached max, breakup
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
return;
return aiAggression;
if (canAttackWrapper(attacker, defender) && isEffectiveAttacker(ai, attacker, combat, defender)) {
combat.addAttacker(attacker, defender);
}
}
// no more creatures to attack
return;
return aiAggression;
}
// Cards that are remembered to attack anyway (e.g. temporarily stolen creatures)
@@ -816,7 +818,7 @@ public class AiAttackController {
for (Card attacker : this.attackers) {
if (canAttackWrapper(attacker, defender) && shouldAttack(ai, attacker, this.blockers, combat, defender)) {
combat.addAttacker(attacker, defender);
return;
return aiAggression;
}
}
}
@@ -835,7 +837,7 @@ public class AiAttackController {
}
}
// no more creatures to attack
return;
return aiAggression;
}
// *******************
@@ -877,7 +879,7 @@ public class AiAttackController {
for (final Card pCard : categorizedOppList) {
// if the creature can attack next turn add it to counter attackers list
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
if (pCard.getNetCombatDamage() > 0 && ComputerUtilCombat.canAttackNextTurn(pCard)) {
nextTurnAttackers.add(pCard);
candidateCounterAttackDamage += pCard.getNetCombatDamage();
humanForces += 1; // player forces they might use to attack
@@ -1108,6 +1110,8 @@ public class AiAttackController {
defender = pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
}
}
return aiAggression;
}
/**
@@ -1133,8 +1137,7 @@ public class AiAttackController {
int numberOfPossibleBlockers = 0;
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
if ((attacker.hasSVar("NonCombatPriority"))
&& (!attacker.hasKeyword(Keyword.VIGILANCE))) {
if (attacker.hasSVar("NonCombatPriority") && !attacker.hasKeyword(Keyword.VIGILANCE)) {
// For each level of priority, enemy has to have life as much as the creature's power
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
// the lower the priroity, the less willing the AI is to use the creature for attacking.
@@ -1159,8 +1162,7 @@ public class AiAttackController {
}
boolean hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasStartOfKeyword("Annihilator");
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"));
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, new Predicate<Card>() {
@@ -1318,7 +1320,7 @@ public class AiAttackController {
return false; // don't attack
}
public static List<Card> exertAttackers(List<Card> attackers) {
public static List<Card> exertAttackers(List<Card> attackers, int aggression) {
List<Card> exerters = Lists.newArrayList();
for (Card c : attackers) {
boolean shouldExert = false;
@@ -1362,9 +1364,14 @@ public class AiAttackController {
continue;
}
if (!shouldExert) {
// TODO Improve when the AI wants to use Exert powers
shouldExert = aggression > 3;
}
// A specific AI condition for Exert: if specified on the card, the AI will always
// exert creatures that meet this condition
if (c.hasSVar("AIExertCondition")) {
if (!shouldExert && c.hasSVar("AIExertCondition")) {
if (!c.getSVar("AIExertCondition").isEmpty()) {
final String needsToExert = c.getSVar("AIExertCondition");
String sVar = needsToExert.split(" ")[0];
@@ -1379,11 +1386,6 @@ public class AiAttackController {
}
}
if (!shouldExert && MyRandom.getRandom().nextBoolean()) {
// TODO Improve when the AI wants to use Exert powers
shouldExert = true;
}
if (shouldExert) {
exerters.add(c);
}

View File

@@ -457,7 +457,7 @@ public class AiBlockController {
return false;
}
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker) || randomTrade;
return lifeInDanger || randomTrade || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
}
});
if (usableBlockers.size() < 2) {
@@ -854,7 +854,7 @@ public class AiBlockController {
}
private void makeChumpBlocksToSavePW(Combat combat) {
if (ComputerUtilCombat.lifeInDanger(ai, combat) || ai.getLife() <= ai.getStartingLife() / 5) {
if (ai.getLife() <= ai.getStartingLife() / 5 || ComputerUtilCombat.lifeInDanger(ai, combat)) {
// most likely not worth trying to protect planeswalkers when at threateningly low life or in
// dangerous combat which threatens lethal or severe damage to face
return;
@@ -1145,7 +1145,7 @@ public class AiBlockController {
// check to see if it's possible to defend a Planeswalker under attack with a chump block,
// unless life is low enough to be more worried about saving preserving the life total
if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) {
if (ai.getController().isAI()) {
makeChumpBlocksToSavePW(combat);
}
@@ -1206,7 +1206,7 @@ public class AiBlockController {
final CardCollection result = new CardCollection();
boolean newBlockerIsAdded = false;
// The new blocker comes right after this one
final Card newBlockerRightAfter = (newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1));
final Card newBlockerRightAfter = newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1);
if (newBlockerRightAfter == null
&& damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
result.add(blocker);
@@ -1274,6 +1274,8 @@ public class AiBlockController {
int oppCreatureCount = 0;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
// simulation must get same results or it may crash
if (!aic.usesSimulation()) {
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
@@ -1285,6 +1287,7 @@ public class AiBlockController {
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
}
}
if (!enableRandomTrades) {
return false;

View File

@@ -88,6 +88,7 @@ public class AiController {
private boolean cheatShuffle;
private boolean useSimulation;
private SpellAbilityPicker simPicker;
private int lastAttackAggression;
public boolean canCheatShuffle() {
return cheatShuffle;
@@ -104,6 +105,10 @@ public class AiController {
this.useSimulation = value;
}
public int getAttackAggression() {
return lastAttackAggression;
}
public SpellAbilityPicker getSimulationPicker() {
return simPicker;
}
@@ -1445,7 +1450,7 @@ public class AiController {
public void declareAttackers(Player attacker, Combat combat) {
// 12/2/10(sol) the decision making here has moved to getAttackers()
AiAttackController aiAtk = new AiAttackController(attacker);
aiAtk.declareAttackers(combat);
lastAttackAggression = aiAtk.declareAttackers(combat);
// if invalid: just try an attack declaration that we know to be legal
if (!CombatUtil.validateAttackers(combat)) {

View File

@@ -1483,6 +1483,7 @@ public class ComputerUtil {
if (sa.getApi() != ApiType.DealDamage) {
continue;
}
sa.setActivatingPlayer(ai);
final String numDam = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), numDam, sa);
if (dmg <= damage) {

View File

@@ -264,6 +264,7 @@ public class ComputerUtilCard {
return ut;
}
// TODO potentially risky if simulation mode currently able to reach this from triggers
return Aggregates.random(bLand); // random tapped land of least represented type
}
@@ -743,7 +744,7 @@ public class ComputerUtilCard {
final String name = c.getName();
Integer currentCnt = map.get(name);
map.put(name, currentCnt == null ? Integer.valueOf(1) : Integer.valueOf(1 + currentCnt));
} // for
}
int max = 0;
String maxName = "";
@@ -1296,12 +1297,16 @@ public class ComputerUtilCard {
boolean combatTrick = false;
boolean holdCombatTricks = false;
int chanceToHoldCombatTricks = -1;
boolean simAI = false;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
simAI = aic.usesSimulation();
if (!simAI) {
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
}
}
if (!c.canBeTargetedBy(sa)) {
return false;
@@ -1629,7 +1634,7 @@ public class ComputerUtilCard {
}
}
return MyRandom.getRandom().nextFloat() < chance;
return simAI || MyRandom.getRandom().nextFloat() < chance;
}
/**

View File

@@ -160,7 +160,7 @@ public class ComputerUtilCombat {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return CombatUtil.canBlock(att, c) && (c.hasFirstStrike() || c.hasDoubleStrike());
return (c.hasFirstStrike() || c.hasDoubleStrike()) && CombatUtil.canBlock(att, c);
}
});
@@ -324,12 +324,12 @@ public class ComputerUtilCombat {
for (final Card attacker : attackers) {
final List<Card> blockers = combat.getBlockers(attacker);
if ((blockers.size() == 0)
if (blockers.size() == 0
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage "
+ "as though it weren't blocked.")) {
unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (getAttack(attacker) > totalShieldDamage(attacker, blockers))) {
&& getAttack(attacker) > totalShieldDamage(attacker, blockers)) {
if (!attacker.hasKeyword(Keyword.INFECT)) {
damage += getAttack(attacker) - totalShieldDamage(attacker, blockers);
}
@@ -366,12 +366,12 @@ public class ComputerUtilCombat {
for (final Card attacker : attackers) {
final List<Card> blockers = combat.getBlockers(attacker);
if ((blockers.size() == 0)
if (blockers.size() == 0
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage"
+ " as though it weren't blocked.")) {
unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (getAttack(attacker) > totalShieldDamage(attacker, blockers))) {
&& getAttack(attacker) > totalShieldDamage(attacker, blockers)) {
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (attacker.hasKeyword(Keyword.INFECT)) {
poison += trampleDamage;
@@ -617,7 +617,7 @@ public class ComputerUtilCombat {
if (flankingMagnitude >= defender.getNetToughness()) {
return 0;
}
if (flankingMagnitude >= (defender.getNetToughness() - defender.getDamage())
if (flankingMagnitude >= defender.getNetToughness() - defender.getDamage()
&& !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
@@ -1660,7 +1660,7 @@ public class ComputerUtilCombat {
*/
public static boolean combatantCantBeDestroyed(Player ai, final Card combatant) {
// either indestructible or may regenerate
if (combatant.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, combatant))) {
if (combatant.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, combatant)) {
return true;
}
@@ -1715,7 +1715,7 @@ public class ComputerUtilCombat {
if (flankingMagnitude >= blocker.getNetToughness()) {
return false;
}
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
if (flankingMagnitude >= blocker.getNetToughness() - blocker.getDamage()
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return false;
}

View File

@@ -330,7 +330,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List<Card> exertAttackers(List<Card> attackers) {
return AiAttackController.exertAttackers(attackers);
return AiAttackController.exertAttackers(attackers, brains.getAttackAggression());
}
@Override

View File

@@ -916,7 +916,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return ComputerUtilCombat.canAttackNextTurn(c) && c.getNetPower() > 0;
return c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
}
});
}
@@ -1278,7 +1278,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.isCreature()) {
return true;
}
return ComputerUtilCombat.canAttackNextTurn(c) && powerBonus + c.getNetPower() > 0;
return powerBonus + c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
}
});
}
@@ -1588,8 +1588,8 @@ public class AttachAi extends SpellAbilityAi {
if (evasive) {
return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.equals("Haste")) {
return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped()
&& card.getNetCombatDamage() + powerBonus > 0
@@ -1609,8 +1609,8 @@ public class AttachAi extends SpellAbilityAi {
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.startsWith("Bushido")) {
return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card))
|| CombatUtil.canBlock(card, true);
@@ -1652,9 +1652,9 @@ public class AttachAi extends SpellAbilityAi {
if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender")
|| keyword.endsWith("CARDNAME can't attack or block.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
return card.getNetCombatDamage() >= 1 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty();
return !ai.getCreaturesInPlay().isEmpty() && ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME can't block.")) {
return CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
@@ -1665,10 +1665,10 @@ public class AttachAi extends SpellAbilityAi {
}
return false;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
return card.getNetCombatDamage() >= 1 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 2;
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
}

View File

@@ -8,8 +8,11 @@ import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import forge.LobbyPlayer;
import forge.ai.AIOption;
import forge.ai.LobbyPlayerAi;
import forge.game.Game;
import forge.game.GameEntity;
@@ -64,9 +67,9 @@ public class GameCopier {
}
public Game makeCopy() {
return makeCopy(null);
return makeCopy(null, null);
}
public Game makeCopy(PhaseType advanceToPhase) {
public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) {
List<RegisteredPlayer> origPlayers = origGame.getMatch().getPlayers();
List<RegisteredPlayer> newPlayers = new ArrayList<>();
for (RegisteredPlayer p : origPlayers) {
@@ -82,8 +85,7 @@ public class GameCopier {
newPlayer.setActivateLoyaltyAbilityThisTurn(origPlayer.getActivateLoyaltyAbilityThisTurn());
for (int j = 0; j < origPlayer.getSpellsCastThisTurn(); j++)
newPlayer.addSpellCastThisTurn();
for (int j = 0; j < origPlayer.getLandsPlayedThisTurn(); j++)
newPlayer.addLandPlayedThisTurn();
newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
@@ -138,18 +140,25 @@ public class GameCopier {
effect.removeMapped(gameObjectMap);
}
if (origPhaseHandler.getCombat() != null) {
newGame.getPhaseHandler().setCombat(new Combat(origPhaseHandler.getCombat(), gameObjectMap));
}
newGame.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
newGame.getTriggerHandler().resetActiveTriggers();
if (GameSimulator.COPY_STACK)
copyStack(origGame, newGame, gameObjectMap);
if (origPhaseHandler.getCombat() != null) {
newGame.getPhaseHandler().setCombat(new Combat(origPhaseHandler.getCombat(), gameObjectMap));
}
// TODO update thisTurnCast
if (advanceToPhase != null) {
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase);
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase, new Runnable() {
@Override
public void run() {
GameSimulator.resolveStack(newGame, aiPlayer.getWeakestOpponent());
}
});
}
return newGame;
@@ -185,7 +194,8 @@ public class GameCopier {
RegisteredPlayer clone = new RegisteredPlayer(p.getDeck());
LobbyPlayer lp = p.getPlayer();
if (!(lp instanceof LobbyPlayerAi)) {
lp = new LobbyPlayerAi(p.getPlayer().getName(), null);
// TODO should probably also override them if they're normal AI
lp = new LobbyPlayerAi(p.getPlayer().getName(), Sets.newHashSet(AIOption.USE_SIMULATION));
}
clone.setPlayer(lp);
return clone;
@@ -197,6 +207,7 @@ public class GameCopier {
for (Card card : origGame.getCardsIn(zone)) {
addCard(newGame, zone, card);
}
// TODO CardsAddedThisTurn is now messed up
}
gameObjectMap = new CopiedGameObjectMap(newGame);
@@ -271,11 +282,14 @@ public class GameCopier {
cardMap.put(c, newCard);
Player zoneOwner = owner;
// everything the CreatureEvaluator checks must be set here
if (zone == ZoneType.Battlefield) {
// TODO: Controllers' list with timestamps should be copied.
zoneOwner = playerMap.get(c.getController());
newCard.setController(zoneOwner, 0);
newCard.setCameUnderControlSinceLastUpkeep(c.cameUnderControlSinceLastUpkeep());
newCard.setPTTable(c.getSetPTTable());
newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable());
@@ -290,9 +304,11 @@ public class GameCopier {
newCard.setChangedCardKeywords(c.getChangedCardKeywords());
newCard.setChangedCardNames(c.getChangedCardNames());
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such?
//for (KeywordInterface kw : c.getHiddenExtrinsicKeywords())
// newCard.addHiddenExtrinsicKeyword(kw);
for (Table.Cell<Long, Long, List<String>> kw : c.getHiddenExtrinsicKeywordsTable().cellSet()) {
newCard.addHiddenExtrinsicKeywords(kw.getRowKey(), kw.getColumnKey(), kw.getValue());
}
newCard.updateKeywordsCache(newCard.getCurrentState());
if (c.isTapped()) {
newCard.setTapped(true);
}

View File

@@ -31,7 +31,7 @@ public class GameSimulator {
public GameSimulator(SimulationController controller, Game origGame, Player origAiPlayer, PhaseType advanceToPhase) {
this.controller = controller;
copier = new GameCopier(origGame);
simGame = copier.makeCopy(advanceToPhase);
simGame = copier.makeCopy(advanceToPhase, origAiPlayer);
aiPlayer = (Player) copier.find(origAiPlayer);
eval = new GameStateEvaluator();
@@ -74,6 +74,8 @@ public class GameSimulator {
eval.getScoreForGameState(origGame, origAiPlayer);
// Print debug info.
printDiff(origLines, simLines);
// make sure it gets printed
System.out.flush();
throw new RuntimeException("Game copy error. See diff output above for details.");
}
eval.setDebugging(false);
@@ -230,8 +232,10 @@ public class GameSimulator {
return score;
}
private static void resolveStack(final Game game, final Player opponent) {
public static void resolveStack(final Game game, final Player opponent) {
// TODO: This needs to set an AI controller for all opponents, in case of multiplayer.
PlayerControllerAi sim = new PlayerControllerAi(game, opponent, opponent.getLobbyPlayer());
sim.setUseSimulation(true);
opponent.runWithController(new Runnable() {
@Override
public void run() {
@@ -259,7 +263,7 @@ public class GameSimulator {
// Continue until stack is empty.
}
}
}, new PlayerControllerAi(game, opponent, opponent.getLobbyPlayer()));
}, sim);
}
public Game getSimulatedGameState() {

View File

@@ -24,7 +24,7 @@ public class GameStateEvaluator {
public GameCopier copier;
public Game gameCopy;
}
private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame) {
private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame, final Player aiPlayer) {
PhaseType phase = evalGame.getPhaseHandler().getPhase();
if (phase.isAfter(PhaseType.COMBAT_DAMAGE) || evalGame.isGameOver()) {
return null;
@@ -38,7 +38,12 @@ public class GameStateEvaluator {
}
GameCopier copier = new GameCopier(evalGame);
Game gameCopy = copier.makeCopy();
gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE);
gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE, new Runnable() {
@Override
public void run() {
GameSimulator.resolveStack(gameCopy, aiPlayer.getWeakestOpponent());
}
});
CombatSimResult result = new CombatSimResult();
result.copier = copier;
result.gameCopy = gameCopy;
@@ -67,7 +72,7 @@ public class GameStateEvaluator {
return getScoreForGameOver(game, aiPlayer);
}
CombatSimResult result = simulateUpcomingCombatThisTurn(game);
CombatSimResult result = simulateUpcomingCombatThisTurn(game, aiPlayer);
if (result != null) {
Player aiPlayerCopy = (Player) result.copier.find(aiPlayer);
if (result.gameCopy.isGameOver()) {

View File

@@ -101,7 +101,7 @@ public class SpellAbilityPicker {
}
public SpellAbility chooseSpellAbilityToPlay(SimulationController controller) {
printOutput = (controller == null);
printOutput = controller == null;
// Pass if top of stack is owned by me.
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {

View File

@@ -56,11 +56,7 @@ public class DelayedTriggerEffect extends SpellAbilityEffect {
if (sa.hasParam("RememberObjects")) {
for (final String rem : sa.getParam("RememberObjects").split(",")) {
for (final Object o : AbilityUtils.getDefinedObjects(sa.getHostCard(), rem, sa)) {
if (o instanceof SpellAbility) {
// "RememberObjects$ Remembered" don't remember spellability
continue;
}
for (final Object o : AbilityUtils.getDefinedEntities(sa.getHostCard(), rem, sa)) {
delTrig.addRemembered(o);
}
}

View File

@@ -81,7 +81,7 @@ public class EffectEffect extends SpellAbilityEffect {
if (sa.hasParam("RememberObjects")) {
rememberList = new FCollection<>();
for (final String rem : sa.getParam("RememberObjects").split(",")) {
rememberList.addAll(AbilityUtils.getDefinedObjects(hostCard, rem, sa));
rememberList.addAll(AbilityUtils.getDefinedEntities(hostCard, rem, sa));
}
if (sa.hasParam("ForgetCounter")) {

View File

@@ -59,11 +59,7 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect {
if (sa.hasParam("RememberObjects")) {
for (final String rem : sa.getParam("RememberObjects").split(",")) {
for (final Object o : AbilityUtils.getDefinedObjects(sa.getHostCard(), rem, sa)) {
if (o instanceof SpellAbility) {
// "RememberObjects$ Remembered" don't remember spellability
continue;
}
for (final Object o : AbilityUtils.getDefinedEntities(sa.getHostCard(), rem, sa)) {
immediateTrig.addRemembered(o);
}
}

View File

@@ -3617,7 +3617,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public Iterable<KeywordsChange> getChangedCardKeywordsList() {
return Iterables.concat(
changedCardKeywordsByText.values(), // Layer 3
ImmutableList.of(new KeywordsChange(ImmutableList.<String>of(), null, this.hasRemoveIntrinsic())), // Layer 4
@@ -3629,7 +3628,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return changedCardKeywords;
}
public Table<Long, Long, CardColor> getChangedCardColorsTable() {
return changedCardColors;
}
@@ -4561,6 +4559,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public final Iterable<String> getHiddenExtrinsicKeywords() {
return Iterables.concat(this.hiddenExtrinsicKeywords.values());
}
public final Table<Long, Long, List<String>> getHiddenExtrinsicKeywordsTable() {
return hiddenExtrinsicKeywords;
}
public final void addHiddenExtrinsicKeywords(long timestamp, long staticId, Iterable<String> keywords) {
// TODO if some keywords aren't removed anymore, then no need for extra Array List

View File

@@ -1036,7 +1036,7 @@ public class CombatUtil {
* @return a boolean.
*/
public static boolean canBlock(final Card attacker, final Card blocker, final boolean nextTurn) {
if ((attacker == null) || (blocker == null)) {
if (attacker == null || blocker == null) {
return false;
}

View File

@@ -1162,11 +1162,17 @@ public class PhaseHandler implements java.io.Serializable {
}
public final boolean devAdvanceToPhase(PhaseType targetPhase) {
return devAdvanceToPhase(targetPhase, null);
}
public final boolean devAdvanceToPhase(PhaseType targetPhase, Runnable resolver) {
boolean isTopsy = playerTurn.getAmountOfKeyword("The phases of your turn are reversed.") % 2 == 1;
while (phase.isBefore(targetPhase, isTopsy)) {
if (checkStateBasedEffects()) {
return false;
}
if (resolver != null) {
resolver.run();
}
onPhaseEnd();
advanceToNextPhase();
onPhaseBegin();

View File

@@ -855,11 +855,7 @@ public final class StaticAbilityContinuous {
if (params.containsKey("TriggerRememberDefined")) {
String triggerRemembered = (params.get("TriggerRememberDefined"));
for (final String rem : triggerRemembered.split(",")) {
for (final Object o : AbilityUtils.getDefinedObjects(hostCard, rem, stAb)) {
if (o instanceof SpellAbility) {
// "RememberObjects$ Remembered" don't remember spellability
continue;
}
for (final Object o : AbilityUtils.getDefinedEntities(hostCard, rem, stAb)) {
actualTrigger.addRemembered(o);
}
}

View File

@@ -81,11 +81,9 @@ public class Zone implements java.io.Serializable, Iterable<Card> {
public final void add(final Card c) {
add(c, null);
}
public final void add(final Card c, final Integer index) {
add(c, index, null);
}
public void add(final Card c, Integer index, final Card latestState) {
if (index != null && cardList.isEmpty() && index.intValue() > 0) {
// something went wrong, most likely the method fired when the game was in an unexpected state