Compare commits

..

18 Commits

Author SHA1 Message Date
Hans Mackowiak
09167a8ed5 fix checkstyle 2019-12-23 11:55:20 +01:00
Agetian
dbf7fc2ea3 - Some extra code in AttachAi to replace the previously removed code. 2019-11-27 22:34:08 +03:00
Agetian
c40d5cd4c5 - Basic CantUntapTurnAi logic. 2019-11-26 18:28:18 +03:00
Hanmac
244e7f1e86 remove This card doesn't untap during your next untap step. 2019-11-26 07:34:05 +01:00
Hanmac
ffff4a0491 cards: update cards with can't untap controller next untap phase 2019-11-25 07:55:24 +01:00
Hanmac
e27a6b8397 cards: update lands that doesn't untap doing YOUR untap step 2019-11-23 17:57:22 +01:00
Hanmac
5d639e7f1a Untap: cant untap turns Effect 2019-11-04 07:48:44 +01:00
Hanmac
7ff7fa67d1 cards: some cards that affects the player 2019-10-22 07:26:57 +02:00
Hanmac
813a91b76c CantUntapPlayer: need extra for Sleep because it needs to remember the player 2019-10-22 07:25:20 +02:00
Hanmac
05d7e17fbd cards: add ForgetOnMoved for the ones that does target and remember 2019-10-20 22:18:22 +02:00
Hanmac
ce93d341a9 Fix blinding beam 2019-10-20 14:42:06 +02:00
Hanmac
2134361010 cards: remove keyword from game and ai 2019-10-19 21:50:31 +02:00
Hanmac
86d123cc44 cards: remove keyword 'CARDNAME doesn't untap during your untap step.' 2019-10-19 21:46:28 +02:00
Hanmac
39a0789d36 cards: static abilities with AddHiddenKeyword 'CARDNAME doesn't untap during your untap step.' 2019-10-19 21:10:04 +02:00
Hanmac
e891f02044 cards: update some cards that dies give 'CARDNAME doesn't untap during your untap step' as ability 2019-10-19 20:26:06 +02:00
Hanmac
b5d19a65ab cards: It doesn't untap during its controller's untap step for as long as CARDNAME rem 2019-10-19 16:42:01 +02:00
Hanmac
7c61c38053 Cards: no 'CARDNAME doesn't untap during your untap step' on Pump or Animate 2019-10-19 10:24:43 +02:00
Hanmac
dd8a92f678 Untap: remove 'This card doesn't untap.' 2019-10-18 07:14:10 +02:00
29333 changed files with 161221 additions and 519765 deletions

View File

@@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,29 +0,0 @@
name: Test build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '8', '11' ]
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
cache: 'maven'
- 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 tests in virtual framebuffer
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
mvn -U -B clean -P windows-linux test

294
.gitignore vendored
View File

@@ -1,80 +1,238 @@
# Ignore IDEA config files
*.idea
*.iml
*.tmp
.metadata
.recommenders
# Ignore Eclipse config files
.settings
.classpath
.project
# Ignore VS Code config files
.vscode/settings.json
.vscode/launch.json
.factorypath
# Ignore NetBeans config files
nbactions.xml
# Ignore binaries, temp files and test output, everywhere
target
test-output
bin
gen
*.log
# Ignore macOS Spotlight rubbish
.DS_Store
# TODO: specify what these ignores are for (releasing?)
forge.profile.properties
/jgv.txt
pom.xml.next
pom.xml.releaseBackup
pom.xml.tag
release.properties
# Ignore mobile-related resources
forge-gui-android/res/*/*
!forge-gui-android/res/*/ic_launcher.png
!forge-gui-android/res/*/ic_launcher*.png
!forge-gui-android/res/layout/main.xml
/*.idea
/*.iml
/*.tmp
/.metadata
/.recommenders
forge-ai/forge-ai.iml
forge-ai/target
forge-core/forge-core.iml
forge-core/target
forge-game/*.iml
forge-game/target
forge-gui-android/*.iml
forge-gui-android/*.keystore
forge-gui-android/**/Thumbs.db
forge-gui-mobile-dev/**/Thumbs.db
forge-gui-android/assets/fallback_skin/Thumbs.db
forge-gui-android/bin
forge-gui-android/gen
forge-gui-android/res/Thumbs.db
forge-gui-android/res/bin
forge-gui-android/res/drawable-hdpi/Thumbs.db
forge-gui-android/res/drawable-hdpi/bin
forge-gui-android/res/drawable-hdpi/gen
forge-gui-android/res/drawable-hdpi/target
forge-gui-android/res/drawable-ldpi/Thumbs.db
forge-gui-android/res/drawable-ldpi/bin
forge-gui-android/res/drawable-ldpi/gen
forge-gui-android/res/drawable-ldpi/target
forge-gui-android/res/drawable-mdpi/Thumbs.db
forge-gui-android/res/drawable-mdpi/bin
forge-gui-android/res/drawable-mdpi/gen
forge-gui-android/res/drawable-mdpi/target
forge-gui-android/res/drawable-xhdpi/Thumbs.db
forge-gui-android/res/drawable-xhdpi/bin
forge-gui-android/res/drawable-xhdpi/gen
forge-gui-android/res/drawable-xhdpi/target
forge-gui-android/res/drawable-xxhdpi/Thumbs.db
forge-gui-android/res/drawable-xxhdpi/bin
forge-gui-android/res/drawable-xxhdpi/gen
forge-gui-android/res/drawable-xxhdpi/target
forge-gui-android/res/gen
forge-gui-android/res/layout/Thumbs.db
forge-gui-android/res/layout/bin
forge-gui-android/res/layout/gen
forge-gui-android/res/layout/target
forge-gui-android/res/target
forge-gui-android/res/values/Thumbs.db
forge-gui-android/res/values/bin
forge-gui-android/res/values/gen
forge-gui-android/res/values/target
forge-gui-android/target
forge-gui-desktop/*.iml
forge-gui-desktop/target
forge-gui-ios/*.iml
forge-gui-ios/target
forge-gui-mobile-dev/*.iml
forge-gui-mobile-dev/bin
forge-gui-mobile-dev/fallback_skin/Thumbs.db
forge-gui-mobile-dev/res
forge-gui-mobile-dev/target
forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat
forge-gui-mobile/*.iml
forge-gui-mobile/bin
forge-gui-mobile/target
forge-gui/forge-gui.iml
forge-gui/forge.profile.properties
forge-gui/res/*.log
forge-gui/res/PerSetTrackingResults
forge-gui/res/cardsfolder/*.bat
forge-gui/res/decks
forge-gui/res/layouts
forge-gui/res/pics*
forge-gui/res/pics_product
forge-gui/res/skins/**/PerSetTrackingResults
forge-gui/res/skins/**/decks
forge-gui/res/skins/**/layouts
forge-gui/res/skins/**/pics*
forge-gui/res/skins/**/pics_product
forge-gui/res/quest/world/1996-05[!!-~]Ice[!!-~]Age/duels/.directory
forge-gui/res/skins/*.log
forge-gui/res/skins/PerSetTrackingResults
forge-gui/res/skins/Thumbs.db
forge-gui/res/skins/arabian_nights/*.log
forge-gui/res/skins/arabian_nights/PerSetTrackingResults
forge-gui/res/skins/arabian_nights/Thumbs.db
forge-gui/res/skins/arabian_nights/decks
forge-gui/res/skins/arabian_nights/layouts
forge-gui/res/skins/arabian_nights/pics*
forge-gui/res/skins/arabian_nights/pics_product
forge-gui/res/skins/comic/*.log
forge-gui/res/skins/comic/PerSetTrackingResults
forge-gui/res/skins/comic/Thumbs.db
forge-gui/res/skins/comic/decks
forge-gui/res/skins/comic/layouts
forge-gui/res/skins/comic/pics*
forge-gui/res/skins/comic/pics_product
forge-gui/res/skins/dark_ascension/*.log
forge-gui/res/skins/dark_ascension/PerSetTrackingResults
forge-gui/res/skins/dark_ascension/Thumbs.db
forge-gui/res/skins/dark_ascension/decks
forge-gui/res/skins/dark_ascension/layouts
forge-gui/res/skins/dark_ascension/pics*
forge-gui/res/skins/dark_ascension/pics_product
forge-gui/res/skins/decks
forge-gui/res/skins/default/*.log
forge-gui/res/skins/default/PerSetTrackingResults
forge-gui/res/skins/default/Thumbs.db
forge-gui/res/skins/default/decks
forge-gui/res/skins/default/layouts
forge-gui/res/skins/default/pics*
forge-gui/res/skins/default/pics_product
forge-gui/res/skins/firebloom/*.log
forge-gui/res/skins/firebloom/PerSetTrackingResults
forge-gui/res/skins/firebloom/Thumbs.db
forge-gui/res/skins/firebloom/decks
forge-gui/res/skins/firebloom/layouts
forge-gui/res/skins/firebloom/pics*
forge-gui/res/skins/firebloom/pics_product
forge-gui/res/skins/inferno/*.log
forge-gui/res/skins/inferno/PerSetTrackingResults
forge-gui/res/skins/inferno/Thumbs.db
forge-gui/res/skins/inferno/decks
forge-gui/res/skins/inferno/layouts
forge-gui/res/skins/inferno/pics*
forge-gui/res/skins/inferno/pics_product
forge-gui/res/skins/innistrad/*.log
forge-gui/res/skins/innistrad/PerSetTrackingResults
forge-gui/res/skins/innistrad/Thumbs.db
forge-gui/res/skins/innistrad/decks
forge-gui/res/skins/innistrad/layouts
forge-gui/res/skins/innistrad/pics*
forge-gui/res/skins/innistrad/pics_product
forge-gui/res/skins/journeyman/*.log
forge-gui/res/skins/journeyman/PerSetTrackingResults
forge-gui/res/skins/journeyman/Thumbs.db
forge-gui/res/skins/journeyman/decks
forge-gui/res/skins/journeyman/layouts
forge-gui/res/skins/journeyman/pics*
forge-gui/res/skins/journeyman/pics_product
forge-gui/res/skins/kamigawa/*.log
forge-gui/res/skins/kamigawa/PerSetTrackingResults
forge-gui/res/skins/kamigawa/Thumbs.db
forge-gui/res/skins/kamigawa/decks
forge-gui/res/skins/kamigawa/layouts
forge-gui/res/skins/kamigawa/pics*
forge-gui/res/skins/kamigawa/pics_product
forge-gui/res/skins/layouts
forge-gui/res/skins/marble_blue/*.log
forge-gui/res/skins/marble_blue/PerSetTrackingResults
forge-gui/res/skins/marble_blue/Thumbs.db
forge-gui/res/skins/marble_blue/decks
forge-gui/res/skins/marble_blue/layouts
forge-gui/res/skins/marble_blue/pics*
forge-gui/res/skins/marble_blue/pics_product
forge-gui/res/skins/metalcraft/*.log
forge-gui/res/skins/metalcraft/PerSetTrackingResults
forge-gui/res/skins/metalcraft/Thumbs.db
forge-gui/res/skins/metalcraft/decks
forge-gui/res/skins/metalcraft/layouts
forge-gui/res/skins/metalcraft/pics*
forge-gui/res/skins/metalcraft/pics_product
forge-gui/res/skins/mythic_rare/*.log
forge-gui/res/skins/mythic_rare/PerSetTrackingResults
forge-gui/res/skins/mythic_rare/Thumbs.db
forge-gui/res/skins/mythic_rare/decks
forge-gui/res/skins/mythic_rare/layouts
forge-gui/res/skins/mythic_rare/pics*
forge-gui/res/skins/mythic_rare/pics_product
forge-gui/res/skins/phyrexia/*.log
forge-gui/res/skins/phyrexia/PerSetTrackingResults
forge-gui/res/skins/phyrexia/Thumbs.db
forge-gui/res/skins/phyrexia/decks
forge-gui/res/skins/phyrexia/layouts
forge-gui/res/skins/phyrexia/pics*
forge-gui/res/skins/phyrexia/pics_product
forge-gui/res/skins/pics*
forge-gui/res/skins/pics_product
forge-gui/res/skins/ravnica/*.log
forge-gui/res/skins/ravnica/PerSetTrackingResults
forge-gui/res/skins/ravnica/Thumbs.db
forge-gui/res/skins/ravnica/decks
forge-gui/res/skins/ravnica/layouts
forge-gui/res/skins/ravnica/pics*
forge-gui/res/skins/ravnica/pics_product
forge-gui/res/skins/rebel/*.log
forge-gui/res/skins/rebel/PerSetTrackingResults
forge-gui/res/skins/rebel/Thumbs.db
forge-gui/res/skins/rebel/decks
forge-gui/res/skins/rebel/layouts
forge-gui/res/skins/rebel/pics*
forge-gui/res/skins/rebel/pics_product
forge-gui/res/skins/sleeping_forest/*.log
forge-gui/res/skins/sleeping_forest/PerSetTrackingResults
forge-gui/res/skins/sleeping_forest/Thumbs.db
forge-gui/res/skins/sleeping_forest/decks
forge-gui/res/skins/sleeping_forest/layouts
forge-gui/res/skins/sleeping_forest/pics*
forge-gui/res/skins/sleeping_forest/pics_product
forge-gui/res/skins/smith/*.log
forge-gui/res/skins/smith/PerSetTrackingResults
forge-gui/res/skins/smith/Thumbs.db
forge-gui/res/skins/smith/decks
forge-gui/res/skins/smith/layouts
forge-gui/res/skins/smith/pics*
forge-gui/res/skins/smith/pics_product
forge-gui/res/skins/the_dale/*.log
forge-gui/res/skins/the_dale/PerSetTrackingResults
forge-gui/res/skins/the_dale/Thumbs.db
forge-gui/res/skins/the_dale/decks
forge-gui/res/skins/the_dale/layouts
forge-gui/res/skins/the_dale/pics*
forge-gui/res/skins/the_dale/pics_product
forge-gui/res/skins/the_simpsons/*.log
forge-gui/res/skins/the_simpsons/PerSetTrackingResults
forge-gui/res/skins/the_simpsons/Thumbs.db
forge-gui/res/skins/the_simpsons/decks
forge-gui/res/skins/the_simpsons/layouts
forge-gui/res/skins/the_simpsons/pics*
forge-gui/res/skins/the_simpsons/pics_product
forge-gui/res/skins/zendikar/*.log
forge-gui/res/skins/zendikar/PerSetTrackingResults
forge-gui/res/skins/zendikar/Thumbs.db
forge-gui/res/skins/zendikar/decks
forge-gui/res/skins/zendikar/layouts
forge-gui/res/skins/zendikar/pics*
forge-gui/res/skins/zendikar/pics_product
forge-gui/target
forge-gui/tools/AllCards.json
forge-gui/tools/EditionTrackingResults
forge-gui/tools/PerSetTrackingResults
forge-gui/tools/oracleScript.log
/forge.profile.properties
/jgv.txt
/nbactions.xml
/pom.xml.next
/pom.xml.releaseBackup
/pom.xml.tag
/release.properties
/target
/test-output
.settings
.classpath
.project
.vscode/settings.json
.vscode/launch.json

View File

@@ -1,15 +0,0 @@
<!--
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>
</settings>

View File

@@ -1 +0,0 @@
--settings ./.mvn/local-settings.xml

116
README.md
View File

@@ -1,44 +1,44 @@
# Forge
[Official repo](https://github.com/Card-Forge/forge.git).
Gitlab repo is found [here](https://git.cardforge.org/core-developers/forge).
Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wiki) (Somewhat outdated)
Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated)
Discord channel [here](https://discord.gg/fWfNgCUNRq)
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
## Requirements / Tools
# Requirements / Tools
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- Java JDK 8 or later (some IDEs such as Eclipse require JDK11+, whereas the Android build currently only works with JDK8)
- Java IDE such as IntelliJ or Eclipse
- Java JDK 8 or later
- Git
- Git client (optional)
- Maven
- GitHub account
- Gitlab account
- Libgdx (optional: familiarity with this library is helpful for mobile platform development)
- Android SDK (optional: for Android releases)
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
## Project Quick Setup
# Project Quick Setup
- Login into GitHub with your user account and fork the project.
- Log in to gitlab with your user account and fork the project.
- Clone your forked project to your local machine
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
## Eclipse
# Eclipse
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
### Project Setup
## Project Setup
- Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
- Follow the instructions for cloning from Gitlab. You'll need a Gitlab account setup and an SSH key defined.
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
- Fork the Forge git repo to your GitHub account.
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your Gitlab profile under
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing Gitlab.
- Fork the Forge git repo to your Gitlab account.
- Clone your forked repo to your local machine.
@@ -48,16 +48,16 @@ Eclipse includes Maven integration so a separate install is not necessary. For
- 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.
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
for this first time through.
- Once everything builds, all errors should disappear. You can now advance to Project launch.
### Project Launch
## Project Launch
#### Desktop
### Desktop
This is the standard configuration used for releasing to Windows / Linux / MacOS.
@@ -65,7 +65,7 @@ This is the standard configuration used for releasing to Windows / Linux / MacOS
- The familiar Forge splash screen, etc. should appear. Enjoy!
#### Mobile (Desktop dev)
### Mobile (Desktop dev)
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
@@ -73,49 +73,50 @@ This is the configuration used for doing mobile development using the Windows /
- A view similar to a mobile phone should appear. Enjoy!
### Eclipse / Android SDK Integration
## Eclipse / Android SDK Integration
Google no longer supports Android SDK releases for Eclipse. That said, it is still possible to build and debug Android platforms.
#### Android SDK
### Android SDK
Reference SO for obtaining a specific release: https://stackoverflow.com/questions/27043522/where-can-i-download-an-older-version-of-the-android-sdk
##### Windows
#### Windows
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
#### Linux / Mac OSX
TBD
#### Android Plugin for Eclipse
### Android Plugin for Eclipse
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
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
### Android Platform
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 26.0.1
- Android 8.0.0 (API 26) SDK Platform
- Google USB Driver (in case your phone is not detected by ADB)
- Android 7.1.1 (API 25) SDK Platform
- Google USB Driver 11
Note that this will populate additional tools in the Android SDK install path extracted above.
#### Proguard update
### Proguard update
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/.
The Proguard included with the Android SDK Build-tools is outdated and does not work with Java 1.8. Download Proguard 6.0.3 from https://sourceforge.net/projects/proguard/files/proguard/6.0/.
- 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>`.
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard4.7/.
#### Android Build
- Extract Proguard 6.0.3 to the Android SDK install path under tools/. You will need to rename the dir proguard6.0.3/ to proguard/.
### Android Build
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.
@@ -134,7 +135,7 @@ things out. The steps below show how to generate a debug Android build.
Assuming you got this far, you should have an Android forge-android-[version].apk in the forge-gui-android/target path.
#### Android Deploy
### Android Deploy
You'll need to have the Android SDK install path platform-tools/ path in your command search path to easily deploy builds.
@@ -148,37 +149,37 @@ You'll need to have the Android SDK install path platform-tools/ path in your co
- Install the new apk: `adb install forge-android-[version].apk`
#### Android Debugging
### Android Debugging
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
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
## Windows / Linux SNAPSHOT build
SNAPSHOT builds can be built via the Maven integration in Eclipse.
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, set Profiles: windows-linux
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
## IntelliJ
# IntelliJ
Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/wikis/intellij-setup).
## Card Scripting
# Card Scripting
Visit [this page](https://github.com/Card-Forge/forge/wiki/Card-scripting-API) for information on scripting.
Visit [this page](https://www.slightlymagic.net/wiki/Forge_API) for information on scripting.
Card scripting resources are found in the forge-gui/res/ path.
## General Notes
# General Notes
### Project Hierarchy
## Project Hierarchy
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
@@ -195,34 +196,35 @@ The platform-specific projects are:
- forge-gui-mobile
- forge-gui-mobile-dev
#### forge-ai
### forge-ai
#### forge-core
### forge-core
#### forge-game
### forge-game
#### forge-gui
### forge-gui
The forge-gui project includes the scripting resource definitions in the res/ path.
#### forge-gui-android
### forge-gui-android
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
#### forge-gui-desktop
### forge-gui-desktop
Java Swing based GUI targeting desktop machines.
Java Swing based GUI targeting desktop machines.
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
#### forge-gui-ios
### forge-gui-ios
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
#### forge-gui-mobile
### forge-gui-mobile
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
#### forge-gui-mobile-dev
### forge-gui-mobile-dev
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

View File

@@ -1,287 +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>1.6.50-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>forge-adventure</artifactId>
<packaging>jar</packaging>
<name>Forge Adventure</name>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<build>
<sourceDirectory>src</sourceDirectory>
<resources>
<resource>
<directory>${project.basedir}</directory>
<includes>
<include>**/*.vert</include>
<include>**/*.frag</include>
<include>**/title_bg_lq.png</include>
<include>**/transition.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-java8.exe</outfile>
<jar>${project.build.finalName}-jar-with-dependencies.jar</jar>
<dontWrapJar>true</dontWrapJar>
<errTitle>forge</errTitle>
<icon>src/main/config/forge-adventure.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</productName>
<internalName>forge-adventure</internalName>
<originalFilename>forge-adventure-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.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.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.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.font=ALL-UNNAMED</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</productName>
<internalName>forge-adventure</internalName>
<originalFilename>forge-adventure.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.sh, forge-adventure.command, forge-adventure.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.github.jetopto1</groupId>
<artifactId>cling</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.10.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.10.0</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl3</artifactId>
<version>1.10.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype-platform</artifactId>
<version>1.10.0</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-game</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>forge</groupId>
<artifactId>forge-ai</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>forge</groupId>
<artifactId>forge-gui-mobile</artifactId>
<version>1.6.50-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>

View File

@@ -1,14 +0,0 @@
# ideally this should be using HTTPS, but this is fine for now
dsn=http://a0b8dbad9b8a49cfa51bf65d462e8dae@sentry.cardforge.org:9000/2
stacktrace.app.packages=forge
# where to store events if offline or can't reach the above server
buffer.dir=sentry-events
buffer.size=100
# allow ample time for graceful shutdown
buffer.shutdowntimeout=5000
async.shutdowntimeout=5000
# allow longer messages
maxmessagelength=1500

View File

@@ -1,15 +0,0 @@
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_color;
varying vec2 v_texCoords;
uniform sampler2D u_texture;
uniform float u_grayness;
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 = vec4(blendedColor.rgb, c.a);
}

View File

@@ -1,14 +0,0 @@
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

@@ -1,40 +0,0 @@
#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

@@ -1,16 +0,0 @@
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

@@ -1,23 +0,0 @@
#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;
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);
gl_FragColor = texture2D(u_texture, uv);
}

View File

@@ -1,57 +0,0 @@
#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

@@ -1,25 +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% GEQ 17 (
java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=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 -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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -1,190 +0,0 @@
package forge.adventure;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Clipboard;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Window;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3WindowListener;
import com.badlogic.gdx.graphics.glutils.HdpiMode;
import forge.Forge;
import forge.adventure.util.Config;
import forge.interfaces.IDeviceAdapter;
import forge.util.BuildInfo;
import forge.util.FileUtil;
import forge.util.OperatingSystem;
import forge.util.RestartUtil;
import io.sentry.Sentry;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* Main entry point
*/
public class Main {
public static void main(String[] args) {
Sentry.init(options -> {
options.setEnableExternalConfiguration(true);
options.setRelease(BuildInfo.getVersionString());
options.setEnvironment(System.getProperty("os.name"));
options.setTag("Java Version", System.getProperty("java.version"));
}, true);
// HACK - temporary solution to "Comparison method violates it's general contract!" crash
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
//Turn off the Java 2D system's use of Direct3D to improve rendering speed (particularly when Full Screen)
System.setProperty("sun.java2d.d3d", "false");
Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
config.setResizable(false);
ApplicationListener start = Forge.getApp(new Lwjgl3Clipboard(), new DesktopAdapter(""), Files.exists(Paths.get("./res"))?"./":"../forge-gui/", true, false, 0, true, 0, "", "");
if (Config.instance().getSettingData().fullScreen) {
config.setFullscreenMode(Lwjgl3ApplicationConfiguration.getDisplayMode());
config.setAutoIconify(true);
config.setHdpiMode(HdpiMode.Logical);
} else {
config.setWindowedMode(Config.instance().getSettingData().width, Config.instance().getSettingData().height);
}
config.setTitle("Forge Adventure Mobile");
config.setWindowIcon(Config.instance().getFilePath("forge-adventure.png"));
config.setWindowListener(new Lwjgl3WindowListener() {
@Override
public void created(Lwjgl3Window lwjgl3Window) {
}
@Override
public void iconified(boolean b) {
}
@Override
public void maximized(boolean b) {
}
@Override
public void focusLost() {
}
@Override
public void focusGained() {
}
@Override
public boolean closeRequested() {
//use the device adpater to exit properly
if (Forge.safeToClose)
Forge.exit(true);
return false;
}
@Override
public void filesDropped(String[] strings) {
}
@Override
public void refreshRequested() {
}
});
new Lwjgl3Application(start, config);
}
private static class DesktopAdapter implements IDeviceAdapter {
private final String switchOrientationFile;
private DesktopAdapter(String switchOrientationFile0) {
switchOrientationFile = switchOrientationFile0;
}
//just assume desktop always connected to wifi
@Override
public boolean isConnectedToInternet() {
return true;
}
@Override
public boolean isConnectedToWifi() {
return true;
}
@Override
public String getDownloadsDir() {
return System.getProperty("user.home") + "/Downloads/";
}
@Override
public boolean openFile(String filename) {
try {
Desktop.getDesktop().open(new File(filename));
return true;
}
catch (IOException e) {
e.printStackTrace();
}
return false;
}
@Override
public void restart() {
if (RestartUtil.prepareForRestart()) {
Gdx.app.exit();
System.exit(0);
}
}
@Override
public void exit() {
Gdx.app.exit(); //can just use Gdx.app.exit for desktop
System.exit(0);
}
@Override
public boolean isTablet() {
return true; //treat desktop the same as a tablet
}
@Override
public void setLandscapeMode(boolean landscapeMode) {
//create file to indicate that landscape mode should be used
if (landscapeMode) {
FileUtil.writeFile(switchOrientationFile, "1");
}
else {
FileUtil.deleteFile(switchOrientationFile);
}
}
@Override
public void preventSystemSleep(boolean preventSleep) {
OperatingSystem.preventSystemSleep(preventSleep);
}
@Override
public void convertToJPEG(InputStream input, OutputStream output) throws IOException {
BufferedImage image = ImageIO.read(input);
ImageIO.write(image, "jpg", output);
}
}
}

View File

@@ -1,32 +0,0 @@
package forge.adventure.editor;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class DocumentChangeListener implements DocumentListener {
private final Runnable run;
public DocumentChangeListener(Runnable run)
{
this.run = run;
}
@Override
public void insertUpdate(DocumentEvent e) {
changedUpdate(e);
}
@Override
public void removeUpdate(DocumentEvent e) {
changedUpdate(e);
}
@Override
public void changedUpdate(DocumentEvent e) {
run.run();
}
}

View File

@@ -1,24 +0,0 @@
package forge.adventure.editor;
import javax.swing.*;
import java.awt.*;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class EditorMainWindow extends JFrame {
JTabbedPane tabs =new JTabbedPane();
public EditorMainWindow()
{
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(tabs);
tabs.addTab("POI",new PointOfInterestEditor());
tabs.addTab("World",new WorldEditor());
tabs.addTab("Items",new ItemsEditor());
tabs.addTab("Enemies",new EnemyEditor());
setVisible(true);
setSize(800,600);
}
}

View File

@@ -1,115 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.EffectData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
public class EffectEditor extends JComponent {
EffectData currentData;
JTextField name =new JTextField();
JSpinner changeStartCards = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner lifeModifier = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner moveSpeed = new JSpinner(new SpinnerNumberModel(0f, 0, 1, 0.1f));
TextListEdit startBattleWithCard =new TextListEdit();
JCheckBox colorView =new JCheckBox();
EffectEditor opponent = null;
private boolean updating=false;
public EffectEditor(boolean isOpponentEffect)
{
if(!isOpponentEffect)
opponent=new EffectEditor(true);
setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
JPanel parameters=new JPanel();
parameters.setBorder(BorderFactory.createTitledBorder("Effect"));
parameters.setLayout(new GridLayout(7,2)) ;
parameters.add(new JLabel("Name:")); parameters.add(name);
parameters.add(new JLabel("Start with extra cards:")); parameters.add(changeStartCards);
parameters.add(new JLabel("Change life:")); parameters.add(lifeModifier);
parameters.add(new JLabel("Movement speed:")); parameters.add(moveSpeed);
parameters.add(new JLabel("Start battle with cards:")); parameters.add(startBattleWithCard);
parameters.add(new JLabel("color view:")); parameters.add(colorView);
add(parameters);
if(!isOpponentEffect)
{ add(new JLabel("Opponent:")); add(opponent);}
changeStartCards.addChangeListener(e -> EffectEditor.this.updateEffect());
lifeModifier.addChangeListener(e -> EffectEditor.this.updateEffect());
moveSpeed.addChangeListener(e -> EffectEditor.this.updateEffect());
colorView.addChangeListener(e -> EffectEditor.this.updateEffect());
name.getDocument().addDocumentListener(new DocumentChangeListener(() -> EffectEditor.this.updateEffect()));
startBattleWithCard.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> EffectEditor.this.updateEffect()));
if(opponent!=null)
opponent.addChangeListener(e -> EffectEditor.this.updateEffect());
}
private void updateEffect() {
if(currentData==null||updating)
return;
currentData.name=name.getText();
currentData.changeStartCards=((Integer)changeStartCards.getValue()).intValue();
currentData.lifeModifier= ((Integer)lifeModifier.getValue()).intValue();
currentData.moveSpeed= ((Float)moveSpeed.getValue()).floatValue();
currentData.startBattleWithCard = startBattleWithCard.getList();
currentData.colorView = colorView.isSelected();
currentData.opponent = opponent.currentData;
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
public void setCurrentEffect(EffectData data)
{
if(data==null)
return;
currentData=data;
refresh();
}
public EffectData getCurrentEffect()
{
return currentData;
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
name.setText(currentData.name);
lifeModifier.setValue(currentData.lifeModifier);
changeStartCards.setValue(currentData.changeStartCards);
startBattleWithCard.setText(currentData.startBattleWithCard);
colorView.setSelected(currentData.colorView);
moveSpeed.setValue(currentData.moveSpeed);
if(opponent!=null)
opponent.setCurrentEffect(currentData.opponent);
updating=false;
}
}

View File

@@ -1,111 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.EnemyData;
import javax.swing.*;
import java.awt.*;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class EnemyEdit extends JComponent {
EnemyData currentData;
JTextField nameField=new JTextField();
JTextField colorField=new JTextField();
JSpinner lifeFiled= new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner spawnRate= new JSpinner(new SpinnerNumberModel(0.0, 0., 1, 0.1));
JSpinner difficulty= new JSpinner(new SpinnerNumberModel(0.0, 0., 1, 0.1));
JSpinner speed= new JSpinner(new SpinnerNumberModel(0.0, 0., 100., 1.0));
FilePicker deck=new FilePicker(new String[]{"dck","json"});
FilePicker atlas=new FilePicker(new String[]{"atlas"});
JTextField equipment=new JTextField();
RewardsEditor rewards=new RewardsEditor();
SwingAtlasPreview preview=new SwingAtlasPreview();
private boolean updating=false;
public EnemyEdit()
{
JComponent center=new JComponent() { };
center.setLayout(new GridLayout(9,2));
center.add(new JLabel("Name:")); center.add(nameField);
center.add(new JLabel("Life:")); center.add(lifeFiled);
center.add(new JLabel("Spawn rate:")); center.add(spawnRate);
center.add(new JLabel("Difficulty:")); center.add(difficulty);
center.add(new JLabel("Speed:")); center.add(speed);
center.add(new JLabel("Deck:")); center.add(deck);
center.add(new JLabel("Sprite:")); center.add(atlas);
center.add(new JLabel("Equipment:")); center.add(equipment);
center.add(new JLabel("Colors:")); center.add(colorField);
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(center,BorderLayout.PAGE_START);
add(rewards,BorderLayout.CENTER);
add(preview,BorderLayout.LINE_START);
equipment.getDocument().addDocumentListener(new DocumentChangeListener(() -> EnemyEdit.this.updateEnemy()));
atlas.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> EnemyEdit.this.updateEnemy()));
colorField.getDocument().addDocumentListener(new DocumentChangeListener(() -> EnemyEdit.this.updateEnemy()));
nameField.getDocument().addDocumentListener(new DocumentChangeListener(() -> EnemyEdit.this.updateEnemy()));
deck.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(() -> EnemyEdit.this.updateEnemy()));
lifeFiled.addChangeListener(e -> EnemyEdit.this.updateEnemy());
speed.addChangeListener(e -> EnemyEdit.this.updateEnemy());
difficulty.addChangeListener(e -> EnemyEdit.this.updateEnemy());
spawnRate.addChangeListener(e -> EnemyEdit.this.updateEnemy());
rewards.addChangeListener(e -> EnemyEdit.this.updateEnemy());
lifeFiled.addChangeListener(e -> EnemyEdit.this.updateEnemy());
refresh();
}
private void updateEnemy() {
if(currentData==null||updating)
return;
currentData.name=nameField.getText();
currentData.colors=colorField.getText();
currentData.life= (int) lifeFiled.getValue();
currentData.sprite= atlas.getEdit().getText();
if(equipment.getText().isEmpty())
currentData.equipment=null;
else
currentData.equipment=equipment.getText().split(",");
currentData.speed= ((Double) speed.getValue()).floatValue();
currentData.spawnRate=((Double) spawnRate.getValue()).floatValue();
currentData.difficulty=((Double) difficulty.getValue()).floatValue();
currentData.deck= deck.getEdit().getText();
currentData.rewards= rewards.getRewards();
preview.setSpritePath(currentData.sprite);
}
public void setCurrentEnemy(EnemyData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
nameField.setText(currentData.name);
colorField.setText(currentData.colors);
lifeFiled.setValue(currentData.life);
atlas.getEdit().setText(currentData.sprite);
if(currentData.equipment!=null)
equipment.setText(String.join(",",currentData.equipment));
else
equipment.setText("");
deck.getEdit().setText(currentData.deck);
speed.setValue(new Float(currentData.speed).doubleValue());
spawnRate.setValue(new Float(currentData.spawnRate).doubleValue());
difficulty.setValue(new Float(currentData.difficulty).doubleValue());
rewards.setRewards(currentData.rewards);
preview.setSpritePath(currentData.sprite);
updating=false;
}
}

View File

@@ -1,161 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.EnemyData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class EnemyEditor extends JComponent {
DefaultListModel<EnemyData> model = new DefaultListModel<>();
JList<EnemyData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
EnemyEdit edit=new EnemyEdit();
public class EnemyDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof EnemyData))
return label;
EnemyData enemy=(EnemyData) value;
// Get the renderer component from parent class
label.setText(enemy.name);
SwingAtlas atlas=new SwingAtlas(Config.instance().getFile(enemy.sprite));
if(atlas.has("Avatar"))
label.setIcon(atlas.get("Avatar"));
else
{
ImageIcon img=atlas.getAny();
if(img!=null)
label.setIcon(img);
}
return label;
}
}
public void addButton(String name,ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public EnemyEditor()
{
list.setCellRenderer(new EnemyDataRenderer());
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
EnemyEditor.this.updateEdit();
}
});
addButton("add", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
EnemyEditor.this.addEnemy();
}
});
addButton("remove", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
EnemyEditor.this.remove();
}
});
addButton("copy", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
EnemyEditor.this.copy();
}
});
addButton("load", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
EnemyEditor.this.load();
}
});
addButton("save", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
EnemyEditor.this.save();
}
});
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(new JScrollPane(list), BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
load();
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
EnemyData data=new EnemyData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentEnemy(model.get(selected));
}
void save()
{
Array<EnemyData> allEnemies=new Array<>();
for(int i=0;i<model.getSize();i++)
allEnemies.add(model.get(i));
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.ENEMIES);
handle.writeString(json.prettyPrint(json.toJson(allEnemies,Array.class, EnemyData.class)),false);
}
void load()
{
model.clear();
Array<EnemyData> allEnemies=new Array<>();
Json json = new Json();
FileHandle handle = Config.instance().getFile(Paths.ENEMIES);
if (handle.exists())
{
Array readEnemies=json.fromJson(Array.class, EnemyData.class, handle);
allEnemies = readEnemies;
}
for (int i=0;i<allEnemies.size;i++) {
model.add(i,allEnemies.get(i));
}
}
void addEnemy()
{
EnemyData data=new EnemyData();
data.name="Enemy "+model.getSize();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
}

View File

@@ -1,58 +0,0 @@
package forge.adventure.editor;
import forge.adventure.util.Config;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class FilePicker extends Box {
JTextField edit=new JTextField();
JButton findButton=new JButton(UIManager.getIcon("FileView.directoryIcon"));
private final String[] fileEndings;
public FilePicker(String[] fileEndings) {
super(BoxLayout.X_AXIS);
this.fileEndings = fileEndings;
findButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
FilePicker.this.find();
}
});
add(edit);
add(findButton);
}
JTextField getEdit()
{
return edit;
}
private void find() {
JFileChooser fc = new JFileChooser();
fc.setCurrentDirectory(new File(Config.instance().getFilePath(edit.getText())));
fc.setFileFilter( new FileNameExtensionFilter("Pick File",fileEndings));
fc.setMultiSelectionEnabled(false);
if (fc.showOpenDialog(this) ==
JFileChooser.APPROVE_OPTION) {
File selected = fc.getSelectedFile();
try {
if (selected != null&&selected.getCanonicalPath().startsWith(new File(Config.instance().getFilePath("")).getCanonicalPath())) {
edit.setText(selected.getCanonicalPath().substring(new File(Config.instance().getFilePath("")).getCanonicalPath().length()+1).replace('\\','/'));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -1,88 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.ItemData;
import javax.swing.*;
import java.awt.*;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class ItemEdit extends JComponent {
ItemData currentData;
JTextField nameField=new JTextField();
JTextField equipmentSlot=new JTextField();
JTextField iconName=new JTextField();
EffectEditor effect=new EffectEditor(false);
JTextField description=new JTextField();
JCheckBox questItem=new JCheckBox();
JSpinner cost= new JSpinner(new SpinnerNumberModel(0, 0, 100000, 1));
private boolean updating=false;
public ItemEdit()
{
setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
JPanel parameters=new JPanel();
parameters.setBorder(BorderFactory.createTitledBorder("Parameter"));
parameters.setLayout(new GridLayout(6,2)) ;
parameters.add(new JLabel("Name:")); parameters.add(nameField);
parameters.add(new JLabel("equipmentSlot:")); parameters.add(equipmentSlot);
parameters.add(new JLabel("description:")); parameters.add(description);
parameters.add(new JLabel("iconName")); parameters.add(iconName);
parameters.add(new JLabel("questItem")); parameters.add(questItem);
parameters.add(new JLabel("cost")); parameters.add(cost);
add(parameters);
add(effect);
nameField.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
equipmentSlot.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
description.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
iconName.getDocument().addDocumentListener(new DocumentChangeListener(() -> ItemEdit.this.updateItem()));
cost.addChangeListener(e -> ItemEdit.this.updateItem());
questItem.addChangeListener(e -> ItemEdit.this.updateItem());
effect.addChangeListener(e -> ItemEdit.this.updateItem());
refresh();
}
private void updateItem() {
if(currentData==null||updating)
return;
currentData.name=nameField.getText();
currentData.equipmentSlot= equipmentSlot.getText();
currentData.effect= effect.getCurrentEffect();
currentData.description=description.getText();
currentData.iconName=iconName.getText();
currentData.questItem=questItem.isSelected();
currentData.cost=((Integer) cost.getValue()).intValue();
}
public void setCurrentItem(ItemData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
nameField.setText(currentData.name);
effect.setCurrentEffect(currentData.effect);
equipmentSlot.setText(currentData.equipmentSlot);
description.setText(currentData.description);
iconName.setText(currentData.iconName);
questItem.setSelected(currentData.questItem);
cost.setValue(currentData.cost);
updating=false;
}
}

View File

@@ -1,128 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import forge.adventure.data.ItemData;
import forge.adventure.util.Config;
import forge.adventure.util.Paths;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
public class ItemsEditor extends JComponent {
DefaultListModel<ItemData> model = new DefaultListModel<>();
JList<ItemData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
ItemEdit edit=new ItemEdit();
static SwingAtlas itemAtlas;
public class ItemDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof ItemData))
return label;
ItemData Item=(ItemData) value;
// Get the renderer component from parent class
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));
else
{
ImageIcon img=itemAtlas.getAny();
if(img!=null)
label.setIcon(img);
}
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public ItemsEditor()
{
list.setCellRenderer(new ItemsEditor.ItemDataRenderer());
list.addListSelectionListener(e -> ItemsEditor.this.updateEdit());
addButton("add", e -> ItemsEditor.this.addItem());
addButton("remove", e -> ItemsEditor.this.remove());
addButton("copy", e -> ItemsEditor.this.copy());
addButton("load", e -> ItemsEditor.this.load());
addButton("save", e -> ItemsEditor.this.save());
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(new JScrollPane(list), BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
load();
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
ItemData data=new ItemData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentItem(model.get(selected));
}
void save()
{
Array<ItemData> allEnemies=new Array<>();
for(int i=0;i<model.getSize();i++)
allEnemies.add(model.get(i));
Json json = new Json(JsonWriter.OutputType.json);
FileHandle handle = Config.instance().getFile(Paths.ITEMS);
handle.writeString(json.prettyPrint(json.toJson(allEnemies,Array.class, ItemData.class)),false);
}
void load()
{
model.clear();
Array<ItemData> allEnemies=new Array<>();
Json json = new Json();
FileHandle handle = Config.instance().getFile(Paths.ITEMS);
if (handle.exists())
{
Array readEnemies=json.fromJson(Array.class, ItemData.class, handle);
allEnemies = readEnemies;
}
for (int i=0;i<allEnemies.size;i++) {
model.add(i,allEnemies.get(i));
}
}
void addItem()
{
ItemData data=new ItemData();
data.name="Item "+model.getSize();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
}

View File

@@ -1,20 +0,0 @@
package forge.adventure.editor;
import forge.GuiMobile;
import forge.adventure.util.Config;
import forge.gui.GuiBase;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class Main {
public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo("", "", 0, 0);
Config.instance();
new EditorMainWindow();
}
}

View File

@@ -1,6 +0,0 @@
package forge.adventure.editor;
import java.awt.*;
public class PointOfInterestEditor extends Component {
}

View File

@@ -1,228 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.RewardData;
import forge.card.CardType;
import forge.game.keyword.Keyword;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class RewardEdit extends JComponent {
RewardData currentData;
JComboBox typeField =new JComboBox(new String[] { "card", "gold", "life", "deckCard", "item"});
JSpinner probability = new JSpinner(new SpinnerNumberModel(0f, 0, 1, 0.1f));
JSpinner count = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JSpinner addMaxCount = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
JTextField cardName =new JTextField();
JTextField itemName =new JTextField();
TextListEdit editions =new TextListEdit();
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.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.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;
public RewardEdit()
{
setLayout(new GridLayout(16,2));
add(new JLabel("Type:")); add(typeField);
add(new JLabel("probability:")); add(probability);
add(new JLabel("count:")); add(count);
add(new JLabel("addMaxCount:")); add(addMaxCount);
add(new JLabel("cardName:")); add(cardName);
add(new JLabel("itemName:")); add(itemName);
add(new JLabel("editions:")); add(editions);
add(new JLabel("colors:")); add(colors);
add(new JLabel("rarity:")); add(rarity);
add(new JLabel("subTypes:")); add(subTypes);
add(new JLabel("cardTypes:")); add(cardTypes);
add(new JLabel("superTypes:")); add(superTypes);
add(new JLabel("manaCosts:")); add(manaCosts);
add(new JLabel("keyWords:")); add(keyWords);
add(new JLabel("colorType:")); add(colorType);
add(new JLabel("cardText:")); add(cardText);
typeField.addActionListener((new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
RewardEdit.this.updateReward();
}
}));
probability.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
RewardEdit.this.updateReward();
}
});
count.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
RewardEdit.this.updateReward();
}
});
addMaxCount.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
RewardEdit.this.updateReward();
}
});
cardName.getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
itemName.getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
editions.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
colors.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
rarity.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
subTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
cardTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
superTypes.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
manaCosts.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
keyWords.getEdit().getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
colorType.addActionListener((new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
RewardEdit.this.updateReward();
}
}));
cardText.getDocument().addDocumentListener(new DocumentChangeListener(new Runnable() {
@Override
public void run() {
RewardEdit.this.updateReward();
}
}));
}
private void updateReward() {
if(currentData==null||updating)
return;
currentData.type=typeField.getSelectedItem()==null?null:typeField.getSelectedItem().toString();
currentData.probability=((Double)probability.getValue()).floatValue();
currentData.count= (int) count.getValue();
currentData.addMaxCount= (int) addMaxCount.getValue();
currentData.cardName = cardName.getText().isEmpty()?null:cardName.getText();
currentData.itemName = itemName.getText().isEmpty()?null:itemName.getText();
currentData.editions = editions.getList();
currentData.colors = colors.getList();
currentData.rarity = rarity.getList();
currentData.subTypes = subTypes.getList();
currentData.cardTypes = cardTypes.getList();
currentData.superTypes = superTypes.getList();
currentData.manaCosts = manaCosts.getListAsInt();
currentData.keyWords = keyWords.getList();
currentData.colorType=colorType.getSelectedItem()==null?null:colorType.getSelectedItem().toString();
currentData.cardText = cardText.getText().isEmpty()?null:cardText.getText();
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
public void setCurrentReward(RewardData data)
{
currentData=data;
refresh();
}
private void refresh() {
setEnabled(currentData!=null);
if(currentData==null)
{
return;
}
updating=true;
typeField.setSelectedItem(currentData.type);
probability.setValue(new Double(currentData.probability));
count.setValue(currentData.count);
addMaxCount.setValue(currentData.addMaxCount);
cardName.setText(currentData.cardName);
itemName.setText(currentData.itemName);
editions.setText(currentData.editions);
colors.setText(currentData.colors);
rarity.setText(currentData.rarity);
subTypes.setText(currentData.subTypes);
cardTypes.setText(currentData.cardTypes);
superTypes.setText(currentData.superTypes);
manaCosts.setText(currentData.manaCosts);
keyWords.setText(currentData.keyWords);
colorType.setSelectedItem(currentData.colorType);
cardText.setText(currentData.cardText);
updating=false;
}
}

View File

@@ -1,160 +0,0 @@
package forge.adventure.editor;
import forge.adventure.data.RewardData;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class RewardsEditor extends JComponent{
DefaultListModel<RewardData> model = new DefaultListModel<>();
JList<RewardData> list = new JList<>(model);
JToolBar toolBar = new JToolBar("toolbar");
RewardEdit edit=new RewardEdit();
public class RewardDataRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if(!(value instanceof RewardData))
return label;
RewardData reward=(RewardData) value;
StringBuilder builder=new StringBuilder();
if(reward.type==null||reward.type.isEmpty())
builder.append("Reward");
else
builder.append(reward.type);
builder.append(" ");
builder.append(reward.count);
if(reward.addMaxCount>0)
{
builder.append("-");
builder.append(reward.count+reward.addMaxCount);
}
label.setText(builder.toString());
return label;
}
}
public void addButton(String name, ActionListener action)
{
JButton newButton=new JButton(name);
newButton.addActionListener(action);
toolBar.add(newButton);
}
public RewardsEditor()
{
list.setCellRenderer(new RewardDataRenderer());
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
RewardsEditor.this.updateEdit();
}
});
addButton("add", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
RewardsEditor.this.addReward();
}
});
addButton("remove", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
RewardsEditor.this.remove();
}
});
addButton("copy", new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
RewardsEditor.this.copy();
}
});
BorderLayout layout=new BorderLayout();
setLayout(layout);
add(list, BorderLayout.LINE_START);
add(toolBar, BorderLayout.PAGE_START);
add(edit,BorderLayout.CENTER);
edit.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
emitChanged();
}
});
}
protected void emitChanged() {
ChangeListener[] listeners = listenerList.getListeners(ChangeListener.class);
if (listeners != null && listeners.length > 0) {
ChangeEvent evt = new ChangeEvent(this);
for (ChangeListener listener : listeners) {
listener.stateChanged(evt);
}
}
}
private void copy() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
RewardData data=new RewardData(model.get(selected));
model.add(model.size(),data);
}
private void updateEdit() {
int selected=list.getSelectedIndex();
if(selected<0)
return;
edit.setCurrentReward(model.get(selected));
}
void addReward()
{
RewardData data=new RewardData();
model.add(model.size(),data);
}
void remove()
{
int selected=list.getSelectedIndex();
if(selected<0)
return;
model.remove(selected);
}
public void setRewards(RewardData[] rewards) {
model.clear();
if(rewards==null)
return;
for (int i=0;i<rewards.length;i++) {
model.add(i,rewards[i]);
}
}
public RewardData[] getRewards() {
RewardData[] rewards= new RewardData[model.getSize()];
for(int i=0;i<model.getSize();i++)
{
rewards[i]=model.get(i);
}
return rewards;
}
public void addChangeListener(ChangeListener listener) {
listenerList.add(ChangeListener.class, listener);
}
}

View File

@@ -1,69 +0,0 @@
package forge.adventure.editor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.utils.Array;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import static java.awt.Image.SCALE_FAST;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class SwingAtlas {
HashMap<String, ArrayList<ImageIcon>> images=new HashMap<>();
public HashMap<String, ArrayList<ImageIcon>> getImages()
{
return images;
}
public SwingAtlas(FileHandle path)
{
if(!path.exists()||!path.toString().endsWith(".atlas"))
return;
TextureAtlas.TextureAtlasData data=new TextureAtlas.TextureAtlasData(path,path.parent(),false);
for(TextureAtlas.TextureAtlasData.Region region: new Array.ArrayIterator<>(data.getRegions()))
{
String name=region.name;
if(!images.containsKey(name))
{
images.put(name,new ArrayList<>());
}
ArrayList<ImageIcon> imageList=images.get(name);
try {
imageList.add(spriteToImage(region));
} catch (IOException e) {
e.printStackTrace();
}
}
}
private ImageIcon spriteToImage(TextureAtlas.TextureAtlasData.Region sprite) throws IOException {
BufferedImage img = ImageIO.read(sprite.page.textureFile.file());
return new ImageIcon(img.getSubimage(sprite.left,sprite.top, sprite.width, sprite.height).getScaledInstance(32,32,SCALE_FAST));
}
public ImageIcon get(String name) {
return images.get(name).get(0);
}
public boolean has(String name) {
return images.containsKey(name);
}
public ImageIcon getAny() {
if(images.isEmpty())
return null;
ArrayList<ImageIcon> imageList= images.get(images.keySet().iterator().next());
if(imageList.isEmpty())
return null;
return imageList.get(0);
}
}

View File

@@ -1,52 +0,0 @@
package forge.adventure.editor;
import forge.adventure.util.Config;
import org.apache.commons.lang3.tuple.Pair;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class SwingAtlasPreview extends Box {
private String sprite="";
Timer timer;
public SwingAtlasPreview() {
super(BoxLayout.Y_AXIS);
timer = new Timer(200, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
counter++;
for (Pair<JLabel, ArrayList<ImageIcon>> element : labels) {
element.getKey().setIcon(element.getValue().get(counter % element.getValue().size()));
}
}
});
}
int counter=0;
List<Pair<JLabel,ArrayList<ImageIcon>>> labels=new ArrayList<>();
public void setSpritePath(String sprite) {
if(this.sprite==null||this.sprite.equals(sprite))
return;
removeAll();
counter=0;
labels.clear();
this.sprite=sprite;
SwingAtlas atlas=new SwingAtlas(Config.instance().getFile(sprite));
for(Map.Entry<String, ArrayList<ImageIcon>> element:atlas.getImages().entrySet())
{
JLabel image=new JLabel(element.getValue().get(0));
add(new JLabel(element.getKey()));
add(image);
labels.add(Pair.of(image, element.getValue()));
}
timer.restart();
repaint();
}
}

View File

@@ -1,108 +0,0 @@
package forge.adventure.editor;
import forge.adventure.util.Config;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
/**
* Editor class to edit configuration, maybe moved or removed
*/
public class TextListEdit extends Box {
JTextField edit=new JTextField();
JButton findButton=new JButton(UIManager.getIcon("add"));
JComboBox elements;
public TextListEdit(String[] possibleElements) {
super(BoxLayout.X_AXIS);
findButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
TextListEdit.this.find();
}
});
add(edit);
//add(findButton);
elements= new JComboBox(possibleElements);
add(elements);
}
public TextListEdit()
{
this(new String[0]);
}
JTextField getEdit()
{
return edit;
}
private void find() {
JFileChooser fc = new JFileChooser();
fc.setCurrentDirectory(new File(Config.instance().getFilePath("")));
fc.setMultiSelectionEnabled(false);
if (fc.showOpenDialog(this) ==
JFileChooser.APPROVE_OPTION) {
File selected = fc.getSelectedFile();
try {
if (selected != null&&selected.getCanonicalPath().startsWith(new File(Config.instance().getFilePath("")).getCanonicalPath())) {
edit.setText(selected.getCanonicalPath().substring(new File(Config.instance().getFilePath("")).getCanonicalPath().length()+1));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void setText(String[] itemName) {
if(itemName==null)
edit.setText("");
else
edit.setText(String.join(";",itemName));
}
public void setText(int[] intValues) {
if(intValues==null)
{
edit.setText("");
return;
}
StringBuilder values= new StringBuilder();
for(int i=0;i<intValues.length;i++)
{
values.append(intValues[i]);
if(intValues.length>i+2)
values.append(";");
}
edit.setText(values.toString());
}
public String[] getList() {
return edit.getText().isEmpty()?null:edit.getText().split(";");
}
public int[] getListAsInt() {
if(edit.getText().isEmpty())
return null;
String[] stringList=getList();
int[] retList=new int[stringList.length];
for(int i=0;i<retList.length;i++)
{
String intName=stringList[i];
try
{
retList[i] = Integer.valueOf(intName);
}
catch (NumberFormatException e)
{
retList[i] =0;
}
}
return retList;
}
}

View File

@@ -1,6 +0,0 @@
package forge.adventure.editor;
import java.awt.*;
public class WorldEditor extends Component {
}

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.50-SNAPSHOT</version>
<version>1.6.30-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>

File diff suppressed because it is too large Load Diff

View File

@@ -17,38 +17,24 @@
*/
package forge.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardStateName;
import forge.game.CardTraitBase;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import java.util.*;
/**
* <p>
@@ -73,11 +59,7 @@ public class AiBlockController {
private boolean lifeInDanger = false;
// set to true when AI is predicting a blocking for another player so it doesn't use hidden information
private boolean checkingOther = false;
public AiBlockController(Player aiPlayer, boolean checkingOther) {
this.checkingOther = checkingOther;
public AiBlockController(Player aiPlayer) {
ai = aiPlayer;
}
@@ -88,8 +70,7 @@ public class AiBlockController {
for (final Card blocker : blockersLeft) {
// if the blocker can block a creature with lure it can't block a creature without
if (CombatUtil.canBlock(attacker, blocker, combat)) {
boolean cantBlockAlone = blocker.hasKeyword("CARDNAME can't attack or block alone.") || blocker.hasKeyword("CARDNAME can't block alone.");
if (solo && cantBlockAlone) {
if (solo && blocker.hasKeyword("CARDNAME can't attack or block alone.")) {
continue;
}
blockers.add(blocker);
@@ -103,13 +84,14 @@ public class AiBlockController {
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>();
// Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus,
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness unless we're simulating an outcome outside of real combat
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
for (final Card b : blockersLeft) {
if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, attacker.getGame().getPhaseHandler().inCombat())) {
if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, true)) {
blockers.add(b);
}
}
return blockers;
}
@@ -117,10 +99,10 @@ public class AiBlockController {
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>();
// Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus,
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness unless we're simulating an outcome outside of real combat
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
for (final Card b : blockersLeft) {
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, attacker.getGame().getPhaseHandler().inCombat())) {
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, true)) {
blockers.add(b);
}
}
@@ -131,6 +113,7 @@ public class AiBlockController {
private List<Card> sortPotentialAttackers(final Combat combat) {
final CardCollection sortedAttackers = new CardCollection();
CardCollection firstAttacker = new CardCollection();
final FCollectionView<GameEntity> defenders = combat.getDefenders();
// If I don't have any planeswalkers then sorting doesn't really matter
@@ -154,11 +137,13 @@ public class AiBlockController {
return attackers;
}
final boolean bLifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
// TODO Add creatures attacking Planeswalkers in order of which we want to protect
// defend planeswalkers with more loyalty before planeswalkers with less loyalty
// if planeswalker will be too difficult to defend don't even bother
for (GameEntity defender : defenders) {
if (defender instanceof Card && ((Card) defender).getController().equals(ai)) {
if (defender instanceof Card) {
final CardCollection attackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers);
@@ -168,7 +153,7 @@ public class AiBlockController {
}
}
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
if (bLifeInDanger) {
// add creatures attacking the Player to the front of the list
for (final Card c : firstAttacker) {
sortedAttackers.add(0, c);
@@ -180,16 +165,24 @@ public class AiBlockController {
return sortedAttackers;
}
// ======================= block assignment functions
// ================================
// Good Blocks means a good trade or no trade
private void makeGoodBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : attackersLeft) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)) {
continue;
}
Card blocker = null;
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
final List<Card> safeBlockers = getSafeBlockers(combat, attacker, blockers);
@@ -235,9 +228,9 @@ public class AiBlockController {
// 3.Blockers that can destroy the attacker and have an upside when dying
killingBlockers = getKillingBlockers(combat, attacker, blockers);
for (Card b : killingBlockers) {
if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterEnumType.P1P1) == 0) || b.hasSVar("SacMe")
|| (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0)
if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe")
|| (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|| b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
break;
@@ -268,13 +261,15 @@ public class AiBlockController {
}
if (mode == TriggerType.DamageDone) {
if (trigger.matchesValidParam("ValidSource", attacker)
if ((!trigParams.containsKey("ValidSource")
|| CardTraitBase.matchesValid(attacker, trigParams.get("ValidSource").split(","), attacker))
&& attacker.getNetCombatDamage() > 0
&& trigger.matchesValidParam("ValidTarget", combat.getDefenderByAttacker(attacker))) {
&& (!trigParams.containsKey("ValidTarget")
|| CardTraitBase.matchesValid(combat.getDefenderByAttacker(attacker), trigParams.get("ValidTarget").split(","), attacker))) {
value += 50;
}
} else if (mode == TriggerType.AttackerUnblocked) {
if (trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
if (CardTraitBase.matchesValid(attacker, trigParams.get("ValidCard").split(","), attacker)) {
value += 50;
}
}
@@ -290,20 +285,22 @@ public class AiBlockController {
combat.addBlocker(attacker, blocker);
}
}
attackersLeft = new ArrayList<>(currentAttackers);
attackersLeft = (new ArrayList<>(currentAttackers));
// 6. Blockers that don't survive until the next turn anyway
for (final Card attacker : attackersLeft) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
Card blocker = null;
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
for (Card b : blockers) {
if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0)
if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|| b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
@@ -317,49 +314,10 @@ public class AiBlockController {
combat.addBlocker(attacker, blocker);
}
}
attackersLeft = new ArrayList<>(currentAttackers);
attackersLeft = (new ArrayList<>(currentAttackers));
}
private Predicate<Card> rampagesOrNeedsManyToBlock(final Combat combat) {
return Predicates.or(CardPredicates.hasKeyword(Keyword.RAMPAGE), new Predicate<Card>() {
@Override
public boolean apply(Card input) {
// select creature that has a max blocker
return StaticAbilityCantAttackBlock.getMinMaxBlocker(input, combat.getDefenderPlayerByAttacker(input)).getRight() < Integer.MAX_VALUE;
}
});
}
private Predicate<Card> changesPTWhenBlocked(final boolean onlyForDefVsTrample) {
return new Predicate<Card>() {
@Override
public boolean apply(Card card) {
for (final Trigger tr : card.getTriggers()) {
if (tr.getMode() == TriggerType.AttackerBlocked) {
SpellAbility ab = tr.getOverridingAbility();
if (ab != null) {
if (ab.getApi() == ApiType.Pump && "Self".equals(ab.getParam("Defined"))) {
String rawP = ab.getParam("NumAtt");
String rawT = ab.getParam("NumDef");
if ("+X".equals(rawP) && "+X".equals(rawT) && "TriggerCount$NumBlockers".equals(card.getSVar("X"))) {
return true;
}
// TODO: maybe also predict calculated bonus above certain threshold?
} else if (ab.getApi() == ApiType.PumpAll && ab.hasParam("ValidCards")
&& ab.getParam("ValidCards").startsWith("Creature.blockingSource")) {
int pBonus = AbilityUtils.calculateAmount(card, ab.getParam("NumAtt"), ab);
int tBonus = AbilityUtils.calculateAmount(card, ab.getParam("NumDef"), ab);
return (!onlyForDefVsTrample && pBonus < 0) || tBonus < 0;
}
}
}
}
return false;
}
};
}
static final Predicate<Card> rampagesOrNeedsManyToBlock = Predicates.or(CardPredicates.containsKeyword("Rampage"), CardPredicates.containsKeyword("CantBeBlockedByAmount GT"));
// Good Gang Blocks means a good trade or no trade
/**
@@ -370,12 +328,12 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object.
*/
private void makeGangBlocks(final Combat combat) {
List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock));
List<Card> blockers;
// Try to block an attacker without first strike with a gang of first strikers
for (final Card attacker : attackersLeft) {
if (ComputerUtilCombat.combatantCantBeDestroyed(ai, attacker)) {
if (ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, attacker)) {
// don't bother with gang blocking if the attacker will regenerate or is indestructible
continue;
}
@@ -395,12 +353,13 @@ public class AiBlockController {
if (firstStrikeBlockers.size() > 1) {
CardLists.sortByPowerDesc(firstStrikeBlockers);
for (final Card blocker : firstStrikeBlockers) {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker, false)
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// if the total damage of the blockgang was not enough
// without but is enough with this blocker finish the blockgang
// without but is enough with this blocker finish the
// blockgang
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) < damageNeeded
|| CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size()) {
|| CombatUtil.needsBlockers(attacker) > blockGang.size()) {
blockGang.add(blocker);
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) >= damageNeeded) {
currentAttackers.remove(attacker);
@@ -416,22 +375,18 @@ public class AiBlockController {
}
}
attackersLeft = new ArrayList<>(currentAttackers);
attackersLeft = (new ArrayList<>(currentAttackers));
currentAttackers = new ArrayList<>(attackersLeft);
boolean considerTripleBlock = true;
// Try to block an attacker with two blockers of which only one will die
for (final Card attacker : attackersLeft) {
if (ComputerUtilCombat.combatantCantBeDestroyed(ai, attacker)) {
if (ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, attacker)) {
// don't bother with gang blocking if the attacker will regenerate or is indestructible
continue;
}
// AI can't handle good blocks with more than three creatures yet
if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
continue;
}
int evalAttackerValue = ComputerUtilCard.evaluateCreature(attacker);
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -441,6 +396,11 @@ public class AiBlockController {
int currentValue; // The value of the creatures in the blockgang
boolean foundDoubleBlock = false; // if true, a good double block is found
// AI can't handle good blocks with more than three creatures yet
if (CombatUtil.needsBlockers(attacker) > (considerTripleBlock ? 3 : 2)) {
continue;
}
// Try to add blockers that could be destroyed, but are worth less than the attacker
// Don't use blockers without First Strike or Double Strike if attacker has it
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@@ -450,7 +410,8 @@ public class AiBlockController {
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
return false;
}
return lifeInDanger || wouldLikeToRandomlyTrade(attacker, c, combat) || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker) || randomTrade;
}
});
if (usableBlockers.size() < 2) {
@@ -471,9 +432,9 @@ public class AiBlockController {
final int additionalDamage = ComputerUtilCombat.dealsDamageAsBlocker(attacker, blocker);
final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(blocker, attacker.getNetCombatDamage(), attacker, true);
final int addedValue = ComputerUtilCard.evaluateCreature(blocker);
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker, false)
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size())
&& !(damageNeeded > currentDamage + additionalDamage)
// The attacker will be killed
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
@@ -483,7 +444,8 @@ public class AiBlockController {
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
// or life is in danger
&& CombatUtil.canBlock(attacker, blocker, combat)) {
// this is needed for attackers that can't be blocked by more than 1
// this is needed for attackers that can't be blocked by
// more than 1
currentAttackers.remove(attacker);
combat.addBlocker(attacker, blocker);
if (CombatUtil.canBlock(attacker, leader, combat)) {
@@ -511,7 +473,7 @@ public class AiBlockController {
final int additionalDamage2 = ComputerUtilCombat.dealsDamageAsBlocker(attacker, secondBlocker);
final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(secondBlocker, attacker.getNetCombatDamage(), attacker, true);
final int addedValue2 = ComputerUtilCard.evaluateCreature(secondBlocker);
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker, false)
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, secondBlocker, combat, false);
List<Card> usableBlockersAsThird = new ArrayList<>(usableBlockers);
@@ -524,7 +486,7 @@ public class AiBlockController {
final int addedValue3 = ComputerUtilCard.evaluateCreature(secondBlocker);
final int netCombatDamage = attacker.getNetCombatDamage();
if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size())
&& !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
// The attacker will be killed
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
@@ -538,7 +500,8 @@ public class AiBlockController {
// or life is in danger
&& CombatUtil.canBlock(attacker, secondBlocker, combat)
&& CombatUtil.canBlock(attacker, thirdBlocker, combat)) {
// this is needed for attackers that can't be blocked by more than 1
// this is needed for attackers that can't be blocked by
// more than 1
currentAttackers.remove(attacker);
combat.addBlocker(attacker, thirdBlocker);
if (CombatUtil.canBlock(attacker, secondBlocker, combat)) {
@@ -553,7 +516,7 @@ public class AiBlockController {
}
}
attackersLeft = new ArrayList<>(currentAttackers);
attackersLeft = (new ArrayList<>(currentAttackers));
}
private void makeGangNonLethalBlocks(final Combat combat) {
@@ -562,19 +525,19 @@ public class AiBlockController {
// Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) <= 1) {
if (!attacker.hasKeyword(Keyword.MENACE) && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
continue;
}
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
List<Card> usableBlockers;
final List<Card> blockGang = new ArrayList<>();
int absorbedDamage; // The amount of damage needed to kill the first blocker
List<Card> usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getNetToughness() > attacker.getNetCombatDamage() // performance shortcut
|| c.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, c, true) > attacker.getNetCombatDamage();
return c.getNetToughness() > attacker.getNetCombatDamage();
}
});
if (usableBlockers.size() < 2) {
@@ -601,7 +564,7 @@ public class AiBlockController {
}
}
attackersLeft = new ArrayList<>(currentAttackers);
attackersLeft = (new ArrayList<>(currentAttackers));
}
// Bad Trade Blocks (should only be made if life is in danger)
@@ -614,11 +577,15 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object.
*/
private void makeTradeBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
List<Card> killingBlockers;
for (final Card attacker : attackersLeft) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
@@ -646,11 +613,12 @@ public class AiBlockController {
}
}
}
attackersLeft = new ArrayList<>(currentAttackers);
attackersLeft = (new ArrayList<>(currentAttackers));
}
// Chump Blocks (should only be made if life is in danger)
private void makeChumpBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
makeChumpBlocks(combat, currentAttackers);
@@ -661,14 +629,17 @@ public class AiBlockController {
}
private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
if (attackers.isEmpty() || !ComputerUtilCombat.lifeInDanger(ai, combat)) {
return;
}
Card attacker = attackers.get(0);
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > 1
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0);
makeChumpBlocks(combat, attackers);
@@ -713,10 +684,14 @@ public class AiBlockController {
// Block creatures with "can't be blocked except by two or more creatures"
private void makeMultiChumpBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : currentAttackers) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) <= 1) {
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
&& !attacker.hasKeyword(Keyword.MENACE)
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
@@ -745,17 +720,19 @@ public class AiBlockController {
/** Reinforce blockers blocking attackers with trample (should only be made if life is in danger) */
private void reinforceBlockersAgainstTrample(final Combat combat) {
List<Card> chumpBlockers;
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
// 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, Predicates.not(changesPTWhenBlocked(true)));
// TODO - should check here for a "rampage-like" trigger that replaced
// the keyword:
// "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
for (final Card attacker : tramplingAttackers) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()
if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) && !combat.isBlocked(attacker))
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
@@ -777,13 +754,13 @@ public class AiBlockController {
/** Support blockers not destroying the attacker with more blockers to try to kill the attacker */
private void reinforceBlockersToKill(final Combat combat) {
List<Card> safeBlockers;
List<Card> blockers;
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock(combat)));
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock));
// 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, Predicates.not(changesPTWhenBlocked(false)));
// TODO - should check here for a "rampage-like" trigger that replaced
// the keyword: "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
for (final Card attacker : targetAttackers) {
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -801,7 +778,7 @@ public class AiBlockController {
if (blockers.size() > 0) {
safeBlockers = getSafeBlockers(combat, attacker, blockers);
for (final Card blocker : safeBlockers) {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker, false)
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// Add an additional blocker if the current blockers are not
// enough and the new one would deal additional damage
@@ -814,7 +791,7 @@ public class AiBlockController {
}
}
// don't try to kill what can't be killed
if (ComputerUtilCombat.combatantCantBeDestroyed(ai, attacker)) {
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, attacker)) {
continue;
}
@@ -828,7 +805,7 @@ public class AiBlockController {
}
for (final Card blocker : safeBlockers) {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker, false)
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// Add an additional blocker if the current blockers are not
// enough and the new one would deal the remaining damage
@@ -847,7 +824,7 @@ public class AiBlockController {
}
private void makeChumpBlocksToSavePW(Combat combat) {
if (ai.getLife() <= ai.getStartingLife() / 5 || ComputerUtilCombat.lifeInDanger(ai, combat)) {
if (ComputerUtilCombat.lifeInDanger(ai, combat) || ai.getLife() <= ai.getStartingLife() / 5) {
// most likely not worth trying to protect planeswalkers when at threateningly low life or in
// dangerous combat which threatens lethal or severe damage to face
return;
@@ -855,7 +832,7 @@ public class AiBlockController {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {
@@ -871,10 +848,10 @@ public class AiBlockController {
int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) {
if (!combat.isBlocked(pwatkr)) {
damageToPW += ComputerUtilCombat.predictDamageTo(def, pwatkr.getNetCombatDamage(), pwatkr, true);
damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true);
}
}
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterEnumType.LOYALTY)) {
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterType.LOYALTY)) {
threatenedPWs.add((Card) def);
}
}
@@ -883,7 +860,7 @@ public class AiBlockController {
CardCollection pwsWithChumpBlocks = new CardCollection();
CardCollection chosenChumpBlockers = new CardCollection();
CardCollection chumpPWDefenders = CardLists.filter(this.blockersLeft, new Predicate<Card>() {
CardCollection chumpPWDefenders = CardLists.filter(new CardCollection(this.blockersLeft), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken
@@ -932,7 +909,7 @@ public class AiBlockController {
damageToPW += ComputerUtilCombat.predictDamageTo(pw, pwAtk.getNetCombatDamage(), pwAtk, true);
}
}
if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterEnumType.LOYALTY)) {
if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterType.LOYALTY)) {
for (Card chump : pwDefenders) {
if (chosenChumpBlockers.contains(chump)) {
combat.removeFromCombat(chump);
@@ -946,9 +923,11 @@ public class AiBlockController {
}
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) {
for (final Card blocker : CardLists.filterControlledBy(combat.getAllBlockers(), ai)) {
// don't touch other player's blockers
combat.removeFromCombat(blocker);
final List<Card> oldBlockers = combat.getAllBlockers();
for (final Card blocker : oldBlockers) {
if (blocker.getController() == ai) // don't touch other player's blockers
combat.removeFromCombat(blocker);
}
attackersLeft = new ArrayList<>(attackers); // keeps track of all currently unblocked attackers
@@ -958,16 +937,11 @@ public class AiBlockController {
/** Assigns blockers for the provided combat instance (in favor of player passes to ctor) */
public void assignBlockersForCombat(final Combat combat) {
assignBlockersForCombat(combat, null);
}
public void assignBlockersForCombat(final Combat combat, final CardCollection exludedBlockers) {
List<Card> possibleBlockers = ai.getCreaturesInPlay();
if (exludedBlockers != null && !exludedBlockers.isEmpty()) {
possibleBlockers.removeAll(exludedBlockers);
}
attackers = sortPotentialAttackers(combat);
assignBlockers(combat, possibleBlockers);
}
/**
* assignBlockersForCombat() with additional and possibly "virtual" blockers.
* @param combat combat instance
@@ -1023,10 +997,6 @@ public class AiBlockController {
}
}
if (attackersLeft.isEmpty()) {
return;
}
// remove all blockers that can't block anyway
for (final Card b : possibleBlockers) {
if (!CombatUtil.canBlock(b, combat)) {
@@ -1034,6 +1004,10 @@ public class AiBlockController {
}
}
if (attackersLeft.isEmpty()) {
return;
}
// Begin with the weakest blockers
CardLists.sortByPowerAsc(blockersLeft);
@@ -1042,7 +1016,7 @@ public class AiBlockController {
makeGangBlocks(combat);
// When the AI holds some Fog effect, don't bother about lifeInDanger
if (!ComputerUtil.hasAFogEffect(ai, checkingOther)) {
if (!ComputerUtil.hasAFogEffect(ai)) {
lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
makeTradeBlocks(combat); // choose necessary trade blocks
@@ -1052,25 +1026,29 @@ public class AiBlockController {
} else {
lifeInDanger = false;
}
// Reinforce blockers blocking attackers with trample if life is still in danger
// if life is still in danger
// Reinforce blockers blocking attackers with trample if life is
// still
// in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat);
} else {
lifeInDanger = false;
}
// Support blockers not destroying the attacker with more blockers
// to try to kill the attacker
// to
// try to kill the attacker
if (!lifeInDanger) {
reinforceBlockersToKill(combat);
}
// TODO could be made more accurate if this would be inside each blocker choosing loop instead
lifeInDanger |= removeUnpayableBlocks(combat);
// == 2. If the AI life would still be in danger make a safer approach ==
// == 2. If the AI life would still be in danger make a safer
// approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); // reset every block assignment
clearBlockers(combat, possibleBlockers); // reset every block
// assignment
makeTradeBlocks(combat); // choose necessary trade blocks
// if life is in danger
makeGoodBlocks(combat);
// choose necessary chump blocks if life is still in danger
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
@@ -1078,7 +1056,8 @@ public class AiBlockController {
} else {
lifeInDanger = false;
}
// Reinforce blockers blocking attackers with trample if life is still in danger
// Reinforce blockers blocking attackers with trample if life is
// still in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat);
} else {
@@ -1088,29 +1067,38 @@ public class AiBlockController {
reinforceBlockersToKill(combat);
}
// == 3. If the AI life would be in serious danger make an even safer approach ==
// == 3. If the AI life would be in serious danger make an even
// safer approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers);
makeChumpBlocks(combat);
clearBlockers(combat, possibleBlockers); // reset every block
// assignment
makeChumpBlocks(combat); // choose chump blocks
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeTradeBlocks(combat);
makeTradeBlocks(combat); // choose necessary trade
}
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeGoodBlocks(combat);
} else {
}
// Reinforce blockers blocking attackers with trample if life is
// still in danger
else {
reinforceBlockersAgainstTrample(combat);
}
makeGangBlocks(combat);
// Support blockers not destroying the attacker with more
// blockers
// to try to kill the attacker
reinforceBlockersToKill(combat);
}
}
// assign blockers that have to block
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able.");
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each turn if able.");
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
// if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
chumpBlockers.add(blocker);
}
}
@@ -1120,10 +1108,11 @@ public class AiBlockController {
blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false);
for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
&& (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
&& (CombatUtil.mustBlockAnAttacker(blocker, combat)
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker);
if (!blocker.getMustBlockCards().isEmpty()) {
if (blocker.getMustBlockCards() != null) {
int mustBlockAmt = blocker.getMustBlockCards().size();
final CardCollectionView blockedSoFar = combat.getAttackersBlockedBy(blocker);
boolean canBlockAnother = CombatUtil.canBlockMoreCreatures(blocker, blockedSoFar);
@@ -1138,9 +1127,10 @@ public class AiBlockController {
}
}
// check to see if it's possible to defend a Planeswalker under attack with a chump block,
// unless life is low enough to be more worried about saving preserving the life total
if (ai.getController().isAI()) {
if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeChumpBlocksToSavePW(combat);
}
@@ -1151,9 +1141,9 @@ public class AiBlockController {
//Check for validity of blocks in case something slipped through
for (Card attacker : attackers) {
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
for (final Card blocker : CardLists.filterControlledBy(combat.getBlockers(attacker), ai)) {
// don't touch other player's blockers
combat.removeFromCombat(blocker);
for (final Card blocker : combat.getBlockers(attacker)) {
if (blocker.getController() == ai) // don't touch other player's blockers
combat.removeFromCombat(blocker);
}
}
}
@@ -1201,7 +1191,7 @@ public class AiBlockController {
final CardCollection result = new CardCollection();
boolean newBlockerIsAdded = false;
// The new blocker comes right after this one
final Card newBlockerRightAfter = newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1);
final Card newBlockerRightAfter = (newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1));
if (newBlockerRightAfter == null
&& damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
result.add(blocker);
@@ -1269,19 +1259,16 @@ public class AiBlockController {
int oppCreatureCount = 0;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
// simulation must get same results or it may crash
if (!aic.usesSimulation()) {
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);
maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE);
maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL);
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
}
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);
maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE);
maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL);
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
}
if (!enableRandomTrades) {
@@ -1294,14 +1281,13 @@ public class AiBlockController {
oppCreatureCount = ComputerUtil.countUsefulCreatures(attackersLeft.get(0).getController());
}
if (attacker != null && attacker.getOwner() != null)
if (attacker.getOwner().equals(ai) && "6".equals(attacker.getSVar("SacMe"))) {
if (attacker.getOwner().equals(ai) && "6".equals(attacker.getSVar("SacMe"))) {
// Temporarily controlled object - don't trade with it
// TODO: find a more reliable way to figure out that control will be reestablished next turn
return false;
}
int numSteps = Math.max(1, ai.getStartingLife() - 5); // e.g. 15 steps between 5 life and 20 life
int numSteps = ai.getStartingLife() - 5; // e.g. 15 steps between 5 life and 20 life
float chanceStep = (maxRandomTradeChance - minRandomTradeChance) / numSteps;
int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep));
if (chance > maxRandomTradeChance) {
@@ -1309,6 +1295,7 @@ public class AiBlockController {
}
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false);
int evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
boolean atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
@@ -1317,20 +1304,17 @@ public class AiBlockController {
chance = Math.max(0, chance - chanceModForEmbalm);
}
int evalBlk;
if (blocker.isFaceDown() && blocker.getView().canFaceDownBeShownTo(ai.getView(), false) && blocker.getState(CardStateName.Original).getType().isCreature()) {
if (blocker.isFaceDown() && blocker.getState(CardStateName.Original).getType().isCreature()) {
// if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it
// in relation to the original state, not to the Morph state
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true);
} else {
evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
}
int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();
boolean creatureParityOrAllowedDiff = aiCreatureCount
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
boolean wantToTradeWithCreatInHand = !checkingOther && randomTradeIfCreatInHand
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.Presets.CREATURES)
boolean wantToTradeWithCreatInHand = randomTradeIfCreatInHand
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).isEmpty()
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
&& combat.getDefenderByAttacker(attacker) instanceof Card
@@ -1342,25 +1326,4 @@ public class AiBlockController {
&& (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand)
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker);
}
private boolean removeUnpayableBlocks(final Combat combat) {
int myFreeMana = ComputerUtilMana.getAvailableManaEstimate(ai);
int currentBlockTax = 0;
List<Card> oldBlockers = CardLists.filterControlledBy(combat.getAllBlockers(), ai);
CardLists.sortByPowerDesc(oldBlockers);
boolean modified = false;
for (final Card blocker : oldBlockers) {
// TODO check all blocked attackers
Cost tax = CombatUtil.getBlockCost(blocker.getGame(), blocker, combat.getAttackersBlockedBy(blocker).get(0));
int taxCMC = tax != null ? tax.getCostMana().getMana().getCMC() : 0;
if (myFreeMana < currentBlockTax + taxCMC) {
combat.removeFromCombat(blocker);
modified = true;
continue;
}
currentBlockTax += taxCMC;
}
return modified;
}
}

View File

@@ -18,13 +18,13 @@
package forge.ai;
import forge.game.card.Card;
import forge.game.player.Player;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import forge.game.card.Card;
import forge.game.player.Player;
/**
* <p>
* AiCardMemory class.
@@ -57,9 +57,7 @@ public class AiCardMemory {
BOUNCED_THIS_TURN, // These cards were bounced this turn
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part
PAYS_SAC_COST // These cards will be sacrificed as part of a cost and cannot be chosen in another part
MARKED_TO_AVOID_REENTRY // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
//REVEALED_CARDS // stub, not linked to AI code yet
}
@@ -75,8 +73,6 @@ public class AiCardMemory {
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;
public AiCardMemory() {
this.memMandatoryAttackers = new HashSet<>();
@@ -91,8 +87,6 @@ public class AiCardMemory {
this.memChosenFogEffect = new HashSet<>();
this.memMarkedToAvoidReentry = new HashSet<>();
this.memHeldManaSourcesForNextSpell = new HashSet<>();
this.memPaysTapCost = new HashSet<>();
this.memPaysSacCost = new HashSet<>();
}
private Set<Card> getMemorySet(MemorySet set) {
@@ -121,10 +115,6 @@ public class AiCardMemory {
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:
@@ -323,12 +313,6 @@ public class AiCardMemory {
}
// Static functions to simplify access to AI card memory of a given AI player.
public static Set<Card> getMemorySet(Player ai, MemorySet set) {
if (!ai.getController().isAI()) {
return null;
}
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().getMemorySet(set);
}
public static void rememberCard(Player ai, Card c, MemorySet set) {
if (!ai.getController().isAI()) {
return;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,19 +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.
@@ -78,6 +78,7 @@ public class AiProfileUtil {
List<String> lines = FileUtil.readFile(buildFileName(profileName));
for (String line : lines) {
if (line.startsWith("#") || (line.length() == 0)) {
continue;
}
@@ -119,7 +120,8 @@ public class AiProfileUtil {
* @return ArrayList<String> - an array of strings containing all
* available profiles.
*/
public static List<String> getAvailableProfiles() {
public static List<String> getAvailableProfiles()
{
final List<String> availableProfiles = new ArrayList<>();
final File dir = new File(AI_PROFILE_DIR);

View File

@@ -35,7 +35,6 @@ public enum AiProps { /** */
MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"),
MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"),
PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"),
SAC_TO_REATTACH_TARGET_EVAL_THRESHOLD ("400"),
PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */
RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */
PLAY_AGGRO ("false"),
@@ -75,7 +74,6 @@ public enum AiProps { /** */
ALWAYS_COPY_SPELL_IF_CMC_DIFF ("2"), /** */
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
ACTIVELY_PROTECT_VS_CURSE_AURAS("false"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,7 @@
package forge.ai;
import java.util.Iterator;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GameActionUtil;
@@ -21,6 +17,9 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import java.util.Iterator;
import java.util.List;
public class ComputerUtilAbility {
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
@@ -36,7 +35,7 @@ public class ComputerUtilAbility {
public boolean apply(final Card c) {
if (!c.getSVar("NeedsToPlay").isEmpty()) {
final String needsToPlay = c.getSVar("NeedsToPlay");
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), needsToPlay, c.getController(), c, null);
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), needsToPlay.split(","), c.getController(), c, null);
if (list.isEmpty()) {
return false;
}
@@ -72,15 +71,24 @@ public class ComputerUtilAbility {
if (!player.getCardsIn(ZoneType.Library).isEmpty()) {
all.add(player.getCardsIn(ZoneType.Library).get(0));
}
all.addAll(game.getPlayers().getCardsIn(ZoneType.Exile));
all.addAll(game.getPlayers().getCardsIn(ZoneType.Battlefield));
for(Player p : game.getPlayers()) {
all.addAll(p.getCardsIn(ZoneType.Exile));
all.addAll(p.getCardsIn(ZoneType.Battlefield));
}
return all;
}
public static List<SpellAbility> getSpellAbilities(final CardCollectionView l, final Player player) {
final List<SpellAbility> spellAbilities = Lists.newArrayList();
for (final Card c : l) {
spellAbilities.addAll(c.getAllPossibleAbilities(player, false));
for (final SpellAbility sa : c.getSpellAbilities()) {
spellAbilities.add(sa);
}
if (c.isFaceDown() && c.isInZone(ZoneType.Exile) && !c.mayPlay(player).isEmpty()) {
for (final SpellAbility sa : c.getState(CardStateName.Original).getSpellAbilities()) {
spellAbilities.add(sa);
}
}
}
return spellAbilities;
}
@@ -101,7 +109,9 @@ public class ComputerUtilAbility {
List<SpellAbility> priorityAltSa = Lists.newArrayList();
List<SpellAbility> otherAltSa = Lists.newArrayList();
for (SpellAbility altSa : saAltCosts) {
if (sa.getPayCosts().isOnlyManaCost()
if (altSa.getPayCosts() == null || sa.getPayCosts() == null) {
otherAltSa.add(altSa);
} else if (sa.getPayCosts().isOnlyManaCost()
&& altSa.getPayCosts().isOnlyManaCost() && sa.getPayCosts().getTotalMana().compareTo(altSa.getPayCosts().getTotalMana()) == 1) {
// the alternative cost is strictly cheaper, so why not? (e.g. Omniscience etc.)
priorityAltSa.add(altSa);
@@ -212,15 +222,4 @@ public class ComputerUtilAbility {
return targeted;
}
public static boolean isFullyTargetable(SpellAbility sa) {
SpellAbility sub = sa;
while (sub != null) {
if (sub.usesTargeting() && sub.getTargetRestrictions().getNumCandidates(sub, true) < sub.getMinTargets()) {
return false;
}
sub = sub.getSubAbility();
}
return true;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,24 @@
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 com.google.common.collect.Sets;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi;
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.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
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;
@@ -52,7 +45,7 @@ public class ComputerUtilCost {
final CostPutCounter addCounter = (CostPutCounter) part;
final CounterType type = addCounter.getCounter();
if (type.is(CounterEnumType.M1M1)) {
if (type.equals(CounterType.M1M1)) {
return false;
}
}
@@ -60,6 +53,9 @@ public class ComputerUtilCost {
return true;
}
public static boolean checkRemoveCounterCost(final Cost cost, final Card source) {
return checkRemoveCounterCost(cost, source, null);
}
/**
* Check remove counter cost.
*
@@ -73,14 +69,13 @@ public class ComputerUtilCost {
if (cost == null) {
return true;
}
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
if (type.is(CounterEnumType.P1P1)) {
if (CounterType.P1P1.equals(type)) {
return false;
}
continue;
@@ -91,8 +86,19 @@ public class ComputerUtilCost {
return false;
}
// Remove X counters - set ChosenX to max possible value here, the SAs should correct that
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
final String sVar = sa.getSVar(remCounter.getAmount());
if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) {
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
}
}
// check the sa what the PaymentDecision is.
// ignore Loyality abilities with Zero as Cost
if (!type.is(CounterEnumType.LOYALTY)) {
if (sa != null && !CounterType.LOYALTY.equals(type)) {
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa);
PaymentDecision pay = decision.visit(remCounter);
if (pay == null || pay.c <= 0) {
return false;
@@ -100,15 +106,19 @@ public class ComputerUtilCost {
}
//don't kill the creature
if (type.is(CounterEnumType.P1P1) && source.getLethalDamage() <= 1
if (CounterType.P1P1.equals(type) && source.getLethalDamage() <= 1
&& !source.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (part instanceof CostRemoveAnyCounter) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
if (sa != null) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision pay = decision.visit(remCounter);
return pay != null;
PaymentDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa).visit(remCounter);
return decision != null;
}
return false;
}
}
return true;
@@ -123,7 +133,7 @@ public class ComputerUtilCost {
* the source
* @return true, if successful
*/
public static boolean checkDiscardCost(final Player ai, final Cost cost, final Card source, SpellAbility sa) {
public static boolean checkDiscardCost(final Player ai, final Cost cost, final Card source) {
if (cost == null) {
return true;
}
@@ -135,28 +145,23 @@ public class ComputerUtilCost {
final CostDiscard disc = (CostDiscard) part;
final String type = disc.getType();
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;
}
if (type.equals("CARDNAME") && source.getAbilityText().contains("Bloodrush")) {
continue;
}
final CardCollection typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
final CardCollection typeList = CardLists.getValidCards(hand, type.split(","), source.getController(), source, null);
if (typeList.size() > ai.getMaxHandSize()) {
continue;
}
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
if (pref == null) {
return false;
} else {
typeList.remove(pref);
hand.remove(pref);
}
typeList.remove(pref);
hand.remove(pref);
}
}
}
@@ -233,51 +238,6 @@ public class ComputerUtilCost {
return true;
}
public static boolean checkForManaSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility, final boolean effect) {
// TODO cheating via autopay can still happen, need to get the real ai player from controlledBy
if (cost == null || !ai.isAI()) {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
CardCollection list = new CardCollection();
final CardCollection exclude = new CardCollection();
if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_SAC_COST) != null) {
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_SAC_COST));
}
if (part.payCostFromSource()) {
list.add(source);
} else if (part.getType().equals("OriginalHost")) {
list.add(sourceAbility.getOriginalHost());
} else if (part.getAmount().equals("All")) {
// Does the AI want to use Sacrifice All?
return false;
} else {
final String amount = part.getAmount();
Integer c = part.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, amount, sourceAbility);
}
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
if (choices != null) {
list.addAll(choices);
}
}
list.removeAll(exclude);
if (list.isEmpty()) {
return false;
}
for (Card choice : list) {
AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_SAC_COST);
}
return true;
}
}
return true;
}
/**
* Check creature sacrifice cost.
*
@@ -306,10 +266,7 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
int count = 0;
while (count < amount) {
@@ -359,14 +316,11 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
int count = 0;
while (count < amount) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList, sourceAbility);
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
if (prefCard == null) {
return false;
}
@@ -379,31 +333,18 @@ public class ComputerUtilCost {
return true;
}
/**
* Check sacrifice cost.
*
* @param cost
* the cost
* @param source
* the source
* @return true, if successful
*/
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, sourceAbility, true);
}
public static boolean isSacrificeSelfCost(final Cost cost) {
if (cost == null) {
return false;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
if ("CARDNAME".equals(part.getType())) {
return true;
}
}
}
return false;
if (cost == null) {
return false;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
if ("CARDNAME".equals(part.getType())) {
return true;
}
}
}
return false;
}
/**
@@ -421,18 +362,17 @@ public class ComputerUtilCost {
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostTapType) {
String type = part.getType();
/*
* Only crew with creatures weaker than vehicle
*
*
* Possible improvements:
* - block against evasive (flyers, intimidate, etc.)
* - break board stall by racing with evasive vehicle
*/
if (sa.hasParam("Crew")) {
Card vehicle = AnimateAi.becomeAnimated(source, sa);
Card vehicle = AnimateAi.becomeAnimated(source, sa);
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
String type = part.getType();
String totalP = type.split("withTotalPowerGE")[1];
type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
CardCollection exclude = CardLists.getValidCards(
@@ -447,41 +387,25 @@ public class ComputerUtilCost {
return ComputerUtil.chooseTapTypeAccumulatePower(ai, type, sa, true,
Integer.parseInt(totalP), exclude) != null;
}
// check if we have a valid card to tap (e.g. Jaspera Sentinel)
Integer c = part.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, part.getAmount(), sa);
}
CardCollection exclude = new CardCollection();
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()) {
return false;
}
// if we want to pay for an ability with tapping the source can't be chosen
if (sa.getPayCosts().hasTapCost()) {
exclude.add(sa.getHostCard());
}
CardCollection tapChoices = ComputerUtil.chooseTapType(ai, type, source, cost.hasTapCost(), c, exclude, sa);
if (tapChoices != null) {
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;
}
return false;
return false;
}
}
return true;
}
/**
* Check sacrifice cost.
*
* @param cost
* the cost
* @param source
* the source
* @return true, if successful
*/
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, sourceAbility,true);
}
/**
* <p>
* shouldPayCost.
@@ -492,8 +416,8 @@ public class ComputerUtilCost {
* @param cost
* @return a boolean.
*/
@Deprecated
public static boolean shouldPayCost(final Player ai, final Card hostCard, final Cost cost) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
if (!ai.cantLoseForZeroOrLessLife()) {
@@ -523,7 +447,7 @@ public class ComputerUtilCost {
* a {@link forge.game.player.Player} object.
* @return a boolean.
*/
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
public static boolean canPayCost(final SpellAbility sa, final Player player) {
if (sa.getActivatingPlayer() == null) {
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
}
@@ -543,22 +467,22 @@ public class ComputerUtilCost {
if(!meetsRestriction)
continue;
if (StringUtils.isNumeric(parts[0])) {
try {
extraManaNeeded += Integer.parseInt(parts[0]);
} else {
} catch (final NumberFormatException e) {
System.out.println("wrong SpellsNeedExtraMana SVar format on " + c);
}
}
}
for (Card c : player.getCardsIn(ZoneType.Command)) {
if (cannotBeCountered) {
continue;
}
if (cannotBeCountered) {
continue;
}
final String snem = c.getSVar("SpellsNeedExtraManaEffect");
if (!StringUtils.isBlank(snem)) {
if (StringUtils.isNumeric(snem)) {
try {
extraManaNeeded += Integer.parseInt(snem);
} else {
} catch (final NumberFormatException e) {
System.out.println("wrong SpellsNeedExtraManaEffect SVar format on " + c);
}
}
@@ -585,19 +509,7 @@ public class ComputerUtilCost {
if (sa.hasParam("Crew")) { // put under checkTapTypeCost?
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostTapType && part.getType().contains("+withTotalPowerGE")) {
return new AiCostDecision(player, sa, false).visit((CostTapType)part) != null;
}
}
}
// Ward - will be accounted for when rechecking a targeted ability
if (!(sa instanceof WrappedAbility) && sa.usesTargeting()) {
for (Card tgt : sa.getTargets().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();
}
return new AiCostDecision(player, sa).visit((CostTapType)part) != null;
}
}
}
@@ -617,7 +529,7 @@ public class ComputerUtilCost {
public boolean apply(Card card) {
boolean hasManaSa = false;
for (final SpellAbility sa : card.getSpellAbilities()) {
if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) {
if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
hasManaSa = true;
break;
}
@@ -632,8 +544,8 @@ public class ComputerUtilCost {
}
}
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
} // canPayCost()
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
@@ -642,6 +554,7 @@ public class ComputerUtilCost {
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payNever = "Never".equals(aiLogic);
boolean shockland = "Shockland".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
@@ -653,9 +566,29 @@ public class ComputerUtilCost {
return false;
}
} else if ("OnlyDontControl".equals(aiLogic)) {
if (source == null || payer.equals(source.getController())) {
if (sa.getHostCard() == null || payer.equals(sa.getHostCard().getController())) {
return false;
}
} else if (shockland) {
if (payer.getLife() > 3 && payer.canPayLife(2)) {
final int landsize = payer.getLandsInPlay().size() + 1;
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
// if the new land size would equal the CMC of a card in AIs hand, consider playing it untapped,
// otherwise don't bother running other checks
if (landsize != c.getCMC()) {
continue;
}
// try to determine in the AI is actually planning to play a spell ability from the card
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
// try to determine if the mana shards provided by the lands would be applicable to pay the mana cost
boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
if (canPay && willPlay) {
return true;
}
}
}
return false;
} else if ("Paralyze".equals(aiLogic)) {
final Card c = source.getEnchantingCard();
if (c == null || c.isUntapped()) {
@@ -663,7 +596,7 @@ public class ComputerUtilCost {
}
} else if ("RiskFactor".equals(aiLogic)) {
final Player activator = sa.getActivatingPlayer();
if (!activator.canDraw()) {
if (!activator.canDraw() || activator.hasKeyword("You can't draw more than one card each turn.")) {
return false;
}
} else if ("MorePowerful".equals(aiLogic)) {
@@ -673,10 +606,10 @@ public class ComputerUtilCost {
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)) {
// if payer can't lose life its no need to pay unless
if (!payer.canLoseLife())
return false;
else if (payer.getLife() <= Integer.valueOf(aiLogic.substring(6))) {
return true;
}
} else if ("WillAttack".equals(aiLogic)) {
@@ -686,36 +619,12 @@ public class ComputerUtilCost {
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()) {
} else if ("nonToken".equals(aiLogic) && AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) {
return false;
} else if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) {
return false;
}
// 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());
return canPay && willPlay;
}
}
}
}
}
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
return false;
@@ -725,18 +634,19 @@ public class ComputerUtilCost {
// 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)
&& (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));
&& checkDamageCost(payer, cost, source, 4)
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source))
&& (!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), Presets.UNTAPPED);
Set<String> colorsAvailable = Sets.newHashSet();
@@ -766,71 +676,4 @@ public class ComputerUtilCost {
}
return false;
}
public static int getMaxXValue(SpellAbility sa, Player ai, final boolean effect) {
final Card source = sa.getHostCard();
SpellAbility root = sa.getRootAbility();
final Cost abCost = root.getPayCosts();
if (abCost == null || !abCost.hasXInAnyCostPart()) {
return 0;
}
Integer val = null;
if (root.costHasManaX()) {
val = ComputerUtilMana.determineLeftoverMana(root, ai, effect);
}
if (sa.usesTargeting()) {
// if announce is used as min targets, check what the max possible number would be
if ("X".equals(sa.getTargetRestrictions().getMinTargets())) {
val = ObjectUtils.min(val, CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa).size());
}
if (sa.hasParam("AIMaxTgtsCount")) {
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified
// TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable?
val = ObjectUtils.min(val, AbilityUtils.calculateAmount(source, "Count$" + sa.getParam("AIMaxTgtsCount"), sa));
}
}
val = ObjectUtils.min(val, abCost.getMaxForNonManaX(root, ai, false));
if (val != null && val > 0) {
// filter cost parts for preferences, don't choose X > than possible preferences
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice) {
if (part.payCostFromSource()) {
continue;
}
if (!part.getAmount().equals("X")) {
continue;
}
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), part.getType().split(";"), source.getController(), source, sa);
int count = 0;
while (count < val) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
if (prefCard == null) {
break;
}
typeList.remove(prefCard);
count++;
}
val = ObjectUtils.min(val, count);
}
}
}
return ObjectUtils.defaultIfNull(val, 0);
}
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
if (source.usesTargeting()) {
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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,19 +2,23 @@ package forge.ai;
import com.google.common.base.Function;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityMustAttack;
import java.util.List;
public class CreatureEvaluator implements Function<Card, Integer> {
protected int getEffectivePower(final Card c) {
return c.getNetCombatDamage();
}
protected int getEffectiveToughness(final Card c) {
return c.getNetToughness();
}
@Override
public Integer apply(Card c) {
return evaluateCreature(c);
@@ -23,35 +27,32 @@ public class CreatureEvaluator implements Function<Card, Integer> {
public int evaluateCreature(final Card c) {
return evaluateCreature(c, true, true);
}
public int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
int value = 80;
if (!c.isToken()) {
value += addValue(20, "non-token"); // tokens should be worth less than actual cards
}
int power = c.getNetCombatDamage();
final int toughness = c.getNetToughness();
// TODO replace with ReplacementEffect checks
if (c.hasKeyword("Prevent all combat damage that would be dealt by CARDNAME.")
|| c.hasKeyword("Prevent all damage that would be dealt by CARDNAME.")
|| c.hasKeyword("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| c.hasKeyword("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
power = 0;
int power = getEffectivePower(c);
final int toughness = getEffectiveToughness(c);
for (KeywordInterface kw : c.getKeywords()) {
String keyword = kw.getOriginal();
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.equals("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
power = 0;
break;
}
}
if (considerPT) {
value += addValue(power * 15, "power");
value += addValue(toughness * 10, "toughness: " + toughness);
// because backside is always stronger the potential makes it better than a single faced card
if (c.hasKeyword(Keyword.DAYBOUND)) {
value += addValue(power * 10, "transforming");
}
}
if (considerCMC) {
value += addValue(c.getCMC() * 5, "cmc");
}
// Evasion keywords
if (c.hasKeyword(Keyword.FLYING)) {
value += addValue(power * 10, "flying");
@@ -74,11 +75,11 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword(Keyword.MENACE)) {
value += addValue(power * 4, "menace");
}
if (c.hasKeyword(Keyword.SKULK)) {
value += addValue(power * 3, "skulk");
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
value += addValue(power * 3, "block-restrict");
}
}
// Other good keywords
if (power > 0) {
if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
@@ -98,28 +99,29 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword(Keyword.VIGILANCE)) {
value += addValue((power * 5) + (toughness * 5), "vigilance");
}
if (c.hasKeyword(Keyword.WITHER)) {
value += addValue(power * 10, "Wither");
}
if (c.hasKeyword(Keyword.INFECT)) {
value += addValue(power * 15, "infect");
}
else if (c.hasKeyword(Keyword.WITHER)) {
value += addValue(power * 10, "Wither");
}
value += addValue(c.getKeywordMagnitude(Keyword.RAMPAGE), "rampage");
value += addValue(c.getKeywordMagnitude(Keyword.AFFLICT) * 5, "afflict");
}
value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido");
value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking");
value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted");
value += addValue(c.getKeywordMagnitude(Keyword.ANNIHILATOR) * 50, "eldrazi");
value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb");
// Keywords that may produce temporary or permanent buffs over time
if (c.hasKeyword(Keyword.PROWESS)) {
value += addValue(5, "prowess");
}
if (c.hasKeyword(Keyword.OUTLAST)) {
value += addValue(10, "outlast");
}
value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido");
value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking");
value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted");
value += addValue(c.getAmountOfKeyword(Keyword.MELEE) * 18, "melee");
value += addValue(c.getAmountOfKeyword(Keyword.PROWESS) * 5, "prowess");
// Defensive Keywords
if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) {
@@ -128,7 +130,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
value += addValue(3, "shadow-block");
}
// Protection
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
value += addValue(70, "darksteel");
@@ -142,69 +144,36 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(35, "hexproof");
} else if (c.hasKeyword(Keyword.SHROUD)) {
value += addValue(30, "shroud");
} else if (c.hasKeyword(Keyword.WARD)) {
value += addValue(10, "ward");
}
if (c.hasKeyword(Keyword.PROTECTION)) {
value += addValue(20, "protection");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
// paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) {
value += addValue(14, "paired");
}
if (c.hasEncodedCard()) {
value += addValue(24, "encoded");
}
if (ComputerUtilCard.hasActiveUndyingOrPersist(c)) {
value += addValue(30, "revive");
}
// Bad keywords
if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) {
value -= subValue((power * 9) + 40, "defender");
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
value -= subValue(40, "sac-end");
}
if (c.hasKeyword("CARDNAME can't attack or block.")) {
value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
} else if (c.hasKeyword("CARDNAME can't block.")) {
if (c.hasKeyword("CARDNAME can't block.")) {
value -= subValue(10, "cant-block");
} else {
List<GameEntity> mAEnt = StaticAbilityMustAttack.entitiesMustAttack(c);
if (mAEnt.contains(c)) {
value -= subValue(10, "must-attack");
} else if (!mAEnt.isEmpty()) {
value -= subValue(10, "must-attack-player");
}/* else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
} else if (c.hasKeyword("CARDNAME attacks each turn if able.")
|| c.hasKeyword("CARDNAME attacks each combat if able.")) {
value -= subValue(10, "must-attack");
} else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
value -= subValue(10, "must-attack-player");
} else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
value -= subValue(toughness * 5, "reverse-reach");
}//*/
}
if (c.hasSVar("DestroyWhenDamaged")) {
value -= subValue((toughness - 1) * 9, "dies-to-dmg");
}
if (c.getSVar("Targeting").equals("Dies")) {
value -= subValue(25, "dies");
if (c.hasKeyword("CARDNAME can't attack or block.")) {
value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
}
if (c.isUntapped()) {
value += addValue(1, "untapped");
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
if (!c.canUntapPhaseController()) {
if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
} else {
@@ -220,21 +189,41 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
value -= subValue(10, "echo-unpaid");
}
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
value -= subValue(20, "upkeep-dmg");
}
if (c.hasKeyword(Keyword.FADING)) {
value -= subValue(20, "fading");
}
if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20, "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.getSVar("Targeting").equals("Dies")) {
value -= subValue(25, "dies");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.isUntapped()) {
value += addValue(1, "untapped");
}
// paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) {
value += addValue(14, "paired");
}
// TODO no longer a KW
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
value -= subValue(20, "upkeep-dmg");
}
if (!c.getEncodedCards().isEmpty()) {
value += addValue(24, "encoded");
}
// card-specific evaluation modifier
if (c.hasSVar("AIEvaluationModifier")) {
@@ -253,11 +242,11 @@ public class CreatureEvaluator implements Function<Card, Integer> {
&& "+X".equals(sa.getParam("NumDef"))
&& !sa.usesTargeting()
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
// Electrostatic Pummeler, can be expanded for similar cards
int initPower = sa.getHostCard().getNetPower();
int initPower = getEffectivePower(sa.getHostCard());
int pumpedPower = initPower;
int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY);
int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY);
if (energy > 0) {
int numActivations = energy / 3;
for (int i = 0; i < numActivations; i++) {

View File

@@ -1,28 +1,19 @@
package forge.ai;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
import java.util.Map.Entry;
import com.google.common.collect.*;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.Card;
import forge.game.card.CardCloneStates;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
@@ -34,12 +25,21 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.SpellAbility;
import forge.game.ability.AbilityKey;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
import java.util.Map.Entry;
public abstract class GameState {
private static final Map<ZoneType, String> ZONES = new HashMap<>();
@@ -50,7 +50,6 @@ public abstract class GameState {
ZONES.put(ZoneType.Library, "library");
ZONES.put(ZoneType.Exile, "exile");
ZONES.put(ZoneType.Command, "command");
ZONES.put(ZoneType.Sideboard, "sideboard");
}
private int humanLife = -1;
@@ -78,12 +77,9 @@ public abstract class GameState {
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>();
private final Map<Card, String> cardToChosenType = new HashMap<>();
private final Map<Card, String> cardToChosenType2 = new HashMap<>();
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
private final Map<Card, List<String>> cardToMergedCards = new HashMap<>();
private final Map<Card, String> cardToNamedCard = new HashMap<>();
private final Map<Card, String> cardToNamedCard2 = new HashMap<>();
private final Map<Card, String> cardToExiledWithId = new HashMap<>();
private final Map<Card, Card> cardAttackMap = new HashMap<>();
@@ -154,7 +150,7 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n"));
}
if (!computerManaPool.isEmpty()) {
sb.append(TextUtil.concatNoSpace("aimanapool=", computerManaPool, "\n"));
sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n"));
}
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
@@ -248,7 +244,7 @@ public abstract class GameState {
if (card instanceof DetachedCardEffect) {
continue;
}
addCard(zone, card.getController() == ai ? aiCardTexts : humanCardTexts, card);
addCard(zone, card.getOwner() == ai ? aiCardTexts : humanCardTexts, card);
}
}
}
@@ -264,13 +260,7 @@ public abstract class GameState {
if (c.getPaperCard() == null) {
return;
}
if (!c.getMergedCards().isEmpty()) {
// we have to go by the current top card name here
newText.append(c.getTopMergedCard().getPaperCard().getName());
} else {
newText.append(c.getPaperCard().getName());
}
newText.append(c.getPaperCard().getName());
}
if (c.isCommander()) {
newText.append("|IsCommander");
@@ -281,10 +271,6 @@ public abstract class GameState {
}
if (zoneType == ZoneType.Battlefield) {
if (c.getOwner() != c.getController()) {
// TODO: Handle more than 2-player games.
newText.append("|Owner:" + (c.getOwner().isAI() ? "AI" : "Human"));
}
if (c.isTapped()) {
newText.append("|Tapped");
}
@@ -312,38 +298,30 @@ public abstract class GameState {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
}
if (c.getPlayerAttachedTo() != null) {
// TODO: improve this for game states with more than two players
newText.append("|EnchantingPlayer:");
Player p = c.getPlayerAttachedTo();
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
} else if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
}
if (c.getDamage() > 0) {
newText.append("|Damage:").append(c.getDamage());
}
if (c.hasChosenColor()) {
if (!c.getChosenColor().isEmpty()) {
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ","));
}
if (c.hasChosenType()) {
if (!c.getChosenType().isEmpty()) {
newText.append("|ChosenType:").append(c.getChosenType());
}
if (c.hasChosenType2()) {
newText.append("|ChosenType2:").append(c.getChosenType2());
}
if (!c.getNamedCard().isEmpty()) {
newText.append("|NamedCard:").append(c.getNamedCard());
}
if (!c.getNamedCard2().isEmpty()) {
newText.append("|NamedCard2:").append(c.getNamedCard2());
}
List<String> chosenCardIds = Lists.newArrayList();
for (Object obj : c.getChosenCards()) {
@@ -375,17 +353,6 @@ public abstract class GameState {
if (!imprintedCardIds.isEmpty()) {
newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ","));
}
if (!c.getMergedCards().isEmpty()) {
List<String> mergedCardNames = new ArrayList<>();
for (Card merged : c.getMergedCards()) {
if (c.getTopMergedCard() == merged) {
continue;
}
mergedCardNames.add(merged.getPaperCard().getName().replace(",", "^"));
}
newText.append("|MergedCards:").append(TextUtil.join(mergedCardNames, ","));
}
}
if (zoneType == ZoneType.Exile) {
@@ -395,18 +362,6 @@ public abstract class GameState {
if (c.isFaceDown()) {
newText.append("|FaceDown"); // Exiled face down
}
if (c.isAdventureCard() && c.getZone().is(ZoneType.Exile)) {
// TODO: this will basically default all exiled cards with Adventure to being "On Adventure".
// Need to figure out a better way to detect if it's actually on adventure.
newText.append("|OnAdventure");
}
if (c.isForetold()) {
newText.append("|Foretold");
}
if (c.isForetoldThisTurn()) {
newText.append("|ForetoldThisTurn");
}
}
if (zoneType == ZoneType.Battlefield || zoneType == ZoneType.Exile) {
@@ -435,7 +390,7 @@ public abstract class GameState {
boolean first = true;
StringBuilder counterString = new StringBuilder();
for (Entry<CounterType, Integer> kv : counters.entrySet()) {
for(Entry<CounterType, Integer> kv : counters.entrySet()) {
if (!first) {
counterString.append(",");
}
@@ -471,7 +426,7 @@ public abstract class GameState {
}
public void parse(List<String> lines) {
for (String line : lines) {
for(String line : lines) {
parseLine(line);
}
}
@@ -573,13 +528,6 @@ public abstract class GameState {
aiCardTexts.put(ZoneType.Command, categoryValue);
}
else if (categoryName.endsWith("sideboard")) {
if (isHuman)
humanCardTexts.put(ZoneType.Sideboard, categoryValue);
else
aiCardTexts.put(ZoneType.Sideboard, categoryValue);
}
else if (categoryName.startsWith("ability")) {
abilityString.put(categoryName.substring("ability".length()), categoryValue);
}
@@ -635,13 +583,10 @@ public abstract class GameState {
cardToEnchantPlayerId.clear();
cardToRememberedId.clear();
cardToExiledWithId.clear();
cardToImprintedId.clear();
markedDamage.clear();
cardToChosenClrs.clear();
cardToChosenCards.clear();
cardToChosenType.clear();
cardToChosenType2.clear();
cardToMergedCards.clear();
cardToScript.clear();
cardAttackMap.clear();
@@ -674,7 +619,6 @@ public abstract class GameState {
handleCardAttachments();
handleChosenEntities();
handleRememberedEntities();
handleMergedCards();
handleScriptExecution(game);
handlePrecastSpells(game);
handleMarkedDamage();
@@ -707,19 +651,11 @@ public abstract class GameState {
}
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
// Set negative or zero life after state effects if need be, important for some puzzles that rely on
// pre-setting negative life (e.g. PS_NEO4).
if (humanLife <= 0) {
human.setLife(humanLife, null);
} else if (computerLife <= 0) {
ai.setLife(computerLife, null);
}
}
private String processManaPool(ManaPool manaPool) {
StringBuilder mana = new StringBuilder();
for (final byte c : ManaAtom.MANATYPES) {
for (final byte c : MagicColor.WUBRGC) {
int amount = manaPool.getAmountOfColor(c);
for (int i = 0; i < amount; i++) {
mana.append(MagicColor.toShortString(c)).append(" ");
@@ -779,10 +715,23 @@ public abstract class GameState {
}
game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap));
for (final Card c : combat.getAttackers()) {
CombatUtil.checkDeclaredAttacker(game, c, combat, false);
if (!combat.getAttackers().isEmpty()) {
List<GameEntity> attackedTarget = Lists.newArrayList();
for (final Card c : combat.getAttackers()) {
attackedTarget.add(combat.getDefenderByAttacker(c));
}
final Map<AbilityKey, Object> runParams = Maps.newEnumMap(AbilityKey.class);
runParams.put(AbilityKey.Attackers, combat.getAttackers());
runParams.put(AbilityKey.AttackingPlayer, combat.getAttackingPlayer());
runParams.put(AbilityKey.AttackedTarget, attackedTarget);
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
}
for (final Card c : combat.getAttackers()) {
CombatUtil.checkDeclaredAttacker(game, c, combat);
}
game.getTriggerHandler().resetActiveTriggers();
game.updateCombatForView();
game.fireEvent(new GameEventCombatChanged());
@@ -824,7 +773,6 @@ public abstract class GameState {
Card exiledWith = idToCard.get(Integer.parseInt(id));
c.setExiledWith(exiledWith);
c.setExiledBy(exiledWith.getController());
}
}
@@ -859,12 +807,6 @@ public abstract class GameState {
break;
}
}
if (sa.hasParam("RememberTargets")) {
for (final GameObject o : sa.getTargets()) {
sa.getHostCard().addRemembered(o);
}
}
}
private void handleScriptExecution(final Game game) {
@@ -1024,27 +966,14 @@ public abstract class GameState {
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
}
Card c = null;
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
if (StringUtils.isNumeric(spellDef)) {
// Precast from a specific host
c = idToCard.get(Integer.parseInt(spellDef));
if (c == null) {
System.err.println("ERROR: Could not find a card with ID " + spellDef + " to precast!");
return;
}
} else {
// Precast from a card by name
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
if (pc == null) {
System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!");
return;
}
c = Card.fromPaperCard(pc, activator);
if (pc == null) {
System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!");
return;
}
Card c = Card.fromPaperCard(pc, activator);
SpellAbility sa = null;
if (!scriptID.isEmpty()) {
@@ -1052,12 +981,7 @@ public abstract class GameState {
return;
}
if (!c.getName().equals(spellDef) && c.hasAlternateState() && spellDef.equals(c.getAlternateState().getName())) {
sa = c.getAlternateState().getFirstSpellAbility();
} else {
sa = c.getFirstSpellAbility();
}
sa = c.getFirstSpellAbility();
sa.setActivatingPlayer(activator);
handleScriptedTargetingForSA(game, sa, tgtID);
@@ -1097,24 +1021,12 @@ public abstract class GameState {
c.setChosenType(entry.getValue());
}
// Chosen type 2
for (Entry<Card, String> entry : cardToChosenType2.entrySet()) {
Card c = entry.getKey();
c.setChosenType2(entry.getValue());
}
// Named card
for (Entry<Card, String> entry : cardToNamedCard.entrySet()) {
Card c = entry.getKey();
c.setNamedCard(entry.getValue());
}
// Named card 2
for (Entry<Card,String> entry : cardToNamedCard2.entrySet()) {
Card c = entry.getKey();
c.setNamedCard2(entry.getValue());
}
// Chosen cards
for (Entry<Card, CardCollection> entry : cardToChosenCards.entrySet()) {
Card c = entry.getKey();
@@ -1124,13 +1036,13 @@ public abstract class GameState {
private void handleCardAttachments() {
// Unattach all permanents first
for (Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
attachedTo.unAttachAllCards();
}
// Attach permanents by ID
for (Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
Card attacher = entry.getKey();
if (attacher.isAttachment()) {
@@ -1139,7 +1051,7 @@ public abstract class GameState {
}
// Enchant players by ID
for (Entry<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
for(Entry<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
// TODO: improve this for game states with more than two players
Card attacher = entry.getKey();
Game game = attacher.getGame();
@@ -1149,61 +1061,12 @@ public abstract class GameState {
}
}
private void handleMergedCards() {
for (Entry<Card, List<String>> entry : cardToMergedCards.entrySet()) {
Card mergedTo = entry.getKey();
for (String mergedCardName : entry.getValue()) {
Card c;
PaperCard pc = StaticData.instance().getCommonCards().getCard(mergedCardName. replace("^", ","));
if (pc == null) {
System.err.println("ERROR: Tried to create a non-existent card named " + mergedCardName + " (as a merged card) when loading game state!");
continue;
}
c = Card.fromPaperCard(pc, mergedTo.getOwner());
emulateMergeViaMutate(mergedTo, c);
}
}
}
private void emulateMergeViaMutate(Card top, Card bottom) {
if (top == null || bottom == null) {
System.err.println("ERROR: Tried to call emulateMergeViaMutate with a null card!");
return;
}
Game game = top.getGame();
bottom.setMergedToCard(top);
if (!top.hasMergedCard()) {
top.addMergedCard(top);
}
top.addMergedCard(bottom);
if (top.getMutatedTimestamp() != -1) {
top.removeCloneState(top.getMutatedTimestamp());
}
final Long ts = game.getNextTimestamp();
top.setMutatedTimestamp(ts);
if (top.getCurrentStateName() != CardStateName.FaceDown) {
final CardCloneStates mutatedStates = CardFactory.getMutatedCloneStates(top, null/*FIXME*/);
top.addCloneState(mutatedStates, ts);
}
bottom.setTapped(top.isTapped());
bottom.setFlipped(top.isFlipped());
top.setTimesMutated(top.getTimesMutated() + 1);
top.updateTokenView();
// TODO: Merged commanders aren't supported yet
}
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
entity.setCounters(Maps.newHashMap());
entity.setCounters(Maps.newEnumMap(CounterType.class));
String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2);
entity.addCounterInternal(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, null);
entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
}
}
@@ -1216,8 +1079,6 @@ public abstract class GameState {
p.getZone(zt).removeAllCards(true);
}
p.setCommanders(Lists.newArrayList());
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<>(ZoneType.class);
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
String value = kv.getValue();
@@ -1228,8 +1089,6 @@ public abstract class GameState {
p.setLandsPlayedThisTurn(landsPlayed);
p.setLandsPlayedLastTurn(landsPlayedLastTurn);
p.clearPaidForSA();
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) {
@@ -1247,21 +1106,22 @@ public abstract class GameState {
boolean tapped = c.isTapped();
boolean sickness = c.hasSickness();
Map<CounterType, Integer> counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters var as-is.
c.setCounters(Maps.newHashMap());
// Note: Not clearCounters() since we want to keep the counters
// var as-is.
c.setCounters(Maps.newEnumMap(CounterType.class));
if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter)
//FIXME it shouldn't be able to attach itself
c.setEntityAttachedTo(CardFactory.copyCard(c, true));
c.setEntityAttachedTo(c);
}
if (cardsWithoutETBTrigs.contains(c)) {
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null, null);
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null);
} else {
p.getZone(ZoneType.Hand).add(c);
p.getGame().getAction().moveToPlay(c, null, null);
p.getGame().getAction().moveToPlay(c, null);
}
c.setTapped(tapped);
@@ -1272,16 +1132,13 @@ public abstract class GameState {
zone.setCards(kv.getValue());
}
}
for (Card cmd : p.getCommanders()) {
p.getZone(ZoneType.Command).add(Player.createCommanderEffect(p.getGame(), cmd));
}
}
/**
* <p>
* processCardsForZone.
* </p>
*
*
* @param data
* an array of {@link java.lang.String} objects.
* @param player
@@ -1300,7 +1157,7 @@ public abstract class GameState {
break;
}
}
Card c;
boolean hasSetCurSet = false;
if (cardinfo[0].startsWith("t:")) {
@@ -1323,7 +1180,7 @@ public abstract class GameState {
for (final String info : cardinfo) {
if (info.startsWith("Tapped")) {
c.tap(false);
c.tap();
} else if (info.startsWith("Renowned")) {
c.setRenowned(true);
} else if (info.startsWith("Monstrous")) {
@@ -1345,25 +1202,11 @@ public abstract class GameState {
c.setState(CardStateName.Flipped, true);
} else if (info.startsWith("Meld")) {
c.setState(CardStateName.Meld, true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell";
SpellAbility saAdventure = AbilityFactory.getAbility(abAdventure, c);
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
saAdventure.setSVar("Play", sbPlay.toString());
saAdventure.setActivatingPlayer(c.getOwner());
saAdventure.resolve();
c.setExiledWith(c); // This seems to be the way it's set up internally. Potentially not needed here?
c.setExiledBy(c.getController());
} else if (info.startsWith("IsCommander")) {
// TODO: This doesn't seem to properly restore the ability to play the commander. Why?
c.setCommander(true);
List<Card> cmd = Lists.newArrayList(player.getCommanders());
cmd.add(c);
player.setCommanders(cmd);
player.setCommanders(Lists.newArrayList(c));
player.getZone(ZoneType.Command).add(Player.createCommanderEffect(player.getGame(), c));
} else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3));
idToCard.put(id, c);
@@ -1392,8 +1235,6 @@ public abstract class GameState {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) {
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ChosenType2:")) {
cardToChosenType2.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ChosenCards:")) {
CardCollection chosen = new CardCollection();
String[] idlist = info.substring(info.indexOf(':') + 1).split(",");
@@ -1401,13 +1242,8 @@ public abstract class GameState {
chosen.add(idToCard.get(Integer.parseInt(id)));
}
cardToChosenCards.put(c, chosen);
} else if (info.startsWith("MergedCards:")) {
List<String> cardNames = Arrays.asList(info.substring(info.indexOf(':') + 1).split(","));
cardToMergedCards.put(c, cardNames);
} else if (info.startsWith("NamedCard:")) {
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("NamedCard2:")) {
cardToNamedCard2.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ExecuteScript:")) {
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("RememberedCards:")) {
@@ -1425,14 +1261,6 @@ public abstract class GameState {
}
} else if (info.equals("NoETBTrigs")) {
cardsWithoutETBTrigs.add(c);
} else if (info.equals("Foretold")) {
c.setForetold(true);
c.turnFaceDown(true);
c.addMayLookTemp(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) {
c.setForetoldThisTurn(true);
} else if (info.equals("IsToken")) {
c.setToken(true);
}
}

View File

@@ -14,17 +14,18 @@ public class LobbyPlayerAi extends LobbyPlayer implements IGameEntitiesFactory {
private boolean rotateProfileEachGame;
private boolean allowCheatShuffle;
private boolean useSimulation;
public LobbyPlayerAi(String name, Set<AIOption> options) {
super(name);
if (options != null && options.contains(AIOption.USE_SIMULATION)) {
this.useSimulation = true;
}
}
public boolean isAllowCheatShuffle() {
return allowCheatShuffle;
}
public void setAllowCheatShuffle(boolean allowCheatShuffle) {
this.allowCheatShuffle = allowCheatShuffle;
}
@@ -32,6 +33,7 @@ public class LobbyPlayerAi extends LobbyPlayer implements IGameEntitiesFactory {
public void setAiProfile(String profileName) {
aiProfile = profileName;
}
public String getAiProfile() {
return aiProfile;
}

View File

@@ -1,23 +1,11 @@
package forge.ai;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
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.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import forge.LobbyPlayer;
import forge.ai.ability.ProtectAi;
import forge.card.CardStateName;
@@ -27,49 +15,25 @@ import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard;
import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.GameType;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CardView;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment;
import forge.game.cost.CostExile;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.cost.*;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
import forge.game.mana.ManaConversionMatrix;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.DelayedReveal;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController;
import forge.game.player.PlayerView;
import forge.game.player.*;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.Ability;
import forge.game.spellability.AbilityStatic;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetChoices;
import forge.game.spellability.*;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
@@ -78,36 +42,32 @@ import forge.util.ITriggerEvent;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.security.InvalidParameterException;
import java.util.*;
/**
/**
* A prototype for player controller class
*
*
* Handles phase skips for now.
*/
public class PlayerControllerAi extends PlayerController {
private final AiController brains;
private boolean pilotsNonAggroDeck = false;
public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) {
super(game, p, lp);
brains = new AiController(p, game);
}
public boolean pilotsNonAggroDeck() {
return pilotsNonAggroDeck;
}
public void setupAutoProfile(Deck deck) {
pilotsNonAggroDeck = deck.getName().contains("Control") || Deck.getAverageCMC(deck) > 3;
}
public void allowCheatShuffle(boolean value) {
public void allowCheatShuffle(boolean value){
brains.allowCheatShuffle(value);
}
public void setUseSimulation(boolean value) {
brains.setUseSimulation(value);
}
@@ -117,7 +77,9 @@ public class PlayerControllerAi extends PlayerController {
if (abilities.size() == 0) {
return null;
}
return abilities.get(0);
else {
return abilities.get(0);
}
}
public AiController getAi() {
@@ -136,35 +98,12 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, CardCollectionView remaining, int damageDealt, GameEntity defender, boolean overrideOrder) {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, remaining, damageDealt, defender, overrideOrder);
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, int damageDealt, GameEntity defender, boolean overrideOrder) {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, damageDealt, defender, overrideOrder);
}
@Override
public Map<GameEntity, Integer> divideShield(Card effectSource, Map<GameEntity, Integer> affected, int shieldAmount) {
// TODO: AI current can't use this so this is not implemented.
return new HashMap<>();
}
@Override
public Map<Byte, Integer> specifyManaCombo(SpellAbility sa, ColorSet colorSet, int manaAmount, boolean different) {
Map<Byte, Integer> result = new HashMap<>();
for (int i = 0; i < manaAmount; ++i) {
Byte chosen = chooseColor("", sa, colorSet);
if (result.containsKey(chosen)) {
result.put(chosen, result.get(chosen) + 1);
} else {
result.put(chosen, 1);
}
if (different) {
colorSet = ColorSet.fromMask(colorSet.getColor() - chosen);
}
}
return result;
}
@Override
public Integer announceRequirements(SpellAbility ability, String announce) {
public Integer announceRequirements(SpellAbility ability, String announce, boolean allowZero) {
// For now, these "announcements" are made within the AI classes of the appropriate SA effects
if (ability.getApi() != null) {
switch (ability.getApi()) {
@@ -179,7 +118,7 @@ public class PlayerControllerAi extends PlayerController {
payingPlayer = ability.getHostCard().getEnchantingCard().getController();
}
int number = ComputerUtilMana.determineLeftoverMana(ability, player, false);
int number = ComputerUtilMana.determineLeftoverMana(ability, player);
if (logic.startsWith("MaxMana.") || logic.startsWith("PowerLeakMaxMana.")) {
number = Math.min(number, Integer.parseInt(logic.substring(logic.indexOf(".") + 1)));
@@ -189,7 +128,7 @@ public class PlayerControllerAi extends PlayerController {
case BidLife:
return 0;
default:
return null;
return null;
}
}
return null; // return incorrect value to indicate that
@@ -206,12 +145,12 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional, Map<String, Object> params) {
return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional, params);
public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional) {
return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional);
}
@Override
public <T extends GameEntity> T chooseSingleEntityForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public <T extends GameEntity> T chooseSingleEntityForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
@@ -219,47 +158,31 @@ public class PlayerControllerAi extends PlayerController {
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer);
}
@Override
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
Player targetedPlayer, Map<String, Object> params) {
Player targetedPlayer) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
FCollection<T> remaining = new FCollection<>(optionList);
List<T> selecteds = new ArrayList<>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer, params);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
FCollection<T> remaining = new FCollection<>(optionList);
List<T> selecteds = new ArrayList<>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
return selecteds;
}
@Override
public List<SpellAbility> chooseSpellAbilitiesForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
int num, Map<String, Object> params) {
List<SpellAbility> remaining = Lists.newArrayList(spells);
List<SpellAbility> selecteds = Lists.newArrayList();
SpellAbility selected;
do {
selected = chooseSingleSpellForEffect(remaining, sa, title, params);
if (selected != null) {
remaining.remove(selected);
selecteds.add(selected);
}
} while (selected != null && selecteds.size() < num);
return selecteds;
}
@Override
public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
public SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
@@ -272,7 +195,7 @@ public class PlayerControllerAi extends PlayerController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return getAi().confirmAction(sa, mode, message);
}
@Override
public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, String string,
int bid, Player winner) {
@@ -285,21 +208,23 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean confirmTrigger(WrappedAbility wrapper) {
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
final SpellAbility sa = wrapper.getWrappedAbility();
//final Trigger regtrig = wrapper.getTrigger();
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true;
}
if (wrapper.isMandatory()) {
if (triggerParams.containsKey("DelayedTrigger") || isMandatory) {
//TODO: The only card with an optional delayed trigger is Shirei, Shizo's Caretaker,
// needs to be expanded when a more difficult cards comes up
return true;
}
// Store/replace target choices more properly to get this SA cleared.
TargetChoices tc = null;
TargetChoices subtc = null;
boolean storeChoices = sa.usesTargeting();
boolean storeChoices = sa.getTargetRestrictions() != null;
final SpellAbility sub = sa.getSubAbility();
boolean storeSubChoices = sub != null && sub.usesTargeting();
boolean storeSubChoices = sub != null && sub.getTargetRestrictions() != null;
boolean ret = true;
if (storeChoices) {
@@ -328,7 +253,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public Player chooseStartingPlayer(boolean isFirstgame) {
public Player chooseStartingPlayer(boolean isFirstGame) {
return this.player; // AI is brave :)
}
@@ -339,7 +264,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List<Card> exertAttackers(List<Card> attackers) {
return AiAttackController.exertAttackers(attackers, brains.getAttackAggression());
return AiAttackController.exertAttackers(attackers);
}
@Override
@@ -370,13 +295,14 @@ public class PlayerControllerAi extends PlayerController {
for (Card c: topN) {
if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c)) {
toBottom.add(c);
} else {
}
else {
toTop.add(c);
}
}
// put the rest on top in random order
CardLists.shuffle(toTop);
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toBottom);
}
@@ -402,7 +328,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
CardLists.shuffle(toTop);
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toGraveyard);
}
@@ -424,13 +350,13 @@ public class PlayerControllerAi extends PlayerController {
if (destinationZone == ZoneType.Graveyard) {
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
if (Iterables.any(getGame().getCardsInGame(), new Predicate<Card>() {
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter");
}
})) {
}).isEmpty()) {
int bestValue = 0;
Card bestCreature = null;
for (Card c : cards) {
@@ -471,7 +397,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
int landsOTB = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
if (!p.isOpponentOf(player)) {
if (landsOTB <= 2) {
@@ -533,7 +459,8 @@ public class PlayerControllerAi extends PlayerController {
if (copySA instanceof Spell) {
Spell spell = (Spell) copySA;
((PlayerControllerAi) player.getController()).getAi().canPlayFromEffectAI(spell, true, true);
} else {
}
else {
getAi().canPlaySa(copySA);
}
}
@@ -544,7 +471,7 @@ public class PlayerControllerAi extends PlayerController {
public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) {
if (canSetupTargets)
brains.doTrigger(effectSA, true); // first parameter does not matter, since return value won't be used
ComputerUtil.playNoStack(player, effectSA, getGame(), true);
ComputerUtil.playNoStack(player, effectSA, game);
}
@Override
@@ -552,16 +479,15 @@ public class PlayerControllerAi extends PlayerController {
return getAi().chooseCardsToDelve(genericAmount, grave);
}
@Override
public TargetChoices chooseNewTargetsFor(SpellAbility ability) {
// AI currently can't do this. But when it can it will need to be based on Ability API
return null;
}
@Override
public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) {
String [] splitUTypes = uType.split(",");
CardCollection cardsOfType = new CardCollection();
for (String part : splitUTypes) {
CardCollection partCards = CardLists.getType(hand, part);
if (!partCards.isEmpty()) {
cardsOfType.addAll(partCards);
}
}
final CardCollectionView cardsOfType = CardLists.getType(hand, uType);
if (!cardsOfType.isEmpty()) {
Card toDiscard = Aggregates.itemWithMin(cardsOfType, CardPredicates.Accessors.fnGetCmc);
return new CardCollection(toDiscard);
@@ -569,29 +495,32 @@ public class PlayerControllerAi extends PlayerController {
return getAi().getCardsToDiscard(num, null, sa);
}
@Override
public Mana chooseManaFromPool(List<Mana> manaChoices) {
return manaChoices.get(0); // no brains used
}
@Override
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes, boolean isOptional) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), validTypes, invalidTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) {
chosen = validTypes.iterator().next();
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: chosen");
public String chooseSomeType(String kindOfType, SpellAbility sa, List<String> validTypes, List<String> invalidTypes, boolean isOptional) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), invalidTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty())
{
chosen = validTypes.get(0);
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to 1st element: chosen");
}
game.getAction().nofityOfValue(sa, player, chosen, player);
return chosen;
}
@Override
public Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes, Player forPlayer) {
return ComputerUtil.vote(player, options, sa, votes, forPlayer);
public Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes) {
return ComputerUtil.vote(player, options, sa, votes);
}
@Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
return brains.aiShouldRun(replacementEffect, effectSA, affected);
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question) {
return brains.aiShouldRun(replacementEffect, effectSA);
}
@Override
@@ -620,7 +549,7 @@ public class PlayerControllerAi extends PlayerController {
hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.count(toReturn, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.filter(toReturn, Presets.LANDS).size();
// If we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {
@@ -668,19 +597,18 @@ public class PlayerControllerAi extends PlayerController {
public List<SpellAbility> chooseSpellAbilityToPlay() {
return brains.chooseSpellAbilityToPlay();
}
@Override
public boolean playChosenSpellAbility(SpellAbility sa) {
public void playChosenSpellAbility(SpellAbility sa) {
// System.out.println("Playing sa: " + sa);
if (sa instanceof LandAbility) {
if (sa.canPlay()) {
sa.resolve();
getGame().updateLastStateForCard(sa.getHostCard());
}
} else {
ComputerUtil.handlePlayingSpellAbility(player, sa, getGame());
ComputerUtil.handlePlayingSpellAbility(player, sa, game);
}
return true;
}
}
@Override
public CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard) {
@@ -695,10 +623,8 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean payManaOptional(Card c, Cost cost, SpellAbility sa, String prompt, ManaPaymentPurpose purpose) {
// TODO replace with EmptySa
final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
ability.setActivatingPlayer(c.getController());
ability.setCardState(sa.getCardState());
// FIXME: This is a hack to check if the AI can play the "exile from library" pay costs (Cumulative Upkeep,
// e.g. Thought Lash). We have to do it and bail early if the AI can't pay, because otherwise the AI will
@@ -718,8 +644,8 @@ public class PlayerControllerAi extends PlayerController {
}
// - End of hack for Exile a card from library Cumulative Upkeep -
if (ComputerUtilCost.canPayCost(ability, c.getController(), true)) {
ComputerUtil.playNoStack(c.getController(), ability, getGame(), true);
if (ComputerUtilCost.canPayCost(ability, c.getController())) {
ComputerUtil.playNoStack(c.getController(), ability, game);
return true;
}
return false;
@@ -735,7 +661,7 @@ public class PlayerControllerAi extends PlayerController {
public int chooseNumber(SpellAbility sa, String title, int min, int max) {
return brains.chooseNumber(sa, title, min, max);
}
@Override
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map<String, Object> params) {
ApiType api = sa.getApi();
@@ -780,6 +706,7 @@ public class PlayerControllerAi extends PlayerController {
return allTargets.get(0);
}
@Override
public void notifyOfValue(SpellAbility saSource, GameObject realtedTarget, String value) {
// AI should take into consideration creature types, numbers and other information (mostly choices) arriving through this channel
@@ -802,7 +729,7 @@ public class PlayerControllerAi extends PlayerController {
return true;
} else {
Card rem = (Card) source.getFirstRemembered();
if (!rem.isInPlay()) {
if (!rem.isInZone(ZoneType.Battlefield)) {
return true;
}
}
@@ -810,8 +737,7 @@ public class PlayerControllerAi extends PlayerController {
case "BetterTgtThanRemembered":
if (source.getRememberedCount() > 0) {
Card rem = (Card) source.getFirstRemembered();
// avoid pumping opponent creature
if (!rem.isInPlay() || rem.getController().isOpponentOf(source.getController())) {
if (!rem.isInZone(ZoneType.Battlefield)) {
return true;
}
for (Card c : source.getController().getCreaturesInPlay()) {
@@ -829,7 +755,6 @@ public class PlayerControllerAi extends PlayerController {
return defaultVal != null && defaultVal.booleanValue();
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
case LeftOrRight: return brains.chooseDirection(sa);
case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even
default:
return MyRandom.getRandom().nextBoolean();
}
@@ -837,7 +762,7 @@ public class PlayerControllerAi extends PlayerController {
/*
* (non-Javadoc)
*
*
* @see
* forge.game.player.PlayerController#chooseBinary(forge.game.spellability.
* SpellAbility, java.lang.String,
@@ -860,8 +785,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {
List<AbilitySub> result = brains.chooseModeForAbility(sa, possible, min, num, allowRepeat);
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
List<AbilitySub> result = brains.chooseModeForAbility(sa, min, num, allowRepeat);
if (result != null) {
return result;
}
@@ -878,22 +803,20 @@ public class PlayerControllerAi extends PlayerController {
}
return sa.getChosenList();
}
@Override
public byte chooseColorAllowColorless(String message, Card card, ColorSet colors) {
final String c = ComputerUtilCard.getMostProminentColor(player.getCardsIn(ZoneType.Hand));
byte chosenColorMask = MagicColor.fromName(c);
if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask;
} else {
return Iterables.getFirst(colors, (byte)0);
}
return Iterables.getFirst(colors, (byte)0);
}
@Override
public byte chooseColor(String message, SpellAbility sa, ColorSet colors) {
if (colors.countColors() < 2) {
return Iterables.getFirst(colors, MagicColor.WHITE);
}
// You may switch on sa.getApi() here and use sa.getParam("AILogic")
CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
if (sa.getApi() == ApiType.Mana) {
@@ -905,7 +828,9 @@ public class PlayerControllerAi extends PlayerController {
if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask;
}
return Iterables.getFirst(colors, MagicColor.WHITE);
else {
return Iterables.getFirst(colors, MagicColor.WHITE);
}
}
@Override
@@ -921,17 +846,13 @@ public class PlayerControllerAi extends PlayerController {
/*
* (non-Javadoc)
*
*
* @see forge.game.player.PlayerController#chooseCounterType(java.util.List,
* forge.game.spellability.SpellAbility, java.lang.String, java.util.Map)
*/
@Override
public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, String prompt,
Map<String, Object> params) {
// short cut if there is no options to choose
if (options.size() <= 1) {
return Iterables.getFirst(options, null);
}
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
@@ -941,7 +862,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean confirmPayment(CostPart costPart, String prompt, SpellAbility sa) {
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
}
@Override
@@ -953,8 +874,8 @@ public class PlayerControllerAi extends PlayerController {
public String chooseProtectionType(String string, SpellAbility sa, List<String> choices) {
String choice = choices.get(0);
SpellAbility hostsa = null; //for Protect sub-ability
if (getGame().stack.size() > 1) {
for (SpellAbilityStackInstance si : getGame().getStack()) {
if (game.stack.size() > 1) {
for (SpellAbilityStackInstance si : game.getStack()) {
SpellAbility spell = si.getSpellAbility(true);
if (sa != spell && sa.getHostCard() != spell.getHostCard()) {
String s = ProtectAi.toProtectFrom(spell.getHostCard(), sa);
@@ -965,10 +886,10 @@ public class PlayerControllerAi extends PlayerController {
}
}
}
final Combat combat = getGame().getCombat();
final Combat combat = game.getCombat();
if (combat != null) {
if (getGame().stack.size() == 1) {
SpellAbility topstack = getGame().stack.peekAbility();
if (game.stack.size() == 1) {
SpellAbility topstack = game.stack.peekAbility();
if (topstack.getSubAbility() == sa) {
hostsa = topstack;
}
@@ -991,7 +912,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
}
final PhaseHandler ph = getGame().getPhaseHandler();
final PhaseHandler ph = game.getPhaseHandler();
if (ph.getPlayerTurn() == sa.getActivatingPlayer() && ph.getPhase() == PhaseType.MAIN1 && sa.getTargetCard() != null) {
AiAttackController aiAtk = new AiAttackController(sa.getActivatingPlayer(), sa.getTargetCard());
String s = aiAtk.toProtectAttacker(sa);
@@ -1001,9 +922,12 @@ public class PlayerControllerAi extends PlayerController {
}
final String logic = sa.getParam("AILogic");
if (logic == null || logic.equals("MostProminentHumanCreatures")) {
CardCollection list = player.getOpponents().getCreaturesInPlay();
CardCollection list = new CardCollection();
for (Player opp : player.getOpponents()) {
list.addAll(opp.getCreaturesInPlay());
}
if (list.isEmpty()) {
list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents());
list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
}
if (!list.isEmpty()) {
choice = ComputerUtilCard.getMostProminentColor(list);
@@ -1015,15 +939,14 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) {
final Card source = sa.getHostCard();
// TODO replace with EmptySa
final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } };
emptyAbility.setActivatingPlayer(player);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setSVars(sa.getSVars());
emptyAbility.setCardState(sa.getCardState());
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player, true)) {
ComputerUtil.playNoStack(player, emptyAbility, getGame(), true); // AI needs something to resolve to pay that cost
for (String sVar : sa.getSVars()) {
emptyAbility.setSVar(sVar, sa.getSVar(sVar));
}
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) {
ComputerUtil.playNoStack(player, emptyAbility, game); // AI needs something to resolve to pay that cost
return true;
}
return false;
@@ -1032,39 +955,13 @@ public class PlayerControllerAi extends PlayerController {
@Override
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {
if (sa.isTrigger()) {
if (prepareSingleSa(sa.getHostCard(), sa, true)) {
ComputerUtil.playStack(sa, player, getGame());
}
} else {
if (sa.isCopied()) {
if (sa.isSpell()) {
if (!sa.getHostCard().isInZone(ZoneType.Stack)) {
sa.setHostCard(getGame().getAction().moveToStack(sa.getHostCard(), sa));
} else {
getGame().getStackZone().add(sa.getHostCard());
}
}
/* FIXME: the new implementation (below) requires implementing setupNewTargets in the AI controller, among other possible changes, otherwise breaks AI
if (sa.isMayChooseNewTargets()) {
sa.setupNewTargets(player);
}
*/
if (sa.isMayChooseNewTargets() && !sa.setupTargets()) {
if (sa.isSpell()) {
getGame().getAction().ceaseToExist(sa.getHostCard(), false);
}
continue;
}
}
// need finally add the new spell to the stack
getGame().getStack().add(sa);
if (prepareSingleSa(sa.getHostCard(),sa,true)) {
ComputerUtil.playStack(sa, player, game);
}
}
}
private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory) {
private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory){
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(host, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
@@ -1077,7 +974,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public void playTrigger(Card host, WrappedAbility wrapperAbility, boolean isMandatory) {
if (prepareSingleSa(host, wrapperAbility, isMandatory)) {
ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, getGame(), true);
ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, game);
}
}
@@ -1087,14 +984,14 @@ public class PlayerControllerAi extends PlayerController {
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA;
// TODO if mandatory AI is only forced to use mana when it's already in the pool
if (tgtSA.checkRestrictions(brains.getPlayer()) && (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional)) {
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
if (noManaCost) {
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, game);
} else {
return ComputerUtil.playStack(tgtSA, player, game);
}
return ComputerUtil.playStack(tgtSA, player, getGame());
}
return false; // didn't play spell
} else
return false; // didn't play spell
}
return true;
}
@@ -1104,12 +1001,6 @@ public class PlayerControllerAi extends PlayerController {
return brains.doTrigger(currentAbility, true);
}
@Override
public TargetChoices chooseNewTargetsFor(SpellAbility ability, Predicate<GameObject> filter, boolean optional) {
// AI currently can't do this. But when it can it will need to be based on Ability API
return null;
}
@Override
public boolean chooseCardsPile(SpellAbility sa, CardCollectionView pile1, CardCollectionView pile2, String faceUp) {
if (faceUp.equals("True")) {
@@ -1123,6 +1014,7 @@ public class PlayerControllerAi extends PlayerController {
boolean allCreatures = Iterables.all(Iterables.concat(pile1, pile2), CardPredicates.Presets.CREATURES);
int cmc1 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile1) : ComputerUtilCard.evaluatePermanentList(pile1);
int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2);
System.out.println("value:" + cmc1 + " " + cmc2);
// for now, this assumes that the outcome will be bad
// TODO: This should really have a ChooseLogic param to
@@ -1135,17 +1027,9 @@ public class PlayerControllerAi extends PlayerController {
public void revealAnte(String message, Multimap<Player, PaperCard> removedAnteCards) {
// Ai won't understand that anyway
}
@Override
public void revealAISkipCards(String message, Map<Player, Map<DeckSection, List<? extends PaperCard>>> deckCards) {
// Ai won't understand that anyway
}
@Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto
setupAutoProfile(myDeck);
public Collection<? extends PaperCard> complainCardsCantPlayWell(Deck myDeck) {
return brains.complainCardsCantPlayWell(myDeck);
}
@@ -1161,8 +1045,10 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean effect) {
return ComputerUtilMana.payManaCost(player, sa, effect);
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean isActivatedSa) {
// TODO Auto-generated method stub
ManaCostBeingPaid cost = isActivatedSa ? ComputerUtilMana.calculateManaCost(sa, false, 0) : new ManaCostBeingPaid(toPay);
return ComputerUtilMana.payManaCost(cost, sa, player);
}
@Override
@@ -1189,7 +1075,7 @@ public class PlayerControllerAi extends PlayerController {
return new HashMap<>();
}
}
//Do not convoke potential blockers until after opponent's attack
final CardCollectionView blockers = ComputerUtilCard.getLikelyBlockers(ai, null);
if ((ph.isPlayerTurn(ai) && ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)) ||
@@ -1212,16 +1098,10 @@ public class PlayerControllerAi extends PlayerController {
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) {
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = player.getStrongestOpponent().getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = player.getWeakestOpponent().getCardsIn(ZoneType.Library);
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic");
// Filter for valid options only
if (!valid.isEmpty()) {
aiLibrary = CardLists.getValidCards(aiLibrary, valid, source.getController(), source, sa);
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
}
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
// If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name)
@@ -1240,21 +1120,20 @@ public class PlayerControllerAi extends PlayerController {
} else if (logic.equals("MostProminentInHumanDeck")) {
return ComputerUtilCard.getMostProminentCardName(oppLibrary);
} else if (logic.equals("MostProminentCreatureInComputerDeck")) {
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard(), sa);
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard());
return ComputerUtilCard.getMostProminentCardName(cards);
} else if (logic.equals("BestCreatureInComputerDeck")) {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiLibrary);
return bestCreature != null ? bestCreature.getName() : "Plains";
return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName();
} else if (logic.equals("RandomInComputerDeck")) {
return Aggregates.random(aiLibrary).getName();
} else if (logic.equals("MostProminentSpellInComputerDeck")) {
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Card.Instant,Card.Sorcery", player, sa.getHostCard(), sa);
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Card.Instant,Card.Sorcery", player, sa.getHostCard());
return ComputerUtilCard.getMostProminentCardName(cards);
} else if (logic.equals("CursedScroll")) {
return SpecialCardAi.CursedScroll.chooseCard(player, sa);
}
} else {
CardCollectionView list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents());
CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
list = CardLists.filter(list, Predicates.not(Presets.LANDS));
if (!list.isEmpty()) {
return list.get(0).getName();
@@ -1310,27 +1189,6 @@ public class PlayerControllerAi extends PlayerController {
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces);
}
@Override
public Card chooseDungeon(Player ai, List<PaperCard> dungeonCards, String message) {
// TODO: improve the conditions that define which dungeon is a viable option to choose
List<String> dungeonNames = Lists.newArrayList();
for (PaperCard pc : dungeonCards) {
dungeonNames.add(pc.getName());
}
// Don't choose Tomb of Annihilation when life in danger unless we can win right away or can't lose for 0 life
if (ai.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player?
int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
if ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife())
&& !(ai.getLife() > 1 && ai.getWeakestOpponent().getLife() == 1)) {
dungeonNames.remove("Tomb of Annihilation");
}
}
int i = MyRandom.getRandom().nextInt(dungeonNames.size());
return Card.fromPaperCard(dungeonCards.get(i), ai);
}
@Override
public List<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards) {
// sort from best to worst
@@ -1353,9 +1211,10 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) {
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen,
List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();
Cost costSoFar = chosen.getPayCosts() != null ? chosen.getPayCosts().copy() : Cost.Zero;
for (OptionalCostValue opt : optionalCostValues) {
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
@@ -1373,7 +1232,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) {
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
}
@@ -1389,16 +1248,17 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public int chooseNumberForKeywordCost(SpellAbility sa, Cost cost, KeywordInterface keyword, String prompt, int max) {
public int chooseNumberForKeywordCost(SpellAbility sa, Cost cost, KeywordInterface keyword, String prompt,
int max) {
// TODO: improve the logic depending on the keyword and the playability of the cost-modified SA (enough targets present etc.)
int chosenAmount = 0;
Cost costSoFar = sa.getPayCosts().copy();
Cost costSoFar = sa.getPayCosts() != null ? sa.getPayCosts().copy() : Cost.Zero;
for (int i = 0; i < max; i++) {
costSoFar.add(cost);
SpellAbility fullCostSa = sa.copyWithDefinedCost(costSoFar);
if (ComputerUtilCost.canPayCost(fullCostSa, player, sa.isTrigger())) {
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenAmount++;
} else {
break;
@@ -1409,16 +1269,13 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public CardCollection chooseCardsForEffectMultiple(Map<String, CardCollection> validMap, SpellAbility sa, String title, boolean isOptional) {
public CardCollection chooseCardsForEffectMultiple(Map<String, CardCollection> validMap, SpellAbility sa, String title) {
CardCollection choices = new CardCollection();
for (String mapKey: validMap.keySet()) {
CardCollection cc = validMap.get(mapKey);
cc.removeAll(choices);
Card chosen = ComputerUtilCard.getBestAI(cc);
if (chosen != null) {
choices.add(chosen);
}
choices.add(ComputerUtilCard.getBestAI(cc)); // TODO: should the AI limit itself here with the max number of cards in hand?
}
return choices;

View File

@@ -1,372 +0,0 @@
package forge.ai;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ability.TokenAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
/*
* This class contains logic which is shared by several cards with different ability types (e.g. AF ChangeZone / AF Destroy)
* Ideally, the naming scheme for methods in this class should be doXXXLogic, where XXX is the name of the logic,
* and the signature of the method should be "public static boolean doXXXLogic(final Player ai, final SpellAbility sa),
* possibly followed with any additional necessary parameters. These AI logic routines generally do all the work, so returning
* true from them should indicate that the AI has made a decision and configured the spell ability (targeting, etc.) as it
* deemed necessary.
*/
public class SpecialAiLogic {
// A logic for cards like Pongify, Crib Swap, Angelic Ascension
public static boolean doPongifyLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
Game game = source.getGame();
PhaseHandler ph = game.getPhaseHandler();
boolean isDestroy = ApiType.Destroy.equals(sa.getApi());
SpellAbility tokenSA = sa.findSubAbilityByType(ApiType.Token);
if (tokenSA == null) {
// Used wrong AI logic?
return false;
}
List<Card> targetable = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa);
CardCollection listOpp = CardLists.filterControlledBy(targetable, ai.getOpponents());
if (isDestroy) {
listOpp = CardLists.getNotKeyword(listOpp, Keyword.INDESTRUCTIBLE);
// TODO add handling for cards like targeting dies
}
Card choice = null;
if (!listOpp.isEmpty()) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(listOpp);
// can choice even be null?
if (choice != null) {
final Card token = TokenAi.spawnToken(choice.getController(), tokenSA);
if (!token.isCreature() || token.getNetToughness() < 1) {
sa.resetTargets();
sa.getTargets().add(choice);
return true;
}
if (choice.isPlaneswalker()) {
if (choice.getCurrentLoyalty() * 35 > ComputerUtilCard.evaluateCreature(token)) {
sa.resetTargets();
sa.getTargets().add(choice);
return true;
} else {
return false;
}
}
if ((!choice.isCreature() || choice.isTapped()) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.isPlayerTurn(ai) // prevent surprise combatant
|| ComputerUtilCard.evaluateCreature(choice) < 1.5 * ComputerUtilCard.evaluateCreature(token)) {
choice = null;
}
}
}
// See if we have anything we can upgrade
if (choice == null) {
CardCollection listOwn = CardLists.filterControlledBy(targetable, ai);
final Card token = TokenAi.spawnToken(ai, tokenSA);
Card bestOwnCardToUpgrade = null;
if (isDestroy) {
// just choose any Indestructible
// TODO maybe filter something that doesn't like to be targeted, or does something benefit by targeting
bestOwnCardToUpgrade = Iterables.getFirst(CardLists.getKeyword(listOwn, Keyword.INDESTRUCTIBLE), null);
}
if (bestOwnCardToUpgrade == null) {
bestOwnCardToUpgrade = ComputerUtilCard.getWorstCreatureAI(CardLists.filter(listOwn, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return card.isCreature() && (ComputerUtilCard.isUselessCreature(ai, card)
|| ComputerUtilCard.evaluateCreature(token) > 2 * ComputerUtilCard.evaluateCreature(card));
}
}));
}
if (bestOwnCardToUpgrade != null) {
if (ComputerUtilCard.isUselessCreature(ai, bestOwnCardToUpgrade) || (ph.getPhase().isAfter(PhaseType.COMBAT_END) || !ph.isPlayerTurn(ai))) {
choice = bestOwnCardToUpgrade;
}
}
}
if (choice != null) {
sa.resetTargets();
sa.getTargets().add(choice);
return true;
}
return false;
}
// A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT"
public static boolean doAristocratLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
final Combat combat = game.getCombat();
final Card source = sa.getHostCard();
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
final int powerBonus = sa.hasParam("NumAtt") ? AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa) : 0;
final int toughnessBonus = sa.hasParam("NumDef") ? AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa) : 0;
final boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible");
final int selfEval = ComputerUtilCard.evaluateCreature(source);
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
if (numOtherCreats == 0) {
return false;
}
// Try to save the card from death by pumping it if it's threatened with a damage spell
if (isThreatened && (toughnessBonus > 0 || indestructible)) {
SpellAbility saTop = game.getStack().peekAbility();
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage();
final int numCreatsToSac = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus));
if (numCreatsToSac > 1) { // probably not worth sacrificing too much
return false;
}
if (indestructible || (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg)) {
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| card.hasSVar("SacMe")
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
}
}
);
return sacFodder.size() >= numCreatsToSac;
}
}
return false;
}
if (combat == null) {
return false;
}
if (combat.isAttacking(source)) {
if (combat.getBlockers(source).isEmpty()) {
// Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if
// the opponent is tapped out or if we're willing to risk it (will currently risk it
// in case it sacs less than half its creatures to deal lethal damage)
// TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully
// for potential damage prevention, various effects like reducing damage to 0, etc.)
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}
final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / (powerBonus != 0 ? powerBonus : 1);
if (defTappedOut || numCreatsToSac < numOtherCreats / 2) {
return source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg;
} else {
return false;
}
} else {
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
// than the card we attacked with.
final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(),
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| ComputerUtilCard.evaluateCreature(card) < selfEval;
}
}
);
if (sacTgts.isEmpty()) {
return false;
}
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
final int DefP = indestructible ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
}
} else {
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| card.hasSVar("SacMe")
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
}
}
);
return !sacFodder.isEmpty();
}
}
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
if (numOtherCreats == 0) {
// Cut short if there's nothing to sac at all
return false;
}
// Check if the standard Aristocrats logic applies first (if in the right conditions for it)
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
if (isDeclareBlockers || isThreatened) {
if (doAristocratLogic(ai, sa)) {
return true;
}
}
// Check if anything is to be gained from the PutCounter subability
SpellAbility countersSa = null;
if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) {
if (sa.getApi() == ApiType.PutCounter) {
// called directly from CountersPutAi
countersSa = sa;
}
} else {
countersSa = sa.getSubAbility();
}
if (countersSa == null) {
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!");
return false;
}
final Game game = ai.getGame();
final Combat combat = game.getCombat();
final int selfEval = ComputerUtilCard.evaluateCreature(source);
String typeToGainCtr = "";
if (logic.contains(".")) {
typeToGainCtr = logic.substring(logic.indexOf(".") + 1);
}
CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay()
: CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr));
relevantCreats.remove(source);
if (relevantCreats.isEmpty()) {
// No relevant creatures to sac
return false;
}
int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa);
if (combat != null && combat.isAttacking(source) && isDeclareBlockers) {
if (combat.getBlockers(source).isEmpty()) {
// Unblocked. Check if we can deal lethal after receiving counters.
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}
// Check if there's anything that will die anyway that can be eaten to gain a perma-bonus
final CardCollection forcedSacTgts = CardLists.filter(relevantCreats,
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat));
}
}
);
if (!forcedSacTgts.isEmpty()) {
return true;
}
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
return source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg;
} else {
return false;
}
} else {
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
// than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing
// things that are also threatened to be destroyed anyway.
final CardCollection sacTgts = CardLists.filter(relevantCreats,
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| ComputerUtilCard.evaluateCreature(card) < selfEval
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
}
}
);
if (sacTgts.isEmpty()) {
return false;
}
final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
}
} else {
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
final boolean isBlocking = combat != null && combat.isBlocking(source);
final CardCollection sacFodder = CardLists.filter(relevantCreats,
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| card.hasSVar("SacMe")
|| (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval)
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
}
}
);
return !sacFodder.isEmpty();
}
}
}

View File

@@ -17,33 +17,19 @@
*/
package forge.ai;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import forge.game.GameEntity;
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 forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GameType;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPart;
@@ -54,7 +40,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.SpellPermanent;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
@@ -64,6 +49,11 @@ import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Special logic for individual cards
@@ -112,63 +102,6 @@ public class SpecialCardAi {
}
}
// Brain in a Jar
public static class BrainInAJar {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
int counterNum = source.getCounters(CounterEnumType.CHARGE);
// no need for logic
if (counterNum == 0) {
return false;
}
int libsize = ai.getCardsIn(ZoneType.Library).size();
final CardCollection hand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.or(
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
if (!hand.isEmpty()) {
// has spell that can be cast in hand with put ability
if (Iterables.any(hand, CardPredicates.hasCMC(counterNum + 1))) {
return false;
}
// has spell that can be cast if one counter is removed
if (Iterables.any(hand, CardPredicates.hasCMC(counterNum))) {
sa.setXManaCostPaid(1);
return true;
}
}
final CardCollection library = CardLists.filter(ai.getCardsIn(ZoneType.Library), Predicates.or(
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
if (!library.isEmpty()) {
// get max cmc of instant or sorceries in the libary
int maxCMC = 0;
for (final Card c : library) {
int v = c.getCMC();
if (c.isSplitCard()) {
v = Math.max(c.getCMC(Card.SplitCMCMode.LeftSplitCMC), c.getCMC(Card.SplitCMCMode.RightSplitCMC));
}
if (v > maxCMC) {
maxCMC = v;
}
}
// there is a spell with more CMC, no need to remove counter
if (counterNum + 1 < maxCMC) {
return false;
}
int maxToRemove = counterNum - maxCMC + 1;
// no Scry 0, even if its catched from later stuff
if (maxToRemove <= 0) {
return false;
}
sa.setXManaCostPaid(maxToRemove);
} else {
// no Instant or Sorceries anymore, just scry
sa.setXManaCostPaid(Math.min(counterNum, libsize));
}
return true;
}
}
// Chain of Acid
public static class ChainOfAcid {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -210,24 +143,6 @@ public class SpecialCardAi {
}
}
// Crawling Barrens
public static class CrawlingBarrens {
public static boolean consider(final Player ai, final SpellAbility sa) {
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final Combat combat = ai.getGame().getCombat();
Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa.getSubAbility());
if (sa.getHostCard().canReceiveCounters(CounterEnumType.P1P1)) {
animated.addCounterInternal(CounterEnumType.P1P1, 2, ai, false, null);
}
boolean isOppEOT = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated);
return isOppEOT || isValuableAttacker || isValuableBlocker;
}
}
// Cursed Scroll
public static class CursedScroll {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -279,7 +194,7 @@ public class SpecialCardAi {
sa.getTargets().add(worstCreat);
}
return sa.getTargets().size() > 0;
return sa.getTargets().getNumTargeted() > 0;
}
}
@@ -379,7 +294,7 @@ public class SpecialCardAi {
}
// Do not activate if damage will be prevented
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
if (source.staticDamagePrevention(predictedPT.getLeft(), source, true, true) == 0) {
return false;
}
@@ -400,7 +315,7 @@ public class SpecialCardAi {
}
boolean isBlocking = combat.isBlocking(source);
boolean cantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
boolean cantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
CardCollection opposition = isBlocking ? combat.getAttackersBlockedBy(source) : combat.getBlockers(source);
int oppP = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetAttack);
@@ -412,9 +327,9 @@ public class SpecialCardAi {
boolean canTrample = source.hasKeyword(Keyword.TRAMPLE);
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY);
int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterType.LOYALTY);
int totalDamageToPW = 0;
for (Card atk :combat.getAttackersOf(combat.getDefenderByAttacker(source))) {
for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) {
if (combat.isUnblocked(atk)) {
totalDamageToPW += atk.getNetCombatDamage();
}
@@ -434,7 +349,7 @@ public class SpecialCardAi {
if (c.hasKeyword(Keyword.FIRST_STRIKE) || c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
oppHasFirstStrike = true;
}
if (!ComputerUtilCombat.combatantCantBeDestroyed(c.getController(), c)) {
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) {
oppCantDie = false;
}
}
@@ -492,7 +407,7 @@ public class SpecialCardAi {
}
public static Pair<Integer, Integer> getPumpedPT(Player ai, int power, int toughness) {
int energy = ai.getCounters(CounterEnumType.ENERGY);
int energy = ai.getCounters(CounterType.ENERGY);
if (energy > 0) {
int numActivations = energy / 3;
for (int i = 0; i < numActivations; i++) {
@@ -545,32 +460,6 @@ public class SpecialCardAi {
}
}
// Fell the Mighty
public static class FellTheMighty {
public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollection aiList = ai.getCreaturesInPlay();
if (aiList.isEmpty()) {
return false;
}
CardLists.sortByPowerAsc(aiList);
Card lowest = aiList.get(0);
if (!sa.canTarget(lowest)) {
return false;
}
CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield),
CardPredicates.Presets.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents()));
oppList = CardLists.filterPower(oppList, lowest.getNetPower() + 1);
if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) {
sa.resetTargets();
sa.getTargets().add(lowest);
return true;
}
return false;
}
}
// Force of Will
public static class ForceOfWill {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -589,7 +478,7 @@ public class SpecialCardAi {
// Need to have something else in hand that is blue in addition to Force of Will itself,
// otherwise the AI will fail to play the card and the card will disappear from the pool
return false;
} else if (!Iterables.any(blueCards, CardPredicates.lessCMC(3))) {
} else if (CardLists.filter(blueCards, CardPredicates.lessCMC(3)).isEmpty()) {
// We probably need a low-CMC card to exile to it, exiling a higher CMC spell may be suboptimal
// since the AI does not prioritize/value cards vs. permission at the moment.
return false;
@@ -643,28 +532,6 @@ public class SpecialCardAi {
}
}
// Goblin Polka Band
public static class GoblinPolkaBand {
public static boolean consider(final Player ai, final SpellAbility sa) {
int maxPotentialTgts = Lists.newArrayList(Iterables.filter(ai.getOpponents().getCreaturesInPlay(), CardPredicates.Presets.UNTAPPED)).size();
int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false);
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
if (numTgts == 0) {
return false;
}
// Set Announce
sa.getHostCard().setSVar("TgtNum", String.valueOf(numTgts));
// Simulate random targeting
List<GameEntity> validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true);
sa.resetTargets();
sa.getTargets().addAll(Aggregates.random(validTgts, numTgts));
return true;
}
}
// Guilty Conscience
public static class GuiltyConscience {
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
@@ -709,8 +576,7 @@ public class SpecialCardAi {
}
}
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(),
sa.getParamOrDefault("ChangeNum", "1"), sa);
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("ChangeNum"), sa);
CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library),
Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName())));
Collections.sort(lib, CardLists.CmcComparatorInv);
@@ -769,7 +635,10 @@ public class SpecialCardAi {
boolean canRetFromGrave = false;
String name = c.getName().replace(',', ';');
for (Trigger t : c.getTriggers()) {
SpellAbility ab = t.ensureAbility();
SpellAbility ab = null;
if (t.hasParam("Execute")) {
ab = AbilityFactory.getAbility(c.getSVar(t.getParam("Execute")), c);
}
if (ab == null) { continue; }
if (ab.getApi() == ApiType.ChangeZone
@@ -790,7 +659,7 @@ public class SpecialCardAi {
for (Card c1 : lib) {
if (c1.getName().equals(c.getName())) {
if (!Iterables.any(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName()))
if (CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName())).isEmpty()
&& ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) {
// Try not to search for things we already have in hand or that we can't cast
libPriorityList.add(c1);
@@ -839,7 +708,7 @@ public class SpecialCardAi {
// if there's another reanimator card currently suspended, don't cast a new one until the previous
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) {
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterType.TIME) > 0) {
return false;
}
}
@@ -887,37 +756,6 @@ public class SpecialCardAi {
}
}
// Maze's End
public static class MazesEnd {
public static boolean consider(final Player ai, final SpellAbility sa) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty();
}
public static Card considerCardToGet(final Player ai, final SpellAbility sa)
{
CardCollection currentGates = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Gate"));
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
if (availableGates.isEmpty())
return null; // shouldn't get here
for (Card gate : availableGates)
{
if (!Iterables.any(currentGates, CardPredicates.nameEquals(gate.getName())))
{
// Diversify our mana base
return gate;
}
}
// Fetch a random gate if we already have all types
return Aggregates.random(availableGates);
}
}
// Mairsil, the Pretender
public static class MairsilThePretender {
// Scan the fetch list for a card with at least one activated ability.
@@ -925,11 +763,11 @@ public class SpecialCardAi {
public static Card considerCardFromList(final CardCollection fetchList) {
for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) {
for (SpellAbility ab : c.getSpellAbilities()) {
if (ab.isActivatedAbility()) {
if (ab.isAbility() && !ab.isTrigger()) {
Player controller = c.getController();
boolean wasCaged = false;
for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile),
CardPredicates.hasCounter(CounterEnumType.CAGE))) {
CardPredicates.hasCounter(CounterType.CAGE))) {
if (c.getName().equals(caged.getName())) {
wasCaged = true;
break;
@@ -995,7 +833,7 @@ public class SpecialCardAi {
}
// Set PayX here to maximum value.
int tokenSize = ComputerUtilCost.getMaxXValue(sa, ai, false);
int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai);
// Some basic strategy for Momir
if (tokenSize < 2) {
@@ -1006,45 +844,12 @@ public class SpecialCardAi {
tokenSize = 11;
}
sa.setXManaCostPaid(tokenSize);
source.setSVar("PayX", Integer.toString(tokenSize));
return true;
}
}
// Multiple Choice
public static class MultipleChoice {
public static boolean consider(final Player ai, final SpellAbility sa) {
int maxX = ComputerUtilCost.getMaxXValue(sa, ai, false);
if (maxX == 0) {
return false;
}
boolean canScryDraw = maxX >= 1 && ai.getCardsIn(ZoneType.Library).size() >= 3; // TODO: generalize / use profile values
boolean canBounce = maxX >= 2 && !ai.getOpponents().getCreaturesInPlay().isEmpty();
boolean shouldBounce = canBounce && ComputerUtilCard.evaluateCreature(ComputerUtilCard.getWorstCreatureAI(ai.getOpponents().getCreaturesInPlay())) > 210; // 180 is the level of a 4/4 token creature
boolean canMakeToken = maxX >= 3;
boolean canDoAll = maxX >= 4 && canScryDraw && shouldBounce;
if (canDoAll) {
sa.setXManaCostPaid(4);
return true;
} else if (canMakeToken) {
sa.setXManaCostPaid(3);
return true;
} else if (shouldBounce) {
sa.setXManaCostPaid(2);
return true;
} else if (canScryDraw) {
sa.setXManaCostPaid(1);
return true;
}
return false;
}
}
// Necropotence
public static class Necropotence {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1056,7 +861,7 @@ public class SpecialCardAi {
return false; // nothing to draw from the library
}
if (Iterables.any(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
if (!CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain")).isEmpty()) {
// Prefer Yawgmoth's Bargain because AI is generally better with it
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
@@ -1074,7 +879,7 @@ public class SpecialCardAi {
}
// TODO: Any other bad effects like that?
boolean blackViseOTB = Iterables.any(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise"));
boolean blackViseOTB = !CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).isEmpty();
if (ph.getNextTurn().equals(ai) && ph.is(PhaseType.MAIN2)
&& ai.getSpellsCastLastTurn() == 0
@@ -1134,7 +939,7 @@ public class SpecialCardAi {
return false;
}
String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield));
int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), "Count$Devotion." + prominentColor, sa);
int devotion = CardFactoryUtil.xCount(sa.getHostCard(), "Count$Devotion." + prominentColor);
int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0);
// do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage)
@@ -1149,7 +954,7 @@ public class SpecialCardAi {
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana();
boolean canPayWithAvailableColors = cost.canBePaidWithAvailable(ColorSet.fromNames(
boolean canPayWithAvailableColors = cost.canBePaidWithAvaliable(ColorSet.fromNames(
ComputerUtilCost.getAvailableManaColors(ai, sa.getHostCard())).getColor());
byte colorProfile = cost.getColorProfile();
@@ -1207,33 +1012,6 @@ public class SpecialCardAi {
}
}
// Power Struggle
public static class PowerStruggle {
public static boolean considerFirstTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = (Card)Aggregates.random(sa.getTargetRestrictions().getAllCandidates(sa, true));
if (firstTgt != null) {
sa.getTargets().add(firstTgt);
return true;
} else {
return false;
}
}
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = sa.getParent().getTargetCard();
Iterable<Card> candidates = Iterables.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
Predicates.and(CardPredicates.sharesCardTypeWith(firstTgt), CardPredicates.isTargetableBy(sa)));
Card secondTgt = Aggregates.random(candidates);
if (secondTgt != null) {
sa.resetTargets();
sa.getTargets().add(secondTgt);
return true;
} else {
return false;
}
}
}
// Price of Progress
public static class PriceOfProgress {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1295,7 +1073,7 @@ public class SpecialCardAi {
// Sarkhan the Mad
public static class SarkhanTheMad {
public static boolean considerDig(final Player ai, final SpellAbility sa) {
return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1;
return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1;
}
public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) {
@@ -1328,55 +1106,10 @@ public class SpecialCardAi {
}
}
// Savior of Ollenbock
public static class SaviorOfOllenbock {
public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollection oppTargetables = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
CardCollection threats = CardLists.filter(oppTargetables, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return !ComputerUtilCard.isUselessCreature(card.getController(), card);
}
});
CardCollection ownTgts = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES);
// TODO: improve the conditions for when the AI is considered threatened (check the possibility of being attacked?)
int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
boolean threatened = !threats.isEmpty() && ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife()) || ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() > 0);
if (threatened) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(threats));
} else if (!ownTgts.isEmpty()) {
Card target = ComputerUtilCard.getBestCreatureAI(ownTgts);
sa.getTargets().add(target);
int ownExiledValue = ComputerUtilCard.evaluateCreature(target), oppExiledValue = 0;
for (Card c : ai.getGame().getCardsIn(ZoneType.Exile)) {
if (c.getExiledWith() == sa.getHostCard()) {
if (c.getOwner() == ai) {
ownExiledValue += ComputerUtilCard.evaluateCreature(c);
} else {
oppExiledValue += ComputerUtilCard.evaluateCreature(c);
}
}
}
if (ownExiledValue > oppExiledValue + 150) {
sa.getHostCard().setSVar("SacMe", "5");
} else {
sa.getHostCard().removeSVar("SacMe");
}
} else if (!threats.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(threats));
}
return sa.isTargetNumberValid();
}
}
// Sorin, Vengeful Bloodlord
public static class SorinVengefulBloodlord {
public static boolean consider(final Player ai, final SpellAbility sa) {
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
int loyalty = sa.getHostCard().getCounters(CounterType.LOYALTY);
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.lessCMC(loyalty - 1), new Predicate<Card>() {
@Override
@@ -1538,7 +1271,7 @@ public class SpecialCardAi {
sa.getTargets().add(worstOwnCreat);
}
return sa.getTargets().size() > 0;
return sa.getTargets().getNumTargeted() > 0;
}
}
@@ -1562,26 +1295,6 @@ public class SpecialCardAi {
}
}
// Timmerian Fiends
public static class TimmerianFiends {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Card targeted = sa.getParentTargetingCard().getTargetCard();
if (targeted == null) {
return false;
}
if (targeted.isCreature()) {
if (ComputerUtil.aiLifeInDanger(ai, true, 0)) {
return true; // do it, hoping to save a valuable potential blocker etc.
}
return ComputerUtilCard.evaluateCreature(targeted) >= 200; // might need tweaking
} else {
// TODO: this currently compares purely by CMC. To be somehow improved, especially for stuff like the Power Nine etc.
return ComputerUtilCard.evaluatePermanentList(new CardCollection(targeted)) >= 3;
}
}
}
// Volrath's Shapeshifter
public static class VolrathsShapeshifter {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1605,7 +1318,7 @@ public class SpecialCardAi {
if (topGY == null
|| !topGY.isCreature()
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false);
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0);
}
}
@@ -1632,11 +1345,11 @@ public class SpecialCardAi {
Card source = sa.getHostCard();
Game game = source.getGame();
final int loyalty = source.getCounters(CounterEnumType.LOYALTY);
final int loyalty = source.getCounters(CounterType.LOYALTY);
int x = -1, best = 0;
Card single = null;
for (int i = 0; i < loyalty; i++) {
sa.setXManaCostPaid(i);
sa.setSVar("ChosenX", "Number$" + i);
oppType = CardLists.filterControlledBy(game.getCardsIn(origin), ai.getOpponents());
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(ai.getCardsIn(origin), sa.getParam("ChangeType"), sa);
@@ -1653,8 +1366,13 @@ public class SpecialCardAi {
}
// check if +1 would be sufficient
if (single != null) {
// TODO use better logic to find the right Deal Damage Effect?
SpellAbility ugin_burn = Iterables.find(source.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.DealDamage), null);
SpellAbility ugin_burn = null;
for (final SpellAbility s : source.getSpellAbilities()) {
if (s.getApi() == ApiType.DealDamage) {
ugin_burn = s;
break;
}
}
if (ugin_burn != null) {
// basic logic copied from DamageDealAi::dealDamageChooseTgtC
if (ugin_burn.canTarget(single)) {
@@ -1665,17 +1383,17 @@ public class SpecialCardAi {
if (can_kill) {
return false;
}
// simple check to burn player instead of exiling planeswalker
if (single.isPlaneswalker() && single.getCurrentLoyalty() <= 3) {
return false;
}
}
// simple check to burn player instead of exiling planeswalker
if (single.isPlaneswalker() && single.getCurrentLoyalty() <= 3) {
return false;
}
}
}
if (x == -1) {
return false;
}
sa.setXManaCostPaid(x);
sa.setSVar("ChosenX", "Number$" + x);
return true;
}
}
@@ -1694,7 +1412,7 @@ public class SpecialCardAi {
int maxHandSize = ai.getMaxHandSize();
// TODO: Any other bad effects like that?
boolean blackViseOTB = Iterables.any(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise"));
boolean blackViseOTB = !CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).isEmpty();
// TODO: Consider effects like "whenever a player draws a card, he loses N life" (e.g. Nekusar, the Mindraiser),
// and effects that draw an additional card whenever a card is drawn.
@@ -1770,7 +1488,7 @@ public class SpecialCardAi {
int CMC = ab.getPayCosts().getTotalMana() != null ? ab.getPayCosts().getTotalMana().getCMC() : 0;
int Xcount = ab.getPayCosts().getTotalMana() != null ? ab.getPayCosts().getTotalMana().countX() : 0;
if ((Xcount == 0 && CMC == 0) || ComputerUtilMana.canPayManaCost(ab, ai, selfCMC + minManaAdj, false)) {
if ((Xcount == 0 && CMC == 0) || ComputerUtilMana.canPayManaCost(ab, ai, selfCMC + minManaAdj)) {
if (src.isInstant() || src.isSorcery()) {
// instants and sorceries are one-shot, so only treat them as 1/2 value for the purpose of meeting minimum
// castable cards in graveyard requirements

View File

@@ -2,7 +2,6 @@ package forge.ai;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.card.ICardFace;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
@@ -19,7 +18,6 @@ import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.Collection;
@@ -58,11 +56,6 @@ public abstract class SpellAbilityAi {
final Card source = sa.getHostCard();
final Cost cost = sa.getPayCosts();
if (sa.hasParam("AICheckCanPlayWithDefinedX")) {
// FIXME: can this somehow be simplified without the need for an extra AI hint?
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
}
if (!checkConditions(ai, sa, sa.getConditions())) {
SpellAbility sub = sa.getSubAbility();
if (sub != null && !checkConditions(ai, sub, sub.getConditions())) {
@@ -72,12 +65,10 @@ public abstract class SpellAbilityAi {
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
final boolean alwaysOnDiscard = "AlwaysOnDiscard".equals(logic) && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
if (!checkAiLogic(ai, sa, logic)) {
return false;
}
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return false;
}
} else {
@@ -86,14 +77,16 @@ public abstract class SpellAbilityAi {
}
}
if (!checkApiLogic(ai, sa)) {
return false;
if (sa.hasParam("AITgtBeforeCostEval")) {
// Cost payment requires a valid target to be specified, e.g. Quillmane Baku, so run the API logic first
// to set the target, then decide on paying costs (slower, so only use for cards where it matters)
return checkApiLogic(ai, sa) && (cost == null || willPayCosts(ai, sa, cost, source));
}
// needs to be after API logic because needs to check possible X Cost?
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return false;
}
return true;
return checkApiLogic(ai, sa);
}
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {
@@ -104,7 +97,7 @@ public abstract class SpellAbilityAi {
if (!con.getManaSpent().isEmpty()) {
// need to use ManaCostBeingPaid check, can't use Cost#canPay
ManaCostBeingPaid paid = new ManaCostBeingPaid(new ManaCost(new ManaCostParser(con.getManaSpent())));
if (ComputerUtilMana.canPayManaCost(paid, sa, ai, sa.isTrigger())) {
if (ComputerUtilMana.canPayManaCost(paid, sa, ai)) {
con.setManaSpent("");
}
}
@@ -119,7 +112,7 @@ public abstract class SpellAbilityAi {
if (aiLogic.equals("CheckCondition")) {
SpellAbility saCopy = sa.copy();
saCopy.setActivatingPlayer(ai);
return saCopy.metConditions();
return saCopy.getConditions().areMet(saCopy);
}
return !("Never".equals(aiLogic));
@@ -134,7 +127,7 @@ public abstract class SpellAbilityAi {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
@@ -162,39 +155,39 @@ public abstract class SpellAbilityAi {
*/
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop
return false; // prevent infinite loop
}
return MyRandom.getRandom().nextFloat() < .8f; // random success
return MyRandom.getRandom().nextFloat() < .8f; // random success
}
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
// this evaluation order is currently intentional as it does more stuff that helps avoiding some crashes
if (!ComputerUtilCost.canPayCost(sa, aiPlayer, true) && !mandatory) {
if (!ComputerUtilCost.canPayCost(sa, aiPlayer) && !mandatory) {
return false;
}
// a mandatory SpellAbility with targeting but without candidates,
// does not need to go any deeper
if (sa.usesTargeting() && mandatory && sa.getTargetRestrictions().getNumCandidates(sa, true) == 0) {
return sa.isTargetNumberValid();
if (sa.usesTargeting() && mandatory && !sa.getTargetRestrictions().hasCandidates(sa, true)) {
return false;
}
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory);
}
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory)
{
if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
return false;
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
}
/**
* Handles the AI decision to play a triggered SpellAbility
*/
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
if (canPlayWithoutRestrict(aiPlayer, sa)) {
return true;
}
@@ -213,7 +206,7 @@ public abstract class SpellAbilityAi {
// try to target opponent, then ally, then itself
for (final Player p : players) {
if (sa.canTarget(p)) {
if (p.canBeTargetedBy(sa) && sa.canTarget(p)) {
sa.resetTargets();
sa.getTargets().add(p);
return true;
@@ -232,7 +225,7 @@ public abstract class SpellAbilityAi {
// sub-SpellAbility might use targets too
if (sa.usesTargeting()) {
// no Candidates, no adding to Stack
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
if (!sa.getTargetRestrictions().hasCandidates(sa, true)) {
return false;
}
// but if it does, it should override this function
@@ -251,11 +244,10 @@ public abstract class SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
protected static boolean isSorcerySpeed(final SpellAbility sa) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|| (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
}
/**
@@ -272,7 +264,7 @@ public abstract class SpellAbilityAi {
// TODO probably also consider if winter orb or similar are out
if (sa instanceof AbilitySub) {
if (sa.getPayCosts() == null || sa instanceof AbilitySub) {
return true; // This is only true for Drawbacks and triggers
}
@@ -290,6 +282,7 @@ public abstract class SpellAbilityAi {
if (sa.isSpell() && !sa.isBuyBackAbility()) {
return false;
}
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
}
@@ -304,14 +297,14 @@ public abstract class SpellAbilityAi {
final AbilitySub subAb = ab.getSubAbility();
return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
System.err.println("Warning: default (ie. inherited from base class) implementation of confirmAction is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return true;
}
@SuppressWarnings("unchecked")
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer) {
boolean hasPlayer = false;
boolean hasCard = false;
boolean hasPlaneswalker = false;
@@ -328,11 +321,11 @@ public abstract class SpellAbilityAi {
}
if (hasPlayer && hasPlaneswalker) {
return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection<GameEntity>) options, params);
return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection<GameEntity>) options);
} else if (hasCard) {
return (T) chooseSingleCard(ai, sa, (Collection<Card>) options, isOptional, targetedPlayer, params);
return (T) chooseSingleCard(ai, sa, (Collection<Card>) options, isOptional, targetedPlayer);
} else if (hasPlayer) {
return (T) chooseSinglePlayer(ai, sa, (Collection<Player>) options, params);
return (T) chooseSinglePlayer(ai, sa, (Collection<Player>) options);
}
return null;
@@ -343,17 +336,17 @@ public abstract class SpellAbilityAi {
return spells.get(0);
}
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleCard is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(options, null);
}
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayer is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(options, null);
}
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayerOrPlaneswalker is used for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(options, null);
}

View File

@@ -1,14 +1,13 @@
package forge.ai;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import forge.ai.ability.*;
import forge.game.ability.ApiType;
import forge.util.ReflectionUtil;
import java.util.Map;
public enum SpellApiToAi {
Converter;
@@ -34,8 +33,8 @@ public enum SpellApiToAi {
.put(ApiType.BidLife, BidLifeAi.class)
.put(ApiType.Bond, BondAi.class)
.put(ApiType.Branch, AlwaysPlayAi.class)
.put(ApiType.Camouflage, ChooseCardAi.class)
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
.put(ApiType.CantUntapTurn, CantUntapTurnAi.class)
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
.put(ApiType.ChangeX, AlwaysPlayAi.class)
.put(ApiType.ChangeZone, ChangeZoneAi.class)
@@ -44,23 +43,19 @@ public enum SpellApiToAi {
.put(ApiType.ChooseCard, ChooseCardAi.class)
.put(ApiType.ChooseColor, ChooseColorAi.class)
.put(ApiType.ChooseDirection, ChooseDirectionAi.class)
.put(ApiType.ChooseEvenOdd, ChooseEvenOddAi.class)
.put(ApiType.ChooseNumber, ChooseNumberAi.class)
.put(ApiType.ChoosePlayer, ChoosePlayerAi.class)
.put(ApiType.ChooseSource, ChooseSourceAi.class)
.put(ApiType.ChooseType, ChooseTypeAi.class)
.put(ApiType.Clash, ClashAi.class)
.put(ApiType.ClassLevelUp, AlwaysPlayAi.class)
.put(ApiType.Cleanup, AlwaysPlayAi.class)
.put(ApiType.Clone, CloneAi.class)
.put(ApiType.CompanionChoose, ChooseCompanionAi.class)
.put(ApiType.CopyPermanent, CopyPermanentAi.class)
.put(ApiType.CopySpellAbility, CopySpellAbilityAi.class)
.put(ApiType.ControlPlayer, CannotPlayAi.class)
.put(ApiType.ControlSpell, CannotPlayAi.class)
.put(ApiType.Counter, CounterAi.class)
.put(ApiType.DamageAll, DamageAllAi.class)
.put(ApiType.DayTime, DayTimeAi.class)
.put(ApiType.DealDamage, DamageDealAi.class)
.put(ApiType.Debuff, DebuffAi.class)
.put(ApiType.DeclareCombatants, CannotPlayAi.class)
@@ -87,22 +82,18 @@ public enum SpellApiToAi {
.put(ApiType.Explore, ExploreAi.class)
.put(ApiType.Fight, FightAi.class)
.put(ApiType.FlipACoin, FlipACoinAi.class)
.put(ApiType.FlipOntoBattlefield, FlipOntoBattlefieldAi.class)
.put(ApiType.Fog, FogAi.class)
.put(ApiType.GainControl, ControlGainAi.class)
.put(ApiType.GainControlVariant, ControlGainVariantAi.class)
.put(ApiType.GainControlVariant, AlwaysPlayAi.class)
.put(ApiType.GainLife, LifeGainAi.class)
.put(ApiType.GainOwnership, CannotPlayAi.class)
.put(ApiType.GameDrawn, CannotPlayAi.class)
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
.put(ApiType.Goad, GoadAi.class)
.put(ApiType.Haunt, HauntAi.class)
.put(ApiType.ImmediateTrigger, ImmediateTriggerAi.class)
.put(ApiType.Investigate, InvestigateAi.class)
.put(ApiType.Learn, LearnAi.class)
.put(ApiType.ImmediateTrigger, AlwaysPlayAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class)
.put(ApiType.LosesGame, GameLossAi.class)
.put(ApiType.MakeCard, AlwaysPlayAi.class)
.put(ApiType.Mana, ManaEffectAi.class)
.put(ApiType.ManaReflected, CannotPlayAi.class)
.put(ApiType.Manifest, ManifestAi.class)
@@ -111,8 +102,8 @@ public enum SpellApiToAi {
.put(ApiType.MoveCounter, CountersMoveAi.class)
.put(ApiType.MultiplePiles, CannotPlayAi.class)
.put(ApiType.MultiplyCounter, CountersMultiplyAi.class)
.put(ApiType.MustAttack, MustAttackAi.class)
.put(ApiType.MustBlock, MustBlockAi.class)
.put(ApiType.Mutate, MutateAi.class)
.put(ApiType.NameCard, ChooseCardNameAi.class)
.put(ApiType.NoteCounters, AlwaysPlayAi.class)
.put(ApiType.PeekAndReveal, PeekAndRevealAi.class)
@@ -139,24 +130,18 @@ public enum SpellApiToAi {
.put(ApiType.RemoveCounter, CountersRemoveAi.class)
.put(ApiType.RemoveCounterAll, CannotPlayAi.class)
.put(ApiType.RemoveFromCombat, RemoveFromCombatAi.class)
.put(ApiType.RemoveFromGame, AlwaysPlayAi.class)
.put(ApiType.RemoveFromMatch, AlwaysPlayAi.class)
.put(ApiType.ReorderZone, AlwaysPlayAi.class)
.put(ApiType.Repeat, RepeatAi.class)
.put(ApiType.RepeatEach, RepeatEachAi.class)
.put(ApiType.ReplaceCounter, AlwaysPlayAi.class)
.put(ApiType.ReplaceEffect, AlwaysPlayAi.class)
.put(ApiType.ReplaceDamage, ReplaceDamageAi.class)
.put(ApiType.ReplaceMana, AlwaysPlayAi.class)
.put(ApiType.ReplaceSplitDamage, ReplaceDamageAi.class)
.put(ApiType.ReplaceToken, AlwaysPlayAi.class)
.put(ApiType.ReplaceDamage, AlwaysPlayAi.class)
.put(ApiType.ReplaceSplitDamage, AlwaysPlayAi.class)
.put(ApiType.RestartGame, RestartGameAi.class)
.put(ApiType.Reveal, RevealAi.class)
.put(ApiType.RevealHand, RevealHandAi.class)
.put(ApiType.ReverseTurnOrder, AlwaysPlayAi.class)
.put(ApiType.RollDice, RollDiceAi.class)
.put(ApiType.RollPlanarDice, RollPlanarDiceAi.class)
.put(ApiType.RunChaos, AlwaysPlayAi.class)
.put(ApiType.RunSVarAbility, AlwaysPlayAi.class)
.put(ApiType.Sacrifice, SacrificeAi.class)
.put(ApiType.SacrificeAll, SacrificeAllAi.class)
.put(ApiType.Scry, ScryAi.class)
@@ -164,10 +149,9 @@ public enum SpellApiToAi {
.put(ApiType.SetLife, LifeSetAi.class)
.put(ApiType.SetState, SetStateAi.class)
.put(ApiType.Shuffle, ShuffleAi.class)
.put(ApiType.SkipPhase, SkipPhaseAi.class)
.put(ApiType.SkipTurn, SkipTurnAi.class)
.put(ApiType.StoreMap, StoreMapAi.class)
.put(ApiType.StoreSVar, StoreSVarAi.class)
.put(ApiType.Subgame, AlwaysPlayAi.class)
.put(ApiType.Surveil, SurveilAi.class)
.put(ApiType.Tap, TapAi.class)
.put(ApiType.TapAll, TapAllAi.class)
@@ -179,7 +163,6 @@ public enum SpellApiToAi {
.put(ApiType.UnattachAll, UnattachAllAi.class)
.put(ApiType.Untap, UntapAi.class)
.put(ApiType.UntapAll, UntapAllAi.class)
.put(ApiType.Venture, VentureAi.class)
.put(ApiType.Vote, VoteAi.class)
.put(ApiType.WinsGame, GameWinAi.class)
@@ -200,6 +183,6 @@ public enum SpellApiToAi {
result = ReflectionUtil.makeDefaultInstanceOf(clz);
apiToInstance.put(api, result);
}
return result;
return result;
}
}

View File

@@ -1,8 +1,5 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -13,21 +10,26 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// AI cannot use this properly until he can use SAs during Humans turn
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Player opp = ai.getStrongestOpponent();
final Player opp = ai.getWeakestOpponent();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
if (list.isEmpty()) {
return false;
}
if (!sa.usesTargeting()) {
if (tgt == null) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (!defined.contains(opp)) {
@@ -35,20 +37,15 @@ public class ActivateAbilityAi extends SpellAbilityAi {
}
} else {
sa.resetTargets();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return false;
}
sa.getTargets().add(opp);
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return randomReturn;
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getStrongestOpponent();
final Player opp = ai.getWeakestOpponent();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
@@ -61,6 +58,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
return defined.contains(opp);
}
} else {
sa.resetTargets();
sa.getTargets().add(opp);
@@ -72,11 +70,12 @@ public class ActivateAbilityAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI cannot use this properly until he can use SAs during Humans turn
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
boolean randomReturn = true;
if (!sa.usesTargeting()) {
if (tgt == null) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(ai)) {

View File

@@ -17,17 +17,15 @@
*/
package forge.ai.ability;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
/**
* <p>
* AbilityFactory_Turns class.
@@ -38,14 +36,14 @@ import forge.game.spellability.SpellAbility;
*/
public class AddTurnAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
final Player opp = ai.getWeakestOpponent();
if (sa.usesTargeting()) {
sa.resetTargets();
if (sa.canTarget(ai) && (mandatory || !ai.getGame().getReplacementHandler().wouldExtraTurnBeSkipped(ai))) {
if (sa.canTarget(ai)) {
sa.getTargets().add(ai);
} else if (mandatory) {
for (final Player ally : ai.getAllies()) {
@@ -54,7 +52,7 @@ public class AddTurnAi extends SpellAbilityAi {
break;
}
}
if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) {
if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return false;

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
@@ -9,11 +7,7 @@ import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
import forge.game.phase.PhaseHandler;
import forge.game.player.Player;
@@ -24,44 +18,46 @@ import forge.game.zone.ZoneType;
public class AmassAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army");
CardCollection aiArmies = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army"));
Card host = sa.getHostCard();
final Game game = ai.getGame();
if (!aiArmies.isEmpty()) {
return CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterEnumType.P1P1)) > 0;
}
final String tokenScript = "b_0_0_zombie_army";
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
return CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterType.P1P1)) > 0;
} else {
final String tokenScript = "b_0_0_zombie_army";
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
Card token = TokenInfo.getProtoType(tokenScript, sa);
if (token == null) {
return false;
if (token == null) {
return false;
}
token.setController(ai, 0);
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
boolean result = true;
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(token);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
if (token.canReceiveCounters(CounterType.P1P1)) {
token.setCounters(CounterType.P1P1, amount);
}
if (token.isCreature() && token.getNetToughness() < 1) {
result = false;
}
//reset static abilities
game.getAction().checkStaticAbilities(false);
return result;
}
token.setController(ai, 0);
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
boolean result = true;
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(token);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
if (token.canReceiveCounters(CounterEnumType.P1P1)) {
token.setCounters(CounterEnumType.P1P1, amount);
}
if (token.isCreature() && token.getNetToughness() < 1) {
result = false;
}
//reset static abilities
game.getAction().checkStaticAbilities(false);
return result;
}
@Override
@@ -90,11 +86,12 @@ public class AmassAi extends SpellAbilityAi {
}
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
Iterable<Card> better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
Iterable<Card> better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterType.P1P1));
if (Iterables.isEmpty(better)) {
better = options;
}
return ComputerUtilCard.getBestAI(better);
}
}

View File

@@ -1,30 +1,36 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
import forge.card.CardType;
import forge.card.ColorSet;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.AnimateEffectBase;
import forge.game.card.*;
import forge.game.cost.CostPutCounter;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import forge.game.ability.effects.AnimateEffectBase;
/**
* <p>
* AbilityFactoryAnimate class.
@@ -37,13 +43,15 @@ import java.util.Map;
public class AnimateAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
if ("Attacking".equals(aiLogic)) { // Launch the Fleet
if (ph.getPlayerTurn().isOpponentOf(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
List<Card> list = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
List<Card> list = CardLists.getValidCards(ai.getCreaturesInPlay(), tgt.getValidTgts(), ai, source, sa);
for (Card c : list) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, c)) {
sa.getTargets().add(c);
@@ -66,18 +74,19 @@ public class AnimateAi extends SpellAbilityAi {
SpellAbility topStack = game.getStack().peekAbility();
if (topStack.getApi() == ApiType.Sacrifice) {
final String valid = topStack.getParamOrDefault("SacValid", "Card.Self");
String num = topStack.getParamOrDefault("Amount", "1");
String num = topStack.getParam("Amount");
num = (num == null) ? "1" : num;
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid,
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
ai.getWeakestOpponent(), topStack.getHostCard(), topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true));
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
ComputerUtilCard.sortByEvaluateCreature(list);
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai, sa.isTrigger())) {
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
Card animatedCopy = becomeAnimated(source, sa);
list.add(animatedCopy);
list = CardLists.getValidCards(list, valid, ai.getWeakestOpponent(), topStack.getHostCard(),
list = CardLists.getValidCards(list, valid.split(","), ai.getWeakestOpponent(), topStack.getHostCard(),
topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true));
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
&& list.contains(animatedCopy)) {
return true;
@@ -86,31 +95,31 @@ public class AnimateAi extends SpellAbilityAi {
}
}
// Don't use instant speed animate abilities before AI's COMBAT_BEGIN
if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("ActivationPhases") && !"Permanent".equals(sa.getParam("Duration"))) {
if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false;
}
// Don't use instant speed animate abilities outside human's
// COMBAT_DECLARE_ATTACKERS or if no attackers
if (ph.getPlayerTurn().isOpponentOf(ai) && !"Permanent".equals(sa.getParam("Duration"))
if (ph.getPlayerTurn().isOpponentOf(ai) && !sa.hasParam("Permanent")
&& (!ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| ph.inCombat() && game.getCombat().getAttackersOf(ai).isEmpty())) {
|| game.getCombat() != null && game.getCombat().getAttackersOf(ai).isEmpty())) {
return false;
}
// Don't activate during MAIN2 unless this effect is permanent
if (ph.is(PhaseType.MAIN2) && !"Permanent".equals(sa.getParam("Duration")) && !"UntilYourNextTurn".equals(sa.getParam("Duration"))) {
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) {
return false;
}
// Don't animate if the AI won't attack anyway or use as a potential blocker
Player opponent = ai.getWeakestOpponent();
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
// the AI will waste resources
boolean activateAsPotentialBlocker = "UntilYourNextTurn".equals(sa.getParam("Duration"))
&& game.getPhaseHandler().getNextTurn() != ai
boolean activateAsPotentialBlocker = sa.hasParam("UntilYourNextTurn")
&& ai.getGame().getPhaseHandler().getNextTurn() != ai
&& source.isPermanent();
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
&& opponent.getZone(ZoneType.Battlefield).contains(CardPredicates.Presets.CREATURES)
&& !sa.hasParam("AILogic") && !"Permanent".equals(sa.getParam("Duration")) && !activateAsPotentialBlocker) {
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) {
return false;
}
return true;
@@ -121,26 +130,18 @@ public class AnimateAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
final Game game = aiPlayer.getGame();
final PhaseHandler ph = game.getPhaseHandler();
if (!sa.metConditions() && sa.getSubAbility() == null) {
return false; // what is this for?
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
return false; // what is this for?
}
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
if (!isAnimatedThisTurn(aiPlayer, source)) {
rememberAnimatedThisTurn(aiPlayer, source);
return true; // interrupt sacrifice
if (!AnimateAi.isAnimatedThisTurn(aiPlayer, source)) {
this.rememberAnimatedThisTurn(aiPlayer, source);
return true; // interrupt sacrifice
}
}
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa)) {
return false; // prevent crewing with equal or better creatures
return false; // prevent crewing with equal or better creatures
}
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
sa.setXManaCostPaid(xPay);
}
if (!sa.usesTargeting()) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
@@ -151,7 +152,7 @@ public class AnimateAi extends SpellAbilityAi {
&& !c.isEquipping();
// for creatures that could be improved (like Figure of Destiny)
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
@@ -168,13 +169,13 @@ public class AnimateAi extends SpellAbilityAi {
}
}
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
}
if (!SpellAbilityAi.isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) {
if (sa.hasParam("Crew") && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return false;
@@ -193,7 +194,7 @@ public class AnimateAi extends SpellAbilityAi {
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
@@ -201,7 +202,7 @@ public class AnimateAi extends SpellAbilityAi {
}
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
this.rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
}
return bFlag; // All of the defined stuff is animated, not very useful
} else {
@@ -228,40 +229,42 @@ public class AnimateAi extends SpellAbilityAi {
} else if (sa.usesTargeting() && mandatory) {
// fallback if animate is mandatory
sa.resetTargets();
List<Card> list = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa);
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
CardCollectionView list = aiPlayer.getGame().getCardsIn(tgt.getZone());
list = CardLists.getValidCards(list, tgt.getValidTgts(), aiPlayer, source, sa);
if (list.isEmpty()) {
return false;
}
Card toAnimate = ComputerUtilCard.getWorstAI(list);
rememberAnimatedThisTurn(aiPlayer, toAnimate);
this.rememberAnimatedThisTurn(aiPlayer, toAnimate);
sa.getTargets().add(toAnimate);
}
return true;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2);
}
private boolean animateTgtAI(final SpellAbility sa) {
final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean alwaysActivatePWAbility = sa.isPwAbility()
final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker")
&& sa.getPayCosts() != null
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
&& sa.usesTargeting()
&& sa.getTargetRestrictions() != null
&& sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0;
final CardType types = new CardType(true);
final CardType types = new CardType();
if (sa.hasParam("Types")) {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
// something is used for animate into creature
if (types.isCreature()) {
final Game game = ai.getGame();
CardCollectionView list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
CardCollectionView list = ai.getGame().getCardsIn(tgt.getZone());
list = CardLists.getValidCards(list, tgt.getValidTgts(), ai, source, sa);
// need to targetable
list = CardLists.getTargetableCards(list, sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
@@ -274,7 +277,7 @@ public class AnimateAi extends SpellAbilityAi {
Map<Card, Integer> data = Maps.newHashMap();
for (final Card c : list) {
// don't use Permanent animate on something that would leave the field
if (c.hasSVar("EndOfTurnLeavePlay") && "Permanent".equals(sa.getParam("Duration"))) {
if (c.hasSVar("EndOfTurnLeavePlay") && sa.hasParam("Permanent")) {
continue;
}
@@ -301,16 +304,16 @@ public class AnimateAi extends SpellAbilityAi {
// evaluate their value to check if it becomes better
if (c.isCreature()) {
int cValue = ComputerUtilCard.evaluateCreature(c);
if (cValue >= aValue)
if (cValue <= aValue)
continue;
}
// if its player turn,
// check if its Permanent or that creature would attack
if (ph.isPlayerTurn(ai)) {
if (!"Permanent".equals(sa.getParam("Duration"))
if (!sa.hasParam("Permanent")
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
&& !"UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
&& !sa.hasParam("UntilHostLeavesPlay")) {
continue;
}
}
@@ -338,38 +341,16 @@ public class AnimateAi extends SpellAbilityAi {
// select the worst of the best
final Card worst = ComputerUtilCard.getWorstAI(maxList);
if (worst != null) {
if (worst.isLand()) {
// e.g. Clan Guildmage, make sure we're not using the same land we want to animate to activate the ability
holdAnimatedTillMain2(ai, worst);
if (!ComputerUtilMana.canPayManaCost(sa, ai, 0, sa.isTrigger())) {
releaseHeldTillMain2(ai, worst);
return false;
}
}
rememberAnimatedThisTurn(ai, worst);
sa.getTargets().add(worst);
}
return true;
this.rememberAnimatedThisTurn(ai, worst);
sa.getTargets().add(worst);
return true;
}
if (logic.equals("SetPT")) {
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
Card buffed = becomeAnimated(worst, sa);
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, buffed)
&& (buffed.getNetPower() - worst.getNetPower() >= 3 || !ComputerUtilCard.doesCreatureAttackAI(ai, worst))) {
sa.getTargets().add(worst);
rememberAnimatedThisTurn(ai, worst);
return true;
}
}
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until
// this can do a reasonably good job of picking a good target
// this can do a reasonably
// good job of picking a good target
return false;
}
@@ -383,6 +364,7 @@ public class AnimateAi extends SpellAbilityAi {
// duplicating AnimateEffect.resolve
final Card source = sa.getHostCard();
final Game game = sa.getActivatingPlayer().getGame();
final Map<String, String> svars = source.getSVars();
final long timestamp = game.getNextTimestamp();
card.setSickness(hasOriginalCardSickness);
@@ -402,12 +384,12 @@ public class AnimateAi extends SpellAbilityAi {
}
}
final CardType types = new CardType(true);
final CardType types = new CardType();
if (sa.hasParam("Types")) {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
final CardType removeTypes = new CardType(true);
final CardType removeTypes = new CardType();
if (sa.hasParam("RemoveTypes")) {
removeTypes.addAll(Arrays.asList(sa.getParam("RemoveTypes").split(",")));
}
@@ -435,22 +417,23 @@ public class AnimateAi extends SpellAbilityAi {
// allow SVar substitution for keywords
for (int i = 0; i < keywords.size(); i++) {
final String k = keywords.get(i);
if (source.hasSVar(k)) {
keywords.add(source.getSVar(k));
if (svars.containsKey(k)) {
keywords.add(svars.get(k));
keywords.remove(k);
}
}
// colors to be added or changed to
ColorSet finalColors = ColorSet.getNullColor();
String tmpDesc = "";
if (sa.hasParam("Colors")) {
final String colors = sa.getParam("Colors");
if (colors.equals("ChosenColor")) {
finalColors = ColorSet.fromNames(source.getChosenColors());
tmpDesc = CardUtil.getShortColorsString(source.getChosenColors());
} else {
finalColors = ColorSet.fromNames(colors.split(","));
tmpDesc = CardUtil.getShortColorsString(Lists.newArrayList(Arrays.asList(colors.split(","))));
}
}
final String finalDesc = tmpDesc;
// abilities to add to the animated being
final List<String> abilities = Lists.newArrayList();
@@ -482,15 +465,75 @@ public class AnimateAi extends SpellAbilityAi {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
}
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalColors,
keywords, removeKeywords, hiddenKeywords,
abilities, triggers, replacements, stAbs,
timestamp);
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, keywords, removeKeywords, hiddenKeywords, timestamp);
// check if animate added static Abilities
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) {
// remove abilities
final List<SpellAbility> removedAbilities = Lists.newArrayList();
boolean clearSpells = sa.hasParam("OverwriteSpells");
boolean removeAll = sa.hasParam("RemoveAllAbilities");
boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
if (clearSpells) {
removedAbilities.addAll(Lists.newArrayList(card.getSpells()));
}
if (sa.hasParam("RemoveThisAbility") && !removedAbilities.contains(sa)) {
removedAbilities.add(sa);
}
// give abilities
final List<SpellAbility> addedAbilities = Lists.newArrayList();
if (abilities.size() > 0) {
for (final String s : abilities) {
final String actualAbility = source.getSVar(s);
addedAbilities.add(AbilityFactory.getAbility(actualAbility, card));
}
}
// Grant triggers
final List<Trigger> addedTriggers = Lists.newArrayList();
if (triggers.size() > 0) {
for (final String s : triggers) {
final String actualTrigger = source.getSVar(s);
final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, card, false);
addedTriggers.add(parsedTrigger);
}
}
// give replacement effects
final List<ReplacementEffect> addedReplacements = Lists.newArrayList();
if (replacements.size() > 0) {
for (final String s : replacements) {
final String actualReplacement = source.getSVar(s);
final ReplacementEffect parsedReplacement = ReplacementHandler.parseReplacement(actualReplacement, card, false);
addedReplacements.add(parsedReplacement);
}
}
// give static abilities (should only be used by cards to give
// itself a static ability)
final List<StaticAbility> addedStaticAbilities = Lists.newArrayList();
if (stAbs.size() > 0) {
for (final String s : stAbs) {
final String actualAbility = source.getSVar(s);
addedStaticAbilities.add(new StaticAbility(actualAbility, card));
}
}
if (removeAll || removeIntrinsic
|| !addedAbilities.isEmpty() || !removedAbilities.isEmpty() || !addedTriggers.isEmpty()
|| !addedReplacements.isEmpty() || !addedStaticAbilities.isEmpty()) {
card.addChangedCardTraits(addedAbilities, removedAbilities, addedTriggers, addedReplacements,
addedStaticAbilities, removeAll, false, removeIntrinsic, timestamp);
}
// give static abilities (should only be used by cards to give
// itself a static ability)
if (stAbs.size() > 0) {
for (final String s : stAbs) {
final String actualAbility = source.getSVar(s);
final StaticAbility stAb = card.addStaticAbility(actualAbility);
if ("Continuous".equals(stAb.getParam("Mode"))) {
for (final StaticAbilityLayer layer : stAb.getLayers()) {
StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer);
@@ -522,12 +565,4 @@ public class AnimateAi extends SpellAbilityAi {
public static boolean isAnimatedThisTurn(Player ai, Card c) {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
}
private void holdAnimatedTillMain2(Player ai, Card c) {
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
}
private void releaseHeldTillMain2(Player ai, Card c) {
AiCardMemory.forgetCard(ai, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
}
}

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -10,23 +8,12 @@ public class AnimateAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String logic = sa.getParamOrDefault("AILogic", "");
if ("CreatureAdvantage".equals(logic) && !aiPlayer.getCreaturesInPlay().isEmpty()) {
// TODO: improve this or implement a better logic for abilities like Oko, the Trickster ultimate
for (Card c : aiPlayer.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(aiPlayer, c)) {
return true;
}
}
}
return "Always".equals(logic);
return "Always".equals(sa.getParam("AILogic"));
} // end animateAllCanPlayAI()
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
return mandatory;
}
}
} // end class AbilityFactoryAnimate

View File

@@ -1,37 +1,17 @@
package forge.ai.ability;
import java.util.*;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiAttackController;
import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
@@ -49,9 +29,10 @@ import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import java.util.*;
public class AttachAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -88,6 +69,11 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !"Curse".equals(sa.getParam("AILogic"))) {
return false;
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
@@ -107,27 +93,33 @@ public class AttachAi extends SpellAbilityAi {
if (ai.getController().isAI()) {
advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC);
}
if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai)))
&& source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
if (source.withFlash(ai) && source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
return false;
}
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. (Endless Scream and Venarian Gold)
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (abCost.getTotalMana().countX() > 0 && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. (Endless Scream and Venarian
// Gold)
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (xPay == 0) {
return false;
}
sa.setXManaCostPaid(xPay);
source.setSVar("PayX", Integer.toString(xPay));
}
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
effectExile.setActivatingPlayer(ai);
final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0);
final TargetRestrictions exile_tgt = effectExile.getTargetRestrictions();
final List<Card> targets = CardUtil.getValidCardsToTarget(exile_tgt, effectExile);
final CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(origin), exile_tgt.getValidTgts(), ai, source, effectExile);
final CardCollection targets = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
}
});
return !targets.isEmpty();
}
@@ -227,7 +219,7 @@ public class AttachAi extends SpellAbilityAi {
boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
boolean willDiscardNow = game.getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
&& ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
boolean willRespondToStack = canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack);
boolean willCastEarly = MyRandom.percentTrue(chanceToCastEarly);
@@ -347,14 +339,14 @@ public class AttachAi extends SpellAbilityAi {
public boolean apply(final Card c) {
//Check for cards that can be sacrificed in response
for (final SpellAbility ability : c.getAllSpellAbilities()) {
if (ability.isActivatedAbility()) {
if (ability.isAbility()) {
final Cost cost = ability.getPayCosts();
for (final CostPart part : cost.getCostParts()) {
if (!(part instanceof CostSacrifice)) {
continue;
}
CostSacrifice sacCost = (CostSacrifice) part;
if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController(), false)) {
if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController())) {
return false;
}
}
@@ -406,7 +398,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.isCreature() && !c.getType().hasSubtype("Vehicle") && !c.isTapped()) {
// try to identify if this thing can actually tap
for (SpellAbility ab : c.getAllSpellAbilities()) {
if (ab.getPayCosts().hasTapCost()) {
if (ab.getPayCosts() != null && ab.getPayCosts().hasTapCost()) {
return true;
}
}
@@ -425,7 +417,8 @@ public class AttachAi extends SpellAbilityAi {
SpellAbility auraSA = aura.getSpells().get(0);
if (auraSA.getApi() == ApiType.Attach) {
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) {
// Don't attach multiple KeepTapped Auras to one card
// Don't attach multiple KeepTapped Auras to one
// card
return false;
}
}
@@ -446,15 +439,22 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach to player ai preferences.
*
* @param sa
* the sa
* @param mandatory
* the mandatory
* @param newParam TODO
*
* @return the player
*/
public static Player attachToPlayerAIPreferences(final Player aiPlayer, final SpellAbility sa, final boolean mandatory, List<Player> targetable) {
private static Player attachToPlayerAIPreferences(final Player aiPlayer, final SpellAbility sa,
final boolean mandatory) {
List<Player> targetable = new ArrayList<>();
for (final Player player : aiPlayer.getGame().getPlayers()) {
if (sa.canTarget(player)) {
targetable.add(player);
}
}
if ("Curse".equals(sa.getParam("AILogic"))) {
if (!mandatory) {
targetable.removeAll(aiPlayer.getAllies());
@@ -538,7 +538,12 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
evenBetterList = CardLists.filter(betterList, CardPredicates.Presets.UNTAPPED);
evenBetterList = CardLists.filter(betterList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.isUntapped();
}
});
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
@@ -555,7 +560,7 @@ public class AttachAi extends SpellAbilityAi {
@Override
public boolean apply(final Card c) {
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
return false;
}
}
@@ -712,15 +717,17 @@ public class AttachAi extends SpellAbilityAi {
*/
private static Card attachAISpecificCardPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource) {
// I know this isn't much better than Hardcoding, but some cards need it for now
final Player ai = sa.getActivatingPlayer();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
Card chosen = null;
if ("Guilty Conscience".equals(sourceName)) {
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
} else if (sa.hasParam("AIValid")) {
// TODO: Make the AI recognize which cards to pump based on the card's abilities alone
chosen = doPumpOrCurseAILogic(ai, sa, list, sa.getParam("AIValid"));
} else if ("Bonds of Faith".equals(sourceName)) {
chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
} else if ("Clutch of Undeath".equals(sourceName)) {
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
}
// If Mandatory (brought directly into play without casting) gotta
@@ -744,7 +751,7 @@ public class AttachAi extends SpellAbilityAi {
int powerBuff = 0;
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) {
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), stAb);
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), null);
}
}
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
@@ -793,6 +800,7 @@ public class AttachAi extends SpellAbilityAi {
// grab Planeswalker first, then Creature
// if Life < 5 grab Creature first, then Planeswalker. Lands,
// Enchantments and Artifacts are probably "not good enough"
}
final Card c = ComputerUtilCard.getBestAI(list);
@@ -830,7 +838,7 @@ public class AttachAi extends SpellAbilityAi {
* @return the card
*/
private static Card attachAICursePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource, final Player ai) {
final Card attachSource) {
// AI For choosing a Card to Curse of.
// TODO Figure out some way to combine The "gathering of data" from
@@ -910,7 +918,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
return ComputerUtilCombat.canAttackNextTurn(c) && c.getNetPower() > 0;
}
});
}
@@ -922,24 +930,6 @@ public class AttachAi extends SpellAbilityAi {
);
}
// If this is already attached and there's a sac cost, make sure we attach to something that's
// seriously better than whatever the attachment is currently attached to (e.g. Bound by Moonsilver)
if (sa.getHostCard().getAttachedTo() != null && sa.getHostCard().getAttachedTo().isCreature()
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
final int oldEvalRating = ComputerUtilCard.evaluateCreature(sa.getHostCard().getAttachedTo());
final int threshold = ai.isAI() ? ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.SAC_TO_REATTACH_TARGET_EVAL_THRESHOLD) : Integer.MAX_VALUE;
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
if (!card.isCreature()) {
return false;
}
return ComputerUtilCard.evaluateCreature(card) >= oldEvalRating + threshold;
}
});
}
c = ComputerUtilCard.getBestAI(prefList);
if (c == null) {
@@ -949,6 +939,7 @@ public class AttachAi extends SpellAbilityAi {
return acceptableChoice(c, mandatory);
}
/**
* Attach do trigger ai.
* @param sa
@@ -965,10 +956,10 @@ public class AttachAi extends SpellAbilityAi {
List<GameObject> targets = new ArrayList<>();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
targets = AbilityUtils.getDefinedObjects(card, sa.getParam("Defined"), sa);
targets = AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
} else {
attachPreference(sa, tgt, mandatory);
targets = sa.getTargets();
AttachAi.attachPreference(sa, tgt, mandatory);
targets = sa.getTargets().getTargets();
}
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
@@ -1008,13 +999,7 @@ public class AttachAi extends SpellAbilityAi {
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
GameObject o;
if (tgt.canTgtPlayer()) {
List<Player> targetable = new ArrayList<>();
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) {
targetable.add(player);
}
}
o = attachToPlayerAIPreferences(sa.getActivatingPlayer(), sa, mandatory, targetable);
o = attachToPlayerAIPreferences(sa.getActivatingPlayer(), sa, mandatory);
} else {
o = attachToCardAIPreferences(sa.getActivatingPlayer(), sa, mandatory);
}
@@ -1046,7 +1031,7 @@ public class AttachAi extends SpellAbilityAi {
Card c = null;
List<Card> magnetList = null;
String stCheck = null;
if (attachSource.isAura() || sa.isBestow()) {
if (attachSource.isAura() || sa.hasParam("Bestow")) {
stCheck = "EnchantedBy";
magnetList = CardLists.filter(list, new Predicate<Card>() {
@Override
@@ -1087,7 +1072,12 @@ public class AttachAi extends SpellAbilityAi {
final Map<String, String> params = t.getMapParams();
if ("Card.Self".equals(params.get("ValidCard"))
&& "Battlefield".equals(params.get("Destination"))) {
SpellAbility trigSa = t.ensureAbility();
SpellAbility trigSa = null;
if (t.hasParam("Execute") && attachSource.hasSVar(t.getParam("Execute"))) {
trigSa = AbilityFactory.getAbility(attachSource.getSVar(params.get("Execute")), attachSource);
} else if (t.getOverridingAbility() != null) {
trigSa = t.getOverridingAbility();
}
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
for (Card target : list) {
if (!target.getController().isOpponentOf(ai)) {
@@ -1104,12 +1094,14 @@ public class AttachAi extends SpellAbilityAi {
list.removeAll(toRemove);
if (magnetList != null) {
// Look for Heroic triggers
if (magnetList.isEmpty() && sa.isSpell()) {
for (Card target : list) {
for (Trigger t : target.getTriggers()) {
if (t.getMode() == TriggerType.SpellCast) {
if ("Card.Self".equals(t.getParam("TargetsValid")) && "You".equals(t.getParam("ValidActivatingPlayer"))) {
final Map<String, String> params = t.getMapParams();
if ("Card.Self".equals(params.get("TargetsValid")) && "You".equals(params.get("ValidActivatingPlayer"))) {
magnetList.add(target);
break;
}
@@ -1154,6 +1146,7 @@ public class AttachAi extends SpellAbilityAi {
final List<String> keywords = new ArrayList<>();
boolean grantingAbilities = false;
boolean grantingExtraBlock = false;
boolean grantingCantUntap = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams();
@@ -1167,12 +1160,13 @@ public class AttachAi extends SpellAbilityAi {
if (affected == null) {
continue;
}
if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility");
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
grantingCantUntap |= stabMap.containsKey("CantUntap");
String kws = stabMap.get("AddKeyword");
if (kws != null) {
@@ -1205,6 +1199,7 @@ public class AttachAi extends SpellAbilityAi {
if (totToughness + totPower < 4 && (!keywords.isEmpty() || grantingExtraBlock)) {
final int pow = totPower;
final boolean extraBlock = grantingExtraBlock;
final boolean cantUntap = grantingCantUntap;
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -1223,6 +1218,9 @@ public class AttachAi extends SpellAbilityAi {
if (extraBlock && CombatUtil.canBlock(c, true) && !c.canBlockAny()) {
return true;
}
if (cantUntap && c.isTapped()) {
return true;
}
return false;
}
});
@@ -1272,7 +1270,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.isCreature()) {
return true;
}
return powerBonus + c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
return ComputerUtilCombat.canAttackNextTurn(c) && powerBonus + c.getNetPower() > 0;
}
});
}
@@ -1317,28 +1315,6 @@ public class AttachAi extends SpellAbilityAi {
return null;
}
// Is a SA that moves target attachment
if ("MoveTgtAura".equals(sa.getParam("AILogic"))) {
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa));
list = CardLists.filter(list, Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()), new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
return ComputerUtilCard.isUselessCreature(aiPlayer, card.getAttachedTo());
}
}));
return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null;
} else if ("Unenchanted".equals(sa.getParam("AILogic"))) {
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa));
CardCollection preferred = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
return !card.hasCardAttachments();
}
});
return preferred.isEmpty() ? Aggregates.random(list) : Aggregates.random(preferred);
}
// is no attachment so no using attach
if (!attachSource.isAttachment()) {
return null;
@@ -1352,9 +1328,21 @@ public class AttachAi extends SpellAbilityAi {
CardCollection list = null;
if (tgt == null) {
list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa);
list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
} else {
list = CardLists.filter(CardUtil.getValidCardsToTarget(tgt, sa), CardPredicates.canBeAttached(attachSource));
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
list = CardLists.filter(list, CardPredicates.canBeAttached(attachSource));
// TODO If Attaching without casting, don't need to actually target.
// I believe this is the only case where mandatory will be true, so just
// check that when starting that work
// But we shouldn't attach to things with Protection
if (!mandatory) {
list = CardLists.getTargetableCards(list, sa);
} else {
list = CardLists.filter(list, Predicates.not(CardPredicates.isProtectedFrom(attachSource)));
}
}
if (list.isEmpty()) {
@@ -1371,7 +1359,7 @@ public class AttachAi extends SpellAbilityAi {
if (c != null && attachSource.isEquipment()
&& attachSource.isEquipping()
&& attachSource.getEquipping().getController() == aiPlayer) {
if (c.equals(attachSource.getEquipping()) && !mandatory) {
if (c.equals(attachSource.getEquipping())) {
// Do not equip if equipping the same card already
return null;
}
@@ -1382,13 +1370,13 @@ public class AttachAi extends SpellAbilityAi {
boolean uselessCreature = ComputerUtilCard.isUselessCreature(aiPlayer, attachSource.getEquipping());
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never") && !mandatory) {
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) {
// Do not equip other creatures if the AI profile does not allow moving equipment around
return null;
} else if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("from_useless_only")) {
// Do not equip other creatures if the AI profile only allows moving equipment from useless creatures
// and the equipped creature is still useful (not non-untapping+tapped and not set to can't attack/block)
if (!uselessCreature && !mandatory) {
if (!uselessCreature) {
return null;
}
}
@@ -1404,13 +1392,13 @@ public class AttachAi extends SpellAbilityAi {
}
// avoid randomly moving the equipment back and forth between several creatures in one turn
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN) && !mandatory) {
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN)) {
return null;
}
// do not equip if the new creature is not significantly better than the previous one (evaluates at least better by evalT)
int evalT = aic.getIntProperty(AiProps.MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD);
if (!decideMoveFromUseless && ComputerUtilCard.evaluateCreature(c) - ComputerUtilCard.evaluateCreature(attachSource.getEquipping()) < evalT && !mandatory) {
if (!decideMoveFromUseless && ComputerUtilCard.evaluateCreature(c) - ComputerUtilCard.evaluateCreature(attachSource.getEquipping()) < evalT) {
return null;
}
}
@@ -1439,22 +1427,12 @@ public class AttachAi extends SpellAbilityAi {
* the logic
* @return the card
*/
public static Card attachGeneralAI(final Player ai, final SpellAbility sa, final List<Card> list, final boolean mandatory,
private static Card attachGeneralAI(final Player ai, final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource, final String logic) {
// AI logic types that do not require a prefList and that evaluate the
// usefulness of attach action autonomously
if ("InstantReequipPowerBuff".equals(logic)) {
return attachAIInstantReequipPreference(sa, attachSource);
}
Player prefPlayer;
if ("Pump".equals(logic) || "Animate".equals(logic) || "Curiosity".equals(logic) || "MoveTgtAura".equals(logic)
|| "MoveAllAuras".equals(logic)) {
Player prefPlayer = ai.getWeakestOpponent();
if ("Pump".equals(logic) || "Animate".equals(logic) || "Curiosity".equals(logic)) {
prefPlayer = ai;
} else {
prefPlayer = AiAttackController.choosePreferredDefenderPlayer(ai);
}
// Some ChangeType cards are beneficial, and PrefPlayer should be
// changed to represent that
final List<Card> prefList;
@@ -1466,18 +1444,25 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filterControlledBy(list, prefPlayer);
}
// AI logic types that do not require a prefList and that evaluate the
// usefulness of attach action autonomously
if ("InstantReequipPowerBuff".equals(logic)) {
return attachAIInstantReequipPreference(sa, attachSource);
}
// If there are no preferred cards, and not mandatory bail out
if (logic == null || prefList.isEmpty()) {
if (prefList.isEmpty()) {
return chooseUnpreferred(mandatory, list);
}
// Preferred list has at least one card in it to make to the actual Logic
// Preferred list has at least one card in it to make to the actual
// Logic
Card c = null;
if ("GainControl".equals(logic)) {
c = attachAIControlPreference(sa, prefList, mandatory, attachSource);
} else if ("Curse".equals(logic)) {
c = attachAICursePreference(sa, prefList, mandatory, attachSource, ai);
} else if ("Pump".equals(logic) || logic.startsWith("Move")) {
c = attachAICursePreference(sa, prefList, mandatory, attachSource);
} else if ("Pump".equals(logic)) {
c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource);
} else if ("Curiosity".equals(logic)) {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
@@ -1555,10 +1540,11 @@ public class AttachAi extends SpellAbilityAi {
}
}
final boolean evasive = keyword.equals("Unblockable") || keyword.equals("Fear")
final boolean evasive = (keyword.equals("Unblockable") || keyword.equals("Fear")
|| keyword.equals("Intimidate") || keyword.equals("Shadow")
|| keyword.equals("Flying") || keyword.equals("Horsemanship")
|| keyword.endsWith("walk") || keyword.equals("All creatures able to block CARDNAME do so.");
|| keyword.endsWith("walk") || keyword.startsWith("CantBeBlockedBy")
|| keyword.equals("All creatures able to block CARDNAME do so."));
// give evasive keywords to creatures that can attack and deal damage
boolean canBeBlocked = false;
@@ -1570,11 +1556,12 @@ public class AttachAi extends SpellAbilityAi {
if (evasive) {
return card.getNetCombatDamage() + powerBonus > 0
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
} else if (keyword.equals("Haste")) {
return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped()
&& card.getNetCombatDamage() + powerBonus > 0
&& !card.hasKeyword("CARDNAME can attack as though it had haste.")
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("Indestructible")) {
@@ -1587,12 +1574,12 @@ public class AttachAi extends SpellAbilityAi {
return card.getNetCombatDamage() + powerBonus > 0
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.equals("First Strike")) {
return card.getNetCombatDamage() + powerBonus > 0 && !card.hasDoubleStrike()
return card.getNetCombatDamage() + powerBonus > 0 && !card.hasKeyword(Keyword.DOUBLE_STRIKE)
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
} else if (keyword.startsWith("Bushido")) {
return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card))
|| CombatUtil.canBlock(card, true);
@@ -1634,8 +1621,10 @@ public class AttachAi extends SpellAbilityAi {
if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender")
|| keyword.endsWith("CARDNAME can't attack or block.")) {
return card.getNetCombatDamage() >= 1 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME can't block.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
} else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty();
} else if (keyword.endsWith("CARDNAME can't block.") || keyword.contains("CantBlock")) {
return CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
for (SpellAbility ability : card.getSpellAbilities()) {
@@ -1645,12 +1634,10 @@ public class AttachAi extends SpellAbilityAi {
}
return false;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 1 && ComputerUtilCombat.canAttackNextTurn(card);
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 2;
}
return true;
}
@@ -1688,7 +1675,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.getController().equals(ai)) {
return false;
}
return c.isValid(type, ai, sa.getHostCard(), sa);
return c.getType().hasCreatureType(type);
}
});
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
@@ -1698,7 +1685,7 @@ public class AttachAi extends SpellAbilityAi {
if (c.getController().equals(ai)) {
return false;
}
return !c.isValid(type, ai, sa.getHostCard(), sa) && !ComputerUtilCard.isUselessCreature(ai, c);
return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
}
});
@@ -1715,38 +1702,18 @@ public class AttachAi extends SpellAbilityAi {
return chosen;
}
@Override
public boolean chkAIDrawback(final SpellAbility sa, final Player ai) {
// TODO for targeting optional Halvar trigger, needs to be coordinated with PumpAi to make it playable
if (sa.isTrigger() && sa.usesTargeting()) {
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
if (tgt != null) {
sa.resetTargets();
sa.getTargets().add(tgt);
}
return sa.isTargetNumberValid();
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return true;
}
return false;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
return attachGeneralAI(ai, sa, (List<Card>)options, !isOptional, sa.getHostCard(), sa.getParam("AILogic"));
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
return attachToCardAIPreferences(ai, sa, true);
}
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
return attachToPlayerAIPreferences(ai, sa, true, (List<Player>)options);
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
return attachToPlayerAIPreferences(ai, sa, true);
}
}

View File

@@ -13,21 +13,18 @@ public class BalanceAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String logic = sa.getParam("AILogic");
int diff = 0;
Player opp = aiPlayer.getWeakestOpponent();
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
for (Player min : aiPlayer.getOpponents()) {
if (min.getCardsIn(ZoneType.Battlefield).size() < opp.getCardsIn(ZoneType.Battlefield).size()) {
opp = min;
}
}
// TODO Add support for multiplayer logic
final Player opp = aiPlayer.getWeakestOpponent();
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
if ("BalanceCreaturesAndLands".equals(logic)) {
// TODO Copied over from hardcoded Balance. We should be checking value of the lands/creatures for each opponent, not just counting
// Copied over from hardcoded Balance. We should be checking value of the lands/creatures not just counting
diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() -
CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size();
diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() -
diff += 1.5 * ( CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() -
CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size());
}
else if ("BalancePermanents".equals(logic)) {

View File

@@ -32,20 +32,20 @@ public class BecomesBlockedAi extends SpellAbilityAi {
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
list = CardLists.getTargetableCards(list, sa);
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
while (sa.canAddMoreTarget()) {
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) {
Card choice = null;
if (list.isEmpty()) {
return false;
}
choice = ComputerUtilCard.getBestCreatureAI(list);
if (choice == null) { // can't find anything left
return false;
}
list.remove(choice);
sa.getTargets().add(choice);
}

View File

@@ -2,7 +2,6 @@ package forge.ai.ability;
import java.util.List;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -25,7 +24,7 @@ public class BidLifeAi extends SpellAbilityAi {
if (tgt != null) {
sa.resetTargets();
if (tgt.canTgtCreature()) {
List<Card> list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
List<Card> list = CardLists.getTargetableCards(aiPlayer.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
if (list.isEmpty()) {
return false;

View File

@@ -17,8 +17,6 @@
*/
package forge.ai.ability;
import java.util.Map;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -49,9 +47,10 @@ public final class BondAi extends SpellAbilityAi {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
} // end bondCanPlayAI()
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
return ComputerUtilCard.getBestCreatureAI(options);
}

View File

@@ -1,9 +1,13 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -32,4 +36,11 @@ public class CanPlayAsDrawbackAi extends SpellAbilityAi {
return false;
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
// This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
return spells.get(0);
}
}

View File

@@ -0,0 +1,64 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.cost.CostPutCounter;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class CantUntapTurnAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
CardCollection oppCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
CardCollection relevantToHold = CardLists.filter(oppCards,
Predicates.and(CardPredicates.Presets.TAPPED, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
if (card.isCreature()) {
return true;
}
for (final SpellAbility ab : card.getSpellAbilities()) {
if (ab.isAbility() && (ab.getPayCosts() != null) && ab.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
}
}));
Card bestToTap = ComputerUtilCard.getBestAI(relevantToHold);
Card validTarget = ComputerUtilCard.getBestAI(CardLists.filter(oppCards, CardPredicates.Presets.TAPPED));
if (validTarget == null) {
validTarget = ComputerUtilCard.getBestAI(oppCards);
}
if (bestToTap != null) {
sa.getTargets().add(bestToTap);
return true;
} else if (sa.hasParam("Planeswalker")
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)) {
sa.getTargets().add(validTarget);
return true;
}
return false;
}
return true;
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
}
}

View File

@@ -1,66 +0,0 @@
package forge.ai.ability;
import java.util.Collection;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
public class ChangeCombatantsAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// TODO: Extend this if possible for cards that have this as an activated ability
return false;
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
final String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("WeakestOppExceptCtrl")) {
PlayerCollection targetableOpps = aiPlayer.getOpponents();
targetableOpps.remove(sa.getHostCard().getController());
if (targetableOpps.isEmpty()) {
return false;
}
return true;
}
return false;
}
@Override
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
PlayerCollection targetableOpps = new PlayerCollection();
for (GameEntity p : options) {
if (p instanceof Player && !p.equals(sa.getHostCard().getController())) {
Player pp = (Player)p;
if (pp.isOpponentOf(ai)) {
targetableOpps.add(pp);
}
}
}
Player weakestTargetableOpp = targetableOpps.filter(PlayerPredicates.isTargetableBy(sa))
.min(PlayerPredicates.compareByLife());
return (T)weakestTargetableOpp;
}
}

View File

@@ -10,7 +10,6 @@ import forge.game.card.Card;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
public class ChangeTargetsAi extends SpellAbilityAi {
@@ -41,20 +40,18 @@ public class ChangeTargetsAi extends SpellAbilityAi {
// nothing on stack, so nothing to target
return false;
}
final TargetChoices topTargets = topSa.getTargets();
final Card topHost = topSa.getHostCard();
if (sa.getTargets().size() != 0 && sa.isTrigger()) {
if (sa.getTargets().getNumTargeted() != 0) {
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
return true;
}
if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) {
if (!topSa.usesTargeting() || topSa.getTargets().getTargetCards().contains(sa.getHostCard())) {
// if this does not target at all or already targets host, no need to redirect it again
return false;
}
for (Card tgt : topTargets.getTargetCards()) {
for (Card tgt : topSa.getTargets().getTargetCards()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) {
// We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites),
// no need to retarget again to another one
@@ -62,7 +59,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
}
}
if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) {
if (topSa.getHostCard() != null && !topSa.getHostCard().getController().isOpponentOf(aiPlayer)) {
// make sure not to redirect our own abilities
return false;
}
@@ -74,31 +71,21 @@ public class ChangeTargetsAi extends SpellAbilityAi {
// don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face)
return false;
}
if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) {
ManaCost manaCost = sa.getPayCosts().getCostMana().getMana();
int payDamage = manaCost.getPhyrexianCount() * 2;
// e.g. Spellskite or a creature receiving its ability that requires Phyrexian mana P/U
int potentialDmg = ComputerUtil.predictDamageFromSpell(topSa, aiPlayer);
ManaCost normalizedMana = manaCost.getNormalizedMana();
boolean canPay = ComputerUtilMana.canPayManaCost(new ManaCostBeingPaid(normalizedMana), sa, aiPlayer, false);
boolean canPay = ComputerUtilMana.canPayManaCost(new ManaCostBeingPaid(normalizedMana), sa, aiPlayer);
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
&& topTargets.contains(aiPlayer)) {
&& topSa.getTargets().getTargets().contains(aiPlayer)) {
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
return false;
}
}
Card firstCard = topTargets.getFirstTargetedCard();
// if we're not the target don't intervene unless we can steal a buff
if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) {
return false;
}
Player firstPlayer = topTargets.getFirstTargetedPlayer();
if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) {
return false;
}
sa.resetTargets();
sa.getTargets().add(topSa);
return true;

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,12 @@
package forge.ai.ability;
import java.util.Collections;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiController;
import forge.ai.AiPlayerPredicates;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -34,6 +17,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.Collections;
public class ChangeZoneAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -51,7 +36,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll");
if (!aiLogicAllowsDiscard) {
@@ -78,6 +63,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
computerType = CardLists.filter(computerType, Predicates.not(CardPredicates.inZone(ZoneType.Library)));
}
// Ugin check need to be done before filterListByType because of ChosenX
// Ugin AI: always try to sweep before considering +1
if (sourceName.equals("Ugin, the Spirit Dragon")) {
return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
@@ -106,16 +92,17 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
return false;
} else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) {
PlayerCollection players = ai.getOpponents();
PlayerCollection players = new PlayerCollection();
players.addAll(ai.getOpponents());
players.add(ai);
int maxSize = 1;
for (Player player : players) {
Player bestTgt = null;
if (player.canBeTargetedBy(sa)) {
int numGY = CardLists.count(player.getCardsIn(ZoneType.Graveyard),
CardCollectionView cardsGY = CardLists.filter(player.getCardsIn(ZoneType.Graveyard),
CardPredicates.Presets.CREATURES);
if (numGY > maxSize) {
maxSize = numGY;
if (cardsGY.size() > maxSize) {
maxSize = cardsGY.size();
bestTgt = player;
}
}
@@ -137,17 +124,13 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
return true;
} else {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
}
final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(), PlayerPredicates.isTargetableBy(sa));
// get the one with the most handsize
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
Player oppTarget = Collections.max(Lists.newArrayList(oppList), PlayerPredicates.compareByZoneSize(origin));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
@@ -155,24 +138,24 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
}
} else if (origin.equals(ZoneType.Battlefield)) {
// this statement is assuming the AI is trying to use this spell offensively
// if the AI is using it defensively, then something else needs to occur
// this statement is assuming the AI is trying to use this spell
// offensively
// if the AI is using it defensively, then something else needs to
// occur
// if only creatures are affected evaluate both lists and pass only
// if human creatures are more valuable
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
}
final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
Player oppTarget = Collections.max(Lists.newArrayList(oppList),
PlayerPredicates.compareByZoneSize(origin));
// set the target
if (oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
@@ -237,7 +220,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
@@ -256,9 +239,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// minimum card advantage unless the hand will be fully reloaded
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
boolean noDiscard = logic.contains(".noDiscard");
if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) {
if (numExiledWithSrc > curHandSize) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
// Try to gain some card advantage if the card will die anyway
// TODO: ideally, should evaluate the hand value and not discard good hands to it
@@ -266,7 +248,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
}
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
}
} else if (origin.equals(ZoneType.Stack)) {
// time stop can do something like this:
@@ -279,8 +261,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (destination.equals(ZoneType.Battlefield)) {
if (sa.getParam("GainControl") != null) {
// Check if the cards are valuable enough
if (CardLists.getNotType(oppType, "Creature").size() == 0
&& CardLists.getNotType(computerType, "Creature").size() == 0) {
if ((CardLists.getNotType(oppType, "Creature").size() == 0)
&& (CardLists.getNotType(computerType, "Creature").size() == 0)) {
if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
.evaluateCreatureList(oppType)) < 400) {
return false;
@@ -370,7 +352,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
// there is no specific AI to support playing it in a smarter way. Feel free to expand.
return Iterables.any(ai.getOpponents().getCardsIn(origin), CardPredicates.Presets.CREATURES);
return !CardLists.filter(ai.getOpponents().getCardsIn(origin), CardPredicates.Presets.CREATURES).isEmpty();
}
CardCollectionView humanType = CardLists.filterControlledBy(ai.getGame().getCardsIn(origin), ai.getOpponents());
@@ -384,22 +366,15 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return true;
}
return false;
}
final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
// get the one with the most handsize
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
Player oppTarget = Collections.max(Lists.newArrayList(oppList),
PlayerPredicates.compareByZoneSize(origin));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty() || mandatory) {
if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
@@ -429,24 +404,16 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} else if (origin.equals(ZoneType.Graveyard)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return true;
}
return sa.isTargetNumberValid();
}
final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = oppList.max(
Player oppTarget = Collections.max(Lists.newArrayList(oppList),
AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty() || mandatory) {
if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {

View File

@@ -1,19 +1,8 @@
package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import forge.ai.AiController;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.ai.*;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -21,26 +10,23 @@ import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.List;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
Collections.shuffle(choices);
final int num;
final int min;
// sa is Entwined, no need for extra logic
if (sa.isEntwine()) {
num = min = choices.size();
} else {
num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num;
}
return true;
}
final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
// Reset the chosen list otherwise it will be locked in forever by earlier calls
sa.setChosenList(null);
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
List<AbilitySub> chosenList;
if (!ai.equals(sa.getActivatingPlayer())) {
@@ -69,9 +55,6 @@ public class CharmAi extends SpellAbilityAi {
if (timingRight) {
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
chosenList = chooseOptionsAi(choices, ai, true, num, min, sa.hasParam("CanRepeatModes"));
if (chosenList.isEmpty() && min != 0) {
return false;
}
} else {
return false;
}
@@ -93,12 +76,12 @@ public class CharmAi extends SpellAbilityAi {
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == num) {
return chosenList; // maximum choices reached
return chosenList; // maximum choices reached
}
}
}
if (isTrigger && chosenList.size() < min) {
// Second pass using doTrigger(false) to fulfill minimum choice
// Second pass using doTrigger(false) to fulfil minimum choice
choices.removeAll(chosenList);
for (AbilitySub sub : choices) {
sub.setActivatingPlayer(ai);
@@ -165,7 +148,7 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(allyTainted ? gain : lose);
} else if (oppTainted || ai.getGame().isCardInPlay("Rain of Gore")) {
// Rain of Gore does negate lifegain, so don't benefit the others
// same for if a opponent does control Tainted Remedy
// same for if a oppoent does control Tainted Remedy
// but if ai cant gain life, the effects are negated
chosenList.add(ai.canGainLife() ? lose : gain);
} else if (ai.getGame().isCardInPlay("Sulfuric Vortex")) {
@@ -181,15 +164,15 @@ public class CharmAi extends SpellAbilityAi {
} else if (ai.canGainLife() && aiLife <= 5) {
// critical Life try to gain more
chosenList.add(gain);
} else if (!ai.canGainLife() && aiLife == 14) {
} else if(!ai.canGainLife() && aiLife == 14 ) {
// ai cant gain life, but try to avoid falling to 13
// but if a opponent does control Tainted Remedy its irrelevant
// but if a oppoent does control Tainted Remedy its irrelevant
chosenList.add(oppTainted ? lose : gain);
} else if (allyTainted) {
// Tainted Remedy negation logic, try gain instead of lose
// because negation does turn it into lose for opponents
boolean oppCritical = false;
// an opponent is Critical = 14, and can't gain life, try to lose life instead
// an oppoent is Critical = 14, and can't gain life, try to lose life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 14) {
for (Player p : opponents) {
@@ -203,7 +186,7 @@ public class CharmAi extends SpellAbilityAi {
} else {
// normal logic, try to gain life if its critical
boolean oppCritical = false;
// an opponent is Critical = 12, and can gain life, try to gain life instead
// an oppoent is Critical = 12, and can gain life, try to gain life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 12) {
for (Player p : opponents) {
@@ -230,12 +213,10 @@ public class CharmAi extends SpellAbilityAi {
goodChoice = sub;
} else {
// Standard canPlayAi()
sub.setActivatingPlayer(ai);
sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone());
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == min) {
break; // enough choices
break; // enough choices
}
}
}
@@ -251,26 +232,7 @@ public class CharmAi extends SpellAbilityAi {
}
@Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents, Map<String, Object> params) {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents) {
return Aggregates.random(opponents);
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
// already done by chooseOrderOfSimultaneousStackEntry
if (sa.getChosenList() != null) {
return true;
}
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}
@Override
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
// choices were already targeted
if (ab.getRootAbility().getChosenList() != null) {
return true;
}
return super.chkDrawbackWithSubs(aiPlayer, ab);
}
}

View File

@@ -2,14 +2,14 @@ package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
@@ -20,10 +20,9 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
@@ -41,7 +40,8 @@ public class ChooseCardAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
sa.resetTargets();
// search targetable Opponents
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
final List<Player> oppList = Lists.newArrayList(Iterables.filter(
ai.getOpponents(), PlayerPredicates.isTargetableBy(sa)));
if (oppList.isEmpty()) {
return false;
@@ -63,9 +63,9 @@ public class ChooseCardAi extends SpellAbilityAi {
if (sa.hasParam("ChoiceZone")) {
choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
}
CardCollectionView choices = game.getCardsIn(choiceZone);
CardCollectionView choices = ai.getGame().getCardsIn(choiceZone);
if (sa.hasParam("Choices")) {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host);
}
if (sa.hasParam("TargetControls")) {
choices = CardLists.filterControlledBy(choices, ai.getOpponents());
@@ -74,9 +74,11 @@ public class ChooseCardAi extends SpellAbilityAi {
return !choices.isEmpty();
} else if (aiLogic.equals("AtLeast2") || aiLogic.equals("BestBlocker")) {
return choices.size() >= 2;
} else if (aiLogic.equals("Clone")) {
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
choices = CardLists.getValidCards(choices, filter, host.getController(), host, sa);
} else if (aiLogic.equals("Clone") || aiLogic.equals("Vesuva")) {
final String filter = aiLogic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
choices = CardLists.getValidCards(choices, filter, host.getController(), host);
return !choices.isEmpty();
} else if (aiLogic.equals("Never")) {
return false;
@@ -97,11 +99,11 @@ public class ChooseCardAi extends SpellAbilityAi {
});
return !choices.isEmpty();
} else if (aiLogic.equals("Ashiok")) {
final int loyalty = host.getCounters(CounterEnumType.LOYALTY) - 1;
final int loyalty = host.getCounters(CounterType.LOYALTY) - 1;
for (int i = loyalty; i >= 0; i--) {
sa.setXManaCostPaid(i);
choices = game.getCardsIn(choiceZone);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa);
host.setSVar("ChosenX", "Number$" + i);
choices = ai.getGame().getCardsIn(choiceZone);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host);
if (!choices.isEmpty()) {
return true;
}
@@ -109,10 +111,10 @@ public class ChooseCardAi extends SpellAbilityAi {
return !choices.isEmpty();
} else if (aiLogic.equals("RandomNonLand")) {
return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty();
return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host).isEmpty();
} else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = AiAttackController.choosePreferredDefenderPlayer(ai).getCreaturesInPlay();
CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
@@ -127,12 +129,6 @@ public class ChooseCardAi extends SpellAbilityAi {
return (ComputerUtilCard.evaluateCreatureList(aiCreatures) + minGain) < ComputerUtilCard
.evaluateCreatureList(oppCreatures);
} else if (aiLogic.equals("OwnCard")) {
CardCollectionView ownChoices = CardLists.filter(choices, CardPredicates.isController(ai));
if (ownChoices.isEmpty()) {
ownChoices = CardLists.filter(choices, CardPredicates.isControlledByAnyOf(ai.getAllies()));
}
return !ownChoices.isEmpty();
}
return true;
}
@@ -144,22 +140,12 @@ public class ChooseCardAi extends SpellAbilityAi {
}
return checkApiLogic(ai, sa);
}
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("AtOppEOT")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
}
return super.checkPhaseRestrictions(ai, sa, ph);
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@Override
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
final Card host = sa.getHostCard();
final Player ctrl = host.getController();
final String logic = sa.getParam("AILogic");
@@ -169,30 +155,29 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = ComputerUtilCard.getBestAI(options);
} else if ("WorstCard".equals(logic)) {
choice = ComputerUtilCard.getWorstAI(options);
} else if ("OwnCard".equals(logic)) {
CardCollectionView ownChoices = CardLists.filter(options, CardPredicates.isController(ai));
if (ownChoices.isEmpty()) {
ownChoices = CardLists.filter(options, CardPredicates.isControlledByAnyOf(ai.getAllies()));
}
choice = ComputerUtilCard.getBestAI(ownChoices);
} else if (logic.equals("BestBlocker")) {
if (Iterables.any(options, Presets.UNTAPPED)) {
if (!CardLists.filter(options, Presets.UNTAPPED).isEmpty()) {
options = CardLists.filter(options, Presets.UNTAPPED);
}
choice = ComputerUtilCard.getBestCreatureAI(options);
} else if (logic.equals("Clone")) {
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
} else if (logic.equals("Clone") || logic.equals("Vesuva")) {
final String filter = logic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary" :
"Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) {
options = newOptions;
}
choice = ComputerUtilCard.getBestAI(options);
if (logic.equals("Vesuva") && "Vesuva".equals(choice.getName())) {
choice = null;
}
} else if ("RandomNonLand".equals(logic)) {
options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host, sa);
options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host);
choice = Aggregates.random(options);
} else if (logic.equals("Untap")) {
final String filter = "Permanent.YouCtrl,Permanent.tapped";
CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) {
options = newOptions;
}
@@ -200,13 +185,13 @@ public class ChooseCardAi extends SpellAbilityAi {
} else if (logic.equals("NeedsPrevention")) {
final Game game = ai.getGame();
final Combat combat = game.getCombat();
CardCollectionView better = CardLists.filter(options, new Predicate<Card>() {
CardCollectionView better = CardLists.filter(options, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) {
return false;
}
int ref = ComputerUtilAbility.getAbilitySourceName(sa).equals("Forcefield") ? 1 : 0;
int ref = ComputerUtilAbility.getAbilitySourceName(sa).equals("Forcefield") ? 1 : 0;
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > ref;
}
});
@@ -248,13 +233,14 @@ public class ChooseCardAi extends SpellAbilityAi {
return false;
}
for (SpellAbility sa : c.getAllSpellAbilities()) {
if (sa.getPayCosts().hasTapCost()) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
return false;
}
}
return true;
}
});
System.out.println("Tangle Wire" + options + " - " + betterList);
if (!betterList.isEmpty()) {
choice = betterList.get(0);
} else {
@@ -290,8 +276,6 @@ public class ChooseCardAi extends SpellAbilityAi {
// Choose the best (hopefully the fattest, whatever) creature so that hopefully it won't die too easily
choice = ComputerUtilCard.getBestAI(creatures);
}
} else if (logic.equals("NextTurnAttacker")) {
choice = ComputerUtilCard.getBestCreatureToAttackNextTurnAI(ai, options);
} else {
choice = ComputerUtilCard.getBestAI(options);
}

View File

@@ -1,17 +1,12 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.StaticData;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.card.CardDb;
import forge.card.CardRules;
import forge.card.CardSplitType;
@@ -28,6 +23,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
if (sa.hasParam("AILogic")) {
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
@@ -35,7 +31,9 @@ public class ChooseCardNameAi extends SpellAbilityAi {
}
String logic = sa.getParam("AILogic");
if (logic.equals("CursedScroll")) {
if (logic.equals("MomirAvatar")) {
return SpecialCardAi.MomirVigAvatar.consider(ai, sa);
} else if (logic.equals("CursedScroll")) {
return SpecialCardAi.CursedScroll.consider(ai, sa);
}
@@ -43,7 +41,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
if (tgt != null) {
sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(AiAttackController.choosePreferredDefenderPlayer(ai));
sa.getTargets().add(ai.getWeakestOpponent());
} else {
sa.getTargets().add(ai);
}
@@ -56,13 +54,14 @@ public class ChooseCardNameAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// TODO - there is no AILogic implemented yet
return mandatory;
return false;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@Override
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
return ComputerUtilCard.getBestAI(options);
}
@@ -87,7 +86,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
if (rules.getSplitType() == CardSplitType.Split) {
Card copy = CardUtil.getLKICopy(card);
// for calcing i need only one split side
// for calcing i need only one split side
if (isOther) {
copy.getCurrentState().copyFrom(card.getState(CardStateName.RightSplit), true);
} else {

View File

@@ -3,11 +3,12 @@ package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
@@ -22,6 +23,7 @@ public class ChooseColorAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = ai.getGame();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final PhaseHandler ph = game.getPhaseHandler();
@@ -44,7 +46,8 @@ public class ChooseColorAi extends SpellAbilityAi {
return false;
}
// Set PayX here to maximum value.
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
int x = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(x));
return true;
}
@@ -54,8 +57,11 @@ public class ChooseColorAi extends SpellAbilityAi {
if (logic.equals("MostExcessOpponentControls")) {
for (byte color : MagicColor.WUBRG) {
CardCollectionView ailist = ai.getColoredCardsInPlay(color);
CardCollectionView opplist = ai.getStrongestOpponent().getColoredCardsInPlay(color);
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView opplist = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield);
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
int excess = ComputerUtilCard.evaluatePermanentList(opplist) - ComputerUtilCard.evaluatePermanentList(ailist);
if (excess > 4) {
@@ -63,7 +69,9 @@ public class ChooseColorAi extends SpellAbilityAi {
}
}
return false;
} else if (logic.equals("MostProminentInComputerDeck")) {
}
if (logic.equals("MostProminentInComputerDeck")) {
if ("Astral Cornucopia".equals(sourceName)) {
// activate in Main 2 hoping that the extra mana surplus will make a difference
// if there are some nonland permanents in hand
@@ -72,11 +80,6 @@ public class ChooseColorAi extends SpellAbilityAi {
return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai);
}
} else if (logic.equals("HighestDevotionToColor")) {
// currently only works more or less reliably in Main2 to cast own spells
if (!ph.is(PhaseType.MAIN2, ai)) {
return false;
}
}
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());

View File

@@ -1,30 +0,0 @@
package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class ChooseCompanionAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@Override
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
List<Card> cards = Lists.newArrayList(options);
if (cards.isEmpty()) {
return null;
}
Collections.shuffle(cards);
return cards.get(0);
}
}

View File

@@ -46,6 +46,6 @@ public class ChooseDirectionAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
return canPlayAI(ai, sa);
}
}

View File

@@ -1,34 +0,0 @@
package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
public class ChooseEvenOddAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
if (!sa.hasParam("AILogic")) {
return false;
}
if (sa.usesTargeting()) {
sa.resetTargets();
Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return false;
}
}
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
}
}

View File

@@ -1,33 +1,18 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.ai.*;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
@@ -36,6 +21,9 @@ import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.collect.FCollection;
import java.util.List;
import java.util.Map;
public class ChooseGenericEffectAi extends SpellAbilityAi {
@@ -53,11 +41,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
} else if ("GideonBlackblade".equals(aiLogic)) {
return SpecialCardAi.GideonBlackblade.consider(ai, sa);
} else if ("AtOppEOT".equals(aiLogic)) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
} else if ("Always".equals(aiLogic)) {
return true;
}
return false;
}
@@ -72,12 +55,12 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
return canPlayAI(aiPlayer, sa);
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) {
if ("CombustibleGearhulk".equals(sa.getParam("AILogic"))) {
for (final Player p : aiPlayer.getOpponents()) {
if (p.canBeTargetedBy(sa)) {
sa.resetTargets();
@@ -121,7 +104,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
SpellAbility paycost = new SpellAbility.EmptySa(sa.getHostCard(), player);
paycost.setPayCosts(unless);
if (ComputerUtilCost.willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
&& ComputerUtilCost.canPayCost(paycost, player, true)) {
&& ComputerUtilCost.canPayCost(paycost, player)) {
return sp;
}
}
@@ -160,22 +143,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
}
return others;
} else if ("Counters".equals(logic)) {
// TODO: this code will need generalization if this logic is used for cards other
// than Elspeth Conquers Death with different choice parameters
SpellAbility p1p1 = null, loyalty = null;
for (final SpellAbility sp : spells) {
if (("P1P1").equals(sp.getParam("CounterType"))) {
p1p1 = sp;
} else {
loyalty = sp;
}
}
if (sa.getParent().getTargetCard() != null && sa.getParent().getTargetCard().isPlaneswalker()) {
return loyalty;
} else {
return p1p1;
}
} else if ("Fatespinner".equals(logic)) {
SpellAbility skipDraw = null, /*skipMain = null,*/ skipCombat = null;
for (final SpellAbility sp : spells) {
@@ -188,10 +155,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
}
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) {
if (player.hasKeyword("Skip your draw step.")) {
return skipDraw;
}
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) {
if (player.hasKeyword("Skip your next combat phase.")) {
return skipCombat;
}
@@ -199,6 +166,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// Todo if hand is empty or mostly empty, skip main phase
// Todo if hand has gas, skip draw
return Aggregates.random(spells);
} else if ("SinProdder".equals(logic)) {
SpellAbility allow = null, deny = null;
for (final SpellAbility sp : spells) {
@@ -212,25 +180,25 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
Card imprinted = host.getImprintedCards().getFirst();
int dmg = imprinted.getCMC();
Player owner = imprinted.getOwner();
//useless cards in hand
if (imprinted.getName().equals("Bridge from Below") ||
imprinted.getName().equals("Haakon, Stromgald Scourge")) {
return allow;
}
//bad cards when are thrown from the library to the graveyard, but Yixlid can prevent that
if (!player.getGame().isCardInPlay("Yixlid Jailer") && (
imprinted.getName().equals("Gaea's Blessing") ||
imprinted.getName().equals("Narcomoeba"))) {
return allow;
}
// milling against Tamiyo is pointless
if (owner.isCardInCommand("Emblem - Tamiyo, the Moon Sage")) {
return allow;
}
// milling a land against Gitrog result in card draw
if (imprinted.isLand() && owner.isCardInPlay("The Gitrog Monster")) {
// try to mill owner
@@ -239,19 +207,19 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
return allow;
}
// milling a creature against Sidisi result in more creatures
if (imprinted.isCreature() && owner.isCardInPlay("Sidisi, Brood Tyrant")) {
return allow;
}
//if Iona does prevent from casting, allow it to draw
//if Iona does prevent from casting, allow it to draw
for (final Card io : player.getCardsIn(ZoneType.Battlefield, "Iona, Shield of Emeria")) {
if (imprinted.getColor().hasAnyColor(MagicColor.fromName(io.getChosenColor()))) {
if (CardUtil.getColors(imprinted).hasAnyColor(MagicColor.fromName(io.getChosenColor()))) {
return allow;
}
}
if (dmg == 0) {
// If CMC = 0, mill it!
return deny;
@@ -276,7 +244,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
SpellAbility counterSA = spells.get(0), tokenSA = spells.get(1);
// check for something which might prevent the counters to be placed on host
if (!host.canReceiveCounters(CounterEnumType.P1P1)) {
if (!host.canReceiveCounters(CounterType.P1P1)) {
return tokenSA;
}
@@ -288,13 +256,13 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// need a copy for one with extra +1/+1 counter boost,
// without causing triggers to run
final Card copy = CardUtil.getLKICopy(host);
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n);
copy.setCounters(CounterType.P1P1, copy.getCounters(CounterType.P1P1) + n);
copy.setZone(host.getZone());
// if host would put into the battlefield attacking
if (combat != null && combat.isAttacking(host)) {
final Player defender = combat.getDefenderPlayerByAttacker(host);
if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true)) {
if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy)) {
return counterSA;
}
return tokenSA;
@@ -305,7 +273,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
for (final Player opp : player.getOpponents()) {
if (CombatUtil.canAttack(copy, opp) &&
opp.canLoseLife() &&
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
!ComputerUtilCard.canBeBlockedProfitably(opp, copy))
return counterSA;
}
}
@@ -313,10 +281,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// 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(player, tokenSA);
final Card tokenCard = TokenAi.spawnToken(player, tokenSA, true);
// Token would not survive
if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) {
// Token would not survive
if (tokenCard.getNetToughness() < 1) {
return counterSA;
}
@@ -354,14 +322,12 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
int totalCMC = 0;
for (Card c : revealedCards) {
for(Card c : revealedCards) {
totalCMC += c.getCMC();
}
int bestGuessDamage = totalCMC * 3 / revealedCards.size();
return life <= bestGuessDamage ? spells.get(0) : spells.get(1);
} else if ("SoulEcho".equals(logic)) {
return sa.getHostCard().getController().getLife() < 10 ? spells.get(0) : Aggregates.random(spells);
} else if ("Pump".equals(logic) || "BestOption".equals(logic)) {
List<SpellAbility> filtered = Lists.newArrayList();
// filter first for the spells which can be done
@@ -370,7 +336,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
filtered.add(sp);
}
}
// TODO find better way to check
if (!filtered.isEmpty()) {
return filtered.get(0);
@@ -396,7 +362,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
game.getAction().checkStaticAbilities(false);
// can't gain counters, use Haste
if (!copy.canReceiveCounters(CounterEnumType.P1P1)) {
if (!copy.canReceiveCounters(CounterType.P1P1)) {
return true;
}

View File

@@ -1,51 +1,22 @@
package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom;
public class ChooseNumberAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.isEmpty()) {
if (!sa.hasParam("AILogic")) {
return false;
} else if (aiLogic.equals("SweepCreatures")) {
int maxChoiceLimit = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Max"), sa);
int ownCreatureCount = aiPlayer.getCreaturesInPlay().size();
int oppMaxCreatureCount = 0;
Player refOpp = null;
for (Player opp : aiPlayer.getOpponents()) {
int oppCreatureCount = Math.max(oppMaxCreatureCount, opp.getCreaturesInPlay().size());
if (oppCreatureCount > oppMaxCreatureCount) {
oppMaxCreatureCount = oppCreatureCount;
refOpp = opp;
}
}
if (refOpp == null) {
return false; // no opponent has any creatures
}
int evalAI = ComputerUtilCard.evaluateCreatureList(aiPlayer.getCreaturesInPlay());
int evalOpp = ComputerUtilCard.evaluateCreatureList(refOpp.getCreaturesInPlay());
if (aiPlayer.getLifeLostLastTurn() + aiPlayer.getLifeLostThisTurn() == 0 && evalAI > evalOpp) {
return false; // we're not pressured and our stuff seems better, don't do it yet
}
return ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit);
}
if (sa.usesTargeting()) {
TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
Player opp = aiPlayer.getWeakestOpponent();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {

View File

@@ -1,17 +1,15 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.List;
public class ChoosePlayerAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -29,7 +27,7 @@ public class ChoosePlayerAi extends SpellAbilityAi {
}
@Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> choices, Map<String, Object> params) {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> choices) {
Player chosen = null;
if ("Curse".equals(sa.getParam("AILogic"))) {
for (Player pc : choices) {

View File

@@ -1,16 +1,15 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameObject;
@@ -26,6 +25,7 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
@@ -46,14 +46,28 @@ public class ChooseSourceAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
if (abCost != null) {
if (!willPayCosts(ai, sa, abCost, source)) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) {
return false;
}
}
if (sa.usesTargeting()) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
Player opp = ai.getWeakestOpponent();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
@@ -65,7 +79,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (sa.getParam("AILogic").equals("NeedsPrevention")) {
if (!game.getStack().isEmpty()) {
final SpellAbility topStack = game.getStack().peekAbility();
if (sa.hasParam("Choices") && !topStack.matchesValid(topStack.getHostCard(), sa.getParam("Choices").split(","))) {
if (sa.hasParam("Choices") && !topStack.getHostCard().isValid(sa.getParam("Choices"), ai, source, sa)) {
return false;
}
final ApiType threatApi = topStack.getApi();
@@ -78,7 +92,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (!topStack.usesTargeting() && topStack.hasParam("ValidPlayers") && !topStack.hasParam("Defined")) {
objects = AbilityUtils.getDefinedPlayers(threatSource, topStack.getParam("ValidPlayers"), topStack);
}
if (!objects.contains(ai) || topStack.hasParam("NoPrevention")) {
return false;
}
@@ -90,7 +104,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
}
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
if (sa.hasParam("Choices")) {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host);
}
final Combat combat = game.getCombat();
choices = CardLists.filter(choices, new Predicate<Card>() {
@@ -108,9 +122,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
return true;
}
@Override
public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
if ("NeedsPrevention".equals(sa.getParam("AILogic"))) {
final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
@@ -122,11 +138,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
}
final Combat combat = game.getCombat();
List<Card> permanentSources = CardLists.filter(options, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (c == null || c.getZone() == null || c.getZone().getZoneType() != ZoneType.Battlefield
if (c == null || c.getZone() == null || c.getZone().getZoneType() != ZoneType.Battlefield
|| combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) {
return false;
}
@@ -138,26 +154,27 @@ public class ChooseSourceAi extends SpellAbilityAi {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(permanentSources);
if (bestCreature != null) {
return bestCreature;
}
// No optimal creature was found above, so try to broaden the choice.
if (!Iterables.isEmpty(options)) {
List<Card> oppCreatures = CardLists.filter(options, Predicates.and(CardPredicates.Presets.CREATURES,
Predicates.not(CardPredicates.isOwner(aiChoser))));
List<Card> aiNonCreatures = CardLists.filter(options, Predicates.and(Predicates.not(CardPredicates.Presets.CREATURES),
CardPredicates.Presets.PERMANENTS, CardPredicates.isOwner(aiChoser)));
} else {
// No optimal creature was found above, so try to broaden the choice.
if (!Iterables.isEmpty(options)) {
List<Card> oppCreatures = CardLists.filter(options, Predicates.and(CardPredicates.Presets.CREATURES,
Predicates.not(CardPredicates.isOwner(aiChoser))));
List<Card> aiNonCreatures = CardLists.filter(options, Predicates.and(Predicates.not(CardPredicates.Presets.CREATURES),
CardPredicates.Presets.PERMANENTS, CardPredicates.isOwner(aiChoser)));
if (!oppCreatures.isEmpty()) {
return ComputerUtilCard.getBestCreatureAI(oppCreatures);
} else if (!aiNonCreatures.isEmpty()) {
return Aggregates.random(aiNonCreatures);
} else {
return Aggregates.random(options);
if (!oppCreatures.isEmpty()) {
return ComputerUtilCard.getBestCreatureAI(oppCreatures);
} else if (!aiNonCreatures.isEmpty()) {
return Aggregates.random(aiNonCreatures);
} else {
return Aggregates.random(options);
}
} else if (!game.getStack().isEmpty()) {
// No permanent for the AI to choose. Should normally not happen unless using dev mode or something,
// but when it does happen, choose the top card on stack if possible (generally it'll be the SA
// source) in order to choose at least something, or the game will hang.
return game.getStack().peekAbility().getHostCard();
}
} else if (!game.getStack().isEmpty()) {
// No permanent for the AI to choose. Should normally not happen unless using dev mode or something,
// but when it does happen, choose the top card on stack if possible (generally it'll be the SA
// source) in order to choose at least something, or the game will hang.
return game.getStack().peekAbility().getHostCard();
}
// Should never get here
@@ -168,13 +185,13 @@ public class ChooseSourceAi extends SpellAbilityAi {
return ComputerUtilCard.getBestAI(options);
}
}
private Card chooseCardOnStack(SpellAbility sa, Player ai, Game game) {
for (SpellAbilityStackInstance si : game.getStack()) {
final Card source = si.getSourceCard();
final SpellAbility abilityOnStack = si.getSpellAbility(true);
if (sa.hasParam("Choices") && !abilityOnStack.matchesValid(source, sa.getParam("Choices").split(","))) {
if (sa.hasParam("Choices") && !abilityOnStack.getHostCard().isValid(sa.getParam("Choices"), ai, sa.getHostCard(), sa)) {
continue;
}
final ApiType threatApi = abilityOnStack.getApi();
@@ -184,9 +201,9 @@ public class ChooseSourceAi extends SpellAbilityAi {
List<? extends GameObject> objects = getTargets(abilityOnStack);
if (!abilityOnStack.usesTargeting() && !abilityOnStack.hasParam("Defined") && abilityOnStack.hasParam("ValidPlayers"))
if (!abilityOnStack.usesTargeting() && !abilityOnStack.hasParam("Defined") && abilityOnStack.hasParam("ValidPlayers"))
objects = AbilityUtils.getDefinedPlayers(source, abilityOnStack.getParam("ValidPlayers"), abilityOnStack);
if (!objects.contains(ai) || abilityOnStack.hasParam("NoPrevention")) {
continue;
}
@@ -197,11 +214,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
return source;
}
return null;
}
}
private static List<GameObject> getTargets(final SpellAbility sa) {
return sa.usesTargeting() && (!sa.hasParam("Defined"))
? Lists.newArrayList(sa.getTargets())
? Lists.newArrayList(sa.getTargets().getTargets())
: AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
}
}

View File

@@ -1,12 +1,7 @@
package forge.ai.ability;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
@@ -14,7 +9,6 @@ import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardType;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
@@ -23,10 +17,10 @@ import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.List;
public class ChooseTypeAi extends SpellAbilityAi {
@Override
@@ -37,9 +31,6 @@ public class ChooseTypeAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
return doMirrorEntityLogic(aiPlayer, sa);
}
return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty();
} else if ("MostProminentOppControls".equals(sa.getParam("AILogic"))) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
}
return doTriggerAINoCost(aiPlayer, sa, false);
@@ -53,12 +44,28 @@ public class ChooseTypeAi extends SpellAbilityAi {
return false;
}
String chosenType = chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield));
if (chosenType.isEmpty()) {
return false;
}
CardCollectionView otb = aiPlayer.getCardsIn(ZoneType.Battlefield);
List<String> valid = Lists.newArrayList(CardType.getAllCreatureTypes());
int maxX = ComputerUtilMana.determineLeftoverMana(sa, aiPlayer, false);
String chosenType = ComputerUtilCard.getMostProminentType(otb, valid);
if (chosenType.isEmpty()) {
// Account for the situation when only changelings are on the battlefield
boolean allChangeling = false;
for (Card c : otb) {
if (c.isCreature() && c.hasStartOfKeyword(Keyword.CHANGELING.toString())) {
chosenType = Aggregates.random(valid); // just choose a random type for changelings
allChangeling = true;
break;
}
}
if (!allChangeling) {
// Still empty, probably no creatures on board
return false;
}
}
int maxX = ComputerUtilMana.determineMaxAffordableX(aiPlayer, sa);
int avgPower = 0;
// predict the opposition
@@ -95,7 +102,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
}
if (maxX > avgPower && maxX > maxOppPower && maxX >= maxOppToughness) {
sa.setXManaCostPaid(maxX);
sa.setSVar("PayX", String.valueOf(maxX));
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
return true;
}
@@ -107,42 +114,12 @@ public class ChooseTypeAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean isCurse = sa.isCurse();
if (sa.usesTargeting()) {
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
final List<Player> alliesList = ai.getAllies().filter(PlayerPredicates.isTargetableBy(sa));
sa.resetTargets();
if (isCurse) {
if (!oppList.isEmpty()) {
sa.getTargets().add(Iterables.getFirst(oppList, null));
} else if (mandatory) {
if (!alliesList.isEmpty()) {
sa.getTargets().add(Iterables.getFirst(alliesList, null));
} else if (ai.canBeTargetedBy(sa)) {
sa.getTargets().add(ai);
}
}
} else {
if (ai.canBeTargetedBy(sa)) {
sa.getTargets().add(ai);
} else {
if (!alliesList.isEmpty()) {
sa.getTargets().add(Iterables.getFirst(alliesList, null));
} else if (!oppList.isEmpty() && mandatory) {
sa.getTargets().add(Iterables.getFirst(oppList, null));
}
}
}
if (!sa.isTargetNumberValid()) {
return false; // nothing to target?
}
sa.getTargets().add(ai);
} else {
for (final Player p : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa)) {
if (p.isOpponentOf(ai) && !mandatory && !isCurse) {
if (p.isOpponentOf(ai) && !mandatory) {
return false;
}
}
@@ -150,40 +127,4 @@ public class ChooseTypeAi extends SpellAbilityAi {
return true;
}
private String chooseType(SpellAbility sa, CardCollectionView cards) {
Set<String> valid = new HashSet<>();
if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.PumpAll
&& sa.getSubAbility().isCurse() && sa.getSubAbility().hasParam("NumDef")) {
final SpellAbility pumpSa = sa.getSubAbility();
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), pumpSa.getParam("NumDef"), pumpSa);
for (Card c : cards) {
if (c.isCreature() && c.getNetToughness() <= -defense) {
valid.addAll(c.getType().getCreatureTypes());
}
}
} else {
valid.addAll(CardType.getAllCreatureTypes());
}
String chosenType = ComputerUtilCard.getMostProminentType(cards, valid);
if (chosenType.isEmpty()) {
// Account for the situation when only changelings are on the battlefield
boolean allChangeling = false;
for (Card c : cards) {
if (c.isCreature() && c.hasStartOfKeyword(Keyword.CHANGELING.toString())) {
chosenType = Aggregates.random(valid); // just choose a random type for changelings
allChangeling = true;
break;
}
}
if (!allChangeling) {
// Still empty, probably no creatures on board
return "";
}
}
return chosenType;
}
}

View File

@@ -1,11 +1,9 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
@@ -33,6 +31,7 @@ public class ClashAi extends SpellAbilityAi {
return legalAction;
}
/*
* (non-Javadoc)
*
@@ -57,7 +56,7 @@ public class ClashAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility, java.lang.Iterable)
*/
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
for (Player p : options) {
if (p.getCardsIn(ZoneType.Library).size() == 0)
return p;
@@ -83,7 +82,7 @@ public class ClashAi extends SpellAbilityAi {
PlayerCollection players = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
// use chooseSinglePlayer function to the select player
Player chosen = chooseSinglePlayer(ai, sa, players, null);
Player chosen = chooseSinglePlayer(ai, sa, players);
if (chosen != null) {
sa.resetTargets();
sa.getTargets().add(chosen);
@@ -92,7 +91,7 @@ public class ClashAi extends SpellAbilityAi {
if ("Creature".equals(valid)) {
// Springjack Knight
// TODO: Whirlpool Whelm also uses creature targeting but it's trickier to support
CardCollectionView aiCreats = ai.getCreaturesInPlay();
CardCollectionView aiCreats = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
CardCollectionView oppCreats = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
Card tgt = aiCreats.isEmpty() ? ComputerUtilCard.getWorstCreatureAI(oppCreats) : ComputerUtilCard.getBestCreatureAI(aiCreats);
@@ -105,7 +104,7 @@ public class ClashAi extends SpellAbilityAi {
}
}
return sa.getTargets().size() > 0;
return sa.getTargets().getNumTargeted() > 0;
}
}

View File

@@ -1,19 +1,12 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -21,6 +14,8 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.List;
public class CloneAi extends SpellAbilityAi {
@Override
@@ -42,7 +37,7 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?"
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false;
}
@@ -53,10 +48,10 @@ public class CloneAi extends SpellAbilityAi {
boolean bFlag = false;
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn());
bFlag |= (!c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn()));
// for creatures that could be improved (like Figure of Destiny)
if (c.isCreature() && (!sa.hasParam("Duration") || (!c.isTapped() && !c.isSick()))) {
if (c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
@@ -72,7 +67,8 @@ public class CloneAi extends SpellAbilityAi {
}
if (!bFlag) { // All of the defined stuff is cloned, not very useful
if (!bFlag) { // All of the defined stuff is cloned, not very
// useful
return false;
}
} else {
@@ -92,23 +88,17 @@ public class CloneAi extends SpellAbilityAi {
chance = cloneTgtAI(sa);
}
return chance;
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
Card host = sa.getHostCard();
boolean chance = true;
if (sa.usesTargeting()) {
chance = cloneTgtAI(sa);
} else {
if (sa.hasParam("Choices")) {
CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield),
sa.getParam("Choices"), host.getController(), host, sa);
chance = !choices.isEmpty();
}
}
// Improve AI for triggers. If source is a creature with:
@@ -133,11 +123,11 @@ public class CloneAi extends SpellAbilityAi {
private boolean cloneTgtAI(final SpellAbility sa) {
// Specific logic for cards
if ("CloneAttacker".equals(sa.getParam("AILogic"))) {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard(), sa);
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
} else if ("CloneBestCreature".equals(sa.getParam("AILogic"))) {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard(), sa);
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
}
@@ -179,26 +169,26 @@ public class CloneAi extends SpellAbilityAi {
*/
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
Player targetedPlayer, Map<String, Object> params) {
Player targetedPlayer) {
final Card host = sa.getHostCard();
final String name = host.getName();
final Player ctrl = host.getController();
final Card cloneTarget = getCloneTarget(sa);
final boolean isOpp = cloneTarget.getController().isOpponentOf(sa.getActivatingPlayer());
final boolean isVesuva = "Vesuva".equals(name) || "Sculpting Steel".equals(name);
final boolean isVesuva = "Vesuva".equals(host.getName());
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name;
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
// TODO: rewrite this block so that this is done somehow more elegantly
if (canCloneLegendary) {
filter = filter.replace(".nonLegendary+", ".").replace(".nonLegendary", "");
}
CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) {
options = newOptions;
}
@@ -210,13 +200,12 @@ public class CloneAi extends SpellAbilityAi {
}
}
// prevent loop of choosing copy of same card
if (isVesuva) {
options = CardLists.filter(options, Predicates.not(CardPredicates.sharesNameWith(host)));
}
Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);
if (isVesuva && "Vesuva".equals(choice.getName())) {
choice = null;
}
return choice;
}
@@ -243,8 +232,8 @@ public class CloneAi extends SpellAbilityAi {
// don't use instant speed clone abilities outside computers
// Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("ActivationPhases") && sa.hasParam("Duration")) {
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false;
}
@@ -255,6 +244,6 @@ public class CloneAi extends SpellAbilityAi {
}
// don't activate during main2 unless this effect is permanent
return !ph.is(PhaseType.MAIN2) || !sa.hasParam("Duration");
return !ph.is(PhaseType.MAIN2) || sa.hasParam("Permanent");
}
}

View File

@@ -4,7 +4,6 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -30,8 +29,9 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.resetTargets();
CardCollection list =
CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on purpose
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on
// purpose
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -65,7 +65,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
}
} else {
if (mandatory) {
return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid();
return chkAIDrawback(sa, aiPlayer);
} else {
return canPlayAI(aiPlayer, sa);
}
@@ -81,16 +81,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if ("PowerStruggle".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.PowerStruggle.considerSecondTarget(aiPlayer, sa);
}
// for TrigTwoTargets logic, only get the opponents' cards for the first target
CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ?
aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) :
aiPlayer.getGame().getCardsIn(ZoneType.Battlefield);
CardCollection list = CardLists.getValidCards(unfilteredList,
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
tgt.getValidTgts(), aiPlayer, sa.getHostCard(), sa);
// only select the cards that can be targeted
@@ -101,12 +92,8 @@ public class ControlExchangeAi extends SpellAbilityAi {
Card best = ComputerUtilCard.getBestAI(list);
// add best Target:
// do it here already even if we don't want to play this as it might be for targeting a trigger
sa.getTargets().add(best);
// if Param has Defined, check if the best Target is better than the Defined
if (sa.hasParam("Defined") && (!sa.isTrigger() || sa.getRootAbility().isOptionalTrigger())) {
if (sa.hasParam("Defined")) {
final Card object = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa).get(0);
// TODO add evaluate Land if able
final Card realBest = ComputerUtilCard.getBestAI(Lists.newArrayList(best, object));
@@ -117,50 +104,9 @@ public class ControlExchangeAi extends SpellAbilityAi {
}
}
// second target needed (the AI's own worst)
if ("TrigTwoTargets".equals(sa.getParam("AILogic"))) {
return doTrigTwoTargetsLogic(aiPlayer, sa, best);
}
// add best Target
sa.getTargets().add(best);
return true;
}
private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final int creatureThreshold = 100; // TODO: make this configurable from the AI profile
final int nonCreatureThreshold = 2;
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield),
tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// only select the cards that can be targeted
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty()) {
return false;
}
Card aiWorst = ComputerUtilCard.getWorstAI(list);
if (aiWorst == null) {
return false;
}
if (aiWorst != bestFirstTgt) {
if (bestFirstTgt.isCreature() && aiWorst.isCreature()) {
if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return true;
}
} else {
// TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine
if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return true;
}
}
}
sa.clearTargets();
return false;
}
}

View File

@@ -18,7 +18,6 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
@@ -26,7 +25,6 @@ import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
@@ -36,13 +34,12 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.collect.FCollectionView;
//AB:GainControl|ValidTgts$Creature|TgtPrompt$Select target legendary creature|LoseControl$Untap,LoseControl|SpellDescription$Gain control of target xxxxxxx
@@ -57,6 +54,8 @@ import forge.util.Aggregates;
// (as a "&"-separated list; like Haste, Sacrifice CARDNAME at EOT, any standard keyword)
// OppChoice - set to True if opponent chooses creature (for Preacher) - not implemented yet
// Untap - set to True if target card should untap when control is taken
// DestroyTgt - actions upon which the tgt should be destroyed. same list as LoseControl
// NoRegen - set if destroyed creature can't be regenerated. used only with DestroyTgt
/**
* <p>
@@ -69,6 +68,7 @@ import forge.util.Aggregates;
public class ControlGainAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
final List<String> lose = Lists.newArrayList();
if (sa.hasParam("LoseControl")) {
@@ -77,7 +77,7 @@ public class ControlGainAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = ai.getGame();
final PlayerCollection opponents = ai.getOpponents();
final FCollectionView<Player> opponents = ai.getOpponents();
// if Defined, then don't worry about targeting
if (tgt == null) {
@@ -94,23 +94,23 @@ public class ControlGainAi extends SpellAbilityAi {
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
}
if (tgt.isRandomTarget()) {
sa.getTargets().add(Aggregates.random(tgt.getAllCandidates(sa, false)));
}
if (tgt.canOnlyTgtOpponent()) {
List<Player> oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa));
List<Player> oppList = Lists
.newArrayList(Iterables.filter(opponents, PlayerPredicates.isTargetableBy(sa)));
if (oppList.isEmpty()) {
return false;
}
if (tgt.isRandomTarget()) {
sa.getTargets().add(Aggregates.random(oppList));
} else {
sa.getTargets().add(oppList.get(0));
}
sa.getTargets().add(oppList.get(0));
}
}
// Don't steal something if I can't Attack without, or prevent it from blocking at least
// Don't steal something if I can't Attack without, or prevent it from
// blocking at least
if (lose.contains("EOT")
&& ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !sa.isTrigger()) {
@@ -122,7 +122,10 @@ public class ControlGainAi extends SpellAbilityAi {
return true;
}
CardCollection list = opponents.getCardsIn(ZoneType.Battlefield);
CardCollection list = new CardCollection();
for (Player pl : opponents) {
list.addAll(pl.getCardsIn(ZoneType.Battlefield));
}
list = CardLists.getValidCards(list, tgt.getValidTgts(), sa.getActivatingPlayer(), sa.getHostCard(), sa);
@@ -135,17 +138,13 @@ public class ControlGainAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (!sa.canTarget(c)) {
if (!c.canBeTargetedBy(sa)) {
return false;
}
if (sa.isTrigger()) {
return true;
}
if (!c.canBeControlledBy(ai)) {
return false;
}
// do not take perm control on something that leaves the play end of turn
if (!lose.contains("EOT") && c.hasSVar("EndOfTurnLeavePlay")) {
return false;
@@ -198,11 +197,11 @@ public class ControlGainAi extends SpellAbilityAi {
}
}
while (sa.canAddMoreTarget()) {
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
Card t = null;
if (list.isEmpty()) {
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
if ((sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -211,12 +210,7 @@ public class ControlGainAi extends SpellAbilityAi {
}
}
// TODO check life of controller and consider stealing from another opponent so the risk of your army disappearing is spread out
while (t == null) {
// filter by MustTarget requirement
CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
if (planeswalkers > 0) {
t = ComputerUtilCard.getBestPlaneswalkerAI(list);
} else if (creatures > 0) {
@@ -226,9 +220,9 @@ public class ControlGainAi extends SpellAbilityAi {
} else if (lands > 0) {
t = ComputerUtilCard.getBestLandAI(list);
} else if (enchantments > 0) {
t = ComputerUtilCard.getBestEnchantmentAI(list, sa, false);
t = ComputerUtilCard.getBestEnchantmentAI(list, sa, true);
} else {
t = ComputerUtilCard.getMostExpensivePermanentAI(list);
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
}
if (t != null) {
@@ -244,11 +238,6 @@ public class ControlGainAi extends SpellAbilityAi {
enchantments--;
}
// Restore original list for next loop if filtered by MustTarget requirement
if (mustTargetFiltered) {
list = originalList;
}
if (!sa.canTarget(t)) {
list.remove(t);
t = null;
@@ -265,30 +254,23 @@ public class ControlGainAi extends SpellAbilityAi {
}
return true;
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) {
if (sa.getTargetRestrictions() == null) {
if (mandatory) {
return true;
}
} else {
if (sa.hasParam("TargetingPlayer") || (!this.canPlayAI(ai, sa) && mandatory)) {
if (sa.getTargetRestrictions().canOnlyTgtOpponent()) {
List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
}
sa.getTargets().add(Aggregates.random(oppList));
return true;
}
if(sa.hasParam("TargetingPlayer") || (!this.canPlayAI(ai, sa) && mandatory)) {
List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return false;
} else {
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
}
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
}
}
@@ -298,16 +280,7 @@ public class ControlGainAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
final Game game = ai.getGame();
// Special card logic that is processed elsewhere
if (sa.hasParam("AILogic")) {
if (("DonateTargetPerm").equals(sa.getParam("AILogic"))) {
// Donate step 2 - target a donatable permanent.
return SpecialCardAi.Donate.considerDonatingPermanent(ai, sa);
}
}
if (!sa.usesTargeting()) {
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (sa.hasParam("AllValid")) {
CardCollectionView tgtCards = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
@@ -326,10 +299,11 @@ public class ControlGainAi extends SpellAbilityAi {
} else {
return this.canPlayAI(ai, sa);
}
}
} // pumpDrawbackAI()
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
final List<Card> cards = Lists.newArrayList();
for (Player p : options) {
cards.addAll(p.getCreaturesInPlay());

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