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

View File

@@ -457,7 +457,7 @@ public class AiBlockController {
return false; return false;
} }
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat); 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) { if (usableBlockers.size() < 2) {
@@ -854,7 +854,7 @@ public class AiBlockController {
} }
private void makeChumpBlocksToSavePW(Combat combat) { 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 // 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 // dangerous combat which threatens lethal or severe damage to face
return; 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, // 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 // 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); makeChumpBlocksToSavePW(combat);
} }
@@ -1206,7 +1206,7 @@ public class AiBlockController {
final CardCollection result = new CardCollection(); final CardCollection result = new CardCollection();
boolean newBlockerIsAdded = false; boolean newBlockerIsAdded = false;
// The new blocker comes right after this one // 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 if (newBlockerRightAfter == null
&& damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) { && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
result.add(blocker); result.add(blocker);
@@ -1274,16 +1274,19 @@ public class AiBlockController {
int oppCreatureCount = 0; int oppCreatureCount = 0;
if (ai.getController().isAI()) { if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK); // simulation must get same results or it may crash
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS); if (!aic.usesSimulation()) {
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT); enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK); randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK); randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM); minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE); maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL); chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER); maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE);
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER); maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL);
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
}
} }
if (!enableRandomTrades) { if (!enableRandomTrades) {

View File

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

View File

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

View File

@@ -264,6 +264,7 @@ public class ComputerUtilCard {
return ut; 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 return Aggregates.random(bLand); // random tapped land of least represented type
} }
@@ -743,7 +744,7 @@ public class ComputerUtilCard {
final String name = c.getName(); final String name = c.getName();
Integer currentCnt = map.get(name); Integer currentCnt = map.get(name);
map.put(name, currentCnt == null ? Integer.valueOf(1) : Integer.valueOf(1 + currentCnt)); map.put(name, currentCnt == null ? Integer.valueOf(1) : Integer.valueOf(1 + currentCnt));
} // for }
int max = 0; int max = 0;
String maxName = ""; String maxName = "";
@@ -1296,11 +1297,15 @@ public class ComputerUtilCard {
boolean combatTrick = false; boolean combatTrick = false;
boolean holdCombatTricks = false; boolean holdCombatTricks = false;
int chanceToHoldCombatTricks = -1; int chanceToHoldCombatTricks = -1;
boolean simAI = false;
if (ai.getController().isAI()) { if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK); simAI = aic.usesSimulation();
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK); 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)) { if (!c.canBeTargetedBy(sa)) {
@@ -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>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { 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) { for (final Card attacker : attackers) {
final List<Card> blockers = combat.getBlockers(attacker); 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 " || attacker.hasKeyword("You may have CARDNAME assign its combat damage "
+ "as though it weren't blocked.")) { + "as though it weren't blocked.")) {
unblocked.add(attacker); unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE) } else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (getAttack(attacker) > totalShieldDamage(attacker, blockers))) { && getAttack(attacker) > totalShieldDamage(attacker, blockers)) {
if (!attacker.hasKeyword(Keyword.INFECT)) { if (!attacker.hasKeyword(Keyword.INFECT)) {
damage += getAttack(attacker) - totalShieldDamage(attacker, blockers); damage += getAttack(attacker) - totalShieldDamage(attacker, blockers);
} }
@@ -366,12 +366,12 @@ public class ComputerUtilCombat {
for (final Card attacker : attackers) { for (final Card attacker : attackers) {
final List<Card> blockers = combat.getBlockers(attacker); 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" || attacker.hasKeyword("You may have CARDNAME assign its combat damage"
+ " as though it weren't blocked.")) { + " as though it weren't blocked.")) {
unblocked.add(attacker); unblocked.add(attacker);
} else if (attacker.hasKeyword(Keyword.TRAMPLE) } else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (getAttack(attacker) > totalShieldDamage(attacker, blockers))) { && getAttack(attacker) > totalShieldDamage(attacker, blockers)) {
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers); int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (attacker.hasKeyword(Keyword.INFECT)) { if (attacker.hasKeyword(Keyword.INFECT)) {
poison += trampleDamage; poison += trampleDamage;
@@ -431,7 +431,7 @@ public class ComputerUtilCombat {
// check for creatures that must be blocked // check for creatures that must be blocked
final List<Card> attackers = combat.getAttackersOf(ai); final List<Card> attackers = combat.getAttackersOf(ai);
final List<Card> threateningCommanders = getLifeThreateningCommanders(ai,combat); final List<Card> threateningCommanders = getLifeThreateningCommanders(ai, combat);
for (final Card attacker : attackers) { for (final Card attacker : attackers) {
final List<Card> blockers = combat.getBlockers(attacker); final List<Card> blockers = combat.getBlockers(attacker);
@@ -617,7 +617,7 @@ public class ComputerUtilCombat {
if (flankingMagnitude >= defender.getNetToughness()) { if (flankingMagnitude >= defender.getNetToughness()) {
return 0; return 0;
} }
if (flankingMagnitude >= (defender.getNetToughness() - defender.getDamage()) if (flankingMagnitude >= defender.getNetToughness() - defender.getDamage()
&& !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) { && !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0; return 0;
} }
@@ -1660,7 +1660,7 @@ public class ComputerUtilCombat {
*/ */
public static boolean combatantCantBeDestroyed(Player ai, final Card combatant) { public static boolean combatantCantBeDestroyed(Player ai, final Card combatant) {
// either indestructible or may regenerate // 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; return true;
} }
@@ -1715,7 +1715,7 @@ public class ComputerUtilCombat {
if (flankingMagnitude >= blocker.getNetToughness()) { if (flankingMagnitude >= blocker.getNetToughness()) {
return false; return false;
} }
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage())) if (flankingMagnitude >= blocker.getNetToughness() - blocker.getDamage()
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) { && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return false; return false;
} }

View File

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

View File

@@ -916,7 +916,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, new Predicate<Card>() { prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { 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()) { if (!c.isCreature()) {
return true; 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) { if (evasive) {
return card.getNetCombatDamage() + powerBonus > 0 return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card) && canBeBlocked
&& canBeBlocked; && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.equals("Haste")) { } else if (keyword.equals("Haste")) {
return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped() return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped()
&& card.getNetCombatDamage() + powerBonus > 0 && card.getNetCombatDamage() + powerBonus > 0
@@ -1609,8 +1609,8 @@ public class AttachAi extends SpellAbilityAi {
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true)); && (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.startsWith("Flanking")) { } else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0 return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card) && canBeBlocked
&& canBeBlocked; && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.startsWith("Bushido")) { } else if (keyword.startsWith("Bushido")) {
return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card)) return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card))
|| CombatUtil.canBlock(card, true); || CombatUtil.canBlock(card, true);
@@ -1652,9 +1652,9 @@ public class AttachAi extends SpellAbilityAi {
if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender") if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender")
|| keyword.endsWith("CARDNAME can't attack or block.")) { || 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.")) { } 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.")) { } else if (keyword.endsWith("CARDNAME can't block.")) {
return CombatUtil.canBlock(card, true); return CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) { } else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
@@ -1665,10 +1665,10 @@ public class AttachAi extends SpellAbilityAi {
} }
return false; return false;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")) { } 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.") } 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.")) { || 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.")) { } else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped(); 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.HashBiMap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import forge.LobbyPlayer; import forge.LobbyPlayer;
import forge.ai.AIOption;
import forge.ai.LobbyPlayerAi; import forge.ai.LobbyPlayerAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
@@ -64,9 +67,9 @@ public class GameCopier {
} }
public Game makeCopy() { 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> origPlayers = origGame.getMatch().getPlayers();
List<RegisteredPlayer> newPlayers = new ArrayList<>(); List<RegisteredPlayer> newPlayers = new ArrayList<>();
for (RegisteredPlayer p : origPlayers) { for (RegisteredPlayer p : origPlayers) {
@@ -82,8 +85,7 @@ public class GameCopier {
newPlayer.setActivateLoyaltyAbilityThisTurn(origPlayer.getActivateLoyaltyAbilityThisTurn()); newPlayer.setActivateLoyaltyAbilityThisTurn(origPlayer.getActivateLoyaltyAbilityThisTurn());
for (int j = 0; j < origPlayer.getSpellsCastThisTurn(); j++) for (int j = 0; j < origPlayer.getSpellsCastThisTurn(); j++)
newPlayer.addSpellCastThisTurn(); newPlayer.addSpellCastThisTurn();
for (int j = 0; j < origPlayer.getLandsPlayedThisTurn(); j++) newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
newPlayer.addLandPlayedThisTurn();
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters())); newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn()); newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn()); newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
@@ -138,18 +140,25 @@ public class GameCopier {
effect.removeMapped(gameObjectMap); 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.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
newGame.getTriggerHandler().resetActiveTriggers(); newGame.getTriggerHandler().resetActiveTriggers();
if (GameSimulator.COPY_STACK) if (GameSimulator.COPY_STACK)
copyStack(origGame, newGame, gameObjectMap); copyStack(origGame, newGame, gameObjectMap);
if (origPhaseHandler.getCombat() != null) { // TODO update thisTurnCast
newGame.getPhaseHandler().setCombat(new Combat(origPhaseHandler.getCombat(), gameObjectMap));
}
if (advanceToPhase != null) { if (advanceToPhase != null) {
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase); newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase, new Runnable() {
@Override
public void run() {
GameSimulator.resolveStack(newGame, aiPlayer.getWeakestOpponent());
}
});
} }
return newGame; return newGame;
@@ -185,7 +194,8 @@ public class GameCopier {
RegisteredPlayer clone = new RegisteredPlayer(p.getDeck()); RegisteredPlayer clone = new RegisteredPlayer(p.getDeck());
LobbyPlayer lp = p.getPlayer(); LobbyPlayer lp = p.getPlayer();
if (!(lp instanceof LobbyPlayerAi)) { 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); clone.setPlayer(lp);
return clone; return clone;
@@ -197,6 +207,7 @@ public class GameCopier {
for (Card card : origGame.getCardsIn(zone)) { for (Card card : origGame.getCardsIn(zone)) {
addCard(newGame, zone, card); addCard(newGame, zone, card);
} }
// TODO CardsAddedThisTurn is now messed up
} }
gameObjectMap = new CopiedGameObjectMap(newGame); gameObjectMap = new CopiedGameObjectMap(newGame);
@@ -271,11 +282,14 @@ public class GameCopier {
cardMap.put(c, newCard); cardMap.put(c, newCard);
Player zoneOwner = owner; Player zoneOwner = owner;
// everything the CreatureEvaluator checks must be set here
if (zone == ZoneType.Battlefield) { if (zone == ZoneType.Battlefield) {
// TODO: Controllers' list with timestamps should be copied. // TODO: Controllers' list with timestamps should be copied.
zoneOwner = playerMap.get(c.getController()); zoneOwner = playerMap.get(c.getController());
newCard.setController(zoneOwner, 0); newCard.setController(zoneOwner, 0);
newCard.setCameUnderControlSinceLastUpkeep(c.cameUnderControlSinceLastUpkeep());
newCard.setPTTable(c.getSetPTTable()); newCard.setPTTable(c.getSetPTTable());
newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable()); newCard.setPTCharacterDefiningTable(c.getSetPTCharacterDefiningTable());
@@ -290,9 +304,11 @@ public class GameCopier {
newCard.setChangedCardKeywords(c.getChangedCardKeywords()); newCard.setChangedCardKeywords(c.getChangedCardKeywords());
newCard.setChangedCardNames(c.getChangedCardNames()); newCard.setChangedCardNames(c.getChangedCardNames());
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such? for (Table.Cell<Long, Long, List<String>> kw : c.getHiddenExtrinsicKeywordsTable().cellSet()) {
//for (KeywordInterface kw : c.getHiddenExtrinsicKeywords()) newCard.addHiddenExtrinsicKeywords(kw.getRowKey(), kw.getColumnKey(), kw.getValue());
// newCard.addHiddenExtrinsicKeyword(kw); }
newCard.updateKeywordsCache(newCard.getCurrentState());
if (c.isTapped()) { if (c.isTapped()) {
newCard.setTapped(true); newCard.setTapped(true);
} }

View File

@@ -31,7 +31,7 @@ public class GameSimulator {
public GameSimulator(SimulationController controller, Game origGame, Player origAiPlayer, PhaseType advanceToPhase) { public GameSimulator(SimulationController controller, Game origGame, Player origAiPlayer, PhaseType advanceToPhase) {
this.controller = controller; this.controller = controller;
copier = new GameCopier(origGame); copier = new GameCopier(origGame);
simGame = copier.makeCopy(advanceToPhase); simGame = copier.makeCopy(advanceToPhase, origAiPlayer);
aiPlayer = (Player) copier.find(origAiPlayer); aiPlayer = (Player) copier.find(origAiPlayer);
eval = new GameStateEvaluator(); eval = new GameStateEvaluator();
@@ -74,6 +74,8 @@ public class GameSimulator {
eval.getScoreForGameState(origGame, origAiPlayer); eval.getScoreForGameState(origGame, origAiPlayer);
// Print debug info. // Print debug info.
printDiff(origLines, simLines); printDiff(origLines, simLines);
// make sure it gets printed
System.out.flush();
throw new RuntimeException("Game copy error. See diff output above for details."); throw new RuntimeException("Game copy error. See diff output above for details.");
} }
eval.setDebugging(false); eval.setDebugging(false);
@@ -117,7 +119,7 @@ public class GameSimulator {
if (debugPrint) { if (debugPrint) {
System.out.println(str); System.out.println(str);
} }
if (debugLines!=null) { if (debugLines != null) {
debugLines.add(str); debugLines.add(str);
} }
} }
@@ -230,8 +232,10 @@ public class GameSimulator {
return score; 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. // 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() { opponent.runWithController(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -259,7 +263,7 @@ public class GameSimulator {
// Continue until stack is empty. // Continue until stack is empty.
} }
} }
}, new PlayerControllerAi(game, opponent, opponent.getLobbyPlayer())); }, sim);
} }
public Game getSimulatedGameState() { public Game getSimulatedGameState() {

View File

@@ -24,7 +24,7 @@ public class GameStateEvaluator {
public GameCopier copier; public GameCopier copier;
public Game gameCopy; public Game gameCopy;
} }
private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame) { private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame, final Player aiPlayer) {
PhaseType phase = evalGame.getPhaseHandler().getPhase(); PhaseType phase = evalGame.getPhaseHandler().getPhase();
if (phase.isAfter(PhaseType.COMBAT_DAMAGE) || evalGame.isGameOver()) { if (phase.isAfter(PhaseType.COMBAT_DAMAGE) || evalGame.isGameOver()) {
return null; return null;
@@ -38,7 +38,12 @@ public class GameStateEvaluator {
} }
GameCopier copier = new GameCopier(evalGame); GameCopier copier = new GameCopier(evalGame);
Game gameCopy = copier.makeCopy(); 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(); CombatSimResult result = new CombatSimResult();
result.copier = copier; result.copier = copier;
result.gameCopy = gameCopy; result.gameCopy = gameCopy;
@@ -67,7 +72,7 @@ public class GameStateEvaluator {
return getScoreForGameOver(game, aiPlayer); return getScoreForGameOver(game, aiPlayer);
} }
CombatSimResult result = simulateUpcomingCombatThisTurn(game); CombatSimResult result = simulateUpcomingCombatThisTurn(game, aiPlayer);
if (result != null) { if (result != null) {
Player aiPlayerCopy = (Player) result.copier.find(aiPlayer); Player aiPlayerCopy = (Player) result.copier.find(aiPlayer);
if (result.gameCopy.isGameOver()) { if (result.gameCopy.isGameOver()) {

View File

@@ -101,7 +101,7 @@ public class SpellAbilityPicker {
} }
public SpellAbility chooseSpellAbilityToPlay(SimulationController controller) { public SpellAbility chooseSpellAbilityToPlay(SimulationController controller) {
printOutput = (controller == null); printOutput = controller == null;
// Pass if top of stack is owned by me. // Pass if top of stack is owned by me.
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) { if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
@@ -338,7 +338,7 @@ public class SpellAbilityPicker {
return AiPlayDecision.CantPlaySa; return AiPlayDecision.CantPlaySa;
} }
// Note: Can'tjust check condition on the top ability, because it may have // Note: Can't just check condition on the top ability, because it may have
// sub-abilities without conditions (e.g. wild slash's main ability has a // sub-abilities without conditions (e.g. wild slash's main ability has a
// main ability with conditions but the burn sub-ability has none). // main ability with conditions but the burn sub-ability has none).
if (!atLeastOneConditionMet(sa)) { if (!atLeastOneConditionMet(sa)) {

View File

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

View File

@@ -81,7 +81,7 @@ public class EffectEffect extends SpellAbilityEffect {
if (sa.hasParam("RememberObjects")) { if (sa.hasParam("RememberObjects")) {
rememberList = new FCollection<>(); rememberList = new FCollection<>();
for (final String rem : sa.getParam("RememberObjects").split(",")) { 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")) { if (sa.hasParam("ForgetCounter")) {

View File

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

View File

@@ -3617,7 +3617,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
public Iterable<KeywordsChange> getChangedCardKeywordsList() { public Iterable<KeywordsChange> getChangedCardKeywordsList() {
return Iterables.concat( return Iterables.concat(
changedCardKeywordsByText.values(), // Layer 3 changedCardKeywordsByText.values(), // Layer 3
ImmutableList.of(new KeywordsChange(ImmutableList.<String>of(), null, this.hasRemoveIntrinsic())), // Layer 4 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; return changedCardKeywords;
} }
public Table<Long, Long, CardColor> getChangedCardColorsTable() { public Table<Long, Long, CardColor> getChangedCardColorsTable() {
return changedCardColors; return changedCardColors;
} }
@@ -4561,6 +4559,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
public final Iterable<String> getHiddenExtrinsicKeywords() { public final Iterable<String> getHiddenExtrinsicKeywords() {
return Iterables.concat(this.hiddenExtrinsicKeywords.values()); 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) { 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 // 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. * @return a boolean.
*/ */
public static boolean canBlock(final Card attacker, final Card blocker, final boolean nextTurn) { 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; return false;
} }

View File

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

View File

@@ -855,11 +855,7 @@ public final class StaticAbilityContinuous {
if (params.containsKey("TriggerRememberDefined")) { if (params.containsKey("TriggerRememberDefined")) {
String triggerRemembered = (params.get("TriggerRememberDefined")); String triggerRemembered = (params.get("TriggerRememberDefined"));
for (final String rem : triggerRemembered.split(",")) { for (final String rem : triggerRemembered.split(",")) {
for (final Object o : AbilityUtils.getDefinedObjects(hostCard, rem, stAb)) { for (final Object o : AbilityUtils.getDefinedEntities(hostCard, rem, stAb)) {
if (o instanceof SpellAbility) {
// "RememberObjects$ Remembered" don't remember spellability
continue;
}
actualTrigger.addRemembered(o); 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) { public final void add(final Card c) {
add(c, null); add(c, null);
} }
public final void add(final Card c, final Integer index) { public final void add(final Card c, final Integer index) {
add(c, index, null); add(c, index, null);
} }
public void add(final Card c, Integer index, final Card latestState) { public void add(final Card c, Integer index, final Card latestState) {
if (index != null && cardList.isEmpty() && index.intValue() > 0) { if (index != null && cardList.isEmpty() && index.intValue() > 0) {
// something went wrong, most likely the method fired when the game was in an unexpected state // something went wrong, most likely the method fired when the game was in an unexpected state