Compare commits

..

2 Commits

Author SHA1 Message Date
GitHub Actions
801c958819 [maven-release-plugin] prepare release forge-2.0.00 2024-11-21 01:09:58 +00:00
Chris H
08b8d9dea0 Temporarily remove flatten to get a release out 2024-11-20 19:59:37 -05:00
8298 changed files with 55899 additions and 97657 deletions

View File

@@ -2,21 +2,10 @@ name: Publish Desktop Forge
on:
workflow_dispatch:
inputs:
debug_enabled:
type: boolean
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
release_android:
type: boolean
description: 'Also try to release android build'
required: false
default: false
jobs:
build:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest
permissions:
contents: write
@@ -43,94 +32,10 @@ jobs:
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
- name: Install old maven (3.8.1)
run: |
curl -o apache-maven-3.8.1-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz
tar xf apache-maven-3.8.1-bin.tar.gz
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
export MAVEN_HOME=$PWD/apache-maven-3.8.1
mvn --version
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
- name: Setup android requirements
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
run: |
JAVA_HOME=${JAVA_HOME_17_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;35.0.0" "platform-tools" "platforms;android-35"
cd forge-gui-android
echo "${{ secrets.FORGE_KEYSTORE }}" > forge.keystore.asc
gpg -d --passphrase "${{ secrets.FORGE_KEYSTORE_PASSPHRASE }}" --batch forge.keystore.asc > forge.keystore
cd -
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
curl -L -o android-maven-plugin-4.6.2.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.jar
curl -L -o android-maven-plugin-4.6.2.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.pom
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
- name: Build/Install/Publish Desktop to GitHub Packages Apache Maven
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_android }}
- name: Build/Install/Publish to GitHub Packages Apache Maven
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build only desktop and only try to move desktop files
mvn -U -B clean -P windows-linux install -e -T 1C release:clean release:prepare release:perform -DskipTests
mkdir izpack
# move bz2 and jar from work dir to izpack dir
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
# move desktop build.txt and version.txt to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
cd izpack
ls
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
mvn -U -B clean -P windows-linux install release:clean release:prepare release:perform -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Build/Install/Publish Desktop+Android to GitHub Packages Apache Maven
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build both desktop and android
mvn -U -B clean -P windows-linux,android-release-build install -e -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=35.0.0
mkdir izpack
# move bz2 and jar from work dir to izpack dir
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
# move desktop build.txt and version.txt to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
# move android apk and assets.zip
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk izpack/
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip izpack/
cd izpack
ls
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Upload snapshot to GitHub Prerelease
uses: ncipollo/release-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: Release ${{ env.GIT_TAG }}
tag: ${{ env.GIT_TAG }}
artifacts: izpack/*
allowUpdates: true
removeArtifacts: true
makeLatest: true
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Release Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -1,19 +1,12 @@
name: Remove stale branches
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *" # Everday at midnight
jobs:
remove-stale-branches:
if: github.repository_owner == 'Card-Forge'
name: Remove Stale Branches
runs-on: ubuntu-latest
steps:
- uses: fpicalausa/remove-stale-branches@v2.1.0
- uses: fpicalausa/remove-stale-branches@v1.6.0
with:
dry-run: false # Check out the console output before setting this to false
ignore-unknown-authors: true
ignore-branches-with-open-prs: true
default-recipient: tehdiplomat
dry-run: true # Check out the console output before setting this to false

View File

@@ -8,9 +8,6 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 18 * * *'
jobs:
build:
@@ -112,21 +109,16 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Upload snapshot to GitHub Prerelease
uses: ncipollo/release-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with:
name: Daily Snapshot
tag: daily-snapshots
prerelease: true
artifacts: izpack/*
allowUpdates: true
removeArtifacts: true
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: izpack/
server-dir: downloads/dailysnapshots/
state-name: .ftp-deploy-both-sync-state.json
exclude: |
*.pom
*.repositories
*.xml

View File

@@ -13,6 +13,10 @@ on:
# description: 'Upload the completed Android package'
# required: false
# default: true
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 19 * * *'
jobs:
build:
@@ -105,10 +109,3 @@ jobs:
local-dir: upload/
server-dir: downloads/dailysnapshots/
state-name: .ftp-deploy-android-sync-state.json
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Android Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.run_url }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -8,6 +8,9 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '30 18 * * *'
jobs:
build:
@@ -86,10 +89,3 @@ jobs:
*.pom
*.repositories
*.xml
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Desktop Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.run_url }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

3
.gitignore vendored
View File

@@ -12,7 +12,6 @@
.settings
.classpath
.project
.checkstyle
# Ignore VS Code config files
@@ -25,8 +24,6 @@
nbactions.xml
# Ignore flattened pom
.flattened-pom.xml
# Ignore binaries, temp files and test output, everywhere

View File

@@ -1,6 +1,18 @@
<!--
Derived from: https://stackoverflow.com/a/67002852
-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 http://maven.apache.org/xsd/settings-1.2.0.xsd">
<mirrors>
<mirror>
<id>4thline-repo-http-unblocker</id>
<mirrorOf>4thline-repo</mirrorOf>
<name></name>
<url>http://4thline.org/m2</url>
</mirror>
</mirrors>
<servers>
<server>
<id>cardforge-repo</id>

View File

@@ -26,13 +26,13 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
### 📥 Desktop Installation
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots).
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/).
- **Tip:** Extract to a new folder to prevent version conflicts.
3. **User Data Management:** Previous players data is preserved during upgrades.
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
### 📱 Android Installation
- Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
- Download the **APK** from the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/). On the first launch, Forge will automatically download all necessary assets.
---

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.00</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -45,16 +45,17 @@ public class BiomeStructureDataMappingEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping biomeData))
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping))
return label;
BiomeStructureData.BiomeStructureDataMapping data=(BiomeStructureData.BiomeStructureDataMapping) value;
// Get the renderer component from parent class
label.setText(biomeData.name);
label.setText(data.name);
if(editor.data!=null)
{
SwingAtlas itemAtlas=new SwingAtlas(Config.instance().getFile(editor.data.structureAtlasPath));
if(itemAtlas.has(biomeData.name))
label.setIcon(itemAtlas.get(biomeData.name));
if(itemAtlas.has(data.name))
label.setIcon(itemAtlas.get(data.name));
else
{
ImageIcon img=itemAtlas.getAny();

View File

@@ -25,8 +25,9 @@ public class DialogOptionEditor extends JComponent{
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof DialogData dialog))
if(!(value instanceof DialogData))
return label;
DialogData dialog=(DialogData) value;
StringBuilder builder=new StringBuilder();
if(dialog.name==null||dialog.name.isEmpty())
builder.append("[[Blank Option]]");

View File

@@ -27,16 +27,17 @@ public class ItemsEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof ItemData item))
if(!(value instanceof ItemData))
return label;
ItemData Item=(ItemData) value;
// Get the renderer component from parent class
label.setText(item.name);
label.setText(Item.name);
if(itemAtlas==null)
itemAtlas=new SwingAtlas(Config.instance().getFile(Paths.ITEMS_ATLAS));
if(itemAtlas.has(item.iconName))
label.setIcon(itemAtlas.get(item.iconName));
if(itemAtlas.has(Item.iconName))
label.setIcon(itemAtlas.get(Item.iconName));
else
{
ImageIcon img=itemAtlas.getAny();

View File

@@ -26,8 +26,9 @@ public class QuestEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestData quest))
if(!(value instanceof AdventureQuestData))
return label;
AdventureQuestData quest=(AdventureQuestData) value;
// Get the renderer component from parent class
label.setText(quest.name);

View File

@@ -26,8 +26,9 @@ public class QuestStageEditor extends JComponent{
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestStage stageData))
if(!(value instanceof AdventureQuestStage))
return label;
AdventureQuestStage stageData=(AdventureQuestStage) value;
label.setText(stageData.name);
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
return label;

View File

@@ -25,10 +25,10 @@ public class RewardEdit extends FormPanel {
TextListEdit colors =new TextListEdit(new String[] { "White", "Blue", "Black", "Red", "Green" });
TextListEdit rarity =new TextListEdit(new String[] { "Basic Land", "Common", "Uncommon", "Rare", "Mythic Rare" });
TextListEdit subTypes =new TextListEdit();
TextListEdit cardTypes =new TextListEdit(Arrays.stream(CardType.CoreType.values()).map(CardType.CoreType::toString).toArray(String[]::new));
TextListEdit superTypes =new TextListEdit(Arrays.stream(CardType.Supertype.values()).map(CardType.Supertype::toString).toArray(String[]::new));
TextListEdit cardTypes =new TextListEdit(Arrays.asList(CardType.CoreType.values()).stream().map(CardType.CoreType::toString).toArray(String[]::new));
TextListEdit superTypes =new TextListEdit(Arrays.asList(CardType.Supertype.values()).stream().map(CardType.Supertype::toString).toArray(String[]::new));
TextListEdit manaCosts =new TextListEdit();
TextListEdit keyWords =new TextListEdit(Arrays.stream(Keyword.values()).map(Keyword::toString).toArray(String[]::new));
TextListEdit keyWords =new TextListEdit(Arrays.asList(Keyword.values()).stream().map(Keyword::toString).toArray(String[]::new));
JComboBox colorType =new JComboBox(new String[] { "Any", "Colorless", "MultiColor", "MonoColor"});
JTextField cardText =new JTextField();
private boolean updating=false;

View File

@@ -43,8 +43,9 @@ public class WorldEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof BiomeData biome))
if(!(value instanceof BiomeData))
return label;
BiomeData biome=(BiomeData) value;
// Get the renderer component from parent class
label.setText(biome.name);

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.00</version>
</parent>
<artifactId>forge-ai</artifactId>

View File

@@ -13,7 +13,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class AiDeckStatistics {
public class AIDeckStatistics {
public float averageCMC = 0;
// TODO implement this. Use a numerically stable algorithm from
@@ -24,9 +24,9 @@ public class AiDeckStatistics {
// in WUBRGC order from ManaCost.getColorShardCounts()
public int[] maxPips = null;
// public int[] numSources = new int[6];
// public int[] numSources = new int[6];
public int numLands = 0;
public AiDeckStatistics(float averageCMC, float stddevCMC, int maxCost, int maxColoredCost, int[] maxPips, int numLands) {
public AIDeckStatistics(float averageCMC, float stddevCMC, int maxCost, int maxColoredCost, int[] maxPips, int numLands) {
this.averageCMC = averageCMC;
this.stddevCMC = stddevCMC;
this.maxCost = maxCost;
@@ -35,7 +35,7 @@ public class AiDeckStatistics {
this.numLands = numLands;
}
public static AiDeckStatistics fromCards(List<Card> cards) {
public static AIDeckStatistics fromCards(List<Card> cards) {
int totalCMC = 0;
int totalCount = 0;
int numLands = 0;
@@ -75,7 +75,7 @@ public class AiDeckStatistics {
}
return new AiDeckStatistics(totalCount == 0 ? 0 : totalCMC / (float)totalCount,
return new AIDeckStatistics(totalCount == 0 ? 0 : totalCMC / (float)totalCount,
0, // TODO use https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
maxCost,
maxColoredCost,
@@ -85,7 +85,7 @@ public class AiDeckStatistics {
}
public static AiDeckStatistics fromDeck(Deck deck, Player player) {
public static AIDeckStatistics fromDeck(Deck deck, Player player) {
List<Card> cardlist = new ArrayList<>();
for (final Map.Entry<DeckSection, CardPool> deckEntry : deck) {
switch (deckEntry.getKey()) {
@@ -104,7 +104,7 @@ public class AiDeckStatistics {
return fromCards(cardlist);
}
public static AiDeckStatistics fromPlayer(Player player) {
public static AIDeckStatistics fromPlayer(Player player) {
Deck deck = player.getRegisteredPlayer().getDeck();
if (deck.isEmpty()) {
// we're in a test or some weird match, search through the hand and library and build the decklist
@@ -120,6 +120,7 @@ public class AiDeckStatistics {
}
return fromDeck(deck, player);
}
}

View File

@@ -17,6 +17,8 @@
*/
package forge.ai;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi;
@@ -37,21 +39,17 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.*;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.function.Predicate;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -76,9 +74,6 @@ public class AiAttackController {
private int aiAggression = 0; // how aggressive the ai is attack will be depending on circumstances
private final boolean nextTurn; // include creature that can only attack/block next turn
private final int timeOut;
private final boolean canUseTimeout;
private List<CompletableFuture<Integer>> futures = new ArrayList<>();
/**
* <p>
@@ -96,8 +91,6 @@ public class AiAttackController {
myList = ai.getCreaturesInPlay();
this.nextTurn = nextTurn;
refreshCombatants(defendingOpponent);
this.timeOut = ai.getGame().getAITimeout();
this.canUseTimeout = ai.getGame().canUseTimeout();
} // overloaded constructor to evaluate attackers that should attack next turn
public AiAttackController(final Player ai, Card attacker) {
@@ -111,13 +104,11 @@ public class AiAttackController {
attackers.add(attacker);
}
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn);
this.timeOut = ai.getGame().getAITimeout();
this.canUseTimeout = ai.getGame().canUseTimeout();
} // overloaded constructor to evaluate single specified attacker
private void refreshCombatants(GameEntity defender) {
if (defender instanceof Card card && card.isBattle()) {
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
if (defender instanceof Card && ((Card) defender).isBattle()) {
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
} else {
this.oppList = getOpponentCreatures(defendingOpponent);
}
@@ -138,7 +129,7 @@ public class AiAttackController {
CardCollection tappedDefenders = new CardCollection();
for (Card c : CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), canAnimate)) {
for (SpellAbility sa : IterableUtil.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.Animate))) {
for (SpellAbility sa : Iterables.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.Animate))) {
if (sa.usesTargeting() || !sa.getParamOrDefault("Defined", "Self").equals("Self")) {
continue;
}
@@ -162,7 +153,7 @@ public class AiAttackController {
defenders.removeAll(tappedDefenders);
// Transform (e.g. Incubator tokens)
for (SpellAbility sa : IterableUtil.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.SetState))) {
for (SpellAbility sa : Iterables.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.SetState))) {
Card transformedCopy = ComputerUtilCombat.canTransform(c);
if (transformedCopy.isCreature()) {
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
@@ -313,8 +304,7 @@ public class AiAttackController {
}
}
// Poison opponent if unblocked
if (defender instanceof Player player
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) {
return true;
}
@@ -625,9 +615,9 @@ public class AiAttackController {
// TODO: the AI should ideally predict how many times it can activate
// for now, unless the opponent is tapped out, break at this point
// and do not predict the blocker limit (which is safer)
if (defendingOpponent.getLandsInPlay().anyMatch(CardPredicates.UNTAPPED)) {
if (Iterables.any(defendingOpponent.getLandsInPlay(), CardPredicates.Presets.UNTAPPED)) {
maxBlockersAfterCrew += CardLists.count(CardLists.getNotType(defendingOpponent.getCardsIn(ZoneType.Battlefield), "Creature"),
CardPredicates.isType("Vehicle").and(CardPredicates.UNTAPPED));
Predicates.and(CardPredicates.isType("Vehicle"), CardPredicates.Presets.UNTAPPED));
}
}
@@ -851,9 +841,10 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons)
if (defender != defendingOpponent) {
if (defender instanceof Player p) {
defendingOpponent = p;
} else if (defender instanceof Card defCard) {
if (defender instanceof Player) {
defendingOpponent = (Player) defender;
} else if (defender instanceof Card) {
Card defCard = (Card) defender;
if (defCard.isBattle()) {
defendingOpponent = defCard.getProtectingPlayer();
} else {
@@ -893,7 +884,7 @@ public class AiAttackController {
// TODO: detect Season of the Witch by presence of a card with a specific trigger
final boolean seasonOfTheWitch = ai.getGame().isCardInPlay("Season of the Witch");
final Queue<Card> attackersLeft = new ConcurrentLinkedQueue<>(this.attackers);
List<Card> attackersLeft = new ArrayList<>(this.attackers);
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
@@ -909,27 +900,25 @@ public class AiAttackController {
}
// Attackers that don't really have a choice
final AtomicInteger numForcedAttackers = new AtomicInteger(0);
int numForcedAttackers = 0;
// nextTurn is now only used by effect from Oracle en-Vec, which can skip check must attack,
// because creatures not chosen can't attack.
if (!nextTurn) {
for (final Card attacker : this.attackers) {
final GameEntity finalDefender = defender;
futures.add(CompletableFuture.supplyAsync(()-> {
GameEntity mustAttackDef = null;
if (attacker.getSVar("MustAttack").equals("True")) {
mustAttackDef = finalDefender;
mustAttackDef = defender;
} else if (attacker.hasSVar("EndOfTurnLeavePlay")
&& isEffectiveAttacker(ai, attacker, combat, finalDefender)) {
mustAttackDef = finalDefender;
&& isEffectiveAttacker(ai, attacker, combat, defender)) {
mustAttackDef = defender;
} else if (seasonOfTheWitch) {
//TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
mustAttackDef = finalDefender;
mustAttackDef = defender;
} else {
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) return 0;
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) continue;
// check defenders in order of maximum requirements
List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements();
final GameEntity def = finalDefender;
final GameEntity def = defender;
reqs.sort((r1, r2) -> {
if (r1.getValue() == r2.getValue()) {
// try to attack the designated defender
@@ -947,8 +936,8 @@ public class AiAttackController {
return 1;
}
// or weakest player
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
return p1.getLife() - p2.getLife();
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
}
}
return r2.getValue() - r1.getValue();
@@ -965,20 +954,9 @@ public class AiAttackController {
if (mustAttackDef != null) {
combat.addAttacker(attacker, mustAttackDef);
attackersLeft.remove(attacker);
numForcedAttackers.incrementAndGet();
numForcedAttackers++;
}
return 0;
}).exceptionally(ex -> {
ex.printStackTrace();
return 0;
}));
}
CompletableFuture<?>[] futuresArray = futures.toArray(new CompletableFuture<?>[0]);
if (canUseTimeout)
CompletableFuture.allOf(futuresArray).completeOnTimeout(null, timeOut, TimeUnit.SECONDS).join();
else
CompletableFuture.allOf(futuresArray).join();
futures.clear();
if (attackersLeft.isEmpty()) {
return aiAggression;
}
@@ -986,19 +964,18 @@ public class AiAttackController {
// Lightmine Field: make sure the AI doesn't wipe out its own creatures
if (lightmineField) {
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers.get(), playAggro);
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro);
}
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily
if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers.get(), attackMax)) {
if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers, attackMax)) {
return aiAggression;
}
if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else
if (LOG_AI_ATTACKS)
System.out.println("Assault");
List<Card> left = new ArrayList<>(attackersLeft);
CardLists.sortByPowerDesc(left);
for (Card attacker : left) {
CardLists.sortByPowerDesc(attackersLeft);
for (Card attacker : attackersLeft) {
// reached max, breakup
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
return aiAggression;
@@ -1288,20 +1265,19 @@ public class AiAttackController {
if ( LOG_AI_ATTACKS )
System.out.println("Normal attack");
List<Card> left = new ArrayList<>(attackersLeft);
left = notNeededAsBlockers(combat.getAttackers(), left);
left = sortAttackers(left);
attackersLeft = notNeededAsBlockers(combat.getAttackers(), attackersLeft);
attackersLeft = sortAttackers(attackersLeft);
if ( LOG_AI_ATTACKS )
System.out.println("attackersLeft = " + left);
System.out.println("attackersLeft = " + attackersLeft);
FCollection<GameEntity> possibleDefenders = new FCollection<>(defendingOpponent);
possibleDefenders.addAll(defendingOpponent.getPlaneswalkersInPlay());
while (!left.isEmpty()) {
while (!attackersLeft.isEmpty()) {
CardCollection attackersAssigned = new CardCollection();
for (int i = 0; i < left.size(); i++) {
final Card attacker = left.get(i);
for (int i = 0; i < attackersLeft.size(); i++) {
final Card attacker = attackersLeft.get(i);
if (aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike()
&& ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, defendingOpponent)
>= ComputerUtilCombat.getDamageToKill(attacker, false)) {
@@ -1315,7 +1291,7 @@ public class AiAttackController {
attackersAssigned.add(attacker);
// check if attackers are enough to finish the attacked planeswalker
if (i < left.size() - 1 && defender instanceof Card card) {
if (i < attackersLeft.size() - 1 && defender instanceof Card) {
final int blockNum = this.blockers.size();
int attackNum = 0;
int damage = 0;
@@ -1329,19 +1305,19 @@ public class AiAttackController {
}
}
// if enough damage: switch to next planeswalker
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
break;
}
}
}
}
left.removeAll(attackersAssigned);
attackersLeft.removeAll(attackersAssigned);
possibleDefenders.remove(defender);
if (left.isEmpty() || possibleDefenders.isEmpty()) {
if (attackersLeft.isEmpty() || possibleDefenders.isEmpty()) {
break;
}
CardCollection pwDefending = new CardCollection(IterableUtil.filter(possibleDefenders, Card.class));
CardCollection pwDefending = new CardCollection(Iterables.filter(possibleDefenders, Card.class));
if (pwDefending.isEmpty()) {
// TODO for now only looks at same player as we'd have to check the others from start too
//defender = new PlayerCollection(Iterables.filter(possibleDefenders, Player.class)).min(PlayerPredicates.compareByLife());
@@ -1391,13 +1367,12 @@ public class AiAttackController {
canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, Card::getNetToughness);
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
dangerousBlockersPresent = validBlockers.anyMatch(
CardPredicates.hasKeyword(Keyword.LIFELINK)
.or(Card::isWitherDamage)
);
dangerousBlockersPresent = Iterables.any(validBlockers, Predicates.or(
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
CardPredicates.hasKeyword(Keyword.LIFELINK)));
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
defPower = CardLists.getTotalPower(validBlockers, null);
defPower = CardLists.getTotalPower(validBlockers, true, false);
// look at the attacker in relation to the blockers to establish a
// number of factors about the attacking context that will be relevant
@@ -1422,7 +1397,7 @@ public class AiAttackController {
canKillAll = false;
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")
|| blocker.isWitherDamage() || blocker.hasKeyword(Keyword.LIFELINK)) {
|| blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT) || blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
@@ -1588,7 +1563,7 @@ public class AiAttackController {
// but there are no creatures it can target, no need to exert with it
boolean missTarget = false;
for (StaticAbility st : c.getStaticAbilities()) {
if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) {
continue;
}
SpellAbility sa = st.getPayingTrigSA();
@@ -1610,12 +1585,12 @@ public class AiAttackController {
break;
}
if (sa.usesTargeting()) {
sa.setActivatingPlayer(c.getController());
sa.setActivatingPlayer(c.getController(), true);
List<Card> validTargets = CardUtil.getValidCardsToTarget(sa);
if (validTargets.isEmpty()) {
missTarget = true;
break;
} else if (sa.isCurse() && validTargets.stream().noneMatch(
} else if (sa.isCurse() && !Iterables.any(validTargets,
CardPredicates.isControlledByAnyOf(c.getController().getOpponents()))) {
// e.g. Ahn-Crop Crasher - the effect is only good when aimed at opponent's creatures
missTarget = true;
@@ -1722,7 +1697,7 @@ public class AiAttackController {
return null; //should never get here
}
private void doLightmineFieldAttackLogic(final Queue<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
private void doLightmineFieldAttackLogic(final List<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
CardCollection attSorted = new CardCollection(attackersLeft);
CardCollection attUnsafe = new CardCollection();
CardLists.sortByToughnessDesc(attSorted);
@@ -1752,15 +1727,13 @@ public class AiAttackController {
attackersLeft.removeAll(attUnsafe);
}
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final List<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false;
if (defender instanceof Player player) {
revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card card) {
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
if (defender instanceof Player) {
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card) {
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
}
if (!revengeOfRavens) {

View File

@@ -18,7 +18,9 @@
package forge.ai;
import java.util.*;
import java.util.function.Predicate;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardStateName;
import forge.game.GameEntity;
@@ -161,12 +163,12 @@ public class AiBlockController {
// defend battles with fewer defense counters before battles with more defense counters,
// if planeswalker/battle will be too difficult to defend don't even bother
for (GameEntity defender : defenders) {
if ((defender instanceof Card card1 && card1.getController().equals(ai))
|| (defender instanceof Card card2 && card2.isBattle() && card2.getProtectingPlayer().equals(ai))) {
final CardCollection ccAttackers = combat.getAttackersOf(defender);
if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
final CardCollection attackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(ccAttackers);
sortedAttackers.addAll(ccAttackers);
CardLists.sortByPowerDesc(attackers);
sortedAttackers.addAll(attackers);
} else if (defender instanceof Player && defender.equals(ai)) {
firstAttacker = combat.getAttackersOf(defender);
CardLists.sortByPowerDesc(firstAttacker);
@@ -325,7 +327,7 @@ public class AiBlockController {
}
private Predicate<Card> rampagesOrNeedsManyToBlock(final Combat combat) {
return CardPredicates.hasKeyword(Keyword.RAMPAGE).or(input -> {
return Predicates.or(CardPredicates.hasKeyword(Keyword.RAMPAGE), input -> {
// select creature that has a max blocker
return StaticAbilityCantAttackBlock.getMinMaxBlocker(input, combat.getDefenderPlayerByAttacker(input)).getRight() < Integer.MAX_VALUE;
});
@@ -366,7 +368,7 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object.
*/
private void makeGangBlocks(final Combat combat) {
List<Card> currentAttackers = CardLists.filter(attackersLeft, rampagesOrNeedsManyToBlock(combat).negate());
List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
List<Card> blockers;
// Try to block an attacker without first strike with a gang of first strikers
@@ -738,11 +740,11 @@ public class AiBlockController {
List<Card> chumpBlockers;
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, rampagesOrNeedsManyToBlock(combat).negate());
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
// TODO - Instead of filtering out rampage-like and similar triggers, make the AI properly count P/T and
// reinforce when actually possible without losing material.
tramplingAttackers = CardLists.filter(tramplingAttackers, changesPTWhenBlocked(true).negate());
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(changesPTWhenBlocked(true)));
for (final Card attacker : tramplingAttackers) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()) {
@@ -793,11 +795,11 @@ public class AiBlockController {
private void reinforceBlockersToKill(final Combat combat) {
List<Card> safeBlockers;
List<Card> blockers;
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, rampagesOrNeedsManyToBlock(combat).negate());
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
// TODO - Instead of filtering out rampage-like and similar triggers, make the AI properly count P/T and
// reinforce when actually possible without losing material.
targetAttackers = CardLists.filter(targetAttackers, changesPTWhenBlocked(false).negate());
targetAttackers = CardLists.filter(targetAttackers, Predicates.not(changesPTWhenBlocked(false)));
for (final Card attacker : targetAttackers) {
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -872,9 +874,9 @@ public class AiBlockController {
CardCollection threatenedPWs = new CardCollection();
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card card) {
if (def instanceof Card) {
if (!onlyIfLethal) {
threatenedPWs.add(card);
threatenedPWs.add((Card) def);
} else {
int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) {
@@ -906,12 +908,12 @@ public class AiBlockController {
continue;
}
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card card && threatenedPWs.contains(def)) {
if (def instanceof Card && threatenedPWs.contains(def)) {
Card blockerDecided = null;
for (final Card blocker : chumpPWDefenders) {
if (CombatUtil.canBlock(attacker, blocker, combat)) {
combat.addBlocker(attacker, blocker);
pwsWithChumpBlocks.add(card);
pwsWithChumpBlocks.add((Card) def);
chosenChumpBlockers.add(blocker);
blockerDecided = blocker;
blockersLeft.remove(blocker);
@@ -1343,11 +1345,11 @@ public class AiBlockController {
boolean creatureParityOrAllowedDiff = aiCreatureCount
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
boolean wantToTradeWithCreatInHand = !checkingOther && randomTradeIfCreatInHand
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES)
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.Presets.CREATURES)
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
&& combat.getDefenderByAttacker(attacker) instanceof Card card
&& card.isPlaneswalker();
&& combat.getDefenderByAttacker(attacker) instanceof Card
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.

View File

@@ -18,17 +18,12 @@
package forge.ai;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import forge.game.card.Card;
import forge.game.player.Player;
/**
* <p>
* AiCardMemory class.
@@ -67,13 +62,75 @@ public class AiCardMemory {
REVEALED_CARDS // These cards were recently revealed to the AI by a call to PlayerControllerAi.reveal
}
private final Supplier<Map<MemorySet, Set<Card>>> memoryMap = Suppliers.memoize(Maps::newConcurrentMap);
private final Set<Card> memMandatoryAttackers;
private final Set<Card> memTrickAttackers;
private final Set<Card> memHeldManaSources;
private final Set<Card> memHeldManaSourcesForCombat;
private final Set<Card> memHeldManaSourcesForEnemyCombat;
private final Set<Card> memHeldManaSourcesForNextSpell;
private final Set<Card> memAttachedThisTurn;
private final Set<Card> memAnimatedThisTurn;
private final Set<Card> memBouncedThisTurn;
private final Set<Card> memActivatedThisTurn;
private final Set<Card> memChosenFogEffect;
private final Set<Card> memMarkedToAvoidReentry;
private final Set<Card> memPaysTapCost;
private final Set<Card> memPaysSacCost;
private final Set<Card> memRevealedCards;
public AiCardMemory() {
this.memMandatoryAttackers = new HashSet<>();
this.memHeldManaSources = new HashSet<>();
this.memHeldManaSourcesForCombat = new HashSet<>();
this.memHeldManaSourcesForEnemyCombat = new HashSet<>();
this.memAttachedThisTurn = new HashSet<>();
this.memAnimatedThisTurn = new HashSet<>();
this.memBouncedThisTurn = new HashSet<>();
this.memActivatedThisTurn = new HashSet<>();
this.memTrickAttackers = new HashSet<>();
this.memChosenFogEffect = new HashSet<>();
this.memMarkedToAvoidReentry = new HashSet<>();
this.memHeldManaSourcesForNextSpell = new HashSet<>();
this.memPaysTapCost = new HashSet<>();
this.memPaysSacCost = new HashSet<>();
this.memRevealedCards = new HashSet<>();
}
private Set<Card> getMemorySet(MemorySet set) {
return memoryMap.get().computeIfAbsent(set, value -> Sets.newConcurrentHashSet());
switch (set) {
case MANDATORY_ATTACKERS:
return memMandatoryAttackers;
case TRICK_ATTACKERS:
return memTrickAttackers;
case HELD_MANA_SOURCES_FOR_MAIN2:
return memHeldManaSources;
case HELD_MANA_SOURCES_FOR_DECLBLK:
return memHeldManaSourcesForCombat;
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
return memHeldManaSourcesForEnemyCombat;
case HELD_MANA_SOURCES_FOR_NEXT_SPELL:
return memHeldManaSourcesForNextSpell;
case ATTACHED_THIS_TURN:
return memAttachedThisTurn;
case ANIMATED_THIS_TURN:
return memAnimatedThisTurn;
case BOUNCED_THIS_TURN:
return memBouncedThisTurn;
case ACTIVATED_THIS_TURN:
return memActivatedThisTurn;
case CHOSEN_FOG_EFFECT:
return memChosenFogEffect;
case MARKED_TO_AVOID_REENTRY:
return memMarkedToAvoidReentry;
case PAYS_TAP_COST:
return memPaysTapCost;
case PAYS_SAC_COST:
return memPaysSacCost;
case REVEALED_CARDS:
return memRevealedCards;
default:
return null;
}
}
/**
@@ -88,7 +145,10 @@ public class AiCardMemory {
if (c == null) {
return false;
}
return getMemorySet(set).contains(c);
Set<Card> memorySet = getMemorySet(set);
return memorySet != null && memorySet.contains(c);
}
/**
@@ -100,7 +160,17 @@ public class AiCardMemory {
* @return true, if at least one card with the given name is remembered in the given memory set
*/
public boolean isRememberedCardByName(String cardName, MemorySet set) {
return getMemorySet(set).stream().anyMatch(c -> c.getName().equals(cardName));
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
for (Card c : memorySet) {
if (c.getName().equals(cardName)) {
return true;
}
}
}
return false;
}
/**
@@ -114,7 +184,17 @@ public class AiCardMemory {
* @return true, if at least one card with the given name is remembered in the given memory set
*/
public boolean isRememberedCardByName(String cardName, MemorySet set, Player owner) {
return getMemorySet(set).stream().anyMatch(c -> c.getName().equals(cardName) && c.getOwner().equals(owner));
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
for (Card c : memorySet) {
if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
return true;
}
}
}
return false;
}
/**
@@ -127,7 +207,14 @@ public class AiCardMemory {
public boolean rememberCard(Card c, MemorySet set) {
if (c == null)
return false;
return getMemorySet(set).add(c);
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
memorySet.add(c);
}
return true;
}
/**
@@ -144,7 +231,14 @@ public class AiCardMemory {
if (!isRememberedCard(c, set)) {
return false;
}
return getMemorySet(set).remove(c);
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
memorySet.remove(c);
}
return true;
}
/**
@@ -155,11 +249,16 @@ public class AiCardMemory {
* @return true, if at least one card with the given name was previously remembered in the given memory set and was successfully forgotten
*/
public boolean forgetAnyCardWithName(String cardName, MemorySet set) {
for (Card c : getMemorySet(set)) {
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
for (Card c : memorySet) {
if (c.getName().equals(cardName)) {
return forgetCard(c, set);
}
}
}
return false;
}
@@ -172,11 +271,16 @@ public class AiCardMemory {
* @return true, if at least one card with the given name was previously remembered in the given memory set and was successfully forgotten
*/
public boolean forgetAnyCardWithName(String cardName, MemorySet set, Player owner) {
for (Card c : getMemorySet(set)) {
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
for (Card c : memorySet) {
if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
return forgetCard(c, set);
}
}
}
return false;
}

View File

@@ -18,17 +18,18 @@
package forge.ai;
import com.esotericsoftware.minlog.Log;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.LearnAi;
import forge.ai.simulation.GameStateEvaluator;
import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName;
import forge.card.CardType;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.card.mana.ManaCost;
import forge.deck.Deck;
import forge.deck.DeckSection;
@@ -38,6 +39,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
@@ -54,30 +56,20 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityDisableTriggers;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.*;
import forge.util.Aggregates;
import forge.util.ComparatorUtil;
import forge.util.Expressions;
import forge.util.MyRandom;
import io.sentry.Breadcrumb;
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.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static forge.ai.ComputerUtilMana.getAvailableManaEstimate;
import static java.lang.Math.max;
/**
* <p>
@@ -98,7 +90,6 @@ public class AiController {
private SpellAbilityPicker simPicker;
private int lastAttackAggression;
private boolean useLivingEnd;
private List<SpellAbility> skipped;
public AiController(final Player computerPlayer, final Game game0) {
player = computerPlayer;
@@ -177,7 +168,7 @@ public class AiController {
for (final Card c : all) {
for (final SpellAbility sa : c.getNonManaAbilities()) {
if (sa instanceof SpellPermanent) {
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
if (checkETBEffects(c, sa, ApiType.Counter)) {
spellAbilities.add(sa);
}
@@ -211,7 +202,7 @@ public class AiController {
return true;
}
}
if (game.getCardsIn(ZoneType.Graveyard).anyMatch(CardPredicates.nameEquals(hostName))) {
if (Iterables.any(game.getCardsIn(ZoneType.Graveyard), CardPredicates.nameEquals(hostName))) {
return true;
}
}
@@ -295,7 +286,7 @@ public class AiController {
}
// can't fetch partner isn't problematic
if (tr.isKeyword(Keyword.PARTNER)) {
if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
continue;
}
@@ -408,22 +399,6 @@ public class AiController {
private static List<SpellAbility> getPlayableCounters(final CardCollection l) {
final List<SpellAbility> spellAbility = Lists.newArrayList();
for (final Card c : l) {
if (c.isForetold() && c.getAlternateState() != null) {
try {
for (final SpellAbility sa : c.getAlternateState().getNonManaAbilities()) {
// Check if this AF is a Counterspell
if (sa.getApi() == ApiType.Counter) {
spellAbility.add(sa);
} else {
if (sa.getApi() != null && sa.getApi().toString().contains("Foretell") && c.getAlternateState().getName().equalsIgnoreCase("Saw It Coming"))
spellAbility.add(sa);
}
}
} catch (Exception e) {
// facedown and alternatestate counters should be accessible
e.printStackTrace();
}
} else {
for (final SpellAbility sa : c.getNonManaAbilities()) {
// Check if this AF is a Counterspell
if (sa.getApi() == ApiType.Counter) {
@@ -431,16 +406,15 @@ public class AiController {
}
}
}
}
return spellAbility;
}
private CardCollection filterLandsToPlay(CardCollection landList) {
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
CardCollection nonLandList = CardLists.filter(hand, CardPredicates.NON_LANDS);
CardCollection nonLandList = CardLists.filter(hand, Predicates.not(CardPredicates.Presets.LANDS));
if (landList.size() == 1 && nonLandList.size() < 3) {
CardCollectionView cardsInPlay = player.getCardsIn(ZoneType.Battlefield);
CardCollection landsInPlay = CardLists.filter(cardsInPlay, CardPredicates.LANDS);
CardCollection landsInPlay = CardLists.filter(cardsInPlay, Presets.LANDS);
CardCollection allCards = new CardCollection(player.getCardsIn(ZoneType.Graveyard));
allCards.addAll(player.getCardsIn(ZoneType.Command));
allCards.addAll(cardsInPlay);
@@ -470,7 +444,7 @@ public class AiController {
String name = c.getName();
CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) {
if (battlefield.anyMatch(CardPredicates.nameEquals(name))) {
if (Iterables.any(battlefield, CardPredicates.nameEquals(name))) {
return false;
}
}
@@ -478,7 +452,7 @@ public class AiController {
final CardCollectionView hand1 = player.getCardsIn(ZoneType.Hand);
CardCollection lands = new CardCollection(battlefield);
lands.addAll(hand1);
lands = CardLists.filter(lands, CardPredicates.LANDS);
lands = CardLists.filter(lands, Presets.LANDS);
int maxCmcInHand = Aggregates.max(hand1, Card::getCMC);
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
@@ -492,7 +466,7 @@ public class AiController {
return false;
}
}
return c.getAllPossibleAbilities(player, true).stream().anyMatch(SpellAbility::isLandAbility);
return Iterables.any(c.getAllPossibleAbilities(player, true), SpellAbility::isLandAbility);
});
return landList;
}
@@ -502,9 +476,7 @@ public class AiController {
return null;
}
landList = ComputerUtilCard.dedupeCards(landList);
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.NON_LANDS);
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
// Some considerations for Momir/MoJhoSto
boolean hasMomir = player.isCardInCommand("Momir Vig, Simic Visionary Avatar");
@@ -544,7 +516,7 @@ public class AiController {
landList = unreflectedLands;
}
// try to skip lands that enter the battlefield tapped if we might want to play something this turn
//try to skip lands that enter the battlefield tapped
if (!nonLandsInHand.isEmpty()) {
CardCollection nonTappedLands = new CardCollection();
for (Card land : landList) {
@@ -552,6 +524,7 @@ public class AiController {
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(land);
repParams.put(AbilityKey.Origin, land.getZone().getZoneType());
repParams.put(AbilityKey.Destination, ZoneType.Battlefield);
repParams.put(AbilityKey.Source, land);
// add Params for AddCounter Replacements
GameEntityCounterTable table = new GameEntityCounterTable();
@@ -565,7 +538,7 @@ public class AiController {
if (reSA == null || !ApiType.Tap.equals(reSA.getApi())) {
continue;
}
reSA.setActivatingPlayer(reSA.getHostCard().getController());
reSA.setActivatingPlayer(reSA.getHostCard().getController(), true);
if (reSA.metConditions()) {
foundTapped = true;
break;
@@ -579,55 +552,17 @@ public class AiController {
nonTappedLands.add(land);
}
// if we have the choice, see if we can play an untapped land
if (!nonTappedLands.isEmpty()) {
// If we have a lot of mana, prefer untapped lands.
// We're either topdecking or have drawn enough the tempo no longer matters.
int mana_available = getAvailableManaEstimate(player);
if (mana_available > 6) {
landList = nonTappedLands;
} else {
// get the costs of the nonland cards in hand and the mana we have available.
// If adding one won't make something new castable, then pick a tapland.
int max_inc = 0;
for (Card c : nonTappedLands) {
max_inc = max(max_inc, c.getMaxManaProduced());
}
// check for lands with no mana abilities
if (max_inc > 0) {
boolean found = false;
for (Card c : nonLandsInHand) {
// TODO make this work better with split cards and Monocolored Hybrid
ManaCost cost = c.getManaCost();
// check for incremental cmc
// check for X cost spells
if ((cost.getCMC() - mana_available) * (cost.getCMC() - mana_available - max_inc - 1) < 0 ||
(cost.countX() > 0 && cost.getCMC() >= mana_available)) {
found = true;
break;
}
}
if (found) {
landList = nonTappedLands;
}
}
}
}
}
// Early out if we only have one card left
if (landList.size() == 1) {
return landList.get(0);
}
// Choose first land to be able to play a one drop
if (player.getLandsInPlay().isEmpty()) {
CardCollection oneDrops = CardLists.filter(nonLandsInHand, CardPredicates.hasCMC(1));
for (int i = 0; i < MagicColor.WUBRG.length; i++) {
byte color = MagicColor.WUBRG[i];
if (oneDrops.anyMatch(CardPredicates.isColor(color))) {
if (Iterables.any(oneDrops, CardPredicates.isColor(color))) {
for (Card land : landList) {
if (land.getType().hasSubtype(MagicColor.Constant.BASIC_LANDS.get(i))) {
return land;
@@ -642,85 +577,39 @@ public class AiController {
}
}
// play lands with a basic type and/or color that is needed the most
//play lands with a basic type that is needed the most
final CardCollectionView landsInBattlefield = player.getCardsIn(ZoneType.Battlefield);
final List<String> basics = Lists.newArrayList();
// what colors are available?
int[] counts = new int[6]; // in WUBRGC order
for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
for (SpellAbility m: c.getManaAbilities()) {
m.setActivatingPlayer(c.getController());
for (AbilityManaPart mp : m.getAllManaParts()) {
for (String part : mp.mana(m).split(" ")) {
// TODO handle any
int index = ManaAtom.getIndexFromName(part);
if (index != -1) {
counts[index] += 1;
}
}
}
}
}
// what types can I go get?
int[] basic_counts = new int[5]; // in WUBRG order
for (final String name : MagicColor.Constant.BASIC_LANDS) {
if (!CardLists.getType(landList, name).isEmpty()) {
basics.add(name);
}
}
if (!basics.isEmpty()) {
for (int i = 0; i < MagicColor.Constant.BASIC_LANDS.size(); i++) {
String b = MagicColor.Constant.BASIC_LANDS.get(i);
// Which basic land is least available
int minSize = Integer.MAX_VALUE;
String minType = null;
for (String b : basics) {
final int num = CardLists.getType(landsInBattlefield, b).size();
basic_counts[i] = num;
}
}
// pick the land with the best score.
// use the evaluation plus a modifier for each new color pip and basic type
Card toReturn = Aggregates.itemWithMax(IterableUtil.filter(landList, Card::hasPlayableLandFace),
(card -> {
// base score is for the evaluation score
int score = GameStateEvaluator.evaluateLand(card);
// add for new basic type
for (String cardType: card.getType()) {
int index = MagicColor.Constant.BASIC_LANDS.indexOf(cardType);
if (index != -1 && basic_counts[index] == 0) {
score += 25;
if (num < minSize) {
minType = b;
minSize = num;
}
}
// TODO handle fetchlands and what they can fetch for
// determine new color pips
int[] card_counts = new int[6]; // in WUBRGC order
for (SpellAbility m: card.getManaAbilities()) {
m.setActivatingPlayer(card.getController());
for (AbilityManaPart mp : m.getAllManaParts()) {
for (String part : mp.mana(m).split(" ")) {
// TODO handle any
int index = ManaAtom.getIndexFromName(part);
if (index != -1) {
card_counts[index] += 1;
}
}
}
if (minType != null) {
landList = CardLists.getType(landList, minType);
}
// use 1 / x+1 for diminishing returns
// TODO use max pips of each color in the deck from deck statistics to weight this
for (int i = 0; i < card_counts.length; i++) {
int diff = (card_counts[i] * 50) / (counts[i] + 1);
score += diff;
// pick dual lands if available
if (Iterables.any(landList, Predicates.not(CardPredicates.Presets.BASIC_LANDS))) {
landList = CardLists.filter(landList, Predicates.not(CardPredicates.Presets.BASIC_LANDS));
}
// TODO utility lands only if we have enough to pay their costs
// TODO Tron lands and other lands that care about land counts
return score;
}));
return toReturn;
}
return ComputerUtilCard.getBestLandToPlayAI(landList);
}
// if return true, go to next phase
@@ -733,7 +622,7 @@ public class AiController {
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(possibleCounters, player)) {
SpellAbility currentSA = sa;
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
// check everything necessary
AiPlayDecision opinion = canPlayAndPayFor(currentSA);
@@ -788,7 +677,7 @@ public class AiController {
if (saApi == ApiType.Counter || saApi == exceptSA) {
continue;
}
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
// TODO: this currently only works as a limited prediction of permanent spells.
// Ideally this should cast canPlaySa to determine that the AI is truly able/willing to cast a spell,
// but that is currently difficult to implement due to various side effects leading to stack overflow.
@@ -810,14 +699,12 @@ public class AiController {
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.getPayCosts(), sa, true, 0, false);
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
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.getPayCosts(), exceptForThisSa, true, 0, false),
exceptForThisSa, player));
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(ComputerUtilMana.calculateManaCost(exceptForThisSa, true, 0), exceptForThisSa, player));
}
if (manaSources.isEmpty()) {
@@ -868,15 +755,9 @@ public class AiController {
if (currentState != null) {
host.setState(sa.getCardStateName(), false);
}
if (sa.isSpell()) {
host.setCastSA(sa);
}
AiPlayDecision decision = canPlayAndPayForFace(sa);
if (sa.isSpell()) {
host.setCastSA(null);
}
if (currentState != null) {
host.setState(currentState, false);
}
@@ -921,7 +802,7 @@ public class AiController {
}
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
if (!sa.getAllTargetChoices().isEmpty()) {
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa).getTotalMana().getCMC();
}
}
@@ -954,7 +835,7 @@ public class AiController {
// check if some target raised cost
if (!xCost && oldCMC > -1) {
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa).getTotalMana().getCMC();
if (finalCMC > oldCMC) {
xCost = true;
}
@@ -1017,7 +898,7 @@ public class AiController {
Sentry.setExtra("Card", card.getName());
Sentry.setExtra("SA", sa.toString());
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);
boolean canPlay = SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(player, sa);
// remove added extra
Sentry.removeExtra("Card");
@@ -1114,7 +995,7 @@ public class AiController {
costWithBuyback.add(opt.getCost());
}
}
costWithBuyback = CostAdjustment.adjust(costWithBuyback, sa, false);
costWithBuyback = CostAdjustment.adjust(costWithBuyback, sa);
if (costWithBuyback.hasSpecificCostType(CostPayLife.class)
|| costWithBuyback.hasSpecificCostType(CostDiscard.class)
|| costWithBuyback.hasSpecificCostType(CostSacrifice.class)) {
@@ -1130,7 +1011,7 @@ public class AiController {
// Memory Crystal-like effects need special handling
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility s : c.getStaticAbilities()) {
if (s.checkMode(StaticAbilityMode.ReduceCost)
if ("ReduceCost".equals(s.getParam("Mode"))
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
}
@@ -1140,7 +1021,7 @@ public class AiController {
neededMana = 0;
}
int hasMana = getAvailableManaEstimate(player, false);
int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false);
if (hasMana < neededMana - 1) {
return true;
}
@@ -1194,7 +1075,7 @@ public class AiController {
if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) {
CardCollection discards = new CardCollection();
final CardCollectionView inHand = player.getCardsIn(ZoneType.Hand);
final int numLandsOTB = CardLists.count(inHand, CardPredicates.LANDS);
final int numLandsOTB = CardLists.count(inHand, CardPredicates.Presets.LANDS);
int numOppInHand = 0;
for (Player p : player.getGame().getPlayers()) {
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
@@ -1252,8 +1133,8 @@ public class AiController {
if (validCards.isEmpty()) {
continue;
}
final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(validCards, CardPredicates.LANDS);
final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(validCards, CardPredicates.Presets.LANDS);
final int numLandsInHand = landsInHand.size();
// Discard a land
@@ -1395,9 +1276,9 @@ public class AiController {
if (spell instanceof SpellApiBased) {
boolean chance = false;
if (withoutPayingManaCost) {
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerNoCostWithSubs(player, spell, mandatory);
} else {
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory);
}
if (!chance) {
return AiPlayDecision.TargetingFailed;
@@ -1451,7 +1332,9 @@ public class AiController {
for (final Card element : combat.getAttackers()) {
// tapping of attackers happens after Propaganda is paid for
Log.debug("Computer just assigned " + element.getName() + " as an attacker.");
final StringBuilder sb = new StringBuilder();
sb.append("Computer just assigned ").append(element.getName()).append(" as an attacker.");
Log.debug(sb.toString());
}
}
@@ -1495,7 +1378,7 @@ public class AiController {
if (landsWannaPlay != null && !landsWannaPlay.isEmpty()) {
// TODO search for other land it might want to play?
Card land = chooseBestLandToPlay(landsWannaPlay);
if (land != null && (!player.canLoseLife() || player.cantLoseForZeroOrLessLife() || ComputerUtil.getDamageFromETB(player, land) < player.getLife())
if ((!player.canLoseLife() || player.cantLoseForZeroOrLessLife() || ComputerUtil.getDamageFromETB(player, land) < player.getLife())
&& (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land))) {
final List<SpellAbility> abilities = land.getAllPossibleAbilities(player, true);
// skip non Land Abilities
@@ -1528,11 +1411,12 @@ public class AiController {
return false;
}
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.NON_LANDS);
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand),
Predicates.not(CardPredicates.Presets.LANDS));
CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
if (getBooleanProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
if (!otb.anyMatch(CardPredicates.NON_LANDS)) {
if (!Iterables.any(otb, Predicates.not(CardPredicates.Presets.LANDS))) {
return false;
}
}
@@ -1549,12 +1433,12 @@ public class AiController {
int minCMCInHand = Aggregates.min(inHand, Card::getCMC);
if (minCMCInHand == Integer.MAX_VALUE)
minCMCInHand = 0;
int predictedMana = getAvailableManaEstimate(player, true);
int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && minCMCInHand > 0 && !isTapLand;
boolean cantCastAnythingNow = predictedMana < minCMCInHand;
boolean hasRelevantAbsOTB = otb.anyMatch(card -> {
boolean hasRelevantAbsOTB = Iterables.any(otb, card -> {
boolean isTapLand1 = false;
for (ReplacementEffect repl : card.getReplacementEffects()) {
// TODO: improve the detection of taplands
@@ -1575,7 +1459,7 @@ public class AiController {
return false;
});
boolean hasLandBasedEffect = otb.anyMatch(card -> {
boolean hasLandBasedEffect = Iterables.any(otb, card -> {
for (Trigger t : card.getTriggers()) {
Map<String, String> params = t.getMapParams();
if ("ChangesZone".equals(params.get("Mode"))
@@ -1627,13 +1511,6 @@ public class AiController {
}
private SpellAbility getSpellAbilityToPlay() {
if (skipped != null) {
//FIXME: this is for failed SA to skip temporarily, don't know why AI computation for mana fails, maybe due to auto mana compute?
for (SpellAbility sa : skipped) {
//System.out.println("Unskip: " + sa.toString() + " (" + sa.getHostCard().getName() + ").");
sa.setSkip(false);
}
}
CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
cards = ComputerUtilCard.dedupeCards(cards);
List<SpellAbility> saList = Lists.newArrayList();
@@ -1677,16 +1554,12 @@ public class AiController {
saList = ComputerUtilAbility.getSpellAbilities(cards, player);
}
saList.removeIf(spellAbility -> { //don't include removedAI cards if somehow the AI can play the ability or gain control of unsupported card
Iterables.removeIf(saList, spellAbility -> { //don't include removedAI cards if somehow the AI can play the ability or gain control of unsupported card
// TODO allow when experimental profile?
return spellAbility.isLandAbility() || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard()));
});
//removed skipped SA
skipped = saList.stream().filter(SpellAbility::isSkip).collect(Collectors.toList());
if (!skipped.isEmpty())
saList.removeAll(skipped);
//update LivingEndPlayer
useLivingEnd = IterableUtil.any(player.getZone(ZoneType.Library), CardPredicates.nameEquals("Living End"));
useLivingEnd = Iterables.any(player.getZone(ZoneType.Library), CardPredicates.nameEquals("Living End"));
SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true);
@@ -1709,9 +1582,6 @@ public class AiController {
String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all);
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
final ExecutorService executor = Executors.newSingleThreadExecutor();
Future<SpellAbility> future = executor.submit(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1722,50 +1592,41 @@ public class AiController {
if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& player.getZone(ZoneType.Hand).contains(
Predicate.not(CardPredicates.LANDS.or(CardPredicates.hasKeyword("Storm")))
)) {
&& player.getZone(ZoneType.Hand).contains(Predicates.not(Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.hasKeyword("Storm"))))) {
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
// skip evaluating Storm unless we reached the minimum Storm count
continue;
}
}
// living end AI decks
// TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed
AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa;
if (useLivingEnd) {
if (sa.isCycling() && sa.canCastTiming(player)
&& player.getCardsIn(ZoneType.Library).size() >= 10) {
if (sa.isCycling() && sa.canCastTiming(player) && player.getCardsIn(ZoneType.Library).size() >= 10) {
if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)
&& !player.cantLoseForZeroOrLessLife() && player.getLife() <= sa.getPayCosts()
.getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
&& !player.cantLoseForZeroOrLessLife()
&& player.getLife() <= sa.getPayCosts().getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
aiPlayDecision = AiPlayDecision.CantAfford;
} else {
aiPlayDecision = AiPlayDecision.WillPlay;
}
}
} else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) {
if (isLifeInDanger) { // needs more tune up for certain conditions
aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa
: AiPlayDecision.WillPlay;
} else if (CardLists
.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES)
.size() > 4) {
if (isLifeInDanger) { //needs more tune up for certain conditions
aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa : AiPlayDecision.WillPlay;
} else if (CardLists.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES).size() > 4) {
if (player.getCreaturesInPlay().size() >= 4) // it's good minimum
continue;
else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player)
&& ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
aiPlayDecision = AiPlayDecision.WillPlay;
// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) && ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
aiPlayDecision = AiPlayDecision.WillPlay;// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
} else {
continue;
}
}
}
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
SpellAbility root = sa.getRootAbility();
if (root.isSpell() || root.isTrigger() || root.isReplacementAbility()) {
@@ -1787,15 +1648,6 @@ public class AiController {
}
return null;
});
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
try {
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
future.cancel(true);
return null;
}
}
public CardCollection chooseCardsToDelve(int genericCost, CardCollection grave) {
@@ -1831,7 +1683,7 @@ public class AiController {
if (spell instanceof WrappedAbility)
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
if (spell.getApi() != null)
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
return SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory);
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
return true;
@@ -2071,7 +1923,7 @@ public class AiController {
}
// AI has decided to help. Now let's figure out how much they can help
int mana = getAvailableManaEstimate(player, true);
int mana = ComputerUtilMana.getAvailableManaEstimate(player, true);
// TODO We should make a logical guess here, but for now just uh yknow randomly decide?
// What do I want to play next? Can I still pay for that and have mana left over to help?
@@ -2133,7 +1985,7 @@ public class AiController {
break;
}
} else {
CardCollectionView viableOptions = CardLists.filter(pool, CardPredicates.isControlledByAnyOf(sa.getActivatingPlayer().getOpponents()), CardPredicates.CAN_BE_DESTROYED);
CardCollectionView viableOptions = CardLists.filter(pool, CardPredicates.isControlledByAnyOf(sa.getActivatingPlayer().getOpponents()), CardPredicates.Presets.CAN_BE_DESTROYED);
Card best = ComputerUtilCard.getBestAI(viableOptions);
if (best != null) {
result.add(best);
@@ -2209,7 +2061,7 @@ public class AiController {
CardLists.shuffle(library);
// remove all land, keep non-basicland in there, shuffled
CardCollection land = CardLists.filter(library, CardPredicates.LANDS);
CardCollection land = CardLists.filter(library, CardPredicates.Presets.LANDS);
for (Card c : land) {
if (c.isLand()) {
library.remove(c);
@@ -2259,7 +2111,7 @@ public class AiController {
}
}
if ("Aminatou".equals(sa.getParam("AILogic")) && game.getPlayers().size() > 2) {
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.NONLAND_PERMANENTS);
CardCollection left = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Left));
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Right));
return Aggregates.sum(left, Card::getCMC) > Aggregates.sum(right, Card::getCMC);
@@ -2315,6 +2167,8 @@ public class AiController {
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());
@@ -2330,48 +2184,48 @@ public class AiController {
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"))) {
if (playerHand.isEmpty() || Iterables.any(playerHand, CardPredicates.hasSVar("DiscardMe"))) {
discardEarly = true;
result.addAll(mandatoryDiscard);
mandatoryDiscard.clear();
}
// optional Discard, probably combined with Draw
result.addAll(discard);
// token should be added first so they might get the pump bonus
result.addAll(token);
result.addAll(pump);
result.addAll(pumpAll);
// do Evolve Trigger before other PutCounter SpellAbilities
// 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
result.addAll(putCounterAll);
// do putCounter before Draw/Discard because it can cause a Draw Trigger
result.addAll(putCounter);
// do Evolve Trigger before other PutCounter SpellAbilities
result.addAll(evolve);
// token should be added first so they might get the pump bonus
result.addAll(pumpAll);
result.addAll(pump);
result.addAll(token);
if (!discardEarly) {
result.addAll(mandatoryDiscard);
}
result.addAll(activePlayerSAs);
//need to reverse because of magic stack
Collections.reverse(result);
return result;
}
// TODO move to more common place
private static <T> List<T> filterList(List<T> input, Predicate<? super T> pred) {
List<T> filtered = input.stream().filter(pred).collect(Collectors.toList());
List<T> filtered = Lists.newArrayList(Iterables.filter(input, pred));
input.removeAll(filtered);
return filtered;
}
// 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 -> trb.ensureAbility() != null && pred.apply(trb.ensureAbility()) == value);
}
public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
return filterList(input, SpellAbilityPredicates.isApi(type));
}
@@ -2408,11 +2262,12 @@ public class AiController {
public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> list) {
// no need to choose anything
if (list.size() <= 1) {
return list.get(0);
return Iterables.getFirst(list, null);
}
ReplacementType mode = list.get(0).getMode();
ReplacementType mode = Iterables.getFirst(list, null).getMode();
// replace lifegain effects
if (mode.equals(ReplacementType.GainLife)) {
List<ReplacementEffect> noGain = filterListByAiLogic(list, "NoLife");
List<ReplacementEffect> loseLife = filterListByAiLogic(list, "LoseLife");
@@ -2421,16 +2276,16 @@ public class AiController {
if (!noGain.isEmpty()) {
// no lifegain is better than lose life
return noGain.get(0);
return Iterables.getFirst(noGain, null);
} else if (!loseLife.isEmpty()) {
// lose life before double life to prevent lose double
return loseLife.get(0);
return Iterables.getFirst(loseLife, null);
} else if (!lichDraw.isEmpty()) {
// lich draw before double life to prevent to draw to much
return lichDraw.get(0);
return Iterables.getFirst(lichDraw, null);
} else if (!doubleLife.isEmpty()) {
// other than that, do double life
return doubleLife.get(0);
return Iterables.getFirst(doubleLife, null);
}
} else if (mode.equals(ReplacementType.DamageDone)) {
List<ReplacementEffect> prevention = filterList(list, CardTraitPredicates.hasParam("Prevent"));
@@ -2438,45 +2293,40 @@ public class AiController {
// TODO when Protection is done as ReplacementEffect do them
// before normal prevention
if (!prevention.isEmpty()) {
return prevention.get(0);
return Iterables.getFirst(prevention, null);
}
} else if (mode.equals(ReplacementType.Destroy)) {
List<ReplacementEffect> shield = filterList(list, CardTraitPredicates.hasParam("ShieldCounter"));
List<ReplacementEffect> regeneration = filterList(list, CardTraitPredicates.hasParam("Regeneration"));
List<ReplacementEffect> umbraArmor = filterList(list, CardTraitPredicates.isKeyword(Keyword.UMBRA_ARMOR));
List<ReplacementEffect> umbraArmorIndestructible = filterList(umbraArmor, x -> x.getHostCard().hasKeyword(Keyword.INDESTRUCTIBLE));
List<ReplacementEffect> umbraArmorIndestructible = filterList(umbraArmor, Predicates.compose(CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE), CardTraitBase::getHostCard));
// Indestructible umbra armor is the best
if (!umbraArmorIndestructible.isEmpty()) {
return umbraArmorIndestructible.get(0);
return Iterables.getFirst(umbraArmorIndestructible, null);
}
// then it might be better to remove shield counter if able?
if (!shield.isEmpty()) {
return shield.get(0);
return Iterables.getFirst(shield, null);
}
// 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 regeneration.get(0);
return Iterables.getFirst(regeneration, null);
}
if (!umbraArmor.isEmpty()) {
// sort them by cmc
umbraArmor.sort(Comparator.comparing(CardTraitBase::getHostCard, Comparator.comparing(Card::getCMC)));
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);
return Iterables.getFirst(umbraArmor, null);
}
}
// TODO always lower counters with Vorinclex first, might turn it from 1 to 0 as final
return list.get(0);
return Iterables.getFirst(list, null);
}
}

View File

@@ -1,5 +1,6 @@
package forge.ai;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory.MemorySet;
@@ -46,14 +47,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostBehold cost) {
final String type = cost.getType();
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
return hand.isEmpty() ? null : PaymentDecision.card(getBestCreatureAI(hand));
}
@Override
public PaymentDecision visit(CostChooseColor cost) {
int c = cost.getAbilityAmount(ability);
@@ -64,7 +57,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostChooseCreatureType cost) {
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes());
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(),
Lists.newArrayList());
return PaymentDecision.type(choice);
}
@@ -117,13 +111,13 @@ public class AiCostDecision extends CostDecisionMakerBase {
Card chosen;
if (!discardMe.isEmpty()) {
chosen = Aggregates.random(discardMe);
discardMe = CardLists.filter(discardMe, CardPredicates.sharesNameWith(chosen).negate());
discardMe = CardLists.filter(discardMe, Predicates.not(CardPredicates.sharesNameWith(chosen)));
} else {
final Card worst = ComputerUtilCard.getWorstAI(hand);
chosen = worst != null ? worst : Aggregates.random(hand);
}
differentNames.add(chosen);
hand = CardLists.filter(hand, CardPredicates.sharesNameWith(chosen).negate());
hand = CardLists.filter(hand, Predicates.not(CardPredicates.sharesNameWith(chosen)));
c--;
}
return PaymentDecision.card(differentNames);

View File

@@ -17,18 +17,19 @@
*/
package forge.ai;
import forge.LobbyPlayer;
import forge.util.Aggregates;
import forge.util.FileUtil;
import forge.util.TextUtil;
import org.apache.commons.lang3.ArrayUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import forge.LobbyPlayer;
import forge.util.Aggregates;
import forge.util.FileUtil;
import forge.util.TextUtil;
/**
* Holds default AI personality profile values in an enum.
* Loads profile from the given text file when setProfile is called.

View File

@@ -17,7 +17,16 @@
*/
package forge.ai;
import java.util.*;
import com.google.common.collect.*;
import forge.game.card.*;
import forge.game.cost.*;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi;
@@ -26,15 +35,20 @@ import forge.card.CardType;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.game.*;
import forge.game.CardTraitPredicates;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.GameEntity;
import forge.game.GameEntityCounterTable;
import forge.game.GameObject;
import forge.game.GameType;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -48,7 +62,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
@@ -56,13 +69,8 @@ import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.StreamUtil;
import forge.util.TextUtil;
import forge.util.collect.FCollection;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.function.Predicate;
/**
@@ -79,8 +87,6 @@ public class ComputerUtil {
}
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game, Runnable chooseTargets) {
final Card source = sa.getHostCard();
final Card host = sa.getHostCard();
final Zone hz = host.isCopiedSpell() ? null : host.getZone();
source.setSplitStateToPlayAbility(sa);
if (sa.isSpell() && !source.isCopiedSpell()) {
@@ -121,6 +127,14 @@ public class ComputerUtil {
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);
@@ -129,15 +143,9 @@ public class ComputerUtil {
}
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() + "]");
sa.setSkip(true);
if (host != null && hz != null && hz.is(ZoneType.Stack)) {
Card c = game.getAction().moveTo(hz.getZoneType(), host, null, null);
for (SpellAbility csa : c.getSpellAbilities()) {
csa.setSkip(true);
}
}
//Should not arrive here
System.out.println("AI failed to play " + sa.getHostCard());
return false;
}
@@ -157,6 +165,7 @@ public class ComputerUtil {
}
public static int counterSpellRestriction(final Player ai, final SpellAbility sa) {
// Move this to AF?
// Restriction Level is Based off a handful of factors
int restrict = 0;
@@ -214,8 +223,9 @@ public class ComputerUtil {
return restrict;
}
// this is used for AI's counterspells
public static final boolean playStack(SpellAbility sa, final Player ai, final Game game) {
sa.setActivatingPlayer(ai);
sa.setActivatingPlayer(ai, true);
if (!ComputerUtilCost.canPayCost(sa, ai, false))
return false;
@@ -229,9 +239,10 @@ public class ComputerUtil {
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);
@@ -241,34 +252,94 @@ public class ComputerUtil {
return false;
}
if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa, false);
game.getStack().add(sa);
} else {
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().add(sa);
}
}
return true;
}
public static final void playSpellAbilityForFree(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
sa.setActivatingPlayer(ai, true);
final Card source = sa.getHostCard();
if (sa.isSpell() && !source.isCopiedSpell()) {
sa.setHostCard(game.getAction().moveToStack(source, sa));
}
game.getStack().add(sa);
}
public static final boolean playSpellAbilityWithoutPayingManaCost(final Player ai, final SpellAbility sa, final Game game) {
SpellAbility newSA = sa.copyWithNoManaCost();
newSA.setActivatingPlayer(ai, true);
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA, false) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) {
return false;
}
newSA = GameActionUtil.addExtraKeywordCost(newSA);
final Card source = newSA.getHostCard();
Zone fromZone = game.getZoneOf(source);
int zonePosition = 0;
if (fromZone != null) {
zonePosition = fromZone.getCards().indexOf(source);
}
if (newSA.isSpell() && !source.isCopiedSpell()) {
newSA.setHostCard(game.getAction().moveToStack(source, newSA));
if (newSA.getApi() == ApiType.Charm && !CharmEffect.makeChoices(newSA)) {
// 603.3c If no mode is chosen, the ability is removed from the stack.
return false;
}
}
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
// do this after card got added to stack
if (!newSA.checkRestrictions(ai)) {
GameActionUtil.rollbackAbility(newSA, fromZone, zonePosition, pay, source);
return false;
}
pay.payComputerCosts(new AiCostDecision(ai, newSA, false));
game.getStack().add(newSA);
return true;
}
public static final boolean playNoStack(final Player ai, SpellAbility sa, final Game game, final boolean effect) {
sa.setActivatingPlayer(ai);
sa.setActivatingPlayer(ai, true);
// TODO: We should really restrict what doesn't use the Stack
if (!ComputerUtilCost.canPayCost(sa, ai, effect)) {
return false;
}
final Card source = sa.getHostCard();
if (!effect && sa.isSpell() && !source.isCopiedSpell()) {
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();
if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa, effect);
} else {
final CostPayment pay = new CostPayment(cost, sa);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, effect))) {
AbilityUtils.resolve(sa);
return true;
pay.payComputerCosts(new AiCostDecision(ai, sa, effect));
}
return false;
AbilityUtils.resolve(sa);
return true;
}
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
@@ -459,7 +530,7 @@ public class ComputerUtil {
// Discard lands
final CardCollection landsInHand = CardLists.getType(typeList, "Land");
if (!landsInHand.isEmpty()) {
final int numLandsInPlay = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final int numLandsInPlay = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
if (numLandsInPlay >= highestCMC
@@ -570,7 +641,7 @@ public class ComputerUtil {
// if the source has "Casualty", don't sacrifice cards that may have granted the effect
// TODO: is there a surefire way to determine which card added Casualty?
if (source.hasKeyword(Keyword.CASUALTY)) {
typeList = CardLists.filter(typeList, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
typeList = CardLists.filter(typeList, Predicates.not(CardPredicates.hasSVar("AIDontSacToCasualty")));
}
if (typeList.size() < amount) {
@@ -604,13 +675,14 @@ public class ComputerUtil {
CardLists.sortByCmcDesc(typeList);
Collections.reverse(typeList);
// TODO AI needs some improvements here
// Whats the best way to choose evidence to collect?
// Probably want to filter out cards that have graveyard abilities/castable from graveyard
// Ideally we remove as few cards as possible "Don't overspend"
final CardCollection exileList = new CardCollection();
while (amount > 0) {
while(amount > 0) {
Card c = typeList.remove(0);
amount -= c.getCMC();
@@ -699,7 +771,7 @@ public class ComputerUtil {
all.removeAll(exclude);
CardCollection typeList = CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.CAN_TAP);
typeList = CardLists.filter(typeList, Presets.CAN_TAP);
if (tap) {
typeList.remove(activate);
@@ -730,7 +802,7 @@ public class ComputerUtil {
all.removeAll(exclude);
CardCollection typeList = CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, sa.isCrew() ? CardPredicates.CAN_CREW : CardPredicates.CAN_TAP);
typeList = CardLists.filter(typeList, sa.isCrew() ? Presets.CAN_CREW : Presets.CAN_TAP);
if (tap) {
typeList.remove(activate);
@@ -752,7 +824,7 @@ public class ComputerUtil {
tapList.clear();
}
tapList.add(next);
totalPower = CardLists.getTotalPower(tapList, sa);
totalPower = CardLists.getTotalPower(tapList, true, sa.isCrew());
if (totalPower >= amount) {
break;
}
@@ -767,7 +839,7 @@ public class ComputerUtil {
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
typeList = CardLists.filter(typeList, Presets.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
if (untap) {
typeList.remove(activate);
@@ -864,7 +936,7 @@ public class ComputerUtil {
// Run non-mandatory trigger.
// These checks only work if the Executing SpellAbility is an Ability_Sub.
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) {
// AI would not run this trigger if given the chance
return sacrificed;
}
@@ -960,7 +1032,7 @@ public class ComputerUtil {
c = ComputerUtilCard.getWorstCreatureAI(remaining);
}
else if (CardLists.getNotType(remaining, "Land").isEmpty()) {
c = ComputerUtilCard.getWorstLand(CardLists.filter(remaining, CardPredicates.LANDS));
c = ComputerUtilCard.getWorstLand(CardLists.filter(remaining, CardPredicates.Presets.LANDS));
}
else {
c = ComputerUtilCard.getWorstPermanentAI(remaining, false, false, false, false);
@@ -996,7 +1068,7 @@ public class ComputerUtil {
if (!sa.isActivatedAbility() || sa.getApi() != ApiType.Regenerate) {
continue; // Not a Regenerate ability
}
sa.setActivatingPlayer(controller);
sa.setActivatingPlayer(controller, true);
if (!(sa.canPlay() && ComputerUtilCost.canPayCost(sa, controller, false))) {
continue; // Can't play ability
}
@@ -1100,11 +1172,6 @@ public class ComputerUtil {
}
}
// if AI has no speed, play start your engines on Main1
if (ai.noSpeed() && cardState.hasKeyword(Keyword.START_YOUR_ENGINES)) {
return true;
}
// cast Blitz in main 1 if the creature attacks
if (sa.isBlitz() && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, card)) {
return true;
@@ -1274,8 +1341,8 @@ public class ComputerUtil {
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
@@ -1414,7 +1481,9 @@ public class ComputerUtil {
}
}
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice sac) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
final String type = sac.getType();
if (type.equals("CARDNAME")) {
@@ -1459,14 +1528,15 @@ public class ComputerUtil {
// check for Continuous abilities that grant Haste
for (final Card c : all) {
for (StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("AddKeyword").contains("Haste")) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
if (c.isEquipment() && c.getEquipping() == null) {
return true;
}
final String affected = stAb.getParam("Affected");
final String affected = params.get("Affected");
if (affected.contains("Creature.YouCtrl")
|| affected.contains("Other+YouCtrl")) {
return true;
@@ -1519,10 +1589,11 @@ public class ComputerUtil {
for (final Card c : opp) {
for (StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("AddKeyword").contains("Haste")) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
final ArrayList<String> affected = Lists.newArrayList(stAb.getParam("Affected").split(","));
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(","));
if (affected.contains("Creature")) {
return true;
}
@@ -1590,7 +1661,7 @@ public class ComputerUtil {
int damage = 0;
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
all.addAll(ai.getCardsActivatableInExternalZones(true));
all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.PERMANENTS.negate()));
all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(Presets.PERMANENTS)));
for (final Card c : all) {
if (c.getZone().getPlayer() != null && c.getZone().getPlayer() != ai && c.mayPlay(ai).isEmpty()) {
@@ -1600,7 +1671,7 @@ public class ComputerUtil {
if (sa.getApi() != ApiType.DealDamage) {
continue;
}
sa.setActivatingPlayer(ai);
sa.setActivatingPlayer(ai, true);
final String numDam = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), numDam, sa);
if (dmg <= damage) {
@@ -1680,7 +1751,7 @@ public class ComputerUtil {
sub = sub.getSubAbility();
}
if (sa == null || (sa != spell && sa != sub)) {
predictThreatenedObjects(ai, sa, spell).forEach(objects::add);
Iterables.addAll(objects, predictThreatenedObjects(ai, sa, spell));
}
if (top) {
break; // only evaluate top-stack
@@ -1778,7 +1849,9 @@ public class ComputerUtil {
noRegen = true;
}
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
@@ -1842,7 +1915,9 @@ public class ComputerUtil {
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
threatened.add(c);
}
} else if (o instanceof Player p) {
} else if (o instanceof Player) {
final Player p = (Player) o;
if (source.hasKeyword(Keyword.INFECT)) {
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
threatened.add(p);
@@ -1860,7 +1935,8 @@ public class ComputerUtil {
|| saviourApi == null)) {
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
final boolean canRemove = (c.getNetToughness() <= dmg)
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
if (!canRemove) {
@@ -1906,7 +1982,9 @@ public class ComputerUtil {
|| saviourApi == ApiType.Protection || saviourApi == null
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1955,7 +2033,8 @@ public class ComputerUtil {
&& topStack.hasParam("Destination")
&& topStack.getParam("Destination").equals("Exile")) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -1982,7 +2061,8 @@ public class ComputerUtil {
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2004,7 +2084,8 @@ public class ComputerUtil {
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
if (enableCurseAuraRemoval) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2020,7 +2101,7 @@ public class ComputerUtil {
}
}
predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility()).forEach(threatened::add);
Iterables.addAll(threatened, predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility()));
return threatened;
}
@@ -2134,7 +2215,7 @@ public class ComputerUtil {
}
CardCollectionView library = ai.getCardsIn(ZoneType.Library);
int landsInDeck = CardLists.count(library, CardPredicates.LANDS);
int landsInDeck = CardLists.count(library, CardPredicates.isType("Land"));
// no land deck, can't do anything better
if (landsInDeck == 0) {
@@ -2279,14 +2360,14 @@ public class ComputerUtil {
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.LANDS_PRODUCING_MANA);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
CardCollection thisLandOTB = CardLists.filter(cardsOTB, CardPredicates.nameEquals(c.getName()));
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.LANDS_PRODUCING_MANA);
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
// valuable mana-producing artifacts that may be equated to a land
List<String> manaArts = Arrays.asList("Mox Pearl", "Mox Sapphire", "Mox Jet", "Mox Ruby", "Mox Emerald");
// evaluate creatures available in deck
CardCollectionView allCreatures = CardLists.filter(allCards, CardPredicates.CREATURES, CardPredicates.isOwner(player));
CardCollectionView allCreatures = CardLists.filter(allCards, CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player));
int numCards = allCreatures.size();
if (landsOTB.size() < maxLandsToScryLandsToTop && landsInHand.isEmpty()) {
@@ -2315,7 +2396,7 @@ public class ComputerUtil {
}
}
} else if (c.isCreature()) {
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.CREATURES);
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES);
int avgCreatureValue = numCards != 0 ? ComputerUtilCard.evaluateCreatureList(allCreatures) / numCards : 0;
int maxControlledCMC = Aggregates.max(creaturesOTB, Card::getCMC);
@@ -2366,10 +2447,7 @@ public class ComputerUtil {
// not enough good choices, need to fill the rest
int minDiff = min - goodChoices.size();
if (minDiff > 0) {
List<Card> choices = validCards.stream()
.filter(Predicate.not(goodChoices::contains))
.collect(StreamUtil.random(minDiff));
goodChoices.addAll(choices);
goodChoices.addAll(Aggregates.random(CardLists.filter(validCards, Predicates.not(Predicates.in(goodChoices))), minDiff));
return goodChoices;
}
@@ -2389,9 +2467,12 @@ public class ComputerUtil {
return getCardsToDiscardFromOpponent(aiChooser, p, sa, validCards, min, max);
}
public static String chooseSomeType(Player ai, String kindOfType, SpellAbility sa, Collection<String> validTypes) {
public static String chooseSomeType(Player ai, String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes) {
final String logic = sa.getParam("AILogic");
if (invalidTypes == null) {
invalidTypes = ImmutableList.of();
}
if (validTypes == null) {
validTypes = ImmutableList.of();
}
@@ -2404,14 +2485,16 @@ public class ComputerUtil {
// otherwise, lib search for most common type left then, reveal chosenType to Human
if (game.getPhaseHandler().is(PhaseType.UNTAP) && logic == null) { // Storage Matrix
double amount = 0;
for (String type : validTypes) {
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType(type), CardPredicates.TAPPED);
for (String type : CardType.getAllCardTypes()) {
if (!invalidTypes.contains(type)) {
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType(type), Presets.TAPPED);
double i = type.equals("Creature") ? list.size() * 1.5 : list.size();
if (i > amount) {
amount = i;
chosen = type;
}
}
}
} else if ("ProtectionFromType".equals(logic)) {
// TODO: protection vs. damage-dealing and milling instants/sorceries in low creature decks and the like?
// Maybe non-creature artifacts in certain cases?
@@ -2424,11 +2507,12 @@ public class ComputerUtil {
if (StringUtils.isEmpty(chosen)) {
chosen = "Creature"; // if in doubt, choose Creature, I guess
}
} else {
}
else {
// Are we picking a type to reduce costs for that type?
boolean reducingCost = false;
for (StaticAbility s : sa.getHostCard().getStaticAbilities()) {
if (s.checkMode(StaticAbilityMode.ReduceCost) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
if ("ReduceCost".equals(s.getParam("Mode")) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
reducingCost = true;
break;
}
@@ -2436,6 +2520,7 @@ public class ComputerUtil {
if (reducingCost) {
List<String> valid = Lists.newArrayList(validTypes);
valid.removeAll(invalidTypes);
valid.remove("Land"); // Lands don't have costs to reduce
chosen = ComputerUtilCard.getMostProminentCardType(ai.getAllCards(), valid);
}
@@ -2445,42 +2530,50 @@ public class ComputerUtil {
}
} else if (kindOfType.equals("Creature")) {
if (logic != null) {
List <String> valid = Lists.newArrayList(CardType.getAllCreatureTypes());
valid.removeAll(invalidTypes);
if (logic.equals("MostProminentOnBattlefield")) {
chosen = ComputerUtilCard.getMostProminentType(game.getCardsIn(ZoneType.Battlefield), validTypes);
} else if (logic.equals("MostProminentComputerControls")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Battlefield), validTypes);
} else if (logic.equals("MostProminentComputerControlsOrOwns")) {
chosen = ComputerUtilCard.getMostProminentType(game.getCardsIn(ZoneType.Battlefield), valid);
}
else if (logic.equals("MostProminentComputerControls")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Battlefield), valid);
}
else if (logic.equals("MostProminentComputerControlsOrOwns")) {
CardCollectionView list = ai.getCardsIn(Arrays.asList(ZoneType.Battlefield, ZoneType.Hand));
if (list.isEmpty()) {
list = ai.getCardsIn(Arrays.asList(ZoneType.Library));
}
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
} else if (logic.equals("MostProminentOppControls")) {
chosen = ComputerUtilCard.getMostProminentType(list, valid);
}
else if (logic.equals("MostProminentOppControls")) {
CardCollection list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
if (!CardType.isACreatureType(chosen)) {
chosen = ComputerUtilCard.getMostProminentType(list, valid);
if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
list = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
chosen = ComputerUtilCard.getMostProminentType(list, valid);
}
} else if (logic.startsWith("MostProminentInComputerDeck")) {
}
else if (logic.startsWith("MostProminentInComputerDeck")) {
boolean includeTokens = !logic.endsWith("NonToken");
chosen = ComputerUtilCard.getMostProminentType(ai.getAllCards(), validTypes, includeTokens);
} else if (logic.equals("MostProminentInComputerGraveyard")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Graveyard), validTypes);
chosen = ComputerUtilCard.getMostProminentType(ai.getAllCards(), valid, includeTokens);
}
else if (logic.equals("MostProminentInComputerGraveyard")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Graveyard), valid);
}
}
if (!CardType.isACreatureType(chosen)) {
chosen = validTypes.size() == 1 ? (String) validTypes.toArray()[0] :
ComputerUtilCard.getMostProminentType(ai.getAllCards(), validTypes, false);
//chosen = "Sliver";
if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
chosen = "Sliver";
}
} else if (kindOfType.equals("Basic Land")) {
if (logic != null) {
if (logic.equals("MostProminentOppControls")) {
CardCollection list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
List<String> valid = Lists.newArrayList(CardType.getBasicTypes());
valid.removeAll(invalidTypes);
chosen = ComputerUtilCard.getMostProminentType(list, valid);
} else if (logic.equals("MostNeededType")) {
// Choose a type that is in the deck, but not in hand or on the battlefield
final List<String> basics = new ArrayList<>(CardType.Constant.BASIC_TYPES);
@@ -2488,13 +2581,13 @@ public class ComputerUtil {
CardCollectionView possibleCards = ai.getAllCards();
for (String b : basics) {
if (!presentCards.anyMatch(CardPredicates.isType(b)) && possibleCards.anyMatch(CardPredicates.isType(b))) {
if (!Iterables.any(presentCards, CardPredicates.isType(b)) && Iterables.any(possibleCards, CardPredicates.isType(b))) {
chosen = b;
}
}
if (chosen.isEmpty()) {
for (String b : basics) {
if (possibleCards.anyMatch(CardPredicates.isType(b))) {
if (Iterables.any(possibleCards, CardPredicates.isType(b))) {
chosen = b;
}
}
@@ -2503,7 +2596,7 @@ public class ComputerUtil {
else if (logic.equals("ChosenLandwalk")) {
for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
for (String t : c.getType()) {
if (CardType.isABasicLandType(t)) {
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
chosen = t;
break;
}
@@ -2512,7 +2605,7 @@ public class ComputerUtil {
}
}
if (!CardType.isABasicLandType(chosen) || !validTypes.contains(chosen)) {
if (!CardType.isABasicLandType(chosen) || invalidTypes.contains(chosen)) {
chosen = "Island";
}
}
@@ -2521,7 +2614,7 @@ public class ComputerUtil {
if (logic.equals("ChosenLandwalk")) {
for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
for (String t : c.getType().getLandTypes()) {
if (validTypes.contains(t)) {
if (!invalidTypes.contains(t)) {
chosen = t;
break;
}
@@ -2705,7 +2798,8 @@ public class ComputerUtil {
}
// has cards with SacMe or Token
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
if (CardLists.count(aiCreatures,
Predicates.or(CardPredicates.hasSVar("SacMe"), CardPredicates.Presets.TOKEN)) >= numDeath) {
return "Death";
}
@@ -2788,13 +2882,17 @@ public class ComputerUtil {
if (!trigger.requirementsCheck(game)) {
continue;
}
if (!trigger.matchesValidParam("ValidCard", card)) {
if (trigger.hasParam("ValidCard")) {
if (!card.isValid(trigger.getParam("ValidCard").split(","), source.getController(), source, sa)) {
continue;
}
if (!trigger.matchesValidParam("ValidActivatingPlayer", player)) {
}
if (trigger.hasParam("ValidActivatingPlayer")) {
if (!player.isValid(trigger.getParam("ValidActivatingPlayer"), source.getController(), source, sa)) {
continue;
}
}
// fall back for OverridingAbility
SpellAbility trigSa = trigger.ensureAbility();
@@ -2851,9 +2949,11 @@ public class ComputerUtil {
&& AbilityUtils.getDefinedCards(permanent, source.getSVar(trigger.getParam("CheckOnTriggeredCard").split(" ")[0]), null).isEmpty()) {
continue;
}
if (!trigger.matchesValidParam("ValidCard", permanent)) {
if (trigger.hasParam("ValidCard")) {
if (!permanent.isValid(trigger.getParam("ValidCard"), source.getController(), source, null)) {
continue;
}
}
// fall back for OverridingAbility
SpellAbility trigSa = trigger.ensureAbility();
if (trigSa == null) {
@@ -2890,7 +2990,7 @@ public class ComputerUtil {
// Iceberg does use Ice as Storage
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
// some lands does use Depletion as Storage Counter
|| (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step."))
// treat Time Counters on suspended Cards as Bad,
// and also on Chronozoa
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
@@ -2982,11 +3082,11 @@ public class ComputerUtil {
repParams,
ReplacementLayer.Other);
if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
return false;
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
return false;
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
return false;
}
return true;
@@ -3011,13 +3111,13 @@ public class ComputerUtil {
ReplacementLayer.Other
);
if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
// no life gain is not negative
return false;
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
// lose life is only negative is the player can lose life
return player.canLoseLife();
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
// if it would draw more cards than player has, then its negative
return player.getCardsIn(ZoneType.Library).size() <= n;
}
@@ -3048,7 +3148,7 @@ public class ComputerUtil {
}
SpellAbility abTest = withoutPayingManaCost ? ab.copyWithNoManaCost() : ab.copy();
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
abTest.setActivatingPlayer(ai);
abTest.setActivatingPlayer(ai, true);
abTest.getRestrictions().setZone(c.getZone().getZoneType());
if (AiPlayDecision.WillPlay == aic.canPlaySa(abTest) && ComputerUtilCost.canPayCost(abTest, ai, false)) {
targets.add(c);

View File

@@ -21,7 +21,6 @@ import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
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.spellability.SpellAbilityStackInstance;
@@ -90,7 +89,7 @@ public class ComputerUtilAbility {
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
for (SpellAbility sa : originList) {
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
}
@@ -117,16 +116,12 @@ public class ComputerUtilAbility {
final List<SpellAbility> result = Lists.newArrayList();
for (SpellAbility sa : newAbilities) {
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
// Optional cost selection through the AI controller
boolean choseOptCost = false;
List<OptionalCostValue> list = GameActionUtil.getOptionalCostValues(sa);
if (!list.isEmpty()) {
// still add base spell in case of Promise Gift
if (list.stream().anyMatch(ocv -> ocv.getType().equals(OptionalCost.PromiseGift))) {
result.add(sa);
}
list = player.getController().chooseOptionalCosts(sa, list);
if (!list.isEmpty()) {
choseOptCost = true;

View File

@@ -2,24 +2,21 @@ package forge.ai;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import forge.StaticData;
import com.google.common.base.Function;
import forge.ai.simulation.GameStateEvaluator;
import forge.card.mana.ManaCost;
import forge.game.card.*;
import forge.util.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.card.CardRules;
import forge.card.CardStateName;
import forge.card.CardType;
import forge.card.ColorSet;
@@ -48,11 +45,14 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.TextUtil;
public class ComputerUtilCard {
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
@@ -86,11 +86,12 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object.
*/
public static Card getBestArtifactAI(final List<Card> list) {
List<Card> all = CardLists.filter(list, CardPredicates.Presets.ARTIFACTS);
if (all.size() == 0) {
return null;
}
// get biggest Artifact
return list.stream()
.filter(CardPredicates.ARTIFACTS)
.max(Comparator.comparing(Card::getCMC))
.orElse(null);
return Aggregates.itemWithMax(all, Card::getCMC);
}
/**
@@ -100,11 +101,12 @@ public class ComputerUtilCard {
* @return best Planeswalker
*/
public static Card getBestPlaneswalkerAI(final List<Card> list) {
List<Card> all = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS);
if (all.isEmpty()) {
return null;
}
// no AI logic, just return most expensive
return list.stream()
.filter(CardPredicates.PLANESWALKERS)
.max(Comparator.comparing(Card::getCMC))
.orElse(null);
return Aggregates.itemWithMax(all, Card::getCMC);
}
/**
@@ -114,11 +116,12 @@ public class ComputerUtilCard {
* @return best Planeswalker
*/
public static Card getWorstPlaneswalkerAI(final List<Card> list) {
List<Card> all = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS);
if (all.isEmpty()) {
return null;
}
// no AI logic, just return least expensive
return list.stream()
.filter(CardPredicates.PLANESWALKERS)
.min(Comparator.comparing(Card::getCMC))
.orElse(null);
return Aggregates.itemWithMin(all, Card::getCMC);
}
public static Card getBestPlaneswalkerToDamage(final List<Card> pws) {
@@ -184,13 +187,13 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object.
*/
public static Card getBestEnchantmentAI(final List<Card> list, final SpellAbility spell, final boolean targeted) {
Stream<Card> cardStream = list.stream().filter(CardPredicates.ENCHANTMENTS);
List<Card> all = CardLists.filter(list, CardPredicates.Presets.ENCHANTMENTS);
if (targeted) {
cardStream = cardStream.filter(c -> c.canBeTargetedBy(spell));
all = CardLists.filter(all, c -> c.canBeTargetedBy(spell));
}
// get biggest Enchantment
return cardStream.max(Comparator.comparing(Card::getCMC)).orElse(null);
return Aggregates.itemWithMax(all, Card::getCMC);
}
/**
@@ -202,30 +205,30 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object.
*/
public static Card getBestLandAI(final Iterable<Card> list) {
final List<Card> land = CardLists.filter(list, CardPredicates.LANDS);
final List<Card> land = CardLists.filter(list, CardPredicates.Presets.LANDS);
if (land.isEmpty()) {
return null;
}
// prefer to target non basic lands
final List<Card> nbLand = CardLists.filter(land, CardPredicates.NONBASIC_LANDS);
final List<Card> nbLand = CardLists.filter(land, Predicates.not(CardPredicates.Presets.BASIC_LANDS));
if (!nbLand.isEmpty()) {
// TODO - Improve ranking various non-basic lands depending on context
// Urza's Mine/Tower/Power Plant
final CardCollectionView aiAvailable = nbLand.get(0).getController().getCardsIn(Arrays.asList(ZoneType.Battlefield, ZoneType.Hand));
if (IterableUtil.any(list, CardPredicates.nameEquals("Urza's Mine"))) {
if (Iterables.any(list, CardPredicates.nameEquals("Urza's Mine"))) {
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Mine")).isEmpty()) {
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Mine")).getFirst();
}
}
if (IterableUtil.any(list, CardPredicates.nameEquals("Urza's Tower"))) {
if (Iterables.any(list, CardPredicates.nameEquals("Urza's Tower"))) {
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Tower")).isEmpty()) {
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Tower")).getFirst();
}
}
if (IterableUtil.any(list, CardPredicates.nameEquals("Urza's Power Plant"))) {
if (Iterables.any(list, CardPredicates.nameEquals("Urza's Power Plant"))) {
if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Power Plant")).isEmpty()) {
return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Power Plant")).getFirst();
}
@@ -247,16 +250,17 @@ public class ComputerUtilCard {
}
if (iminBL == Integer.MAX_VALUE) {
// All basic lands have no basic land type. Just return something
return land.stream().filter(CardPredicates.UNTAPPED).findFirst().orElse(land.get(0));
return Iterables.find(land, CardPredicates.Presets.UNTAPPED, land.get(0));
}
final List<Card> bLand = CardLists.getType(land, sminBL);
return bLand.stream()
.filter(CardPredicates.UNTAPPED)
.findFirst()
for (Card ut : Iterables.filter(bLand, CardPredicates.Presets.UNTAPPED)) {
return ut;
}
// TODO potentially risky if simulation mode currently able to reach this from triggers
.orElseGet(() -> Aggregates.random(bLand)); // random tapped land of least represented type
return Aggregates.random(bLand); // random tapped land of least represented type
}
/**
@@ -358,10 +362,10 @@ public class ComputerUtilCard {
*/
public static Card getBestAI(final Iterable<Card> list) {
// Get Best will filter by appropriate getBest list if ALL of the list is of that type
if (IterableUtil.all(list, CardPredicates.CREATURES)) {
if (Iterables.all(list, CardPredicates.Presets.CREATURES)) {
return getBestCreatureAI(list);
}
if (IterableUtil.all(list, CardPredicates.LANDS)) {
if (Iterables.all(list, CardPredicates.Presets.LANDS)) {
return getBestLandAI(list);
}
// TODO - Once we get an EvaluatePermanent this should call getBestPermanent()
@@ -378,7 +382,7 @@ public class ComputerUtilCard {
if (Iterables.size(list) == 1) {
return Iterables.get(list, 0);
}
return Aggregates.itemWithMax(IterableUtil.filter(list, CardPredicates.CREATURES), ComputerUtilCard.creatureEvaluator);
return Aggregates.itemWithMax(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator);
}
/**
@@ -391,7 +395,7 @@ public class ComputerUtilCard {
if (Iterables.size(list) == 1) {
return Iterables.get(list, 0);
}
return Aggregates.itemWithMax(IterableUtil.filter(list, Card::hasPlayableLandFace), ComputerUtilCard.landEvaluator);
return Aggregates.itemWithMax(Iterables.filter(list, Card::hasPlayableLandFace), ComputerUtilCard.landEvaluator);
}
/**
@@ -406,7 +410,7 @@ public class ComputerUtilCard {
if (Iterables.size(list) == 1) {
return Iterables.get(list, 0);
}
return Aggregates.itemWithMin(IterableUtil.filter(list, CardPredicates.CREATURES), ComputerUtilCard.creatureEvaluator);
return Aggregates.itemWithMin(Iterables.filter(list, CardPredicates.Presets.CREATURES), ComputerUtilCard.creatureEvaluator);
}
// This selection rates tokens higher
@@ -427,7 +431,7 @@ public class ComputerUtilCard {
Card biggest = null;
int biggestvalue = -1;
for (Card card : CardLists.filter(list, CardPredicates.CREATURES)) {
for (Card card : CardLists.filter(list, CardPredicates.Presets.CREATURES)) {
int newvalue = evaluateCreature(card);
newvalue += card.isToken() ? tokenBonus : 0; // raise the value of tokens
@@ -480,40 +484,40 @@ public class ComputerUtilCard {
return null;
}
final boolean hasEnchantmants = IterableUtil.any(list, CardPredicates.ENCHANTMENTS);
final boolean hasEnchantmants = Iterables.any(list, CardPredicates.Presets.ENCHANTMENTS);
if (biasEnch && hasEnchantmants) {
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.ENCHANTMENTS), null, false);
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.Presets.ENCHANTMENTS), null, false);
}
final boolean hasArtifacts = IterableUtil.any(list, CardPredicates.ARTIFACTS);
final boolean hasArtifacts = Iterables.any(list, CardPredicates.Presets.ARTIFACTS);
if (biasArt && hasArtifacts) {
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.ARTIFACTS), null, false);
return getCheapestPermanentAI(CardLists.filter(list, CardPredicates.Presets.ARTIFACTS), null, false);
}
if (biasLand && IterableUtil.any(list, CardPredicates.LANDS)) {
return getWorstLand(CardLists.filter(list, CardPredicates.LANDS));
if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) {
return getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
}
final boolean hasCreatures = IterableUtil.any(list, CardPredicates.CREATURES);
final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES);
if (biasCreature && hasCreatures) {
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.CREATURES));
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
}
List<Card> lands = CardLists.filter(list, CardPredicates.LANDS);
List<Card> lands = CardLists.filter(list, CardPredicates.Presets.LANDS);
if (lands.size() > 6) {
return getWorstLand(lands);
}
if (hasEnchantmants || hasArtifacts) {
final List<Card> ae = CardLists.filter(list,
(CardPredicates.ARTIFACTS.or(CardPredicates.ENCHANTMENTS))
.and(card -> !card.hasSVar("DoNotDiscardIfAble"))
);
final List<Card> ae = CardLists.filter(list, Predicates.and(
Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS),
card -> !card.hasSVar("DoNotDiscardIfAble")
));
return getCheapestPermanentAI(ae, null, false);
}
if (hasCreatures) {
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.CREATURES));
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
}
// Planeswalkers fall through to here, lands will fall through if there aren't very many
@@ -522,7 +526,8 @@ public class ComputerUtilCard {
public static final Card getCheapestSpellAI(final Iterable<Card> list) {
if (!Iterables.isEmpty(list)) {
CardCollection cc = CardLists.filter(list, CardPredicates.INSTANTS_AND_SORCERIES);
CardCollection cc = CardLists.filter(list,
Predicates.or(CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
if (cc.isEmpty()) {
return null;
@@ -692,8 +697,6 @@ public class ComputerUtilCard {
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
Combat combat = new Combat(ai);
// avoid removing original attacker
attacker.setCombatLKI(null);
combat.addAttacker(attacker, ai);
final List<Card> attackers = Lists.newArrayList(attacker);
aiBlk.assignBlockersGivenAttackers(combat, attackers);
@@ -711,7 +714,7 @@ public class ComputerUtilCard {
if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) {
continue;
}
sa.setActivatingPlayer(opp);
sa.setActivatingPlayer(opp, true);
if (sa.canTarget(card)) {
continue;
}
@@ -788,8 +791,9 @@ public class ComputerUtilCard {
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid) {
return getMostProminentType(list, valid, true);
}
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid, boolean includeTokens) {
if (list.isEmpty()) {
if (list.size() == 0) {
return "";
}
@@ -830,35 +834,51 @@ public class ComputerUtilCard {
//also take into account abilities that generate tokens
if (includeTokens) {
if (c.getRules() != null) {
for (String token : c.getRules().getTokens()) {
CardRules tokenCR = StaticData.instance().getAllTokens().getToken(token).getRules();
if (tokenCR == null)
for (SpellAbility sa : c.getAllSpellAbilities()) {
if (sa.getApi() != ApiType.Token) {
continue;
for (String type : tokenCR.getType().getCreatureTypes()) {
Integer count = typesInDeck.getOrDefault(type, 0);
typesInDeck.put(type, count + 1);
}
if (sa.hasParam("TokenTypes")) {
for (String var : sa.getParam("TokenTypes").split(",")) {
if (!CardType.isACreatureType(var)) {
continue;
}
Integer count = typesInDeck.getOrDefault(var, 0);
typesInDeck.put(var, count + weight);
}
}
}
// same for Trigger that does make Tokens
for (Trigger t : c.getTriggers()) {
SpellAbility sa = t.ensureAbility();
if (sa != null) {
if (sa.getApi() != ApiType.Token || !sa.hasParam("TokenTypes")) {
continue;
}
for (String var : sa.getParam("TokenTypes").split(",")) {
if (!CardType.isACreatureType(var)) {
continue;
}
Integer count = typesInDeck.getOrDefault(var, 0);
typesInDeck.put(var, count + weight);
}
}
}
// special rule for Fabricate and Servo
if (c.hasKeyword(Keyword.FABRICATE)) {
Integer count = typesInDeck.getOrDefault("Servo", 0);
typesInDeck.put("Servo", count + weight);
}
}
}
} // for
int max = 0;
String maxType = "";
// Iterate through typesInDeck and consider only valid types
for (final Entry<String, Integer> entry : typesInDeck.entrySet()) {
final String type = entry.getKey();
// consider the types that are in the valid list
if ((valid.isEmpty() || valid.contains(type)) && max < entry.getValue()) {
if (max < entry.getValue()) {
max = entry.getValue();
maxType = type;
}
@@ -987,7 +1007,7 @@ public class ComputerUtilCard {
} else if (logic.equals("MostProminentHumanCreatures")) {
CardCollectionView list = opp.getCreaturesInPlay();
if (list.isEmpty()) {
list = CardLists.filter(CardLists.filterControlledBy(game.getCardsInGame(), opp), CardPredicates.CREATURES);
list = CardLists.filter(CardLists.filterControlledBy(game.getCardsInGame(), opp), CardPredicates.Presets.CREATURES);
}
chosen.add(getMostProminentColor(list, colorChoices));
} else if (logic.equals("MostProminentComputerControls")) {
@@ -1042,7 +1062,7 @@ public class ComputerUtilCard {
String devotionCode = "Count$Devotion." + MagicColor.toLongString(c);
int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa);
if (devotion > curDevotion && hand.anyMatch(CardPredicates.isColor(c))) {
if (devotion > curDevotion && Iterables.any(hand, CardPredicates.isColor(c))) {
curDevotion = devotion;
chosenColor = MagicColor.toLongString(c);
}
@@ -1214,7 +1234,8 @@ public class ComputerUtilCard {
// if this thing is both owned and controlled by an opponent and it has a continuous ability,
// assume it either benefits the player or disrupts the opponent
for (final StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.isIntrinsic()) {
final Map<String, String> params = stAb.getMapParams();
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
priority = true;
break;
}
@@ -1245,16 +1266,17 @@ public class ComputerUtilCard {
}
} else {
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
//continuous buffs
if (stAb.checkMode(StaticAbilityMode.Continuous) && "Creature.YouCtrl".equals(stAb.getParam("Affected"))) {
if (params.get("Mode").equals("Continuous") && "Creature.YouCtrl".equals(params.get("Affected"))) {
int bonusPT = 0;
if (stAb.hasParam("AddPower")) {
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
if (params.containsKey("AddPower")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
}
if (stAb.hasParam("AddToughness")) {
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
if (params.containsKey("AddToughness")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
}
String kws = stAb.getParam("AddKeyword");
String kws = params.get("AddKeyword");
if (kws != null) {
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
}
@@ -1405,7 +1427,7 @@ public class ComputerUtilCard {
//1. become attacker for whatever reason
if (!doesCreatureAttackAI(ai, c) && doesSpecifiedCreatureAttackAI(ai, pumped)) {
float threat = 1.0f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
if (oppCreatures.stream().noneMatch(CardPredicates.possibleBlockers(pumped))) {
if (!Iterables.any(oppCreatures, CardPredicates.possibleBlockers(pumped))) {
threat *= 2;
}
if (c.getNetPower() == 0 && c == sa.getHostCard() && power > 0) {
@@ -1457,8 +1479,8 @@ public class ComputerUtilCard {
}
//3. grant evasive
if (oppCreatures.stream().anyMatch(CardPredicates.possibleBlockers(c))) {
if (oppCreatures.stream().noneMatch(CardPredicates.possibleBlockers(pumped))
if (Iterables.any(oppCreatures, CardPredicates.possibleBlockers(c))) {
if (!Iterables.any(oppCreatures, CardPredicates.possibleBlockers(pumped))
&& doesSpecifiedCreatureAttackAI(ai, pumped)) {
chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
}
@@ -1785,7 +1807,7 @@ public class ComputerUtilCard {
// remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
if (!stAb.checkMode("Continuous")) {
continue;
}
if (!stAb.hasParam("Affected")) {
@@ -1863,7 +1885,7 @@ public class ComputerUtilCard {
if (!c.isCreature()) {
return false;
}
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
return true;
}
return false;
@@ -1927,7 +1949,7 @@ public class ComputerUtilCard {
CardCollection aiCreats = ai.getCreaturesInPlay();
if (temporary) {
// Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped.
oppCards = CardLists.filter(oppCards, CardPredicates.UNTAPPED);
oppCards = CardLists.filter(oppCards, CardPredicates.Presets.UNTAPPED);
}
CardCollection priorityCards = new CardCollection();
@@ -2080,7 +2102,6 @@ public class ComputerUtilCard {
return false;
}
// use this function to skip expensive calculations on identical cards
public static CardCollection dedupeCards(CardCollection cc) {
if (cc.size() <= 1) {
return cc;
@@ -2088,7 +2109,7 @@ public class ComputerUtilCard {
CardCollection deduped = new CardCollection();
for (Card c : cc) {
boolean unique = true;
if (c.isInZone(ZoneType.Hand) && !c.hasPerpetual()) {
if (c.isInZone(ZoneType.Hand)) {
for (Card d : deduped) {
if (d.isInZone(ZoneType.Hand) && d.getOwner().equals(c.getOwner()) && d.getName().equals(c.getName())) {
unique = false;

View File

@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
@@ -39,12 +39,10 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.IterableUtil;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollection;
@@ -80,7 +78,7 @@ public class ComputerUtilCombat {
*/
public static boolean canAttackNextTurn(final Card attacker) {
final Iterable<GameEntity> defenders = CombatUtil.getAllPossibleDefenders(attacker.getController());
return IterableUtil.any(defenders, input -> canAttackNextTurn(attacker, input));
return Iterables.any(defenders, input -> canAttackNextTurn(attacker, input));
}
/**
@@ -102,7 +100,7 @@ public class ComputerUtilCombat {
return false;
}
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
return false;
}
@@ -119,7 +117,7 @@ public class ComputerUtilCombat {
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay"));
// The creature won't untap next turn
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
}
/**
@@ -177,7 +175,7 @@ public class ComputerUtilCombat {
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
int damage = attacker.getNetCombatDamage();
int sum = 0;
if (attacked instanceof Player player && !player.canLoseLife()) {
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) {
return 0;
}
@@ -215,10 +213,10 @@ public class ComputerUtilCombat {
int damage = attacker.getNetCombatDamage();
int poison = 0;
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
if (attacker.isInfectDamage(attacked)) {
if (attacker.hasKeyword(Keyword.INFECT)) {
int pd = predictDamageTo(attacked, damage, attacker, true);
// opponent can always order it so that he gets 0
if (pd == 1 && attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
if (pd == 1 && Iterables.any(attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
pd = 0;
}
poison += pd;
@@ -358,7 +356,7 @@ public class ComputerUtilCombat {
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (trampleDamage > 0) {
if (attacker.isInfectDamage(ai)) {
if (attacker.hasKeyword(Keyword.INFECT)) {
poison += trampleDamage;
}
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
@@ -406,11 +404,11 @@ public class ComputerUtilCombat {
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);
// Special cases:
// AI can't lose in combat in presence of Worship (with creatures)
if (otb.anyMatch(CardPredicates.nameEquals("Worship")) && !ai.getCreaturesInPlay().isEmpty()) {
if (Iterables.any(otb, CardPredicates.nameEquals("Worship")) && !ai.getCreaturesInPlay().isEmpty()) {
return false;
}
// AI can't lose in combat in presence of Elderscale Wurm (at 7 life or more)
if (otb.anyMatch(CardPredicates.nameEquals("Elderscale Wurm")) && ai.getLife() >= 7) {
if (Iterables.any(otb, CardPredicates.nameEquals("Elderscale Wurm")) && ai.getLife() >= 7) {
return false;
}
@@ -458,11 +456,11 @@ public class ComputerUtilCombat {
maxTreshold--;
}
if (resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters())) {
if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())) {
return true;
}
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife());
return resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters());
}
/**
@@ -501,11 +499,11 @@ public class ComputerUtilCombat {
}
}
if (resultingPoison(ai, combat) >= ai.getGame().getRules().getPoisonCountersToLose()) {
if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1) {
return true;
}
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1;
return resultingPoison(ai, combat) >= ai.getGame().getRules().getPoisonCountersToLose();
}
// This calculates the amount of damage a blockgang can deal to the attacker
@@ -901,7 +899,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
if (!stAb.checkMode("Continuous")) {
continue;
}
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
@@ -1197,7 +1195,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
if (!stAb.checkMode("Continuous")) {
continue;
}
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
@@ -1245,7 +1243,7 @@ public class ComputerUtilCombat {
continue;
}
sa.setActivatingPlayer(source.getController());
sa.setActivatingPlayer(source.getController(), true);
if (sa.hasParam("Cost")) {
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
@@ -1388,7 +1386,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
if (!"Continuous".equals(stAb.getParam("Mode"))) {
continue;
}
if (!stAb.hasParam("Affected")) {
@@ -1429,13 +1427,12 @@ public class ComputerUtilCombat {
if (sa == null) {
continue;
}
sa.setActivatingPlayer(source.getController(), true);
if (sa.usesTargeting()) {
continue; // targeted pumping not supported
}
sa.setActivatingPlayer(source.getController());
// DealDamage triggers
if (ApiType.DealDamage.equals(sa.getApi())) {
if (!sa.hasParam("Defined") || !sa.getParam("Defined").startsWith("TriggeredAttacker")) {
@@ -1735,7 +1732,6 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about Deathtouch
if (blocker.hasDoubleStrike()) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true;
@@ -1965,7 +1961,6 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about deathtouch
if (attacker.hasDoubleStrike()) {
if (attackerDamage >= defenderLife) {
return true;
@@ -2542,20 +2537,20 @@ public class ComputerUtilCombat {
if (combat != null) {
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
if (def instanceof Card card && Iterables.contains(defenders, def)) {
if (card.isPlaneswalker()) {
if (def instanceof Card && Iterables.contains(defenders, def)) {
if (((Card)def).isPlaneswalker()) {
return def;
}
if (card.isBattle()) {
if (((Card)def).isBattle()) {
return def;
}
}
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
for (GameEntity p : defenders) {
if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) {
return p;
}
if (p instanceof Card card && !ComputerUtilCard.canBeBlockedProfitably(card.getController(), attacker, true)) {
if (p instanceof Card && !ComputerUtilCard.canBeBlockedProfitably(((Card)p).getController(), attacker, true)) {
return p;
}
}

View File

@@ -1,22 +1,20 @@
package forge.ai;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi;
import forge.ai.ability.TokenAi;
import forge.card.ColorSet;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
@@ -25,9 +23,15 @@ import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType;
import forge.util.IterableUtil;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import java.util.List;
import java.util.Set;
public class ComputerUtilCost {
@@ -50,7 +54,8 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPutCounter addCounter) {
if (part instanceof CostPutCounter) {
final CostPutCounter addCounter = (CostPutCounter) part;
final CounterType type = addCounter.getCounter();
if (type.is(CounterEnumType.M1M1)) {
@@ -76,7 +81,9 @@ public class ComputerUtilCost {
}
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter remCounter) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
if (type.is(CounterEnumType.P1P1)) {
@@ -103,7 +110,9 @@ public class ComputerUtilCost {
&& !source.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (part instanceof CostRemoveAnyCounter remCounter) {
} else if (part instanceof CostRemoveAnyCounter) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision pay = decision.visit(remCounter);
return pay != null;
}
@@ -128,14 +137,10 @@ public class ComputerUtilCost {
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard disc) {
if (part instanceof CostDiscard) {
final CostDiscard disc = (CostDiscard) part;
final String type = disc.getType();
final CardCollection typeList;
int num;
if (type.equals("Hand")) {
typeList = hand;
num = hand.size();
} else {
if (type.equals("CARDNAME")) {
if (source.getAbilityText().contains("Bloodrush")) {
continue;
@@ -145,12 +150,12 @@ public class ComputerUtilCost {
return true;
}
}
typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
final CardCollection typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) {
continue;
}
num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
}
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
if (pref == null) {
@@ -180,7 +185,8 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDamage pay) {
if (part instanceof CostDamage) {
final CostDamage pay = (CostDamage) part;
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
if (ai.getLife() - realDamage < remainingLife
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
@@ -212,8 +218,13 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife payLife) {
int amount = payLife.getAbilityAmount(sourceAbility);
if (part instanceof CostPayLife) {
final CostPayLife payLife = (CostPayLife) part;
Integer amount = payLife.convertAmount();
if (amount == null) {
amount = AbilityUtils.calculateAmount(source, payLife.getAmount(), sourceAbility);
}
// check if there's override for the remainingLife threshold
if (sourceAbility != null && sourceAbility.hasParam("AILifeThreshold")) {
@@ -286,7 +297,8 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice sac) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
if (sac.payCostFromSource() && source.isCreature()) {
@@ -335,11 +347,12 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice sac) {
if (part instanceof CostSacrifice) {
if (suppressRecursiveSacCostCheck) {
return false;
}
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
String type = sac.getType();
@@ -515,18 +528,14 @@ public class ComputerUtilCost {
* @return a boolean.
*/
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
return canPayCost(sa.getPayCosts(), sa, player, effect);
}
public static boolean canPayCost(final Cost cost, final SpellAbility sa, final Player player, final boolean effect) {
if (sa.getActivatingPlayer() == null) {
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
sa.setActivatingPlayer(player, true); // complaints on NPE had came before this line was added.
}
boolean cannotBeCountered = false;
// Check for stuff like Nether Void
int extraManaNeeded = 0;
if (!effect) {
if (sa instanceof Spell) {
cannotBeCountered = !sa.isCounterableBy(null);
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
@@ -564,7 +573,7 @@ public class ComputerUtilCost {
// Try not to lose Planeswalker if not threatened
if (sa.isPwAbility()) {
for (final CostPart part : cost.getCostParts()) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
// refuse to pay if opponent has no creature threats or
@@ -594,28 +603,211 @@ public class ComputerUtilCost {
// Bail early on Casualty in case there are no cards that would make sense to pay with
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
for (final CostPart part : cost.getCostParts()) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostSacrifice) {
CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
sa.getActivatingPlayer(), sa.getHostCard(), sa);
valid = CardLists.filter(valid, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
valid = CardLists.filter(valid, Predicates.not(CardPredicates.hasSVar("AIDontSacToCasualty")));
if (valid.isEmpty()) {
return false;
}
}
}
}
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect);
}
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payNever = "Never".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
if (payForOwnOnly && !isMine) { return false; }
if (payOwner) {
final String defined = aiLogic.substring(7);
final Player player = AbilityUtils.getDefinedPlayers(source, defined, sa).get(0);
if (!payer.equals(player)) {
return false;
}
} else if ("OnlyDontControl".equals(aiLogic)) {
if (source == null || payer.equals(source.getController())) {
return false;
}
} else if ("Paralyze".equals(aiLogic)) {
final Card c = source.getEnchantingCard();
if (c == null || c.isUntapped()) {
return false;
}
} else if ("RiskFactor".equals(aiLogic)) {
final Player activator = sa.getActivatingPlayer();
if (!activator.canDraw()) {
return false;
}
} else if ("MorePowerful".equals(aiLogic)) {
final int sourceCreatures = sa.getActivatingPlayer().getCreaturesInPlay().size();
final int payerCreatures = payer.getCreaturesInPlay().size();
if (payerCreatures > sourceCreatures + 1) {
return false;
}
} else if (aiLogic != null && aiLogic.startsWith("LifeLE")) {
// if payer can't lose life its no need to pay unless
if (!payer.canLoseLife())
return false;
else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substring(6), sa)) {
return true;
}
} else if ("WillAttack".equals(aiLogic)) {
AiAttackController aiAtk = new AiAttackController(payer);
Combat combat = new Combat(payer);
aiAtk.declareAttackers(combat);
if (combat.getAttackers().isEmpty()) {
return false;
}
} else if ("nonToken".equals(aiLogic) && !AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).isEmpty()
&& AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) {
return false;
} else if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) {
return false;
} else if (aiLogic != null && aiLogic.startsWith("Fabricate")) {
final int n = Integer.parseInt(aiLogic.substring("Fabricate".length()));
// if host would leave the play or if host is useless, create tokens
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(payer, source)) {
return false;
}
// need a copy for one with extra +1/+1 counter boost,
// without causing triggers to run
final Card copy = CardCopyService.getLKICopy(source);
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n);
copy.setZone(source.getZone());
// if host would put into the battlefield attacking
Combat combat = source.getGame().getCombat();
if (combat != null && combat.isAttacking(source)) {
final Player defender = combat.getDefenderPlayerByAttacker(source);
if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true)) {
return true;
}
return false;
}
// if the host has haste and can attack
if (CombatUtil.canAttack(copy)) {
for (final Player opp : payer.getOpponents()) {
if (CombatUtil.canAttack(copy, opp) &&
opp.canLoseLife() &&
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
return true;
}
}
// TODO check for trigger to turn token ETB into +1/+1 counter for host
// TODO check for trigger to turn token ETB into damage or life loss for opponent
// in this cases Token might be prefered even if they would not survive
final Card tokenCard = TokenAi.spawnToken(payer, sa);
// Token would not survive
if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) {
return true;
}
// Special Card logic, this one try to median its power with the number of artifacts
if ("Marionette Master".equals(source.getName())) {
CardCollection list = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS);
return list.size() >= copy.getNetPower();
} else if ("Cultivator of Blades".equals(source.getName())) {
// Cultivator does try to median with number of Creatures
CardCollection list = payer.getCreaturesInPlay();
return list.size() >= copy.getNetPower();
}
// evaluate Creature with +1/+1
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
final CardCollection tokenList = new CardCollection(source);
for (int i = 0; i < n; ++i) {
tokenList.add(TokenAi.spawnToken(payer, sa));
}
// evaluate Host with Tokens
int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList);
return evalToken < evalCounter;
} else if ("Riot".equals(aiLogic)) {
return !SpecialAiLogic.preferHasteForRiot(sa, payer);
}
// Check for shocklands and similar ETB replacement effects
if (sa.hasParam("ETB") && sa.getApi().equals(ApiType.Tap)) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
final CostPayLife lifeCost = (CostPayLife) part;
Integer amount = lifeCost.convertAmount();
if (payer.getLife() > (amount + 1) && payer.canPayLife(amount, true, sa)) {
final int landsize = payer.getLandsInPlay().size() + 1;
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
// Check if the AI has enough lands to play the card
if (landsize != c.getCMC()) {
continue;
}
// Check if the AI intends to play the card and if it can pay for it with the mana it has
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
boolean canPay = c.getManaCost().canBePaidWithAvailable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
if (canPay && willPlay) {
return true;
}
}
}
return false;
}
}
}
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
return false;
}
// ward or human misplay
if (ApiType.Counter.equals(sa.getApi())) {
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
for (SpellAbility toBeCountered : spells) {
if (!toBeCountered.isCounterableBy(sa)) {
return false;
}
// no reason to pay if we don't plan to confirm
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered.getApi()).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
return false;
}
// TODO check hasFizzled
}
}
// AI was crashing because the blank ability used to pay costs
// Didn't have any of the data on the original SA to pay dependant costs
return checkLifeCost(payer, cost, source, 4, sa)
&& checkDamageCost(payer, cost, source, 4, sa)
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source, sa))
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
}
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
}
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) {
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.UNTAPPED);
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED);
Set<String> colorsAvailable = Sets.newHashSet();
if (additionalLands != null) {
@@ -705,8 +897,8 @@ public class ComputerUtilCost {
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
if (source.usesTargeting()) {
final CardCollectionView targets = source.getTargets().getTargetCards();
choices = IterableUtil.filter(choices, Predicate.not(CardPredicates.isController(ai).and(targets::contains)));
final CardCollection targets = new CardCollection(source.getTargets().getTargetCards());
choices = Iterables.filter(choices, Predicates.not(Predicates.and(CardPredicates.isController(ai), Predicates.in(targets))));
}
return new CardCollection(choices);
}

View File

@@ -1,10 +1,7 @@
package forge.ai;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
@@ -46,7 +43,6 @@ import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
public class ComputerUtilMana {
private final static boolean DEBUG_MANA_PAYMENT = false;
@@ -56,28 +52,25 @@ public class ComputerUtilMana {
return payManaCost(cost, sa, ai, true, true, effect);
}
public static boolean canPayManaCost(final SpellAbility sa, final Player ai, final int extraMana, final boolean effect) {
return canPayManaCost(sa.getPayCosts(), sa, ai, extraMana, effect);
}
public static boolean canPayManaCost(final Cost cost, final SpellAbility sa, final Player ai, final int extraMana, final boolean effect) {
return payManaCost(cost, sa, ai, true, extraMana, true, effect);
return payManaCost(sa, ai, true, extraMana, true, effect);
}
public static boolean payManaCost(ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean effect) {
return payManaCost(cost, sa, ai, false, true, effect);
}
public static boolean payManaCost(final Cost cost, final Player ai, final SpellAbility sa, final boolean effect) {
return payManaCost(cost, sa, ai, false, 0, true, effect);
public static boolean payManaCost(final Player ai, final SpellAbility sa, final boolean effect) {
return payManaCost(sa, ai, false, 0, true, effect);
}
private static boolean payManaCost(final Cost cost, final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable, final boolean effect) {
ManaCostBeingPaid manaCost = calculateManaCost(cost, sa, test, extraMana, effect);
return payManaCost(manaCost, sa, ai, test, checkPlayable, effect);
private static boolean payManaCost(final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable, final boolean effect) {
ManaCostBeingPaid cost = calculateManaCost(sa, test, extraMana);
return payManaCost(cost, sa, ai, test, checkPlayable, effect);
}
/**
* Return the number of colors used for payment for Converge
*/
public static int getConvergeCount(final SpellAbility sa, final Player ai) {
ManaCostBeingPaid cost = calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
ManaCostBeingPaid cost = calculateManaCost(sa, true, 0);
if (payManaCost(cost, sa, ai, true, true, false)) {
return cost.getSunburst();
}
@@ -88,15 +81,15 @@ public class ComputerUtilMana {
public static boolean hasEnoughManaSourcesToCast(final SpellAbility sa, final Player ai) {
if (ai == null || sa == null)
return false;
sa.setActivatingPlayer(ai);
return payManaCost(sa.getPayCosts(), sa, ai, true, 0, false, false);
sa.setActivatingPlayer(ai, true);
return payManaCost(sa, ai, true, 0, false, false);
}
private static Integer scoreManaProducingCard(final Card card) {
int score = 0;
for (SpellAbility ability : card.getSpellAbilities()) {
ability.setActivatingPlayer(card.getController());
ability.setActivatingPlayer(card.getController(), true);
if (ability.isManaAbility()) {
score += ability.calculateScoreForManaAbility();
// TODO check TriggersWhenSpent
@@ -158,7 +151,7 @@ public class ComputerUtilMana {
}
// Mana abilities on the same card
String shardMana = shard.toShortString();
String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", "");
boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
@@ -267,10 +260,7 @@ public class ComputerUtilMana {
saList = filteredList;
break;
case "NotSameCard":
String hostName = sa.getHostCard().getName();
saList = filteredList.stream()
.filter(saPay -> !saPay.getHostCard().getName().equals(hostName))
.collect(Collectors.toList());
saList = Lists.newArrayList(Iterables.filter(filteredList, saPay -> !saPay.getHostCard().getName().equals(sa.getHostCard().getName())));
break;
default:
break;
@@ -320,7 +310,7 @@ public class ComputerUtilMana {
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
// be tapped to pay its own cost if there's another untapped land like that available
if (ma.getHostCard().equals(sa.getTargetCard())) {
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(ma.getHostCard().getName()).and(CardPredicates.UNTAPPED)) > 1) {
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.nameEquals(ma.getHostCard().getName()), CardPredicates.Presets.UNTAPPED)) > 1) {
continue;
}
}
@@ -477,18 +467,18 @@ public class ComputerUtilMana {
public static String predictManafromSpellAbility(SpellAbility saPayment, Player ai, ManaCostShard toPay) {
Card hostCard = saPayment.getHostCard();
StringBuilder manaProduced = new StringBuilder(predictManaReplacement(saPayment, ai, toPay));
String originalProduced = manaProduced.toString();
String manaProduced = predictManaReplacement(saPayment, ai, toPay);
String originalProduced = manaProduced;
if (originalProduced.isEmpty()) {
return manaProduced.toString();
return manaProduced;
}
// Run triggers like Nissa
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(hostCard);
runParams.put(AbilityKey.Activator, ai); // assuming AI would only ever gives itself mana
runParams.put(AbilityKey.AbilityMana, saPayment);
runParams.put(AbilityKey.Produced, manaProduced.toString());
runParams.put(AbilityKey.Produced, manaProduced);
for (Trigger tr : ai.getGame().getTriggerHandler().getActiveTrigger(TriggerType.TapsForMana, runParams)) {
SpellAbility trSA = tr.ensureAbility();
if (trSA == null) {
@@ -500,7 +490,7 @@ public class ComputerUtilMana {
if (produced.equals("Chosen")) {
produced = MagicColor.toShortString(trSA.getHostCard().getChosenColor());
}
manaProduced.append(" ").append(StringUtils.repeat(produced, " ", pAmount));
manaProduced += " " + StringUtils.repeat(produced, " ", pAmount);
} else if (ApiType.ManaReflected.equals(trSA.getApi())) {
final String colorOrType = trSA.getParamOrDefault("ColorOrType", "Color");
// currently Color or Type, Type is colors + colorless
@@ -509,11 +499,11 @@ public class ComputerUtilMana {
if (reflectProperty.equals("Produced") && !originalProduced.isEmpty()) {
// check if a colorless shard can be paid from the trigger
if (toPay.equals(ManaCostShard.COLORLESS) && colorOrType.equals("Type") && originalProduced.contains("C")) {
manaProduced.append(" " + "C");
manaProduced += " " + "C";
} else if (originalProduced.length() == 1) {
// if length is only one, and it either is equal C == Type
if (colorOrType.equals("Type") || !originalProduced.equals("C")) {
manaProduced.append(" ").append(originalProduced);
manaProduced += " " + originalProduced;
}
} else {
// should it look for other shards too?
@@ -521,7 +511,7 @@ public class ComputerUtilMana {
for (String s : originalProduced.split(" ")) {
if (colorOrType.equals("Type") || !s.equals("C") && toPay.canBePaidWithManaOfColor(MagicColor.fromName(s))) {
found = true;
manaProduced.append(" ").append(s);
manaProduced += " " + s;
break;
}
}
@@ -529,7 +519,7 @@ public class ComputerUtilMana {
if (!found) {
for (String s : originalProduced.split(" ")) {
if (colorOrType.equals("Type") || !s.equals("C")) {
manaProduced.append(" ").append(s);
manaProduced += " " + s;
break;
}
}
@@ -538,7 +528,7 @@ public class ComputerUtilMana {
}
}
}
return manaProduced.toString();
return manaProduced;
}
public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai) {
@@ -619,7 +609,7 @@ public class ComputerUtilMana {
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard()));
}
CostPayment.handleOfferings(sa, true, cost.isPaid());
@@ -642,8 +632,7 @@ public class ComputerUtilMana {
List<SpellAbility> paymentList = Lists.newArrayList();
final ManaPool manapool = ai.getManaPool();
// Apply color/type conversion matrix if necessary (already done via autopay)
if (ai.getControllingPlayer() == null) {
// Apply the color/type conversion matrix if necessary
manapool.restoreColorReplacements();
CardPlayOption mayPlay = sa.getMayPlayOption();
if (!effect) {
@@ -657,13 +646,10 @@ public class ComputerUtilMana {
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
}
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
}
// not worth checking if it makes sense to not spend floating first
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
CostPayment.handleOfferings(sa, test, cost.isPaid());
// paid all from floating mana
return true;
return true; // paid all from floating mana
}
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
@@ -762,7 +748,7 @@ public class ComputerUtilMana {
break; // unwise to pay
} else if (sa.getParam("AIPhyrexianPayment").startsWith("OnFatalDamage.")) {
int dmg = Integer.parseInt(sa.getParam("AIPhyrexianPayment").substring(14));
if (ai.getOpponents().stream().noneMatch(PlayerPredicates.lifeLessOrEqualTo(dmg))) {
if (!Iterables.any(ai.getOpponents(), PlayerPredicates.lifeLessOrEqualTo(dmg))) {
break; // no one to finish with the gut shot
}
}
@@ -805,7 +791,7 @@ public class ComputerUtilMana {
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard()));
} else {
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect))) {
@@ -822,7 +808,7 @@ public class ComputerUtilMana {
if (hasConverge) {
// hack to prevent converge re-using sources
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard()));
}
}
}
@@ -841,8 +827,7 @@ public class ComputerUtilMana {
if (test) {
resetPayment(paymentList);
} else {
System.out.println("ComputerUtilMana: payManaCost() cost was not paid for " + sa + " (" + sa.getHostCard().getName() + "). Didn't find what to pay for " + toPay);
sa.setSkip(true);
System.out.println("ComputerUtilMana: payManaCost() cost was not paid for " + sa.toString() + " (" + sa.getHostCard().getName() + "). Didn't find what to pay for " + toPay);
}
return false;
}
@@ -957,7 +942,7 @@ public class ComputerUtilMana {
if (checkCosts) {
// Check if AI can still play this mana ability
ma.setActivatingPlayer(ai);
ma.setActivatingPlayer(ai, true);
// if the AI can't pay the additional costs skip the mana ability
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) {
return false;
@@ -1276,7 +1261,7 @@ public class ComputerUtilMana {
* @param extraMana extraMana
* @return ManaCost
*/
public static ManaCostBeingPaid calculateManaCost(final Cost cost, final SpellAbility sa, final boolean test, final int extraMana, final boolean effect) {
public static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final boolean test, final int extraMana) {
Card card = sa.getHostCard();
Zone castFromBackup = null;
if (test && sa.isSpell() && !card.isInZone(ZoneType.Stack)) {
@@ -1284,22 +1269,16 @@ public class ComputerUtilMana {
card.setCastFrom(card.getZone() != null ? card.getZone() : null);
}
Cost payCosts;
if (test) {
payCosts = CostAdjustment.adjust(cost, sa, effect);
} else {
// when not testing CostPayment already handled raise
payCosts = cost;
}
Cost payCosts = CostAdjustment.adjust(sa.getPayCosts(), sa);
CostPartMana manapart = payCosts != null ? payCosts.getCostMana() : null;
final ManaCost mana = payCosts != null ? ( manapart == null ? ManaCost.ZERO : manapart.getManaCostFor(sa) ) : ManaCost.NO_COST;
ManaCostBeingPaid manaCost = new ManaCostBeingPaid(mana);
ManaCostBeingPaid cost = new ManaCostBeingPaid(mana);
// Tack xMana Payments into mana here if X is a set value
if (manaCost.getXcounter() > 0 || extraMana > 0) {
if (cost.getXcounter() > 0 || extraMana > 0) {
int manaToAdd = 0;
int xCounter = manaCost.getXcounter();
int xCounter = cost.getXcounter();
if (test && extraMana > 0) {
final int multiplicator = Math.max(xCounter, 1);
manaToAdd = extraMana * multiplicator;
@@ -1320,9 +1299,9 @@ public class ComputerUtilMana {
xColor = "WUBRGX";
}
if (xCounter > 0) {
manaCost.setXManaCostPaid(manaToAdd / xCounter, xColor);
cost.setXManaCostPaid(manaToAdd / xCounter, xColor);
} else {
manaCost.increaseShard(ManaCostShard.parseNonGeneric(xColor), manaToAdd);
cost.increaseShard(ManaCostShard.parseNonGeneric(xColor), manaToAdd);
}
if (!test) {
@@ -1330,9 +1309,7 @@ public class ComputerUtilMana {
}
}
if (!effect) {
CostAdjustment.adjust(manaCost, sa, null, test);
}
CostAdjustment.adjust(cost, sa, null, test);
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
ManaCost mkCost = sa.getPayCosts().getTotalMana();
@@ -1351,7 +1328,7 @@ public class ComputerUtilMana {
sa.getHostCard().setCastFrom(castFromBackup);
}
return manaCost;
return cost;
}
// This method can be used to estimate the total amount of mana available to the player,
@@ -1372,7 +1349,7 @@ public class ComputerUtilMana {
maxProduced = 0;
for (SpellAbility ma : src.getManaAbilities()) {
ma.setActivatingPlayer(p);
ma.setActivatingPlayer(p, true);
if (!checkPlayable || ma.canPlay()) {
int costsToActivate = ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length;
@@ -1409,7 +1386,7 @@ public class ComputerUtilMana {
final CardCollectionView list = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand));
final List<Card> manaSources = CardLists.filter(list, c -> {
for (final SpellAbility am : getAIPlayableMana(c)) {
am.setActivatingPlayer(ai);
am.setActivatingPlayer(ai, true);
if (!checkPlayable || (am.canPlay() && am.checkRestrictions(ai))) {
return true;
}
@@ -1485,7 +1462,7 @@ public class ComputerUtilMana {
if (cost != null) {
// if the AI can't pay the additional costs skip the mana ability
m.setActivatingPlayer(ai);
m.setActivatingPlayer(ai, true);
if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m, false)) {
continue;
}
@@ -1503,7 +1480,7 @@ public class ComputerUtilMana {
AbilitySub sub = m.getSubAbility();
// We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) {
continue;
}
needsLimitedResources = true; // TODO: check for good drawbacks (gainLife)
@@ -1569,7 +1546,7 @@ public class ComputerUtilMana {
if (DEBUG_MANA_PAYMENT) {
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor m = " + m);
}
m.setActivatingPlayer(ai);
m.setActivatingPlayer(ai, true);
if (checkPlayable && !m.canPlay()) {
continue;
}
@@ -1583,7 +1560,7 @@ public class ComputerUtilMana {
// don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) {
continue;
}
}

View File

@@ -1,5 +1,7 @@
package forge.ai;
import com.google.common.base.Function;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -11,11 +13,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import java.util.List;
import java.util.function.Function;
public class CreatureEvaluator implements Function<Card, Integer> {
@Override
@@ -160,6 +159,12 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(20, "protection");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
// paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) {
value += addValue(14, "paired");
@@ -207,7 +212,11 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(1, "untapped");
}
if (!c.canUntap(c.getController(), true)) {
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
} else {
@@ -216,64 +225,30 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else {
value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
if (c.hasSVar("EndOfTurnLeavePlay")) {
value -= subValue(50, "eot-leaves");
} else if (c.hasKeyword(Keyword.CUMULATIVE_UPKEEP)) {
value -= subValue(30, "cupkeep");
} else if (c.hasStartOfKeyword("UpkeepCost")) {
value -= subValue(20, "sac-unless");
} else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
value -= subValue(10, "echo-unpaid");
}
if (c.hasKeyword(Keyword.FADING)) {
value -= subValue(20 / (Math.max(1, c.getCounters(CounterEnumType.FADE))), "fading");
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20 / (Math.max(1, c.getCounters(CounterEnumType.TIME))), "vanishing");
}
// use scaling because the creature is only available halfway
if (c.hasKeyword(Keyword.PHASING)) {
value -= subValue(Math.max(20, value / 2), "phasing");
}
if (c.hasSVar("EndOfTurnLeavePlay")) {
value -= subValue(50, "eot-leaves");
} else {
for (Trigger t : c.getTriggers()) {
if (!TriggerType.Phase.equals(t.getMode())) {
continue;
}
if (!"Upkeep".equals(t.getParam("Phase"))) {
continue;
}
if (t.isKeyword(Keyword.CUMULATIVE_UPKEEP)) {
value -= subValue(30, "cupkeep");
} else if (t.isKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
value -= subValue(10, "echo-unpaid");
}
if (t.isKeyword(Keyword.FADING)) {
value -= subValue(20 / (Math.max(1, c.isInPlay() ? c.getCounters(CounterEnumType.FADE) : c.getKeywordMagnitude(Keyword.FADING))), "fading");
}
if (t.isKeyword(Keyword.VANISHING)) {
value -= subValue(20 / (Math.max(1, c.isInPlay() ? c.getCounters(CounterEnumType.TIME) : c.getKeywordMagnitude(Keyword.VANISHING))), "vanishing");
}
SpellAbility ab = t.ensureAbility();
if (ab == null) {
continue;
}
if (ApiType.DealDamage.equals(ab.getApi())) {
if (!"You".equals(ab.getParamOrDefault("Defined", "You"))) {
continue;
}
if (c.getController().canLoseLife()) {
// TODO no longer a KW
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
value -= subValue(20, "upkeep-dmg");
}
} else if (ApiType.Sacrifice.equals(ab.getApi())) {
if (!ab.hasParam("UnlessCost")) {
continue;
}
value -= subValue(20, "sac-unless");
}
}
}
// card-specific evaluation modifier
if (c.hasSVar("AIEvaluationModifier")) {
@@ -305,9 +280,8 @@ public class CreatureEvaluator implements Function<Card, Integer> {
}
}
}
} else if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) {
return -10; // can be sacrificed in response to ability or spell, thus, less prioritable
}
// default value
return 10;
}

View File

@@ -13,7 +13,6 @@ import forge.card.mana.ManaAtom;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
@@ -62,7 +61,6 @@ public abstract class GameState {
private int landsPlayed = 0;
private int landsPlayedLastTurn = 0;
private int numRingTemptedYou = 0;
private int speed = 0;
private String precast = null;
private String putOnStack = null;
private final Map<ZoneType, String> cardTexts = new EnumMap<>(ZoneType.class);
@@ -139,7 +137,6 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace(prefix + "landsplayed=", String.valueOf(p.landsPlayed), "\n"));
sb.append(TextUtil.concatNoSpace(prefix + "landsplayedlastturn=", String.valueOf(p.landsPlayedLastTurn), "\n"));
sb.append(TextUtil.concatNoSpace(prefix + "numringtemptedyou=", String.valueOf(p.numRingTemptedYou), "\n"));
sb.append(TextUtil.concatNoSpace(prefix + "speed=", String.valueOf(p.speed), "\n"));
if (!p.counters.isEmpty()) {
sb.append(TextUtil.concatNoSpace(prefix + "counters=", p.counters, "\n"));
}
@@ -170,7 +167,6 @@ public abstract class GameState {
p.counters = countersToString(player.getCounters());
p.manaPool = processManaPool(player.getManaPool());
p.numRingTemptedYou = player.getNumRingTemptedYou();
p.speed = player.getSpeed();
playerStates.add(p);
}
@@ -229,7 +225,7 @@ public abstract class GameState {
if (card instanceof DetachedCardEffect) {
continue;
}
int playerIndex = game.getPlayers().indexOf(card.getZone().getPlayer());
int playerIndex = game.getPlayers().indexOf(card.getController());
addCard(zone, playerStates.get(playerIndex).cardTexts, card);
}
}
@@ -546,8 +542,6 @@ public abstract class GameState {
getPlayerState(categoryName).landsPlayedLastTurn = Integer.parseInt(categoryValue);
} else if (categoryName.endsWith("numringtemptedyou")) {
getPlayerState(categoryName).numRingTemptedYou = Integer.parseInt(categoryValue);
} else if (categoryName.endsWith("speed")) {
getPlayerState(categoryName).speed = Integer.parseInt(categoryValue);
} else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) {
getPlayerState(categoryName).cardTexts.put(ZoneType.Battlefield, categoryValue);
} else if (categoryName.endsWith("hand")) {
@@ -1139,7 +1133,7 @@ public abstract class GameState {
p.getZone(zt).removeAllCards(true);
}
p.getCommanders().clear();
p.setCommanders(Lists.newArrayList());
p.clearTheRing();
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<>(ZoneType.class);
@@ -1152,7 +1146,6 @@ public abstract class GameState {
p.setLandsPlayedThisTurn(state.landsPlayed);
p.setLandsPlayedLastTurn(state.landsPlayedLastTurn);
p.setNumRingTemptedYou(state.numRingTemptedYou);
p.setSpeed(state.speed);
p.clearPaidForSA();
@@ -1215,7 +1208,6 @@ public abstract class GameState {
p.setRingLevel(i);
}
}
if (state.speed > 0) p.createSpeedEffect();
}
/**
@@ -1306,10 +1298,10 @@ public abstract class GameState {
} else if (info.startsWith("FaceDown")) {
c.turnFaceDown(true);
if (info.endsWith("Manifested")) {
c.setManifested(new SpellAbility.EmptySa(ApiType.Manifest, c));
c.setManifested(true);
}
if (info.endsWith("Cloaked")) {
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
c.setCloaked(true);
}
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
@@ -1409,7 +1401,7 @@ public abstract class GameState {
} else if (info.equals("Foretold")) {
c.setForetold(true);
c.turnFaceDown(true);
c.addMayLookFaceDownExile(c.getOwner());
c.addMayLookTemp(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) {
c.setTurnInZone(turn);
} else if (info.equals("IsToken")) {

View File

@@ -1,5 +1,7 @@
package forge.ai;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
import forge.LobbyPlayer;
import forge.ai.ability.ProtectAi;
@@ -15,14 +17,13 @@ import forge.game.*;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostEnlist;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
@@ -34,21 +35,21 @@ import forge.game.player.*;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.*;
import forge.util.Aggregates;
import forge.util.ITriggerEvent;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.security.InvalidParameterException;
import java.util.*;
import java.util.function.Predicate;
/**
@@ -301,14 +302,6 @@ public class PlayerControllerAi extends PlayerController {
return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional, params);
}
@Override
public List<Card> chooseContraptionsToCrank(List<Card> contraptions) {
return CardLists.filter(contraptions, c -> {
Trigger crankTrigger = IterableUtil.find(c.getTriggers(), t -> t.getMode() == TriggerType.CrankContraption);
return confirmTrigger(new WrappedAbility(crankTrigger, crankTrigger.getOverridingAbility(), player));
});
}
@Override
public boolean helpPayForAssistSpell(ManaCostBeingPaid cost, SpellAbility sa, int max, int requested) {
int toPay = getAi().attemptToAssist(sa, max, requested);
@@ -352,7 +345,11 @@ public class PlayerControllerAi extends PlayerController {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
return SpellApiToAi.Converter.get(sa).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
}
@Override
@@ -394,7 +391,11 @@ public class PlayerControllerAi extends PlayerController {
@Override
public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params) {
return SpellApiToAi.Converter.get(sa).chooseSingleSpellAbility(player, sa, spells, params);
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params);
}
@Override
@@ -454,13 +455,13 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean confirmPayment(CostPart costPart, String prompt, SpellAbility sa) {
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
public Player chooseStartingPlayer(boolean isFirstgame) {
return this.player; // AI is brave :)
}
@Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
return brains.aiShouldRun(replacementEffect, effectSA, affected);
public CardCollection orderBlockers(Card attacker, CardCollection blockers) {
return AiBlockController.orderBlockers(attacker, blockers);
}
@Override
@@ -483,11 +484,6 @@ public class PlayerControllerAi extends PlayerController {
return chosenAttackers;
}
@Override
public CardCollection orderBlockers(Card attacker, CardCollection blockers) {
return AiBlockController.orderBlockers(attacker, blockers);
}
@Override
public CardCollection orderBlocker(Card attacker, Card blocker, CardCollection oldBlockers) {
return AiBlockController.orderBlocker(attacker, blocker, oldBlockers);
@@ -574,7 +570,7 @@ public class PlayerControllerAi extends PlayerController {
if (destinationZone == ZoneType.Graveyard) {
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
if (getGame().getCardsInGame().anyMatch(card -> {
if (Iterables.any(getGame().getCardsInGame(), card -> {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter");
})) {
@@ -618,7 +614,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
int landsOTB = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
int landsOTB = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
if (!p.isOpponentOf(player)) {
if (landsOTB <= 2) {
@@ -679,6 +675,20 @@ public class PlayerControllerAi extends PlayerController {
: ComputerUtil.getCardsToDiscardFromOpponent(player, p, sa, validCards, min, max);
}
@Override
public void playSpellAbilityForFree(SpellAbility copySA, boolean mayChooseNewTargets) {
// Ai is known to set targets in doTrigger, so if it cannot choose new targets, we won't call canPlays
if (mayChooseNewTargets) {
if (copySA instanceof Spell) {
Spell spell = (Spell) copySA;
((PlayerControllerAi) player.getController()).getAi().canPlayFromEffectAI(spell, true, true);
} else {
getAi().canPlaySa(copySA);
}
}
ComputerUtil.playSpellAbilityForFree(player, copySA);
}
@Override
public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) {
if (canSetupTargets)
@@ -693,7 +703,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) {
Iterable<Card> cardsOfType = IterableUtil.filter(hand, CardPredicates.restriction(uType.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa));
Iterable<Card> cardsOfType = Iterables.filter(hand, CardPredicates.restriction(uType.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa));
if (!Iterables.isEmpty(cardsOfType)) {
Card toDiscard = Aggregates.itemWithMin(cardsOfType, Card::getCMC);
return new CardCollection(toDiscard);
@@ -707,8 +717,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, boolean isOptional) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa, validTypes);
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes, boolean isOptional) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa, validTypes, invalidTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) {
chosen = validTypes.iterator().next();
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: " + chosen);
@@ -726,14 +736,6 @@ public class PlayerControllerAi extends PlayerController {
return Aggregates.random(sectors);
}
@Override
public int chooseSprocket(Card assignee, boolean forceDifferent) {
int nextSprocket = (player.getCrankCounter() % 3) + 1;
if(forceDifferent && nextSprocket == assignee.getSprocket())
return (nextSprocket % 3) + 1;
return nextSprocket;
}
@Override
public PlanarDice choosePDRollToIgnore(List<PlanarDice> rolls) {
//TODO create AI logic for this
@@ -746,30 +748,6 @@ public class PlayerControllerAi extends PlayerController {
return Aggregates.random(rolls);
}
@Override
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
//TODO create AI logic for this
return new ArrayList<>();
}
@Override
public Integer chooseRollToModify(List<Integer> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
//TODO create AI logic for this
return Aggregates.random(swapChoices);
}
@Override
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn);
@@ -786,13 +764,13 @@ public class PlayerControllerAi extends PlayerController {
for (int i = 0; i < cardsToReturn; i++) {
hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, CardPredicates.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.count(toReturn, CardPredicates.LANDS);
CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.count(toReturn, Presets.LANDS);
// If we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {
CardCollection producingLands = CardLists.filter(landsInHand, CardPredicates.LANDS_PRODUCING_MANA);
CardCollection nonProducingLands = CardLists.filter(landsInHand, CardPredicates.LANDS_PRODUCING_MANA.negate());
CardCollection producingLands = CardLists.filter(landsInHand, Presets.LANDS_PRODUCING_MANA);
CardCollection nonProducingLands = CardLists.filter(landsInHand, Predicates.not(Presets.LANDS_PRODUCING_MANA));
Card worstLand = nonProducingLands.isEmpty() ? ComputerUtilCard.getWorstLand(producingLands)
: ComputerUtilCard.getWorstLand(nonProducingLands);
toReturn.add(worstLand);
@@ -860,8 +838,23 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public Player chooseStartingPlayer(boolean isFirstgame) {
return this.player; // AI is brave :)
public boolean payManaOptional(Card c, Cost cost, SpellAbility sa, String prompt, ManaPaymentPurpose purpose) {
// TODO replace with EmptySa
final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
ability.setActivatingPlayer(c.getController(), true);
ability.setCardState(sa.getCardState());
if (ComputerUtil.playNoStack(c.getController(), ability, getGame(), true)) {
// transfer this info for Balduvian Fallen
sa.setPayingMana(ability.getPayingMana());
return true;
}
return false;
}
@Override
public List<SpellAbility> chooseSaToActivateFromOpeningHand(List<SpellAbility> usableFromOpeningHand) {
return brains.chooseSaToActivateFromOpeningHand(usableFromOpeningHand);
}
@Override
@@ -880,11 +873,6 @@ public class PlayerControllerAi extends PlayerController {
return bestZone;
}
@Override
public List<SpellAbility> chooseSaToActivateFromOpeningHand(List<SpellAbility> usableFromOpeningHand) {
return brains.chooseSaToActivateFromOpeningHand(usableFromOpeningHand);
}
@Override
public int chooseNumber(SpellAbility sa, String title, int min, int max) {
return brains.chooseNumber(sa, title, min, max);
@@ -892,7 +880,11 @@ public class PlayerControllerAi extends PlayerController {
@Override
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map<String, Object> params) {
return SpellApiToAi.Converter.get(sa).chooseNumber(player, sa, min, max, params);
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseNumber(player, sa, min, max, params);
}
@Override
@@ -977,6 +969,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
return defaultVal != null && defaultVal;
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
case LeftOrRight: return brains.chooseDirection(sa);
case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even
default:
@@ -994,7 +987,11 @@ public class PlayerControllerAi extends PlayerController {
*/
@Override
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Map<String, Object> params) {
return SpellApiToAi.Converter.get(sa).chooseBinary(kindOfChoice, sa, params);
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseBinary(kindOfChoice, sa, params);
}
@Override
@@ -1046,6 +1043,12 @@ public class PlayerControllerAi extends PlayerController {
return Iterables.getFirst(colors, MagicColor.WHITE);
}
@Override
public ICardFace chooseSingleCardFace(SpellAbility sa, String message,
Predicate<ICardFace> cpp, String name) {
throw new UnsupportedOperationException("Should not be called for AI"); // or implement it if you know how
}
@Override
public List<String> chooseColors(String message, SpellAbility sa, int min, int max, List<String> options) {
return ComputerUtilCard.chooseColor(sa, min, max, options);
@@ -1064,7 +1067,11 @@ public class PlayerControllerAi extends PlayerController {
if (options.size() <= 1) {
return Iterables.getFirst(options, null);
}
return SpellApiToAi.Converter.get(sa).chooseCounterType(options, sa, params);
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCounterType(options, sa, params);
}
@Override
@@ -1073,7 +1080,7 @@ public class PlayerControllerAi extends PlayerController {
return Iterables.getFirst(options, null);
}
List<String> possible = Lists.newArrayList();
CardCollection oppUntappedCreatures = CardLists.filter(player.getOpponents().getCreaturesInPlay(), CardPredicates.UNTAPPED);
CardCollection oppUntappedCreatures = CardLists.filter(player.getOpponents().getCreaturesInPlay(), CardPredicates.Presets.UNTAPPED);
if (tgtCard != null) {
for (String kw : options) {
if (tgtCard.hasKeyword(kw)) {
@@ -1127,9 +1134,19 @@ public class PlayerControllerAi extends PlayerController {
}
if (!possible.isEmpty()) {
return Aggregates.random(possible);
} else {
return Aggregates.random(options); // if worst comes to worst, at least do something
}
}
return Aggregates.random(options); // if worst comes to worst, at least do something
@Override
public boolean confirmPayment(CostPart costPart, String prompt, SpellAbility sa) {
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
}
@Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
return brains.aiShouldRun(replacementEffect, effectSA, affected);
}
@Override
@@ -1206,34 +1223,29 @@ public class PlayerControllerAi extends PlayerController {
return choice;
}
@Override
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean effect) {
return ComputerUtilMana.payManaCost(new Cost(toPay, effect), player, sa, effect);
}
@Override
public boolean payCombatCost(Card c, Cost cost, SpellAbility sa, String prompt) {
if (ComputerUtil.playNoStack(c.getController(), sa, getGame(), true)) {
return true;
}
return false;
}
@Override
public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) {
if (SpellApiToAi.Converter.get(sa).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
if (!ComputerUtilCost.canPayCost(cost, sa, player, true)) {
return false;
}
final Card source = sa.getHostCard();
// TODO replace with EmptySa
final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } };
emptyAbility.setActivatingPlayer(player, true);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setReplacingObjects(sa.getReplacingObjects());
emptyAbility.setTrigger(sa.getTrigger());
emptyAbility.setReplacementEffect(sa.getReplacementEffect());
emptyAbility.setSVars(sa.getSVars());
emptyAbility.setCardState(sa.getCardState());
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());
emptyAbility.setTargets(sa.getTargets().clone());
final CostPayment pay = new CostPayment(cost, sa);
return pay.payComputerCosts(new AiCostDecision(player, sa, true));
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
boolean result = ComputerUtil.playNoStack(player, emptyAbility, getGame(), true); // AI needs something to resolve to pay that cost
if (!emptyAbility.getPaidHash().isEmpty()) {
// report info to original sa (Argentum Masticore)
sa.setPaidHash(emptyAbility.getPaidHash());
}
return false;
return result;
}
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
// TODO logic for AI to pay rerolls and modification costs
return false;
}
@@ -1293,11 +1305,15 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
boolean optional = !tgtSA.getPayCosts().isMandatory();
boolean optional = tgtSA.hasParam("Optional");
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell spell) { // Isn't it ALWAYS a spell?
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA;
// TODO if mandatory AI is only forced to use mana when it's already in the pool
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
if (noManaCost) {
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
}
return ComputerUtil.playStack(tgtSA, player, getGame());
}
return false; // didn't play spell
@@ -1326,7 +1342,7 @@ public class PlayerControllerAi extends PlayerController {
// Probably want to see if the face up pile has anything "worth it", then potentially take face down pile
return pile1.size() >= pile2.size();
} else {
boolean allCreatures = IterableUtil.all(Iterables.concat(pile1, pile2), CardPredicates.CREATURES);
boolean allCreatures = Iterables.all(Iterables.concat(pile1, pile2), CardPredicates.Presets.CREATURES);
int cmc1 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile1) : ComputerUtilCard.evaluatePermanentList(pile1);
int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2);
@@ -1366,6 +1382,11 @@ public class PlayerControllerAi extends PlayerController {
return losses;
}
@Override
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean effect) {
return ComputerUtilMana.payManaCost(player, sa, effect);
}
@Override
public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise) {
final Player ai = sa.getActivatingPlayer();
@@ -1400,11 +1421,6 @@ public class PlayerControllerAi extends PlayerController {
return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, improvise);
}
@Override
public String chooseCardName(SpellAbility sa, List<ICardFace> faces, String message) {
return SpellApiToAi.Converter.get(sa).chooseCardName(player, sa, faces);
}
@Override
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) {
@@ -1419,14 +1435,14 @@ public class PlayerControllerAi extends PlayerController {
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
}
if (source != null && source.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
// If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name)
for (Card consp : player.getCardsIn(ZoneType.Command)) {
if (consp.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
String chosenName = consp.getNamedCard();
if (!chosenName.isEmpty()) {
aiLibrary = CardLists.filter(aiLibrary, CardPredicates.nameNotEquals(chosenName));
aiLibrary = CardLists.filter(aiLibrary, Predicates.not(CardPredicates.nameEquals(chosenName)));
}
}
}
@@ -1459,7 +1475,7 @@ public class PlayerControllerAi extends PlayerController {
}
} else {
CardCollectionView list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents());
list = CardLists.filter(list, CardPredicates.NON_LANDS);
list = CardLists.filter(list, Predicates.not(Presets.LANDS));
if (!list.isEmpty()) {
return list.get(0).getName();
}
@@ -1506,18 +1522,30 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
return SpellApiToAi.Converter.get(sa).chooseCardFace(player, sa, faces);
public String chooseCardName(SpellAbility sa, List<ICardFace> faces, String message) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces);
}
@Override
public ICardFace chooseSingleCardFace(SpellAbility sa, String message, Predicate<ICardFace> cpp, String name) {
throw new UnsupportedOperationException("Should not be called for AI"); // or implement it if you know how
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardFace(player, sa, faces);
}
@Override
public CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params) {
return SpellApiToAi.Converter.get(sa).chooseCardState(player, sa, states, params);
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardState(player, sa, states, params);
}
@Override
@@ -1537,13 +1565,8 @@ public class PlayerControllerAi extends PlayerController {
}
}
try {
// if this fail somehow add fallback to get any from dungeonCards
int i = MyRandom.getRandom().nextInt(dungeonNames.size());
return Card.fromPaperCard(dungeonCards.get(i), ai);
} catch (Exception e) {
return Card.fromPaperCard(Aggregates.random(dungeonCards), ai);
}
}
@Override
@@ -1569,7 +1592,32 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) {
return SpellApiToAi.Converter.get(chosen).chooseOptionalCosts(chosen, player, optionalCostValues);
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();
for (OptionalCostValue opt : optionalCostValues) {
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
Cost fullCost = opt.getCost().copy().add(costSoFar);
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
// Playability check for Kicker
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
SpellAbility kickedSaCopy = fullCostSa.copy();
kickedSaCopy.addOptionalCost(opt.getType());
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
copy.setCastSA(kickedSaCopy);
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
continue; // don't choose kickers we don't want to play
}
}
if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
}
}
return chosenOptCosts;
}
@Override
@@ -1609,11 +1657,6 @@ public class PlayerControllerAi extends PlayerController {
return max;
}
@Override
public List<CostPart> orderCosts(List<CostPart> costs) {
return costs;
}
@Override
public CardCollection chooseCardsForEffectMultiple(Map<String, CardCollection> validMap, SpellAbility sa, String title, boolean isOptional) {
CardCollection choices = new CardCollection();

View File

@@ -17,6 +17,7 @@
*/
package forge.ai;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi;
@@ -45,7 +46,9 @@ import forge.game.spellability.SpellPermanent;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.zone.ZoneType;
import forge.util.*;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount;
import org.apache.commons.lang3.tuple.Pair;
@@ -125,8 +128,8 @@ public class SpecialCardAi {
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
int numManaSrcs = manaSources.size();
CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.NON_TOKEN,
CardPredicates.NON_LANDS, CardPredicates.isOwner(ai)));
CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.Presets.NON_TOKEN,
Predicates.not(CardPredicates.Presets.LANDS), CardPredicates.isOwner(ai)));
int numHighCMC = CardLists.count(allCards, CardPredicates.greaterCMC(5));
int numLowCMC = CardLists.count(allCards, CardPredicates.lessCMC(3));
@@ -156,21 +159,21 @@ public class SpecialCardAi {
}
int libsize = ai.getCardsIn(ZoneType.Library).size();
final CardCollection hand = CardLists.filter(ai.getCardsIn(ZoneType.Hand),
CardPredicates.INSTANTS_AND_SORCERIES);
final CardCollection hand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.or(
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
if (!hand.isEmpty()) {
// has spell that can be cast in hand with put ability
if (hand.anyMatch(CardPredicates.hasCMC(counterNum + 1))) {
if (Iterables.any(hand, CardPredicates.hasCMC(counterNum + 1))) {
return false;
}
// has spell that can be cast if one counter is removed
if (hand.anyMatch(CardPredicates.hasCMC(counterNum))) {
if (Iterables.any(hand, CardPredicates.hasCMC(counterNum))) {
sa.setXManaCostPaid(1);
return true;
}
}
final CardCollection library = CardLists.filter(ai.getCardsIn(ZoneType.Library),
CardPredicates.INSTANTS_AND_SORCERIES);
final CardCollection library = CardLists.filter(ai.getCardsIn(ZoneType.Library), Predicates.or(
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
if (!library.isEmpty()) {
// get max cmc of instant or sorceries in the libary
int maxCMC = 0;
@@ -205,9 +208,9 @@ public class SpecialCardAi {
public static class ChainOfAcid {
public static boolean consider(final Player ai, final SpellAbility sa) {
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS);
CardPredicates.Presets.LANDS);
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
CardPredicates.NON_CREATURES);
Predicates.not(CardPredicates.Presets.CREATURES));
// TODO: improve this logic (currently the AI has difficulty evaluating non-creature permanents,
// which it can only distinguish by their CMC, considering >CMC higher value).
@@ -331,13 +334,13 @@ public class SpecialCardAi {
// Deathgorge Scavenger
public static class DeathgorgeScavenger {
public static boolean consider(final Player ai, final SpellAbility sa) {
Card worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
Card worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.NON_CREATURES));
Card worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
Card worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), Predicates.not(CardPredicates.Presets.CREATURES)));
if (worstCreat == null) {
worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
}
if (worstNonCreat == null) {
worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.NON_CREATURES));
worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Predicates.not(CardPredicates.Presets.CREATURES)));
}
sa.resetTargets();
@@ -360,8 +363,8 @@ public class SpecialCardAi {
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.UNTAPPED.and(
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH))));
Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(
CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH))));
boolean hasUsefulBlocker = false;
for (Card c : flyingCreatures) {
@@ -385,7 +388,7 @@ public class SpecialCardAi {
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
// first filter for opponents which can be targeted by SA
final Iterable<Player> oppList = IterableUtil.filter(ai.getOpponents(),
final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
// All opponents have hexproof or something like that
@@ -394,7 +397,7 @@ public class SpecialCardAi {
}
// filter for player who does not have donate target already
Iterable<Player> oppTarget = IterableUtil.filter(oppList,
Iterable<Player> oppTarget = Iterables.filter(oppList,
PlayerPredicates.isNotCardInPlay(donateTarget.getName()));
// fall back to previous list
if (Iterables.isEmpty(oppTarget)) {
@@ -403,7 +406,7 @@ public class SpecialCardAi {
// select player with less lands on the field (helpful for Illusions of Grandeur and probably Pacts too)
Player opp = Collections.min(Lists.newArrayList(oppTarget),
PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.LANDS));
PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.LANDS));
if (opp != null) {
sa.resetTargets();
@@ -582,9 +585,9 @@ public class SpecialCardAi {
Card bestBasic = null;
Card bestBasicSelfOnly = null;
CardCollection aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
CardCollection aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
CardCollection oppLands = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS_PRODUCING_MANA);
CardPredicates.Presets.LANDS_PRODUCING_MANA);
int bestCount = 0;
int bestSelfOnlyCount = 0;
@@ -630,7 +633,7 @@ public class SpecialCardAi {
}
CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield),
CardPredicates.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents()));
CardPredicates.Presets.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents()));
oppList = CardLists.filterPower(oppList, lowest.getNetPower() + 1);
if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) {
@@ -660,7 +663,7 @@ public class SpecialCardAi {
// Need to have something else in hand that is blue in addition to Force of Will itself,
// otherwise the AI will fail to play the card and the card will disappear from the pool
return false;
} else if (!blueCards.anyMatch(CardPredicates.lessCMC(3))) {
} else if (!Iterables.any(blueCards, CardPredicates.lessCMC(3))) {
// We probably need a low-CMC card to exile to it, exiling a higher CMC spell may be suboptimal
// since the AI does not prioritize/value cards vs. permission at the moment.
return false;
@@ -686,7 +689,7 @@ public class SpecialCardAi {
// Goblin Polka Band
public static class GoblinPolkaBand {
public static boolean consider(final Player ai, final SpellAbility sa) {
int maxPotentialTgts = ai.getOpponents().getCreaturesInPlay().filter(CardPredicates.UNTAPPED).size();
int maxPotentialTgts = Lists.newArrayList(Iterables.filter(ai.getOpponents().getCreaturesInPlay(), CardPredicates.Presets.UNTAPPED)).size();
int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false);
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
@@ -787,7 +790,7 @@ public class SpecialCardAi {
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(),
sa.getParamOrDefault("ChangeNum", "1"), sa);
CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library),
CardPredicates.nameNotEquals(sa.getHostCard().getName()));
Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName())));
lib.sort(CardLists.CmcComparatorInv);
// Additional cards which are difficult to auto-classify but which are generally good to Intuition for
@@ -865,7 +868,7 @@ public class SpecialCardAi {
for (Card c1 : lib) {
if (c1.getName().equals(c.getName())) {
if (!ai.getCardsIn(ZoneType.Hand).anyMatch(CardPredicates.nameEquals(c1.getName()))
if (!Iterables.any(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName()))
&& ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) {
// Try not to search for things we already have in hand or that we can't cast
libPriorityList.add(c1);
@@ -922,7 +925,7 @@ public class SpecialCardAi {
int aiBattlefieldPower = 0, aiGraveyardPower = 0;
int threshold = 320; // approximately a 4/4 Flying creature worth of extra value
CardCollection aiCreaturesInGY = CardLists.filter(ai.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES);
CardCollection aiCreaturesInGY = CardLists.filter(ai.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES);
if (aiCreaturesInGY.isEmpty()) {
// nothing in graveyard, so cut short
@@ -946,7 +949,7 @@ public class SpecialCardAi {
for (Card c : p.getCreaturesInPlay()) {
playerPower += ComputerUtilCard.evaluateCreature(c);
}
for (Card c : CardLists.filter(p.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES)) {
for (Card c : CardLists.filter(p.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES)) {
tempGraveyardPower += ComputerUtilCard.evaluateCreature(c);
}
if (playerPower > oppBattlefieldPower) {
@@ -981,7 +984,7 @@ public class SpecialCardAi {
for (Card gate : availableGates)
{
if (!currentGates.anyMatch(CardPredicates.nameEquals(gate.getName())))
if (!Iterables.any(currentGates, CardPredicates.nameEquals(gate.getName())))
{
// Diversify our mana base
return gate;
@@ -998,7 +1001,7 @@ public class SpecialCardAi {
// Scan the fetch list for a card with at least one activated ability.
// TODO: can be improved to a full consider(sa, ai) logic which would scan the graveyard first and hand last
public static Card considerCardFromList(final CardCollection fetchList) {
for (Card c : CardLists.filter(fetchList, CardPredicates.ARTIFACTS.or(CardPredicates.CREATURES))) {
for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) {
for (SpellAbility ab : c.getSpellAbilities()) {
if (ab.isActivatedAbility()) {
Player controller = c.getController();
@@ -1059,7 +1062,7 @@ public class SpecialCardAi {
// In MoJhoSto, prefer Jhoira sorcery ability from time to time
if (source.getGame().getRules().hasAppliedVariant(GameType.MoJhoSto)
&& CardLists.filter(ai.getLandsInPlay(), CardPredicates.UNTAPPED).size() >= 3) {
&& CardLists.filter(ai.getLandsInPlay(), CardPredicates.Presets.UNTAPPED).size() >= 3) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
int chanceToPrefJhoira = aic.getIntProperty(AiProps.MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR);
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA);
@@ -1131,7 +1134,7 @@ public class SpecialCardAi {
return false; // nothing to draw from the library
}
if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
if (Iterables.any(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
// Prefer Yawgmoth's Bargain because AI is generally better with it
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
@@ -1149,7 +1152,7 @@ public class SpecialCardAi {
}
// TODO: Any other bad effects like that?
boolean blackViseOTB = game.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Black Vise"));
boolean blackViseOTB = Iterables.any(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise"));
if (ph.getNextTurn().equals(ai) && ph.is(PhaseType.MAIN2)
&& ai.getSpellsCastLastTurn() == 0
@@ -1220,7 +1223,7 @@ public class SpecialCardAi {
final CardCollectionView cards = ai.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command));
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai);
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableManaSources(ai, true), CardPredicates.UNTAPPED).size();
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableManaSources(ai, true), CardPredicates.Presets.UNTAPPED).size();
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana();
@@ -1296,8 +1299,8 @@ public class SpecialCardAi {
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = sa.getParent().getTargetCard();
CardCollection candidates = ai.getOpponents().getCardsIn(ZoneType.Battlefield).filter(
CardPredicates.sharesCardTypeWith(firstTgt).and(CardPredicates.isTargetableBy(sa)));
Iterable<Card> candidates = Iterables.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
Predicates.and(CardPredicates.sharesCardTypeWith(firstTgt), CardPredicates.isTargetableBy(sa)));
Card secondTgt = Aggregates.random(candidates);
if (secondTgt != null) {
sa.resetTargets();
@@ -1317,7 +1320,7 @@ public class SpecialCardAi {
return false;
}
int aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
int aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.LANDS, Predicates.not(CardPredicates.Presets.BASIC_LANDS))).size();
boolean hasBridge = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
@@ -1335,7 +1338,7 @@ public class SpecialCardAi {
}
for (Player opp : ai.getOpponents()) {
int oppLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
int oppLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.LANDS, Predicates.not(CardPredicates.Presets.BASIC_LANDS))).size();
// Always if enemy would die and we don't!
// TODO : predict actual damage instead of assuming it'll be 2*lands
// Don't if we lose, unless we lose anyway to unblocked creatures next turn
@@ -1403,7 +1406,7 @@ public class SpecialCardAi {
public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollection oppTargetables = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
CardCollection threats = CardLists.filter(oppTargetables, card -> !ComputerUtilCard.isUselessCreature(card.getController(), card));
CardCollection ownTgts = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES);
CardCollection ownTgts = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES);
// TODO: improve the conditions for when the AI is considered threatened (check the possibility of being attacked?)
int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
@@ -1443,14 +1446,11 @@ public class SpecialCardAi {
public static boolean consider(final Player ai, final SpellAbility sa) {
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
CardPredicates.CREATURES
.and(CardPredicates.lessCMC(loyalty - 1))
.and(card -> {
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.lessCMC(loyalty - 1), card -> {
final Card copy = CardCopyService.getLKICopy(card);
ComputerUtilCard.applyStaticContPT(ai.getGame(), copy, null);
return copy.getNetToughness() > 0;
})
);
}));
CardLists.sortByCmcDesc(creaturesToGet);
if (creaturesToGet.isEmpty()) {
@@ -1469,7 +1469,6 @@ public class SpecialCardAi {
if (best != null) {
sa.resetTargets();
sa.getTargets().add(best);
sa.setXManaCostPaid(best.getCMC());
return true;
}
@@ -1486,9 +1485,9 @@ public class SpecialCardAi {
// face down (on the battlefield or in exile). Might need some kind of an update to consider hidden information
// like that properly (probably by adding all those cards to the evaluation mix so the AI doesn't "know" which
// ones are already face down in play and which are still in the library)
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
CardCollectionView creatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.CREATURES);
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS_PRODUCING_MANA);
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
CardCollectionView creatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES);
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
if (creatsInHand.isEmpty() || creatsInLib.isEmpty()) { return null; }
@@ -1557,10 +1556,10 @@ public class SpecialCardAi {
}
public static Card considerCardToGet(final Player ai, final SpellAbility sa) {
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
if (creatsInLib.isEmpty()) { return null; }
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS_PRODUCING_MANA);
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
int numManaSrcs = ComputerUtilMana.getAvailableManaEstimate(ai, false)
+ Math.min(1, manaSrcsInHand.size());
@@ -1604,8 +1603,8 @@ public class SpecialCardAi {
// The Scarab God
public static class TheScarabGod {
public static boolean consider(final Player ai, final SpellAbility sa) {
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
sa.resetTargets();
if (bestOppCreat != null) {
@@ -1706,7 +1705,7 @@ public class SpecialCardAi {
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
Card topGY = null;
Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand));
int numCreatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.CREATURES).size();
int numCreatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).size();
if (!aiGY.isEmpty()) {
topGY = ai.getCardsIn(ZoneType.Graveyard).get(0);
@@ -1765,7 +1764,7 @@ public class SpecialCardAi {
// check if +1 would be sufficient
if (single != null) {
// TODO use better logic to find the right Deal Damage Effect?
SpellAbility ugin_burn = IterableUtil.find(source.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.DealDamage), null);
SpellAbility ugin_burn = Iterables.find(source.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.DealDamage), null);
if (ugin_burn != null) {
// basic logic copied from DamageDealAi::dealDamageChooseTgtC
if (ugin_burn.canTarget(single)) {
@@ -1805,7 +1804,7 @@ public class SpecialCardAi {
int maxHandSize = ai.getMaxHandSize();
// TODO: Any other bad effects like that?
boolean blackViseOTB = game.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Black Vise"));
boolean blackViseOTB = Iterables.any(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise"));
// TODO: Consider effects like "whenever a player draws a card, he loses N life" (e.g. Nekusar, the Mindraiser),
// and effects that draw an additional card whenever a card is drawn.

View File

@@ -1,19 +1,13 @@
package forge.ai;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.card.ICardFace;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CardState;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
@@ -24,13 +18,14 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Base class for API-specific AI logic
@@ -86,9 +81,11 @@ public abstract class SpellAbilityAi {
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return false;
}
} else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
} else {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false;
}
}
if (!checkApiLogic(ai, sa)) {
return false;
@@ -122,7 +119,7 @@ public abstract class SpellAbilityAi {
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (aiLogic.equals("CheckCondition")) {
SpellAbility saCopy = sa.copy();
saCopy.setActivatingPlayer(ai);
saCopy.setActivatingPlayer(ai, true);
return saCopy.metConditions();
}
@@ -258,7 +255,7 @@ public abstract class SpellAbilityAi {
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
}
@@ -306,7 +303,7 @@ public abstract class SpellAbilityAi {
*/
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
final AbilitySub subAb = ab.getSubAbility();
return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
@@ -314,25 +311,6 @@ public abstract class SpellAbilityAi {
return true;
}
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payNever = "Never".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && isMine)) {
return false;
}
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
}
@SuppressWarnings("unchecked")
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
boolean hasPlayer = false;
@@ -342,9 +320,9 @@ public abstract class SpellAbilityAi {
for (T ent : options) {
if (ent instanceof Player) {
hasPlayer = true;
} else if (ent instanceof Card card) {
} else if (ent instanceof Card) {
hasCard = true;
if (card.isPlaneswalker() || card.isBattle()) {
if (((Card)ent).isPlaneswalker() || ((Card)ent).isBattle()) {
hasAttackableCard = true;
}
}
@@ -411,33 +389,4 @@ public abstract class SpellAbilityAi {
public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<String, Object> params) {
return MyRandom.getRandom().nextBoolean();
}
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, Player player, List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();
for (OptionalCostValue opt : optionalCostValues) {
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
Cost fullCost = opt.getCost().copy().add(costSoFar);
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
// Playability check for Kicker
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
SpellAbility kickedSaCopy = fullCostSa.copy();
kickedSaCopy.addOptionalCost(opt.getType());
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
copy.setCastSA(kickedSaCopy);
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
continue; // don't choose kickers we don't want to play
}
}
if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
}
}
return chosenOptCosts;
}
}

View File

@@ -1,15 +1,14 @@
package forge.ai;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import forge.ai.ability.*;
import forge.game.ability.ApiType;
import forge.game.spellability.SpellAbility;
import forge.util.ReflectionUtil;
import java.security.InvalidParameterException;
import java.util.Map;
public enum SpellApiToAi {
Converter;
@@ -23,14 +22,12 @@ public enum SpellApiToAi {
.put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class)
.put(ApiType.AddPhase, AddPhaseAi.class)
.put(ApiType.AddTurn, AddTurnAi.class)
.put(ApiType.AdvanceCrank, AdvanceCrankAi.class)
.put(ApiType.AlterAttribute, AlterAttributeAi.class)
.put(ApiType.Amass, AmassAi.class)
.put(ApiType.Animate, AnimateAi.class)
.put(ApiType.AnimateAll, AnimateAllAi.class)
.put(ApiType.Attach, AttachAi.class)
.put(ApiType.Ascend, AlwaysPlayAi.class)
.put(ApiType.AssembleContraption, AssembleContraptionAi.class)
.put(ApiType.AssignGroup, AssignGroupAi.class)
.put(ApiType.Balance, BalanceAi.class)
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
@@ -41,7 +38,6 @@ public enum SpellApiToAi {
.put(ApiType.Branch, BranchAi.class)
.put(ApiType.Camouflage, ChooseCardAi.class)
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
.put(ApiType.ChangeSpeed, AlwaysPlayAi.class)
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
.put(ApiType.ChangeX, AlwaysPlayAi.class)
.put(ApiType.ChangeZone, ChangeZoneAi.class)
@@ -57,9 +53,8 @@ public enum SpellApiToAi {
.put(ApiType.ChooseSector, AlwaysPlayAi.class)
.put(ApiType.ChooseSource, ChooseSourceAi.class)
.put(ApiType.ChooseType, ChooseTypeAi.class)
.put(ApiType.ClaimThePrize, AlwaysPlayAi.class)
.put(ApiType.Clash, ClashAi.class)
.put(ApiType.ClassLevelUp, ClassLevelUpAi.class)
.put(ApiType.ClassLevelUp, AlwaysPlayAi.class)
.put(ApiType.Cleanup, AlwaysPlayAi.class)
.put(ApiType.Cloak, CloakAi.class)
.put(ApiType.Clone, CloneAi.class)
@@ -88,7 +83,6 @@ public enum SpellApiToAi {
.put(ApiType.EachDamage, DamageEachAi.class)
.put(ApiType.Effect, EffectAi.class)
.put(ApiType.Encode, EncodeAi.class)
.put(ApiType.Endure, EndureAi.class)
.put(ApiType.EndCombatPhase, EndTurnAi.class)
.put(ApiType.EndTurn, EndTurnAi.class)
.put(ApiType.ExchangeLife, LifeExchangeAi.class)
@@ -131,7 +125,6 @@ public enum SpellApiToAi {
.put(ApiType.Mutate, MutateAi.class)
.put(ApiType.NameCard, ChooseCardNameAi.class)
//.put(ApiType.NoteCounters, AlwaysPlayAi.class)
.put(ApiType.OpenAttraction, AssembleContraptionAi.class)
.put(ApiType.PeekAndReveal, PeekAndRevealAi.class)
.put(ApiType.PermanentCreature, PermanentCreatureAi.class)
.put(ApiType.PermanentNoncreature, PermanentNoncreatureAi.class)
@@ -211,14 +204,6 @@ public enum SpellApiToAi {
.put(ApiType.InternalRadiation, AlwaysPlayAi.class)
.build());
public SpellAbilityAi get(final SpellAbility sa) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return get(api);
}
public SpellAbilityAi get(final ApiType api) {
SpellAbilityAi result = apiToInstance.get(api);
if (null == result) {

View File

@@ -1,5 +1,8 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -10,9 +13,6 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@Override

View File

@@ -17,15 +17,16 @@
*/
package forge.ai.ability;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* <p>

View File

@@ -1,30 +0,0 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class AdvanceCrankAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
int nextSprocket = (ai.getCrankCounter() % 3) + 1;
int crankCount = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isContraptionOnSprocket(nextSprocket));
//Could evaluate whether we actually want to crank those, but this is probably fine for now.
if(crankCount < 2)
return false;
return super.canPlayAI(ai, sa);
}
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if(logic.equals("AtOppEOT"))
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
}

View File

@@ -1,5 +1,8 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -10,9 +13,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
public class AlterAttributeAi extends SpellAbilityAi {
@Override

View File

@@ -1,13 +1,13 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import java.util.Map;
public class AlwaysPlayAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)

View File

@@ -1,13 +1,20 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.token.TokenInfo;
import forge.game.phase.PhaseHandler;
import forge.game.player.Player;
@@ -15,8 +22,6 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Map;
public class AmassAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
@@ -25,7 +30,7 @@ public class AmassAi extends SpellAbilityAi {
final Game game = ai.getGame();
if (!aiArmies.isEmpty()) {
return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
return Iterables.any(aiArmies, CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
}
final String type = sa.getParam("Type");
StringBuilder sb = new StringBuilder("b_0_0_");

View File

@@ -1,8 +1,8 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
import forge.card.CardType;
import forge.card.ColorSet;
@@ -13,9 +13,7 @@ import forge.game.ability.ApiType;
import forge.game.ability.effects.AnimateEffectBase;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostPutCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -24,10 +22,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import forge.util.collect.FCollectionView;
import java.util.Arrays;
import java.util.List;
@@ -74,7 +70,7 @@ public class AnimateAi extends SpellAbilityAi {
}
// check for duplicate static ability
if (host.getStaticAbilities().anyMatch(CardTraitPredicates.hasParam("Description", map.get("Description")))) {
if (Iterables.any(host.getStaticAbilities(), CardTraitPredicates.hasParam("Description", map.get("Description")))) {
return false;
}
// TODO check if Bone Man would deal damage to something that otherwise would regenerate
@@ -134,7 +130,7 @@ public class AnimateAi extends SpellAbilityAi {
&& game.getPhaseHandler().getNextTurn() != ai
&& source.isPermanent();
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
&& opponent.getZone(ZoneType.Battlefield).contains(CardPredicates.CREATURES)
&& opponent.getZone(ZoneType.Battlefield).contains(CardPredicates.Presets.CREATURES)
&& !sa.hasParam("AILogic") && !"Permanent".equals(sa.getParam("Duration")) && !activateAsPotentialBlocker) {
return false;
}
@@ -248,12 +244,9 @@ public class AnimateAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
if(animateTgtAI(sa))
return true;
else if (!mandatory)
if (sa.usesTargeting() && !animateTgtAI(sa) && !mandatory) {
return false;
else {
} else if (sa.usesTargeting() && mandatory) {
// fallback if animate is mandatory
sa.resetTargets();
List<Card> list = CardUtil.getValidCardsToTarget(sa);
@@ -264,7 +257,6 @@ public class AnimateAi extends SpellAbilityAi {
rememberAnimatedThisTurn(aiPlayer, toAnimate);
sa.getTargets().add(toAnimate);
}
}
return true;
}
@@ -287,6 +279,8 @@ public class AnimateAi extends SpellAbilityAi {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
// something is used for animate into creature
if (types.isCreature()) {
final Game game = ai.getGame();
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
@@ -298,8 +292,6 @@ public class AnimateAi extends SpellAbilityAi {
return false;
}
// something is used for animate into creature
if (types.isCreature()) {
Map<Card, Integer> data = Maps.newHashMap();
for (final Card c : list) {
// don't use Permanent animate on something that would leave the field
@@ -410,6 +402,7 @@ public class AnimateAi extends SpellAbilityAi {
if (logic.equals("ValuableAttackerOrBlocker")) {
if (ph.inCombat()) {
final Combat combat = ph.getCombat();
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
for (Card c : list) {
Card animated = becomeAnimated(c, sa);
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
@@ -419,26 +412,6 @@ public class AnimateAi extends SpellAbilityAi {
}
}
}
if (logic.equals("Worst")) {
Card worst = ComputerUtilCard.getWorstPermanentAI(list, false, false, false, false);
if(worst != null) {
sa.getTargets().add(worst);
rememberAnimatedThisTurn(ai, worst);
return true;
}
}
if (sa.hasParam("AITgts") && !list.isEmpty()) {
//No logic, but we do have preferences. Pick the best among those?
Card best = ComputerUtilCard.getBestAI(list);
if(best != null) {
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return true;
}
}
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until
@@ -563,7 +536,7 @@ public class AnimateAi extends SpellAbilityAi {
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
for (final StaticAbilityLayer layer : stAb.getLayers()) {
StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer);
}
@@ -604,12 +577,4 @@ public class AnimateAi extends SpellAbilityAi {
private void releaseHeldTillMain2(Player ai, Card c) {
AiCardMemory.forgetCard(ai, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
if (sa.isKeyword(Keyword.RIOT)) {
return !SpecialAiLogic.preferHasteForRiot(sa, payer);
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -1,109 +0,0 @@
package forge.ai.ability;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.List;
public class AssembleContraptionAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
//Pulls double duty as the OpenAttraction API. Same logic; usually good to do as long as we have the appropriate cards.
CardCollectionView deck = getDeck(ai, sa);
if(deck.isEmpty())
return false;
if(!super.canPlayAI(ai, sa))
return false;
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
xPay = Math.max(xPay, deck.size());
if (xPay == 0) {
return false;
}
sa.getRootAbility().setXManaCostPaid(xPay);
}
if(sa.hasParam("DefinedContraption") && sa.usesTargeting()) {
return getGoodReassembleTarget(ai, sa) != null;
}
return true;
}
private static CardCollectionView getDeck(Player ai, SpellAbility sa) {
return ai.getCardsIn(sa.getApi() == ApiType.OpenAttraction ?
ZoneType.AttractionDeck : ZoneType.ContraptionDeck);
}
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) {
return false;
}
sa.getRootAbility().setXManaCostPaid(xPay);
}
if(sa.hasParam("DefinedContraption") && sa.usesTargeting()) {
Card target = getGoodReassembleTarget(ai, sa);
if(target != null)
sa.getTargets().add(target);
else
return false;
}
return super.checkApiLogic(ai, sa);
}
private Card getGoodReassembleTarget(Player ai, SpellAbility sa) {
List<GameEntity> targets = sa.getTargetRestrictions().getAllCandidates(sa, true);
int nextSprocket = (ai.getCrankCounter() % 3) + 1;
return targets.stream()
.filter(e -> {
if(!(e instanceof Card))
return false;
Card c = (Card) e;
if(c.getController().isOpponentOf(ai))
return true;
return c.isContraption() && c.getSprocket() != nextSprocket;
}).map(c -> (Card) c)
.findFirst().orElse(null);
}
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if(logic.equals("AtOppEOT"))
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
return super.checkPhaseRestrictions(ai, sa, ph);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if(getDeck(aiPlayer, sa).isEmpty())
return false;
return super.chkAIDrawback(sa, aiPlayer);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if(!mandatory && getDeck(aiPlayer, sa).isEmpty())
return false;
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}
}

View File

@@ -1,13 +1,14 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class AssignGroupAi extends SpellAbilityAi {
protected boolean canPlayAI(Player ai, SpellAbility sa) {

View File

@@ -1,44 +1,51 @@
package forge.ai.ability;
import java.util.*;
import forge.game.card.*;
import org.apache.commons.lang3.ObjectUtils;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.*;
import forge.ai.AiAttackController;
import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
public class AttachAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -110,7 +117,7 @@ public class AttachAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
effectExile.setActivatingPlayer(ai);
effectExile.setActivatingPlayer(ai, true);
final List<Card> targets = CardUtil.getValidCardsToTarget(effectExile);
return !targets.isEmpty();
}
@@ -133,7 +140,7 @@ public class AttachAi extends SpellAbilityAi {
int power = 0, toughness = 0;
List<String> keywords = Lists.newArrayList();
for (StaticAbility stAb : source.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
if (stAb.hasParam("AddPower")) {
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
}
@@ -310,8 +317,9 @@ public class AttachAi extends SpellAbilityAi {
String type = "";
for (final StaticAbility stAb : attachSource.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddType")) {
type = stAb.getParam("AddType");
final Map<String, String> stab = stAb.getMapParams();
if (stab.get("Mode").equals("Continuous") && stab.containsKey("AddType")) {
type = stab.get("AddType");
}
}
@@ -373,39 +381,9 @@ public class AttachAi extends SpellAbilityAi {
*/
private static Card attachAIKeepTappedPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, final Card attachSource) {
// AI For Cards like Paralyzing Grasp and Glimmerdust Nap
// check for ETB Trigger
boolean tapETB = isAuraSpell(sa) && attachSource.getTriggers().anyMatch(t -> {
if (t.getMode() != TriggerType.ChangesZone) {
return false;
}
if (!ZoneType.Battlefield.toString().equals(t.getParam("Destination"))) {
return false;
}
if (t.hasParam("ValidCard") && !t.getParam("ValidCard").contains("Self")) {
return false;
}
SpellAbility tSa = t.ensureAbility();
if (tSa == null) {
return false;
}
if (!ApiType.Tap.equals(tSa.getApi())) {
return false;
}
if (!"Enchanted".equals(tSa.getParam("Defined"))) {
return false;
}
return true;
});
final List<Card> prefList = CardLists.filter(list, c -> {
// Don't do Untapped Vigilance cards
if (!tapETB && c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
return false;
}
@@ -420,10 +398,21 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
}
// already affected
if (!c.canUntap(c.getController(), true)) {
if (!c.isEnchanted()) {
return true;
}
final Iterable<Card> auras = c.getEnchantedBy();
for (Card aura : auras) {
SpellAbility auraSA = aura.getSpells().get(0);
if (auraSA.getApi() == ApiType.Attach) {
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) {
// Don't attach multiple KeepTapped Auras to one card
return false;
}
}
}
return true;
});
@@ -521,7 +510,7 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
evenBetterList = CardLists.filter(betterList, CardPredicates.UNTAPPED);
evenBetterList = CardLists.filter(betterList, CardPredicates.Presets.UNTAPPED);
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
@@ -570,46 +559,28 @@ public class AttachAi extends SpellAbilityAi {
final Card attachSource) {
// AI For choosing a Card to Animate.
final Player ai = sa.getActivatingPlayer();
Card attachSourceLki = null;
for (Trigger t : attachSource.getTriggers()) {
if (!t.getMode().equals(TriggerType.ChangesZone)) {
continue;
}
if (!"Battlefield".equals(t.getParam("Destination"))) {
continue;
}
if (!"Card.Self".equals(t.getParam("ValidCard"))) {
continue;
}
SpellAbility trigSa = t.ensureAbility();
SpellAbility animateSa = trigSa.findSubAbilityByType(ApiType.Animate);
if (animateSa == null) {
continue;
}
animateSa.setActivatingPlayer(sa.getActivatingPlayer());
attachSourceLki = AnimateAi.becomeAnimated(attachSource, animateSa);
}
if (attachSourceLki == null) {
return null;
}
final Card attachSourceLki = CardCopyService.getLKICopy(attachSource);
attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
final Card finalAttachSourceLki = attachSourceLki;
// Suppress original attach Spell to replace it with another
attachSourceLki.getFirstAttachSpell().setSuppressed(true);
//TODO for Reanimate Auras i need the new Attach Spell, in later versions it might be part of the Enchant Keyword
attachSourceLki.addSpellAbility(AbilityFactory.getAbility(attachSourceLki, "NewAttach"));
List<Card> betterList = CardLists.filter(list, c -> {
final Card lki = CardCopyService.getLKICopy(c);
// need to fake it as if lki would be on the battlefield
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
finalAttachSourceLki.clearRemembered();
finalAttachSourceLki.addRemembered(lki);
attachSourceLki.clearRemembered();
attachSourceLki.addRemembered(lki);
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(lki);
preList.add(finalAttachSourceLki);
preList.add(attachSourceLki);
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
boolean result = lki.canBeAttached(finalAttachSourceLki, null);
boolean result = lki.canBeAttached(attachSourceLki, null);
//reset static abilities
c.getGame().getAction().checkStaticAbilities(false);
@@ -834,45 +805,27 @@ public class AttachAi extends SpellAbilityAi {
int totPower = 0;
final List<String> keywords = new ArrayList<>();
boolean cantAttack = false;
boolean cantBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
if (stAbility.checkMode(StaticAbilityMode.CantAttack)) {
String valid = stAbility.getParam("ValidCard");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantAttack = true;
}
} else if (stAbility.checkMode(StaticAbilityMode.CantBlock)) {
String valid = stAbility.getParam("ValidCard");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantBlock = true;
}
} else if (stAbility.checkMode(StaticAbilityMode.CantBlockBy)) {
String valid = stAbility.getParam("ValidBlocker");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantBlock = true;
}
}
final Map<String, String> stabMap = stAbility.getMapParams();
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
if (!stabMap.get("Mode").equals("Continuous")) {
continue;
}
final String affected = stAbility.getParam("Affected");
final String affected = stabMap.get("Affected");
if (affected == null) {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), sa);
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
String kws = stAbility.getParam("AddKeyword");
String kws = stabMap.get("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
kws = stAbility.getParam("AddHiddenKeyword");
kws = stabMap.get("AddHiddenKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
@@ -908,16 +861,10 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, c -> c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c));
}
if (cantAttack) {
prefList = CardLists.filter(prefList, c -> c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c));
} else if (cantBlock) { // TODO better can block filter?
prefList = CardLists.filter(prefList, c -> c.isCreature() && !ComputerUtilCard.isUselessCreature(ai, c));
}
//some auras aren't useful in multiples
if (attachSource.hasSVar("NonStackingAttachEffect")) {
prefList = CardLists.filter(prefList,
CardPredicates.isEnchantedBy(attachSource.getName()).negate()
Predicates.not(CardPredicates.isEnchantedBy(attachSource.getName()))
);
}
@@ -988,10 +935,6 @@ public class AttachAi extends SpellAbilityAi {
return true;
}
private static boolean isAuraSpell(final SpellAbility sa) {
return sa.isSpell() && sa.getHostCard().isAura();
}
/**
* Attach preference.
*
@@ -1007,23 +950,7 @@ public class AttachAi extends SpellAbilityAi {
*/
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
GameObject o;
boolean spellCanTargetPlayer = false;
if (isAuraSpell(sa)) {
Card source = sa.getHostCard();
if (!source.hasKeyword(Keyword.ENCHANT)) {
return false;
}
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
String ko = ki.getOriginal();
String m[] = ko.split(":");
String v = m[1];
if (v.contains("Player") || v.contains("Opponent")) {
spellCanTargetPlayer = true;
break;
}
}
}
if (tgt.canTgtPlayer() && (!isAuraSpell(sa) || spellCanTargetPlayer)) {
if (tgt.canTgtPlayer()) {
List<Player> targetable = new ArrayList<>();
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) {
@@ -1088,8 +1015,9 @@ public class AttachAi extends SpellAbilityAi {
CardCollection toRemove = new CardCollection();
for (Trigger t : attachSource.getTriggers()) {
if (t.getMode() == TriggerType.ChangesZone) {
if ("Card.Self".equals(t.getParam("ValidCard"))
&& "Battlefield".equals(t.getParam("Destination"))) {
final Map<String, String> params = t.getMapParams();
if ("Card.Self".equals(params.get("ValidCard"))
&& "Battlefield".equals(params.get("Destination"))) {
SpellAbility trigSa = t.ensureAbility();
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
for (Card target : list) {
@@ -1149,27 +1077,29 @@ public class AttachAi extends SpellAbilityAi {
boolean grantingExtraBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
final Map<String, String> stabMap = stAbility.getMapParams();
if (!"Continuous".equals(stabMap.get("Mode"))) {
continue;
}
final String affected = stAbility.getParam("Affected");
final String affected = stabMap.get("Affected");
if (affected == null) {
continue;
}
if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), stAbility);
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
grantingAbilities |= stAbility.hasParam("AddAbility");
grantingExtraBlock |= stAbility.hasParam("CanBlockAmount") || stAbility.hasParam("CanBlockAny");
grantingAbilities |= stabMap.containsKey("AddAbility");
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
String kws = stAbility.getParam("AddKeyword");
String kws = stabMap.get("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
kws = stAbility.getParam("AddHiddenKeyword");
kws = stabMap.get("AddHiddenKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
@@ -1213,10 +1143,10 @@ public class AttachAi extends SpellAbilityAi {
//some auras/equipments aren't useful in multiples
if (attachSource.hasSVar("NonStackingAttachEffect")) {
prefList = CardLists.filter(prefList, Predicate.not(
CardPredicates.isEquippedBy(attachSource.getName())
.or(CardPredicates.isEnchantedBy(attachSource.getName()))
));
prefList = CardLists.filter(prefList, Predicates.not(Predicates.or(
CardPredicates.isEquippedBy(attachSource.getName()),
CardPredicates.isEnchantedBy(attachSource.getName())
)));
}
// Don't pump cards that will die.
@@ -1238,17 +1168,12 @@ public class AttachAi extends SpellAbilityAi {
// TODO Somehow test for definitive advantage (e.g. opponent low on health, AI is attacking)
// to be able to deal the final blow with an enchanted vehicle like that
boolean canOnlyTargetCreatures = true;
if (attachSource.isAura()) {
for (KeywordInterface ki : attachSource.getKeywords(Keyword.ENCHANT)) {
String o = ki.getOriginal();
String m[] = o.split(":");
String v = m[1];
if (!v.startsWith("Creature")) {
for (String valid : ObjectUtils.firstNonNull(attachSource.getFirstAttachSpell(), sa).getTargetRestrictions().getValidTgts()) {
if (!valid.startsWith("Creature")) {
canOnlyTargetCreatures = false;
break;
}
}
}
if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) {
prefList = CardLists.filter(prefList, c -> c.getTimesCrewedThisTurn() == 0 || (attachSource.isEquipment() && attachSource.getGame().getPhaseHandler().is(PhaseType.MAIN1, ai)));
}
@@ -1322,8 +1247,8 @@ public class AttachAi extends SpellAbilityAi {
// Is a SA that moves target attachment
if ("MoveTgtAura".equals(sa.getParam("AILogic"))) {
CardCollection list = CardLists.filter(CardUtil.getValidCardsToTarget(sa), CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents())
.or(card -> ComputerUtilCard.isUselessCreature(aiPlayer, card.getAttachedTo())));
CardCollection list = CardLists.filter(CardUtil.getValidCardsToTarget(sa), Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()),
card -> ComputerUtilCard.isUselessCreature(aiPlayer, card.getAttachedTo())));
return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null;
} else if ("Unenchanted".equals(sa.getParam("AILogic"))) {
@@ -1472,6 +1397,8 @@ public class AttachAi extends SpellAbilityAi {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
} else if ("ChangeType".equals(logic)) {
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
} else if ("KeepTapped".equals(logic)) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
} else if ("Animate".equals(logic)) {
c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource);
} else if ("Reanimate".equals(logic)) {
@@ -1482,12 +1409,6 @@ public class AttachAi extends SpellAbilityAi {
c = attachAIHighestEvaluationPreference(prefList);
}
if (isAuraSpell(sa)) {
if (attachSource.getReplacementEffects().anyMatch(re -> re.getMode().equals(ReplacementType.Untap) && re.getLayer().equals(ReplacementLayer.CantHappen))) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
}
}
// Consider exceptional cases which break the normal evaluation rules
if (!isUsefulAttachAction(ai, c, sa)) {
return null;
@@ -1640,6 +1561,8 @@ public class AttachAi extends SpellAbilityAi {
} 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.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
}
return true;
}

View File

@@ -25,10 +25,10 @@ public class BalanceAi extends SpellAbilityAi {
if ("BalanceCreaturesAndLands".equals(logic)) {
// TODO Copied over from hardcoded Balance. We should be checking value of the lands/creatures for each opponent, not just counting
diff += CardLists.filter(humPerms, CardPredicates.LANDS).size() -
CardLists.filter(compPerms, CardPredicates.LANDS).size();
diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.CREATURES).size() -
CardLists.filter(compPerms, CardPredicates.CREATURES).size());
diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() -
CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size();
diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() -
CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size());
}
else if ("BalancePermanents".equals(logic)) {
// Don't cast if you have to sacrifice permanents

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.List;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
@@ -12,8 +14,6 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
public class BidLifeAi extends SpellAbilityAi {
@Override

View File

@@ -17,14 +17,14 @@
*/
package forge.ai.ability;
import java.util.Map;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.Map;
/**
* <p>
* AbilityFactoryBond class.

View File

@@ -1,5 +1,8 @@
package forge.ai.ability;
import java.util.Collection;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.player.Player;
@@ -7,9 +10,6 @@ import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import java.util.Collection;
import java.util.Map;
public class ChangeCombatantsAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -13,6 +14,7 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
@@ -22,13 +24,12 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
@@ -137,6 +138,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic != null) {
if (aiLogic.equals("Always")) {
return true;
} else if (aiLogic.startsWith("ExileSpell")) {
return doExileSpellLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
@@ -371,7 +374,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// remove cards that won't be seen if library can't be searched
if (!ai.canSearchLibraryWith(sa, p)) {
list = CardLists.filter(list, CardPredicates.inZone(ZoneType.Library).negate());
list = CardLists.filter(list, Predicates.not(CardPredicates.inZone(ZoneType.Library)));
}
if (type != null && p == ai) {
@@ -452,7 +455,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb);
}
/**
@@ -614,8 +617,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
// pick dual lands if available
if (result.stream().anyMatch(CardPredicates.NONBASIC_LANDS)) {
result = CardLists.filter(result, CardPredicates.NONBASIC_LANDS);
if (Iterables.any(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS))) {
result = CardLists.filter(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS));
}
return result.get(0);
@@ -770,7 +773,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb);
}
/*
@@ -818,7 +821,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
//don't unearth after attacking is possible
if (sa.isKeyword(Keyword.UNEARTH) && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
if (sa.hasParam("Unearth") && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
@@ -875,10 +878,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone")));
}
if (origin.contains(ZoneType.Stack) && doExileSpellLogic(ai, sa, mandatory)) {
return true;
}
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final Game game = ai.getGame();
@@ -903,20 +902,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
}
if (source.isInZone(ZoneType.Hand)) {
list = CardLists.filter(list, CardPredicates.nameNotEquals(source.getName())); // Don't get the same card back.
list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(source.getName()))); // Don't get the same card back.
}
if (sa.isSpell()) {
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
}
// list = CardLists.canSubsequentlyTarget(list, sa);
if (sa.hasParam("AttachedTo")) {
list = CardLists.filter(list, c -> {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
@@ -944,10 +941,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
immediately = immediately || ComputerUtil.playImmediately(ai, sa);
if (list.isEmpty() && immediately && sa.getMaxTargets() == 0) {
return true;
}
// Narrow down the list:
if (origin.contains(ZoneType.Battlefield)) {
if ("Polymorph".equals(sa.getParam("AILogic"))) {
@@ -1023,7 +1016,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean saheeliFelidarCombo = ComputerUtilAbility.getAbilitySourceName(sa).equals("Felidar Guardian")
&& tobounce.getName().equals("Saheeli Rai")
&& CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Felidar Guardian")).size() <
CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.CREATURES).size() + ai.getOpponentsGreatestLifeTotal() + 10;
CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Creature")).size() + ai.getOpponentsGreatestLifeTotal() + 10;
// remember that the card was bounced already unless it's a special combo case
if (!saheeliFelidarCombo) {
@@ -1207,7 +1200,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
List<Card> nonLands = CardLists.getNotType(list, "Land");
// Prefer to pull a creature, generally more useful for AI.
choice = chooseCreature(ai, CardLists.filter(nonLands, CardPredicates.CREATURES));
choice = chooseCreature(ai, CardLists.filter(nonLands, CardPredicates.Presets.CREATURES));
if (choice == null) { // Could not find a creature.
if (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon.
@@ -1285,10 +1278,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
list.remove(choice);
if (sa.canTarget(choice)) {
sa.getTargets().add(choice);
}
}
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
@@ -1321,7 +1312,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
Game game = ai.getGame();
// filter out untargetables
CardCollectionView aiPermanents = CardLists.filterControlledBy(list, ai);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, CardPredicates.PLANESWALKERS);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
// Felidar Guardian + Saheeli Rai combo support
if (sa.getHostCard().getName().equals("Felidar Guardian")) {
@@ -1347,7 +1338,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
else if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
Combat combat = game.getCombat();
final CardCollection combatants = CardLists.filter(aiPermanents,
CardPredicates.CREATURES);
CardPredicates.Presets.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants);
for (final Card c : combatants) {
@@ -1427,10 +1418,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
}
if (bestChoice != null) {
return bestChoice;
}
return null;
}
private static boolean isUnpreferredTarget(final Player ai, final SpellAbility sa, final boolean mandatory) {
if (!mandatory) {
if (!"Always".equals(sa.getParam("AILogic"))) {
@@ -1453,9 +1447,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
// AI Targeting
Card choice = null;
// Filter out cards TargetsForEachPlayer
list = CardLists.canSubsequentlyTarget(list, sa);
if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
if (mostExpensivePermanent.isCreature()
@@ -1467,7 +1458,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
List<Card> nonLands = CardLists.getNotType(list, "Land");
// Prefer to pull a creature, generally more useful for AI.
choice = chooseCreature(ai, CardLists.filter(nonLands, CardPredicates.CREATURES));
choice = chooseCreature(ai, CardLists.filter(nonLands, CardPredicates.Presets.CREATURES));
if (choice == null) { // Could not find a creature.
if (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon.
@@ -1579,8 +1570,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
} else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList);
} else if (logic.equals("BounceOwnTrigger")) {
return doBounceOwnTriggerLogic(decider, fetchList);
}
}
if (fetchList.isEmpty()) {
@@ -1643,14 +1632,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
} else {
// Don't fetch another tutor with the same name
CardCollection sameNamed = CardLists.filter(fetchList, CardPredicates.nameNotEquals(ComputerUtilAbility.getAbilitySourceName(sa)));
CardCollection sameNamed = CardLists.filter(fetchList, Predicates.not(CardPredicates.nameEquals(ComputerUtilAbility.getAbilitySourceName(sa))));
if (origin.contains(ZoneType.Library) && !sameNamed.isEmpty()) {
fetchList = sameNamed;
}
// Does AI need a land?
CardCollectionView hand = decider.getCardsIn(ZoneType.Hand);
if (!hand.anyMatch(CardPredicates.LANDS) && CardLists.count(decider.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS) < 4) {
if (!Iterables.any(hand, Presets.LANDS) && CardLists.count(decider.getCardsIn(ZoneType.Battlefield), Presets.LANDS) < 4) {
boolean canCastSomething = false;
for (Card cardInHand : hand) {
canCastSomething = canCastSomething || ComputerUtilMana.hasEnoughManaSourcesToCast(cardInHand.getFirstSpellAbility(), decider);
@@ -1660,13 +1649,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (c == null) {
if (fetchList.allMatch(CardPredicates.LANDS)) {
if (Iterables.all(fetchList, Presets.LANDS)) {
// we're only choosing from lands, so get the best land
c = ComputerUtilCard.getBestLandAI(fetchList);
} else {
fetchList = CardLists.getNotType(fetchList, "Land");
// Prefer to pull a creature, generally more useful for AI.
c = chooseCreature(decider, CardLists.filter(fetchList, CardPredicates.CREATURES));
c = chooseCreature(decider, CardLists.filter(fetchList, CardPredicates.Presets.CREATURES));
}
}
if (c == null) { // Could not find a creature.
@@ -1784,7 +1773,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
CardCollection listToSac = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), definedSac, ai, source, sa);
listToSac.sort(Collections.reverseOrder(CardLists.CmcComparatorInv));
CardCollection listToRet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES);
CardCollection listToRet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Presets.CREATURES);
listToRet.sort(CardLists.CmcComparatorInv);
if (!listToSac.isEmpty() && !listToRet.isEmpty()) {
@@ -1975,7 +1964,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (logic.contains("NonLand")) {
scanList = CardLists.filter(scanList, CardPredicates.NON_LANDS);
scanList = CardLists.filter(scanList, Predicates.not(Presets.LANDS));
}
if (logic.contains("NonExiled")) {
@@ -2069,26 +2058,33 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
private static boolean doExileSpellLogic(final Player ai, final SpellAbility sa, final boolean mandatory) {
List<ApiType> dangerousApi = null;
CardCollection spells = new CardCollection(ai.getGame().getStackZone().getCards());
Collections.reverse(spells);
if (!mandatory && !spells.isEmpty()) {
spells = spells.subList(0, 1);
spells = ComputerUtil.filterAITgts(sa, ai, spells, true);
dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
private boolean doExileSpellLogic(final Player aiPlayer, final SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
SpellAbilityStackInstance top = aiPlayer.getGame().getStack().peek();
List<ApiType> dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
int manaCost = 0;
int minCost = 0;
if (aiLogic.contains(".")) {
minCost = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
}
for (Card c : spells) {
SpellAbility topSA = ai.getGame().getStack().getSpellMatchingHost(c);
if (topSA != null && (dangerousApi == null ||
(dangerousApi.contains(topSA.getApi()) && topSA.getActivatingPlayer().isOpponentOf(ai)))
&& sa.canTarget(topSA)) {
if (top != null) {
SpellAbility topSA = top.getSpellAbility();
if (topSA != null) {
if (topSA.getPayCosts().hasManaCost()) {
manaCost = topSA.getPayCosts().getTotalMana().getCMC();
}
if ((manaCost >= minCost || dangerousApi.contains(topSA.getApi()))
&& topSA.getActivatingPlayer().isOpponentOf(aiPlayer)
&& sa.canTargetSpellAbility(topSA)) {
sa.resetTargets();
sa.getTargets().add(topSA);
return sa.isTargetNumberValid();
}
}
}
return false;
}
@@ -2134,47 +2130,4 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static boolean isBouncedThisTurn(Player ai, Card c) {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
}
private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
if (!prefChoices.isEmpty()) {
return ComputerUtilCard.getBestAI(prefChoices);
} else if (!unprefChoices.isEmpty()) {
return ComputerUtilCard.getWorstAI(unprefChoices);
} else {
return null;
}
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card host = sa.getHostCard();
int lifeLoss = 0;
if (cost.hasSpecificCostType(CostDamage.class)) {
if (!payer.canLoseLife()) {
return true;
}
CostDamage damageCost = cost.getCostPartByType(CostDamage.class);
lifeLoss = ComputerUtilCombat.predictDamageTo(payer, damageCost.getAbilityAmount(sa), host, false);
if (lifeLoss == 0) {
return true;
}
} else if (cost.hasSpecificCostType(CostPayLife.class)) {
CostPayLife lifeCost = cost.getCostPartByType(CostPayLife.class);
lifeLoss = lifeCost.getAbilityAmount(sa);
}
for (Card c : AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa)) {
if (c.isToken()) {
return false;
}
if (!c.isCreature() || c.getBasePower() < lifeLoss || payer.getLife() < lifeLoss * 2) { // costs use either pay 3 life or deal 3 damage
return false;
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -1,9 +1,28 @@
package forge.ai.ability;
import forge.ai.*;
import java.util.Collections;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.AiController;
import forge.ai.AiPlayerPredicates;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -14,9 +33,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.Collections;
import java.util.Map;
public class ChangeZoneAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -77,7 +93,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} else if ("ExileGraveyards".equals(aiLogic)) {
for (Player opp : ai.getOpponents()) {
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.CREATURES);
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES);
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
return true;
@@ -92,7 +108,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
Player bestTgt = null;
if (player.canBeTargetedBy(sa)) {
int numGY = CardLists.count(player.getCardsIn(ZoneType.Graveyard),
CardPredicates.CREATURES);
CardPredicates.Presets.CREATURES);
if (numGY > maxSize) {
maxSize = numGY;
bestTgt = player;
@@ -338,7 +354,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
// there is no specific AI to support playing it in a smarter way. Feel free to expand.
return ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
return Iterables.any(ai.getOpponents().getCardsIn(origin), CardPredicates.Presets.CREATURES);
}
CardCollectionView humanType = ai.getOpponents().getCardsIn(origin);

View File

@@ -1,7 +1,16 @@
package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.AiController;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
@@ -12,10 +21,6 @@ import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
@@ -32,14 +37,13 @@ public class CharmAi extends SpellAbilityAi {
}
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
boolean choiceForOpp = !ai.equals(sa.getActivatingPlayer());
// Reset the chosen list otherwise it will be locked in forever by earlier calls
sa.setChosenList(null);
sa.setSubAbility(null);
List<AbilitySub> chosenList;
if (choiceForOpp) {
if (!ai.equals(sa.getActivatingPlayer())) {
// This branch is for "An Opponent chooses" Charm spells from Alliances
// Current just choose the first available spell, which seem generally less disastrous for the AI.
chosenList = choices.subList(1, choices.size());
@@ -79,11 +83,6 @@ public class CharmAi extends SpellAbilityAi {
// store the choices so they'll get reused
sa.setChosenList(chosenList);
if (choiceForOpp) {
return true;
}
if (sa.isSpell()) {
// prebuild chain to improve cost calculation accuracy
CharmEffect.chainAbilities(sa, chosenList);
@@ -93,7 +92,8 @@ public class CharmAi extends SpellAbilityAi {
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
}
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num,
int min) {
List<AbilitySub> chosenList = Lists.newArrayList();
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
boolean allowRepeat = sa.hasParam("CanRepeatModes"); // FIXME: unused for now, the AI doesn't know how to effectively handle repeated choices
@@ -107,15 +107,16 @@ public class CharmAi extends SpellAbilityAi {
// First pass using standard canPlayAi() for good choices
for (AbilitySub sub : choices) {
sub.setActivatingPlayer(ai);
sub.setActivatingPlayer(ai, true);
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
if (pawprintLimit > 0) {
int curPawprintAmount = AbilityUtils.calculateAmount(sub.getHostCard(), sub.getParamOrDefault("Pawprint", "0"), sub);
if (pawprintAmount + curPawprintAmount > pawprintLimit) {
continue;
}
} else {
pawprintAmount += curPawprintAmount;
}
}
chosenList.add(sub);
if (chosenList.size() == num) {
return chosenList; // maximum choices reached
@@ -245,13 +246,13 @@ public class CharmAi extends SpellAbilityAi {
List<AbilitySub> chosenList = Lists.newArrayList();
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
for (AbilitySub sub : choices) {
sub.setActivatingPlayer(ai);
sub.setActivatingPlayer(ai, true);
// Assign generic good choice to fill up choices if necessary
if ("Good".equals(sub.getParam("AILogic")) && aic.doTrigger(sub, false)) {
goodChoice = sub;
} else {
// Standard canPlayAi()
sub.setActivatingPlayer(ai);
sub.setActivatingPlayer(ai, true);
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == min) {

View File

@@ -1,10 +1,12 @@
package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.game.Game;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
@@ -14,7 +16,6 @@ import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.IterableUtil;
import java.util.Collections;
import java.util.List;
@@ -181,8 +182,8 @@ public class ChooseCardAi extends SpellAbilityAi {
}
choice = ComputerUtilCard.getBestAI(ownChoices);
} else if (logic.equals("BestBlocker")) {
if (IterableUtil.any(options, CardPredicates.UNTAPPED)) {
options = CardLists.filter(options, CardPredicates.UNTAPPED);
if (Iterables.any(options, Presets.UNTAPPED)) {
options = CardLists.filter(options, Presets.UNTAPPED);
}
choice = ComputerUtilCard.getBestCreatureAI(options);
} else if (logic.equals("Clone")) {
@@ -219,7 +220,7 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = ComputerUtilCard.getWorstAI(aiControlled);
}
} else if ("LowestCMCCreature".equals(logic)) {
CardCollection creats = CardLists.filter(options, CardPredicates.CREATURES);
CardCollection creats = CardLists.filter(options, Presets.CREATURES);
creats = CardLists.filterToughness(creats, 1);
if (creats.isEmpty()) {
choice = ComputerUtilCard.getWorstAI(options);
@@ -271,10 +272,10 @@ public class ChooseCardAi extends SpellAbilityAi {
// might also be good to do a separate AI for Noble Heritage
}
} else if (logic.equals("Phylactery")) {
CardCollection aiArtifacts = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.ARTIFACTS);
CardCollection aiArtifacts = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS);
CardCollection indestructibles = CardLists.filter(aiArtifacts, CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE));
CardCollection nonCreatures = CardLists.filter(aiArtifacts, CardPredicates.NON_CREATURES);
CardCollection creatures = CardLists.filter(aiArtifacts, CardPredicates.CREATURES);
CardCollection nonCreatures = CardLists.filter(aiArtifacts, Predicates.not(Presets.CREATURES));
CardCollection creatures = CardLists.filter(aiArtifacts, Presets.CREATURES);
if (!indestructibles.isEmpty()) {
// Choose the worst (smallest) indestructible artifact so that the opponent would have to waste
// removal on something unpreferred

View File

@@ -1,10 +1,22 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.StaticData;
import forge.ai.*;
import forge.card.*;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.card.CardDb;
import forge.card.CardRules;
import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.card.ICardFace;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCopyService;
@@ -16,9 +28,6 @@ import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class ChooseCardNameAi extends SpellAbilityAi {
@Override

View File

@@ -1,6 +1,11 @@
package forge.ai.ability;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.card.CardCollectionView;
@@ -63,7 +68,7 @@ public class ChooseColorAi extends SpellAbilityAi {
// activate in Main 2 hoping that the extra mana surplus will make a difference
// if there are some nonland permanents in hand
CardCollectionView permanents = CardLists.filter(ai.getCardsIn(ZoneType.Hand),
CardPredicates.NONLAND_PERMANENTS);
CardPredicates.Presets.NONLAND_PERMANENTS);
return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai);
}

View File

@@ -1,15 +1,16 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class ChooseCompanionAi extends SpellAbilityAi {
/* (non-Javadoc)

View File

@@ -6,7 +6,7 @@ import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
@@ -25,7 +25,7 @@ public class ChooseDirectionAi extends SpellAbilityAi {
return false;
} else {
if ("Aminatou".equals(logic)) {
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.NONLAND_PERMANENTS);
CardCollection aiPermanent = CardLists.filterControlledBy(all, ai);
aiPermanent.remove(sa.getHostCard());
int aiValue = Aggregates.sum(aiPermanent, Card::getCMC);

View File

@@ -1,11 +1,11 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -18,7 +18,6 @@ import forge.util.collect.FCollection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ChooseGenericAi extends SpellAbilityAi {
@@ -29,7 +28,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) {
if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) {
return true;
}
}
@@ -75,7 +74,8 @@ public class ChooseGenericAi extends SpellAbilityAi {
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
Card host = sa.getHostCard();
final Game game = host.getGame();
final String logic = sa.getParam("AILogic");
@@ -84,17 +84,17 @@ public class ChooseGenericAi extends SpellAbilityAi {
} else if ("Random".equals(logic)) {
return Aggregates.random(spells);
} else if ("Phasing".equals(logic)) { // Teferi's Realm : keep aggressive
List<SpellAbility> filtered = spells.stream()
.filter(sp -> !sp.getDescription().contains("Creature") && !sp.getDescription().contains("Land"))
.collect(Collectors.toList());
List<SpellAbility> filtered = Lists.newArrayList(Iterables.filter(spells, sp -> !sp.getDescription().contains("Creature") && !sp.getDescription().contains("Land")));
return Aggregates.random(filtered);
} else if ("PayUnlessCost".equals(logic)) {
for (final SpellAbility sp : spells) {
String unlessCost = sp.getParam("UnlessCost");
sp.setActivatingPlayer(sa.getActivatingPlayer());
sp.setActivatingPlayer(sa.getActivatingPlayer(), true);
Cost unless = new Cost(unlessCost, false);
if (SpellApiToAi.Converter.get(sp).willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
&& ComputerUtilCost.canPayCost(unless, sp, player, true)) {
SpellAbility paycost = new SpellAbility.EmptySa(sa.getHostCard(), player);
paycost.setPayCosts(unless);
if (ComputerUtilCost.willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
&& ComputerUtilCost.canPayCost(paycost, player, true)) {
return sp;
}
}
@@ -161,10 +161,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
}
}
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.DRAW)) {
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) {
return skipDraw;
}
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.COMBAT_BEGIN)) {
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) {
return skipCombat;
}
@@ -262,7 +262,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
List<SpellAbility> filtered = Lists.newArrayList();
// filter first for the spells which can be done
for (SpellAbility sp : spells) {
if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) {
if (SpellApiToAi.Converter.get(sp.getApi()).canPlayAIWithSubs(player, sp)) {
filtered.add(sp);
}
}

View File

@@ -1,7 +1,11 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
@@ -10,9 +14,6 @@ import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.List;
import java.util.Map;
public class ChoosePlayerAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {

View File

@@ -1,6 +1,11 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
@@ -22,10 +27,6 @@ import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
public class ChooseSourceAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -134,14 +135,10 @@ public class ChooseSourceAi extends SpellAbilityAi {
}
// No optimal creature was found above, so try to broaden the choice.
if (!Iterables.isEmpty(options)) {
List<Card> oppCreatures = CardLists.filter(options, Predicate.not(
CardPredicates.CREATURES.and(CardPredicates.isOwner(aiChoser))
));
List<Card> aiNonCreatures = CardLists.filter(options,
CardPredicates.NON_CREATURES
.and(CardPredicates.PERMANENTS)
.and(CardPredicates.isOwner(aiChoser))
);
List<Card> oppCreatures = CardLists.filter(options, Predicates.and(CardPredicates.Presets.CREATURES,
Predicates.not(CardPredicates.isOwner(aiChoser))));
List<Card> aiNonCreatures = CardLists.filter(options, Predicates.and(Predicates.not(CardPredicates.Presets.CREATURES),
CardPredicates.Presets.PERMANENTS, CardPredicates.isOwner(aiChoser)));
if (!oppCreatures.isEmpty()) {
return ComputerUtilCard.getBestCreatureAI(oppCreatures);

View File

@@ -1,11 +1,24 @@
package forge.ai.ability;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.ai.AiCardMemory;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardType;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -14,11 +27,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ChooseTypeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
@@ -57,7 +65,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
int avgPower = 0;
// predict the opposition
CardCollection oppCreatures = CardLists.filter(aiPlayer.getOpponents().getCreaturesInPlay(), CardPredicates.UNTAPPED);
CardCollection oppCreatures = CardLists.filter(aiPlayer.getOpponents().getCreaturesInPlay(), CardPredicates.Presets.UNTAPPED);
int maxOppPower = 0;
int maxOppToughness = 0;
int oppUsefulCreatures = 0;
@@ -77,7 +85,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
if (maxX > 1) {
CardCollection cre = CardLists.filter(aiPlayer.getCardsIn(ZoneType.Battlefield),
CardPredicates.isType(chosenType), CardPredicates.UNTAPPED);
CardPredicates.isType(chosenType), CardPredicates.Presets.UNTAPPED);
if (!cre.isEmpty()) {
for (Card c: cre) {
avgPower += c.getNetPower();

View File

@@ -1,7 +1,10 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -14,8 +17,6 @@ import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Map;
public class ClashAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -92,7 +93,7 @@ public class ClashAi extends SpellAbilityAi {
// Springjack Knight
// TODO: Whirlpool Whelm also uses creature targeting but it's trickier to support
CardCollectionView aiCreats = ai.getCreaturesInPlay();
CardCollectionView oppCreats = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.CREATURES);
CardCollectionView oppCreats = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
Card tgt = aiCreats.isEmpty() ? ComputerUtilCard.getWorstCreatureAI(oppCreats) : ComputerUtilCard.getBestCreatureAI(aiCreats);

View File

@@ -1,36 +0,0 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
public class ClassLevelUpAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
Card host = sa.getHostCard();
final int level = host.getClassLevel() + 1;
for (StaticAbility stAb : host.getStaticAbilities()) {
if (!stAb.hasParam("AddTrigger") || !stAb.isClassLevelNAbility(level)) {
continue;
}
for (String sTrig : stAb.getParam("AddTrigger").split(" & ")) {
Trigger t = host.getTriggerForStaticAbility(AbilityUtils.getSVar(stAb, sTrig), stAb);
if (t.getMode() != TriggerType.ClassLevelGained) {
continue;
}
SpellAbility effect = t.ensureAbility();
if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) {
return false;
}
}
}
return true;
}
}

View File

@@ -14,7 +14,7 @@ public class CloakAi extends ManifestBaseAi {
// (e.g. Grafdigger's Cage)
Card topCopy = CardCopyService.getLKICopy(card);
topCopy.turnFaceDownNoUpdate();
topCopy.setCloaked(sa);
topCopy.setCloaked(true);
if (ComputerUtil.isETBprevented(topCopy)) {
return false;

View File

@@ -1,10 +1,19 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -12,9 +21,6 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.List;
import java.util.Map;
public class CloneAi extends SpellAbilityAi {
@Override
@@ -205,7 +211,7 @@ public class CloneAi extends SpellAbilityAi {
// prevent loop of choosing copy of same card
if (isVesuva) {
options = CardLists.filter(options, CardPredicates.sharesNameWith(host).negate());
options = CardLists.filter(options, Predicates.not(CardPredicates.sharesNameWith(host)));
}
Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);

View File

@@ -4,7 +4,6 @@ import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -19,13 +18,6 @@ public class ConniveAi extends SpellAbilityAi {
return false; // can't draw anything
}
Card host = sa.getHostCard();
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
if (num == 0) {
return false; // Won't do anything
}
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
// Filter AI-specific targets if provided

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;

View File

@@ -17,16 +17,23 @@
*/
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
@@ -36,10 +43,6 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.collect.FCollectionView;
import java.util.List;
import java.util.Map;
//AB:GainControl|ValidTgts$Creature|TgtPrompt$Select target legendary creature|LoseControl$Untap,LoseControl|SpellDescription$Gain control of target xxxxxxx
@@ -205,9 +208,6 @@ public class ControlGainAi extends SpellAbilityAi {
while (t == null) {
// filter by MustTarget requirement
CardCollection originalList = new CardCollection(list);
list = CardLists.canSubsequentlyTarget(list, sa);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
if (planeswalkers > 0) {
@@ -330,22 +330,4 @@ public class ControlGainAi extends SpellAbilityAi {
Card chosen = ComputerUtilCard.getBestCreatureAI(cards);
return chosen != null ? chosen.getController() : Iterables.getFirst(options, null);
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
// Pay to gain Control
if (sa.hasParam("UnlessSwitched")) {
final Card host = sa.getHostCard();
final Card gameCard = host.getGame().getCardState(host, null);
if (gameCard == null
|| !gameCard.isInPlay() // not in play
|| payer.equals(gameCard.getController()) // already in control
) {
return false;
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -17,7 +17,12 @@
*/
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -27,9 +32,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.List;
import java.util.Map;
/**
* <p>
@@ -63,7 +65,7 @@ public class ControlGainVariantAi extends SpellAbilityAi {
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
Iterable<Card> otherCtrl = CardLists.filter(options, CardPredicates.isController(ai).negate());
Iterable<Card> otherCtrl = CardLists.filter(options, Predicates.not(CardPredicates.isController(ai)));
if (Iterables.isEmpty(otherCtrl)) {
return ComputerUtilCard.getWorstAI(options);
} else {

View File

@@ -1,12 +1,31 @@
package forge.ai.ability;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -15,11 +34,6 @@ import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
public class CopyPermanentAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
@@ -122,8 +136,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
// TODO: possibly improve the check, currently only checks if the name is the same
// Possibly also check if the card is threatened, and then allow to copy (this will, however, require a bit
// of a rewrite in canPlayAI to allow a response form of CopyPermanentAi)
Predicate<Card> nameEquals = CardPredicates.nameEquals(host.getName());
list = CardLists.filter(list, nameEquals.negate());
list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(host.getName())));
}
//Nothing to target
@@ -131,7 +144,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
return false;
}
CardCollection betterList = CardLists.filter(list, CardPredicates.isRemAIDeck().negate());
CardCollection betterList = CardLists.filter(list, Predicates.not(CardPredicates.isRemAIDeck()));
if (betterList.isEmpty()) {
if (!mandatory) {
return false;
@@ -152,8 +165,6 @@ public class CopyPermanentAi extends SpellAbilityAi {
// target loop
while (sa.canAddMoreTarget()) {
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets();
@@ -166,7 +177,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
list = CardLists.filter(list, c -> (!c.getType().isLegendary() || canCopyLegendary) || !c.getController().equals(aiPlayer));
Card choice;
if (list.stream().anyMatch(CardPredicates.CREATURES)) {
if (Iterables.any(list, Presets.CREATURES)) {
if (sa.hasParam("TargetingPlayer")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {

View File

@@ -1,18 +1,22 @@
package forge.ai.ability;
import forge.ai.*;
import java.util.List;
import java.util.Map;
import forge.ai.AiCardMemory;
import forge.ai.AiPlayDecision;
import forge.ai.AiProps;
import forge.ai.ComputerUtilCard;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.ApiType;
import forge.game.cost.Cost;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import java.util.List;
import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi {
@@ -24,7 +28,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
String logic = sa.getParamOrDefault("AILogic", "");
if (game.getStack().isEmpty()) {
return sa.isMandatory() || "Always".equals(logic);
return sa.isMandatory();
}
final SpellAbility top = game.getStack().peekAbility();
@@ -144,23 +148,4 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
return true;
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final String aiLogic = sa.getParam("UnlessAI");
if ("Never".equals(aiLogic)) { return false; }
if (sa.hasParam("UnlessSwitched")) {
// TODO try without AI Logic flag
if ("ChainOfVapor".equals(aiLogic)) {
if (payer.getLandsInPlay().size() < 3) {
return false;
}
// TODO make better logic in to pick which opponent
if (payer.getOpponents().getCreaturesInPlay().size() < 0) {
return false;
}
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -1,20 +1,24 @@
package forge.ai.ability;
import java.util.Iterator;
import java.util.List;
import forge.game.ability.effects.CounterEffect;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import forge.ai.*;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CounterEffect;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostExile;
@@ -24,8 +28,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class CounterAi extends SpellAbilityAi {
@@ -78,18 +80,11 @@ public class CounterAi extends SpellAbilityAi {
return false;
}
if (sa.hasParam("UnlessCost") && "TargetedController".equals(sa.getParamOrDefault("UnlessPayer", "TargetedController"))) {
Cost unlessCost = AbilityUtils.calculateUnlessCost(sa, sa.getParam("UnlessCost"), false);
if (unlessCost.hasSpecificCostType(CostDiscard.class)) {
CostDiscard discardCost = unlessCost.getCostPartByType(CostDiscard.class);
if ("Hand".equals(discardCost.getType())) {
if ("OppDiscardsHand".equals(sa.getParam("AILogic"))) {
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
return false;
}
}
}
// TODO check if Player can pay the unless cost?
}
sa.resetTargets();
if (sa.canTargetSpellAbility(topSA)) {
@@ -255,9 +250,6 @@ public class CounterAi extends SpellAbilityAi {
}
sa.resetTargets();
if (mandatory && !sa.canAddMoreTarget()) {
return true;
}
Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory);
SpellAbility tgtSA = pair.getLeft();
@@ -359,51 +351,4 @@ public class CounterAi extends SpellAbilityAi {
return new ImmutablePair<>(bestOption != null ? bestOption : leastBadOption, bestOption != null);
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
// ward or human misplay
final Card source = sa.getHostCard();
final Game game = source.getGame();
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
for (SpellAbility toBeCountered : spells) {
if (!toBeCountered.isCounterableBy(sa)) {
return false;
}
if (toBeCountered.isSpell()) {
Card spellHost = toBeCountered.getHostCard();
Card gameCard = game.getCardState(spellHost, null);
// Spell Host already left the Stack Zone
if (gameCard == null || !gameCard.isInZone(ZoneType.Stack) || !gameCard.equalsWithGameTimestamp(spellHost)) {
return false;
}
}
// no reason to pay if we don't plan to confirm
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
return false;
}
// TODO check hasFizzled
}
CardCollectionView hand = payer.getCardsIn(ZoneType.Hand);
if (cost.hasSpecificCostType(CostDiscard.class)) {
CostDiscard discard = cost.getCostPartByType(CostDiscard.class);
String type = discard.getType();
if (type.equals("Hand")) {
if (hand.isEmpty()) {
return true;
}
// TODO how to check if the Spell on the Stack is more valuable than the Cards in Hand?
int spellSum = spells.stream().map(SpellAbility::getHostCard).filter(CardPredicates.CREATURES).mapToInt(ComputerUtilCard::evaluateCreature).sum();
int handSum = hand.stream().filter(CardPredicates.CREATURES).mapToInt(ComputerUtilCard::evaluateCreature).sum();
if (spellSum <= handSum) {
return false;
}
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -17,16 +17,24 @@
*/
package forge.ai.ability;
import java.util.List;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.List;
/**
* <p>
@@ -57,7 +65,7 @@ public abstract class CountersAi extends SpellAbilityAi {
Card choice;
// opponent can always order it so that he gets 0
if (amount == 1 && ai.getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
if (amount == 1 && Iterables.any(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
return null;
}

View File

@@ -1,6 +1,10 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
@@ -16,9 +20,6 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.List;
import java.util.Map;
public class CountersMoveAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {

View File

@@ -1,12 +1,24 @@
package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -14,10 +26,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class CountersMultiplyAi extends SpellAbilityAi {
@Override
@@ -96,9 +104,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (list.isEmpty()) {
return false;
}
Card safeMatch = list.stream()
.filter(CardPredicates.hasCounters().negate())
.findFirst().orElse(null);
Card safeMatch = Iterables.getFirst(Iterables.filter(list, Predicates.not(CardPredicates.hasCounters())), null);
sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch);
return true;
}

View File

@@ -1,17 +1,22 @@
package forge.ai.ability;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.card.*;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.IterableUtil;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class CountersProliferateAi extends SpellAbilityAi {
@@ -105,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
// because countertype can't be chosen anymore, only look for poison counters
for (final Player p : IterableUtil.filter(options, Player.class)) {
for (final Player p : Iterables.filter(options, Player.class)) {
if (p.isOpponentOf(ai)) {
if (p.getCounters(poison) > 0 && p.canReceiveCounters(poison)) {
return (T)p;
@@ -118,7 +123,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
}
}
for (final Card c : IterableUtil.filter(options, Card.class)) {
for (final Card c : Iterables.filter(options, Card.class)) {
// AI planeswalker always, opponent planeswalkers never
if (c.isPlaneswalker()) {
if (c.getController().isOpponentOf(ai)) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
@@ -25,13 +26,11 @@ import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.IterableUtil;
import forge.util.MyRandom;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
public class CountersPutAi extends CountersAi {
@@ -272,9 +271,8 @@ public class CountersPutAi extends CountersAi {
return false;
}
Predicate<Card> predicate = CardPredicates.hasCounter(CounterType.getType(type));
CardCollection oppCreats = CardLists.filter(ai.getOpponents().getCreaturesInPlay(),
predicate.negate(),
Predicates.not(CardPredicates.hasCounter(CounterType.getType(type))),
CardPredicates.isTargetableBy(sa));
if (!oppCreats.isEmpty()) {
@@ -381,10 +379,7 @@ public class CountersPutAi extends CountersAi {
sa.setXManaCostPaid(amount);
} else if ("ExiledCreatureFromGraveCMC".equals(logic)) {
// e.g. Necropolis
amount = ai.getCardsIn(ZoneType.Graveyard).stream()
.filter(CardPredicates.CREATURES)
.mapToInt(Card::getCMC)
.max().orElse(0);
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), Card::getCMC);
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
return true;
}
@@ -408,7 +403,7 @@ public class CountersPutAi extends CountersAi {
}
// need to set Activating player
oa.setActivatingPlayer(ai);
oa.setActivatingPlayer(ai, true);
CardCollection targets = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), oa);
if (!targets.isEmpty()) {
@@ -712,7 +707,7 @@ public class CountersPutAi extends CountersAi {
if (sa.isCurse()) {
choice = chooseCursedTarget(list, type, amount, ai);
} else {
CardCollection lands = CardLists.filter(list, CardPredicates.LANDS);
CardCollection lands = CardLists.filter(list, CardPredicates.Presets.LANDS);
SpellAbility animate = sa.findSubAbilityByType(ApiType.Animate);
if (!lands.isEmpty() && animate != null) {
choice = ComputerUtilCard.getWorstLand(lands);
@@ -797,7 +792,7 @@ public class CountersPutAi extends CountersAi {
}
} else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) {
// can only target opponent
PlayerCollection playerList = new PlayerCollection(IterableUtil.filter(
PlayerCollection playerList = new PlayerCollection(Iterables.filter(
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
if (playerList.isEmpty()) {
@@ -805,7 +800,7 @@ public class CountersPutAi extends CountersAi {
}
// try to choose player with less creatures
Player choice = playerList.min(PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.CREATURES));
Player choice = playerList.min(PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.CREATURES));
if (choice != null) {
sa.getTargets().add(choice);
@@ -1202,7 +1197,7 @@ public class CountersPutAi extends CountersAi {
private boolean doChargeToCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Creature"));
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
int maxCMC = Aggregates.max(ownLib, Card::getCMC);
int optimalCMC = 0;
@@ -1220,7 +1215,7 @@ public class CountersPutAi extends CountersAi {
private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.NONLAND_PERMANENTS);
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
int maxCMC = Aggregates.max(oppInPlay, Card::getCMC);
int optimalCMC = 0;

View File

@@ -1,6 +1,10 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
@@ -17,9 +21,6 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class CountersPutAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {

View File

@@ -17,13 +17,24 @@
*/
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerController.BinaryChoiceType;
@@ -31,10 +42,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
/**
* <p>
* AbilityFactory_PutOrRemoveCountersAi class.
@@ -115,7 +122,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
// with one touch
CardCollection planeswalkerList = CardLists.filter(
CardLists.filterControlledBy(countersList, ai.getOpponents()),
CardPredicates.PLANESWALKERS,
CardPredicates.Presets.PLANESWALKERS,
CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount));
if (!planeswalkerList.isEmpty()) {

View File

@@ -1,6 +1,12 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
@@ -8,7 +14,13 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -17,10 +29,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
public class CountersRemoveAi extends SpellAbilityAi {
@Override
@@ -129,7 +137,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.PLANESWALKERS,
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS,
CardPredicates.hasCounter(CounterEnumType.LOYALTY, 5));
if (!planeswalkerList.isEmpty()) {
@@ -176,7 +184,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
CardCollection planeswalkerList = CardLists.filter(list,
CardPredicates.PLANESWALKERS.and(CardPredicates.isControlledByAnyOf(ai.getOpponents())),
Predicates.and(CardPredicates.Presets.PLANESWALKERS, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount));
if (!planeswalkerList.isEmpty()) {
@@ -218,7 +226,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// remove P1P1 counters from opposing creatures
CardCollection oppP1P1List = CardLists.filter(list,
CardPredicates.CREATURES.and(CardPredicates.isControlledByAnyOf(ai.getOpponents())),
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
CardPredicates.hasCounter(CounterEnumType.P1P1));
if (!oppP1P1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));

View File

@@ -1,6 +1,12 @@
package forge.ai.ability;
import forge.ai.*;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -13,8 +19,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.function.Predicate;
public class DamageAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
@@ -26,9 +27,8 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
@@ -42,7 +42,7 @@ public class DamageDealAi extends DamageAiBase {
final SpellAbility root = sa.getRootAbility();
final String damage = sa.getParam("NumDmg");
Card source = sa.getHostCard();
int dmg = calculateDamageAmount(sa, source, damage);
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
final String logic = sa.getParam("AILogic");
if ("MadSarkhanDigDmg".equals(logic)) {
@@ -93,9 +93,9 @@ public class DamageDealAi extends DamageAiBase {
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String damage = sa.getParam("NumDmg");
int dmg = calculateDamageAmount(sa, source, damage);
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
if (damage.equals("X") || (dmg == 0 && source.getSVar("X").equals("Count$xPaid"))) {
if (damage.equals("X") || source.getSVar("X").equals("Count$xPaid")) {
if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) {
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
@@ -163,10 +163,8 @@ public class DamageDealAi extends DamageAiBase {
}
} else if ("WildHunt".equals(logic)) {
// This dummy ability will just deal 0 damage, but holds the logic for the AI for Master of Wild Hunt
dmg = ai.getCardsIn(ZoneType.Battlefield).stream()
.filter(CardPredicates.restriction("Creature.Wolf+untapped+YouCtrl+Other", ai, source, sa))
.mapToInt(Card::getNetPower)
.sum();
List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source, sa);
dmg = Aggregates.sum(wolves, Card::getNetPower);
} else if ("Triskelion".equals(logic)) {
final int n = source.getCounters(CounterEnumType.P1P1);
if (n > 0) {
@@ -402,7 +400,7 @@ public class DamageDealAi extends DamageAiBase {
final Player activator = sa.getActivatingPlayer();
final Card source = sa.getHostCard();
final Game game = source.getGame();
List<Card> hPlay = CardLists.filter(getTargetableCards(ai, sa, pl, tgt, activator, source, game), CardPredicates.PLANESWALKERS);
List<Card> hPlay = CardLists.filter(getTargetableCards(ai, sa, pl, tgt, activator, source, game), CardPredicates.Presets.PLANESWALKERS);
CardCollection killables = CardLists.filter(hPlay, c -> c.getSVar("Targeting").equals("Dies")
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d)
@@ -887,10 +885,7 @@ public class DamageDealAi extends DamageAiBase {
// See if there's an indestructible target that can be used
CardCollection indestructible = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
(CardPredicates.CREATURES.or(CardPredicates.PLANESWALKERS))
.and(CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE))
.and(CardPredicates.isTargetableBy(sa))
);
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.Presets.PLANESWALKERS, CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE), CardPredicates.isTargetableBy(sa)));
if (!indestructible.isEmpty()) {
Card c = ComputerUtilCard.getWorstPermanentAI(indestructible, false, false, false, false);
@@ -903,7 +898,7 @@ public class DamageDealAi extends DamageAiBase {
}
else if (tgt.canTgtPlaneswalker()) {
// Second pass for planeswalkers: choose AI's worst planeswalker
final Card c = ComputerUtilCard.getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.PLANESWALKERS, CardPredicates.isTargetableBy(sa)));
final Card c = ComputerUtilCard.getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.PLANESWALKERS), CardPredicates.isTargetableBy(sa)));
if (c != null) {
sa.getTargets().add(c);
if (divided) {
@@ -935,7 +930,7 @@ public class DamageDealAi extends DamageAiBase {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final String damage = sa.getParam("NumDmg");
int dmg = calculateDamageAmount(sa, source, damage);
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
// Remove all damage
if (sa.hasParam("Remove")) {
@@ -979,17 +974,6 @@ public class DamageDealAi extends DamageAiBase {
return true;
}
private static int calculateDamageAmount(SpellAbility sa, Card source, String damage) {
if(damage == null)
return 0;
//Used when the value isn't yet known when making decisions, e.g. dice rolls.
if(sa.hasParam("AIExpectAmount"))
return AbilityUtils.calculateAmount(source, sa.getParam("AIExpectAmount"), sa);
return AbilityUtils.calculateAmount(source, damage, sa);
}
private boolean doXLifeDrainLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
@@ -1136,28 +1120,4 @@ public class DamageDealAi extends DamageAiBase {
return null;
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid,
FCollectionView<Player> payers) {
if (!payer.canLoseLife() || payer.cantLoseForZeroOrLessLife()) {
return false;
}
final Card hostCard = sa.getHostCard();
final List<Card> definedSources = AbilityUtils.getDefinedCards(hostCard, sa.getParam("DamageSource"), sa);
if (definedSources == null || definedSources.isEmpty()) {
return false;
}
int dmg = AbilityUtils.calculateAmount(hostCard, sa.getParam("NumDmg"), sa);
for (Card source : definedSources) {
int predictedDamage = ComputerUtilCombat.predictDamageTo(payer, dmg, source, false);
if (payer.getLife() < predictedDamage * 1.5) {
return true;
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -1,5 +1,8 @@
package forge.ai.ability;
import java.util.ArrayList;
import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
@@ -7,7 +10,11 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
@@ -18,9 +25,6 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.ArrayList;
import java.util.List;
public class DamagePreventAi extends SpellAbilityAi {
@Override
@@ -122,7 +126,7 @@ public class DamagePreventAi extends SpellAbilityAi {
if (targetables.isEmpty()) {
return false;
}
final CardCollection combatants = CardLists.filter(targetables, CardPredicates.CREATURES);
final CardCollection combatants = CardLists.filter(targetables, CardPredicates.Presets.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants);
for (final Card c : combatants) {
@@ -183,7 +187,7 @@ public class DamagePreventAi extends SpellAbilityAi {
}
if (!compTargetables.isEmpty()) {
final CardCollection combatants = CardLists.filter(compTargetables, CardPredicates.CREATURES);
final CardCollection combatants = CardLists.filter(compTargetables, CardPredicates.Presets.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants);
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
Combat combat = game.getCombat();

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -7,8 +9,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import java.util.Map;
public class DayTimeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {

View File

@@ -1,6 +1,12 @@
package forge.ai.ability;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
@@ -19,10 +25,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DebuffAi extends SpellAbilityAi {
@Override
@@ -66,7 +68,7 @@ public class DebuffAi extends SpellAbilityAi {
List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
final Combat combat = game.getCombat();
return cards.stream().anyMatch(c -> {
return Iterables.any(cards, c -> {
if (c.getController().equals(sa.getActivatingPlayer()) || combat == null)
return false;

View File

@@ -24,10 +24,10 @@ public class DelayedTriggerAi extends SpellAbilityAi {
if (trigsa == null) {
return false;
}
trigsa.setActivatingPlayer(ai);
trigsa.setActivatingPlayer(ai, true);
if (trigsa instanceof AbilitySub) {
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
} else {
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}
@@ -41,7 +41,7 @@ public class DelayedTriggerAi extends SpellAbilityAi {
}
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
trigsa.setActivatingPlayer(ai);
trigsa.setActivatingPlayer(ai, true);
if (!sa.hasParam("OptionalDecider")) {
return aic.doTrigger(trigsa, true);
@@ -153,7 +153,7 @@ public class DelayedTriggerAi extends SpellAbilityAi {
if (trigsa == null) {
return false;
}
trigsa.setActivatingPlayer(ai);
trigsa.setActivatingPlayer(ai, true);
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}

View File

@@ -1,7 +1,6 @@
package forge.ai.ability;
import java.util.function.Predicate;
import com.google.common.base.Predicates;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -16,7 +15,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
public class DestroyAi extends SpellAbilityAi {
@Override
@@ -169,8 +167,7 @@ public class DestroyAi extends SpellAbilityAi {
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
}
if (!playReusable(ai, sa)) {
Predicate<Card> hasCounter = CardPredicates.hasCounter(CounterEnumType.SHIELD, 1);
list = CardLists.filter(list, hasCounter.negate());
list = CardLists.filter(list, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
list = CardLists.filter(list, c -> {
//Check for cards that can be sacrificed in response
@@ -216,8 +213,6 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
@@ -277,7 +272,6 @@ public class DestroyAi extends SpellAbilityAi {
choice = aura;
}
}
// TODO What about stolen permanents we're getting back at the end of the turn?
}
}
@@ -287,10 +281,8 @@ public class DestroyAi extends SpellAbilityAi {
}
list.remove(choice);
if (sa.canTarget(choice)) {
sa.getTargets().add(choice);
}
}
} else if (sa.hasParam("Defined")) {
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if ("WillSkipTurn".equals(logic) && (source.getController().equals(ai)
@@ -329,8 +321,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
Predicate<Card> hasCounter = CardPredicates.hasCounter(CounterEnumType.SHIELD, 1);
preferred = CardLists.filter(preferred, hasCounter.negate());
preferred = CardLists.filter(preferred, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);
}
@@ -366,10 +357,7 @@ public class DestroyAi extends SpellAbilityAi {
}
} else {
Card c = ComputerUtilCard.getBestAI(preferred);
if (sa.canTarget(c)) {
sa.getTargets().add(c);
}
preferred.remove(c);
}
}
@@ -390,9 +378,7 @@ public class DestroyAi extends SpellAbilityAi {
} else {
c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false);
}
if (sa.canTarget(c)) {
sa.getTargets().add(c);
}
list.remove(c);
}
}
@@ -436,8 +422,8 @@ public class DestroyAi extends SpellAbilityAi {
boolean nonBasicTgt = !tgtLand.isBasicLand();
// Try not to lose tempo too much and not to mana-screw yourself when considering this logic
int numLandsInHand = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS_PRODUCING_MANA);
int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
int numLandsInHand = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
// If the opponent skipped a land drop, consider not looking at having the extra land in hand if the profile allows it
boolean isHighPriority = highPriorityIfNoLandDrop && oppSkippedLandDrop;
@@ -455,20 +441,4 @@ public class DestroyAi extends SpellAbilityAi {
}
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card host = sa.getHostCard();
if (alreadyPaid) {
return false;
}
if (sa.hasParam("Defined")) {
CardCollection cards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
if (!cards.anyMatch(CardPredicates.isController(payer))) {
return false;
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -1,19 +1,21 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.*;
import forge.card.MagicColor;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostDamage;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
import java.util.function.Predicate;
public class DestroyAllAi extends SpellAbilityAi {
@@ -107,8 +109,8 @@ public class DestroyAllAi extends SpellAbilityAi {
// Special handling for Raiding Party
if (logic.equals("RaidingParty")) {
int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, ailist.size());
int numOppsCanSave = Math.min(CardLists.count(ai.getOpponents().getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, opplist.size());
int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)) * 2, ailist.size());
int numOppsCanSave = Math.min(CardLists.count(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)) * 2, opplist.size());
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
}
@@ -181,38 +183,4 @@ public class DestroyAllAi extends SpellAbilityAi {
return false;
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
if (payers.size() > 1) {
if (alreadyPaid) {
return false;
}
}
String valid = sa.getParamOrDefault("ValidCards", "");
CardCollection ailist = CardLists.getValidCards(payer.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
ailist = CardLists.filter(ailist, predicate);
if (ailist.isEmpty()) {
return false;
}
if (cost.hasSpecificCostType(CostDamage.class)) {
if (!payer.canLoseLife()) {
return false;
}
final CostDamage pay = cost.getCostPartByType(CostDamage.class);
int realDamage = ComputerUtilCombat.predictDamageTo(payer, pay.getAbilityAmount(sa), source, false);
if (realDamage > payer.getLife()) {
return false;
}
if (realDamage > ailist.size() * 3) { // three life points per one creature
return false;
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -1,7 +1,17 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
@@ -18,8 +28,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
import java.util.Map;
public class DigAi extends SpellAbilityAi {
/* (non-Javadoc)

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.Map;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
@@ -12,8 +14,6 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Map;
public class DigMultipleAi extends SpellAbilityAi {
/* (non-Javadoc)

View File

@@ -1,5 +1,8 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
@@ -13,9 +16,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class DigUntilAi extends SpellAbilityAi {
@Override
@@ -45,7 +45,7 @@ public class DigUntilAi extends SpellAbilityAi {
return false;
}
if ("Land.Basic".equals(sa.getParam("Valid"))
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.LANDS_PRODUCING_MANA)) {
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.Presets.LANDS_PRODUCING_MANA)) {
// We already have a mana-producing land in hand, so bail
// until opponent's end of turn phase!
// But we still want more (and want to fill grave) if nothing better to do then
@@ -128,7 +128,7 @@ public class DigUntilAi extends SpellAbilityAi {
final String logic = sa.getParam("AILogic");
if ("OathOfDruids".equals(logic)) {
final List<Card> creaturesInLibrary =
CardLists.filter(player.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
CardLists.filter(player.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
final List<Card> creaturesInBattlefield = player.getCreaturesInPlay();
// if there are at least 3 creatures in library,
// or none in play with one in library, oath

View File

@@ -4,15 +4,18 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost;
import forge.game.cost.CostDamage;
import forge.game.cost.CostDraw;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -21,7 +24,6 @@ import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class DiscardAi extends SpellAbilityAi {
@@ -92,7 +94,7 @@ public class DiscardAi extends SpellAbilityAi {
if (sa.hasParam("AnyNumber")) {
if ("DiscardUncastableAndExcess".equals(aiLogic)) {
final CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
final int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS);
int numDiscard = 0;
int numOppInHand = 0;
for (Player p : ai.getGame().getPlayers()) {
@@ -217,57 +219,4 @@ public class DiscardAi extends SpellAbilityAi {
}
return super.confirmAction(player, sa, mode, message, params);
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card host = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
if ("Never".equals(aiLogic)) { return false; }
CardCollectionView hand = payer.getCardsIn(ZoneType.Hand);
if ("Hand".equals(sa.getParam("Mode"))) {
if (hand.size() <= 2) {
return false;
}
} else {
int amount = AbilityUtils.calculateAmount(host, sa.getParam("NumCards"), sa);
// damage cost with prevention?
if (cost.hasSpecificCostType(CostDamage.class)) {
if (!payer.canLoseLife()) {
return false;
}
final CostDamage pay = cost.getCostPartByType(CostDamage.class);
int realDamage = ComputerUtilCombat.predictDamageTo(payer, pay.getAbilityAmount(sa), host, false);
if (realDamage > payer.getLife()) {
return false;
}
if (realDamage > amount * 2) { // two life points per not discarded card?
return false;
}
}
boolean isDrawDiscard = cost.hasOnlySpecificCostType(CostDraw.class) && sa.hasParam("UnlessSwitched");
// TODO should AI do draw + discard effects when hand is empty?
// maybe if deck supports Graveyard or discard effects?
if (hand.isEmpty()) {
return false;
}
// is it always better?
if (isDrawDiscard) {
// check to not deck yourself
int libSize = payer.getCardsIn(ZoneType.Library).size();
if (amount >= libSize - 3) {
if (payer.isCardInPlay("Laboratory Maniac") && !payer.cantWin()) {
return true;
}
// Don't deck yourself
return false;
}
return true;
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -8,6 +8,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import java.util.List;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -7,8 +9,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
import java.util.List;
public class DrainManaAi extends SpellAbilityAi {
@Override

View File

@@ -20,7 +20,15 @@ package forge.ai.ability;
import java.util.Map;
import forge.ai.*;
import forge.ai.AiCostDecision;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -30,11 +38,13 @@ import forge.game.card.CounterType;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.*;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class DrawAi extends SpellAbilityAi {
@@ -515,17 +525,12 @@ public class DrawAi extends SpellAbilityAi {
return false;
}
if ((computerHandSize + numCards > computerMaxHandSize)) {
// Don't draw too many cards and then risk discarding cards at EOT
if (game.getPhaseHandler().isPlayerTurn(ai)
if ((computerHandSize + numCards > computerMaxHandSize)
&& game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX
&& !drawback) {
return false;
}
if (computerHandSize > computerMaxHandSize) {
// Don't make my hand size get too big if already at max
&& !assumeSafeX) {
// Don't draw too many cards and then risk discarding cards at EOT
if (!drawback) {
return false;
}
}
@@ -554,36 +559,4 @@ public class DrawAi extends SpellAbilityAi {
// except it has Laboratory Maniac
return player.isCardInPlay("Laboratory Maniac");
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card host = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) {
return false;
}
// Risk Factor Effects
for (Player p : AbilityUtils.getDefinedPlayers(host, sa.getParam("Defined"), sa)) {
if (p.isOpponentOf(payer)) {
if (!p.canDraw()) {
return false;
}
if (cost.hasSpecificCostType(CostDamage.class)) {
if (!payer.canLoseLife()) {
continue;
}
final CostDamage pay = cost.getCostPartByType(CostDamage.class);
int realDamage = ComputerUtilCombat.predictDamageTo(payer, pay.getAbilityAmount(sa), host, false);
if (payer.getLife() < realDamage * 2) {
return false;
}
}
}
}
// TODO add logic for Discard + Draw Effects
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.game.CardTraitPredicates;
@@ -9,9 +10,9 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -27,7 +28,6 @@ import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import java.util.ArrayList;
import java.util.List;
@@ -57,8 +57,8 @@ public class EffectAi extends SpellAbilityAi {
for (Player opp : ai.getOpponents()) {
boolean worthHolding = false;
CardCollectionView oppCreatsLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS.or(CardPredicates.CREATURES));
CardCollectionView oppCreatsLandsTapped = CardLists.filter(oppCreatsLands, CardPredicates.TAPPED);
Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.Presets.CREATURES));
CardCollectionView oppCreatsLandsTapped = CardLists.filter(oppCreatsLands, CardPredicates.Presets.TAPPED);
if (oppCreatsLandsTapped.size() >= 3 || oppCreatsLands.size() == oppCreatsLandsTapped.size()) {
worthHolding = true;
@@ -84,7 +84,7 @@ public class EffectAi extends SpellAbilityAi {
Player opp = ai.getStrongestOpponent();
List<Card> possibleAttackers = ai.getCreaturesInPlay();
List<Card> possibleBlockers = opp.getCreaturesInPlay();
possibleBlockers = CardLists.filter(possibleBlockers, CardPredicates.UNTAPPED);
possibleBlockers = CardLists.filter(possibleBlockers, Presets.UNTAPPED);
final Combat combat = game.getCombat();
int oppLife = opp.getLife();
int potentialDmg = 0;
@@ -287,7 +287,7 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("Burn")) {
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
SpellAbility burn = sa.getSubAbility();
return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn);
return SpellApiToAi.Converter.get(burn.getApi()).canPlayAIWithSubs(ai, burn);
} else if (logic.equals("YawgmothsWill")) {
return SpecialCardAi.YawgmothsWill.consider(ai, sa);
} else if (logic.startsWith("NeedCreatures")) {
@@ -333,12 +333,12 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("CantRegenerate")) {
if (sa.usesTargeting()) {
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.filter(list, CardPredicates.CAN_BE_DESTROYED, input -> {
list = CardLists.filter(list, CardPredicates.Presets.CAN_BE_DESTROYED, input -> {
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(input);
runParams.put(AbilityKey.Regeneration, true);
List<ReplacementEffect> repDestroyList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
List<ReplacementEffect> repDestoryList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
if (repDestroyList.isEmpty() || repDestroyList.stream().anyMatch(CardTraitPredicates.hasParam("Regeneration").negate())) {
if (repDestoryList.isEmpty() || Iterables.any(repDestoryList, Predicates.not(CardTraitPredicates.hasParam("Regeneration")))) {
return false;
}
@@ -367,9 +367,9 @@ public class EffectAi extends SpellAbilityAi {
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
runParams.put(AbilityKey.Regeneration, true);
List<ReplacementEffect> repDestroyList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
List<ReplacementEffect> repDestoryList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
if (repDestroyList.isEmpty() || repDestroyList.stream().anyMatch(CardTraitPredicates.hasParam("Regeneration").negate())) {
if (repDestoryList.isEmpty() || Iterables.any(repDestoryList, Predicates.not(CardTraitPredicates.hasParam("Regeneration")))) {
return false;
}
@@ -639,19 +639,4 @@ public class EffectAi extends SpellAbilityAi {
return false;
}
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final String aiLogic = sa.getParam("UnlessAI");
if ("WillAttack".equals(aiLogic)) {
// TODO use AiController::getPredictedCombat
AiAttackController aiAtk = new AiAttackController(payer);
Combat combat = new Combat(payer);
aiAtk.declareAttackers(combat);
if (combat.getAttackers().isEmpty()) {
return false;
}
}
return super.willPayUnlessCost(sa, payer, cost, alreadyPaid, payers);
}
}

View File

@@ -17,6 +17,9 @@
*/
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
@@ -27,9 +30,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
/**
* <p>
* AbilityFactoryBond class.

View File

@@ -1,140 +0,0 @@
package forge.ai.ability;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.Game;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Map;
public class EndureAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// Support for possible targeted Endure (e.g. target creature endures X)
if (sa.usesTargeting()) {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
if (bestCreature == null) {
return false;
}
sa.resetTargets();
sa.getTargets().add(bestCreature);
}
return true;
}
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
// TODO: adapted from Fabricate AI in TokenAi, maybe can be refactored to a single method
final Card source = sa.getHostCard();
final Game game = source.getGame();
final String num = sa.getParamOrDefault("Num", "1");
final int amount = AbilityUtils.calculateAmount(source, num, sa);
// if host would leave the play or if host is useless, create the token
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(ai, source)) {
return false;
}
// need a copy for one with extra +1/+1 counter boost,
// without causing triggers to run
final Card copy = CardCopyService.getLKICopy(source);
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + amount);
copy.setZone(source.getZone());
// if host would put into the battlefield attacking
Combat combat = source.getGame().getCombat();
if (combat != null && combat.isAttacking(source)) {
final Player defender = combat.getDefenderPlayerByAttacker(source);
return defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true);
}
// if the host has haste and can attack
if (CombatUtil.canAttack(copy)) {
for (final Player opp : ai.getOpponents()) {
if (CombatUtil.canAttack(copy, opp) &&
opp.canLoseLife() &&
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
return true;
}
}
// TODO check for trigger to turn token ETB into +1/+1 counter for host
// TODO check for trigger to turn token ETB into damage or life loss for opponent
// in these cases token might be preferred even if they would not survive
// evaluate creature with counters
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
// spawn the token so it's possible to evaluate it
final Card token = TokenInfo.getProtoType("w_x_x_spirit", sa, ai, false);
token.setController(ai, 0);
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
token.setTokenSpawningAbility(sa);
// evaluate the generated token
token.setBasePowerString(num);
token.setBasePower(amount);
token.setBaseToughnessString(num);
token.setBaseToughness(amount);
boolean result = true;
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(token);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
// token would not survive
if (!token.isCreature() || token.getNetToughness() < 1) {
result = false;
}
if (result) {
int evalToken = ComputerUtilCard.evaluateCreature(token);
result = evalToken < evalCounter;
}
//reset static abilities
game.getAction().checkStaticAbilities(false);
return result;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
return shouldPutCounters(player, sa);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// Support for possible targeted Endure (e.g. target creature endures X)
if (sa.usesTargeting()) {
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
if (!list.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return true;
}
return false;
}
return canPlayAI(aiPlayer, sa) || mandatory;
}
}

View File

@@ -1,8 +1,17 @@
package forge.ai.ability;
import forge.ai.*;
import forge.game.card.*;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
@@ -34,8 +43,8 @@ public class ExploreAi extends SpellAbilityAi {
int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size();
CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.LANDS_PRODUCING_MANA);
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.LANDS_PRODUCING_MANA);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
int maxCMCDiff = 1;
int numLandsToStillNeedMore = 2;

Some files were not shown because too many files have changed in this diff Show More