Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
Original file line number Diff line number Diff line change
Expand Up @@ -1754,13 +1754,13 @@ && doesSpecifiedCreatureAttackAI(ai, pumped)) {
}
}

float value = 1.0f * (pumpedDmg - dmg);
float value = pumpedDmg - dmg;
if (c == sa.getHostCard() && power > 0) {
int divisor = sa.getPayCosts().getTotalMana().getCMC();
if (divisor <= 0) {
divisor = 1;
}
value *= power / divisor;
value *= (float) power / divisor;
} else {
value /= opp.getLife();
}
Expand Down
127 changes: 40 additions & 87 deletions forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,11 @@ protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
}
}

if (sa.canTarget(ai)) {
if (sa.canTarget(ai) && !ai.getCounters().isEmpty()) {
// don't target itself when its forced to add poison counters too
if (!ai.getCounters().isEmpty()) {
if (!eachExisting || ai.getPoisonCounters() < 5) {
sa.getTargets().add(ai);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (!eachExisting || ai.getPoisonCounters() < 5) {
sa.getTargets().add(ai);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

Expand Down Expand Up @@ -335,16 +333,10 @@ protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
amount = 1; // TODO: improve this to possibly account for some variability depending on the roll outcome (e.g. 4 for 1d8, perhaps)
}

if (sa.hasParam("Adapt") &&
(!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0)) {
if (sa.hasParam("Adapt") && source.getCounters(CounterEnumType.P1P1) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}

if (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) &&
(sa.hasParam("Adapt") || sourceName.equals("Psychic Frog"))) {
return doCombatAdaptLogic(source, amount, game.getCombat());
}

if ("Fight".equals(logic) || "PowerDmg".equals(logic)) {
int nPump = 0;
if (type.equals("P1P1")) {
Expand Down Expand Up @@ -544,40 +536,38 @@ protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {

if (sa.isCurse()) {
choice = chooseCursedTarget(list, type, amount, ai);
} else {
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount, Lists.newArrayList())) {
choice = c;
break;
}
} else if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount, Lists.newArrayList())) {
choice = c;
break;
}
}

if (choice == null) {
// try to use as cheap kill
choice = ComputerUtil.getKilledByTargeting(sa, CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa));
}
if (choice == null) {
// try to use as cheap kill
choice = ComputerUtil.getKilledByTargeting(sa, CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa));
}

if (choice == null) {
// find generic target
boolean increasesCharmOutcome = false;
if (sa.getRootAbility().getApi() == ApiType.Charm && source.getStaticAbilities().isEmpty()) {
List<AbilitySub> choices = Lists.newArrayList(sa.getRootAbility().getAdditionalAbilityList("Choices"));
choices.remove(sa);
// check if other choice will already be played
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
}
if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|| ph.getTurn() - source.getTurnInZone() >= game.getPlayers().size() * 2) {
if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) {
// only use at opponent EOT unless it is free
choice = chooseBoonTarget(list, type);
}
if (choice == null) {
// find generic target
boolean increasesCharmOutcome = false;
if (sa.getRootAbility().getApi() == ApiType.Charm && source.getStaticAbilities().isEmpty()) {
List<AbilitySub> choices = Lists.newArrayList(sa.getRootAbility().getAdditionalAbilityList("Choices"));
choices.remove(sa);
// check if other choice will already be played
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
}
if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|| ph.getTurn() - source.getTurnInZone() >= game.getPlayers().size() * 2) {
if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) {
// only use at opponent EOT unless it is free
choice = chooseBoonTarget(list, type);
}
}
} else {
choice = chooseBoonTarget(list, type);
}
} else {
choice = chooseBoonTarget(list, type);
}

if (choice == null) { // can't find anything left
Expand All @@ -602,14 +592,16 @@ protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
final CounterType cType = CounterType.getType(type);
final CardCollection cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)
.filter(CardPredicates.canReceiveCounters(cType));
// Don't activate Curse abilities on my cards and non-curse abilities
// on my opponents
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}

final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
final int currCounters = cards.get(0).getCounters(cType);

// adding counters would cause counter amount to overflow
if (Integer.MAX_VALUE - currCounters <= amount) {
Expand All @@ -627,23 +619,20 @@ protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
// each non +1/+1 counter on the card is a 10% chance of not
// activating this ability.

if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) {
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && MyRandom.getRandom().nextFloat() < (.1 * currCounters)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Instant +1/+1
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
final Combat combat = game.getCombat();
// e.g. Power-Up abilities: use it to survive or win combat even without a sac cost
final boolean savesOrWinsCombat = combat != null
&& doCombatAdaptLogic(cards.get(0), amount, combat).willingToPlay();
if (!hasSacCost && !savesOrWinsCombat
&& !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
// e.g. Power-Up abilities: use it to survive or win combat
if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())
&& !ComputerUtilCard.shouldPumpCard(ai, sa, cards.get(0), amount, amount, Lists.newArrayList())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

// Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
if (ComputerUtil.isUselessCounter(cType, cards.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
Expand Down Expand Up @@ -1159,42 +1148,6 @@ private AiAbilityDecision doMoveCounterLogic(final Player ai, SpellAbility sa, P
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}

private AiAbilityDecision doCombatAdaptLogic(Card source, int amount, Combat combat) {
// TODO use smarter ComputerUtilCombat checks
if (combat.isAttacking(source)) {
if (!combat.isBlocked(source)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
for (Card blockedBy : combat.getBlockers(source)) {
if (blockedBy.getNetToughness() > source.getNetPower()
&& blockedBy.getNetToughness() <= source.getNetPower() + amount) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
if (source.getNetToughness() <= totBlkPower
&& source.getNetToughness() + amount > totBlkPower) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}
} else if (combat.isBlocking(source)) {
for (Card blocked : combat.getAttackersBlockedBy(source)) {
if (blocked.getNetToughness() > source.getNetPower()
&& blocked.getNetToughness() <= source.getNetPower() + amount) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}

int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower);
if (source.getNetToughness() <= totAtkPower
&& source.getNetToughness() + amount > totAtkPower) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}

@Override
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
if (sa.isKeyword(Keyword.READ_AHEAD)) {
Expand Down
14 changes: 7 additions & 7 deletions forge-core/src/main/java/forge/card/CardChangedType.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,32 @@

public record CardChangedType(CardTypeView addType, CardTypeView removeType, boolean addAllCreatureTypes, Set<RemoveType> remove) implements ICardChangedType {

public final boolean isRemoveSuperTypes() {
public boolean isRemoveSuperTypes() {
return remove.contains(RemoveType.SuperTypes);
}

public final boolean isRemoveCardTypes() {
public boolean isRemoveCardTypes() {
return remove.contains(RemoveType.CardTypes);
}

public final boolean isRemoveSubTypes() {
public boolean isRemoveSubTypes() {
return remove.contains(RemoveType.SubTypes);
}

@Override
public final boolean isRemoveLandTypes() {
public boolean isRemoveLandTypes() {
return remove.contains(RemoveType.LandTypes);
}

public final boolean isRemoveCreatureTypes() {
public boolean isRemoveCreatureTypes() {
return remove.contains(RemoveType.CreatureTypes);
}

public final boolean isRemoveArtifactTypes() {
public boolean isRemoveArtifactTypes() {
return remove.contains(RemoveType.ArtifactTypes);
}

public final boolean isRemoveEnchantmentTypes() {
public boolean isRemoveEnchantmentTypes() {
return remove.contains(RemoveType.EnchantmentTypes);
}

Expand Down
1 change: 1 addition & 0 deletions forge-game/src/main/java/forge/game/card/CardLists.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ public static CardCollection getTargetableCards(Iterable<Card> cardList, SpellAb
}

public static CardCollection canSubsequentlyTarget(CardCollection list, SpellAbility source) {
// TODO should check first that there's a dependent restriction
if (source.getTargets().isEmpty()) {
return list;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;

public class CountersPutAiPowerUpTest extends SimulationTest {
public class CountersPutAiTest extends SimulationTest {

@Test
public void testPowerUp_savesAttackerFromLethalCombat_aiActivates() {
Expand Down
Loading