Compare commits

..

1 Commits

Author SHA1 Message Date
GitHub Actions
fdc290138c [maven-release-plugin] prepare release forge-1.6.64 2024-08-15 00:54:14 +00:00
11473 changed files with 82600 additions and 151480 deletions

View File

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

View File

@@ -20,10 +20,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
- name: Set up JDK 8
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '8'
distribution: 'temurin'
cache: 'maven'
server-id: cardforge-repo
@@ -31,20 +31,20 @@ jobs:
server-password: ${{ secrets.FTP_PASSWORD }}
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Install old maven (3.8.1)
- name: Install old maven (3.6.3)
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
curl -o apache-maven-3.6.3-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
tar xf apache-maven-3.6.3-bin.tar.gz
export PATH=$PWD/apache-maven-3.6.3/bin:$PATH
export MAVEN_HOME=$PWD/apache-maven-3.6.3
mvn --version
- name: Install android SDK
uses: maxim-lobanov/setup-android-tools@v1
with:
packages: |
platforms;android-35
build-tools;35.0.0
platforms;android-26
build-tools;30.0.3
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
run: |
@@ -71,11 +71,11 @@ jobs:
- name: Install Android maven plugin
run: |
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
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
curl -L -o android-maven-plugin-4.6.1.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.jar
curl -L -o android-maven-plugin-4.6.1.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.pom
#mvn install:install-file -Dfile=android-maven-plugin-4.6.1.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.1 -Dpackaging=jar
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
@@ -83,7 +83,7 @@ jobs:
- name: Build/Install/Publish to GitHub Packages Apache Maven
run: |
export _JAVA_OPTIONS="-Xmx2g"
mvn -U -B -P android-release-build,android-release-upload 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 -Dmaven.test.skip=true
mvn -U -B -P android-release-build,android-release-sign,android-release-upload install -e -Dsign.keystore=forge.keystore -Dsign.alias=Forge -Dsign.storepass=${{ secrets.SIGN_STORE_PASS }} -Dsign.keypass=${{ secrets.SIGN_STORE_PASS }} -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=30.0.3 -Dmaven.test.skip=true
env:
GITHUB_TOKEN: ${{ github.token }}

View File

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

View File

@@ -1,132 +0,0 @@
name: Create Snapshot Desktop and Android
on:
workflow_dispatch:
inputs:
debug_enabled:
type: boolean
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 18 * * *'
jobs:
build:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest
permissions:
contents: write
deployments: write
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
server-id: cardforge-repo
server-username: ${{ secrets.FTP_USERNAME }}
server-password: ${{ secrets.FTP_PASSWORD }}
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
run: command -v Xvfb >/dev/null 2>&1 || { sudo apt update && sudo apt install -y xvfb; }
- name: Configure Git User
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
- name: Install old maven (3.8.1)
run: |
curl -o apache-maven-3.8.1-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz
tar xf apache-maven-3.8.1-bin.tar.gz
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
export MAVEN_HOME=$PWD/apache-maven-3.8.1
mvn --version
- name: Set Up Android tools
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"
- name: Extract Android keystore
run: |
ls
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 -
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
- name: Install Android maven plugin
run: |
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
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
- name: Build/Install/Publish to GitHub Packages Apache Maven
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build 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
# rename files and append date
for file in *.jar; do
bname="${file%.*}"
echo "file renamed to ${bname}-${d}.jar"
mv "${bname}.jar" "${bname}-${d}.jar"
done
for file in *.bz2; do
# remove .bz2
fname="${file%.*}"
# remove .tar
bname="${fname%.*}"
echo "file renamed to ${bname}-${d}.tar.bz2"
mv "${fname}.bz2" "${bname}-${d}.tar.bz2"
done
ls
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Upload snapshot to GitHub Prerelease
uses: ncipollo/release-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: Daily Snapshot
tag: daily-snapshots
prerelease: true
artifacts: izpack/*
allowUpdates: true
removeArtifacts: true
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -8,11 +8,10 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
#upload_package:
# type: boolean
# description: 'Upload the completed Android package'
# required: false
# default: true
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 19 * * *'
jobs:
build:
@@ -25,10 +24,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
- name: Set up JDK 8
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '8'
distribution: 'temurin'
cache: 'maven'
server-id: cardforge-repo
@@ -36,17 +35,19 @@ jobs:
server-password: ${{ secrets.FTP_PASSWORD }}
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Install old maven (3.8.1)
- name: Install old maven (3.6.3)
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
curl -o apache-maven-3.6.3-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
tar xf apache-maven-3.6.3-bin.tar.gz
export PATH=$PWD/apache-maven-3.6.3/bin:$PATH
export MAVEN_HOME=$PWD/apache-maven-3.6.3
mvn --version
- name: Set Up Android tools
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"
JAVA_HOME=${JAVA_HOME_11_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT "platform-tools"
JAVA_HOME=${JAVA_HOME_11_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "platforms;android-26"
JAVA_HOME=${JAVA_HOME_11_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;30.0.3"
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
run: |
@@ -73,11 +74,11 @@ jobs:
- name: Install Android maven plugin
run: |
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
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.1
curl -L -o android-maven-plugin-4.6.1.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.jar
curl -L -o android-maven-plugin-4.6.1.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.1/android-maven-plugin-4.6.1.pom
#mvn install:install-file -Dfile=android-maven-plugin-4.6.1.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.1 -Dpackaging=jar
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
@@ -85,30 +86,40 @@ jobs:
- name: Build/Install/Publish to GitHub Packages Apache Maven
run: |
export _JAVA_OPTIONS="-Xmx2g"
mvn -U -B -P 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 -Dmaven.test.skip=true
mkdir upload
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk upload/
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip upload/
mv /home/runner/work/forge/forge/forge-gui-android/target/classes/assets/version.txt upload/
cd upload
d=$(date +%m-%d)
# Replace date in forge-gui-mobile/src/forge/Forge.java
sed -i -e "s/-SNAPSHOT/-SNAPSHOT-${d}/g" forge-gui-mobile/src/forge/Forge.java
mvn -U -B -P android-release-build,android-release-sign install -e -Dsign.keystore=forge.keystore -Dsign.alias=Forge -Dsign.storepass=${{ secrets.SIGN_STORE_PASS }} -Dsign.keypass=${{ secrets.SIGN_STORE_PASS }} -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=30.0.3 -Dmaven.test.skip=true
mkdir -p forge-gui-android/target/upload
mv forge-gui-android/target/*-signed-aligned.apk forge-gui-android/target/upload/
mv forge-gui-android/target/assets.zip forge-gui-android/target/upload/
cd forge-gui-android/target/upload/
# Get the first APK file in the folder
ls
apk_file=$(find . -maxdepth 1 -type f -name '*.apk' -print -quit)
if [ -n "$apk_file" ]; then
version=$(echo "$apk_file" | grep -oP 'forge-android-\K\d+\.\d+\.\d+-SNAPSHOT' | sed 's/-signed-aligned.apk//')
echo "APK File: $apk_file"
echo "Version: $version"
mv *.apk "forge-android-$version-$d-signed-aligned.apk"
echo "$version-$d" > version.txt
else
echo "No .apk files found in the specified folder."
fi
cd -
env:
GITHUB_TOKEN: ${{ github.token }}
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
#if: ${{ inputs.upload_package }}
with:
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: upload/
local-dir: forge-gui-android/target/upload/
server-dir: downloads/dailysnapshots/
state-name: .ftp-deploy-android-sync-state.json
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Android Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.run_url }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -8,6 +8,9 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '30 18 * * *'
jobs:
build:
@@ -20,10 +23,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '11'
distribution: 'temurin'
cache: 'maven'
server-id: cardforge-repo
@@ -42,7 +45,7 @@ jobs:
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
mvn -U -B clean -P windows-linux install -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
mvn -U -B clean -P windows-linux install -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -52,27 +55,13 @@ jobs:
- name: Rename before upload
run: |
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 to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/build.txt izpack/
cd izpack
d=$(date +%m.%d)
# rename files and append date
for file in *.jar; do
bname="${file%.*}"
echo "file renamed to ${bname}-${d}.jar"
mv "${bname}.jar" "${bname}-${d}.jar"
done
for file in *.bz2; do
# remove .bz2
fname="${file%.*}"
# remove .tar
bname="${fname%.*}"
echo "file renamed to ${bname}-${d}.tar.bz2"
mv "${fname}.bz2" "${bname}-${d}.tar.bz2"
done
mkdir tarball
# If this works just gotta figure out how to append datetime
mv /home/runner/.m2/repository/forge/forge-gui-desktop/*/*.bz2 tarball/
cd tarball
out="$(basename -s .tar.bz2 *)"
d=$(date +%m-%d)
mv "${out}.tar.bz2" "${out}-${d}.tar.bz2"
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
@@ -80,16 +69,13 @@ jobs:
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: izpack/
local-dir: tarball/
server-dir: downloads/dailysnapshots/
exclude: |
*.jar
*.pom
*.repositories
*.xml
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Desktop Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.run_url }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -1,60 +0,0 @@
name: Test Android build
on:
push:
paths: [ 'forge-gui-android/**' ]
pull_request:
paths: [ 'forge-gui-android/**' ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
deployments: write
packages: write
strategy:
matrix:
java: [ '17' ]
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
cache: '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: Set Up Android tools
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"
- name: Install Android maven plugin
run: |
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
#mvn install:install-file -Dfile=android-maven-plugin-4.6.2.jar -DgroupId=com.simpligility.maven.plugins -DartifactId=android-maven-plugin -Dversion=4.6.2 -Dpackaging=jar
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
- name: Install virtual framebuffer (if not available) to allow running GUI on a headless server
run: command -v Xvfb >/dev/null 2>&1 || { sudo apt update && sudo apt install -y xvfb; }
- name: Run build in virtual framebuffer
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
mvn -U -B -P android-test-build verify -e -T 1C -Dandroid.sdk.path=$ANDROID_SDK_ROOT -Dandroid.buildToolsVersion=35.0.0 -Dmaven.test.skip=true

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17' ]
java: [ '8', '11' ]
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3
@@ -26,4 +26,4 @@ jobs:
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
mvn -U -B clean test
mvn -U -B clean -P windows-linux test

View File

@@ -14,10 +14,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '11'
distribution: 'temurin'
cache: 'maven'
server-id: cardforge-repo

3
.gitignore vendored
View File

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

View File

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

View File

@@ -7,7 +7,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
## Requirements / Tools
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- Java JDK 17 or later
- Java JDK 8 or later (some IDEs such as Eclipse require JDK11+, whereas the Android build currently only works with JDK8)
- Git
- Git client (optional)
- Maven
@@ -46,9 +46,9 @@ At this time, Eclipse is not the recommended IDE for Forge development.
- Clone your forked repo to your local machine.
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
- Make sure the Java SDK is installed -- not just the JRE. Java 8 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 1.8 or later.
- Install Eclipse 2021-12 or later for Java. Launch it.
- Install Eclipse 2018-12 or later for Java. Launch it.
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
ensure everything is checked > Finish.
@@ -79,15 +79,16 @@ This is the configuration used for doing mobile development using the Windows /
### Eclipse / Android SDK Integration
Google no longer supports Android SDK releases for Eclipse. use IntelliJ.
Google no longer supports Android SDK releases for Eclipse. That said, it is still possible to build and debug Android platforms.
#### Android SDK
TBD
Reference SO for obtaining a specific release: https://stackoverflow.com/questions/27043522/where-can-i-download-an-older-version-of-the-android-sdk
##### Windows
TBD
Download the following archived version of the Android SDK: http://dl-ssl.google.com/android/repository/tools_r25.2.3-windows.zip. Install it somewhere on your machine. This is referenced
in the following instructions as your 'Android SDK Install' path.
##### Linux / Mac OSX
@@ -95,30 +96,68 @@ TBD
#### Android Plugin for Eclipse
TBD
Google's last plugin release does not work completely with target's running Android 7.0 or later. Download the ADT-24.2.0-20160729.zip plugin
from: https://github.com/khaledev/ADT/releases
In Eclipse go to: Help > Install New Software... > Add > Name: ADT Update, Click on the "Archive:" button and navigate to the downloaded ADT-24.2.0-20160729.zip file > Add. Install all "Developer Tools". Eclipse
should restart and prompt you to run the SDK Manager. Launch it and continue to the next steps below.
#### Android Platform
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
In Eclipse, if the SDK Manager is not already running, go to Window > Android SDK Manager. Install the following options / versions:
- Android SDK Build-tools 35.0.0
- Android 15 (API 35) SDK Platform
- Android SDK Build-tools 26.0.1
- Android 8.0.0 (API 26) SDK Platform
- Google USB Driver (in case your phone is not detected by ADB)
Note that this will populate additional tools in the Android SDK install path extracted above.
#### Proguard update
Standalone Proguard 7.6.0 is included with the project (proguard.jar) under forge-gui-android > tools and supports up to Java 23 (latest android uses Java 17).
The Proguard included with the Android SDK Build-tools is outdated and does not work with Java 1.8. Download Proguard 6.0.3 or later (last tested with 7.0.1) from https://github.com/Guardsquare/proguard
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard-4.7/.
- Extract your Proguard version to the Android SDK install path under tools/. You will need to either rename the dir proguard-<your-version> to proguard/ or, if your filesystem supports it, use a symbolic link (the later is highly recommended), such as `ln -s proguard proguard-<your-version>`.
#### Android Build
TBD
The Eclipse plug-ins do NOT support building things for Android. They do however allow you to use the debugger so you can still set breakpoints and trace
things out. The steps below show how to generate a debug Android build.
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
- On the Main tab, set Goals: clean install
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
3) Right-click on the forge-gui-android project. Run as.. > Maven build...
- On the Main tab, set Goals: install, Profiles: android-debug
- On the Environment tab, you may need to define the variable ANDROID_HOME with the value containing the path to your Android SDK installation. For example, Variable: ANDROID_HOME, Value: Your Android SDK install path here.
4) Run the forge-gui-android Maven build. This may take a few minutes. If everything worked, you should see "BUILD SUCCESS" in the Console View.
Assuming you got this far, you should have an Android forge-android-[version].apk in the forge-gui-android/target path.
#### Android Deploy
TBD
You'll need to have the Android SDK install path platform-tools/ path in your command search path to easily deploy builds.
- Open a command prompt. Navigate to the forge-gui-android/target/ path.
- Connect your Android device to your dev machine.
- Ensure the device is visible using `adb devices`
- Remove the old Forge install if present: `adb uninstall forge.app`
- Install the new apk: `adb install forge-android-[version].apk`
#### Android Debugging
TBD
Assuming the apk is installed, launch it from the device.
In Eclipse, launch the DDMS. Window > Perspective > Open Perspective > Other... > DDMS. You should see the forge app in the list. Highlight the app, click on the green debug button and a
green debug button should appear next to the app's name. You can now set breakpoints and step through the source code.
### Windows / Linux SNAPSHOT build

106
README.md
View File

@@ -1,100 +1,50 @@
# ⚔️ Forge: The Magic: The Gathering Rules Engine
# Forge
Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
Join the [Discord](https://discord.gg/HcPJNyD66a)
[![Test build](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml/badge.svg)](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml)
---
## Introduction
## ✨ Introduction
Forge is a "Rules Engine" for the game Magic: the Gathering.
Forge is not related in any way with Wizards of the Coast.
Forge is open source software released under the GNU Public License.
Forge is developed by a community of programmers who love trading card games.
**Forge** is a dynamic and open-source **Rules Engine** tailored for **Magic: The Gathering** enthusiasts. Developed by a community of passionate programmers, Forge allows players to explore the rich universe of MTG through a flexible, engaging platform.
Forge is a cross-platform application and can be run on Windows, Mac, Linux and Android. It is written in Java. The engine is written in Java. The engine is designed to be extensible, so any interested programmer can join and help add new features and cards to the game. Any tech savvy user could read out card scripting system to create cards to be used inside Forge.
The engine allows you to play in a handful of different single player environments or online against other players.
**Note:** Forge operates independently and is not affiliated with Wizards of the Coast.
---
## Installation
## 🌟 Key Features
For a more in depth User Guide, please visit the [User Guide](https://github.com/Card-Forge/forge/wiki/User-Guide)
- **🌐 Cross-Platform Support:** Play on **Windows, Mac, Linux,** and **Android**.
- **🔧 Extensible Architecture:** Built in **Java**, Forge encourages developers to contribute by adding features and cards.
- **🎮 Versatile Gameplay:** Dive into single-player modes or challenge opponents online!
For Desktop users, download the [Latest Releases](https://github.com/Card-Forge/forge/releases/latest) which are typically based around Set releases.
Or download the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/) the file that starts with "forge-gui-desktop".
This file is tarball, and may need to be extracted twice depending on which program is being used to extract it.
We recommend extracting to a new folder rather than on top of an existing installation.
**For users who have played Forge before all of your user data is stored separately so you don't have to worry about losing it on upgrade.**
---
Java 8 or later is required to run Forge. Please make sure is the right version is installed in your enviroment. Check the user guide for more info.
## 🛠️ Installation Guide
For Android users, download the APK file from [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/) to your device.
On first run, Forge will download all needed data.
### 📥 Desktop Installation
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots).
- **Tip:** Extract to a new folder to prevent version conflicts.
3. **User Data Management:** Previous players data is preserved during upgrades.
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
## Modes of Play
### 📱 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.
Forge has a variety of ways to play the game. The most popular way is our Adventure mode, which is a single player campaign that allows you to play against a variety of AI opponents.
You walk around an overworld map, and can challenge opponents to games of Magic. As you play, you'll collect more cards and items to improve your abilities.
---
## 🎮 Modes of Play
Forge offers various exciting gameplay options:
### 🌍 Adventure Mode
Embark on a thrilling single-player journey where you can:
- Explore an overworld map.
- Challenge diverse AI opponents.
- Collect cards and items to boost your abilities.
Check the [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide) for more info.
![Adventure Mode](https://downloads.cardforge.org/images/site/adventure-mode.png "Adventure Mode")
### 🔍 Quest Modes
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
### 🤖 AI Formats
Test your skills against AI in multiple formats:
- **Sealed**
- **Draft**
- **Commander**
- **Cube**
Forge has several Quest modes, which is similar but without the overworld map.
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
You can also play against the AI in a variety of formats, such as Sealed, Draft, Commander and Cube.
---
## Questions
## 💬 Support & Community
Need help? Join our vibrant Discord community!
- 📜 Read the **#rules** and explore the **FAQ**.
- ❓ Ask your questions in the **#help** channel for assistance.
---
## 🤝 Contributing to Forge
We love community contributions! Interested in helping? Check out our [Contributing Guidelines](CONTRIBUTING.md) for details on how to get started.
---
## About Forge
Forge aims to deliver an immersive and customizable Magic: The Gathering experience for fans around the world.
### 📊 Repository Statistics
| Metric | Count |
|----------------|-------------------------------------------------------------|
| **⭐ Stars:** | [![GitHub stars](https://img.shields.io/github/stars/Card-Forge/forge?style=flat-square)](https://github.com/Card-Forge/forge/stargazers) |
| **🍴 Forks:** | [![GitHub forks](https://img.shields.io/github/forks/Card-Forge/forge?style=flat-square)](https://github.com/Card-Forge/forge/network) |
| **👥 Contributors:** | [![GitHub contributors](https://img.shields.io/github/contributors/Card-Forge/forge?style=flat-square)](https://github.com/Card-Forge/forge/graphs/contributors) |
---
**📄 License:** [GPL-3.0](LICENSE)
<div align="center" style="display: flex; align-items: center; justify-content: center;">
<div style="margin-left: auto;">
<a href="#top">
<img src="https://img.shields.io/badge/Back%20to%20Top-000000?style=for-the-badge&logo=github&logoColor=white" alt="Back to Top">
</a>
</div>
</div>
If you have any questions, please join the Discord channel. Read the #rules and the frequently-asked-questions.
If your question is not answered there, feel free to ask in the #help channel.

View File

@@ -1,117 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>adventure-editor</artifactId>
<packaging>jar</packaging>
<name>Adventure Editor</name>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<resources>
<resource>
<directory>${project.basedir}</directory>
<includes>
<include>**/gear.gif</include>
</includes>
</resource>
</resources>
<finalName>adventure-editor</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
<version>1.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<basedir>${basedir}/${configSourceDirectory}</basedir>
<filesToInclude>adventure-editor.sh, adventure-editor.command, adventure-editor.cmd</filesToInclude>
<outputBasedir>${project.build.directory}</outputBasedir>
<outputDir>.</outputDir>
<regex>false</regex>
<replacements>
<replacement>
<token>$project.build.finalName$</token>
<value>${project.build.finalName}-jar-with-dependencies.jar</value>
</replacement>
</replacements>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<attach>false</attach>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>forge.adventure.Main</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<SplashScreen-Image>splash/gear.gif</SplashScreen-Image>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<!-- this is used for inheritance merges -->
<phase>package</phase>
<!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>forge</groupId>
<artifactId>forge-gui</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>forge</groupId>
<artifactId>forge-gui-mobile</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>26.0.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

View File

@@ -1,24 +0,0 @@
@echo off
pushd %~dp0
java -version 1>nul 2>nul || (
echo no java installed
popd
exit /b 2
)
for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j"
if %jver% LEQ 16 (
echo unsupported java
popd
exit /b 2
)
if %jver% GEQ 17 (
java -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd
exit /b 0
)
popd

View File

@@ -1,97 +0,0 @@
package forge.adventure.editor;
import forge.adventure.util.Config;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.UIManager;
import java.awt.BorderLayout;
import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.List;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class EditorMainWindow extends JFrame {
public final static WorldEditor worldEditor = new WorldEditor();
JTabbedPane tabs = new JTabbedPane();
public EditorMainWindow(Config config) {
UIManager.LookAndFeelInfo[] var1 = UIManager.getInstalledLookAndFeels();
for (UIManager.LookAndFeelInfo info : var1) {
if ("Nimbus".equals(info.getName())) {
try {
UIManager.setLookAndFeel(info.getClassName());
} catch (Throwable ignored) {
}
break;
}
}
this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
setVisible(false);
System.exit(0);
}
});
BorderLayout layout = new BorderLayout();
JToolBar toolBar = new JToolBar("toolbar");
JButton newButton = new JButton("GDX Particle Editor Tool");
newButton.addActionListener(e -> EventQueue.invokeLater(() -> {
newButton.setEnabled(false);
try {
CodeSource codeSource = EditorMainWindow.class.getProtectionDomain().getCodeSource();
File jarFile = new File(codeSource.getLocation().toURI().getPath());
String jarDir = jarFile.getParentFile().getPath();
Desktop.getDesktop().open(new File(jarDir + "/gdx-particle-editor.jar"));
} catch (Exception ex) {
new ErrorDialog("Error", ex.getMessage());
newButton.setEnabled(true);
}
}));
JButton quit = new JButton("Quit");
quit.addActionListener(e -> System.exit(0));
toolBar.add(newButton);
toolBar.add(quit);
setLayout(layout);
toolBar.setFloatable(false);
add(toolBar, BorderLayout.NORTH);
add(tabs, BorderLayout.CENTER);
tabs.addTab("World", worldEditor);
tabs.addTab("POI", new PointOfInterestEditor());
tabs.addTab("Items", new ItemsEditor());
tabs.addTab("Enemies", new EnemyEditor());
tabs.addTab("Quests", new QuestEditor());
setSize(config.getSettingData().width, config.getSettingData().height);
setLocationRelativeTo(null);
setVisible(true);
}
static class ErrorDialog {
public ErrorDialog(String title, String message) {
List<Object> options = new ArrayList<>();
JButton ok = new JButton("OK");
options.add(ok);
JOptionPane pane = new JOptionPane(message, JOptionPane.PLAIN_MESSAGE, JOptionPane.DEFAULT_OPTION, null, options.toArray());
JDialog dlg = pane.createDialog(JOptionPane.getRootFrame(), title);
ok.addActionListener(e -> {
dlg.setVisible(false);
System.exit(0);
});
dlg.setResizable(false);
dlg.setVisible(true);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

309
forge-adventure/pom.xml Normal file
View File

@@ -0,0 +1,309 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.64</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>forge-adventure</artifactId>
<packaging>jar</packaging>
<name>Forge Adventure</name>
<repositories>
<repository>
<id>4thline-repo</id>
<url>http://4thline.org/m2</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<resources>
<resource>
<directory>${project.basedir}</directory>
<includes>
<include>**/*.vert</include>
<include>**/*.frag</include>
<include>**/title_bg_lq.png</include>
<include>**/title_bg_lq_portrait.png</include>
<include>**/transition.png</include>
<include>**/adv_bg_texture.jpg</include>
<include>**/adv_bg_splash.png</include>
<include>**/bg_splash.png</include>
<include>**/bg_texture.jpg</include>
<include>**/font1.ttf</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>com.akathist.maven.plugins.launch4j</groupId>
<artifactId>launch4j-maven-plugin</artifactId>
<version>1.7.25</version>
<executions>
<execution>
<id>l4j-adv</id>
<phase>package</phase>
<goals>
<goal>launch4j</goal>
</goals>
<configuration>
<headerType>gui</headerType>
<outfile>${project.build.directory}/forge-adventure-editor-java8.exe</outfile>
<jar>${project.build.finalName}-jar-with-dependencies.jar</jar>
<dontWrapJar>true</dontWrapJar>
<errTitle>forge</errTitle>
<icon>src/main/config/forge-adventure-editor.ico</icon>
<classPath>
<mainClass>forge.adventure.Main</mainClass>
<addDependencies>false</addDependencies>
<preCp>anything</preCp>
</classPath>
<jre>
<minVersion>1.8.0</minVersion>
<maxHeapSize>4096</maxHeapSize>
<opts>
<opt>-Dfile.encoding=UTF-8</opt>
</opts>
</jre>
<versionInfo>
<fileVersion>
1.0.0.0
</fileVersion>
<txtFileVersion>
1.0.0.0
</txtFileVersion>
<fileDescription>Forge</fileDescription>
<copyright>Forge</copyright>
<productVersion>
1.0.0.0
</productVersion>
<txtProductVersion>
1.0.0.0
</txtProductVersion>
<productName>forge-adventure-editor</productName>
<internalName>forge-adventure-editor</internalName>
<originalFilename>forge-adventure-editor-java8.exe</originalFilename>
</versionInfo>
</configuration>
</execution>
<!--extra-->
<execution>
<id>l4j-adv2</id>
<phase>package</phase>
<goals>
<goal>launch4j</goal>
</goals>
<configuration>
<headerType>gui</headerType>
<outfile>${project.build.directory}/forge-adventure-editor.exe</outfile>
<jar>${project.build.finalName}-jar-with-dependencies.jar</jar>
<dontWrapJar>true</dontWrapJar>
<errTitle>forge</errTitle>
<downloadUrl>https://www.oracle.com/java/technologies/downloads/</downloadUrl>
<icon>src/main/config/forge-adventure-editor.ico</icon>
<classPath>
<mainClass>forge.adventure.Main</mainClass>
<addDependencies>false</addDependencies>
<preCp>anything</preCp>
</classPath>
<jre>
<minVersion>11.0.1</minVersion>
<jdkPreference>jdkOnly</jdkPreference>
<maxHeapSize>4096</maxHeapSize>
<opts>
<opt>-Dfile.encoding=UTF-8</opt>
<opt>--add-opens java.base/java.lang=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.math=ALL-UNNAMED</opt>
<opt>--add-opens java.base/jdk.internal.misc=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.nio=ALL-UNNAMED</opt>
<opt>--add-opens=java.base/sun.nio.ch=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.util=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.lang.reflect=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.text=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt.font=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt.image=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.awt.color=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/sun.awt.image=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/javax.swing=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/javax.swing.border=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/javax.swing.event=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/sun.swing=ALL-UNNAMED</opt>
<opt>--add-opens java.desktop/java.beans=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.util.concurrent=ALL-UNNAMED</opt>
<opt>--add-opens java.base/java.net=ALL-UNNAMED</opt>
<opt>-Dio.netty.tryReflectionSetAccessible=true</opt>
</opts>
</jre>
<versionInfo>
<fileVersion>
1.0.0.0
</fileVersion>
<txtFileVersion>
1.0.0.0
</txtFileVersion>
<fileDescription>Forge</fileDescription>
<copyright>Forge</copyright>
<productVersion>
1.0.0.0
</productVersion>
<txtProductVersion>
1.0.0.0
</txtProductVersion>
<productName>forge-adventure-editor</productName>
<internalName>forge-adventure-editor</internalName>
<originalFilename>forge-adventure-editor.exe</originalFilename>
</versionInfo>
</configuration>
</execution>
<!--extra-->
</executions>
</plugin>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
<version>1.5.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<basedir>${basedir}/${configSourceDirectory}</basedir>
<filesToInclude>forge-adventure-editor.sh, forge-adventure-editor-mac.sh, forge-adventure-editor.command, forge-adventure-editor.cmd</filesToInclude>
<outputBasedir>${project.build.directory}</outputBasedir>
<outputDir>.</outputDir>
<regex>false</regex>
<replacements>
<replacement>
<token>$project.build.finalName$</token>
<value>${project.build.finalName}-jar-with-dependencies.jar</value>
</replacement>
</replacements>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<attach>false</attach>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>forge.adventure.Main</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<!-- this is used for inheritance merges -->
<phase>package</phase>
<!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.12.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype</artifactId>
<version>1.12.1</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>1.12.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-tools</artifactId>
<version>1.12.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>forge</groupId>
<artifactId>forge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>forge</groupId>
<artifactId>forge-gui</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>forge</groupId>
<artifactId>forge-gui-mobile</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>22.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx-controllers</groupId>
<artifactId>gdx-controllers-desktop</artifactId>
<version>2.2.3</version>
<exclusions>
<exclusion>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>

View File

@@ -0,0 +1,16 @@
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_color;
varying vec2 v_texCoords;
uniform sampler2D u_texture;
uniform float u_grayness;
uniform float u_bias;
void main() {
vec4 c = v_color * texture2D(u_texture, v_texCoords);
float grey = dot( c.rgb, vec3(0.22, 0.707, 0.071) );
vec3 blendedColor = mix(c.rgb, vec3(grey), u_grayness);
gl_FragColor = mix(vec4(0.0, 0.0, 0.0, 1.0), vec4(blendedColor.rgb, c.a), u_bias);
}

View File

@@ -0,0 +1,14 @@
attribute vec4 a_position;
attribute vec4 a_color;
attribute vec2 a_texCoord0;
uniform mat4 u_projTrans;
varying vec4 v_color;
varying vec2 v_texCoords;
void main() {
v_color = a_color;
v_texCoords = a_texCoord0;
gl_Position = u_projTrans * a_position;
}

View File

@@ -0,0 +1,40 @@
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
uniform sampler2D u_texture;
uniform vec2 u_viewportInverse;
uniform vec3 u_color;
uniform float u_offset;
uniform float u_step;
varying vec4 v_color;
varying vec2 v_texCoord;
#define ALPHA_VALUE_BORDER 0.5
void main() {
vec2 T = v_texCoord.xy;
float alpha = 0.0;
bool allin = true;
for( float ix = -u_offset; ix < u_offset; ix += u_step )
{
for( float iy = -u_offset; iy < u_offset; iy += u_step )
{
float newAlpha = texture2D(u_texture, T + vec2(ix, iy) * u_viewportInverse).a;
allin = allin && newAlpha > ALPHA_VALUE_BORDER;
if (newAlpha > ALPHA_VALUE_BORDER && newAlpha >= alpha)
{
alpha = newAlpha;
}
}
}
if (allin)
{
alpha = 0.0;
}
gl_FragColor = vec4(u_color,alpha);
}

View File

@@ -0,0 +1,16 @@
uniform mat4 u_projTrans;
attribute vec4 a_position;
attribute vec2 a_texCoord0;
attribute vec4 a_color;
varying vec4 v_color;
varying vec2 v_texCoord;
uniform vec2 u_viewportInverse;
void main() {
gl_Position = u_projTrans * a_position;
v_texCoord = a_texCoord0;
v_color = a_color;
}

View File

@@ -0,0 +1,26 @@
#ifdef GL_ES
#define PRECISION mediump
precision PRECISION float;
precision PRECISION int;
#else
#define PRECISION
#endif
varying vec2 v_texCoords;
uniform sampler2D u_texture;
uniform float u_amount;
uniform float u_speed;
uniform float u_time;
uniform float u_bias;
void main () {
vec2 uv = v_texCoords;
uv.y += (cos((uv.y + (u_time * 0.04 * u_speed)) * 45.0) * 0.0019 * u_amount) + (cos((uv.y + (u_time * 0.1 * u_speed)) * 10.0) * 0.002 * u_amount);
uv.x += (sin((uv.y + (u_time * 0.07 * u_speed)) * 15.0) * 0.0029 * u_amount) + (sin((uv.y + (u_time * 0.1 * u_speed)) * 15.0) * 0.002 * u_amount);
vec4 texColor = texture2D(u_texture, uv);
gl_FragColor = mix(vec4(0.0, 0.0, 0.0, 1.0), texColor, u_bias);
}

View File

@@ -0,0 +1,57 @@
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 v_texCoords;
uniform sampler2D u_texture;
uniform float u_time;
uniform float u_speed;
uniform float u_amount;
uniform vec2 u_viewport;
uniform vec2 u_position;
float random2d(vec2 n) {
return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}
float randomRange (in vec2 seed, in float min, in float max) {
return min + random2d(seed) * (max - min);
}
float insideRange(float v, float bottom, float top) {
return step(bottom, v) - step(top, v);
}
void main()
{
float time = floor(u_time * u_speed * 60.0);
vec3 outCol = texture2D(u_texture, v_texCoords).rgb;
float maxOffset = u_amount/2.0;
for (float i = 0.0; i < 2.0; i += 1.0) {
float sliceY = random2d(vec2(time, 2345.0 + float(i)));
float sliceH = random2d(vec2(time, 9035.0 + float(i))) * 0.25;
float hOffset = randomRange(vec2(time, 9625.0 + float(i)), -maxOffset, maxOffset);
vec2 uvOff = v_texCoords;
uvOff.x += hOffset;
if (insideRange(v_texCoords.y, sliceY, fract(sliceY+sliceH)) == 1.0){
outCol = texture2D(u_texture, uvOff).rgb;
}
}
float maxColOffset = u_amount / 6.0;
float rnd = random2d(vec2(time , 9545.0));
vec2 colOffset = vec2(randomRange(vec2(time , 9545.0), -maxColOffset, maxColOffset),
randomRange(vec2(time , 7205.0), -maxColOffset, maxColOffset));
if (rnd < 0.33) {
outCol.r = texture2D(u_texture, v_texCoords + colOffset).r;
} else if (rnd < 0.66) {
outCol.g = texture2D(u_texture, v_texCoords + colOffset).g;
} else {
outCol.b = texture2D(u_texture, v_texCoords + colOffset).b;
}
gl_FragColor = vec4(outCol, 1.0);
}

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd $(dirname "${0}")
java -XstartOnFirstThread -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$

View File

@@ -0,0 +1,25 @@
@echo off
pushd %~dp0
java -version 1>nul 2>nul || (
echo no java installed
popd
exit /b 2
)
for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j"
if %jver% GEQ 17 (
java --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED --add-opens java.desktop/sun.swing=ALL-UNNAMED --add-opens java.desktop/java.awt.image=ALL-UNNAMED --add-opens java.desktop/java.awt.color=ALL-UNNAMED --add-opens java.desktop/sun.awt.image=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd
exit /b 0
)
if %jver% GEQ 11 (
java --illegal-access=permit -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd
exit /b 0
)
java -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -16,6 +16,7 @@ public class Main {
public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo("", "", 0, 0);
new EditorMainWindow(Config.instance());
Config.instance();
new EditorMainWindow();
}
}

View File

@@ -2,9 +2,7 @@ package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
@@ -160,9 +158,9 @@ public class ActionEdit extends FormPanel {
advanceQuestFlag.setText(currentData.advanceQuestFlag);
advanceCharacterFlag.setText(currentData.advanceCharacterFlag);
battleWithActorID.setText(String.valueOf(currentData.battleWithActorID));
activateObjectID.setText(String.valueOf(currentData.battleWithActorID));
deleteMapObject.setText(String.valueOf(currentData.deleteMapObject));
battleWithActorID.setText("" + currentData.battleWithActorID);
activateObjectID.setText("" + currentData.battleWithActorID);
deleteMapObject.setText("" + currentData.deleteMapObject);
setColorIdentity.setText(currentData.setColorIdentity);
addLife.getModel().setValue(currentData.addLife);
addReputation.getModel().setValue(currentData.addMapReputation);

View File

@@ -2,27 +2,20 @@ package forge.adventure.editor;
import forge.adventure.data.DialogData;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JToolBar;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.*;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class ActionEditor extends JComponent {
public class ActionEditor extends JComponent{
DefaultListModel<DialogData.ActionData> model = new DefaultListModel<>();
JList<DialogData.ActionData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
ActionEdit edit = new ActionEdit();
ActionEdit edit=new ActionEdit();
boolean updating;
@@ -32,43 +25,43 @@ public class ActionEditor extends JComponent {
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (!(value instanceof DialogData.ActionData))
if(!(value instanceof DialogData.ActionData))
return label;
/*DialogData.ActionData action=(DialogData.ActionData) value;
DialogData.ActionData action=(DialogData.ActionData) value;
StringBuilder builder=new StringBuilder();
if(action.type==null||action.type.isEmpty())
// if(action.type==null||action.type.isEmpty())
builder.append("Action");
else
builder.append(action.type);*/
label.setText("Action");
// else
// builder.append(action.type);
label.setText(builder.toString());
return label;
}
}
public void addButton(String name, ActionListener action) {
JButton newButton = new JButton(name);
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public ActionEditor() {
public ActionEditor()
{
list.setCellRenderer(new RewardDataRenderer());
list.addListSelectionListener(e -> ActionEditor.this.updateEdit());
addButton("add", e -> ActionEditor.this.addAction());
addButton("remove", e -> ActionEditor.this.remove());
addButton("copy", e -> ActionEditor.this.copy());
BorderLayout layout = new BorderLayout();
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(list, BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
add(edit, BorderLayout.CENTER);
add(edit,BorderLayout.CENTER);
edit.addChangeListener(e -> emitChanged());
}
protected void emitChanged() {
if (updating)
return;
@@ -80,64 +73,63 @@ public class ActionEditor extends JComponent {
}
}
}
private void copy() {
int selected = list.getSelectedIndex();
if (selected < 0)
int selected=list.getSelectedIndex();
if(selected<0)
return;
DialogData.ActionData data = new DialogData.ActionData(model.get(selected));
model.add(model.size(), data);
DialogData.ActionData data=new DialogData.ActionData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected = list.getSelectedIndex();
if (selected < 0)
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentAction(model.get(selected));
}
void addAction() {
DialogData.ActionData data = new DialogData.ActionData();
model.add(model.size(), data);
void addAction()
{
DialogData.ActionData data=new DialogData.ActionData();
model.add(model.size(),data);
}
void remove() {
int selected = list.getSelectedIndex();
if (selected < 0)
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
public void setAction(DialogData.ActionData[] actions) {
model.clear();
if (actions == null)
if(actions==null)
return;
for (int i = 0; i < actions.length; i++) {
if (actions[i].grantRewards.length > 0) {
for (int i=0;i<actions.length;i++) {
if (actions[i].grantRewards.length > 0){
continue; //handled in separate editor and joined in on save, will get duplicated if it appears here
}
model.add(i, actions[i]);
model.add(i,actions[i]);
}
}
public DialogData.ActionData[] getAction() {
DialogData.ActionData[] action = new DialogData.ActionData[model.getSize()];
for (int i = 0; i < model.getSize(); i++) {
action[i] = model.get(i);
DialogData.ActionData[] action= new DialogData.ActionData[model.getSize()];
for(int i=0;i<model.getSize();i++)
{
action[i]=model.get(i);
}
return action;
}
public void clear() {
public void clear(){
updating = true;
model.clear();
updating = false;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
package forge.adventure.editor;
import com.badlogic.gdx.tools.particleeditor.ParticleEditor;
import forge.localinstance.properties.ForgeConstants;
import forge.localinstance.properties.ForgePreferences;
import forge.model.FModel;
import forge.util.Lang;
import forge.util.Localizer;
import javax.swing.*;
import java.awt.*;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class EditorMainWindow extends JFrame {
public final static WorldEditor worldEditor = new WorldEditor();
JTabbedPane tabs =new JTabbedPane();
public EditorMainWindow()
{
UIManager.LookAndFeelInfo[] var1 = UIManager.getInstalledLookAndFeels();
FModel.initialize(null, preferences -> {
preferences.setPref(ForgePreferences.FPref.LOAD_CARD_SCRIPTS_LAZILY, true);
return null;
});
Lang.createInstance(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_LANGUAGE));
Localizer.getInstance().initialize(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_LANGUAGE), ForgeConstants.LANG_DIR);
int var2 = var1.length;
for (UIManager.LookAndFeelInfo info : var1) {
if ("Nimbus".equals(info.getName())) {
try {
UIManager.setLookAndFeel(info.getClassName());
} catch (Throwable var6) {
}
break;
}
}
BorderLayout layout=new BorderLayout();
JToolBar toolBar = new JToolBar("toolbar");
JButton newButton=new JButton("open ParticleEditor");
newButton.addActionListener(e -> EventQueue.invokeLater(ParticleEditor::new));
toolBar.add(newButton);
setLayout(layout);
toolBar.setFloatable(false);
add(toolBar, BorderLayout.NORTH);
add(tabs, BorderLayout.CENTER);
tabs.addTab("World",worldEditor);
tabs.addTab("POI",new PointOfInterestEditor());
tabs.addTab("Items",new ItemsEditor());
tabs.addTab("Enemies",new EnemyEditor());
tabs.addTab("Quests",new QuestEditor());
setVisible(true);
setSize(800,600);
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow( this );
}
}

View File

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

View File

@@ -2,23 +2,10 @@ package forge.adventure.editor;
import forge.adventure.data.AdventureQuestData;
import javax.swing.AbstractAction;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Enumeration;
@@ -298,7 +285,7 @@ public class QuestEdit extends FormPanel {
}
setVisible(true);
updating=true;
id.setText(String.valueOf(currentData.getID()));
id.setText(currentData.getID() + "");
name.setText(currentData.name);
description.setText(currentData.description);
synopsis.setText(currentData.synopsis);

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.utils.Array;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
@@ -65,7 +65,7 @@ public class SwingAtlas {
return new ImageIcon(img.getSubimage(sprite.left,sprite.top, sprite.width, sprite.height).getScaledInstance((int) (imageSize*(sprite.width/(float)sprite.height)),imageSize,SCALE_FAST));
}
catch (Exception e)
catch (IOException e)
{
return null;
}

View File

@@ -3,17 +3,10 @@ package forge.adventure.editor;
import forge.adventure.data.BiomeData;
import forge.adventure.data.BiomeTerrainData;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JToolBar;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.*;
import java.awt.event.ActionListener;
/**
@@ -36,11 +29,11 @@ public class TerrainsEditor extends JComponent{
if(!(value instanceof BiomeTerrainData))
return label;
BiomeTerrainData terrainData=(BiomeTerrainData) value;
/*StringBuilder builder=new StringBuilder();
StringBuilder builder=new StringBuilder();
builder.append("Terrain");
builder.append(" ");
builder.append(terrainData.spriteName);*/
label.setText("Terrain " + terrainData.spriteName);
builder.append(terrainData.spriteName);
label.setText(builder.toString());
return label;
}
}

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,8 @@
*/
package forge.ai;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi;
@@ -30,28 +32,23 @@ import forge.game.combat.CombatUtil;
import forge.game.combat.GlobalAttackRestrictions;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.*;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.function.Predicate;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -76,9 +73,6 @@ public class AiAttackController {
private int aiAggression = 0; // how aggressive the ai is attack will be depending on circumstances
private final boolean nextTurn; // include creature that can only attack/block next turn
private final int timeOut;
private final boolean canUseTimeout;
private List<CompletableFuture<Integer>> futures = new ArrayList<>();
/**
* <p>
@@ -96,8 +90,6 @@ public class AiAttackController {
myList = ai.getCreaturesInPlay();
this.nextTurn = nextTurn;
refreshCombatants(defendingOpponent);
this.timeOut = ai.getGame().getAITimeout();
this.canUseTimeout = ai.getGame().canUseTimeout();
} // overloaded constructor to evaluate attackers that should attack next turn
public AiAttackController(final Player ai, Card attacker) {
@@ -111,13 +103,11 @@ public class AiAttackController {
attackers.add(attacker);
}
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn);
this.timeOut = ai.getGame().getAITimeout();
this.canUseTimeout = ai.getGame().canUseTimeout();
} // overloaded constructor to evaluate single specified attacker
private void refreshCombatants(GameEntity defender) {
if (defender instanceof Card card && card.isBattle()) {
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
if (defender instanceof Card && ((Card) defender).isBattle()) {
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
} else {
this.oppList = getOpponentCreatures(defendingOpponent);
}
@@ -138,7 +128,7 @@ public class AiAttackController {
CardCollection tappedDefenders = new CardCollection();
for (Card c : CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), canAnimate)) {
for (SpellAbility sa : IterableUtil.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.Animate))) {
for (SpellAbility sa : Iterables.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.Animate))) {
if (sa.usesTargeting() || !sa.getParamOrDefault("Defined", "Self").equals("Self")) {
continue;
}
@@ -162,7 +152,7 @@ public class AiAttackController {
defenders.removeAll(tappedDefenders);
// Transform (e.g. Incubator tokens)
for (SpellAbility sa : IterableUtil.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.SetState))) {
for (SpellAbility sa : Iterables.filter(c.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.SetState))) {
Card transformedCopy = ComputerUtilCombat.canTransform(c);
if (transformedCopy.isCreature()) {
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
@@ -178,7 +168,7 @@ public class AiAttackController {
}
public void removeBlocker(Card blocker) {
this.oppList.remove(blocker);
this.oppList.remove(blocker);
this.blockers.remove(blocker);
}
@@ -295,7 +285,7 @@ public class AiAttackController {
}
if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) {
return true;
return true;
}
// Damage opponent if unblocked
@@ -313,8 +303,7 @@ public class AiAttackController {
}
}
// Poison opponent if unblocked
if (defender instanceof Player player
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) {
return true;
}
@@ -398,7 +387,7 @@ public class AiAttackController {
// reduce the search space
final List<Card> opponentsAttackers = CardLists.filter(ai.getOpponents().getCreaturesInPlay(), c -> !c.hasSVar("EndOfTurnLeavePlay")
&& (c.toughnessAssignsDamage() || c.getNetCombatDamage() > 0 // performance shortcuts
|| c.getNetCombatDamage() + ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true) > 0)
|| c.getNetCombatDamage() + ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true) > 0)
&& ComputerUtilCombat.canAttackNextTurn(c));
// don't hold back creatures that can't block any of the human creatures
@@ -520,9 +509,13 @@ public class AiAttackController {
return;
}
List<String> bandsWithString = Arrays.asList("Bands with Other Legendary Creatures",
"Bands with Other Creatures named Wolves of the Hunt",
"Bands with Other Dinosaurs");
List<Card> bandingCreatures = null;
if (test == null) {
bandingCreatures = CardLists.filter(myList, card -> card.hasKeyword(Keyword.BANDING) || card.hasKeyword(Keyword.BANDSWITH));
bandingCreatures = CardLists.filter(myList, card -> card.hasKeyword(Keyword.BANDING) || card.hasAnyKeyword(bandsWithString));
// filter out anything that can't legally attack or is already declared as an attacker
bandingCreatures = CardLists.filter(bandingCreatures, card -> !combat.isAttacking(card) && CombatUtil.canAttack(card));
@@ -530,7 +523,7 @@ public class AiAttackController {
bandingCreatures = notNeededAsBlockers(attackers, bandingCreatures);
} else {
// Test a specific creature for Banding
if (test.hasKeyword(Keyword.BANDING) || test.hasKeyword(Keyword.BANDSWITH)) {
if (test.hasKeyword(Keyword.BANDING) || test.hasAnyKeyword(bandsWithString)) {
bandingCreatures = new CardCollection(test);
}
}
@@ -548,7 +541,7 @@ public class AiAttackController {
// TODO: Assign to band with the best attacker for now, but needs better logic.
for (Card c : bandingCreatures) {
Card bestBand = null;
Card bestBand;
if (c.getNetPower() <= 0) {
// Don't band a zero power creature if there's already a banding creature in a band
@@ -556,16 +549,12 @@ public class AiAttackController {
}
Card bestAttacker = ComputerUtilCard.getBestCreatureAI(attackers);
// TODO how should this work with multiple bands with other abilities?
if (c.hasKeyword(Keyword.BANDSWITH)) {
for (KeywordInterface kw : c.getKeywords(Keyword.BANDSWITH)) {
final String o = kw.getOriginal();
String m[] = o.split(":");
CardCollection bandPartner = CardLists.getValidCards(attackers, m[1], c.getController(), c, null);
bestBand = ComputerUtilCard.getBestCreatureAI(bandPartner);
break; // ?
}
if (c.hasKeyword("Bands with Other Legendary Creatures")) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.getType(attackers, "Legendary"));
} else if (c.hasKeyword("Bands with Other Dinosaurs")) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.getType(attackers, "Dinosaur"));
} else if (c.hasKeyword("Bands with Other Creatures named Wolves of the Hunt")) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.filter(attackers, CardPredicates.nameEquals("Wolves of the Hunt")));
} else if (!c.hasAnyKeyword(evasionKeywords) && bestAttacker != null && bestAttacker.hasAnyKeyword(evasionKeywords)) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.filter(attackers, card -> !card.hasAnyKeyword(evasionKeywords)));
} else {
@@ -625,9 +614,9 @@ public class AiAttackController {
// TODO: the AI should ideally predict how many times it can activate
// for now, unless the opponent is tapped out, break at this point
// and do not predict the blocker limit (which is safer)
if (defendingOpponent.getLandsInPlay().anyMatch(CardPredicates.UNTAPPED)) {
if (Iterables.any(defendingOpponent.getLandsInPlay(), CardPredicates.Presets.UNTAPPED)) {
maxBlockersAfterCrew += CardLists.count(CardLists.getNotType(defendingOpponent.getCardsIn(ZoneType.Battlefield), "Creature"),
CardPredicates.isType("Vehicle").and(CardPredicates.UNTAPPED));
Predicates.and(CardPredicates.isType("Vehicle"), CardPredicates.Presets.UNTAPPED));
}
}
@@ -804,7 +793,6 @@ public class AiAttackController {
if (bAssault) {
return prefDefender;
}
// 2. attack planeswalkers
List<Card> pwDefending = c.getDefendingPlaneswalkers();
if (!pwDefending.isEmpty()) {
@@ -812,7 +800,7 @@ public class AiAttackController {
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
}
// 3. Get the preferred battle (prefer own battles, then ally battles)
// Get the preferred battle (prefer own battles, then ally battles)
final CardCollection defBattles = c.getDefendingBattles();
List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
@@ -851,9 +839,10 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons)
if (defender != defendingOpponent) {
if (defender instanceof Player p) {
defendingOpponent = p;
} else if (defender instanceof Card defCard) {
if (defender instanceof Player) {
defendingOpponent = (Player) defender;
} else if (defender instanceof Card) {
Card defCard = (Card) defender;
if (defCard.isBattle()) {
defendingOpponent = defCard.getProtectingPlayer();
} else {
@@ -893,7 +882,7 @@ public class AiAttackController {
// TODO: detect Season of the Witch by presence of a card with a specific trigger
final boolean seasonOfTheWitch = ai.getGame().isCardInPlay("Season of the Witch");
final Queue<Card> attackersLeft = new ConcurrentLinkedQueue<>(this.attackers);
List<Card> attackersLeft = new ArrayList<>(this.attackers);
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
@@ -909,76 +898,63 @@ public class AiAttackController {
}
// Attackers that don't really have a choice
final AtomicInteger numForcedAttackers = new AtomicInteger(0);
int numForcedAttackers = 0;
// nextTurn is now only used by effect from Oracle en-Vec, which can skip check must attack,
// because creatures not chosen can't attack.
if (!nextTurn) {
for (final Card attacker : this.attackers) {
final GameEntity finalDefender = defender;
futures.add(CompletableFuture.supplyAsync(()-> {
GameEntity mustAttackDef = null;
if (attacker.getSVar("MustAttack").equals("True")) {
mustAttackDef = finalDefender;
} else if (attacker.hasSVar("EndOfTurnLeavePlay")
&& isEffectiveAttacker(ai, attacker, combat, finalDefender)) {
mustAttackDef = finalDefender;
} else if (seasonOfTheWitch) {
//TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
mustAttackDef = finalDefender;
} else {
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) return 0;
// check defenders in order of maximum requirements
List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements();
final GameEntity def = finalDefender;
reqs.sort((r1, r2) -> {
if (r1.getValue() == r2.getValue()) {
// try to attack the designated defender
if (r1.getKey().equals(def) && !r2.getKey().equals(def)) {
return -1;
}
if (r2.getKey().equals(def) && !r1.getKey().equals(def)) {
return 1;
}
// otherwise PW
if (r1.getKey() instanceof Card && r2.getKey() instanceof Player) {
return -1;
}
if (r2.getKey() instanceof Card && r1.getKey() instanceof Player) {
return 1;
}
// or weakest player
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
return p1.getLife() - p2.getLife();
}
GameEntity mustAttackDef = null;
if (attacker.getSVar("MustAttack").equals("True")) {
mustAttackDef = defender;
} else if (attacker.hasSVar("EndOfTurnLeavePlay")
&& isEffectiveAttacker(ai, attacker, combat, defender)) {
mustAttackDef = defender;
} else if (seasonOfTheWitch) {
//TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
mustAttackDef = defender;
} else {
if (combat.getAttackConstraints().getRequirements().get(attacker) == null) continue;
// check defenders in order of maximum requirements
List<Pair<GameEntity, Integer>> reqs = combat.getAttackConstraints().getRequirements().get(attacker).getSortedRequirements();
final GameEntity def = defender;
reqs.sort((r1, r2) -> {
if (r1.getValue() == r2.getValue()) {
// try to attack the designated defender
if (r1.getKey().equals(def) && !r2.getKey().equals(def)) {
return -1;
}
return r2.getValue() - r1.getValue();
});
for (Pair<GameEntity, Integer> e : reqs) {
if (e.getRight() == 0) continue;
GameEntity mustAttackDefMaybe = e.getLeft();
if (canAttackWrapper(attacker, mustAttackDefMaybe) && CombatUtil.getAttackCost(ai.getGame(), attacker, mustAttackDefMaybe) == null) {
mustAttackDef = mustAttackDefMaybe;
break;
if (r2.getKey().equals(def) && !r1.getKey().equals(def)) {
return 1;
}
// otherwise PW
if (r1.getKey() instanceof Card && r2.getKey() instanceof Player) {
return -1;
}
if (r2.getKey() instanceof Card && r1.getKey() instanceof Player) {
return 1;
}
// or weakest player
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
}
}
return r2.getValue() - r1.getValue();
});
for (Pair<GameEntity, Integer> e : reqs) {
if (e.getRight() == 0) continue;
GameEntity mustAttackDefMaybe = e.getLeft();
if (canAttackWrapper(attacker, mustAttackDefMaybe) && CombatUtil.getAttackCost(ai.getGame(), attacker, mustAttackDefMaybe) == null) {
mustAttackDef = mustAttackDefMaybe;
break;
}
}
if (mustAttackDef != null) {
combat.addAttacker(attacker, mustAttackDef);
attackersLeft.remove(attacker);
numForcedAttackers.incrementAndGet();
}
return 0;
}).exceptionally(ex -> {
ex.printStackTrace();
return 0;
}));
}
if (mustAttackDef != null) {
combat.addAttacker(attacker, mustAttackDef);
attackersLeft.remove(attacker);
numForcedAttackers++;
}
}
CompletableFuture<?>[] futuresArray = futures.toArray(new CompletableFuture<?>[0]);
if (canUseTimeout)
CompletableFuture.allOf(futuresArray).completeOnTimeout(null, timeOut, TimeUnit.SECONDS).join();
else
CompletableFuture.allOf(futuresArray).join();
futures.clear();
if (attackersLeft.isEmpty()) {
return aiAggression;
}
@@ -986,19 +962,18 @@ public class AiAttackController {
// Lightmine Field: make sure the AI doesn't wipe out its own creatures
if (lightmineField) {
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers.get(), playAggro);
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro);
}
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily
if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers.get(), attackMax)) {
if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers, attackMax)) {
return aiAggression;
}
if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else
if (LOG_AI_ATTACKS)
System.out.println("Assault");
List<Card> left = new ArrayList<>(attackersLeft);
CardLists.sortByPowerDesc(left);
for (Card attacker : left) {
CardLists.sortByPowerDesc(attackersLeft);
for (Card attacker : attackersLeft) {
// reached max, breakup
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
return aiAggression;
@@ -1192,8 +1167,10 @@ public class AiAttackController {
attritionalAttackers.remove(attritionalAttackers.size() - 1);
}
}
attackRounds++;
doAttritionalAttack = humanLife <= 0;
attackRounds += 1;
if (humanLife <= 0) {
doAttritionalAttack = true;
}
}
// *********************
// end attritional attack calculation
@@ -1250,7 +1227,7 @@ public class AiAttackController {
if (ratioDiff > 0 && doAttritionalAttack) {
aiAggression = 5; // attack at all costs
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|| (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) {
|| (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) {
aiAggression = 4; // attack expecting to trade or damage player.
} else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1
&& defendingOpponent != null
@@ -1260,7 +1237,7 @@ public class AiAttackController {
&& (ComputerUtilMana.getAvailableManaEstimate(ai) > 0) || tradeIfTappedOut
&& (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana)
&& (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() <
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) {
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) {
aiAggression = 4; // random (chance-based) attack expecting to trade or damage player.
} else if (ratioDiff >= 0 && this.attackers.size() > 1) {
aiAggression = 3; // attack expecting to make good trades or damage player.
@@ -1288,20 +1265,19 @@ public class AiAttackController {
if ( LOG_AI_ATTACKS )
System.out.println("Normal attack");
List<Card> left = new ArrayList<>(attackersLeft);
left = notNeededAsBlockers(combat.getAttackers(), left);
left = sortAttackers(left);
attackersLeft = notNeededAsBlockers(combat.getAttackers(), attackersLeft);
attackersLeft = sortAttackers(attackersLeft);
if ( LOG_AI_ATTACKS )
System.out.println("attackersLeft = " + left);
System.out.println("attackersLeft = " + attackersLeft);
FCollection<GameEntity> possibleDefenders = new FCollection<>(defendingOpponent);
possibleDefenders.addAll(defendingOpponent.getPlaneswalkersInPlay());
while (!left.isEmpty()) {
while (!attackersLeft.isEmpty()) {
CardCollection attackersAssigned = new CardCollection();
for (int i = 0; i < left.size(); i++) {
final Card attacker = left.get(i);
for (int i = 0; i < attackersLeft.size(); i++) {
final Card attacker = attackersLeft.get(i);
if (aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike()
&& ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, defendingOpponent)
>= ComputerUtilCombat.getDamageToKill(attacker, false)) {
@@ -1315,7 +1291,7 @@ public class AiAttackController {
attackersAssigned.add(attacker);
// check if attackers are enough to finish the attacked planeswalker
if (i < left.size() - 1 && defender instanceof Card card) {
if (i < attackersLeft.size() - 1 && defender instanceof Card) {
final int blockNum = this.blockers.size();
int attackNum = 0;
int damage = 0;
@@ -1329,19 +1305,19 @@ public class AiAttackController {
}
}
// if enough damage: switch to next planeswalker
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
break;
}
}
}
}
left.removeAll(attackersAssigned);
attackersLeft.removeAll(attackersAssigned);
possibleDefenders.remove(defender);
if (left.isEmpty() || possibleDefenders.isEmpty()) {
if (attackersLeft.isEmpty() || possibleDefenders.isEmpty()) {
break;
}
CardCollection pwDefending = new CardCollection(IterableUtil.filter(possibleDefenders, Card.class));
CardCollection pwDefending = new CardCollection(Iterables.filter(possibleDefenders, Card.class));
if (pwDefending.isEmpty()) {
// TODO for now only looks at same player as we'd have to check the others from start too
//defender = new PlayerCollection(Iterables.filter(possibleDefenders, Player.class)).min(PlayerPredicates.compareByLife());
@@ -1355,113 +1331,6 @@ public class AiAttackController {
return aiAggression;
}
private class SpellAbilityFactors {
Card attacker = null;
boolean canBeKilled = false; // indicates if the attacker can be killed
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
boolean canKillAll = true; // indicates if the attacker can kill all single blockers
boolean canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
boolean isWorthLessThanAllKillers = true;
boolean hasAttackEffect = false;
boolean hasCombatEffect = false;
boolean dangerousBlockersPresent = false;
boolean canTrampleOverDefenders = false;
int numberOfPossibleBlockers = 0;
int defPower = 0;
SpellAbilityFactors(Card c) {
attacker = c;
}
private boolean canBeBlocked() {
return numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent));
}
private void calculate(final List<Card> defenders, final Combat combat) {
hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasKeyword(Keyword.ANNIHILATOR);
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"))
|| attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT);
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, Card::getNetToughness);
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
dangerousBlockersPresent = validBlockers.anyMatch(
CardPredicates.hasKeyword(Keyword.LIFELINK)
.or(Card::isWitherDamage)
);
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
defPower = CardLists.getTotalPower(validBlockers, null);
// look at the attacker in relation to the blockers to establish a
// number of factors about the attacking context that will be relevant
// to the attackers decision according to the selected strategy
for (final Card blocker : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
if (isWorthLessThanAllKillers && !attacker.hasSVar("SacMe")
&& ComputerUtilCard.evaluateCreature(blocker) <= ComputerUtilCard.evaluateCreature(attacker)) {
isWorthLessThanAllKillers = false;
}
}
// see if this attacking creature can destroy this defender, if
// not record that it can't kill everything
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
canKillAll = false;
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")
|| blocker.isWitherDamage() || blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
}
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
boolean avoidAttackingIntoBlock = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
boolean attackerWillDie = defPower >= attacker.getNetToughness();
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
boolean noContributionToAttack = attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
// We are attacking too recklessly if we can't kill a single blocker and:
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
canKillAllDangerous = false;
}
}
}
}
}
// performance-wise it doesn't seem worth it to check attackVigilance() instead (only includes a single niche card)
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
isWorthLessThanAllKillers = false;
hasCombatEffect = false;
} else if ((canKillAllDangerous || !canBeKilled) && ComputerUtilCard.canBeBlockedProfitably(defendingOpponent, attacker, true)) {
canKillAllDangerous = false;
canBeKilled = true;
}
}
}
/**
* <p>
* shouldAttack.
@@ -1476,6 +1345,14 @@ public class AiAttackController {
* @return a boolean.
*/
public final boolean shouldAttack(final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) {
boolean canBeKilled = false; // indicates if the attacker can be killed
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
boolean canKillAll = true; // indicates if the attacker can kill all single blockers
boolean canKillAllDangerous = true; // indicates if the attacker can kill all single blockers with wither or infect
boolean isWorthLessThanAllKillers = true;
boolean canBeBlocked = false;
int numberOfPossibleBlockers = 0;
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
if (attacker.hasSVar("NonCombatPriority") && !attacker.hasKeyword(Keyword.VIGILANCE)) {
// For each level of priority, enemy has to have life as much as the creature's power
@@ -1486,7 +1363,7 @@ public class AiAttackController {
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine!
for (SpellAbility sa : attacker.getSpellAbilities()) {
// Do not attack if we can afford using the ability.
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) {
if (sa.isActivatedAbility()) {
if (ComputerUtilCost.canPayCost(sa, ai, false)) {
return false;
}
@@ -1500,72 +1377,156 @@ public class AiAttackController {
if (!isEffectiveAttacker(ai, attacker, combat, defender)) {
return false;
}
boolean hasAttackEffect = attacker.getSVar("HasAttackEffect").equals("TRUE") || attacker.hasKeyword(Keyword.ANNIHILATOR);
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE") || "Blocked".equals(attacker.getSVar("HasAttackEffect"));
SpellAbilityFactors saf = new SpellAbilityFactors(attacker);
if (aiAggression != 5) {
saf.calculate(defenders, combat);
if (!hasCombatEffect) {
if (attacker.isWitherDamage() || attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasKeyword(Keyword.AFFLICT)) {
hasCombatEffect = true;
}
}
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, defender1 -> CombatUtil.canBlock(attacker, defender1));
boolean 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
boolean dangerousBlockersPresent = Iterables.any(validBlockers, Predicates.or(
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
CardPredicates.hasKeyword(Keyword.LIFELINK)));
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
int defPower = CardLists.getTotalPower(validBlockers, true, false);
// look at the attacker in relation to the blockers to establish a
// number of factors about the attacking context that will be relevant
// to the attackers decision according to the selected strategy
for (final Card blocker : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
if (isWorthLessThanAllKillers && !attacker.hasSVar("SacMe")
&& ComputerUtilCard.evaluateCreature(blocker) <= ComputerUtilCard.evaluateCreature(attacker)) {
isWorthLessThanAllKillers = false;
}
}
// see if this attacking creature can destroy this defender, if
// not record that it can't kill everything
if (canKillAllDangerous && !ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, false)) {
canKillAll = false;
if (blocker.getSVar("HasCombatEffect").equals("TRUE") || blocker.getSVar("HasBlockEffect").equals("TRUE")) {
canKillAllDangerous = false;
} else {
if (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)
|| blocker.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
}
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
boolean avoidAttackingIntoBlock = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
boolean attackerWillDie = defPower >= attacker.getNetToughness();
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
boolean noContributionToAttack = this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
// We are attacking too recklessly if we can't kill a single blocker and:
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
canKillAllDangerous = false;
}
}
}
}
}
}
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
isWorthLessThanAllKillers = false;
hasCombatEffect = false;
} else if ((canKillAllDangerous || !canBeKilled) && ComputerUtilCard.canBeBlockedProfitably(defendingOpponent, attacker, true)) {
canKillAllDangerous = false;
canBeKilled = true;
}
// if the creature cannot block and can kill all opponents they might as
// well attack, they do nothing staying back
if (saf.canKillAll && saf.isWorthLessThanAllKillers && !CombatUtil.canBlock(attacker)) {
if (canKillAll && isWorthLessThanAllKillers && !CombatUtil.canBlock(attacker)) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player");
return true;
}
if (!saf.canBeKilled && !saf.dangerousBlockersPresent && saf.canTrampleOverDefenders) {
} else if (!canBeKilled && !dangerousBlockersPresent && canTrampleOverDefenders) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through");
return true;
}
if (numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent))) {
canBeBlocked = true;
}
// decide if the creature should attack based on the prevailing strategy choice in aiAggression
switch (aiAggression) {
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
if ((saf.canKillAll && saf.isWorthLessThanAllKillers) || !saf.canBeBlocked()) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
return true;
}
break;
case 5: // all out attacking
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = all out attacking");
System.out.println(attacker.getName() + " = attacking expecting to kill creature, or is unblockable");
return true;
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
if (saf.canKillAll || (saf.dangerousBlockersPresent && saf.canKillAllDangerous && !saf.canBeKilledByOne) || !saf.canBeBlocked()
|| saf.defPower == 0) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
return true;
}
break;
case 3: // expecting to at least kill a creature of equal value or not be blocked
if ((saf.canKillAll && saf.isWorthLessThanAllKillers)
|| (((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne)
|| !saf.canBeBlocked()) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
return true;
}
break;
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
if (!saf.canBeBlocked() || ((saf.canKillAll || saf.hasAttackEffect || saf.hasCombatEffect) && !saf.canBeKilledByOne &&
((saf.dangerousBlockersPresent && saf.canKillAllDangerous) || !saf.canBeKilled))) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
return true;
}
break;
case 1: // unblockable creatures only
if (!saf.canBeBlocked() || (saf.numberOfPossibleBlockers == 1 && saf.canKillAll && !saf.canBeKilledByOne)) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
return true;
}
break;
default:
break;
}
break;
case 5: // all out attacking
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = all out attacking");
return true;
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
|| (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
return true;
}
break;
case 3: // expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers)
|| (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|| !canBeBlocked) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
return true;
}
break;
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
return true;
}
break;
case 1: // unblockable creatures only
if (!canBeBlocked || (numberOfPossibleBlockers == 1 && canKillAll && !canBeKilledByOne)) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting not to be blocked");
return true;
}
break;
default:
break;
}
return false; // don't attack
}
@@ -1588,7 +1549,7 @@ public class AiAttackController {
// but there are no creatures it can target, no need to exert with it
boolean missTarget = false;
for (StaticAbility st : c.getStaticAbilities()) {
if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) {
continue;
}
SpellAbility sa = st.getPayingTrigSA();
@@ -1610,12 +1571,12 @@ public class AiAttackController {
break;
}
if (sa.usesTargeting()) {
sa.setActivatingPlayer(c.getController());
sa.setActivatingPlayer(c.getController(), true);
List<Card> validTargets = CardUtil.getValidCardsToTarget(sa);
if (validTargets.isEmpty()) {
missTarget = true;
break;
} else if (sa.isCurse() && validTargets.stream().noneMatch(
} else if (sa.isCurse() && !Iterables.any(validTargets,
CardPredicates.isControlledByAnyOf(c.getController().getOpponents()))) {
// e.g. Ahn-Crop Crasher - the effect is only good when aimed at opponent's creatures
missTarget = true;
@@ -1682,31 +1643,31 @@ public class AiAttackController {
}
if (color != null) {
switch (color) {
case "black":
if (!c.isBlack()) {
color = null;
}
break;
case "blue":
if (!c.isBlue()) {
color = null;
}
break;
case "green":
if (!c.isGreen()) {
color = null;
}
break;
case "red":
if (!c.isRed()) {
color = null;
}
break;
case "white":
if (!c.isWhite()) {
color = null;
}
break;
case "black":
if (!c.isBlack()) {
color = null;
}
break;
case "blue":
if (!c.isBlue()) {
color = null;
}
break;
case "green":
if (!c.isGreen()) {
color = null;
}
break;
case "red":
if (!c.isRed()) {
color = null;
}
break;
case "white":
if (!c.isWhite()) {
color = null;
}
break;
}
}
if (color == null && artifact == null) { //nothing can make the attacker unblockable
@@ -1722,7 +1683,7 @@ public class AiAttackController {
return null; //should never get here
}
private void doLightmineFieldAttackLogic(final Queue<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
private void doLightmineFieldAttackLogic(final List<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
CardCollection attSorted = new CardCollection(attackersLeft);
CardCollection attUnsafe = new CardCollection();
CardLists.sortByToughnessDesc(attSorted);
@@ -1752,15 +1713,13 @@ public class AiAttackController {
attackersLeft.removeAll(attUnsafe);
}
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final List<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false;
if (defender instanceof Player player) {
revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card card) {
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
if (defender instanceof Player) {
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card) {
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
}
if (!revengeOfRavens) {

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
package forge.ai;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory.MemorySet;
import forge.card.CardType;
import forge.card.MagicColor;
import forge.game.Game;
@@ -33,10 +32,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
discarded = new CardCollection();
tapped = new CardCollection();
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
if (tappedForMana != null) {
tapped.addAll(tappedForMana);
}
}
@Override
@@ -46,14 +41,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostBehold cost) {
final String type = cost.getType();
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
return hand.isEmpty() ? null : PaymentDecision.card(getBestCreatureAI(hand));
}
@Override
public PaymentDecision visit(CostChooseColor cost) {
int c = cost.getAbilityAmount(ability);
@@ -64,7 +51,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostChooseCreatureType cost) {
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes());
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(),
Lists.newArrayList());
return PaymentDecision.type(choice);
}
@@ -117,13 +105,13 @@ public class AiCostDecision extends CostDecisionMakerBase {
Card chosen;
if (!discardMe.isEmpty()) {
chosen = Aggregates.random(discardMe);
discardMe = CardLists.filter(discardMe, CardPredicates.sharesNameWith(chosen).negate());
discardMe = CardLists.filter(discardMe, Predicates.not(CardPredicates.sharesNameWith(chosen)));
} else {
final Card worst = ComputerUtilCard.getWorstAI(hand);
chosen = worst != null ? worst : Aggregates.random(hand);
}
differentNames.add(chosen);
hand = CardLists.filter(hand, CardPredicates.sharesNameWith(chosen).negate());
hand = CardLists.filter(hand, Predicates.not(CardPredicates.sharesNameWith(chosen)));
c--;
}
return PaymentDecision.card(differentNames);
@@ -452,6 +440,21 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
if ("DontPayTapCostWithManaSources".equals(source.getSVar("AIPaymentPreference"))) {
CardCollectionView toExclude =
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"),
ability.getActivatingPlayer(), ability.getHostCard(), ability);
toExclude = CardLists.filter(toExclude, card -> {
for (final SpellAbility sa : card.getSpellAbilities()) {
if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
});
exclude.addAll(toExclude);
}
String totalP = "";
CardCollectionView totap;
if (isVehicle) {
@@ -648,7 +651,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
// TODO sort negatives to remove from best Cards first?
for (final Card crd : negatives) {
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(crd).entrySet()) {
if (ComputerUtil.isNegativeCounter(e.getKey(), crd) && crd.canRemoveCounters(e.getKey())) {
if (ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
int over = Math.min(e.getValue(), c - toRemove);
if (over > 0) {
toRemove += over;
@@ -759,7 +762,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
}
// if table is empty, then no counter was removed
// if table is empty, than no counter was removed
return table.isEmpty() ? null : PaymentDecision.counters(table);
}
@@ -835,12 +838,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostUnattach cost) {
final CardCollection cardToUnattach = cost.findCardToUnattach(source, player, ability);
if (cardToUnattach.isEmpty()) {
final Card cardToUnattach = cost.findCardToUnattach(source, player, ability);
if (cardToUnattach == null) {
// We really shouldn't be able to get here if there's nothing to unattach
return null;
}
return PaymentDecision.card(cardToUnattach.getFirst());
return PaymentDecision.card(cardToUnattach);
}
@Override

View File

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

View File

@@ -17,7 +17,16 @@
*/
package forge.ai;
import java.util.*;
import com.google.common.collect.*;
import forge.game.card.*;
import forge.game.cost.*;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi;
@@ -26,19 +35,23 @@ import forge.card.CardType;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.game.*;
import forge.game.CardTraitPredicates;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.GameEntity;
import forge.game.GameEntityCounterTable;
import forge.game.GameObject;
import forge.game.GameType;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
@@ -48,7 +61,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
@@ -56,13 +68,8 @@ import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.StreamUtil;
import forge.util.TextUtil;
import forge.util.collect.FCollection;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.function.Predicate;
/**
@@ -79,8 +86,6 @@ public class ComputerUtil {
}
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game, Runnable chooseTargets) {
final Card source = sa.getHostCard();
final Card host = sa.getHostCard();
final Zone hz = host.isCopiedSpell() ? null : host.getZone();
source.setSplitStateToPlayAbility(sa);
if (sa.isSpell() && !source.isCopiedSpell()) {
@@ -119,25 +124,29 @@ public class ComputerUtil {
// Spell Permanents inherit their cost from Mana Cost
final Cost cost = sa.getPayCosts();
// Remember the now-forgotten kicker cost? Why is this needed?
sa.getHostCard().setKickerMagnitude(source.getKickerMagnitude());
game.getStack().freezeStack(sa);
final CostPayment pay = new CostPayment(cost, sa);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().addAndUnfreeze(sa);
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty()) {
game.getAction().reveal(sa.getSplicedCards(), ai, true, "Computer reveals spliced cards from ");
// 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;
}
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);
} else {
final CostPayment pay = new CostPayment(cost, sa);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().addAndUnfreeze(sa);
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty()) {
game.getAction().reveal(sa.getSplicedCards(), ai, true, "Computer reveals spliced cards from ");
}
return true;
}
}
//Should not arrive here
System.out.println("AI failed to play " + sa.getHostCard());
return false;
}
@@ -157,6 +166,7 @@ public class ComputerUtil {
}
public static int counterSpellRestriction(final Player ai, final SpellAbility sa) {
// Move this to AF?
// Restriction Level is Based off a handful of factors
int restrict = 0;
@@ -214,8 +224,9 @@ public class ComputerUtil {
return restrict;
}
// this is used for AI's counterspells
public static final boolean playStack(SpellAbility sa, final Player ai, final Game game) {
sa.setActivatingPlayer(ai);
sa.setActivatingPlayer(ai, true);
if (!ComputerUtilCost.canPayCost(sa, ai, false))
return false;
@@ -229,9 +240,10 @@ public class ComputerUtil {
if (sa.isSpell() && !source.isCopiedSpell()) {
sa.setHostCard(game.getAction().moveToStack(source, sa));
sa = GameActionUtil.addExtraKeywordCost(sa);
}
sa = GameActionUtil.addExtraKeywordCost(sa);
final Cost cost = sa.getPayCosts();
final CostPayment pay = new CostPayment(cost, sa);
@@ -241,34 +253,94 @@ public class ComputerUtil {
return false;
}
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa, false);
game.getStack().add(sa);
return true;
} else {
if (pay.payComputerCosts(new AiCostDecision(ai, sa, false))) {
game.getStack().add(sa);
}
}
return false;
return true;
}
public static final void playSpellAbilityForFree(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
sa.setActivatingPlayer(ai, true);
final Card source = sa.getHostCard();
if (sa.isSpell() && !source.isCopiedSpell()) {
sa.setHostCard(game.getAction().moveToStack(source, sa));
}
game.getStack().add(sa);
}
public static final boolean playSpellAbilityWithoutPayingManaCost(final Player ai, final SpellAbility sa, final Game game) {
SpellAbility newSA = sa.copyWithNoManaCost();
newSA.setActivatingPlayer(ai, true);
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA, false) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) {
return false;
}
newSA = GameActionUtil.addExtraKeywordCost(newSA);
final Card source = newSA.getHostCard();
Zone fromZone = game.getZoneOf(source);
int zonePosition = 0;
if (fromZone != null) {
zonePosition = fromZone.getCards().indexOf(source);
}
if (newSA.isSpell() && !source.isCopiedSpell()) {
newSA.setHostCard(game.getAction().moveToStack(source, newSA));
if (newSA.getApi() == ApiType.Charm && !CharmEffect.makeChoices(newSA)) {
// 603.3c If no mode is chosen, the ability is removed from the stack.
return false;
}
}
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
// do this after card got added to stack
if (!newSA.checkRestrictions(ai)) {
GameActionUtil.rollbackAbility(newSA, fromZone, zonePosition, pay, source);
return false;
}
pay.payComputerCosts(new AiCostDecision(ai, newSA, false));
game.getStack().add(newSA);
return true;
}
public static final boolean playNoStack(final Player ai, SpellAbility sa, final Game game, final boolean effect) {
sa.setActivatingPlayer(ai);
sa.setActivatingPlayer(ai, true);
// TODO: We should really restrict what doesn't use the Stack
if (!ComputerUtilCost.canPayCost(sa, ai, effect)) {
return false;
}
final Card source = sa.getHostCard();
if (!effect && sa.isSpell() && !source.isCopiedSpell()) {
if (sa.isSpell() && !source.isCopiedSpell()) {
sa.setHostCard(game.getAction().moveToStack(source, sa));
sa = GameActionUtil.addExtraKeywordCost(sa);
}
sa = GameActionUtil.addExtraKeywordCost(sa);
final Cost cost = sa.getPayCosts();
final CostPayment pay = new CostPayment(cost, sa);
if (pay.payComputerCosts(new AiCostDecision(ai, sa, effect))) {
AbilityUtils.resolve(sa);
return true;
if (cost == null) {
ComputerUtilMana.payManaCost(ai, sa, effect);
} else {
final CostPayment pay = new CostPayment(cost, sa);
pay.payComputerCosts(new AiCostDecision(ai, sa, effect));
}
return false;
AbilityUtils.resolve(sa);
return true;
}
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
@@ -459,7 +531,7 @@ public class ComputerUtil {
// Discard lands
final CardCollection landsInHand = CardLists.getType(typeList, "Land");
if (!landsInHand.isEmpty()) {
final int numLandsInPlay = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final int numLandsInPlay = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
if (numLandsInPlay >= highestCMC
@@ -570,7 +642,7 @@ public class ComputerUtil {
// if the source has "Casualty", don't sacrifice cards that may have granted the effect
// TODO: is there a surefire way to determine which card added Casualty?
if (source.hasKeyword(Keyword.CASUALTY)) {
typeList = CardLists.filter(typeList, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
typeList = CardLists.filter(typeList, Predicates.not(CardPredicates.hasSVar("AIDontSacToCasualty")));
}
if (typeList.size() < amount) {
@@ -604,13 +676,14 @@ public class ComputerUtil {
CardLists.sortByCmcDesc(typeList);
Collections.reverse(typeList);
// TODO AI needs some improvements here
// Whats the best way to choose evidence to collect?
// Probably want to filter out cards that have graveyard abilities/castable from graveyard
// Ideally we remove as few cards as possible "Don't overspend"
final CardCollection exileList = new CardCollection();
while (amount > 0) {
while(amount > 0) {
Card c = typeList.remove(0);
amount -= c.getCMC();
@@ -699,7 +772,7 @@ public class ComputerUtil {
all.removeAll(exclude);
CardCollection typeList = CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.CAN_TAP);
typeList = CardLists.filter(typeList, Presets.CAN_TAP);
if (tap) {
typeList.remove(activate);
@@ -710,7 +783,6 @@ public class ComputerUtil {
}
CardLists.sortByPowerAsc(typeList);
// TODO prefer noncreatures without tap abilities
final CardCollection tapList = new CardCollection();
@@ -730,7 +802,7 @@ public class ComputerUtil {
all.removeAll(exclude);
CardCollection typeList = CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, sa.isCrew() ? CardPredicates.CAN_CREW : CardPredicates.CAN_TAP);
typeList = CardLists.filter(typeList, sa.isCrew() ? Presets.CAN_CREW : Presets.CAN_TAP);
if (tap) {
typeList.remove(activate);
@@ -752,7 +824,7 @@ public class ComputerUtil {
tapList.clear();
}
tapList.add(next);
totalPower = CardLists.getTotalPower(tapList, sa);
totalPower = CardLists.getTotalPower(tapList, true, sa.isCrew());
if (totalPower >= amount) {
break;
}
@@ -765,9 +837,10 @@ public class ComputerUtil {
}
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
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);
if (untap) {
typeList.remove(activate);
@@ -779,7 +852,12 @@ public class ComputerUtil {
CardLists.sortByPowerDesc(typeList);
return typeList.subList(0, amount);
final CardCollection untapList = new CardCollection();
for (int i = 0; i < amount; i++) {
untapList.add(typeList.get(i));
}
return untapList;
}
public static CardCollection chooseReturnType(final Player ai, final String type, final Card activate, final Card target, final int amount, SpellAbility sa) {
@@ -844,7 +922,7 @@ public class ComputerUtil {
}
} else if (isOptional && source.getActivatingPlayer().isOpponentOf(ai)) {
if ("Pillar Tombs of Aku".equals(host.getName())) {
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
if (!ai.canLoseLife() || ai.cantLose()) {
return sacrificed; // sacrifice none
}
} else {
@@ -864,7 +942,7 @@ public class ComputerUtil {
// Run non-mandatory trigger.
// These checks only work if the Executing SpellAbility is an Ability_Sub.
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) {
// AI would not run this trigger if given the chance
return sacrificed;
}
@@ -960,7 +1038,7 @@ public class ComputerUtil {
c = ComputerUtilCard.getWorstCreatureAI(remaining);
}
else if (CardLists.getNotType(remaining, "Land").isEmpty()) {
c = ComputerUtilCard.getWorstLand(CardLists.filter(remaining, CardPredicates.LANDS));
c = ComputerUtilCard.getWorstLand(CardLists.filter(remaining, CardPredicates.Presets.LANDS));
}
else {
c = ComputerUtilCard.getWorstPermanentAI(remaining, false, false, false, false);
@@ -996,7 +1074,7 @@ public class ComputerUtil {
if (!sa.isActivatedAbility() || sa.getApi() != ApiType.Regenerate) {
continue; // Not a Regenerate ability
}
sa.setActivatingPlayer(controller);
sa.setActivatingPlayer(controller, true);
if (!(sa.canPlay() && ComputerUtilCost.canPayCost(sa, controller, false))) {
continue; // Can't play ability
}
@@ -1100,11 +1178,6 @@ public class ComputerUtil {
}
}
// if AI has no speed, play start your engines on Main1
if (ai.noSpeed() && cardState.hasKeyword(Keyword.START_YOUR_ENGINES)) {
return true;
}
// cast Blitz in main 1 if the creature attacks
if (sa.isBlitz() && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, card)) {
return true;
@@ -1274,8 +1347,8 @@ public class ComputerUtil {
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
@@ -1414,7 +1487,9 @@ public class ComputerUtil {
}
}
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice sac) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
final String type = sac.getType();
if (type.equals("CARDNAME")) {
@@ -1459,14 +1534,15 @@ public class ComputerUtil {
// check for Continuous abilities that grant Haste
for (final Card c : all) {
for (StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("AddKeyword").contains("Haste")) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
if (c.isEquipment() && c.getEquipping() == null) {
return true;
}
final String affected = stAb.getParam("Affected");
final String affected = params.get("Affected");
if (affected.contains("Creature.YouCtrl")
|| affected.contains("Other+YouCtrl")) {
return true;
@@ -1519,10 +1595,11 @@ public class ComputerUtil {
for (final Card c : opp) {
for (StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("AddKeyword").contains("Haste")) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
final ArrayList<String> affected = Lists.newArrayList(stAb.getParam("Affected").split(","));
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(","));
if (affected.contains("Creature")) {
return true;
}
@@ -1556,7 +1633,7 @@ public class ComputerUtil {
for (final Card c : all) {
// check if card is at least available to be played
// further improvements might consider if AI has options to steal the spell by making it playable first
if (c.getZone() != null && c.getZone().getPlayer() != null && c.getZone().getPlayer() != defender && c.mayPlay(defender).isEmpty()) {
if (c.getZone().getPlayer() != null && c.getZone().getPlayer() != defender && c.mayPlay(defender).isEmpty()) {
continue;
}
for (final SpellAbility sa : c.getSpellAbilities()) {
@@ -1590,7 +1667,7 @@ public class ComputerUtil {
int damage = 0;
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
all.addAll(ai.getCardsActivatableInExternalZones(true));
all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.PERMANENTS.negate()));
all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(Presets.PERMANENTS)));
for (final Card c : all) {
if (c.getZone().getPlayer() != null && c.getZone().getPlayer() != ai && c.mayPlay(ai).isEmpty()) {
@@ -1600,7 +1677,7 @@ public class ComputerUtil {
if (sa.getApi() != ApiType.DealDamage) {
continue;
}
sa.setActivatingPlayer(ai);
sa.setActivatingPlayer(ai, true);
final String numDam = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), numDam, sa);
if (dmg <= damage) {
@@ -1680,7 +1757,7 @@ public class ComputerUtil {
sub = sub.getSubAbility();
}
if (sa == null || (sa != spell && sa != sub)) {
predictThreatenedObjects(ai, sa, spell).forEach(objects::add);
Iterables.addAll(objects, predictThreatenedObjects(ai, sa, spell));
}
if (top) {
break; // only evaluate top-stack
@@ -1778,7 +1855,9 @@ public class ComputerUtil {
noRegen = true;
}
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
@@ -1842,7 +1921,9 @@ public class ComputerUtil {
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
threatened.add(c);
}
} else if (o instanceof Player p) {
} else if (o instanceof Player) {
final Player p = (Player) o;
if (source.hasKeyword(Keyword.INFECT)) {
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
threatened.add(p);
@@ -1860,7 +1941,8 @@ public class ComputerUtil {
|| saviourApi == null)) {
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
final boolean canRemove = (c.getNetToughness() <= dmg)
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
if (!canRemove) {
@@ -1906,7 +1988,9 @@ public class ComputerUtil {
|| saviourApi == ApiType.Protection || saviourApi == null
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1955,7 +2039,8 @@ public class ComputerUtil {
&& topStack.hasParam("Destination")
&& topStack.getParam("Destination").equals("Exile")) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -1982,7 +2067,8 @@ public class ComputerUtil {
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2004,7 +2090,8 @@ public class ComputerUtil {
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
if (enableCurseAuraRemoval) {
for (final Object o : objects) {
if (o instanceof Card c) {
if (o instanceof Card) {
final Card c = (Card) o;
// give Shroud to targeted creatures
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
continue;
@@ -2020,7 +2107,7 @@ public class ComputerUtil {
}
}
predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility()).forEach(threatened::add);
Iterables.addAll(threatened, predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility()));
return threatened;
}
@@ -2134,7 +2221,7 @@ public class ComputerUtil {
}
CardCollectionView library = ai.getCardsIn(ZoneType.Library);
int landsInDeck = CardLists.count(library, CardPredicates.LANDS);
int landsInDeck = CardLists.count(library, CardPredicates.isType("Land"));
// no land deck, can't do anything better
if (landsInDeck == 0) {
@@ -2279,14 +2366,14 @@ public class ComputerUtil {
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.LANDS_PRODUCING_MANA);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
CardCollection thisLandOTB = CardLists.filter(cardsOTB, CardPredicates.nameEquals(c.getName()));
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.LANDS_PRODUCING_MANA);
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
// valuable mana-producing artifacts that may be equated to a land
List<String> manaArts = Arrays.asList("Mox Pearl", "Mox Sapphire", "Mox Jet", "Mox Ruby", "Mox Emerald");
// evaluate creatures available in deck
CardCollectionView allCreatures = CardLists.filter(allCards, CardPredicates.CREATURES, CardPredicates.isOwner(player));
CardCollectionView allCreatures = CardLists.filter(allCards, CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player));
int numCards = allCreatures.size();
if (landsOTB.size() < maxLandsToScryLandsToTop && landsInHand.isEmpty()) {
@@ -2315,7 +2402,7 @@ public class ComputerUtil {
}
}
} else if (c.isCreature()) {
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.CREATURES);
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES);
int avgCreatureValue = numCards != 0 ? ComputerUtilCard.evaluateCreatureList(allCreatures) / numCards : 0;
int maxControlledCMC = Aggregates.max(creaturesOTB, Card::getCMC);
@@ -2366,10 +2453,7 @@ public class ComputerUtil {
// not enough good choices, need to fill the rest
int minDiff = min - goodChoices.size();
if (minDiff > 0) {
List<Card> choices = validCards.stream()
.filter(Predicate.not(goodChoices::contains))
.collect(StreamUtil.random(minDiff));
goodChoices.addAll(choices);
goodChoices.addAll(Aggregates.random(CardLists.filter(validCards, Predicates.not(Predicates.in(goodChoices))), minDiff));
return goodChoices;
}
@@ -2389,9 +2473,12 @@ public class ComputerUtil {
return getCardsToDiscardFromOpponent(aiChooser, p, sa, validCards, min, max);
}
public static String chooseSomeType(Player ai, String kindOfType, SpellAbility sa, Collection<String> validTypes) {
public static String chooseSomeType(Player ai, String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes) {
final String logic = sa.getParam("AILogic");
if (invalidTypes == null) {
invalidTypes = ImmutableList.of();
}
if (validTypes == null) {
validTypes = ImmutableList.of();
}
@@ -2404,12 +2491,14 @@ public class ComputerUtil {
// otherwise, lib search for most common type left then, reveal chosenType to Human
if (game.getPhaseHandler().is(PhaseType.UNTAP) && logic == null) { // Storage Matrix
double amount = 0;
for (String type : validTypes) {
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType(type), CardPredicates.TAPPED);
double i = type.equals("Creature") ? list.size() * 1.5 : list.size();
if (i > amount) {
amount = i;
chosen = type;
for (String type : CardType.getAllCardTypes()) {
if (!invalidTypes.contains(type)) {
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType(type), Presets.TAPPED);
double i = type.equals("Creature") ? list.size() * 1.5 : list.size();
if (i > amount) {
amount = i;
chosen = type;
}
}
}
} else if ("ProtectionFromType".equals(logic)) {
@@ -2424,11 +2513,12 @@ public class ComputerUtil {
if (StringUtils.isEmpty(chosen)) {
chosen = "Creature"; // if in doubt, choose Creature, I guess
}
} else {
}
else {
// Are we picking a type to reduce costs for that type?
boolean reducingCost = false;
for (StaticAbility s : sa.getHostCard().getStaticAbilities()) {
if (s.checkMode(StaticAbilityMode.ReduceCost) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
if ("ReduceCost".equals(s.getParam("Mode")) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
reducingCost = true;
break;
}
@@ -2436,6 +2526,7 @@ public class ComputerUtil {
if (reducingCost) {
List<String> valid = Lists.newArrayList(validTypes);
valid.removeAll(invalidTypes);
valid.remove("Land"); // Lands don't have costs to reduce
chosen = ComputerUtilCard.getMostProminentCardType(ai.getAllCards(), valid);
}
@@ -2445,42 +2536,50 @@ public class ComputerUtil {
}
} else if (kindOfType.equals("Creature")) {
if (logic != null) {
List <String> valid = Lists.newArrayList(CardType.getAllCreatureTypes());
valid.removeAll(invalidTypes);
if (logic.equals("MostProminentOnBattlefield")) {
chosen = ComputerUtilCard.getMostProminentType(game.getCardsIn(ZoneType.Battlefield), validTypes);
} else if (logic.equals("MostProminentComputerControls")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Battlefield), validTypes);
} else if (logic.equals("MostProminentComputerControlsOrOwns")) {
chosen = ComputerUtilCard.getMostProminentType(game.getCardsIn(ZoneType.Battlefield), valid);
}
else if (logic.equals("MostProminentComputerControls")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Battlefield), valid);
}
else if (logic.equals("MostProminentComputerControlsOrOwns")) {
CardCollectionView list = ai.getCardsIn(Arrays.asList(ZoneType.Battlefield, ZoneType.Hand));
if (list.isEmpty()) {
list = ai.getCardsIn(Arrays.asList(ZoneType.Library));
}
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
} else if (logic.equals("MostProminentOppControls")) {
chosen = ComputerUtilCard.getMostProminentType(list, valid);
}
else if (logic.equals("MostProminentOppControls")) {
CardCollection list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
if (!CardType.isACreatureType(chosen)) {
chosen = ComputerUtilCard.getMostProminentType(list, valid);
if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
list = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
chosen = ComputerUtilCard.getMostProminentType(list, valid);
}
} else if (logic.startsWith("MostProminentInComputerDeck")) {
}
else if (logic.startsWith("MostProminentInComputerDeck")) {
boolean includeTokens = !logic.endsWith("NonToken");
chosen = ComputerUtilCard.getMostProminentType(ai.getAllCards(), validTypes, includeTokens);
} else if (logic.equals("MostProminentInComputerGraveyard")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Graveyard), validTypes);
chosen = ComputerUtilCard.getMostProminentType(ai.getAllCards(), valid, includeTokens);
}
else if (logic.equals("MostProminentInComputerGraveyard")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Graveyard), valid);
}
}
if (!CardType.isACreatureType(chosen)) {
chosen = validTypes.size() == 1 ? (String) validTypes.toArray()[0] :
ComputerUtilCard.getMostProminentType(ai.getAllCards(), validTypes, false);
//chosen = "Sliver";
if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
chosen = "Sliver";
}
} else if (kindOfType.equals("Basic Land")) {
if (logic != null) {
if (logic.equals("MostProminentOppControls")) {
CardCollection list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
chosen = ComputerUtilCard.getMostProminentType(list, validTypes);
List<String> valid = Lists.newArrayList(CardType.getBasicTypes());
valid.removeAll(invalidTypes);
chosen = ComputerUtilCard.getMostProminentType(list, valid);
} else if (logic.equals("MostNeededType")) {
// Choose a type that is in the deck, but not in hand or on the battlefield
final List<String> basics = new ArrayList<>(CardType.Constant.BASIC_TYPES);
@@ -2488,13 +2587,13 @@ public class ComputerUtil {
CardCollectionView possibleCards = ai.getAllCards();
for (String b : basics) {
if (!presentCards.anyMatch(CardPredicates.isType(b)) && possibleCards.anyMatch(CardPredicates.isType(b))) {
if (!Iterables.any(presentCards, CardPredicates.isType(b)) && Iterables.any(possibleCards, CardPredicates.isType(b))) {
chosen = b;
}
}
if (chosen.isEmpty()) {
for (String b : basics) {
if (possibleCards.anyMatch(CardPredicates.isType(b))) {
if (Iterables.any(possibleCards, CardPredicates.isType(b))) {
chosen = b;
}
}
@@ -2503,7 +2602,7 @@ public class ComputerUtil {
else if (logic.equals("ChosenLandwalk")) {
for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
for (String t : c.getType()) {
if (CardType.isABasicLandType(t)) {
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
chosen = t;
break;
}
@@ -2512,7 +2611,7 @@ public class ComputerUtil {
}
}
if (!CardType.isABasicLandType(chosen) || !validTypes.contains(chosen)) {
if (!CardType.isABasicLandType(chosen) || invalidTypes.contains(chosen)) {
chosen = "Island";
}
}
@@ -2521,7 +2620,7 @@ public class ComputerUtil {
if (logic.equals("ChosenLandwalk")) {
for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
for (String t : c.getType().getLandTypes()) {
if (validTypes.contains(t)) {
if (!invalidTypes.contains(t)) {
chosen = t;
break;
}
@@ -2591,7 +2690,7 @@ public class ComputerUtil {
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
// try to mill opponent with Quill vote
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
if (opponent && !controller.cantLose()) {
int numQuill = votes.get("Quill").size();
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
@@ -2705,7 +2804,8 @@ public class ComputerUtil {
}
// has cards with SacMe or Token
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
if (CardLists.count(aiCreatures,
Predicates.or(CardPredicates.hasSVar("SacMe"), CardPredicates.Presets.TOKEN)) >= numDeath) {
return "Death";
}
@@ -2788,12 +2888,16 @@ public class ComputerUtil {
if (!trigger.requirementsCheck(game)) {
continue;
}
if (!trigger.matchesValidParam("ValidCard", card)) {
continue;
if (trigger.hasParam("ValidCard")) {
if (!card.isValid(trigger.getParam("ValidCard").split(","), source.getController(), source, sa)) {
continue;
}
}
if (!trigger.matchesValidParam("ValidActivatingPlayer", player)) {
continue;
if (trigger.hasParam("ValidActivatingPlayer")) {
if (!player.isValid(trigger.getParam("ValidActivatingPlayer"), source.getController(), source, sa)) {
continue;
}
}
// fall back for OverridingAbility
@@ -2851,8 +2955,10 @@ public class ComputerUtil {
&& AbilityUtils.getDefinedCards(permanent, source.getSVar(trigger.getParam("CheckOnTriggeredCard").split(" ")[0]), null).isEmpty()) {
continue;
}
if (!trigger.matchesValidParam("ValidCard", permanent)) {
continue;
if (trigger.hasParam("ValidCard")) {
if (!permanent.isValid(trigger.getParam("ValidCard"), source.getController(), source, null)) {
continue;
}
}
// fall back for OverridingAbility
SpellAbility trigSa = trigger.ensureAbility();
@@ -2890,7 +2996,7 @@ public class ComputerUtil {
// Iceberg does use Ice as Storage
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
// some lands does use Depletion as Storage Counter
|| (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step."))
// treat Time Counters on suspended Cards as Bad,
// and also on Chronozoa
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
@@ -2982,11 +3088,11 @@ public class ComputerUtil {
repParams,
ReplacementLayer.Other);
if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
return false;
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
return false;
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
return false;
}
return true;
@@ -3011,13 +3117,13 @@ public class ComputerUtil {
ReplacementLayer.Other
);
if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
// no life gain is not negative
return false;
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
// lose life is only negative is the player can lose life
return player.canLoseLife();
} else if (list.stream().anyMatch(CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
// if it would draw more cards than player has, then its negative
return player.getCardsIn(ZoneType.Library).size() <= n;
}
@@ -3048,7 +3154,7 @@ public class ComputerUtil {
}
SpellAbility abTest = withoutPayingManaCost ? ab.copyWithNoManaCost() : ab.copy();
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
abTest.setActivatingPlayer(ai);
abTest.setActivatingPlayer(ai, true);
abTest.getRestrictions().setZone(c.getZone().getZoneType());
if (AiPlayDecision.WillPlay == aic.canPlaySa(abTest) && ComputerUtilCost.canPayCost(abTest, ai, false)) {
targets.add(c);
@@ -3148,7 +3254,7 @@ public class ComputerUtil {
// performance shortcut
// TODO if checking upcoming turn it should be a permanent effect
if (ai.cantLoseForZeroOrLessLife()) {
if (ai.cantLose()) {
return remainingLife;
}
@@ -3207,7 +3313,8 @@ public class ComputerUtil {
repParams.put(AbilityKey.EffectOnly, true);
repParams.put(AbilityKey.CounterTable, table);
repParams.put(AbilityKey.CounterMap, table.column(c));
return c.getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Moved, repParams);
List<ReplacementEffect> list = c.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.CantHappen);
return !list.isEmpty();
}
public static boolean shouldSacrificeThreatenedCard(Player ai, Card c, SpellAbility sa) {

View File

@@ -14,6 +14,7 @@ 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.Presets;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostPutCounter;
@@ -21,7 +22,6 @@ import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
@@ -32,14 +32,20 @@ public class ComputerUtilAbility {
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
return null;
}
CardCollection landList = new CardCollection(player.getCardsIn(ZoneType.Hand));
final CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand));
hand.addAll(player.getCardsIn(ZoneType.Exile));
CardCollection landList = CardLists.filter(hand, Presets.LANDS);
//filter out cards that can't be played
landList = CardLists.filter(landList, c -> {
if (!c.hasPlayableLandFace()) {
return false;
if (!c.getSVar("NeedsToPlay").isEmpty()) {
final String needsToPlay = c.getSVar("NeedsToPlay");
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), needsToPlay, c.getController(), c, null);
if (list.isEmpty()) {
return false;
}
}
return player.canPlayLand(c, false, c.getFirstSpellAbility());
return player.canPlayLand(c);
});
final CardCollection landsNotInHand = new CardCollection(player.getCardsIn(ZoneType.Graveyard));
@@ -48,7 +54,7 @@ public class ComputerUtilAbility {
landsNotInHand.add(player.getCardsIn(ZoneType.Library).get(0));
}
for (final Card crd : landsNotInHand) {
if (!(crd.hasPlayableLandFace() || (crd.isFaceDown() && crd.getState(CardStateName.Original).getType().isLand()))) {
if (!(crd.isLand() || (crd.isFaceDown() && crd.getState(CardStateName.Original).getType().isLand()))) {
continue;
}
if (!crd.mayPlay(player).isEmpty()) {
@@ -90,7 +96,7 @@ public class ComputerUtilAbility {
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
for (SpellAbility sa : originList) {
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
}
@@ -117,16 +123,12 @@ public class ComputerUtilAbility {
final List<SpellAbility> result = Lists.newArrayList();
for (SpellAbility sa : newAbilities) {
sa.setActivatingPlayer(player);
sa.setActivatingPlayer(player, true);
// Optional cost selection through the AI controller
boolean choseOptCost = false;
List<OptionalCostValue> list = GameActionUtil.getOptionalCostValues(sa);
if (!list.isEmpty()) {
// still add base spell in case of Promise Gift
if (list.stream().anyMatch(ocv -> ocv.getType().equals(OptionalCost.PromiseGift))) {
result.add(sa);
}
list = player.getController().chooseOptionalCosts(sa, list);
if (!list.isEmpty()) {
choseOptCost = true;
@@ -358,7 +360,7 @@ public class ComputerUtilAbility {
}
// 1. increase chance of using Surge effects
// 2. non-surged versions are usually inefficient
if (source.hasKeyword(Keyword.SURGE) && !sa.isSurged()) {
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
p -= 9;
}
// move snap-casted spells to front
@@ -391,10 +393,8 @@ public class ComputerUtilAbility {
}
if (ApiType.DestroyAll == sa.getApi()) {
// check boardwipe earlier
p += 4;
} else if (ApiType.Mana == sa.getApi()) {
// keep mana abilities for paying
p -= 9;
}
@@ -405,7 +405,7 @@ public class ComputerUtilAbility {
return p;
}
}
};
public static List<SpellAbility> sortCreatureSpells(final List<SpellAbility> all) {
// try to smoothen power creep by making CMC less of a factor

View File

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

View File

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

View File

@@ -1,22 +1,20 @@
package forge.ai;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi;
import forge.ai.ability.TokenAi;
import forge.card.ColorSet;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
@@ -25,9 +23,14 @@ import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType;
import forge.util.IterableUtil;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Set;
public class ComputerUtilCost {
@@ -50,7 +53,8 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPutCounter addCounter) {
if (part instanceof CostPutCounter) {
final CostPutCounter addCounter = (CostPutCounter) part;
final CounterType type = addCounter.getCounter();
if (type.is(CounterEnumType.M1M1)) {
@@ -76,7 +80,9 @@ public class ComputerUtilCost {
}
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter remCounter) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
if (type.is(CounterEnumType.P1P1)) {
@@ -85,7 +91,7 @@ public class ComputerUtilCost {
continue;
}
// even if it can be paid, removing zero counters should not be done.
// even if it can be paid, removing zero counters should not be done.
if (part.payCostFromSource() && source.getCounters(type) <= 0) {
return false;
}
@@ -103,7 +109,9 @@ public class ComputerUtilCost {
&& !source.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (part instanceof CostRemoveAnyCounter remCounter) {
} else if (part instanceof CostRemoveAnyCounter) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision pay = decision.visit(remCounter);
return pay != null;
}
@@ -128,29 +136,25 @@ public class ComputerUtilCost {
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard disc) {
if (part instanceof CostDiscard) {
final CostDiscard disc = (CostDiscard) part;
final String type = disc.getType();
final CardCollection typeList;
int num;
if (type.equals("Hand")) {
typeList = hand;
num = hand.size();
} else {
if (type.equals("CARDNAME")) {
if (source.getAbilityText().contains("Bloodrush")) {
continue;
} else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
// Better do something than just discard stuff
return true;
}
}
typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) {
if (type.equals("CARDNAME")) {
if (source.getAbilityText().contains("Bloodrush")) {
continue;
} else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
// Better do something than just discard stuff
return true;
}
num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
}
final CardCollection typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) {
continue;
}
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
if (pref == null) {
@@ -180,7 +184,8 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDamage pay) {
if (part instanceof CostDamage) {
final CostDamage pay = (CostDamage) part;
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
if (ai.getLife() - realDamage < remainingLife
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
@@ -212,8 +217,13 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife payLife) {
int amount = payLife.getAbilityAmount(sourceAbility);
if (part instanceof CostPayLife) {
final CostPayLife payLife = (CostPayLife) part;
Integer amount = payLife.convertAmount();
if (amount == null) {
amount = AbilityUtils.calculateAmount(source, payLife.getAmount(), sourceAbility);
}
// check if there's override for the remainingLife threshold
if (sourceAbility != null && sourceAbility.hasParam("AILifeThreshold")) {
@@ -286,7 +296,8 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice sac) {
if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
if (sac.payCostFromSource() && source.isCreature()) {
@@ -335,11 +346,12 @@ public class ComputerUtilCost {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice sac) {
if (part instanceof CostSacrifice) {
if (suppressRecursiveSacCostCheck) {
return false;
}
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
String type = sac.getType();
@@ -438,7 +450,7 @@ public class ComputerUtilCost {
* the source
* @return true, if successful
*/
public static boolean checkTapTypeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sa, final Collection<Card> alreadyTapped) {
public static boolean checkTapTypeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sa, final CardCollection alreadyTapped) {
if (cost == null) {
return true;
}
@@ -475,8 +487,8 @@ public class ComputerUtilCost {
c = AbilityUtils.calculateAmount(source, part.getAmount(), sa);
}
CardCollection exclude = new CardCollection();
if (alreadyTapped != null) {
exclude.addAll(alreadyTapped);
if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST) != null) {
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST));
}
// trying to produce mana that includes tapping source that will already be tapped
if (exclude.contains(source) && cost.hasTapCost()) {
@@ -488,12 +500,12 @@ public class ComputerUtilCost {
}
CardCollection tapChoices = ComputerUtil.chooseTapType(ai, type, source, cost.hasTapCost(), c, exclude, sa);
if (tapChoices != null) {
if (alreadyTapped != null) {
alreadyTapped.addAll(tapChoices);
// if manasource gets tapped to produce it also can't help paying another
if (cost.hasTapCost()) {
alreadyTapped.add(source);
}
for (Card choice : tapChoices) {
AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_TAP_COST);
}
// if manasource gets tapped to produce it also can't help paying another
if (cost.hasTapCost()) {
AiCardMemory.rememberCard(ai, source, MemorySet.PAYS_TAP_COST);
}
return true;
}
@@ -515,91 +527,58 @@ public class ComputerUtilCost {
* @return a boolean.
*/
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
return canPayCost(sa.getPayCosts(), sa, player, effect);
}
public static boolean canPayCost(final Cost cost, final SpellAbility sa, final Player player, final boolean effect) {
if (sa.getActivatingPlayer() == null) {
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
sa.setActivatingPlayer(player, true); // complaints on NPE had came before this line was added.
}
boolean cannotBeCountered = false;
// Check for stuff like Nether Void
int extraManaNeeded = 0;
if (!effect) {
if (sa instanceof Spell) {
cannotBeCountered = !sa.isCounterableBy(null);
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) {
if (cannotBeCountered && c.getName().equals("Nether Void")) {
continue;
}
String[] parts = TextUtil.split(snem, ' ');
boolean meetsRestriction = parts.length == 1 || player.isValid(parts[1], c.getController(), c, sa);
if(!meetsRestriction)
continue;
if (StringUtils.isNumeric(parts[0])) {
extraManaNeeded += Integer.parseInt(parts[0]);
} else {
System.out.println("wrong SpellsNeedExtraMana SVar format on " + c);
}
}
}
for (Card c : player.getCardsIn(ZoneType.Command)) {
if (cannotBeCountered) {
if (sa instanceof Spell) {
cannotBeCountered = !sa.isCounterableBy(null);
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) {
if (cannotBeCountered && c.getName().equals("Nether Void")) {
continue;
}
final String snem = c.getSVar("SpellsNeedExtraManaEffect");
if (!StringUtils.isBlank(snem)) {
if (StringUtils.isNumeric(snem)) {
extraManaNeeded += Integer.parseInt(snem);
} else {
System.out.println("wrong SpellsNeedExtraManaEffect SVar format on " + c);
}
String[] parts = TextUtil.split(snem, ' ');
boolean meetsRestriction = parts.length == 1 || player.isValid(parts[1], c.getController(), c, sa);
if(!meetsRestriction)
continue;
if (StringUtils.isNumeric(parts[0])) {
extraManaNeeded += Integer.parseInt(parts[0]);
} else {
System.out.println("wrong SpellsNeedExtraMana SVar format on " + c);
}
}
}
// Try not to lose Planeswalker if not threatened
if (sa.isPwAbility()) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
// refuse to pay if opponent has no creature threats or
// 50% chance otherwise
if (player.getOpponents().getCreaturesInPlay().isEmpty()
|| MyRandom.getRandom().nextFloat() < .5f) {
return false;
}
}
for (Card c : player.getCardsIn(ZoneType.Command)) {
if (cannotBeCountered) {
continue;
}
final String snem = c.getSVar("SpellsNeedExtraManaEffect");
if (!StringUtils.isBlank(snem)) {
if (StringUtils.isNumeric(snem)) {
extraManaNeeded += Integer.parseInt(snem);
} else {
System.out.println("wrong SpellsNeedExtraManaEffect SVar format on " + c);
}
}
}
}
// Ward - will be accounted for when rechecking a targeted ability
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
extraManaNeeded += wardCost.getTotalMana().getCMC();
}
}
}
}
}
// Bail early on Casualty in case there are no cards that would make sense to pay with
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
sa.getActivatingPlayer(), sa.getHostCard(), sa);
valid = CardLists.filter(valid, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
if (valid.isEmpty()) {
// Try not to lose Planeswalker if not threatened
if (sa.isPwAbility()) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
// refuse to pay if opponent has no creature threats or
// 50% chance otherwise
if (player.getOpponents().getCreaturesInPlay().isEmpty()
|| MyRandom.getRandom().nextFloat() < .5f) {
return false;
}
}
@@ -607,15 +586,254 @@ public class ComputerUtilCost {
}
}
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
// Ward - will be accounted for when rechecking a targeted ability
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
extraManaNeeded += wardCost.getTotalMana().getCMC();
}
}
}
}
}
// TODO: Alternate costs which involve both paying mana and tapping a card, e.g. Zahid, Djinn of the Lamp
// Current AI decides on each part separately, thus making it possible for the AI to cheat by
// tapping a mana source for mana and for the tap cost at the same time. Until this is improved, AI
// will not consider mana sources valid for paying the tap cost to avoid this exact situation.
if ("DontPayTapCostWithManaSources".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostTapType) {
CardCollectionView nonManaSources =
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
sa.getActivatingPlayer(), sa.getHostCard(), sa);
nonManaSources = CardLists.filter(nonManaSources, card -> {
boolean hasManaSa = false;
for (final SpellAbility sa1 : card.getSpellAbilities()) {
if (sa1.isManaAbility() && sa1.getPayCosts().hasTapCost()) {
hasManaSa = true;
break;
}
}
return !hasManaSa;
});
if (nonManaSources.size() < part.convertAmount()) {
return false;
}
}
}
}
// Bail early on Casualty in case there are no cards that would make sense to pay with
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostSacrifice) {
CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
sa.getActivatingPlayer(), sa.getHostCard(), sa);
valid = CardLists.filter(valid, Predicates.not(CardPredicates.hasSVar("AIDontSacToCasualty")));
if (valid.isEmpty()) {
return false;
}
}
}
}
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect);
}
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payNever = "Never".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
if (payForOwnOnly && !isMine) { return false; }
if (payOwner) {
final String defined = aiLogic.substring(7);
final Player player = AbilityUtils.getDefinedPlayers(source, defined, sa).get(0);
if (!payer.equals(player)) {
return false;
}
} else if ("OnlyDontControl".equals(aiLogic)) {
if (source == null || payer.equals(source.getController())) {
return false;
}
} else if ("Paralyze".equals(aiLogic)) {
final Card c = source.getEnchantingCard();
if (c == null || c.isUntapped()) {
return false;
}
} else if ("RiskFactor".equals(aiLogic)) {
final Player activator = sa.getActivatingPlayer();
if (!activator.canDraw()) {
return false;
}
} else if ("MorePowerful".equals(aiLogic)) {
final int sourceCreatures = sa.getActivatingPlayer().getCreaturesInPlay().size();
final int payerCreatures = payer.getCreaturesInPlay().size();
if (payerCreatures > sourceCreatures + 1) {
return false;
}
} else if (aiLogic != null && aiLogic.startsWith("LifeLE")) {
// if payer can't lose life its no need to pay unless
if (!payer.canLoseLife())
return false;
else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substring(6), sa)) {
return true;
}
} else if ("WillAttack".equals(aiLogic)) {
AiAttackController aiAtk = new AiAttackController(payer);
Combat combat = new Combat(payer);
aiAtk.declareAttackers(combat);
if (combat.getAttackers().isEmpty()) {
return false;
}
} else if ("nonToken".equals(aiLogic) && !AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).isEmpty()
&& AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) {
return false;
} else if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) {
return false;
} else if (aiLogic != null && aiLogic.startsWith("Fabricate")) {
final int n = Integer.parseInt(aiLogic.substring("Fabricate".length()));
// if host would leave the play or if host is useless, create tokens
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(payer, source)) {
return false;
}
// need a copy for one with extra +1/+1 counter boost,
// without causing triggers to run
final Card copy = CardCopyService.getLKICopy(source);
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n);
copy.setZone(source.getZone());
// if host would put into the battlefield attacking
Combat combat = source.getGame().getCombat();
if (combat != null && combat.isAttacking(source)) {
final Player defender = combat.getDefenderPlayerByAttacker(source);
if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true)) {
return true;
}
return false;
}
// if the host has haste and can attack
if (CombatUtil.canAttack(copy)) {
for (final Player opp : payer.getOpponents()) {
if (CombatUtil.canAttack(copy, opp) &&
opp.canLoseLife() &&
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
return true;
}
}
// TODO check for trigger to turn token ETB into +1/+1 counter for host
// TODO check for trigger to turn token ETB into damage or life loss for opponent
// in this cases Token might be prefered even if they would not survive
final Card tokenCard = TokenAi.spawnToken(payer, sa);
// Token would not survive
if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) {
return true;
}
// Special Card logic, this one try to median its power with the number of artifacts
if ("Marionette Master".equals(source.getName())) {
CardCollection list = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS);
return list.size() >= copy.getNetPower();
} else if ("Cultivator of Blades".equals(source.getName())) {
// Cultivator does try to median with number of Creatures
CardCollection list = payer.getCreaturesInPlay();
return list.size() >= copy.getNetPower();
}
// evaluate Creature with +1/+1
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
final CardCollection tokenList = new CardCollection(source);
for (int i = 0; i < n; ++i) {
tokenList.add(TokenAi.spawnToken(payer, sa));
}
// evaluate Host with Tokens
int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList);
return evalToken < evalCounter;
} else if ("Riot".equals(aiLogic)) {
return !SpecialAiLogic.preferHasteForRiot(sa, payer);
}
// Check for shocklands and similar ETB replacement effects
if (sa.hasParam("ETB") && sa.getApi().equals(ApiType.Tap)) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
final CostPayLife lifeCost = (CostPayLife) part;
Integer amount = lifeCost.convertAmount();
if (payer.getLife() > (amount + 1) && payer.canPayLife(amount, true, sa)) {
final int landsize = payer.getLandsInPlay().size() + 1;
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
// Check if the AI has enough lands to play the card
if (landsize != c.getCMC()) {
continue;
}
// Check if the AI intends to play the card and if it can pay for it with the mana it has
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
boolean canPay = c.getManaCost().canBePaidWithAvailable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
if (canPay && willPlay) {
return true;
}
}
}
return false;
}
}
}
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
return false;
}
// ward or human misplay
if (ApiType.Counter.equals(sa.getApi())) {
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
for (SpellAbility toBeCountered : spells) {
if (!toBeCountered.isCounterableBy(sa)) {
return false;
}
// no reason to pay if we don't plan to confirm
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered.getApi()).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
return false;
}
// TODO check hasFizzled
}
}
// AI was crashing because the blank ability used to pay costs
// Didn't have any of the data on the original SA to pay dependant costs
return checkLifeCost(payer, cost, source, 4, sa)
&& checkDamageCost(payer, cost, source, 4, sa)
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source, sa))
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
}
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
}
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) {
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.UNTAPPED);
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED);
Set<String> colorsAvailable = Sets.newHashSet();
if (additionalLands != null) {
@@ -705,8 +923,8 @@ public class ComputerUtilCost {
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
if (source.usesTargeting()) {
final CardCollectionView targets = source.getTargets().getTargetCards();
choices = IterableUtil.filter(choices, Predicate.not(CardPredicates.isController(ai).and(targets::contains)));
final CardCollection targets = new CardCollection(source.getTargets().getTargetCards());
choices = Iterables.filter(choices, Predicates.not(Predicates.and(CardPredicates.isController(ai), Predicates.in(targets))));
}
return new CardCollection(choices);
}

View File

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

View File

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

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