Skip to content

Commit

Permalink
Merge branch 'master' into fullControl2
Browse files Browse the repository at this point in the history
  • Loading branch information
tool4ever authored Dec 24, 2024
2 parents 1a75c06 + c7d6446 commit fe635ab
Show file tree
Hide file tree
Showing 171 changed files with 1,191 additions and 733 deletions.
7 changes: 3 additions & 4 deletions forge-ai/src/main/java/forge/ai/AiAttackController.java
Original file line number Diff line number Diff line change
Expand Up @@ -1391,9 +1391,8 @@ private void calculate(final List<Card> defenders, final Combat combat) {

// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
dangerousBlockersPresent = validBlockers.anyMatch(
CardPredicates.hasKeyword(Keyword.WITHER)
.or(CardPredicates.hasKeyword(Keyword.INFECT))
.or(CardPredicates.hasKeyword(Keyword.LIFELINK))
CardPredicates.hasKeyword(Keyword.LIFELINK)
.or(Card::isWitherDamage)
);

// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
Expand Down Expand Up @@ -1422,7 +1421,7 @@ private void calculate(final List<Card> defenders, final Combat combat) {
canKillAll = false;

if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")
|| blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT) || blocker.hasKeyword(Keyword.LIFELINK)) {
|| blocker.isWitherDamage() || blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
Expand Down
86 changes: 45 additions & 41 deletions forge-ai/src/main/java/forge/ai/AiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package forge.ai;

import com.esotericsoftware.minlog.Log;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import forge.ai.AiCardMemory.MemorySet;
Expand Down Expand Up @@ -64,6 +63,7 @@
import io.sentry.Sentry;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -717,12 +717,14 @@ public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean
return reserveManaSources(sa, phaseType, enemy, true, null);
}
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) {
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);

// used for chained spells where two spells need to be cast in succession
if (exceptForThisSa != null) {
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(ComputerUtilMana.calculateManaCost(exceptForThisSa, true, 0), exceptForThisSa, player));
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(
ComputerUtilMana.calculateManaCost(exceptForThisSa.getPayCosts(), exceptForThisSa, true, 0, false),
exceptForThisSa, player));
}

if (manaSources.isEmpty()) {
Expand Down Expand Up @@ -820,7 +822,7 @@ private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
}
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
if (!sa.getAllTargetChoices().isEmpty()) {
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa).getTotalMana().getCMC();
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
}
}

Expand Down Expand Up @@ -853,7 +855,7 @@ private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {

// check if some target raised cost
if (!xCost && oldCMC > -1) {
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa).getTotalMana().getCMC();
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
if (finalCMC > oldCMC) {
xCost = true;
}
Expand Down Expand Up @@ -1013,7 +1015,7 @@ private boolean canPlaySpellWithoutBuyback(Card card, SpellAbility sa) {
costWithBuyback.add(opt.getCost());
}
}
costWithBuyback = CostAdjustment.adjust(costWithBuyback, sa);
costWithBuyback = CostAdjustment.adjust(costWithBuyback, sa, false);
if (costWithBuyback.hasSpecificCostType(CostPayLife.class)
|| costWithBuyback.hasSpecificCostType(CostDiscard.class)
|| costWithBuyback.hasSpecificCostType(CostSacrifice.class)) {
Expand Down Expand Up @@ -2208,8 +2210,6 @@ public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) {
return activePlayerSAs;
}

List<SpellAbility> result = Lists.newArrayList();

// filter list by ApiTypes
List<SpellAbility> discard = filterListByApi(activePlayerSAs, ApiType.Discard);
List<SpellAbility> mandatoryDiscard = filterList(discard, SpellAbilityPredicates.isMandatory());
Expand All @@ -2225,37 +2225,33 @@ public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) {
List<SpellAbility> pump = filterListByApi(activePlayerSAs, ApiType.Pump);
List<SpellAbility> pumpAll = filterListByApi(activePlayerSAs, ApiType.PumpAll);

List<SpellAbility> result = Lists.newArrayList(activePlayerSAs);

// do mandatory discard early if hand is empty or has DiscardMe card
boolean discardEarly = false;
CardCollectionView playerHand = player.getCardsIn(ZoneType.Hand);
if (playerHand.isEmpty() || playerHand.anyMatch(CardPredicates.hasSVar("DiscardMe"))) {
discardEarly = true;
if (!playerHand.isEmpty() && !playerHand.anyMatch(CardPredicates.hasSVar("DiscardMe"))) {
result.addAll(mandatoryDiscard);
mandatoryDiscard.clear();
}

// token should be added first so they might get the pump bonus
result.addAll(token);
result.addAll(pump);
result.addAll(pumpAll);
// optional Discard, probably combined with Draw
result.addAll(discard);
// do Draw before Discard
result.addAll(draw);

// do Evolve Trigger before other PutCounter SpellAbilities
result.addAll(putCounterAll);
// do putCounter before Draw/Discard because it can cause a Draw Trigger
result.addAll(evolve);
result.addAll(putCounter);
result.addAll(putCounterAll);

// do Draw before Discard
result.addAll(draw);
result.addAll(discard); // optional Discard, probably combined with Draw
// do Evolve Trigger before other PutCounter SpellAbilities
result.addAll(evolve);

if (!discardEarly) {
result.addAll(mandatoryDiscard);
}
// token should be added first so they might get the pump bonus
result.addAll(pumpAll);
result.addAll(pump);
result.addAll(token);

result.addAll(activePlayerSAs);
result.addAll(mandatoryDiscard);

//need to reverse because of magic stack
Collections.reverse(result);
return result;
}

Expand All @@ -2267,6 +2263,10 @@ private static <T> List<T> filterList(List<T> input, Predicate<? super T> pred)
}

// TODO move to more common place
public static <T extends TriggerReplacementBase> List<T> filterList(List<T> input, Function<SpellAbility, Object> pred, Object value) {
return filterList(input, trb -> pred.apply(trb.ensureAbility()) == value);
}

public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
return filterList(input, SpellAbilityPredicates.isApi(type));
}
Expand Down Expand Up @@ -2303,12 +2303,11 @@ private boolean checkAiSpecificRestrictions(final SpellAbility sa) {
public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> list) {
// no need to choose anything
if (list.size() <= 1) {
return Iterables.getFirst(list, null);
return list.get(0);
}

ReplacementType mode = Iterables.getFirst(list, null).getMode();
ReplacementType mode = list.get(0).getMode();

// replace lifegain effects
if (mode.equals(ReplacementType.GainLife)) {
List<ReplacementEffect> noGain = filterListByAiLogic(list, "NoLife");
List<ReplacementEffect> loseLife = filterListByAiLogic(list, "LoseLife");
Expand All @@ -2317,24 +2316,24 @@ public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> l

if (!noGain.isEmpty()) {
// no lifegain is better than lose life
return Iterables.getFirst(noGain, null);
return noGain.get(0);
} else if (!loseLife.isEmpty()) {
// lose life before double life to prevent lose double
return Iterables.getFirst(loseLife, null);
return loseLife.get(0);
} else if (!lichDraw.isEmpty()) {
// lich draw before double life to prevent to draw to much
return Iterables.getFirst(lichDraw, null);
return lichDraw.get(0);
} else if (!doubleLife.isEmpty()) {
// other than that, do double life
return Iterables.getFirst(doubleLife, null);
return doubleLife.get(0);
}
} else if (mode.equals(ReplacementType.DamageDone)) {
List<ReplacementEffect> prevention = filterList(list, CardTraitPredicates.hasParam("Prevent"));

// TODO when Protection is done as ReplacementEffect do them
// before normal prevention
if (!prevention.isEmpty()) {
return Iterables.getFirst(prevention, null);
return prevention.get(0);
}
} else if (mode.equals(ReplacementType.Destroy)) {
List<ReplacementEffect> shield = filterList(list, CardTraitPredicates.hasParam("ShieldCounter"));
Expand All @@ -2344,30 +2343,35 @@ public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> l

// Indestructible umbra armor is the best
if (!umbraArmorIndestructible.isEmpty()) {
return Iterables.getFirst(umbraArmorIndestructible, null);
return umbraArmorIndestructible.get(0);
}

// then it might be better to remove shield counter if able?
if (!shield.isEmpty()) {
return Iterables.getFirst(shield, null);
return shield.get(0);
}

// TODO get the RunParams for Affected to check if the creature already dealt combat damage for Regeneration effects
// is using a Regeneration Effect better than using a Umbra Armor?
if (!regeneration.isEmpty()) {
return Iterables.getFirst(regeneration, null);
return regeneration.get(0);
}

if (!umbraArmor.isEmpty()) {
// sort them by cmc
umbraArmor.sort(Comparator.comparing(CardTraitBase::getHostCard, Comparator.comparing(Card::getCMC)));
return Iterables.getFirst(umbraArmor, null);
return umbraArmor.get(0);
}
} else if (mode.equals(ReplacementType.Draw)) {
List<ReplacementEffect> winGame = filterList(list, SpellAbility::getApi, ApiType.WinsGame);
if (!winGame.isEmpty()) {
return winGame.get(0);
}
}

// TODO always lower counters with Vorinclex first, might turn it from 1 to 0 as final

return Iterables.getFirst(list, null);
return list.get(0);
}

}
51 changes: 17 additions & 34 deletions forge-ai/src/main/java/forge/ai/ComputerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,22 +120,13 @@ public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa

game.getStack().freezeStack(sa);

// TODO: update mana color conversion for Daxos of Meletis
if (cost == null) {
// Is this fork even used for anything anymore?
if (ComputerUtilMana.payManaCost(ai, sa, false)) {
game.getStack().addAndUnfreeze(sa);
return true;
}
} else {
final CostPayment pay = new CostPayment(cost, sa);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().addAndUnfreeze(sa);
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty()) {
game.getAction().reveal(sa.getSplicedCards(), ai, true, "Computer reveals spliced cards from ");
}
return true;
final CostPayment pay = new CostPayment(cost, sa);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().addAndUnfreeze(sa);
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty()) {
game.getAction().reveal(sa.getSplicedCards(), ai, true, "Computer reveals spliced cards from ");
}
return true;
}
// FIXME: Should not arrive here, though the card seems to be stucked on stack zone and invalidated and nowhere to be found, try to put back to original zone and maybe try to cast again if possible at later time?
System.out.println("[" + sa.getActivatingPlayer() + "] AI failed to play " + sa.getHostCard() + " [" + sa.getHostCard().getZone() + "]");
Expand Down Expand Up @@ -239,10 +230,9 @@ public static final boolean playStack(SpellAbility sa, final Player ai, final Ga

if (sa.isSpell() && !source.isCopiedSpell()) {
sa.setHostCard(game.getAction().moveToStack(source, sa));
sa = GameActionUtil.addExtraKeywordCost(sa);
}

sa = GameActionUtil.addExtraKeywordCost(sa);

final Cost cost = sa.getPayCosts();
final CostPayment pay = new CostPayment(cost, sa);

Expand All @@ -252,15 +242,11 @@ public static final boolean playStack(SpellAbility sa, final Player ai, final Ga
return false;
}

if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa, false);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().add(sa);
} else {
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().add(sa);
}
return true;
}
return true;
return false;
}

public static final void playSpellAbilityForFree(final Player ai, final SpellAbility sa) {
Expand Down Expand Up @@ -324,22 +310,19 @@ public static final boolean playNoStack(final Player ai, SpellAbility sa, final
}

final Card source = sa.getHostCard();
if (sa.isSpell() && !source.isCopiedSpell()) {
if (!effect && sa.isSpell() && !source.isCopiedSpell()) {
sa.setHostCard(game.getAction().moveToStack(source, sa));
sa = GameActionUtil.addExtraKeywordCost(sa);
}

sa = GameActionUtil.addExtraKeywordCost(sa);

final Cost cost = sa.getPayCosts();
if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa, effect);
} else {
final CostPayment pay = new CostPayment(cost, sa);
pay.payComputerCosts(new AiCostDecision(ai, sa, effect));
final CostPayment pay = new CostPayment(cost, sa);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, effect))) {
AbilityUtils.resolve(sa);
return true;
}

AbilityUtils.resolve(sa);
return true;
return false;
}

public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
Expand Down
Loading

0 comments on commit fe635ab

Please sign in to comment.