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: on:
workflow_dispatch: 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: jobs:
build: build:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -43,94 +32,10 @@ jobs:
run: | run: |
git config user.email "actions@github.com" git config user.email "actions@github.com"
git config user.name "GitHub Actions" git config user.name "GitHub Actions"
- name: Build/Install/Publish to GitHub Packages Apache Maven
- 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 }}
run: | run: |
export DISPLAY=":1" export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 & Xvfb :1 -screen 0 800x600x8 &
export _JAVA_OPTIONS="-Xmx2g" 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 }}
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
env: env:
GITHUB_TOKEN: ${{ github.token }} 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: on:
workflow_dispatch:
schedule: schedule:
- cron: "0 0 * * *" # Everday at midnight - cron: "0 0 * * *" # Everday at midnight
jobs: jobs:
remove-stale-branches: remove-stale-branches:
if: github.repository_owner == 'Card-Forge'
name: Remove Stale Branches name: Remove Stale Branches
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: fpicalausa/remove-stale-branches@v2.1.0 - uses: fpicalausa/remove-stale-branches@v1.6.0
with: with:
dry-run: false # Check out the console output before setting this to false dry-run: true # Check out the console output before setting this to false
ignore-unknown-authors: true
ignore-branches-with-open-prs: true
default-recipient: tehdiplomat

View File

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

View File

@@ -13,6 +13,10 @@ on:
# description: 'Upload the completed Android package' # description: 'Upload the completed Android package'
# required: false # required: false
# default: true # default: true
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 19 * * *'
jobs: jobs:
build: build:
@@ -105,10 +109,3 @@ jobs:
local-dir: upload/ local-dir: upload/
server-dir: downloads/dailysnapshots/ server-dir: downloads/dailysnapshots/
state-name: .ftp-deploy-android-sync-state.json 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)' description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false required: false
default: false default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '30 18 * * *'
jobs: jobs:
build: build:
@@ -86,10 +89,3 @@ jobs:
*.pom *.pom
*.repositories *.repositories
*.xml *.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 .settings
.classpath .classpath
.project .project
.checkstyle
# Ignore VS Code config files # Ignore VS Code config files
@@ -25,8 +24,6 @@
nbactions.xml nbactions.xml
# Ignore flattened pom
.flattened-pom.xml
# Ignore binaries, temp files and test output, everywhere # 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" <settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> 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> <servers>
<server> <server>
<id>cardforge-repo</id> <id>cardforge-repo</id>

View File

@@ -26,13 +26,13 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
### 📥 Desktop Installation ### 📥 Desktop Installation
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest). 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. - **Tip:** Extract to a new folder to prevent version conflicts.
3. **User Data Management:** Previous players data is preserved during upgrades. 3. **User Data Management:** Previous players data is preserved during upgrades.
4. **Java Requirement:** Ensure you have **Java 17 or later** installed. 4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
### 📱 Android Installation ### 📱 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> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>${revision}</version> <version>2.0.00</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,8 +26,9 @@ public class QuestStageEditor extends JComponent{
JList list, Object value, int index, JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) { boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof AdventureQuestStage stageData)) if(!(value instanceof AdventureQuestStage))
return label; return label;
AdventureQuestStage stageData=(AdventureQuestStage) value;
label.setText(stageData.name); label.setText(stageData.name);
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually? //label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
return label; 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 colors =new TextListEdit(new String[] { "White", "Blue", "Black", "Red", "Green" });
TextListEdit rarity =new TextListEdit(new String[] { "Basic Land", "Common", "Uncommon", "Rare", "Mythic Rare" }); TextListEdit rarity =new TextListEdit(new String[] { "Basic Land", "Common", "Uncommon", "Rare", "Mythic Rare" });
TextListEdit subTypes =new TextListEdit(); TextListEdit subTypes =new TextListEdit();
TextListEdit cardTypes =new TextListEdit(Arrays.stream(CardType.CoreType.values()).map(CardType.CoreType::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.stream(CardType.Supertype.values()).map(CardType.Supertype::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 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"}); JComboBox colorType =new JComboBox(new String[] { "Any", "Colorless", "MultiColor", "MonoColor"});
JTextField cardText =new JTextField(); JTextField cardText =new JTextField();
private boolean updating=false; private boolean updating=false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,17 +18,12 @@
package forge.ai; package forge.ai;
import java.util.Map; import java.util.HashSet;
import java.util.Set; 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.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
/** /**
* <p> * <p>
* AiCardMemory class. * 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 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() { 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) { 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) { if (c == null) {
return false; 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 * @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) { 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 * @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) { 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) { public boolean rememberCard(Card c, MemorySet set) {
if (c == null) if (c == null)
return false; 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)) { if (!isRememberedCard(c, set)) {
return false; 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 * @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) { 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)) { if (c.getName().equals(cardName)) {
return forgetCard(c, set); return forgetCard(c, set);
} }
} }
}
return false; 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 * @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) { 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)) { if (c.getName().equals(cardName) && c.getOwner().equals(owner)) {
return forgetCard(c, set); return forgetCard(c, set);
} }
} }
}
return false; return false;
} }

View File

@@ -18,17 +18,18 @@
package forge.ai; package forge.ai;
import com.esotericsoftware.minlog.Log; 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 com.google.common.collect.Lists;
import forge.ai.AiCardMemory.MemorySet; import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.ChangeZoneAi; import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.LearnAi; import forge.ai.ability.LearnAi;
import forge.ai.simulation.GameStateEvaluator;
import forge.ai.simulation.SpellAbilityPicker; import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.CardType; import forge.card.CardType;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.deck.Deck; import forge.deck.Deck;
import forge.deck.DeckSection; import forge.deck.DeckSection;
@@ -38,6 +39,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased; import forge.game.ability.SpellApiBased;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.*; import forge.game.cost.*;
@@ -54,30 +56,20 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.*; import forge.game.spellability.*;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityDisableTriggers; import forge.game.staticability.StaticAbilityDisableTriggers;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility; import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.item.PaperCard; 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.Breadcrumb;
import io.sentry.Sentry; import io.sentry.Sentry;
import java.util.*; 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> * <p>
@@ -98,7 +90,6 @@ public class AiController {
private SpellAbilityPicker simPicker; private SpellAbilityPicker simPicker;
private int lastAttackAggression; private int lastAttackAggression;
private boolean useLivingEnd; private boolean useLivingEnd;
private List<SpellAbility> skipped;
public AiController(final Player computerPlayer, final Game game0) { public AiController(final Player computerPlayer, final Game game0) {
player = computerPlayer; player = computerPlayer;
@@ -177,7 +168,7 @@ public class AiController {
for (final Card c : all) { for (final Card c : all) {
for (final SpellAbility sa : c.getNonManaAbilities()) { for (final SpellAbility sa : c.getNonManaAbilities()) {
if (sa instanceof SpellPermanent) { if (sa instanceof SpellPermanent) {
sa.setActivatingPlayer(player); sa.setActivatingPlayer(player, true);
if (checkETBEffects(c, sa, ApiType.Counter)) { if (checkETBEffects(c, sa, ApiType.Counter)) {
spellAbilities.add(sa); spellAbilities.add(sa);
} }
@@ -211,7 +202,7 @@ public class AiController {
return true; return true;
} }
} }
if (game.getCardsIn(ZoneType.Graveyard).anyMatch(CardPredicates.nameEquals(hostName))) { if (Iterables.any(game.getCardsIn(ZoneType.Graveyard), CardPredicates.nameEquals(hostName))) {
return true; return true;
} }
} }
@@ -295,7 +286,7 @@ public class AiController {
} }
// can't fetch partner isn't problematic // can't fetch partner isn't problematic
if (tr.isKeyword(Keyword.PARTNER)) { if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
continue; continue;
} }
@@ -408,22 +399,6 @@ public class AiController {
private static List<SpellAbility> getPlayableCounters(final CardCollection l) { private static List<SpellAbility> getPlayableCounters(final CardCollection l) {
final List<SpellAbility> spellAbility = Lists.newArrayList(); final List<SpellAbility> spellAbility = Lists.newArrayList();
for (final Card c : l) { 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()) { for (final SpellAbility sa : c.getNonManaAbilities()) {
// Check if this AF is a Counterspell // Check if this AF is a Counterspell
if (sa.getApi() == ApiType.Counter) { if (sa.getApi() == ApiType.Counter) {
@@ -431,16 +406,15 @@ public class AiController {
} }
} }
} }
}
return spellAbility; return spellAbility;
} }
private CardCollection filterLandsToPlay(CardCollection landList) { private CardCollection filterLandsToPlay(CardCollection landList) {
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand); 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) { if (landList.size() == 1 && nonLandList.size() < 3) {
CardCollectionView cardsInPlay = player.getCardsIn(ZoneType.Battlefield); 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)); CardCollection allCards = new CardCollection(player.getCardsIn(ZoneType.Graveyard));
allCards.addAll(player.getCardsIn(ZoneType.Command)); allCards.addAll(player.getCardsIn(ZoneType.Command));
allCards.addAll(cardsInPlay); allCards.addAll(cardsInPlay);
@@ -470,7 +444,7 @@ public class AiController {
String name = c.getName(); String name = c.getName();
CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield); CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) { if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) {
if (battlefield.anyMatch(CardPredicates.nameEquals(name))) { if (Iterables.any(battlefield, CardPredicates.nameEquals(name))) {
return false; return false;
} }
} }
@@ -478,7 +452,7 @@ public class AiController {
final CardCollectionView hand1 = player.getCardsIn(ZoneType.Hand); final CardCollectionView hand1 = player.getCardsIn(ZoneType.Hand);
CardCollection lands = new CardCollection(battlefield); CardCollection lands = new CardCollection(battlefield);
lands.addAll(hand1); lands.addAll(hand1);
lands = CardLists.filter(lands, CardPredicates.LANDS); lands = CardLists.filter(lands, Presets.LANDS);
int maxCmcInHand = Aggregates.max(hand1, Card::getCMC); int maxCmcInHand = Aggregates.max(hand1, Card::getCMC);
if (lands.size() >= Math.max(maxCmcInHand, 6)) { if (lands.size() >= Math.max(maxCmcInHand, 6)) {
@@ -492,7 +466,7 @@ public class AiController {
return false; return false;
} }
} }
return c.getAllPossibleAbilities(player, true).stream().anyMatch(SpellAbility::isLandAbility); return Iterables.any(c.getAllPossibleAbilities(player, true), SpellAbility::isLandAbility);
}); });
return landList; return landList;
} }
@@ -502,9 +476,7 @@ public class AiController {
return null; return null;
} }
landList = ComputerUtilCard.dedupeCards(landList); CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.NON_LANDS);
// Some considerations for Momir/MoJhoSto // Some considerations for Momir/MoJhoSto
boolean hasMomir = player.isCardInCommand("Momir Vig, Simic Visionary Avatar"); boolean hasMomir = player.isCardInCommand("Momir Vig, Simic Visionary Avatar");
@@ -544,7 +516,7 @@ public class AiController {
landList = unreflectedLands; 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()) { if (!nonLandsInHand.isEmpty()) {
CardCollection nonTappedLands = new CardCollection(); CardCollection nonTappedLands = new CardCollection();
for (Card land : landList) { for (Card land : landList) {
@@ -552,6 +524,7 @@ public class AiController {
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(land); final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(land);
repParams.put(AbilityKey.Origin, land.getZone().getZoneType()); repParams.put(AbilityKey.Origin, land.getZone().getZoneType());
repParams.put(AbilityKey.Destination, ZoneType.Battlefield); repParams.put(AbilityKey.Destination, ZoneType.Battlefield);
repParams.put(AbilityKey.Source, land);
// add Params for AddCounter Replacements // add Params for AddCounter Replacements
GameEntityCounterTable table = new GameEntityCounterTable(); GameEntityCounterTable table = new GameEntityCounterTable();
@@ -565,7 +538,7 @@ public class AiController {
if (reSA == null || !ApiType.Tap.equals(reSA.getApi())) { if (reSA == null || !ApiType.Tap.equals(reSA.getApi())) {
continue; continue;
} }
reSA.setActivatingPlayer(reSA.getHostCard().getController()); reSA.setActivatingPlayer(reSA.getHostCard().getController(), true);
if (reSA.metConditions()) { if (reSA.metConditions()) {
foundTapped = true; foundTapped = true;
break; break;
@@ -579,55 +552,17 @@ public class AiController {
nonTappedLands.add(land); nonTappedLands.add(land);
} }
// if we have the choice, see if we can play an untapped land
if (!nonTappedLands.isEmpty()) { 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; 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 // Choose first land to be able to play a one drop
if (player.getLandsInPlay().isEmpty()) { if (player.getLandsInPlay().isEmpty()) {
CardCollection oneDrops = CardLists.filter(nonLandsInHand, CardPredicates.hasCMC(1)); CardCollection oneDrops = CardLists.filter(nonLandsInHand, CardPredicates.hasCMC(1));
for (int i = 0; i < MagicColor.WUBRG.length; i++) { for (int i = 0; i < MagicColor.WUBRG.length; i++) {
byte color = MagicColor.WUBRG[i]; byte color = MagicColor.WUBRG[i];
if (oneDrops.anyMatch(CardPredicates.isColor(color))) { if (Iterables.any(oneDrops, CardPredicates.isColor(color))) {
for (Card land : landList) { for (Card land : landList) {
if (land.getType().hasSubtype(MagicColor.Constant.BASIC_LANDS.get(i))) { if (land.getType().hasSubtype(MagicColor.Constant.BASIC_LANDS.get(i))) {
return land; 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 CardCollectionView landsInBattlefield = player.getCardsIn(ZoneType.Battlefield);
final List<String> basics = Lists.newArrayList(); 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? // what types can I go get?
int[] basic_counts = new int[5]; // in WUBRG order
for (final String name : MagicColor.Constant.BASIC_LANDS) { for (final String name : MagicColor.Constant.BASIC_LANDS) {
if (!CardLists.getType(landList, name).isEmpty()) { if (!CardLists.getType(landList, name).isEmpty()) {
basics.add(name); basics.add(name);
} }
} }
if (!basics.isEmpty()) { if (!basics.isEmpty()) {
for (int i = 0; i < MagicColor.Constant.BASIC_LANDS.size(); i++) { // Which basic land is least available
String b = MagicColor.Constant.BASIC_LANDS.get(i); int minSize = Integer.MAX_VALUE;
String minType = null;
for (String b : basics) {
final int num = CardLists.getType(landsInBattlefield, b).size(); final int num = CardLists.getType(landsInBattlefield, b).size();
basic_counts[i] = num; if (num < minSize) {
} minType = b;
} minSize = 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;
} }
} }
// TODO handle fetchlands and what they can fetch for if (minType != null) {
// determine new color pips landList = CardLists.getType(landList, minType);
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;
}
}
}
} }
// use 1 / x+1 for diminishing returns // pick dual lands if available
// TODO use max pips of each color in the deck from deck statistics to weight this if (Iterables.any(landList, Predicates.not(CardPredicates.Presets.BASIC_LANDS))) {
for (int i = 0; i < card_counts.length; i++) { landList = CardLists.filter(landList, Predicates.not(CardPredicates.Presets.BASIC_LANDS));
int diff = (card_counts[i] * 50) / (counts[i] + 1);
score += diff;
} }
}
// TODO utility lands only if we have enough to pay their costs return ComputerUtilCard.getBestLandToPlayAI(landList);
// TODO Tron lands and other lands that care about land counts
return score;
}));
return toReturn;
} }
// if return true, go to next phase // if return true, go to next phase
@@ -733,7 +622,7 @@ public class AiController {
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(possibleCounters, player)) { for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(possibleCounters, player)) {
SpellAbility currentSA = sa; SpellAbility currentSA = sa;
sa.setActivatingPlayer(player); sa.setActivatingPlayer(player, true);
// check everything necessary // check everything necessary
AiPlayDecision opinion = canPlayAndPayFor(currentSA); AiPlayDecision opinion = canPlayAndPayFor(currentSA);
@@ -788,7 +677,7 @@ public class AiController {
if (saApi == ApiType.Counter || saApi == exceptSA) { if (saApi == ApiType.Counter || saApi == exceptSA) {
continue; continue;
} }
sa.setActivatingPlayer(player); sa.setActivatingPlayer(player, true);
// TODO: this currently only works as a limited prediction of permanent spells. // 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, // 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. // 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); return reserveManaSources(sa, phaseType, enemy, true, null);
} }
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) { 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); CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
// used for chained spells where two spells need to be cast in succession // used for chained spells where two spells need to be cast in succession
if (exceptForThisSa != null) { if (exceptForThisSa != null) {
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost( manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(ComputerUtilMana.calculateManaCost(exceptForThisSa, true, 0), exceptForThisSa, player));
ComputerUtilMana.calculateManaCost(exceptForThisSa.getPayCosts(), exceptForThisSa, true, 0, false),
exceptForThisSa, player));
} }
if (manaSources.isEmpty()) { if (manaSources.isEmpty()) {
@@ -868,15 +755,9 @@ public class AiController {
if (currentState != null) { if (currentState != null) {
host.setState(sa.getCardStateName(), false); host.setState(sa.getCardStateName(), false);
} }
if (sa.isSpell()) {
host.setCastSA(sa);
}
AiPlayDecision decision = canPlayAndPayForFace(sa); AiPlayDecision decision = canPlayAndPayForFace(sa);
if (sa.isSpell()) {
host.setCastSA(null);
}
if (currentState != null) { if (currentState != null) {
host.setState(currentState, false); host.setState(currentState, false);
} }
@@ -921,7 +802,7 @@ public class AiController {
} }
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable // TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
if (!sa.getAllTargetChoices().isEmpty()) { 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 // check if some target raised cost
if (!xCost && oldCMC > -1) { 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) { if (finalCMC > oldCMC) {
xCost = true; xCost = true;
} }
@@ -1017,7 +898,7 @@ public class AiController {
Sentry.setExtra("Card", card.getName()); Sentry.setExtra("Card", card.getName());
Sentry.setExtra("SA", sa.toString()); 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 // remove added extra
Sentry.removeExtra("Card"); Sentry.removeExtra("Card");
@@ -1114,7 +995,7 @@ public class AiController {
costWithBuyback.add(opt.getCost()); costWithBuyback.add(opt.getCost());
} }
} }
costWithBuyback = CostAdjustment.adjust(costWithBuyback, sa, false); costWithBuyback = CostAdjustment.adjust(costWithBuyback, sa);
if (costWithBuyback.hasSpecificCostType(CostPayLife.class) if (costWithBuyback.hasSpecificCostType(CostPayLife.class)
|| costWithBuyback.hasSpecificCostType(CostDiscard.class) || costWithBuyback.hasSpecificCostType(CostDiscard.class)
|| costWithBuyback.hasSpecificCostType(CostSacrifice.class)) { || costWithBuyback.hasSpecificCostType(CostSacrifice.class)) {
@@ -1130,7 +1011,7 @@ public class AiController {
// Memory Crystal-like effects need special handling // Memory Crystal-like effects need special handling
for (Card c : game.getCardsIn(ZoneType.Battlefield)) { for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility s : c.getStaticAbilities()) { for (StaticAbility s : c.getStaticAbilities()) {
if (s.checkMode(StaticAbilityMode.ReduceCost) if ("ReduceCost".equals(s.getParam("Mode"))
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) { && "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s); neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
} }
@@ -1140,7 +1021,7 @@ public class AiController {
neededMana = 0; neededMana = 0;
} }
int hasMana = getAvailableManaEstimate(player, false); int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false);
if (hasMana < neededMana - 1) { if (hasMana < neededMana - 1) {
return true; return true;
} }
@@ -1194,7 +1075,7 @@ public class AiController {
if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) { if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) {
CardCollection discards = new CardCollection(); CardCollection discards = new CardCollection();
final CardCollectionView inHand = player.getCardsIn(ZoneType.Hand); 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; int numOppInHand = 0;
for (Player p : player.getGame().getPlayers()) { for (Player p : player.getGame().getPlayers()) {
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) { if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
@@ -1252,8 +1133,8 @@ public class AiController {
if (validCards.isEmpty()) { if (validCards.isEmpty()) {
continue; continue;
} }
final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA); final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(validCards, CardPredicates.LANDS); final CardCollection landsInHand = CardLists.filter(validCards, CardPredicates.Presets.LANDS);
final int numLandsInHand = landsInHand.size(); final int numLandsInHand = landsInHand.size();
// Discard a land // Discard a land
@@ -1395,9 +1276,9 @@ public class AiController {
if (spell instanceof SpellApiBased) { if (spell instanceof SpellApiBased) {
boolean chance = false; boolean chance = false;
if (withoutPayingManaCost) { if (withoutPayingManaCost) {
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory); chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerNoCostWithSubs(player, spell, mandatory);
} else { } else {
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory); chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory);
} }
if (!chance) { if (!chance) {
return AiPlayDecision.TargetingFailed; return AiPlayDecision.TargetingFailed;
@@ -1451,7 +1332,9 @@ public class AiController {
for (final Card element : combat.getAttackers()) { for (final Card element : combat.getAttackers()) {
// tapping of attackers happens after Propaganda is paid for // 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()) { if (landsWannaPlay != null && !landsWannaPlay.isEmpty()) {
// TODO search for other land it might want to play? // TODO search for other land it might want to play?
Card land = chooseBestLandToPlay(landsWannaPlay); 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))) { && (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land))) {
final List<SpellAbility> abilities = land.getAllPossibleAbilities(player, true); final List<SpellAbility> abilities = land.getAllPossibleAbilities(player, true);
// skip non Land Abilities // skip non Land Abilities
@@ -1528,11 +1411,12 @@ public class AiController {
return false; 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); CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
if (getBooleanProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) { 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; return false;
} }
} }
@@ -1549,12 +1433,12 @@ public class AiController {
int minCMCInHand = Aggregates.min(inHand, Card::getCMC); int minCMCInHand = Aggregates.min(inHand, Card::getCMC);
if (minCMCInHand == Integer.MAX_VALUE) if (minCMCInHand == Integer.MAX_VALUE)
minCMCInHand = 0; minCMCInHand = 0;
int predictedMana = getAvailableManaEstimate(player, true); int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && minCMCInHand > 0 && !isTapLand; boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && minCMCInHand > 0 && !isTapLand;
boolean cantCastAnythingNow = predictedMana < minCMCInHand; boolean cantCastAnythingNow = predictedMana < minCMCInHand;
boolean hasRelevantAbsOTB = otb.anyMatch(card -> { boolean hasRelevantAbsOTB = Iterables.any(otb, card -> {
boolean isTapLand1 = false; boolean isTapLand1 = false;
for (ReplacementEffect repl : card.getReplacementEffects()) { for (ReplacementEffect repl : card.getReplacementEffects()) {
// TODO: improve the detection of taplands // TODO: improve the detection of taplands
@@ -1575,7 +1459,7 @@ public class AiController {
return false; return false;
}); });
boolean hasLandBasedEffect = otb.anyMatch(card -> { boolean hasLandBasedEffect = Iterables.any(otb, card -> {
for (Trigger t : card.getTriggers()) { for (Trigger t : card.getTriggers()) {
Map<String, String> params = t.getMapParams(); Map<String, String> params = t.getMapParams();
if ("ChangesZone".equals(params.get("Mode")) if ("ChangesZone".equals(params.get("Mode"))
@@ -1627,13 +1511,6 @@ public class AiController {
} }
private SpellAbility getSpellAbilityToPlay() { 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); CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
cards = ComputerUtilCard.dedupeCards(cards); cards = ComputerUtilCard.dedupeCards(cards);
List<SpellAbility> saList = Lists.newArrayList(); List<SpellAbility> saList = Lists.newArrayList();
@@ -1677,16 +1554,12 @@ public class AiController {
saList = ComputerUtilAbility.getSpellAbilities(cards, player); 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? // TODO allow when experimental profile?
return spellAbility.isLandAbility() || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard())); 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 //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); SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true);
@@ -1709,9 +1582,6 @@ public class AiController {
String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all); String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all);
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex); 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... //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); boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) { for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1722,25 +1592,21 @@ public class AiController {
if (sa.getHostCard().hasKeyword(Keyword.STORM) if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell && sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& player.getZone(ZoneType.Hand).contains( && player.getZone(ZoneType.Hand).contains(Predicates.not(Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.hasKeyword("Storm"))))) {
Predicate.not(CardPredicates.LANDS.or(CardPredicates.hasKeyword("Storm")))
)) {
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) { if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
// skip evaluating Storm unless we reached the minimum Storm count // skip evaluating Storm unless we reached the minimum Storm count
continue; continue;
} }
} }
// living end AI decks // living end AI decks
// TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed // TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed
AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa; AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa;
if (useLivingEnd) { if (useLivingEnd) {
if (sa.isCycling() && sa.canCastTiming(player) if (sa.isCycling() && sa.canCastTiming(player) && player.getCardsIn(ZoneType.Library).size() >= 10) {
&& player.getCardsIn(ZoneType.Library).size() >= 10) {
if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) { if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class) if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)
&& !player.cantLoseForZeroOrLessLife() && player.getLife() <= sa.getPayCosts() && !player.cantLoseForZeroOrLessLife()
.getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) { && player.getLife() <= sa.getPayCosts().getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
aiPlayDecision = AiPlayDecision.CantAfford; aiPlayDecision = AiPlayDecision.CantAfford;
} else { } else {
aiPlayDecision = AiPlayDecision.WillPlay; aiPlayDecision = AiPlayDecision.WillPlay;
@@ -1748,24 +1614,19 @@ public class AiController {
} }
} else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) { } else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) {
if (isLifeInDanger) { //needs more tune up for certain conditions if (isLifeInDanger) { //needs more tune up for certain conditions
aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa : AiPlayDecision.WillPlay;
: AiPlayDecision.WillPlay; } else if (CardLists.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES).size() > 4) {
} else if (CardLists
.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES)
.size() > 4) {
if (player.getCreaturesInPlay().size() >= 4) // it's good minimum if (player.getCreaturesInPlay().size() >= 4) // it's good minimum
continue; continue;
else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) && ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
&& ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) aiPlayDecision = AiPlayDecision.WillPlay;// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
aiPlayDecision = AiPlayDecision.WillPlay;
// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
} else { } else {
continue; continue;
} }
} }
} }
sa.setActivatingPlayer(player); sa.setActivatingPlayer(player, true);
SpellAbility root = sa.getRootAbility(); SpellAbility root = sa.getRootAbility();
if (root.isSpell() || root.isTrigger() || root.isReplacementAbility()) { if (root.isSpell() || root.isTrigger() || root.isReplacementAbility()) {
@@ -1787,15 +1648,6 @@ public class AiController {
} }
return null; 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) { public CardCollection chooseCardsToDelve(int genericCost, CardCollection grave) {
@@ -1831,7 +1683,7 @@ public class AiController {
if (spell instanceof WrappedAbility) if (spell instanceof WrappedAbility)
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory); return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
if (spell.getApi() != null) 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()) { 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 // For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
return true; return true;
@@ -2071,7 +1923,7 @@ public class AiController {
} }
// AI has decided to help. Now let's figure out how much they can help // 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? // 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? // 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; break;
} }
} else { } 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); Card best = ComputerUtilCard.getBestAI(viableOptions);
if (best != null) { if (best != null) {
result.add(best); result.add(best);
@@ -2209,7 +2061,7 @@ public class AiController {
CardLists.shuffle(library); CardLists.shuffle(library);
// remove all land, keep non-basicland in there, shuffled // 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) { for (Card c : land) {
if (c.isLand()) { if (c.isLand()) {
library.remove(c); library.remove(c);
@@ -2259,7 +2111,7 @@ public class AiController {
} }
} }
if ("Aminatou".equals(sa.getParam("AILogic")) && game.getPlayers().size() > 2) { 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 left = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Left));
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Right)); CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Right));
return Aggregates.sum(left, Card::getCMC) > Aggregates.sum(right, Card::getCMC); return Aggregates.sum(left, Card::getCMC) > Aggregates.sum(right, Card::getCMC);
@@ -2315,6 +2167,8 @@ public class AiController {
return activePlayerSAs; return activePlayerSAs;
} }
List<SpellAbility> result = Lists.newArrayList();
// filter list by ApiTypes // filter list by ApiTypes
List<SpellAbility> discard = filterListByApi(activePlayerSAs, ApiType.Discard); List<SpellAbility> discard = filterListByApi(activePlayerSAs, ApiType.Discard);
List<SpellAbility> mandatoryDiscard = filterList(discard, SpellAbilityPredicates.isMandatory()); List<SpellAbility> mandatoryDiscard = filterList(discard, SpellAbilityPredicates.isMandatory());
@@ -2330,48 +2184,48 @@ public class AiController {
List<SpellAbility> pump = filterListByApi(activePlayerSAs, ApiType.Pump); List<SpellAbility> pump = filterListByApi(activePlayerSAs, ApiType.Pump);
List<SpellAbility> pumpAll = filterListByApi(activePlayerSAs, ApiType.PumpAll); 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 // do mandatory discard early if hand is empty or has DiscardMe card
boolean discardEarly = false;
CardCollectionView playerHand = player.getCardsIn(ZoneType.Hand); 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); result.addAll(mandatoryDiscard);
mandatoryDiscard.clear();
} }
// optional Discard, probably combined with Draw // token should be added first so they might get the pump bonus
result.addAll(discard); 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 // do Draw before Discard
result.addAll(draw); result.addAll(draw);
result.addAll(discard); // optional Discard, probably combined with Draw
result.addAll(putCounterAll); if (!discardEarly) {
// 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);
result.addAll(mandatoryDiscard); result.addAll(mandatoryDiscard);
}
result.addAll(activePlayerSAs);
//need to reverse because of magic stack
Collections.reverse(result);
return result; return result;
} }
// TODO move to more common place // TODO move to more common place
private static <T> List<T> filterList(List<T> input, Predicate<? super T> pred) { 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); input.removeAll(filtered);
return filtered; return filtered;
} }
// TODO move to more common place // 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) { public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
return filterList(input, SpellAbilityPredicates.isApi(type)); return filterList(input, SpellAbilityPredicates.isApi(type));
} }
@@ -2408,11 +2262,12 @@ public class AiController {
public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> list) { public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> list) {
// no need to choose anything // no need to choose anything
if (list.size() <= 1) { 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)) { if (mode.equals(ReplacementType.GainLife)) {
List<ReplacementEffect> noGain = filterListByAiLogic(list, "NoLife"); List<ReplacementEffect> noGain = filterListByAiLogic(list, "NoLife");
List<ReplacementEffect> loseLife = filterListByAiLogic(list, "LoseLife"); List<ReplacementEffect> loseLife = filterListByAiLogic(list, "LoseLife");
@@ -2421,16 +2276,16 @@ public class AiController {
if (!noGain.isEmpty()) { if (!noGain.isEmpty()) {
// no lifegain is better than lose life // no lifegain is better than lose life
return noGain.get(0); return Iterables.getFirst(noGain, null);
} else if (!loseLife.isEmpty()) { } else if (!loseLife.isEmpty()) {
// lose life before double life to prevent lose double // lose life before double life to prevent lose double
return loseLife.get(0); return Iterables.getFirst(loseLife, null);
} else if (!lichDraw.isEmpty()) { } else if (!lichDraw.isEmpty()) {
// lich draw before double life to prevent to draw to much // lich draw before double life to prevent to draw to much
return lichDraw.get(0); return Iterables.getFirst(lichDraw, null);
} else if (!doubleLife.isEmpty()) { } else if (!doubleLife.isEmpty()) {
// other than that, do double life // other than that, do double life
return doubleLife.get(0); return Iterables.getFirst(doubleLife, null);
} }
} else if (mode.equals(ReplacementType.DamageDone)) { } else if (mode.equals(ReplacementType.DamageDone)) {
List<ReplacementEffect> prevention = filterList(list, CardTraitPredicates.hasParam("Prevent")); List<ReplacementEffect> prevention = filterList(list, CardTraitPredicates.hasParam("Prevent"));
@@ -2438,45 +2293,40 @@ public class AiController {
// TODO when Protection is done as ReplacementEffect do them // TODO when Protection is done as ReplacementEffect do them
// before normal prevention // before normal prevention
if (!prevention.isEmpty()) { if (!prevention.isEmpty()) {
return prevention.get(0); return Iterables.getFirst(prevention, null);
} }
} else if (mode.equals(ReplacementType.Destroy)) { } else if (mode.equals(ReplacementType.Destroy)) {
List<ReplacementEffect> shield = filterList(list, CardTraitPredicates.hasParam("ShieldCounter")); List<ReplacementEffect> shield = filterList(list, CardTraitPredicates.hasParam("ShieldCounter"));
List<ReplacementEffect> regeneration = filterList(list, CardTraitPredicates.hasParam("Regeneration")); List<ReplacementEffect> regeneration = filterList(list, CardTraitPredicates.hasParam("Regeneration"));
List<ReplacementEffect> umbraArmor = filterList(list, CardTraitPredicates.isKeyword(Keyword.UMBRA_ARMOR)); 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 // Indestructible umbra armor is the best
if (!umbraArmorIndestructible.isEmpty()) { if (!umbraArmorIndestructible.isEmpty()) {
return umbraArmorIndestructible.get(0); return Iterables.getFirst(umbraArmorIndestructible, null);
} }
// then it might be better to remove shield counter if able? // then it might be better to remove shield counter if able?
if (!shield.isEmpty()) { 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 // 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? // is using a Regeneration Effect better than using a Umbra Armor?
if (!regeneration.isEmpty()) { if (!regeneration.isEmpty()) {
return regeneration.get(0); return Iterables.getFirst(regeneration, null);
} }
if (!umbraArmor.isEmpty()) { if (!umbraArmor.isEmpty()) {
// sort them by cmc // sort them by cmc
umbraArmor.sort(Comparator.comparing(CardTraitBase::getHostCard, Comparator.comparing(Card::getCMC))); umbraArmor.sort(Comparator.comparing(CardTraitBase::getHostCard, Comparator.comparing(Card::getCMC)));
return umbraArmor.get(0); return Iterables.getFirst(umbraArmor, null);
}
} else if (mode.equals(ReplacementType.Draw)) {
List<ReplacementEffect> winGame = filterList(list, SpellAbility::getApi, ApiType.WinsGame);
if (!winGame.isEmpty()) {
return winGame.get(0);
} }
} }
// TODO always lower counters with Vorinclex first, might turn it from 1 to 0 as final // 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; package forge.ai;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.AiCardMemory.MemorySet; import forge.ai.AiCardMemory.MemorySet;
@@ -46,14 +47,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c); 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 @Override
public PaymentDecision visit(CostChooseColor cost) { public PaymentDecision visit(CostChooseColor cost) {
int c = cost.getAbilityAmount(ability); int c = cost.getAbilityAmount(ability);
@@ -64,7 +57,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostChooseCreatureType cost) { 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); return PaymentDecision.type(choice);
} }
@@ -117,13 +111,13 @@ public class AiCostDecision extends CostDecisionMakerBase {
Card chosen; Card chosen;
if (!discardMe.isEmpty()) { if (!discardMe.isEmpty()) {
chosen = Aggregates.random(discardMe); chosen = Aggregates.random(discardMe);
discardMe = CardLists.filter(discardMe, CardPredicates.sharesNameWith(chosen).negate()); discardMe = CardLists.filter(discardMe, Predicates.not(CardPredicates.sharesNameWith(chosen)));
} else { } else {
final Card worst = ComputerUtilCard.getWorstAI(hand); final Card worst = ComputerUtilCard.getWorstAI(hand);
chosen = worst != null ? worst : Aggregates.random(hand); chosen = worst != null ? worst : Aggregates.random(hand);
} }
differentNames.add(chosen); differentNames.add(chosen);
hand = CardLists.filter(hand, CardPredicates.sharesNameWith(chosen).negate()); hand = CardLists.filter(hand, Predicates.not(CardPredicates.sharesNameWith(chosen)));
c--; c--;
} }
return PaymentDecision.card(differentNames); return PaymentDecision.card(differentNames);

View File

@@ -17,18 +17,19 @@
*/ */
package forge.ai; 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.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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. * Holds default AI personality profile values in an enum.
* Loads profile from the given text file when setProfile is called. * Loads profile from the given text file when setProfile is called.

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,20 @@
package forge.ai; package forge.ai;
import java.util.Collection; import com.google.common.base.Predicates;
import java.util.List; import com.google.common.collect.Iterables;
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.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.ai.AiCardMemory.MemorySet; import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi; import forge.ai.ability.AnimateAi;
import forge.ai.ability.TokenAi;
import forge.card.ColorSet;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*; import forge.game.cost.*;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -25,9 +23,15 @@ import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices; import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.IterableUtil;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil; 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 { public class ComputerUtilCost {
@@ -50,7 +54,8 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { 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(); final CounterType type = addCounter.getCounter();
if (type.is(CounterEnumType.M1M1)) { if (type.is(CounterEnumType.M1M1)) {
@@ -76,7 +81,9 @@ public class ComputerUtilCost {
} }
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false); final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
for (final CostPart part : cost.getCostParts()) { 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; final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) { if (!part.payCostFromSource()) {
if (type.is(CounterEnumType.P1P1)) { if (type.is(CounterEnumType.P1P1)) {
@@ -103,7 +110,9 @@ public class ComputerUtilCost {
&& !source.hasKeyword(Keyword.UNDYING)) { && !source.hasKeyword(Keyword.UNDYING)) {
return false; return false;
} }
} else if (part instanceof CostRemoveAnyCounter remCounter) { } else if (part instanceof CostRemoveAnyCounter) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision pay = decision.visit(remCounter); PaymentDecision pay = decision.visit(remCounter);
return pay != null; return pay != null;
} }
@@ -128,14 +137,10 @@ public class ComputerUtilCost {
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand)); CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
for (final CostPart part : cost.getCostParts()) { 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 String type = disc.getType();
final CardCollection typeList;
int num;
if (type.equals("Hand")) {
typeList = hand;
num = hand.size();
} else {
if (type.equals("CARDNAME")) { if (type.equals("CARDNAME")) {
if (source.getAbilityText().contains("Bloodrush")) { if (source.getAbilityText().contains("Bloodrush")) {
continue; continue;
@@ -145,12 +150,12 @@ public class ComputerUtilCost {
return true; 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()) { if (typeList.size() > ai.getMaxHandSize()) {
continue; continue;
} }
num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa); int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
}
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList); Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
if (pref == null) { if (pref == null) {
@@ -180,7 +185,8 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { 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); int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
if (ai.getLife() - realDamage < remainingLife if (ai.getLife() - realDamage < remainingLife
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife() && realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
@@ -212,8 +218,13 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife payLife) { if (part instanceof CostPayLife) {
int amount = payLife.getAbilityAmount(sourceAbility); 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 // check if there's override for the remainingLife threshold
if (sourceAbility != null && sourceAbility.hasParam("AILifeThreshold")) { if (sourceAbility != null && sourceAbility.hasParam("AILifeThreshold")) {
@@ -286,7 +297,8 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { 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); final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
if (sac.payCostFromSource() && source.isCreature()) { if (sac.payCostFromSource() && source.isCreature()) {
@@ -335,11 +347,12 @@ public class ComputerUtilCost {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice sac) { if (part instanceof CostSacrifice) {
if (suppressRecursiveSacCostCheck) { if (suppressRecursiveSacCostCheck) {
return false; return false;
} }
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility); final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
String type = sac.getType(); String type = sac.getType();
@@ -515,18 +528,14 @@ public class ComputerUtilCost {
* @return a boolean. * @return a boolean.
*/ */
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) { 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) { 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; boolean cannotBeCountered = false;
// Check for stuff like Nether Void // Check for stuff like Nether Void
int extraManaNeeded = 0; int extraManaNeeded = 0;
if (!effect) {
if (sa instanceof Spell) { if (sa instanceof Spell) {
cannotBeCountered = !sa.isCounterableBy(null); cannotBeCountered = !sa.isCounterableBy(null);
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) { for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
@@ -564,7 +573,7 @@ public class ComputerUtilCost {
// Try not to lose Planeswalker if not threatened // Try not to lose Planeswalker if not threatened
if (sa.isPwAbility()) { if (sa.isPwAbility()) {
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) { if (part instanceof CostRemoveCounter) {
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) { if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
// refuse to pay if opponent has no creature threats or // 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 // Bail early on Casualty in case there are no cards that would make sense to pay with
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) { if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostSacrifice) { if (part instanceof CostSacrifice) {
CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"), CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
sa.getActivatingPlayer(), sa.getHostCard(), sa); 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()) { if (valid.isEmpty()) {
return false; return false;
} }
} }
} }
} }
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect);
} }
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect) public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player); 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) { public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand)); return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
} }
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) { 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(); Set<String> colorsAvailable = Sets.newHashSet();
if (additionalLands != null) { if (additionalLands != null) {
@@ -705,8 +897,8 @@ public class ComputerUtilCost {
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) { public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
if (source.usesTargeting()) { if (source.usesTargeting()) {
final CardCollectionView targets = source.getTargets().getTargetCards(); final CardCollection targets = new CardCollection(source.getTargets().getTargetCards());
choices = IterableUtil.filter(choices, Predicate.not(CardPredicates.isController(ai).and(targets::contains))); choices = Iterables.filter(choices, Predicates.not(Predicates.and(CardPredicates.isController(ai), Predicates.in(targets))));
} }
return new CardCollection(choices); return new CardCollection(choices);
} }

View File

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

View File

@@ -1,5 +1,7 @@
package forge.ai; package forge.ai;
import com.google.common.base.Function;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
@@ -11,11 +13,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityCantAttackBlock; import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMustAttack; import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import java.util.List; import java.util.List;
import java.util.function.Function;
public class CreatureEvaluator implements Function<Card, Integer> { public class CreatureEvaluator implements Function<Card, Integer> {
@Override @Override
@@ -160,6 +159,12 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(20, "protection"); 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 // paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) { if (c.isPaired()) {
value += addValue(14, "paired"); value += addValue(14, "paired");
@@ -207,7 +212,11 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(1, "untapped"); 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()) { if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
} else { } else {
@@ -216,64 +225,30 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else { } else {
value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned"); value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
} }
if (c.hasSVar("EndOfTurnLeavePlay")) {
for (final SpellAbility sa : c.getSpellAbilities()) { value -= subValue(50, "eot-leaves");
if (sa.isAbility()) { } else if (c.hasKeyword(Keyword.CUMULATIVE_UPKEEP)) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa); 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.hasKeyword(Keyword.VANISHING)) {
if (!c.getManaAbilities().isEmpty()) { value -= subValue(20 / (Math.max(1, c.getCounters(CounterEnumType.TIME))), "vanishing");
value += addValue(10, "manadork");
} }
// use scaling because the creature is only available halfway // use scaling because the creature is only available halfway
if (c.hasKeyword(Keyword.PHASING)) { if (c.hasKeyword(Keyword.PHASING)) {
value -= subValue(Math.max(20, value / 2), "phasing"); value -= subValue(Math.max(20, value / 2), "phasing");
} }
if (c.hasSVar("EndOfTurnLeavePlay")) { // TODO no longer a KW
value -= subValue(50, "eot-leaves"); if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
} 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()) {
value -= subValue(20, "upkeep-dmg"); 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 // card-specific evaluation modifier
if (c.hasSVar("AIEvaluationModifier")) { 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 // default value
return 10; return 10;
} }

View File

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

View File

@@ -1,5 +1,7 @@
package forge.ai; package forge.ai;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.*; import com.google.common.collect.*;
import forge.LobbyPlayer; import forge.LobbyPlayer;
import forge.ai.ability.ProtectAi; import forge.ai.ability.ProtectAi;
@@ -15,14 +17,13 @@ import forge.game.*;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostEnlist; import forge.game.cost.CostEnlist;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana; import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana; import forge.game.mana.Mana;
@@ -34,21 +35,21 @@ import forge.game.player.*;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.*; import forge.game.spellability.*;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility; import forge.game.trigger.WrappedAbility;
import forge.game.zone.PlayerZone; import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.item.PaperCard; 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.FCollection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import java.security.InvalidParameterException;
import java.util.*; 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); 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 @Override
public boolean helpPayForAssistSpell(ManaCostBeingPaid cost, SpellAbility sa, int max, int requested) { public boolean helpPayForAssistSpell(ManaCostBeingPaid cost, SpellAbility sa, int max, int requested) {
int toPay = getAi().attemptToAssist(sa, max, requested); int toPay = getAi().attemptToAssist(sa, max, requested);
@@ -352,7 +345,11 @@ public class PlayerControllerAi extends PlayerController {
if (delayedReveal != null) { if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); 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 @Override
@@ -394,7 +391,11 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title, public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params) { 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 @Override
@@ -454,13 +455,13 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public boolean confirmPayment(CostPart costPart, String prompt, SpellAbility sa) { public Player chooseStartingPlayer(boolean isFirstgame) {
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method) return this.player; // AI is brave :)
} }
@Override @Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) { public CardCollection orderBlockers(Card attacker, CardCollection blockers) {
return brains.aiShouldRun(replacementEffect, effectSA, affected); return AiBlockController.orderBlockers(attacker, blockers);
} }
@Override @Override
@@ -483,11 +484,6 @@ public class PlayerControllerAi extends PlayerController {
return chosenAttackers; return chosenAttackers;
} }
@Override
public CardCollection orderBlockers(Card attacker, CardCollection blockers) {
return AiBlockController.orderBlockers(attacker, blockers);
}
@Override @Override
public CardCollection orderBlocker(Card attacker, Card blocker, CardCollection oldBlockers) { public CardCollection orderBlocker(Card attacker, Card blocker, CardCollection oldBlockers) {
return AiBlockController.orderBlocker(attacker, blocker, oldBlockers); return AiBlockController.orderBlocker(attacker, blocker, oldBlockers);
@@ -574,7 +570,7 @@ public class PlayerControllerAi extends PlayerController {
if (destinationZone == ZoneType.Graveyard) { if (destinationZone == ZoneType.Graveyard) {
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the 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 // 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"); 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 (!p.isOpponentOf(player)) {
if (landsOTB <= 2) { if (landsOTB <= 2) {
@@ -679,6 +675,20 @@ public class PlayerControllerAi extends PlayerController {
: ComputerUtil.getCardsToDiscardFromOpponent(player, p, sa, validCards, min, max); : 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 @Override
public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) { public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) {
if (canSetupTargets) if (canSetupTargets)
@@ -693,7 +703,7 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) { 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)) { if (!Iterables.isEmpty(cardsOfType)) {
Card toDiscard = Aggregates.itemWithMin(cardsOfType, Card::getCMC); Card toDiscard = Aggregates.itemWithMin(cardsOfType, Card::getCMC);
return new CardCollection(toDiscard); return new CardCollection(toDiscard);
@@ -707,8 +717,8 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, boolean isOptional) { public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes, boolean isOptional) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa, validTypes); String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa, validTypes, invalidTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) { if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) {
chosen = validTypes.iterator().next(); chosen = validTypes.iterator().next();
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: " + chosen); 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); 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 @Override
public PlanarDice choosePDRollToIgnore(List<PlanarDice> rolls) { public PlanarDice choosePDRollToIgnore(List<PlanarDice> rolls) {
//TODO create AI logic for this //TODO create AI logic for this
@@ -746,30 +748,6 @@ public class PlayerControllerAi extends PlayerController {
return Aggregates.random(rolls); 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 @Override
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) { public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn); return !ComputerUtil.wantMulligan(player, cardsToReturn);
@@ -786,13 +764,13 @@ public class PlayerControllerAi extends PlayerController {
for (int i = 0; i < cardsToReturn; i++) { for (int i = 0; i < cardsToReturn; i++) {
hand.removeAll(toReturn); hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, CardPredicates.LANDS); CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.count(toReturn, CardPredicates.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 we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) { if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {
CardCollection producingLands = CardLists.filter(landsInHand, CardPredicates.LANDS_PRODUCING_MANA); CardCollection producingLands = CardLists.filter(landsInHand, Presets.LANDS_PRODUCING_MANA);
CardCollection nonProducingLands = CardLists.filter(landsInHand, CardPredicates.LANDS_PRODUCING_MANA.negate()); CardCollection nonProducingLands = CardLists.filter(landsInHand, Predicates.not(Presets.LANDS_PRODUCING_MANA));
Card worstLand = nonProducingLands.isEmpty() ? ComputerUtilCard.getWorstLand(producingLands) Card worstLand = nonProducingLands.isEmpty() ? ComputerUtilCard.getWorstLand(producingLands)
: ComputerUtilCard.getWorstLand(nonProducingLands); : ComputerUtilCard.getWorstLand(nonProducingLands);
toReturn.add(worstLand); toReturn.add(worstLand);
@@ -860,8 +838,23 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public Player chooseStartingPlayer(boolean isFirstgame) { public boolean payManaOptional(Card c, Cost cost, SpellAbility sa, String prompt, ManaPaymentPurpose purpose) {
return this.player; // AI is brave :) // 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 @Override
@@ -880,11 +873,6 @@ public class PlayerControllerAi extends PlayerController {
return bestZone; return bestZone;
} }
@Override
public List<SpellAbility> chooseSaToActivateFromOpeningHand(List<SpellAbility> usableFromOpeningHand) {
return brains.chooseSaToActivateFromOpeningHand(usableFromOpeningHand);
}
@Override @Override
public int chooseNumber(SpellAbility sa, String title, int min, int max) { public int chooseNumber(SpellAbility sa, String title, int min, int max) {
return brains.chooseNumber(sa, title, min, max); return brains.chooseNumber(sa, title, min, max);
@@ -892,7 +880,11 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map<String, Object> params) { 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 @Override
@@ -977,6 +969,7 @@ public class PlayerControllerAi extends PlayerController {
} }
} }
return defaultVal != null && defaultVal; return defaultVal != null && defaultVal;
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
case LeftOrRight: return brains.chooseDirection(sa); case LeftOrRight: return brains.chooseDirection(sa);
case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even
default: default:
@@ -994,7 +987,11 @@ public class PlayerControllerAi extends PlayerController {
*/ */
@Override @Override
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Map<String, Object> params) { 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 @Override
@@ -1046,6 +1043,12 @@ public class PlayerControllerAi extends PlayerController {
return Iterables.getFirst(colors, MagicColor.WHITE); 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 @Override
public List<String> chooseColors(String message, SpellAbility sa, int min, int max, List<String> options) { public List<String> chooseColors(String message, SpellAbility sa, int min, int max, List<String> options) {
return ComputerUtilCard.chooseColor(sa, min, max, options); return ComputerUtilCard.chooseColor(sa, min, max, options);
@@ -1064,7 +1067,11 @@ public class PlayerControllerAi extends PlayerController {
if (options.size() <= 1) { if (options.size() <= 1) {
return Iterables.getFirst(options, null); 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 @Override
@@ -1073,7 +1080,7 @@ public class PlayerControllerAi extends PlayerController {
return Iterables.getFirst(options, null); return Iterables.getFirst(options, null);
} }
List<String> possible = Lists.newArrayList(); 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) { if (tgtCard != null) {
for (String kw : options) { for (String kw : options) {
if (tgtCard.hasKeyword(kw)) { if (tgtCard.hasKeyword(kw)) {
@@ -1127,9 +1134,19 @@ public class PlayerControllerAi extends PlayerController {
} }
if (!possible.isEmpty()) { if (!possible.isEmpty()) {
return Aggregates.random(possible); 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 @Override
@@ -1206,34 +1223,29 @@ public class PlayerControllerAi extends PlayerController {
return choice; 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 @Override
public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) { public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) {
if (SpellApiToAi.Converter.get(sa).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) { final Card source = sa.getHostCard();
if (!ComputerUtilCost.canPayCost(cost, sa, player, true)) { // TODO replace with EmptySa
return false; 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); if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
return pay.payComputerCosts(new AiCostDecision(player, sa, true)); 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; return false;
} }
@@ -1293,11 +1305,15 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public boolean playSaFromPlayEffect(SpellAbility tgtSA) { public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
boolean optional = !tgtSA.getPayCosts().isMandatory(); boolean optional = tgtSA.hasParam("Optional");
boolean noManaCost = tgtSA.hasParam("WithoutManaCost"); 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 // 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 (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
if (noManaCost) {
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
}
return ComputerUtil.playStack(tgtSA, player, getGame()); return ComputerUtil.playStack(tgtSA, player, getGame());
} }
return false; // didn't play spell 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 // Probably want to see if the face up pile has anything "worth it", then potentially take face down pile
return pile1.size() >= pile2.size(); return pile1.size() >= pile2.size();
} else { } 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 cmc1 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile1) : ComputerUtilCard.evaluatePermanentList(pile1);
int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2); int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2);
@@ -1366,6 +1382,11 @@ public class PlayerControllerAi extends PlayerController {
return losses; 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 @Override
public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise) { public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise) {
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
@@ -1400,11 +1421,6 @@ public class PlayerControllerAi extends PlayerController {
return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, improvise); 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 @Override
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) { public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
@@ -1419,14 +1435,14 @@ public class PlayerControllerAi extends PlayerController {
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa); 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 // If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name) // (otherwise the AI will spam the same name)
for (Card consp : player.getCardsIn(ZoneType.Command)) { 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(); String chosenName = consp.getNamedCard();
if (!chosenName.isEmpty()) { 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 { } else {
CardCollectionView list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents()); 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()) { if (!list.isEmpty()) {
return list.get(0).getName(); return list.get(0).getName();
} }
@@ -1506,18 +1522,30 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) { public String chooseCardName(SpellAbility sa, List<ICardFace> faces, String message) {
return SpellApiToAi.Converter.get(sa).chooseCardFace(player, sa, faces); 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 @Override
public ICardFace chooseSingleCardFace(SpellAbility sa, String message, Predicate<ICardFace> cpp, String name) { public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
throw new UnsupportedOperationException("Should not be called for AI"); // or implement it if you know how 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 @Override
public CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params) { 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 @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()); int i = MyRandom.getRandom().nextInt(dungeonNames.size());
return Card.fromPaperCard(dungeonCards.get(i), ai); return Card.fromPaperCard(dungeonCards.get(i), ai);
} catch (Exception e) {
return Card.fromPaperCard(Aggregates.random(dungeonCards), ai);
}
} }
@Override @Override
@@ -1569,7 +1592,32 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) { 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 @Override
@@ -1609,11 +1657,6 @@ public class PlayerControllerAi extends PlayerController {
return max; return max;
} }
@Override
public List<CostPart> orderCosts(List<CostPart> costs) {
return costs;
}
@Override @Override
public CardCollection chooseCardsForEffectMultiple(Map<String, CardCollection> validMap, SpellAbility sa, String title, boolean isOptional) { public CardCollection chooseCardsForEffectMultiple(Map<String, CardCollection> validMap, SpellAbility sa, String title, boolean isOptional) {
CardCollection choices = new CardCollection(); CardCollection choices = new CardCollection();

View File

@@ -17,6 +17,7 @@
*/ */
package forge.ai; package forge.ai;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi; import forge.ai.ability.AnimateAi;
@@ -45,7 +46,9 @@ import forge.game.spellability.SpellPermanent;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.zone.ZoneType; 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.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount; import forge.util.maps.MapToAmount;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -125,8 +128,8 @@ public class SpecialCardAi {
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true); CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
int numManaSrcs = manaSources.size(); int numManaSrcs = manaSources.size();
CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.NON_TOKEN, CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.Presets.NON_TOKEN,
CardPredicates.NON_LANDS, CardPredicates.isOwner(ai))); Predicates.not(CardPredicates.Presets.LANDS), CardPredicates.isOwner(ai)));
int numHighCMC = CardLists.count(allCards, CardPredicates.greaterCMC(5)); int numHighCMC = CardLists.count(allCards, CardPredicates.greaterCMC(5));
int numLowCMC = CardLists.count(allCards, CardPredicates.lessCMC(3)); int numLowCMC = CardLists.count(allCards, CardPredicates.lessCMC(3));
@@ -156,21 +159,21 @@ public class SpecialCardAi {
} }
int libsize = ai.getCardsIn(ZoneType.Library).size(); int libsize = ai.getCardsIn(ZoneType.Library).size();
final CardCollection hand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), final CardCollection hand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.or(
CardPredicates.INSTANTS_AND_SORCERIES); CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
if (!hand.isEmpty()) { if (!hand.isEmpty()) {
// has spell that can be cast in hand with put ability // 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; return false;
} }
// has spell that can be cast if one counter is removed // 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); sa.setXManaCostPaid(1);
return true; return true;
} }
} }
final CardCollection library = CardLists.filter(ai.getCardsIn(ZoneType.Library), final CardCollection library = CardLists.filter(ai.getCardsIn(ZoneType.Library), Predicates.or(
CardPredicates.INSTANTS_AND_SORCERIES); CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
if (!library.isEmpty()) { if (!library.isEmpty()) {
// get max cmc of instant or sorceries in the libary // get max cmc of instant or sorceries in the libary
int maxCMC = 0; int maxCMC = 0;
@@ -205,9 +208,9 @@ public class SpecialCardAi {
public static class ChainOfAcid { public static class ChainOfAcid {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS); CardPredicates.Presets.LANDS);
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), 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, // 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). // which it can only distinguish by their CMC, considering >CMC higher value).
@@ -331,13 +334,13 @@ public class SpecialCardAi {
// Deathgorge Scavenger // Deathgorge Scavenger
public static class DeathgorgeScavenger { public static class DeathgorgeScavenger {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
Card worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.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), CardPredicates.NON_CREATURES)); Card worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), Predicates.not(CardPredicates.Presets.CREATURES)));
if (worstCreat == null) { 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) { 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(); sa.resetTargets();
@@ -360,8 +363,8 @@ public class SpecialCardAi {
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) { public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.UNTAPPED.and( Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH)))); CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH))));
boolean hasUsefulBlocker = false; boolean hasUsefulBlocker = false;
for (Card c : flyingCreatures) { for (Card c : flyingCreatures) {
@@ -385,7 +388,7 @@ public class SpecialCardAi {
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) { if (donateTarget != null) {
// first filter for opponents which can be targeted by SA // 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)); PlayerPredicates.isTargetableBy(sa));
// All opponents have hexproof or something like that // 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 // 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())); PlayerPredicates.isNotCardInPlay(donateTarget.getName()));
// fall back to previous list // fall back to previous list
if (Iterables.isEmpty(oppTarget)) { 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) // select player with less lands on the field (helpful for Illusions of Grandeur and probably Pacts too)
Player opp = Collections.min(Lists.newArrayList(oppTarget), Player opp = Collections.min(Lists.newArrayList(oppTarget),
PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.LANDS)); PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.LANDS));
if (opp != null) { if (opp != null) {
sa.resetTargets(); sa.resetTargets();
@@ -582,9 +585,9 @@ public class SpecialCardAi {
Card bestBasic = null; Card bestBasic = null;
Card bestBasicSelfOnly = 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), CardCollection oppLands = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS_PRODUCING_MANA); CardPredicates.Presets.LANDS_PRODUCING_MANA);
int bestCount = 0; int bestCount = 0;
int bestSelfOnlyCount = 0; int bestSelfOnlyCount = 0;
@@ -630,7 +633,7 @@ public class SpecialCardAi {
} }
CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), 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); oppList = CardLists.filterPower(oppList, lowest.getNetPower() + 1);
if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) { 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, // 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 // otherwise the AI will fail to play the card and the card will disappear from the pool
return false; 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 // 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. // since the AI does not prioritize/value cards vs. permission at the moment.
return false; return false;
@@ -686,7 +689,7 @@ public class SpecialCardAi {
// Goblin Polka Band // Goblin Polka Band
public static class GoblinPolkaBand { public static class GoblinPolkaBand {
public static boolean consider(final Player ai, final SpellAbility sa) { 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 maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false);
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts); int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
@@ -787,7 +790,7 @@ public class SpecialCardAi {
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(),
sa.getParamOrDefault("ChangeNum", "1"), sa); sa.getParamOrDefault("ChangeNum", "1"), sa);
CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library),
CardPredicates.nameNotEquals(sa.getHostCard().getName())); Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName())));
lib.sort(CardLists.CmcComparatorInv); lib.sort(CardLists.CmcComparatorInv);
// Additional cards which are difficult to auto-classify but which are generally good to Intuition for // 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) { for (Card c1 : lib) {
if (c1.getName().equals(c.getName())) { 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)) { && ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) {
// Try not to search for things we already have in hand or that we can't cast // Try not to search for things we already have in hand or that we can't cast
libPriorityList.add(c1); libPriorityList.add(c1);
@@ -922,7 +925,7 @@ public class SpecialCardAi {
int aiBattlefieldPower = 0, aiGraveyardPower = 0; int aiBattlefieldPower = 0, aiGraveyardPower = 0;
int threshold = 320; // approximately a 4/4 Flying creature worth of extra value 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()) { if (aiCreaturesInGY.isEmpty()) {
// nothing in graveyard, so cut short // nothing in graveyard, so cut short
@@ -946,7 +949,7 @@ public class SpecialCardAi {
for (Card c : p.getCreaturesInPlay()) { for (Card c : p.getCreaturesInPlay()) {
playerPower += ComputerUtilCard.evaluateCreature(c); 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); tempGraveyardPower += ComputerUtilCard.evaluateCreature(c);
} }
if (playerPower > oppBattlefieldPower) { if (playerPower > oppBattlefieldPower) {
@@ -981,7 +984,7 @@ public class SpecialCardAi {
for (Card gate : availableGates) for (Card gate : availableGates)
{ {
if (!currentGates.anyMatch(CardPredicates.nameEquals(gate.getName()))) if (!Iterables.any(currentGates, CardPredicates.nameEquals(gate.getName())))
{ {
// Diversify our mana base // Diversify our mana base
return gate; return gate;
@@ -998,7 +1001,7 @@ public class SpecialCardAi {
// Scan the fetch list for a card with at least one activated ability. // 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 // 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) { 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()) { for (SpellAbility ab : c.getSpellAbilities()) {
if (ab.isActivatedAbility()) { if (ab.isActivatedAbility()) {
Player controller = c.getController(); Player controller = c.getController();
@@ -1059,7 +1062,7 @@ public class SpecialCardAi {
// In MoJhoSto, prefer Jhoira sorcery ability from time to time // In MoJhoSto, prefer Jhoira sorcery ability from time to time
if (source.getGame().getRules().hasAppliedVariant(GameType.MoJhoSto) 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(); AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
int chanceToPrefJhoira = aic.getIntProperty(AiProps.MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR); int chanceToPrefJhoira = aic.getIntProperty(AiProps.MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR);
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA); 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 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 // 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? // 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? // 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) if (ph.getNextTurn().equals(ai) && ph.is(PhaseType.MAIN2)
&& ai.getSpellsCastLastTurn() == 0 && ai.getSpellsCastLastTurn() == 0
@@ -1220,7 +1223,7 @@ public class SpecialCardAi {
final CardCollectionView cards = ai.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command)); final CardCollectionView cards = ai.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command));
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai); 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)) { for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana(); ManaCost cost = testSa.getPayCosts().getTotalMana();
@@ -1296,8 +1299,8 @@ public class SpecialCardAi {
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) { public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = sa.getParent().getTargetCard(); Card firstTgt = sa.getParent().getTargetCard();
CardCollection candidates = ai.getOpponents().getCardsIn(ZoneType.Battlefield).filter( Iterable<Card> candidates = Iterables.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
CardPredicates.sharesCardTypeWith(firstTgt).and(CardPredicates.isTargetableBy(sa))); Predicates.and(CardPredicates.sharesCardTypeWith(firstTgt), CardPredicates.isTargetableBy(sa)));
Card secondTgt = Aggregates.random(candidates); Card secondTgt = Aggregates.random(candidates);
if (secondTgt != null) { if (secondTgt != null) {
sa.resetTargets(); sa.resetTargets();
@@ -1317,7 +1320,7 @@ public class SpecialCardAi {
return false; 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; boolean hasBridge = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) { for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
@@ -1335,7 +1338,7 @@ public class SpecialCardAi {
} }
for (Player opp : ai.getOpponents()) { 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! // Always if enemy would die and we don't!
// TODO : predict actual damage instead of assuming it'll be 2*lands // 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 // 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) { public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollection oppTargetables = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa); CardCollection oppTargetables = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
CardCollection threats = CardLists.filter(oppTargetables, card -> !ComputerUtilCard.isUselessCreature(card.getController(), card)); 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?) // 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)); 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) { public static boolean consider(final Player ai, final SpellAbility sa) {
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY); int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
CardPredicates.CREATURES Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.lessCMC(loyalty - 1), card -> {
.and(CardPredicates.lessCMC(loyalty - 1))
.and(card -> {
final Card copy = CardCopyService.getLKICopy(card); final Card copy = CardCopyService.getLKICopy(card);
ComputerUtilCard.applyStaticContPT(ai.getGame(), copy, null); ComputerUtilCard.applyStaticContPT(ai.getGame(), copy, null);
return copy.getNetToughness() > 0; return copy.getNetToughness() > 0;
}) }));
);
CardLists.sortByCmcDesc(creaturesToGet); CardLists.sortByCmcDesc(creaturesToGet);
if (creaturesToGet.isEmpty()) { if (creaturesToGet.isEmpty()) {
@@ -1469,7 +1469,6 @@ public class SpecialCardAi {
if (best != null) { if (best != null) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(best); sa.getTargets().add(best);
sa.setXManaCostPaid(best.getCMC());
return true; 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 // 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 // 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) // 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 creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
CardCollectionView creatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.CREATURES); CardCollectionView creatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES);
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);
if (creatsInHand.isEmpty() || creatsInLib.isEmpty()) { return null; } if (creatsInHand.isEmpty() || creatsInLib.isEmpty()) { return null; }
@@ -1557,10 +1556,10 @@ public class SpecialCardAi {
} }
public static Card considerCardToGet(final Player ai, final SpellAbility sa) { 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; } 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) int numManaSrcs = ComputerUtilMana.getAvailableManaEstimate(ai, false)
+ Math.min(1, manaSrcsInHand.size()); + Math.min(1, manaSrcsInHand.size());
@@ -1604,8 +1603,8 @@ public class SpecialCardAi {
// The Scarab God // The Scarab God
public static class TheScarabGod { public static class TheScarabGod {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().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.CREATURES)); Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
sa.resetTargets(); sa.resetTargets();
if (bestOppCreat != null) { if (bestOppCreat != null) {
@@ -1706,7 +1705,7 @@ public class SpecialCardAi {
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard); CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
Card topGY = null; Card topGY = null;
Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand)); 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()) { if (!aiGY.isEmpty()) {
topGY = ai.getCardsIn(ZoneType.Graveyard).get(0); topGY = ai.getCardsIn(ZoneType.Graveyard).get(0);
@@ -1765,7 +1764,7 @@ public class SpecialCardAi {
// check if +1 would be sufficient // check if +1 would be sufficient
if (single != null) { if (single != null) {
// TODO use better logic to find the right Deal Damage Effect? // 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) { if (ugin_burn != null) {
// basic logic copied from DamageDealAi::dealDamageChooseTgtC // basic logic copied from DamageDealAi::dealDamageChooseTgtC
if (ugin_burn.canTarget(single)) { if (ugin_burn.canTarget(single)) {
@@ -1805,7 +1804,7 @@ public class SpecialCardAi {
int maxHandSize = ai.getMaxHandSize(); int maxHandSize = ai.getMaxHandSize();
// TODO: Any other bad effects like that? // 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), // 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. // and effects that draw an additional card whenever a card is drawn.

View File

@@ -1,19 +1,13 @@
package forge.ai; 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.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.ICardFace; import forge.card.ICardFace;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser; import forge.card.mana.ManaCostParser;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CardState; import forge.game.card.CardState;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -24,13 +18,14 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.SpellAbilityCondition;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; 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 * Base class for API-specific AI logic
@@ -86,9 +81,11 @@ public abstract class SpellAbilityAi {
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) { if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return false; return false;
} }
} else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) { } else {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false; return false;
} }
}
if (!checkApiLogic(ai, sa)) { if (!checkApiLogic(ai, sa)) {
return false; return false;
@@ -122,7 +119,7 @@ public abstract class SpellAbilityAi {
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (aiLogic.equals("CheckCondition")) { if (aiLogic.equals("CheckCondition")) {
SpellAbility saCopy = sa.copy(); SpellAbility saCopy = sa.copy();
saCopy.setActivatingPlayer(ai); saCopy.setActivatingPlayer(ai, true);
return saCopy.metConditions(); return saCopy.metConditions();
} }
@@ -258,7 +255,7 @@ public abstract class SpellAbilityAi {
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) { protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery()) return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed()) || (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)); || (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
} }
@@ -306,7 +303,7 @@ public abstract class SpellAbilityAi {
*/ */
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
final AbilitySub subAb = ab.getSubAbility(); 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) { 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; 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") @SuppressWarnings("unchecked")
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) { public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
boolean hasPlayer = false; boolean hasPlayer = false;
@@ -342,9 +320,9 @@ public abstract class SpellAbilityAi {
for (T ent : options) { for (T ent : options) {
if (ent instanceof Player) { if (ent instanceof Player) {
hasPlayer = true; hasPlayer = true;
} else if (ent instanceof Card card) { } else if (ent instanceof Card) {
hasCard = true; hasCard = true;
if (card.isPlaneswalker() || card.isBattle()) { if (((Card)ent).isPlaneswalker() || ((Card)ent).isBattle()) {
hasAttackableCard = true; hasAttackableCard = true;
} }
} }
@@ -411,33 +389,4 @@ public abstract class SpellAbilityAi {
public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<String, Object> params) { public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<String, Object> params) {
return MyRandom.getRandom().nextBoolean(); 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; package forge.ai;
import java.util.Map;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.ability.*; import forge.ai.ability.*;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.spellability.SpellAbility;
import forge.util.ReflectionUtil; import forge.util.ReflectionUtil;
import java.security.InvalidParameterException;
import java.util.Map;
public enum SpellApiToAi { public enum SpellApiToAi {
Converter; Converter;
@@ -23,14 +22,12 @@ public enum SpellApiToAi {
.put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class) .put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class)
.put(ApiType.AddPhase, AddPhaseAi.class) .put(ApiType.AddPhase, AddPhaseAi.class)
.put(ApiType.AddTurn, AddTurnAi.class) .put(ApiType.AddTurn, AddTurnAi.class)
.put(ApiType.AdvanceCrank, AdvanceCrankAi.class)
.put(ApiType.AlterAttribute, AlterAttributeAi.class) .put(ApiType.AlterAttribute, AlterAttributeAi.class)
.put(ApiType.Amass, AmassAi.class) .put(ApiType.Amass, AmassAi.class)
.put(ApiType.Animate, AnimateAi.class) .put(ApiType.Animate, AnimateAi.class)
.put(ApiType.AnimateAll, AnimateAllAi.class) .put(ApiType.AnimateAll, AnimateAllAi.class)
.put(ApiType.Attach, AttachAi.class) .put(ApiType.Attach, AttachAi.class)
.put(ApiType.Ascend, AlwaysPlayAi.class) .put(ApiType.Ascend, AlwaysPlayAi.class)
.put(ApiType.AssembleContraption, AssembleContraptionAi.class)
.put(ApiType.AssignGroup, AssignGroupAi.class) .put(ApiType.AssignGroup, AssignGroupAi.class)
.put(ApiType.Balance, BalanceAi.class) .put(ApiType.Balance, BalanceAi.class)
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class) .put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
@@ -41,7 +38,6 @@ public enum SpellApiToAi {
.put(ApiType.Branch, BranchAi.class) .put(ApiType.Branch, BranchAi.class)
.put(ApiType.Camouflage, ChooseCardAi.class) .put(ApiType.Camouflage, ChooseCardAi.class)
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class) .put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
.put(ApiType.ChangeSpeed, AlwaysPlayAi.class)
.put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class)
.put(ApiType.ChangeX, AlwaysPlayAi.class) .put(ApiType.ChangeX, AlwaysPlayAi.class)
.put(ApiType.ChangeZone, ChangeZoneAi.class) .put(ApiType.ChangeZone, ChangeZoneAi.class)
@@ -57,9 +53,8 @@ public enum SpellApiToAi {
.put(ApiType.ChooseSector, AlwaysPlayAi.class) .put(ApiType.ChooseSector, AlwaysPlayAi.class)
.put(ApiType.ChooseSource, ChooseSourceAi.class) .put(ApiType.ChooseSource, ChooseSourceAi.class)
.put(ApiType.ChooseType, ChooseTypeAi.class) .put(ApiType.ChooseType, ChooseTypeAi.class)
.put(ApiType.ClaimThePrize, AlwaysPlayAi.class)
.put(ApiType.Clash, ClashAi.class) .put(ApiType.Clash, ClashAi.class)
.put(ApiType.ClassLevelUp, ClassLevelUpAi.class) .put(ApiType.ClassLevelUp, AlwaysPlayAi.class)
.put(ApiType.Cleanup, AlwaysPlayAi.class) .put(ApiType.Cleanup, AlwaysPlayAi.class)
.put(ApiType.Cloak, CloakAi.class) .put(ApiType.Cloak, CloakAi.class)
.put(ApiType.Clone, CloneAi.class) .put(ApiType.Clone, CloneAi.class)
@@ -88,7 +83,6 @@ public enum SpellApiToAi {
.put(ApiType.EachDamage, DamageEachAi.class) .put(ApiType.EachDamage, DamageEachAi.class)
.put(ApiType.Effect, EffectAi.class) .put(ApiType.Effect, EffectAi.class)
.put(ApiType.Encode, EncodeAi.class) .put(ApiType.Encode, EncodeAi.class)
.put(ApiType.Endure, EndureAi.class)
.put(ApiType.EndCombatPhase, EndTurnAi.class) .put(ApiType.EndCombatPhase, EndTurnAi.class)
.put(ApiType.EndTurn, EndTurnAi.class) .put(ApiType.EndTurn, EndTurnAi.class)
.put(ApiType.ExchangeLife, LifeExchangeAi.class) .put(ApiType.ExchangeLife, LifeExchangeAi.class)
@@ -131,7 +125,6 @@ public enum SpellApiToAi {
.put(ApiType.Mutate, MutateAi.class) .put(ApiType.Mutate, MutateAi.class)
.put(ApiType.NameCard, ChooseCardNameAi.class) .put(ApiType.NameCard, ChooseCardNameAi.class)
//.put(ApiType.NoteCounters, AlwaysPlayAi.class) //.put(ApiType.NoteCounters, AlwaysPlayAi.class)
.put(ApiType.OpenAttraction, AssembleContraptionAi.class)
.put(ApiType.PeekAndReveal, PeekAndRevealAi.class) .put(ApiType.PeekAndReveal, PeekAndRevealAi.class)
.put(ApiType.PermanentCreature, PermanentCreatureAi.class) .put(ApiType.PermanentCreature, PermanentCreatureAi.class)
.put(ApiType.PermanentNoncreature, PermanentNoncreatureAi.class) .put(ApiType.PermanentNoncreature, PermanentNoncreatureAi.class)
@@ -211,14 +204,6 @@ public enum SpellApiToAi {
.put(ApiType.InternalRadiation, AlwaysPlayAi.class) .put(ApiType.InternalRadiation, AlwaysPlayAi.class)
.build()); .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) { public SpellAbilityAi get(final ApiType api) {
SpellAbilityAi result = apiToInstance.get(api); SpellAbilityAi result = apiToInstance.get(api);
if (null == result) { if (null == result) {

View File

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

View File

@@ -17,15 +17,16 @@
*/ */
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/** /**
* <p> * <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; package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -10,9 +13,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
public class AlterAttributeAi extends SpellAbilityAi { public class AlterAttributeAi extends SpellAbilityAi {
@Override @Override

View File

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

View File

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

View File

@@ -1,44 +1,51 @@
package forge.ai.ability; 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.Lists;
import com.google.common.collect.Sets; 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.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice; import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock; import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.MyRandom; 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 { public class AttachAi extends SpellAbilityAi {
/* (non-Javadoc) /* (non-Javadoc)
@@ -110,7 +117,7 @@ public class AttachAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source); final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
effectExile.setActivatingPlayer(ai); effectExile.setActivatingPlayer(ai, true);
final List<Card> targets = CardUtil.getValidCardsToTarget(effectExile); final List<Card> targets = CardUtil.getValidCardsToTarget(effectExile);
return !targets.isEmpty(); return !targets.isEmpty();
} }
@@ -133,7 +140,7 @@ public class AttachAi extends SpellAbilityAi {
int power = 0, toughness = 0; int power = 0, toughness = 0;
List<String> keywords = Lists.newArrayList(); List<String> keywords = Lists.newArrayList();
for (StaticAbility stAb : source.getStaticAbilities()) { for (StaticAbility stAb : source.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous)) { if ("Continuous".equals(stAb.getParam("Mode"))) {
if (stAb.hasParam("AddPower")) { if (stAb.hasParam("AddPower")) {
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb); power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
} }
@@ -310,8 +317,9 @@ public class AttachAi extends SpellAbilityAi {
String type = ""; String type = "";
for (final StaticAbility stAb : attachSource.getStaticAbilities()) { for (final StaticAbility stAb : attachSource.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddType")) { final Map<String, String> stab = stAb.getMapParams();
type = stAb.getParam("AddType"); 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) { 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 // 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 -> { final List<Card> prefList = CardLists.filter(list, c -> {
// Don't do Untapped Vigilance cards // 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; return false;
} }
@@ -420,10 +398,21 @@ public class AttachAi extends SpellAbilityAi {
return false; 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 false;
} }
}
}
return true; return true;
}); });
@@ -521,7 +510,7 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
evenBetterList = CardLists.filter(betterList, CardPredicates.UNTAPPED); evenBetterList = CardLists.filter(betterList, CardPredicates.Presets.UNTAPPED);
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
@@ -570,46 +559,28 @@ public class AttachAi extends SpellAbilityAi {
final Card attachSource) { final Card attachSource) {
// AI For choosing a Card to Animate. // AI For choosing a Card to Animate.
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
Card attachSourceLki = null; final Card attachSourceLki = CardCopyService.getLKICopy(attachSource);
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;
}
attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield)); 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 -> { List<Card> betterList = CardLists.filter(list, c -> {
final Card lki = CardCopyService.getLKICopy(c); final Card lki = CardCopyService.getLKICopy(c);
// need to fake it as if lki would be on the battlefield // need to fake it as if lki would be on the battlefield
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield)); lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered // Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
finalAttachSourceLki.clearRemembered(); attachSourceLki.clearRemembered();
finalAttachSourceLki.addRemembered(lki); attachSourceLki.addRemembered(lki);
// need to check what the cards would be on the battlefield // need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events // do not attach yet, that would cause Events
CardCollection preList = new CardCollection(lki); CardCollection preList = new CardCollection(lki);
preList.add(finalAttachSourceLki); preList.add(attachSourceLki);
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList); c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
boolean result = lki.canBeAttached(finalAttachSourceLki, null); boolean result = lki.canBeAttached(attachSourceLki, null);
//reset static abilities //reset static abilities
c.getGame().getAction().checkStaticAbilities(false); c.getGame().getAction().checkStaticAbilities(false);
@@ -834,45 +805,27 @@ public class AttachAi extends SpellAbilityAi {
int totPower = 0; int totPower = 0;
final List<String> keywords = new ArrayList<>(); final List<String> keywords = new ArrayList<>();
boolean cantAttack = false;
boolean cantBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) { for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
if (stAbility.checkMode(StaticAbilityMode.CantAttack)) { final Map<String, String> stabMap = stAbility.getMapParams();
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;
}
}
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) { if (!stabMap.get("Mode").equals("Continuous")) {
continue; continue;
} }
final String affected = stAbility.getParam("Affected"); final String affected = stabMap.get("Affected");
if (affected == null) { if (affected == null) {
continue; continue;
} }
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) { if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), sa); totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), sa); totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
String kws = stAbility.getParam("AddKeyword"); String kws = stabMap.get("AddKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); keywords.addAll(Arrays.asList(kws.split(" & ")));
} }
kws = stAbility.getParam("AddHiddenKeyword"); kws = stabMap.get("AddHiddenKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); 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)); 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 //some auras aren't useful in multiples
if (attachSource.hasSVar("NonStackingAttachEffect")) { if (attachSource.hasSVar("NonStackingAttachEffect")) {
prefList = CardLists.filter(prefList, 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; return true;
} }
private static boolean isAuraSpell(final SpellAbility sa) {
return sa.isSpell() && sa.getHostCard().isAura();
}
/** /**
* Attach preference. * Attach preference.
* *
@@ -1007,23 +950,7 @@ public class AttachAi extends SpellAbilityAi {
*/ */
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) { private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
GameObject o; GameObject o;
boolean spellCanTargetPlayer = false; if (tgt.canTgtPlayer()) {
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)) {
List<Player> targetable = new ArrayList<>(); List<Player> targetable = new ArrayList<>();
for (final Player player : sa.getHostCard().getGame().getPlayers()) { for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) { if (sa.canTarget(player)) {
@@ -1088,8 +1015,9 @@ public class AttachAi extends SpellAbilityAi {
CardCollection toRemove = new CardCollection(); CardCollection toRemove = new CardCollection();
for (Trigger t : attachSource.getTriggers()) { for (Trigger t : attachSource.getTriggers()) {
if (t.getMode() == TriggerType.ChangesZone) { if (t.getMode() == TriggerType.ChangesZone) {
if ("Card.Self".equals(t.getParam("ValidCard")) final Map<String, String> params = t.getMapParams();
&& "Battlefield".equals(t.getParam("Destination"))) { if ("Card.Self".equals(params.get("ValidCard"))
&& "Battlefield".equals(params.get("Destination"))) {
SpellAbility trigSa = t.ensureAbility(); SpellAbility trigSa = t.ensureAbility();
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) { if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
for (Card target : list) { for (Card target : list) {
@@ -1149,27 +1077,29 @@ public class AttachAi extends SpellAbilityAi {
boolean grantingExtraBlock = false; boolean grantingExtraBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) { 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; continue;
} }
final String affected = stAbility.getParam("Affected"); final String affected = stabMap.get("Affected");
if (affected == null) { if (affected == null) {
continue; continue;
} }
if (affected.contains(stCheck) || affected.contains("AttachedBy")) { if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), stAbility); totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), stAbility); totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
grantingAbilities |= stAbility.hasParam("AddAbility"); grantingAbilities |= stabMap.containsKey("AddAbility");
grantingExtraBlock |= stAbility.hasParam("CanBlockAmount") || stAbility.hasParam("CanBlockAny"); grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
String kws = stAbility.getParam("AddKeyword"); String kws = stabMap.get("AddKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); keywords.addAll(Arrays.asList(kws.split(" & ")));
} }
kws = stAbility.getParam("AddHiddenKeyword"); kws = stabMap.get("AddHiddenKeyword");
if (kws != null) { if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & "))); keywords.addAll(Arrays.asList(kws.split(" & ")));
} }
@@ -1213,10 +1143,10 @@ public class AttachAi extends SpellAbilityAi {
//some auras/equipments aren't useful in multiples //some auras/equipments aren't useful in multiples
if (attachSource.hasSVar("NonStackingAttachEffect")) { if (attachSource.hasSVar("NonStackingAttachEffect")) {
prefList = CardLists.filter(prefList, Predicate.not( prefList = CardLists.filter(prefList, Predicates.not(Predicates.or(
CardPredicates.isEquippedBy(attachSource.getName()) CardPredicates.isEquippedBy(attachSource.getName()),
.or(CardPredicates.isEnchantedBy(attachSource.getName())) CardPredicates.isEnchantedBy(attachSource.getName())
)); )));
} }
// Don't pump cards that will die. // 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) // 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 // to be able to deal the final blow with an enchanted vehicle like that
boolean canOnlyTargetCreatures = true; boolean canOnlyTargetCreatures = true;
if (attachSource.isAura()) { for (String valid : ObjectUtils.firstNonNull(attachSource.getFirstAttachSpell(), sa).getTargetRestrictions().getValidTgts()) {
for (KeywordInterface ki : attachSource.getKeywords(Keyword.ENCHANT)) { if (!valid.startsWith("Creature")) {
String o = ki.getOriginal();
String m[] = o.split(":");
String v = m[1];
if (!v.startsWith("Creature")) {
canOnlyTargetCreatures = false; canOnlyTargetCreatures = false;
break; break;
} }
} }
}
if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) { if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) {
prefList = CardLists.filter(prefList, c -> c.getTimesCrewedThisTurn() == 0 || (attachSource.isEquipment() && attachSource.getGame().getPhaseHandler().is(PhaseType.MAIN1, ai))); 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 // Is a SA that moves target attachment
if ("MoveTgtAura".equals(sa.getParam("AILogic"))) { if ("MoveTgtAura".equals(sa.getParam("AILogic"))) {
CardCollection list = CardLists.filter(CardUtil.getValidCardsToTarget(sa), CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()) CardCollection list = CardLists.filter(CardUtil.getValidCardsToTarget(sa), Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()),
.or(card -> ComputerUtilCard.isUselessCreature(aiPlayer, card.getAttachedTo()))); card -> ComputerUtilCard.isUselessCreature(aiPlayer, card.getAttachedTo())));
return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null; return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null;
} else if ("Unenchanted".equals(sa.getParam("AILogic"))) { } else if ("Unenchanted".equals(sa.getParam("AILogic"))) {
@@ -1472,6 +1397,8 @@ public class AttachAi extends SpellAbilityAi {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource); c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
} else if ("ChangeType".equals(logic)) { } else if ("ChangeType".equals(logic)) {
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource); c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
} else if ("KeepTapped".equals(logic)) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
} else if ("Animate".equals(logic)) { } else if ("Animate".equals(logic)) {
c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource); c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource);
} else if ("Reanimate".equals(logic)) { } else if ("Reanimate".equals(logic)) {
@@ -1482,12 +1409,6 @@ public class AttachAi extends SpellAbilityAi {
c = attachAIHighestEvaluationPreference(prefList); 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 // Consider exceptional cases which break the normal evaluation rules
if (!isUsefulAttachAction(ai, c, sa)) { if (!isUsefulAttachAction(ai, c, sa)) {
return null; 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.") } 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.")) { || keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card); return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
} }
return true; return true;
} }

View File

@@ -25,10 +25,10 @@ public class BalanceAi extends SpellAbilityAi {
if ("BalanceCreaturesAndLands".equals(logic)) { 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 // 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() - diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() -
CardLists.filter(compPerms, CardPredicates.LANDS).size(); CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size();
diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.CREATURES).size() - diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() -
CardLists.filter(compPerms, CardPredicates.CREATURES).size()); CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size());
} }
else if ("BalancePermanents".equals(logic)) { else if ("BalancePermanents".equals(logic)) {
// Don't cast if you have to sacrifice permanents // Don't cast if you have to sacrifice permanents

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Collection;
import java.util.Map;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.player.Player; import forge.game.player.Player;
@@ -7,9 +10,6 @@ import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import java.util.Collection;
import java.util.Map;
public class ChangeCombatantsAi extends SpellAbilityAi { public class ChangeCombatantsAi extends SpellAbilityAi {
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) * @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; package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
@@ -13,6 +14,7 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.*; import forge.game.cost.*;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
@@ -22,13 +24,12 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.*; import java.util.*;
@@ -137,6 +138,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic != null) { if (aiLogic != null) {
if (aiLogic.equals("Always")) { if (aiLogic.equals("Always")) {
return true; return true;
} else if (aiLogic.startsWith("ExileSpell")) {
return doExileSpellLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc. } else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa); return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc. } 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 // remove cards that won't be seen if library can't be searched
if (!ai.canSearchLibraryWith(sa, p)) { 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) { if (type != null && p == ai) {
@@ -452,7 +455,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
final AbilitySub subAb = sa.getSubAbility(); 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 // pick dual lands if available
if (result.stream().anyMatch(CardPredicates.NONBASIC_LANDS)) { if (Iterables.any(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS))) {
result = CardLists.filter(result, CardPredicates.NONBASIC_LANDS); result = CardLists.filter(result, Predicates.not(CardPredicates.Presets.BASIC_LANDS));
} }
return result.get(0); return result.get(0);
@@ -770,7 +773,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
final AbilitySub subAb = sa.getSubAbility(); 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 //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; return false;
} }
@@ -875,10 +878,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone"))); 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 ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -903,20 +902,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true); list = ComputerUtil.filterAITgts(sa, ai, list, true);
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) { if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30); list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
} }
if (source.isInZone(ZoneType.Hand)) { 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()) { if (sa.isSpell()) {
list.remove(source); // spells can't target their own source, because it's actually in the stack zone 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")) { if (sa.hasParam("AttachedTo")) {
list = CardLists.filter(list, c -> { list = CardLists.filter(list, c -> {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) { for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
@@ -944,10 +941,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
immediately = immediately || ComputerUtil.playImmediately(ai, sa); immediately = immediately || ComputerUtil.playImmediately(ai, sa);
if (list.isEmpty() && immediately && sa.getMaxTargets() == 0) {
return true;
}
// Narrow down the list: // Narrow down the list:
if (origin.contains(ZoneType.Battlefield)) { if (origin.contains(ZoneType.Battlefield)) {
if ("Polymorph".equals(sa.getParam("AILogic"))) { if ("Polymorph".equals(sa.getParam("AILogic"))) {
@@ -1023,7 +1016,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean saheeliFelidarCombo = ComputerUtilAbility.getAbilitySourceName(sa).equals("Felidar Guardian") boolean saheeliFelidarCombo = ComputerUtilAbility.getAbilitySourceName(sa).equals("Felidar Guardian")
&& tobounce.getName().equals("Saheeli Rai") && tobounce.getName().equals("Saheeli Rai")
&& CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Felidar Guardian")).size() < && 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 // remember that the card was bounced already unless it's a special combo case
if (!saheeliFelidarCombo) { if (!saheeliFelidarCombo) {
@@ -1207,7 +1200,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) { } else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
List<Card> nonLands = CardLists.getNotType(list, "Land"); List<Card> nonLands = CardLists.getNotType(list, "Land");
// Prefer to pull a creature, generally more useful for AI. // 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 (choice == null) { // Could not find a creature.
if (ai.getLife() <= 5) { // Desperate? if (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon. // Get something AI can cast soon.
@@ -1285,10 +1278,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
list.remove(choice); list.remove(choice);
if (sa.canTarget(choice)) {
sa.getTargets().add(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. // 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 // 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(); Game game = ai.getGame();
// filter out untargetables // filter out untargetables
CardCollectionView aiPermanents = CardLists.filterControlledBy(list, ai); 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 // Felidar Guardian + Saheeli Rai combo support
if (sa.getHostCard().getName().equals("Felidar Guardian")) { 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)) { else if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
Combat combat = game.getCombat(); Combat combat = game.getCombat();
final CardCollection combatants = CardLists.filter(aiPermanents, final CardCollection combatants = CardLists.filter(aiPermanents,
CardPredicates.CREATURES); CardPredicates.Presets.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants); ComputerUtilCard.sortByEvaluateCreature(combatants);
for (final Card c : combatants) { for (final Card c : combatants) {
@@ -1427,10 +1418,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
} }
if (bestChoice != null) {
return bestChoice; return bestChoice;
} }
return null;
}
private static boolean isUnpreferredTarget(final Player ai, final SpellAbility sa, final boolean mandatory) { private static boolean isUnpreferredTarget(final Player ai, final SpellAbility sa, final boolean mandatory) {
if (!mandatory) { if (!mandatory) {
if (!"Always".equals(sa.getParam("AILogic"))) { if (!"Always".equals(sa.getParam("AILogic"))) {
@@ -1453,9 +1447,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
// AI Targeting // AI Targeting
Card choice = null; Card choice = null;
// Filter out cards TargetsForEachPlayer
list = CardLists.canSubsequentlyTarget(list, sa);
if (!list.isEmpty()) { if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list); Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
if (mostExpensivePermanent.isCreature() if (mostExpensivePermanent.isCreature()
@@ -1467,7 +1458,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) { } else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
List<Card> nonLands = CardLists.getNotType(list, "Land"); List<Card> nonLands = CardLists.getNotType(list, "Land");
// Prefer to pull a creature, generally more useful for AI. // 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 (choice == null) { // Could not find a creature.
if (ai.getLife() <= 5) { // Desperate? if (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon. // Get something AI can cast soon.
@@ -1579,8 +1570,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} else if (logic.startsWith("ExilePreference")) { } else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList); return doExilePreferenceLogic(decider, sa, fetchList);
} else if (logic.equals("BounceOwnTrigger")) {
return doBounceOwnTriggerLogic(decider, fetchList);
} }
} }
if (fetchList.isEmpty()) { if (fetchList.isEmpty()) {
@@ -1643,14 +1632,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} else { } else {
// Don't fetch another tutor with the same name // 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()) { if (origin.contains(ZoneType.Library) && !sameNamed.isEmpty()) {
fetchList = sameNamed; fetchList = sameNamed;
} }
// Does AI need a land? // Does AI need a land?
CardCollectionView hand = decider.getCardsIn(ZoneType.Hand); 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; boolean canCastSomething = false;
for (Card cardInHand : hand) { for (Card cardInHand : hand) {
canCastSomething = canCastSomething || ComputerUtilMana.hasEnoughManaSourcesToCast(cardInHand.getFirstSpellAbility(), decider); canCastSomething = canCastSomething || ComputerUtilMana.hasEnoughManaSourcesToCast(cardInHand.getFirstSpellAbility(), decider);
@@ -1660,13 +1649,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
if (c == null) { 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 // we're only choosing from lands, so get the best land
c = ComputerUtilCard.getBestLandAI(fetchList); c = ComputerUtilCard.getBestLandAI(fetchList);
} else { } else {
fetchList = CardLists.getNotType(fetchList, "Land"); fetchList = CardLists.getNotType(fetchList, "Land");
// Prefer to pull a creature, generally more useful for AI. // 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. 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); CardCollection listToSac = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), definedSac, ai, source, sa);
listToSac.sort(Collections.reverseOrder(CardLists.CmcComparatorInv)); 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); listToRet.sort(CardLists.CmcComparatorInv);
if (!listToSac.isEmpty() && !listToRet.isEmpty()) { if (!listToSac.isEmpty() && !listToRet.isEmpty()) {
@@ -1975,7 +1964,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
if (logic.contains("NonLand")) { if (logic.contains("NonLand")) {
scanList = CardLists.filter(scanList, CardPredicates.NON_LANDS); scanList = CardLists.filter(scanList, Predicates.not(Presets.LANDS));
} }
if (logic.contains("NonExiled")) { 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) { private boolean doExileSpellLogic(final Player aiPlayer, final SpellAbility sa) {
List<ApiType> dangerousApi = null; String aiLogic = sa.getParamOrDefault("AILogic", "");
CardCollection spells = new CardCollection(ai.getGame().getStackZone().getCards()); SpellAbilityStackInstance top = aiPlayer.getGame().getStack().peek();
Collections.reverse(spells); List<ApiType> dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
if (!mandatory && !spells.isEmpty()) { int manaCost = 0;
spells = spells.subList(0, 1); int minCost = 0;
spells = ComputerUtil.filterAITgts(sa, ai, spells, true);
dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll); if (aiLogic.contains(".")) {
minCost = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
} }
for (Card c : spells) { if (top != null) {
SpellAbility topSA = ai.getGame().getStack().getSpellMatchingHost(c); SpellAbility topSA = top.getSpellAbility();
if (topSA != null && (dangerousApi == null || if (topSA != null) {
(dangerousApi.contains(topSA.getApi()) && topSA.getActivatingPlayer().isOpponentOf(ai))) if (topSA.getPayCosts().hasManaCost()) {
&& sa.canTarget(topSA)) { manaCost = topSA.getPayCosts().getTotalMana().getCMC();
}
if ((manaCost >= minCost || dangerousApi.contains(topSA.getApi()))
&& topSA.getActivatingPlayer().isOpponentOf(aiPlayer)
&& sa.canTargetSpellAbility(topSA)) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(topSA); sa.getTargets().add(topSA);
return sa.isTargetNumberValid(); return sa.isTargetNumberValid();
} }
} }
}
return false; return false;
} }
@@ -2134,47 +2130,4 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static boolean isBouncedThisTurn(Player ai, Card c) { private static boolean isBouncedThisTurn(Player ai, Card c) {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN); 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; 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.Game;
import forge.game.ability.AbilityUtils; 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.cost.Cost;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -14,9 +33,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import java.util.Collections;
import java.util.Map;
public class ChangeZoneAllAi extends SpellAbilityAi { public class ChangeZoneAllAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -77,7 +93,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} else if ("ExileGraveyards".equals(aiLogic)) { } else if ("ExileGraveyards".equals(aiLogic)) {
for (Player opp : ai.getOpponents()) { for (Player opp : ai.getOpponents()) {
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard); 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) { if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
return true; return true;
@@ -92,7 +108,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
Player bestTgt = null; Player bestTgt = null;
if (player.canBeTargetedBy(sa)) { if (player.canBeTargetedBy(sa)) {
int numGY = CardLists.count(player.getCardsIn(ZoneType.Graveyard), int numGY = CardLists.count(player.getCardsIn(ZoneType.Graveyard),
CardPredicates.CREATURES); CardPredicates.Presets.CREATURES);
if (numGY > maxSize) { if (numGY > maxSize) {
maxSize = numGY; maxSize = numGY;
bestTgt = player; 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 // 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 // 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. // 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); CardCollectionView humanType = ai.getOpponents().getCardsIn(origin);

View File

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

View File

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

View File

@@ -1,10 +1,22 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.StaticData; import forge.StaticData;
import forge.ai.*; import forge.ai.AiAttackController;
import forge.card.*; 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.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCopyService; import forge.game.card.CardCopyService;
@@ -16,9 +28,6 @@ import forge.game.zone.ZoneType;
import forge.item.PaperCard; import forge.item.PaperCard;
import forge.util.MyRandom; import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class ChooseCardNameAi extends SpellAbilityAi { public class ChooseCardNameAi extends SpellAbilityAi {
@Override @Override

View File

@@ -1,6 +1,11 @@
package forge.ai.ability; 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.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.card.CardCollectionView; 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 // activate in Main 2 hoping that the extra mana surplus will make a difference
// if there are some nonland permanents in hand // if there are some nonland permanents in hand
CardCollectionView permanents = CardLists.filter(ai.getCardsIn(ZoneType.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); return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai);
} }

View File

@@ -1,15 +1,16 @@
package forge.ai.ability; 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.Collections;
import java.util.List; import java.util.List;
import java.util.Map; 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 { public class ChooseCompanionAi extends SpellAbilityAi {
/* (non-Javadoc) /* (non-Javadoc)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,24 @@
package forge.ai.ability; 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 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.card.CardType;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; 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.keyword.Keyword;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -14,11 +27,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; 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 { public class ChooseTypeAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
@@ -57,7 +65,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
int avgPower = 0; int avgPower = 0;
// predict the opposition // 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 maxOppPower = 0;
int maxOppToughness = 0; int maxOppToughness = 0;
int oppUsefulCreatures = 0; int oppUsefulCreatures = 0;
@@ -77,7 +85,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
if (maxX > 1) { if (maxX > 1) {
CardCollection cre = CardLists.filter(aiPlayer.getCardsIn(ZoneType.Battlefield), CardCollection cre = CardLists.filter(aiPlayer.getCardsIn(ZoneType.Battlefield),
CardPredicates.isType(chosenType), CardPredicates.UNTAPPED); CardPredicates.isType(chosenType), CardPredicates.Presets.UNTAPPED);
if (!cre.isEmpty()) { if (!cre.isEmpty()) {
for (Card c: cre) { for (Card c: cre) {
avgPower += c.getNetPower(); avgPower += c.getNetPower();

View File

@@ -1,7 +1,10 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
@@ -14,8 +17,6 @@ import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import java.util.Map;
public class ClashAi extends SpellAbilityAi { public class ClashAi extends SpellAbilityAi {
/* (non-Javadoc) /* (non-Javadoc)
@@ -92,7 +93,7 @@ public class ClashAi extends SpellAbilityAi {
// Springjack Knight // Springjack Knight
// TODO: Whirlpool Whelm also uses creature targeting but it's trickier to support // TODO: Whirlpool Whelm also uses creature targeting but it's trickier to support
CardCollectionView aiCreats = ai.getCreaturesInPlay(); 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); 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) // (e.g. Grafdigger's Cage)
Card topCopy = CardCopyService.getLKICopy(card); Card topCopy = CardCopyService.getLKICopy(card);
topCopy.turnFaceDownNoUpdate(); topCopy.turnFaceDownNoUpdate();
topCopy.setCloaked(sa); topCopy.setCloaked(true);
if (ComputerUtil.isETBprevented(topCopy)) { if (ComputerUtil.isETBprevented(topCopy)) {
return false; return false;

View File

@@ -1,10 +1,19 @@
package forge.ai.ability; 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.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; 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.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -12,9 +21,6 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import java.util.List;
import java.util.Map;
public class CloneAi extends SpellAbilityAi { public class CloneAi extends SpellAbilityAi {
@Override @Override
@@ -205,7 +211,7 @@ public class CloneAi extends SpellAbilityAi {
// prevent loop of choosing copy of same card // prevent loop of choosing copy of same card
if (isVesuva) { 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); 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.ComputerUtilCard;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
@@ -19,13 +18,6 @@ public class ConniveAi extends SpellAbilityAi {
return false; // can't draw anything 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); CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
// Filter AI-specific targets if provided // Filter AI-specific targets if provided

View File

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

View File

@@ -17,16 +17,23 @@
*/ */
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; 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.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
@@ -36,10 +43,6 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; 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 //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) { while (t == null) {
// filter by MustTarget requirement // filter by MustTarget requirement
CardCollection originalList = new CardCollection(list); CardCollection originalList = new CardCollection(list);
list = CardLists.canSubsequentlyTarget(list, sa);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa); boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
if (planeswalkers > 0) { if (planeswalkers > 0) {
@@ -330,22 +330,4 @@ public class ControlGainAi extends SpellAbilityAi {
Card chosen = ComputerUtilCard.getBestCreatureAI(cards); Card chosen = ComputerUtilCard.getBestCreatureAI(cards);
return chosen != null ? chosen.getController() : Iterables.getFirst(options, null); 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; 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 com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
@@ -27,9 +32,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
@@ -63,7 +65,7 @@ public class ControlGainVariantAi extends SpellAbilityAi {
@Override @Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) { 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)) { if (Iterables.isEmpty(otherCtrl)) {
return ComputerUtilCard.getWorstAI(options); return ComputerUtilCard.getWorstAI(options);
} else { } else {

View File

@@ -1,12 +1,31 @@
package forge.ai.ability; 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 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.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; 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.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -15,11 +34,6 @@ import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; 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 { public class CopyPermanentAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { 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 // 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 // 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) // of a rewrite in canPlayAI to allow a response form of CopyPermanentAi)
Predicate<Card> nameEquals = CardPredicates.nameEquals(host.getName()); list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(host.getName())));
list = CardLists.filter(list, nameEquals.negate());
} }
//Nothing to target //Nothing to target
@@ -131,7 +144,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
return false; return false;
} }
CardCollection betterList = CardLists.filter(list, CardPredicates.isRemAIDeck().negate()); CardCollection betterList = CardLists.filter(list, Predicates.not(CardPredicates.isRemAIDeck()));
if (betterList.isEmpty()) { if (betterList.isEmpty()) {
if (!mandatory) { if (!mandatory) {
return false; return false;
@@ -152,8 +165,6 @@ public class CopyPermanentAi extends SpellAbilityAi {
// target loop // target loop
while (sa.canAddMoreTarget()) { while (sa.canAddMoreTarget()) {
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets(); sa.resetTargets();
@@ -166,7 +177,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
list = CardLists.filter(list, c -> (!c.getType().isLegendary() || canCopyLegendary) || !c.getController().equals(aiPlayer)); list = CardLists.filter(list, c -> (!c.getType().isLegendary() || canCopyLegendary) || !c.getController().equals(aiPlayer));
Card choice; Card choice;
if (list.stream().anyMatch(CardPredicates.CREATURES)) { if (Iterables.any(list, Presets.CREATURES)) {
if (sa.hasParam("TargetingPlayer")) { if (sa.hasParam("TargetingPlayer")) {
choice = ComputerUtilCard.getWorstCreatureAI(list); choice = ComputerUtilCard.getWorstCreatureAI(list);
} else { } else {

View File

@@ -1,18 +1,22 @@
package forge.ai.ability; 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.Game;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.cost.Cost;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell; import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import java.util.List;
import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi { public class CopySpellAbilityAi extends SpellAbilityAi {
@@ -24,7 +28,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
String logic = sa.getParamOrDefault("AILogic", ""); String logic = sa.getParamOrDefault("AILogic", "");
if (game.getStack().isEmpty()) { if (game.getStack().isEmpty()) {
return sa.isMandatory() || "Always".equals(logic); return sa.isMandatory();
} }
final SpellAbility top = game.getStack().peekAbility(); final SpellAbility top = game.getStack().peekAbility();
@@ -144,23 +148,4 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
return true; 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; package forge.ai.ability;
import java.util.Iterator; 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.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; 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.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CounterEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostDiscard; import forge.game.cost.CostDiscard;
import forge.game.cost.CostExile; import forge.game.cost.CostExile;
@@ -24,8 +28,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class CounterAi extends SpellAbilityAi { public class CounterAi extends SpellAbilityAi {
@@ -78,18 +80,11 @@ public class CounterAi extends SpellAbilityAi {
return false; return false;
} }
if (sa.hasParam("UnlessCost") && "TargetedController".equals(sa.getParamOrDefault("UnlessPayer", "TargetedController"))) { if ("OppDiscardsHand".equals(sa.getParam("AILogic"))) {
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 (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) { if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
return false; return false;
} }
} }
}
// TODO check if Player can pay the unless cost?
}
sa.resetTargets(); sa.resetTargets();
if (sa.canTargetSpellAbility(topSA)) { if (sa.canTargetSpellAbility(topSA)) {
@@ -255,9 +250,6 @@ public class CounterAi extends SpellAbilityAi {
} }
sa.resetTargets(); sa.resetTargets();
if (mandatory && !sa.canAddMoreTarget()) {
return true;
}
Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory); Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory);
SpellAbility tgtSA = pair.getLeft(); SpellAbility tgtSA = pair.getLeft();
@@ -359,51 +351,4 @@ public class CounterAi extends SpellAbilityAi {
return new ImmutablePair<>(bestOption != null ? bestOption : leastBadOption, bestOption != null); 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; package forge.ai.ability;
import java.util.List;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; 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.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import java.util.List;
/** /**
* <p> * <p>
@@ -57,7 +65,7 @@ public abstract class CountersAi extends SpellAbilityAi {
Card choice; Card choice;
// opponent can always order it so that he gets 0 // 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; return null;
} }

View File

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

View File

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

View File

@@ -1,17 +1,22 @@
package forge.ai.ability; 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 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.GameEntity;
import forge.game.card.*; import forge.game.card.*;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; 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 { public class CountersProliferateAi extends SpellAbilityAi {
@@ -105,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO); boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
// because countertype can't be chosen anymore, only look for poison counters // 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.isOpponentOf(ai)) {
if (p.getCounters(poison) > 0 && p.canReceiveCounters(poison)) { if (p.getCounters(poison) > 0 && p.canReceiveCounters(poison)) {
return (T)p; 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 // AI planeswalker always, opponent planeswalkers never
if (c.isPlaneswalker()) { if (c.isPlaneswalker()) {
if (c.getController().isOpponentOf(ai)) { if (c.getController().isOpponentOf(ai)) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,12 @@
package forge.ai.ability; 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.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
@@ -13,8 +19,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import java.util.function.Predicate;
public class DamageAllAi extends SpellAbilityAi { public class DamageAllAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.*; import forge.ai.*;
@@ -26,9 +27,8 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -42,7 +42,7 @@ public class DamageDealAi extends DamageAiBase {
final SpellAbility root = sa.getRootAbility(); final SpellAbility root = sa.getRootAbility();
final String damage = sa.getParam("NumDmg"); final String damage = sa.getParam("NumDmg");
Card source = sa.getHostCard(); Card source = sa.getHostCard();
int dmg = calculateDamageAmount(sa, source, damage); int dmg = AbilityUtils.calculateAmount(source, damage, sa);
final String logic = sa.getParam("AILogic"); final String logic = sa.getParam("AILogic");
if ("MadSarkhanDigDmg".equals(logic)) { if ("MadSarkhanDigDmg".equals(logic)) {
@@ -93,9 +93,9 @@ public class DamageDealAi extends DamageAiBase {
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String damage = sa.getParam("NumDmg"); 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")) { if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) {
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
@@ -163,10 +163,8 @@ public class DamageDealAi extends DamageAiBase {
} }
} else if ("WildHunt".equals(logic)) { } 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 // 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() List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source, sa);
.filter(CardPredicates.restriction("Creature.Wolf+untapped+YouCtrl+Other", ai, source, sa)) dmg = Aggregates.sum(wolves, Card::getNetPower);
.mapToInt(Card::getNetPower)
.sum();
} else if ("Triskelion".equals(logic)) { } else if ("Triskelion".equals(logic)) {
final int n = source.getCounters(CounterEnumType.P1P1); final int n = source.getCounters(CounterEnumType.P1P1);
if (n > 0) { if (n > 0) {
@@ -402,7 +400,7 @@ public class DamageDealAi extends DamageAiBase {
final Player activator = sa.getActivatingPlayer(); final Player activator = sa.getActivatingPlayer();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Game game = source.getGame(); 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") CardCollection killables = CardLists.filter(hPlay, c -> c.getSVar("Targeting").equals("Dies")
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d) || (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 // See if there's an indestructible target that can be used
CardCollection indestructible = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardCollection indestructible = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
(CardPredicates.CREATURES.or(CardPredicates.PLANESWALKERS)) Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.Presets.PLANESWALKERS, CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE), CardPredicates.isTargetableBy(sa)));
.and(CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE))
.and(CardPredicates.isTargetableBy(sa))
);
if (!indestructible.isEmpty()) { if (!indestructible.isEmpty()) {
Card c = ComputerUtilCard.getWorstPermanentAI(indestructible, false, false, false, false); Card c = ComputerUtilCard.getWorstPermanentAI(indestructible, false, false, false, false);
@@ -903,7 +898,7 @@ public class DamageDealAi extends DamageAiBase {
} }
else if (tgt.canTgtPlaneswalker()) { else if (tgt.canTgtPlaneswalker()) {
// Second pass for planeswalkers: choose AI's worst planeswalker // 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) { if (c != null) {
sa.getTargets().add(c); sa.getTargets().add(c);
if (divided) { if (divided) {
@@ -935,7 +930,7 @@ public class DamageDealAi extends DamageAiBase {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String damage = sa.getParam("NumDmg"); final String damage = sa.getParam("NumDmg");
int dmg = calculateDamageAmount(sa, source, damage); int dmg = AbilityUtils.calculateAmount(source, damage, sa);
// Remove all damage // Remove all damage
if (sa.hasParam("Remove")) { if (sa.hasParam("Remove")) {
@@ -979,17 +974,6 @@ public class DamageDealAi extends DamageAiBase {
return true; 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) { private boolean doXLifeDrainLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
@@ -1136,28 +1120,4 @@ public class DamageDealAi extends DamageAiBase {
return null; 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; package forge.ai.ability;
import java.util.ArrayList;
import java.util.List;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
@@ -7,7 +10,11 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.ability.AbilityUtils; 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.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
@@ -18,9 +25,6 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import java.util.ArrayList;
import java.util.List;
public class DamagePreventAi extends SpellAbilityAi { public class DamagePreventAi extends SpellAbilityAi {
@Override @Override
@@ -122,7 +126,7 @@ public class DamagePreventAi extends SpellAbilityAi {
if (targetables.isEmpty()) { if (targetables.isEmpty()) {
return false; return false;
} }
final CardCollection combatants = CardLists.filter(targetables, CardPredicates.CREATURES); final CardCollection combatants = CardLists.filter(targetables, CardPredicates.Presets.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants); ComputerUtilCard.sortByEvaluateCreature(combatants);
for (final Card c : combatants) { for (final Card c : combatants) {
@@ -183,7 +187,7 @@ public class DamagePreventAi extends SpellAbilityAi {
} }
if (!compTargetables.isEmpty()) { if (!compTargetables.isEmpty()) {
final CardCollection combatants = CardLists.filter(compTargetables, CardPredicates.CREATURES); final CardCollection combatants = CardLists.filter(compTargetables, CardPredicates.Presets.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants); ComputerUtilCard.sortByEvaluateCreature(combatants);
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
Combat combat = game.getCombat(); Combat combat = game.getCombat();

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.function.Predicate; import com.google.common.base.Predicates;
import forge.ai.*; import forge.ai.*;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
@@ -16,7 +15,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityMustTarget; import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
public class DestroyAi extends SpellAbilityAi { public class DestroyAi extends SpellAbilityAi {
@Override @Override
@@ -169,8 +167,7 @@ public class DestroyAi extends SpellAbilityAi {
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false); list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
} }
if (!playReusable(ai, sa)) { if (!playReusable(ai, sa)) {
Predicate<Card> hasCounter = CardPredicates.hasCounter(CounterEnumType.SHIELD, 1); list = CardLists.filter(list, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
list = CardLists.filter(list, hasCounter.negate());
list = CardLists.filter(list, c -> { list = CardLists.filter(list, c -> {
//Check for cards that can be sacrificed in response //Check for cards that can be sacrificed in response
@@ -216,8 +213,6 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection originalList = new CardCollection(list); CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa); boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets(); sa.resetTargets();
@@ -277,7 +272,6 @@ public class DestroyAi extends SpellAbilityAi {
choice = aura; 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); list.remove(choice);
if (sa.canTarget(choice)) {
sa.getTargets().add(choice); sa.getTargets().add(choice);
} }
}
} else if (sa.hasParam("Defined")) { } else if (sa.hasParam("Defined")) {
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if ("WillSkipTurn".equals(logic) && (source.getController().equals(ai) if ("WillSkipTurn".equals(logic) && (source.getController().equals(ai)
@@ -329,8 +321,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE); CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents()); preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
Predicate<Card> hasCounter = CardPredicates.hasCounter(CounterEnumType.SHIELD, 1); preferred = CardLists.filter(preferred, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
preferred = CardLists.filter(preferred, hasCounter.negate());
if (CardLists.getNotType(preferred, "Creature").isEmpty()) { if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false); preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);
} }
@@ -366,10 +357,7 @@ public class DestroyAi extends SpellAbilityAi {
} }
} else { } else {
Card c = ComputerUtilCard.getBestAI(preferred); Card c = ComputerUtilCard.getBestAI(preferred);
if (sa.canTarget(c)) {
sa.getTargets().add(c); sa.getTargets().add(c);
}
preferred.remove(c); preferred.remove(c);
} }
} }
@@ -390,9 +378,7 @@ public class DestroyAi extends SpellAbilityAi {
} else { } else {
c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false); c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false);
} }
if (sa.canTarget(c)) {
sa.getTargets().add(c); sa.getTargets().add(c);
}
list.remove(c); list.remove(c);
} }
} }
@@ -436,8 +422,8 @@ public class DestroyAi extends SpellAbilityAi {
boolean nonBasicTgt = !tgtLand.isBasicLand(); boolean nonBasicTgt = !tgtLand.isBasicLand();
// Try not to lose tempo too much and not to mana-screw yourself when considering this logic // 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 numLandsInHand = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.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 // 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; 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; package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.*; import forge.ai.*;
import forge.card.MagicColor; 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.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostDamage;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
import java.util.function.Predicate;
public class DestroyAllAi extends SpellAbilityAi { public class DestroyAllAi extends SpellAbilityAi {
@@ -107,8 +109,8 @@ public class DestroyAllAi extends SpellAbilityAi {
// Special handling for Raiding Party // Special handling for Raiding Party
if (logic.equals("RaidingParty")) { if (logic.equals("RaidingParty")) {
int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, ailist.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(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, opplist.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); return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
} }
@@ -181,38 +183,4 @@ public class DestroyAllAi extends SpellAbilityAi {
return false; 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; package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables; 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.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -18,8 +28,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.TextUtil; import forge.util.TextUtil;
import java.util.Map;
public class DigAi extends SpellAbilityAi { public class DigAi extends SpellAbilityAi {
/* (non-Javadoc) /* (non-Javadoc)

View File

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

View File

@@ -1,5 +1,8 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.AiAttackController; import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -13,9 +16,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class DigUntilAi extends SpellAbilityAi { public class DigUntilAi extends SpellAbilityAi {
@Override @Override
@@ -45,7 +45,7 @@ public class DigUntilAi extends SpellAbilityAi {
return false; return false;
} }
if ("Land.Basic".equals(sa.getParam("Valid")) 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 // We already have a mana-producing land in hand, so bail
// until opponent's end of turn phase! // until opponent's end of turn phase!
// But we still want more (and want to fill grave) if nothing better to do then // 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"); final String logic = sa.getParam("AILogic");
if ("OathOfDruids".equals(logic)) { if ("OathOfDruids".equals(logic)) {
final List<Card> creaturesInLibrary = 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(); final List<Card> creaturesInBattlefield = player.getCreaturesInPlay();
// if there are at least 3 creatures in library, // if there are at least 3 creatures in library,
// or none in play with one in library, oath // 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.List;
import java.util.Map; 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.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostDamage;
import forge.game.cost.CostDraw;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
@@ -21,7 +24,6 @@ import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class DiscardAi extends SpellAbilityAi { public class DiscardAi extends SpellAbilityAi {
@@ -92,7 +94,7 @@ public class DiscardAi extends SpellAbilityAi {
if (sa.hasParam("AnyNumber")) { if (sa.hasParam("AnyNumber")) {
if ("DiscardUncastableAndExcess".equals(aiLogic)) { if ("DiscardUncastableAndExcess".equals(aiLogic)) {
final CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand); 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 numDiscard = 0;
int numOppInHand = 0; int numOppInHand = 0;
for (Player p : ai.getGame().getPlayers()) { for (Player p : ai.getGame().getPlayers()) {
@@ -217,57 +219,4 @@ public class DiscardAi extends SpellAbilityAi {
} }
return super.confirmAction(player, sa, mode, message, params); 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.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell; import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;

View File

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

View File

@@ -20,7 +20,15 @@ package forge.ai.ability;
import java.util.Map; 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.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
@@ -30,11 +38,13 @@ import forge.game.card.CounterType;
import forge.game.cost.*; import forge.game.cost.*;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; 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.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class DrawAi extends SpellAbilityAi { public class DrawAi extends SpellAbilityAi {
@@ -515,17 +525,12 @@ public class DrawAi extends SpellAbilityAi {
return false; return false;
} }
if ((computerHandSize + numCards > computerMaxHandSize)) { if ((computerHandSize + numCards > computerMaxHandSize)
// Don't draw too many cards and then risk discarding cards at EOT && game.getPhaseHandler().isPlayerTurn(ai)
if (game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger() && !sa.isTrigger()
&& !assumeSafeX && !assumeSafeX) {
&& !drawback) { // Don't draw too many cards and then risk discarding cards at EOT
return false; if (!drawback) {
}
if (computerHandSize > computerMaxHandSize) {
// Don't make my hand size get too big if already at max
return false; return false;
} }
} }
@@ -554,36 +559,4 @@ public class DrawAi extends SpellAbilityAi {
// except it has Laboratory Maniac // except it has Laboratory Maniac
return player.isCardInPlay("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; package forge.ai.ability;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.*; import forge.ai.*;
import forge.game.CardTraitPredicates; import forge.game.CardTraitPredicates;
@@ -9,9 +10,9 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -27,7 +28,6 @@ import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil; import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -57,8 +57,8 @@ public class EffectAi extends SpellAbilityAi {
for (Player opp : ai.getOpponents()) { for (Player opp : ai.getOpponents()) {
boolean worthHolding = false; boolean worthHolding = false;
CardCollectionView oppCreatsLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), CardCollectionView oppCreatsLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS.or(CardPredicates.CREATURES)); Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.Presets.CREATURES));
CardCollectionView oppCreatsLandsTapped = CardLists.filter(oppCreatsLands, CardPredicates.TAPPED); CardCollectionView oppCreatsLandsTapped = CardLists.filter(oppCreatsLands, CardPredicates.Presets.TAPPED);
if (oppCreatsLandsTapped.size() >= 3 || oppCreatsLands.size() == oppCreatsLandsTapped.size()) { if (oppCreatsLandsTapped.size() >= 3 || oppCreatsLands.size() == oppCreatsLandsTapped.size()) {
worthHolding = true; worthHolding = true;
@@ -84,7 +84,7 @@ public class EffectAi extends SpellAbilityAi {
Player opp = ai.getStrongestOpponent(); Player opp = ai.getStrongestOpponent();
List<Card> possibleAttackers = ai.getCreaturesInPlay(); List<Card> possibleAttackers = ai.getCreaturesInPlay();
List<Card> possibleBlockers = opp.getCreaturesInPlay(); List<Card> possibleBlockers = opp.getCreaturesInPlay();
possibleBlockers = CardLists.filter(possibleBlockers, CardPredicates.UNTAPPED); possibleBlockers = CardLists.filter(possibleBlockers, Presets.UNTAPPED);
final Combat combat = game.getCombat(); final Combat combat = game.getCombat();
int oppLife = opp.getLife(); int oppLife = opp.getLife();
int potentialDmg = 0; int potentialDmg = 0;
@@ -287,7 +287,7 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("Burn")) { } else if (logic.equals("Burn")) {
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack) // for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
SpellAbility burn = sa.getSubAbility(); 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")) { } else if (logic.equals("YawgmothsWill")) {
return SpecialCardAi.YawgmothsWill.consider(ai, sa); return SpecialCardAi.YawgmothsWill.consider(ai, sa);
} else if (logic.startsWith("NeedCreatures")) { } else if (logic.startsWith("NeedCreatures")) {
@@ -333,12 +333,12 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("CantRegenerate")) { } else if (logic.equals("CantRegenerate")) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa); 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); Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(input);
runParams.put(AbilityKey.Regeneration, true); 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 // 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; return false;
} }
@@ -367,9 +367,9 @@ public class EffectAi extends SpellAbilityAi {
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard()); Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
runParams.put(AbilityKey.Regeneration, true); 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 // 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; return false;
} }
@@ -639,19 +639,4 @@ public class EffectAi extends SpellAbilityAi {
return false; 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; package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -27,9 +30,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
* AbilityFactoryBond class. * 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; package forge.ai.ability;
import forge.ai.*; import forge.ai.AiController;
import forge.game.card.*; 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.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -34,8 +43,8 @@ public class ExploreAi extends SpellAbilityAi {
int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size(); int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size();
CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield); CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand); CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.LANDS_PRODUCING_MANA); CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.LANDS_PRODUCING_MANA); CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
int maxCMCDiff = 1; int maxCMCDiff = 1;
int numLandsToStillNeedMore = 2; int numLandsToStillNeedMore = 2;

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