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
204 changes: 133 additions & 71 deletions forge-ai/src/main/java/forge/ai/SpecialCardAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,10 @@ public static String chooseCard(final Player ai, final SpellAbility sa) {
public static class PithingNeedle {
// TODO Build out exclusion list based off cards in my deck and cards that other needles have chosen
public static String chooseCard(final Player ai, final SpellAbility sa) {
// TODO Choose cards via deck key cards

String keyCardChoice = chooseCardViaKeyCard(ai, sa);
if (keyCardChoice != null) {
return keyCardChoice;
}

String choice = chooseCardViaScoring(ai, sa);
if (choice != null) {
Expand All @@ -313,6 +315,134 @@ public static String chooseCard(final Player ai, final SpellAbility sa) {
return chooseNonBattlefieldName();
}

// Helper method to score a card's abilities and static effects
// Used by both chooseCardViaKeyCard and chooseCardViaScoring
private static int scoreCardAbilities(final Card c, boolean skipManaAbilities) {
int score = 0;

for (SpellAbility ab : c.getSpellAbilities()) {
if (!ab.isActivatedAbility()) {
continue;
}
if (skipManaAbilities && ab.isManaAbility()) {
continue;
}

// Alter this score based off the ApiType
switch (ab.getApi()) {
case Destroy:
score += 20;
break;
case DamageAll:
case DestroyAll:
score += 30;
break;
case WinsGame:
case LosesGame:
score += 50;
break;
case Draw:
score += 10;
break;
case GainControl:
case Play:
case DealDamage:
score += 15;
break;
case ChangeZone:
if (ab.getParam("Destination") != null && ab.getParam("Destination").equals("Battlefield")) {
score += 15;
} else {
score += 5;
}
break;
default:
score += 5;
}

score += 10;

// Give higher score to cheaper abilities, as they are more likely to be used and thus worth naming
if (ab.getPayCosts().getCostMana() != null) {
if (ab.getPayCosts().hasXInAnyCostPart()) {
score += 15;
} else {
Integer convertedAmount = ab.getPayCosts().getCostMana().convertAmount();
if (convertedAmount != null) {
score += Math.max(0, 20 - convertedAmount ^ 2);
}
}
}
if (ab.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
score += 10;
}
}

for (StaticAbility st : c.getStaticAbilities()) {
if (st.hasParam("GainsAbilitiesOf") && st.getParamOrDefault("Affected", "Self").contains("Self")) {
score += 10;
}

if (st.hasParam("AddAbility") && st.getParamOrDefault("Affected", "Self").contains("Self")) {
score += 10;
}
}

return score;
}

public static String chooseCardViaKeyCard(final Player ai, final SpellAbility sa) {
boolean skipManaAbilities = sa.getParam("AILogic").equals("PithingNeedle");
boolean skipLands = sa.getParam("AILogic").equals("PhyrexianRevoker");
boolean knowHand = sa.getParam("AILogic").equals("SorcerousSpyglass");

String bestKeyCard = null;
int bestScore = Integer.MIN_VALUE;

for (Player opp : ai.getOpponents()) {
List<String> keyCards = opp.getRegisteredPlayer().getDeck().getKeyCards();

for (Card c : opp.getAllCards()) {
String name = c.getName();
if (!keyCards.contains(name)) {
continue;
}

// Skip lands if required
if (skipLands && c.isLand()) {
continue;
}

// Base score for key cards
int score = 100;

// Add ability-based scoring
score += scoreCardAbilities(c, skipManaAbilities);

if (score == 100) {
// No activated abilities found, skip this key card
continue;
}

// Bonus for cards on battlefield (more likely to be a key card in play)
if (c.isInZone(ZoneType.Battlefield)) {
score += 20;
}

if (knowHand && c.isInZone(ZoneType.Hand)) {
score += 8;
}

if (score > bestScore) {
bestScore = score;
bestKeyCard = name;
}
}
}

return bestKeyCard;
}

public static String chooseNonBattlefieldName() {
return "Liliana of the Veil";
}
Expand All @@ -334,75 +464,7 @@ public static String chooseCardViaScoring(final Player ai, final SpellAbility sa
}

String name = c.getName();
int score = 0;

for(SpellAbility ab : c.getSpellAbilities()) {
if (!ab.isActivatedAbility()) {
continue;
}
if (skipManaAbilities && ab.isManaAbility()) {
continue;
}

// Alter this score based off the APiType
switch (ab.getApi()) {
case Destroy:
score += 20;
break;
case DamageAll:
case DestroyAll:
score += 30;
break;
case WinsGame:
case LosesGame:
score += 50;
break;
case Draw:
score += 10;
break;
case GainControl:
case Play:
case DealDamage:
score += 15;
break;
case ChangeZone:
if (ab.getParam("Destination").equals("Battlefield")) {
score += 15;
} else {
score += 5;
}
break;
default:
score += 5;
}

score += 10;

// GIve higher score to cheaper abilities, as they are more likely to be used and thus worth naming
if (ab.getPayCosts().getCostMana() != null) {
if (ab.getPayCosts().hasXInAnyCostPart()) {
score += 15;
} else {
Integer convertedAmount = ab.getPayCosts().getCostMana().convertAmount();
if (convertedAmount != null) {
score += Math.max(0, 20 - convertedAmount ^ 2);
}
}
}
if (ab.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
score += 10;
}
}

for(StaticAbility st : c.getStaticAbilities()) {
if (st.hasParam("GainsAbilitiesOf") && st.getParamOrDefault("Affected", "Self").contains("Self")) {
score += 10;
}

if (st.hasParam("AddAbility") && st.getParamOrDefault("Affected", "Self").contains("Self")) {
score += 10;
}
}
int score = scoreCardAbilities(c, skipManaAbilities);

if (score == 0) {
continue;
Expand Down
8 changes: 5 additions & 3 deletions forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -1468,9 +1468,11 @@ public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List
return null;
}
List<String> keyCards = player.getRegisteredPlayer().getDeck().getKeyCards();
// Only grab the keycards I don't already have access to
if (destination.equals(ZoneType.Battlefield) || destination.equals(ZoneType.Hand)) {
for (Card c : player.getCardsIn(Lists.newArrayList(ZoneType.Hand, ZoneType.Battlefield))) {
String position = sa.getParamOrDefault("LibraryPosition", null);
// FOcus on the keycards I don't already have access to
if (destination.equals(ZoneType.Battlefield) || destination.equals(ZoneType.Hand) ||
(destination.equals(ZoneType.Library) && "0".equals(position))) {
for(Card c : player.getCardsIn(Lists.newArrayList(ZoneType.Hand, ZoneType.Battlefield))) {
keyCards.remove(c.getName());
}
}
Expand Down
Loading