Skip to content
Open
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
113 changes: 110 additions & 3 deletions forge-ai/src/main/java/forge/ai/ability/CharmAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import com.google.common.collect.Lists;
import forge.ai.*;
import forge.game.GameActionUtil;
import forge.game.ability.AbilityUtils;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.cost.Cost;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.util.collect.FCollection;
Expand Down Expand Up @@ -94,9 +99,8 @@ protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
List<AbilitySub> chosen = Lists.newArrayList();
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
// TODO unused for now, the AI doesn't know how to effectively handle repeated choices
boolean allowRepeat = sa.hasParam("CanRepeatModes");
Comment thread
Madwand99 marked this conversation as resolved.

// TODO the AI doesn't know how to effectively handle repeated choices from CanRepeatModes yet.
final int pawprintLimit = sa.hasParam("Pawprint") ? AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Pawprint"), sa) : 0;
if (pawprintLimit > 0) {
// try to pay for the more expensive subs first
Expand All @@ -109,7 +113,7 @@ private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choic
handleDependentModes(sa, chosen, sub);
sub.setActivatingPlayer(ai);
// TODO refactor to obtain the AiAbilityDecision instead, then we can check all to sort by value
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub) && canPayForAdditionalMode(sa, chosen, sub, ai)) {
if (pawprintLimit > 0) {
int curPawprintAmount = AbilityUtils.calculateAmount(sub.getHostCard(), sub.getParamOrDefault("Pawprint", "0"), sub);
if (pawprintAmount + curPawprintAmount > pawprintLimit) {
Expand Down Expand Up @@ -150,6 +154,19 @@ private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choic
}
}
}
if (!isTrigger && chosen.size() < num && min < num) {
// Optional extra modes can be worth adding even when canPlaySa() is too strict for a standalone mode.
choices.removeAll(chosen);
for (AbilitySub sub : choices) {
handleDependentModes(sa, chosen, sub);
if (aic.doTrigger(sub, false) && canPayForAdditionalMode(sa, chosen, sub, ai)) {
chosen.add(sub);
if (chosen.size() == num) {
break;
}
}
}
}
if (chosen.size() < min) {
// not enough choices
chosen.clear();
Expand All @@ -158,6 +175,48 @@ private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choic
return chosen;
}

private boolean canPayForAdditionalMode(SpellAbility sa, List<AbilitySub> chosen, AbilitySub sub, Player ai) {
Card source = sa.getHostCard();
if (source.hasStartOfKeyword("Spree") || source.hasStartOfKeyword("Tiered")) {
Cost fullCost = sa.getPayCosts().copy();
for (AbilitySub mode : chosen) {
if (!mode.hasParam("ModeCost")) {
return false;
}
fullCost.add(new Cost(mode.getParam("ModeCost"), false));
}
if (!sub.hasParam("ModeCost")) {
return false;
}
fullCost.add(new Cost(sub.getParam("ModeCost"), false));
return ComputerUtilCost.canPayCost(sa.copyWithDefinedCost(fullCost), ai, false);
}

if (!source.hasStartOfKeyword("Escalate")) {
return true;
}
String escalateCost = getEscalateCost(source);
if (escalateCost == null) {
return false;
}

Cost fullCost = sa.getPayCosts().copy();
for (int i = 0; i < chosen.size(); i++) {
fullCost.add(new Cost(escalateCost, false));
}
return ComputerUtilCost.canPayCost(sa.copyWithDefinedCost(fullCost), ai, false);
}

private String getEscalateCost(Card source) {
for (KeywordInterface inst : source.getKeywords()) {
String kw = inst.getOriginal();
if (kw.startsWith("Escalate:")) {
return kw.substring("Escalate:".length());
}
}
return null;
}

private List<AbilitySub> chooseTriskaidekaphobia(List<AbilitySub> choices, final Player ai) {
List<AbilitySub> chosenList = Lists.newArrayList();
if (choices == null || choices.isEmpty()) { return chosenList; }
Expand Down Expand Up @@ -288,6 +347,54 @@ public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> op
return Aggregates.random(opponents);
}

@Override
public List<OptionalCostValue> chooseOptionalCosts(Player payer, SpellAbility chosen, List<OptionalCostValue> optionalCostValues) {
OptionalCostValue entwine = null;
List<OptionalCostValue> otherCosts = Lists.newArrayList();
for (OptionalCostValue opt : optionalCostValues) {
if (opt.getType() == OptionalCost.Entwine) {
entwine = opt;
} else {
otherCosts.add(opt);
}
}

List<OptionalCostValue> chosenCosts = super.chooseOptionalCosts(payer, chosen, otherCosts);
if (entwine == null || !shouldPayEntwine(payer, chosen, chosenCosts, entwine)) {
return chosenCosts;
}

chosenCosts.add(entwine);
return chosenCosts;
}

private boolean shouldPayEntwine(Player payer, SpellAbility chosen, List<OptionalCostValue> chosenCosts, OptionalCostValue entwine) {
List<OptionalCostValue> costsWithEntwine = Lists.newArrayList(chosenCosts);
costsWithEntwine.add(entwine);

SpellAbility entwined = GameActionUtil.addOptionalCosts(chosen, costsWithEntwine);
if (!ComputerUtilCost.canPayCost(entwined, payer, false)) {
return false;
}

List<AbilitySub> choices = CharmEffect.makePossibleOptions(entwined);
if (choices.size() < 2) {
return false;
}

AiController aic = ((PlayerControllerAi) payer.getController()).getAi();
for (AbilitySub sub : choices) {
sub.setActivatingPlayer(payer);
if (!aic.doTrigger(sub, false)) {
entwined.setSubAbility(null);
return false;
}
}

entwined.setSubAbility(null);
return true;
}

@Override
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
// choices were already targeted
Expand Down
138 changes: 138 additions & 0 deletions forge-gui-desktop/src/test/java/forge/ai/AIIntegrationTests.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package forge.ai;

import forge.card.mana.ManaAtom;
import forge.game.GameActionUtil;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.mana.Mana;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import org.testng.AssertJUnit;
import org.testng.annotations.Test;

import java.util.List;

public class AIIntegrationTests extends AITest {
@Test
public void testSwingForLethal() {
Expand Down Expand Up @@ -113,4 +119,136 @@ public void testDoesNotCastRepopulateWhenNoCreaturesInOpponentGraveyard() {

AssertJUnit.assertTrue("Repopulate must still be in hand", hand.contains(repopulate));
}

@Test
public void testCrushContrabandChoosesBothModes() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);

Card crushContraband = addCardToZone("Crush Contraband", p, ZoneType.Hand);
SpellAbility sa = crushContraband.getSpellAbilities().get(0);
sa.setActivatingPlayer(p);

Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
addCard("Sol Ring", opponent);
addCard("Honor of the Pure", opponent);

AiPlayDecision decision = ((PlayerControllerAi) p.getController()).getAi().canPlaySa(sa);

AssertJUnit.assertEquals(AiPlayDecision.WillPlay, decision);
AssertJUnit.assertEquals("AI should choose both available modes", 2, sa.getChosenList().size());
}

@Test
public void testEscalateDoesNotChooseUnaffordableExtraMode() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCard("Forest", p);
addCard("Wastes", p);

Card collectiveResistance = addCardToZone("Collective Resistance", p, ZoneType.Hand);
SpellAbility sa = collectiveResistance.getSpellAbilities().get(0);
sa.setActivatingPlayer(p);

Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
addCard("Sol Ring", opponent);
addCard("Honor of the Pure", opponent);

AiPlayDecision decision = ((PlayerControllerAi) p.getController()).getAi().canPlaySa(sa);

AssertJUnit.assertEquals(AiPlayDecision.WillPlay, decision);
AssertJUnit.assertEquals("AI should not choose an extra Escalate mode it cannot pay for", 1, sa.getChosenList().size());
}

@Test
public void testEscalateChoosesAffordableExtraMode() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCards("Forest", 3, p);

Card collectiveResistance = addCardToZone("Collective Resistance", p, ZoneType.Hand);
SpellAbility sa = collectiveResistance.getSpellAbilities().get(0);
sa.setActivatingPlayer(p);

Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
addCard("Sol Ring", opponent);
addCard("Honor of the Pure", opponent);

AiPlayDecision decision = ((PlayerControllerAi) p.getController()).getAi().canPlaySa(sa);

AssertJUnit.assertEquals(AiPlayDecision.WillPlay, decision);
AssertJUnit.assertEquals("AI should choose an extra Escalate mode when it is useful and payable", 2, sa.getChosenList().size());
}

@Test
public void testEntwineChoosesCostWhenAllModesAreUseful() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCards("Forest", 6, p);
addCardToZone("Forest", p, ZoneType.Library);
addCardToZone("Plains", p, ZoneType.Library);

Card journey = addCardToZone("Journey of Discovery", p, ZoneType.Hand);
SpellAbility sa = journey.getSpellAbilities().get(0);
sa.setActivatingPlayer(p);

List<OptionalCostValue> optionalCosts = GameActionUtil.getOptionalCostValues(sa);
List<OptionalCostValue> chosenCosts = p.getController().chooseOptionalCosts(sa, optionalCosts);

AssertJUnit.assertTrue("AI should pay Entwine when both modes are useful",
chosenCosts.stream().anyMatch(cost -> cost.getType() == OptionalCost.Entwine));
}

@Test
public void testSpreeDoesNotChooseUnaffordableExtraMode() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCard("Plains", p);
addCard("Wastes", p);

Card requisitionRaid = addCardToZone("Requisition Raid", p, ZoneType.Hand);
SpellAbility sa = requisitionRaid.getSpellAbilities().get(0);
sa.setActivatingPlayer(p);

Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
addCard("Sol Ring", opponent);
addCard("Honor of the Pure", opponent);

AiPlayDecision decision = ((PlayerControllerAi) p.getController()).getAi().canPlaySa(sa);

AssertJUnit.assertEquals(AiPlayDecision.WillPlay, decision);
AssertJUnit.assertEquals("AI should not choose a Spree mode it cannot pay for", 1, sa.getChosenList().size());
}

@Test
public void testSpreeChoosesAffordableExtraMode() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCard("Plains", p);
addCards("Wastes", 2, p);

Card requisitionRaid = addCardToZone("Requisition Raid", p, ZoneType.Hand);
SpellAbility sa = requisitionRaid.getSpellAbilities().get(0);
sa.setActivatingPlayer(p);

Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
addCard("Sol Ring", opponent);
addCard("Honor of the Pure", opponent);

AiPlayDecision decision = ((PlayerControllerAi) p.getController()).getAi().canPlaySa(sa);

AssertJUnit.assertEquals(AiPlayDecision.WillPlay, decision);
AssertJUnit.assertEquals("AI should choose an extra Spree mode when it is useful and payable", 2, sa.getChosenList().size());
}
}