Compare commits

..

17 Commits

Author SHA1 Message Date
Hans Mackowiak
25b6db1536 fix unused variables 2021-03-25 09:32:20 +01:00
Hans Mackowiak
245586cefa GameAction: removeExiledWith when zone is not exile 2021-03-25 09:32:20 +01:00
Hans Mackowiak
21999488f4 fix usage of CardLists getValidCards using CardTraitBase if able 2021-03-25 09:32:20 +01:00
Hans Mackowiak
d8b9524f80 Card: ChosenType linked to CTB 2021-03-25 09:32:20 +01:00
Hans Mackowiak
3bda4ffe0a AI: fix moved Devotion xcount 2021-03-25 09:32:20 +01:00
Hans Mackowiak
4dba174a6c Card: chosenColors use Table for LinkedAbilities 2021-03-25 09:32:20 +01:00
Hans Mackowiak
f57f6e1c40 fix skyclave apparition and spell queller exiling multiple cards 2021-03-25 09:32:20 +01:00
Hans Mackowiak
c9c974ad00 ExiledWithTable: add way to exile without CardTraitBase for GameState 2021-03-25 09:32:20 +01:00
Hans Mackowiak
18be2c67b6 CardTraitBase: add matchesValidParam and use for Trigger 2021-03-25 09:32:20 +01:00
Michael Kamensky
c27fa18ed7 - Unbreak some AI logic parts 2021-03-25 09:32:20 +01:00
Michael Kamensky
fb7a212597 - Port over the AI logic for Ashiok 2021-03-25 09:32:20 +01:00
Hans Mackowiak
c05f2396a1 Hideaway: fix keyword using ExiledWith 2021-03-25 09:32:20 +01:00
Hans Mackowiak
0ac7fb59e6 CardTraitBase: matchesValid as instance method, better use for isValid 2021-03-25 09:32:20 +01:00
Hans Mackowiak
1db6a772ed cards: update more cards with Remember and ExiledWith 2021-03-25 09:32:20 +01:00
Michael Kamensky
d12527ff21 - Update PC_041415 to work with the changed card script. 2021-03-25 09:32:20 +01:00
Hans Mackowiak
ab5eb24d7a Fix ixalans_binding using Static+ExiledWith 2021-03-25 09:32:20 +01:00
Hans Mackowiak
9f5fd3f35f ExiledWithTable to reduce the need for Remember 2021-03-25 09:32:20 +01:00
25804 changed files with 119007 additions and 333585 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

3
.gitignore vendored
View File

@@ -17,7 +17,6 @@
.vscode/settings.json .vscode/settings.json
.vscode/launch.json .vscode/launch.json
.factorypath
# Ignore NetBeans config files # Ignore NetBeans config files
@@ -33,8 +32,6 @@ bin
gen gen
*.log *.log
# Ignore macOS Spotlight rubbish
.DS_Store
# TODO: specify what these ignores are for (releasing?) # TODO: specify what these ignores are for (releasing?)

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

100
README.md
View File

@@ -1,44 +1,44 @@
# Forge # 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 IDE such as IntelliJ or Eclipse
- Java JDK 8 or later (some IDEs such as Eclipse require JDK11+, whereas the Android build currently only works with JDK8) - Java JDK 8 or later
- Git - Git
- Git client (optional) - Git client (optional)
- Maven - Maven
- GitHub account - Gitlab account
- Libgdx (optional: familiarity with this library is helpful for mobile platform development) - Libgdx (optional: familiarity with this library is helpful for mobile platform development)
- Android SDK (optional: for Android releases) - Android SDK (optional: for Android releases)
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx) - 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 - 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` - 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. 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 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 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 GitHub. "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 GitHub account. - Fork the Forge git repo to your Gitlab account.
- Clone your forked repo to your local machine. - Clone your forked repo to your local machine.
@@ -55,9 +55,9 @@ Eclipse includes Maven integration so a separate install is not necessary. For
- Once everything builds, all errors should disappear. You can now advance to Project launch. - 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. 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! - 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. This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
@@ -73,24 +73,24 @@ This is the configuration used for doing mobile development using the Windows /
- A view similar to a mobile phone should appear. Enjoy! - 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. 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 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 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. in the following instructions as your 'Android SDK Install' path.
##### Linux / Mac OSX #### Linux / Mac OSX
TBD 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 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 from: https://github.com/khaledev/ADT/releases
@@ -98,24 +98,25 @@ 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 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. 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: 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 SDK Build-tools 26.0.1
- Android 8.0.0 (API 26) SDK Platform - Android 7.1.1 (API 25) SDK Platform
- Google USB Driver (in case your phone is not detected by ADB) - Google USB Driver 11
Note that this will populate additional tools in the Android SDK install path extracted above. 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 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/.
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard-4.7/.
- Extract your Proguard version to the Android SDK install path under tools/. You will need to either rename the dir proguard-<your-version> to proguard/ or, if your filesystem supports it, use a symbolic link (the later is highly recommended), such as `ln -s proguard proguard-<your-version>`. - 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 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. 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. 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. You'll need to have the Android SDK install path platform-tools/ path in your command search path to easily deploy builds.
@@ -148,14 +149,14 @@ 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` - Install the new apk: `adb install forge-android-[version].apk`
#### Android Debugging ### Android Debugging
Assuming the apk is installed, launch it from the device. 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. 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. SNAPSHOT builds can be built via the Maven integration in Eclipse.
@@ -166,19 +167,19 @@ SNAPSHOT builds can be built via the Maven integration in Eclipse.
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT 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. 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: 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
- forge-gui-mobile-dev - 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. 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. 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. 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. 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. 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. 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> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>1.6.50-SNAPSHOT</version> <version>1.6.40-SNAPSHOT</version>
</parent> </parent>
<artifactId>forge-ai</artifactId> <artifactId>forge-ai</artifactId>

File diff suppressed because it is too large Load Diff

View File

@@ -28,8 +28,6 @@ import com.google.common.base.Predicates;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
@@ -38,11 +36,8 @@ import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -73,11 +68,7 @@ public class AiBlockController {
private boolean lifeInDanger = false; private boolean lifeInDanger = false;
// set to true when AI is predicting a blocking for another player so it doesn't use hidden information public AiBlockController(Player aiPlayer) {
private boolean checkingOther = false;
public AiBlockController(Player aiPlayer, boolean checkingOther) {
this.checkingOther = checkingOther;
ai = aiPlayer; ai = aiPlayer;
} }
@@ -103,13 +94,14 @@ public class AiBlockController {
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) { private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>(); final List<Card> blockers = new ArrayList<>();
// Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus, // 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 unless we're simulating an outcome outside of real combat // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
for (final Card b : blockersLeft) { 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); blockers.add(b);
} }
} }
return blockers; return blockers;
} }
@@ -117,10 +109,10 @@ public class AiBlockController {
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) { private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>(); final List<Card> blockers = new ArrayList<>();
// Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus, // 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 unless we're simulating an outcome outside of real combat // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
for (final Card b : blockersLeft) { 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); blockers.add(b);
} }
} }
@@ -131,6 +123,7 @@ public class AiBlockController {
private List<Card> sortPotentialAttackers(final Combat combat) { private List<Card> sortPotentialAttackers(final Combat combat) {
final CardCollection sortedAttackers = new CardCollection(); final CardCollection sortedAttackers = new CardCollection();
CardCollection firstAttacker = new CardCollection(); CardCollection firstAttacker = new CardCollection();
final FCollectionView<GameEntity> defenders = combat.getDefenders(); final FCollectionView<GameEntity> defenders = combat.getDefenders();
// If I don't have any planeswalkers then sorting doesn't really matter // If I don't have any planeswalkers then sorting doesn't really matter
@@ -154,11 +147,13 @@ public class AiBlockController {
return attackers; return attackers;
} }
final boolean bLifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
// TODO Add creatures attacking Planeswalkers in order of which we want to protect // TODO Add creatures attacking Planeswalkers in order of which we want to protect
// defend planeswalkers with more loyalty before planeswalkers with less loyalty // defend planeswalkers with more loyalty before planeswalkers with less loyalty
// if planeswalker will be too difficult to defend don't even bother // if planeswalker will be too difficult to defend don't even bother
for (GameEntity defender : defenders) { for (GameEntity defender : defenders) {
if (defender instanceof Card && ((Card) defender).getController().equals(ai)) { if (defender instanceof Card) {
final CardCollection attackers = combat.getAttackersOf(defender); final CardCollection attackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat // Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers); CardLists.sortByPowerDesc(attackers);
@@ -168,7 +163,7 @@ public class AiBlockController {
} }
} }
if (ComputerUtilCombat.lifeInDanger(ai, combat)) { if (bLifeInDanger) {
// add creatures attacking the Player to the front of the list // add creatures attacking the Player to the front of the list
for (final Card c : firstAttacker) { for (final Card c : firstAttacker) {
sortedAttackers.add(0, c); sortedAttackers.add(0, c);
@@ -180,16 +175,24 @@ public class AiBlockController {
return sortedAttackers; return sortedAttackers;
} }
// ======================= block assignment functions
// ================================
// Good Blocks means a good trade or no trade // Good Blocks means a good trade or no trade
private void makeGoodBlocks(final Combat combat) { private void makeGoodBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : 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; continue;
} }
Card blocker = null; Card blocker = null;
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true); final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
final List<Card> safeBlockers = getSafeBlockers(combat, attacker, blockers); final List<Card> safeBlockers = getSafeBlockers(combat, attacker, blockers);
@@ -268,9 +271,11 @@ public class AiBlockController {
} }
if (mode == TriggerType.DamageDone) { if (mode == TriggerType.DamageDone) {
if (trigger.matchesValidParam("ValidSource", attacker) if ((!trigParams.containsKey("ValidSource")
|| trigger.matchesValid(attacker, trigParams.get("ValidSource").split(",")))
&& attacker.getNetCombatDamage() > 0 && attacker.getNetCombatDamage() > 0
&& trigger.matchesValidParam("ValidTarget", combat.getDefenderByAttacker(attacker))) { && (!trigParams.containsKey("ValidTarget")
|| trigger.matchesValid(combat.getDefenderByAttacker(attacker), trigParams.get("ValidTarget").split(",")))) {
value += 50; value += 50;
} }
} else if (mode == TriggerType.AttackerUnblocked) { } else if (mode == TriggerType.AttackerUnblocked) {
@@ -290,15 +295,17 @@ public class AiBlockController {
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
} }
} }
attackersLeft = new ArrayList<>(currentAttackers); attackersLeft = (new ArrayList<>(currentAttackers));
// 6. Blockers that don't survive until the next turn anyway // 6. Blockers that don't survive until the next turn anyway
for (final Card attacker : attackersLeft) { 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; continue;
} }
Card blocker = null; Card blocker = null;
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true); final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
for (Card b : blockers) { for (Card b : blockers) {
@@ -317,49 +324,10 @@ public class AiBlockController {
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
} }
} }
attackersLeft = new ArrayList<>(currentAttackers); attackersLeft = (new ArrayList<>(currentAttackers));
} }
private Predicate<Card> rampagesOrNeedsManyToBlock(final Combat combat) { static final Predicate<Card> rampagesOrNeedsManyToBlock = Predicates.or(CardPredicates.containsKeyword("Rampage"), CardPredicates.containsKeyword("CantBeBlockedByAmount GT"));
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;
}
};
}
// Good Gang Blocks means a good trade or no trade // Good Gang Blocks means a good trade or no trade
/** /**
@@ -370,12 +338,12 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object. * @param combat a {@link forge.game.combat.Combat} object.
*/ */
private void makeGangBlocks(final Combat combat) { private void makeGangBlocks(final Combat combat) {
List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock(combat))); List<Card> currentAttackers = CardLists.filter(attackersLeft, Predicates.not(rampagesOrNeedsManyToBlock));
List<Card> blockers; List<Card> blockers;
// Try to block an attacker without first strike with a gang of first strikers // Try to block an attacker without first strike with a gang of first strikers
for (final Card attacker : attackersLeft) { 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 // don't bother with gang blocking if the attacker will regenerate or is indestructible
continue; continue;
} }
@@ -395,12 +363,13 @@ public class AiBlockController {
if (firstStrikeBlockers.size() > 1) { if (firstStrikeBlockers.size() > 1) {
CardLists.sortByPowerDesc(firstStrikeBlockers); CardLists.sortByPowerDesc(firstStrikeBlockers);
for (final Card blocker : 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); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// if the total damage of the blockgang was not enough // 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 if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) < damageNeeded
|| CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size()) { || CombatUtil.needsBlockers(attacker) > blockGang.size()) {
blockGang.add(blocker); blockGang.add(blocker);
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) >= damageNeeded) { if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) >= damageNeeded) {
currentAttackers.remove(attacker); currentAttackers.remove(attacker);
@@ -416,22 +385,18 @@ public class AiBlockController {
} }
} }
attackersLeft = new ArrayList<>(currentAttackers); attackersLeft = (new ArrayList<>(currentAttackers));
currentAttackers = new ArrayList<>(attackersLeft);
boolean considerTripleBlock = true; boolean considerTripleBlock = true;
// Try to block an attacker with two blockers of which only one will die // Try to block an attacker with two blockers of which only one will die
for (final Card attacker : attackersLeft) { 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 // don't bother with gang blocking if the attacker will regenerate or is indestructible
continue; 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); int evalAttackerValue = ComputerUtilCard.evaluateCreature(attacker);
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false); blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -441,6 +406,11 @@ public class AiBlockController {
int currentValue; // The value of the creatures in the blockgang int currentValue; // The value of the creatures in the blockgang
boolean foundDoubleBlock = false; // if true, a good double block is found 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 // 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 // Don't use blockers without First Strike or Double Strike if attacker has it
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() { usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@@ -450,7 +420,8 @@ public class AiBlockController {
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) { && !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
return false; 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) { if (usableBlockers.size() < 2) {
@@ -471,9 +442,9 @@ public class AiBlockController {
final int additionalDamage = ComputerUtilCombat.dealsDamageAsBlocker(attacker, blocker); final int additionalDamage = ComputerUtilCombat.dealsDamageAsBlocker(attacker, blocker);
final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(blocker, attacker.getNetCombatDamage(), attacker, true); final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(blocker, attacker.getNetCombatDamage(), attacker, true);
final int addedValue = ComputerUtilCard.evaluateCreature(blocker); 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); + 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) && !(damageNeeded > currentDamage + additionalDamage)
// The attacker will be killed // The attacker will be killed
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage() && (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
@@ -483,7 +454,8 @@ public class AiBlockController {
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat))) || (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
// or life is in danger // or life is in danger
&& CombatUtil.canBlock(attacker, blocker, combat)) { && 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); currentAttackers.remove(attacker);
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
if (CombatUtil.canBlock(attacker, leader, combat)) { if (CombatUtil.canBlock(attacker, leader, combat)) {
@@ -511,7 +483,7 @@ public class AiBlockController {
final int additionalDamage2 = ComputerUtilCombat.dealsDamageAsBlocker(attacker, secondBlocker); final int additionalDamage2 = ComputerUtilCombat.dealsDamageAsBlocker(attacker, secondBlocker);
final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(secondBlocker, attacker.getNetCombatDamage(), attacker, true); final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(secondBlocker, attacker.getNetCombatDamage(), attacker, true);
final int addedValue2 = ComputerUtilCard.evaluateCreature(secondBlocker); 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); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, secondBlocker, combat, false);
List<Card> usableBlockersAsThird = new ArrayList<>(usableBlockers); List<Card> usableBlockersAsThird = new ArrayList<>(usableBlockers);
@@ -524,7 +496,7 @@ public class AiBlockController {
final int addedValue3 = ComputerUtilCard.evaluateCreature(secondBlocker); final int addedValue3 = ComputerUtilCard.evaluateCreature(secondBlocker);
final int netCombatDamage = attacker.getNetCombatDamage(); 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) && !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
// The attacker will be killed // The attacker will be killed
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage && ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
@@ -538,7 +510,8 @@ public class AiBlockController {
// or life is in danger // or life is in danger
&& CombatUtil.canBlock(attacker, secondBlocker, combat) && CombatUtil.canBlock(attacker, secondBlocker, combat)
&& CombatUtil.canBlock(attacker, thirdBlocker, 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); currentAttackers.remove(attacker);
combat.addBlocker(attacker, thirdBlocker); combat.addBlocker(attacker, thirdBlocker);
if (CombatUtil.canBlock(attacker, secondBlocker, combat)) { if (CombatUtil.canBlock(attacker, secondBlocker, combat)) {
@@ -553,7 +526,7 @@ public class AiBlockController {
} }
} }
attackersLeft = new ArrayList<>(currentAttackers); attackersLeft = (new ArrayList<>(currentAttackers));
} }
private void makeGangNonLethalBlocks(final Combat combat) { private void makeGangNonLethalBlocks(final Combat combat) {
@@ -562,19 +535,19 @@ public class AiBlockController {
// Try to block a Menace attacker with two blockers, neither of which will die // Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) { for (final Card attacker : attackersLeft) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) <= 1) { if (!attacker.hasKeyword(Keyword.MENACE) && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
continue; continue;
} }
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false); blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
List<Card> usableBlockers;
final List<Card> blockGang = new ArrayList<>(); final List<Card> blockGang = new ArrayList<>();
int absorbedDamage; // The amount of damage needed to kill the first blocker 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 @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
return c.getNetToughness() > attacker.getNetCombatDamage() // performance shortcut return c.getNetToughness() > attacker.getNetCombatDamage();
|| c.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, c, true) > attacker.getNetCombatDamage();
} }
}); });
if (usableBlockers.size() < 2) { if (usableBlockers.size() < 2) {
@@ -601,7 +574,7 @@ public class AiBlockController {
} }
} }
attackersLeft = new ArrayList<>(currentAttackers); attackersLeft = (new ArrayList<>(currentAttackers));
} }
// Bad Trade Blocks (should only be made if life is in danger) // Bad Trade Blocks (should only be made if life is in danger)
@@ -614,11 +587,15 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object. * @param combat a {@link forge.game.combat.Combat} object.
*/ */
private void makeTradeBlocks(final Combat combat) { private void makeTradeBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
List<Card> killingBlockers; List<Card> killingBlockers;
for (final Card attacker : attackersLeft) { 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; continue;
} }
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
@@ -646,11 +623,12 @@ public class AiBlockController {
} }
} }
} }
attackersLeft = new ArrayList<>(currentAttackers); attackersLeft = (new ArrayList<>(currentAttackers));
} }
// Chump Blocks (should only be made if life is in danger) // Chump Blocks (should only be made if life is in danger)
private void makeChumpBlocks(final Combat combat) { private void makeChumpBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
makeChumpBlocks(combat, currentAttackers); makeChumpBlocks(combat, currentAttackers);
@@ -661,14 +639,17 @@ public class AiBlockController {
} }
private void makeChumpBlocks(final Combat combat, List<Card> attackers) { private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
if (attackers.isEmpty() || !ComputerUtilCombat.lifeInDanger(ai, combat)) { if (attackers.isEmpty() || !ComputerUtilCombat.lifeInDanger(ai, combat)) {
return; return;
} }
Card attacker = attackers.get(0); 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("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)) { || ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0); attackers.remove(0);
makeChumpBlocks(combat, attackers); makeChumpBlocks(combat, attackers);
@@ -713,10 +694,14 @@ public class AiBlockController {
// Block creatures with "can't be blocked except by two or more creatures" // Block creatures with "can't be blocked except by two or more creatures"
private void makeMultiChumpBlocks(final Combat combat) { private void makeMultiChumpBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : currentAttackers) { 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; continue;
} }
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true); List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
@@ -745,17 +730,19 @@ public class AiBlockController {
/** Reinforce blockers blocking attackers with trample (should only be made if life is in danger) */ /** Reinforce blockers blocking attackers with trample (should only be made if life is in danger) */
private void reinforceBlockersAgainstTrample(final Combat combat) { private void reinforceBlockersAgainstTrample(final Combat combat) {
List<Card> chumpBlockers; List<Card> chumpBlockers;
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE); List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, 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 // TODO - should check here for a "rampage-like" trigger that replaced
// reinforce when actually possible without losing material. // the keyword:
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(changesPTWhenBlocked(true))); // "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
for (final Card attacker : tramplingAttackers) { 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("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("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue; continue;
@@ -777,13 +764,13 @@ public class AiBlockController {
/** Support blockers not destroying the attacker with more blockers to try to kill the attacker */ /** Support blockers not destroying the attacker with more blockers to try to kill the attacker */
private void reinforceBlockersToKill(final Combat combat) { private void reinforceBlockersToKill(final Combat combat) {
List<Card> safeBlockers; List<Card> safeBlockers;
List<Card> blockers; List<Card> blockers;
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, 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 // TODO - should check here for a "rampage-like" trigger that replaced
// reinforce when actually possible without losing material. // the keyword: "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
targetAttackers = CardLists.filter(targetAttackers, Predicates.not(changesPTWhenBlocked(false)));
for (final Card attacker : targetAttackers) { for (final Card attacker : targetAttackers) {
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false); blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -801,7 +788,7 @@ public class AiBlockController {
if (blockers.size() > 0) { if (blockers.size() > 0) {
safeBlockers = getSafeBlockers(combat, attacker, blockers); safeBlockers = getSafeBlockers(combat, attacker, blockers);
for (final Card blocker : safeBlockers) { for (final Card blocker : safeBlockers) {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker, false) final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// Add an additional blocker if the current blockers are not // Add an additional blocker if the current blockers are not
// enough and the new one would deal additional damage // enough and the new one would deal additional damage
@@ -814,7 +801,7 @@ public class AiBlockController {
} }
} }
// don't try to kill what can't be killed // 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; continue;
} }
@@ -828,7 +815,7 @@ public class AiBlockController {
} }
for (final Card blocker : safeBlockers) { for (final Card blocker : safeBlockers) {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker, false) final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// Add an additional blocker if the current blockers are not // Add an additional blocker if the current blockers are not
// enough and the new one would deal the remaining damage // enough and the new one would deal the remaining damage
@@ -847,7 +834,7 @@ public class AiBlockController {
} }
private void makeChumpBlocksToSavePW(Combat combat) { 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 // 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 // dangerous combat which threatens lethal or severe damage to face
return; return;
@@ -855,7 +842,7 @@ public class AiBlockController {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER); 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); final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) { if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {
@@ -871,7 +858,7 @@ public class AiBlockController {
int damageToPW = 0; int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) { for (final Card pwatkr : combat.getAttackersOf(def)) {
if (!combat.isBlocked(pwatkr)) { 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(CounterEnumType.LOYALTY)) {
@@ -883,7 +870,7 @@ public class AiBlockController {
CardCollection pwsWithChumpBlocks = new CardCollection(); CardCollection pwsWithChumpBlocks = new CardCollection();
CardCollection chosenChumpBlockers = 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 @Override
public boolean apply(Card card) { public boolean apply(Card card) {
return ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken return ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken
@@ -946,8 +933,10 @@ public class AiBlockController {
} }
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) { 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 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); combat.removeFromCombat(blocker);
} }
@@ -958,16 +947,11 @@ public class AiBlockController {
/** Assigns blockers for the provided combat instance (in favor of player passes to ctor) */ /** Assigns blockers for the provided combat instance (in favor of player passes to ctor) */
public void assignBlockersForCombat(final Combat combat) { public void assignBlockersForCombat(final Combat combat) {
assignBlockersForCombat(combat, null);
}
public void assignBlockersForCombat(final Combat combat, final CardCollection exludedBlockers) {
List<Card> possibleBlockers = ai.getCreaturesInPlay(); List<Card> possibleBlockers = ai.getCreaturesInPlay();
if (exludedBlockers != null && !exludedBlockers.isEmpty()) {
possibleBlockers.removeAll(exludedBlockers);
}
attackers = sortPotentialAttackers(combat); attackers = sortPotentialAttackers(combat);
assignBlockers(combat, possibleBlockers); assignBlockers(combat, possibleBlockers);
} }
/** /**
* assignBlockersForCombat() with additional and possibly "virtual" blockers. * assignBlockersForCombat() with additional and possibly "virtual" blockers.
* @param combat combat instance * @param combat combat instance
@@ -1023,10 +1007,6 @@ public class AiBlockController {
} }
} }
if (attackersLeft.isEmpty()) {
return;
}
// remove all blockers that can't block anyway // remove all blockers that can't block anyway
for (final Card b : possibleBlockers) { for (final Card b : possibleBlockers) {
if (!CombatUtil.canBlock(b, combat)) { if (!CombatUtil.canBlock(b, combat)) {
@@ -1034,6 +1014,10 @@ public class AiBlockController {
} }
} }
if (attackersLeft.isEmpty()) {
return;
}
// Begin with the weakest blockers // Begin with the weakest blockers
CardLists.sortByPowerAsc(blockersLeft); CardLists.sortByPowerAsc(blockersLeft);
@@ -1042,7 +1026,7 @@ public class AiBlockController {
makeGangBlocks(combat); makeGangBlocks(combat);
// When the AI holds some Fog effect, don't bother about lifeInDanger // 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); lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
makeTradeBlocks(combat); // choose necessary trade blocks makeTradeBlocks(combat); // choose necessary trade blocks
@@ -1052,25 +1036,29 @@ public class AiBlockController {
} else { } else {
lifeInDanger = false; 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)) { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat); reinforceBlockersAgainstTrample(combat);
} else { } else {
lifeInDanger = false; lifeInDanger = false;
} }
// Support blockers not destroying the attacker with more blockers // Support blockers not destroying the attacker with more blockers
// to try to kill the attacker // to
// try to kill the attacker
if (!lifeInDanger) { if (!lifeInDanger) {
reinforceBlockersToKill(combat); reinforceBlockersToKill(combat);
} }
// TODO could be made more accurate if this would be inside each blocker choosing loop instead // == 2. If the AI life would still be in danger make a safer
lifeInDanger |= removeUnpayableBlocks(combat); // approach ==
// == 2. If the AI life would still be in danger make a safer approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { 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 makeTradeBlocks(combat); // choose necessary trade blocks
// if life is in danger
makeGoodBlocks(combat); makeGoodBlocks(combat);
// choose necessary chump blocks if life is still in danger // choose necessary chump blocks if life is still in danger
if (ComputerUtilCombat.lifeInDanger(ai, combat)) { if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
@@ -1078,7 +1066,8 @@ public class AiBlockController {
} else { } else {
lifeInDanger = false; 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)) { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat); reinforceBlockersAgainstTrample(combat);
} else { } else {
@@ -1088,29 +1077,38 @@ public class AiBlockController {
reinforceBlockersToKill(combat); 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)) { if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); clearBlockers(combat, possibleBlockers); // reset every block
makeChumpBlocks(combat); // assignment
makeChumpBlocks(combat); // choose chump blocks
if (ComputerUtilCombat.lifeInDanger(ai, combat)) { if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeTradeBlocks(combat); makeTradeBlocks(combat); // choose necessary trade
} }
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeGoodBlocks(combat); makeGoodBlocks(combat);
} else { }
// Reinforce blockers blocking attackers with trample if life is
// still in danger
else {
reinforceBlockersAgainstTrample(combat); reinforceBlockersAgainstTrample(combat);
} }
makeGangBlocks(combat); makeGangBlocks(combat);
// Support blockers not destroying the attacker with more
// blockers
// to try to kill the attacker
reinforceBlockersToKill(combat); reinforceBlockersToKill(combat);
} }
} }
// assign blockers that have to block // 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 // if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) { for (final Card blocker : blockersLeft) {
if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) { if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
chumpBlockers.add(blocker); chumpBlockers.add(blocker);
} }
} }
@@ -1120,10 +1118,11 @@ public class AiBlockController {
blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false); blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false);
for (final Card blocker : blockers) { for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker) 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."))) { || blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
if (!blocker.getMustBlockCards().isEmpty()) { if (blocker.getMustBlockCards() != null) {
int mustBlockAmt = blocker.getMustBlockCards().size(); int mustBlockAmt = blocker.getMustBlockCards().size();
final CardCollectionView blockedSoFar = combat.getAttackersBlockedBy(blocker); final CardCollectionView blockedSoFar = combat.getAttackersBlockedBy(blocker);
boolean canBlockAnother = CombatUtil.canBlockMoreCreatures(blocker, blockedSoFar); boolean canBlockAnother = CombatUtil.canBlockMoreCreatures(blocker, blockedSoFar);
@@ -1138,9 +1137,10 @@ public class AiBlockController {
} }
} }
// check to see if it's possible to defend a Planeswalker under attack with a chump block, // 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 // 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); makeChumpBlocksToSavePW(combat);
} }
@@ -1151,8 +1151,8 @@ public class AiBlockController {
//Check for validity of blocks in case something slipped through //Check for validity of blocks in case something slipped through
for (Card attacker : attackers) { for (Card attacker : attackers) {
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) { if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
for (final Card blocker : CardLists.filterControlledBy(combat.getBlockers(attacker), ai)) { for (final Card blocker : combat.getBlockers(attacker)) {
// don't touch other player's blockers if (blocker.getController() == ai) // don't touch other player's blockers
combat.removeFromCombat(blocker); combat.removeFromCombat(blocker);
} }
} }
@@ -1201,7 +1201,7 @@ public class AiBlockController {
final CardCollection result = new CardCollection(); final CardCollection result = new CardCollection();
boolean newBlockerIsAdded = false; boolean newBlockerIsAdded = false;
// The new blocker comes right after this one // 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 if (newBlockerRightAfter == null
&& damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) { && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
result.add(blocker); result.add(blocker);
@@ -1269,8 +1269,6 @@ public class AiBlockController {
int oppCreatureCount = 0; int oppCreatureCount = 0;
if (ai.getController().isAI()) { if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); 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); enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS); randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT); randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
@@ -1282,7 +1280,6 @@ public class AiBlockController {
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER); chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER); chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
} }
}
if (!enableRandomTrades) { if (!enableRandomTrades) {
return false; return false;
@@ -1301,7 +1298,7 @@ public class AiBlockController {
return false; 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; float chanceStep = (maxRandomTradeChance - minRandomTradeChance) / numSteps;
int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep)); int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep));
if (chance > maxRandomTradeChance) { if (chance > maxRandomTradeChance) {
@@ -1309,6 +1306,7 @@ public class AiBlockController {
} }
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false); 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 atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken(); boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
@@ -1317,20 +1315,17 @@ public class AiBlockController {
chance = Math.max(0, chance - chanceModForEmbalm); chance = Math.max(0, chance - chanceModForEmbalm);
} }
int evalBlk; if (blocker.isFaceDown() && blocker.getState(CardStateName.Original).getType().isCreature()) {
if (blocker.isFaceDown() && blocker.getView().canFaceDownBeShownTo(ai.getView(), false) && blocker.getState(CardStateName.Original).getType().isCreature()) {
// if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it // 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 // in relation to the original state, not to the Morph state
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true); 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; int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower(); boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();
boolean creatureParityOrAllowedDiff = aiCreatureCount boolean creatureParityOrAllowedDiff = aiCreatureCount
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount; + (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
boolean wantToTradeWithCreatInHand = !checkingOther && randomTradeIfCreatInHand boolean wantToTradeWithCreatInHand = randomTradeIfCreatInHand
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.Presets.CREATURES) && !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).isEmpty()
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount; && aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW) boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
&& combat.getDefenderByAttacker(attacker) instanceof Card && combat.getDefenderByAttacker(attacker) instanceof Card
@@ -1342,25 +1337,4 @@ public class AiBlockController {
&& (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand) && (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand)
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker); && (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

@@ -57,9 +57,7 @@ public class AiCardMemory {
BOUNCED_THIS_TURN, // These cards were bounced this turn BOUNCED_THIS_TURN, // These cards were bounced this turn
ACTIVATED_THIS_TURN, // These cards had their ability activated 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 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 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
//REVEALED_CARDS // stub, not linked to AI code yet //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> memActivatedThisTurn;
private final Set<Card> memChosenFogEffect; private final Set<Card> memChosenFogEffect;
private final Set<Card> memMarkedToAvoidReentry; private final Set<Card> memMarkedToAvoidReentry;
private final Set<Card> memPaysTapCost;
private final Set<Card> memPaysSacCost;
public AiCardMemory() { public AiCardMemory() {
this.memMandatoryAttackers = new HashSet<>(); this.memMandatoryAttackers = new HashSet<>();
@@ -91,8 +87,6 @@ public class AiCardMemory {
this.memChosenFogEffect = new HashSet<>(); this.memChosenFogEffect = new HashSet<>();
this.memMarkedToAvoidReentry = new HashSet<>(); this.memMarkedToAvoidReentry = new HashSet<>();
this.memHeldManaSourcesForNextSpell = new HashSet<>(); this.memHeldManaSourcesForNextSpell = new HashSet<>();
this.memPaysTapCost = new HashSet<>();
this.memPaysSacCost = new HashSet<>();
} }
private Set<Card> getMemorySet(MemorySet set) { private Set<Card> getMemorySet(MemorySet set) {
@@ -121,10 +115,6 @@ public class AiCardMemory {
return memChosenFogEffect; return memChosenFogEffect;
case MARKED_TO_AVOID_REENTRY: case MARKED_TO_AVOID_REENTRY:
return memMarkedToAvoidReentry; return memMarkedToAvoidReentry;
case PAYS_TAP_COST:
return memPaysTapCost;
case PAYS_SAC_COST:
return memPaysSacCost;
//case REVEALED_CARDS: //case REVEALED_CARDS:
// return memRevealedCards; // return memRevealedCards;
default: default:
@@ -323,12 +313,6 @@ public class AiCardMemory {
} }
// Static functions to simplify access to AI card memory of a given AI player. // 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) { public static void rememberCard(Player ai, Card c, MemorySet set) {
if (!ai.getController().isAI()) { if (!ai.getController().isAI()) {
return; return;

File diff suppressed because it is too large Load Diff

View File

@@ -6,16 +6,15 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import forge.game.cost.*;
import org.apache.commons.lang3.ObjectUtils;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.card.CardType; import forge.card.CardType;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntityCounterTable; import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
@@ -23,12 +22,43 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.cost.CostAddMana;
import forge.game.cost.CostChooseCreatureType;
import forge.game.cost.CostDamage;
import forge.game.cost.CostDecisionMakerBase;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostDraw;
import forge.game.cost.CostExert;
import forge.game.cost.CostExile;
import forge.game.cost.CostExileFromStack;
import forge.game.cost.CostExiledMoveToGrave;
import forge.game.cost.CostFlipCoin;
import forge.game.cost.CostGainControl;
import forge.game.cost.CostGainLife;
import forge.game.cost.CostMill;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostPayLife;
import forge.game.cost.CostPutCardToLib;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveAnyCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostReturn;
import forge.game.cost.CostReveal;
import forge.game.cost.CostSacrifice;
import forge.game.cost.CostTap;
import forge.game.cost.CostTapType;
import forge.game.cost.CostUnattach;
import forge.game.cost.CostUntap;
import forge.game.cost.CostUntapType;
import forge.game.cost.PaymentDecision;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.Localizer;
import forge.util.TextUtil; import forge.util.TextUtil;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
@@ -39,8 +69,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
private final CardCollection discarded; private final CardCollection discarded;
private final CardCollection tapped; private final CardCollection tapped;
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) { public AiCostDecision(Player ai0, SpellAbility sa) {
super(ai0, effect); super(ai0);
ability = sa; ability = sa;
source = ability.getHostCard(); source = ability.getHostCard();
@@ -50,16 +80,27 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostAddMana cost) { public PaymentDecision visit(CostAddMana cost) {
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
return PaymentDecision.number(c); return PaymentDecision.number(c);
} }
@Override @Override
public PaymentDecision visit(CostChooseCreatureType cost) { public PaymentDecision visit(CostChooseCreatureType cost) {
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(), Integer amount = cost.convertAmount();
Lists.newArrayList()); Iterable<String> choices = player.getController().chooseSomeType(
return PaymentDecision.type(choice); Localizer.getInstance().getMessage("lblCreature"), ability, amount, amount, Lists.newArrayList(CardType.getAllCreatureTypes()));
if (choices == null || Iterables.isEmpty(choices)) {
return null;
}
return PaymentDecision.types(choices);
} }
@Override @Override
@@ -72,13 +113,15 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null; return null;
} }
return PaymentDecision.card(player.getLastDrawnCard()); return PaymentDecision.card(player.getLastDrawnCard());
} else if (cost.payCostFromSource()) { }
else if (cost.payCostFromSource()) {
if (!hand.contains(source)) { if (!hand.contains(source)) {
return null; return null;
} }
return PaymentDecision.card(source); return PaymentDecision.card(source);
} else if (type.equals("Hand")) { }
else if (type.equals("Hand")) {
if (hand.size() > 1 && ability.getActivatingPlayer() != null) { if (hand.size() > 1 && ability.getActivatingPlayer() != null) {
hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard, ability); hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard, ability);
} }
@@ -88,7 +131,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (type.contains("WithSameName")) { if (type.contains("WithSameName")) {
return null; return null;
} }
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
if (type.equals("Random")) { if (type.equals("Random")) {
CardCollectionView randomSubset = CardLists.getRandomSubList(new CardCollection(hand), c); CardCollectionView randomSubset = CardLists.getRandomSubList(new CardCollection(hand), c);
@@ -96,7 +142,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability); randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
} }
return PaymentDecision.card(randomSubset); return PaymentDecision.card(randomSubset);
} else if (type.equals("DifferentNames")) { }
else if (type.equals("DifferentNames")) {
CardCollection differentNames = new CardCollection(); CardCollection differentNames = new CardCollection();
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe")); CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
while (c > 0) { while (c > 0) {
@@ -113,7 +160,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
c--; c--;
} }
return PaymentDecision.card(differentNames); return PaymentDecision.card(differentNames);
} else { }
else {
final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollection result = aic.getCardsToDiscard(c, type.split(";"), ability, discarded); CardCollection result = aic.getCardsToDiscard(c, type.split(";"), ability, discarded);
@@ -126,23 +174,24 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostDamage cost) { public PaymentDecision visit(CostDamage cost) {
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
return PaymentDecision.number(c); return PaymentDecision.number(c);
} }
@Override @Override
public PaymentDecision visit(CostDraw cost) { public PaymentDecision visit(CostDraw cost) {
if (!cost.canPay(ability, player, isEffect())) { Integer c = cost.convertAmount();
return null;
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
} }
int c = cost.getAbilityAmount(ability);
List<Player> res = cost.getPotentialPlayers(player, ability); return PaymentDecision.number(c);
PaymentDecision decision = PaymentDecision.players(res);
decision.c = c;
return decision;
} }
@Override @Override
@@ -158,7 +207,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null; return null;
} }
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
if (cost.getFrom().equals(ZoneType.Library)) { if (cost.getFrom().equals(ZoneType.Library)) {
return PaymentDecision.card(player.getCardsIn(ZoneType.Library, c)); return PaymentDecision.card(player.getCardsIn(ZoneType.Library, c));
@@ -166,7 +218,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
else if (cost.sameZone) { else if (cost.sameZone) {
// TODO Determine exile from same zone for AI // TODO Determine exile from same zone for AI
return null; return null;
} else { }
else {
CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability); CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability);
return null == chosen ? null : PaymentDecision.card(chosen); return null == chosen ? null : PaymentDecision.card(chosen);
} }
@@ -174,6 +227,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostExileFromStack cost) { public PaymentDecision visit(CostExileFromStack cost) {
Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
List<SpellAbility> chosen = Lists.newArrayList(); List<SpellAbility> chosen = Lists.newArrayList();
for (SpellAbilityStackInstance si :source.getGame().getStack()) { for (SpellAbilityStackInstance si :source.getGame().getStack()) {
SpellAbility sp = si.getSpellAbility(true).getRootAbility(); SpellAbility sp = si.getSpellAbility(true).getRootAbility();
@@ -186,9 +244,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostExiledMoveToGrave cost) { public PaymentDecision visit(CostExiledMoveToGrave cost) {
Integer c = cost.convertAmount();
CardCollection chosen = new CardCollection(); CardCollection chosen = new CardCollection();
int c = cost.getAbilityAmount(ability); if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Exile), cost.getType().split(";"), player, source, ability); CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Exile), cost.getType().split(";"), player, source, ability);
@@ -196,7 +257,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null; return null;
} }
CardLists.sortByPowerDesc(typeList); CardLists.sortByPowerAsc(typeList);
Collections.reverse(typeList);
for (int i = 0; i < c; i++) { for (int i = 0; i < c; i++) {
chosen.add(typeList.get(i)); chosen.add(typeList.get(i));
@@ -211,7 +273,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.card(source); return PaymentDecision.card(source);
} }
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
final CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, source, ability); final CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, source, ability);
@@ -230,13 +295,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostFlipCoin cost) { public PaymentDecision visit(CostFlipCoin cost) {
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
return PaymentDecision.number(c); if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
} }
@Override
public PaymentDecision visit(CostRollDice cost) {
int c = cost.getAbilityAmount(ability);
return PaymentDecision.number(c); return PaymentDecision.number(c);
} }
@@ -246,7 +308,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.card(source); return PaymentDecision.card(source);
} }
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
final CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, source, ability); final CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, source, ability);
@@ -263,11 +328,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
return res.isEmpty() ? null : PaymentDecision.card(res); return res.isEmpty() ? null : PaymentDecision.card(res);
} }
@Override @Override
public PaymentDecision visit(CostGainLife cost) { public PaymentDecision visit(CostGainLife cost) {
final List<Player> oppsThatCanGainLife = Lists.newArrayList(); final List<Player> oppsThatCanGainLife = Lists.newArrayList();
for (final Player opp : cost.getPotentialTargets(player, ability)) { for (final Player opp : cost.getPotentialTargets(player, source)) {
if (opp.canGainLife()) { if (opp.canGainLife()) {
oppsThatCanGainLife.add(opp); oppsThatCanGainLife.add(opp);
} }
@@ -280,9 +346,13 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.players(oppsThatCanGainLife); return PaymentDecision.players(oppsThatCanGainLife);
} }
@Override @Override
public PaymentDecision visit(CostMill cost) { public PaymentDecision visit(CostMill cost) {
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
CardCollectionView topLib = player.getCardsIn(ZoneType.Library, c); CardCollectionView topLib = player.getCardsIn(ZoneType.Library, c);
return topLib.size() < c ? null : PaymentDecision.number(c); return topLib.size() < c ? null : PaymentDecision.number(c);
@@ -295,8 +365,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostPayLife cost) { public PaymentDecision visit(CostPayLife cost) {
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (!player.canPayLife(c, isEffect(), ability)) { if (c == null) {
// Generalize cost
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
if (!player.canPayLife(c)) {
return null; return null;
} }
// activator.payLife(c, null); // activator.payLife(c, null);
@@ -305,29 +379,37 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostPayEnergy cost) { public PaymentDecision visit(CostPayEnergy cost) {
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
if (!player.canPayEnergy(c)) { if (!player.canPayEnergy(c)) {
return null; return null;
} }
return PaymentDecision.number(c); return PaymentDecision.number(c);
} }
@Override @Override
public PaymentDecision visit(CostPutCardToLib cost) { public PaymentDecision visit(CostPutCardToLib cost) {
if (cost.payCostFromSource()) { if (cost.payCostFromSource()) {
return PaymentDecision.card(source); return PaymentDecision.card(source);
} }
Integer c = cost.convertAmount();
final Game game = player.getGame(); final Game game = player.getGame();
CardCollection chosen = new CardCollection(); CardCollection chosen = new CardCollection();
CardCollectionView list; CardCollectionView list;
if (cost.isSameZone()) { if (cost.isSameZone()) {
list = new CardCollection(game.getCardsIn(cost.getFrom())); list = new CardCollection(game.getCardsIn(cost.getFrom()));
} else { }
else {
list = new CardCollection(player.getCardsIn(cost.getFrom())); list = new CardCollection(player.getCardsIn(cost.getFrom()));
} }
int c = cost.getAbilityAmount(ability); if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
list = CardLists.getValidCards(list, cost.getType().split(";"), player, source, ability); list = CardLists.getValidCards(list, cost.getType().split(";"), player, source, ability);
@@ -343,7 +425,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
} }
} }
chosen = chosen.subList(0, c); chosen = chosen.subList(0, c);
} else { }
else {
chosen = ComputerUtil.choosePutToLibraryFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability); chosen = ComputerUtil.choosePutToLibraryFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability);
} }
return chosen.isEmpty() ? null : PaymentDecision.card(chosen); return chosen.isEmpty() ? null : PaymentDecision.card(chosen);
@@ -361,12 +444,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
Card card; Card card;
if (cost.getType().equals("Creature.YouCtrl")) { if (cost.getType().equals("Creature.YouCtrl")) {
card = ComputerUtilCard.getWorstCreatureAI(typeList); card = ComputerUtilCard.getWorstCreatureAI(typeList);
} else { }
else {
card = ComputerUtilCard.getWorstPermanentAI(typeList, false, false, false, false); card = ComputerUtilCard.getWorstPermanentAI(typeList, false, false, false, false);
} }
return PaymentDecision.card(card); return PaymentDecision.card(card);
} }
@Override @Override
public PaymentDecision visit(CostTap cost) { public PaymentDecision visit(CostTap cost) {
return PaymentDecision.number(0); return PaymentDecision.number(0);
@@ -374,12 +459,17 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostTapType cost) { public PaymentDecision visit(CostTapType cost) {
final String amount = cost.getAmount();
Integer c = cost.convertAmount();
String type = cost.getType(); String type = cost.getType();
boolean isVehicle = type.contains("+withTotalPowerGE"); boolean isVehicle = type.contains("+withTotalPowerGE");
CardCollection exclude = new CardCollection(); CardCollection exclude = new CardCollection();
exclude.addAll(tapped); exclude.addAll(tapped);
if (c == null && !isVehicle) {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
if (type.contains("sharesCreatureTypeWith")) { if (type.contains("sharesCreatureTypeWith")) {
return null; return null;
} }
@@ -409,7 +499,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
type = TextUtil.fastReplace(type, "+withTotalPowerGE", ""); type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), exclude); totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), exclude);
} else { } else {
int c = cost.getAbilityAmount(ability);
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, exclude, ability); totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, exclude, ability);
} }
@@ -421,23 +510,28 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.card(totap); return PaymentDecision.card(totap);
} }
@Override @Override
public PaymentDecision visit(CostSacrifice cost) { public PaymentDecision visit(CostSacrifice cost) {
if (cost.payCostFromSource()) { if (cost.payCostFromSource()) {
return PaymentDecision.card(source); return PaymentDecision.card(source);
} }
if (cost.getType().equals("OriginalHost")) { if (cost.getType().equals("OriginalHost")) {
return PaymentDecision.card(ability.getOriginalHost()); return PaymentDecision.card(ability.getHostCard());
} }
if (cost.getAmount().equals("All")) { if (cost.getAmount().equals("All")) {
// Does the AI want to use Sacrifice All? // Does the AI want to use Sacrifice All?
return null; return null;
} }
int c = cost.getAbilityAmount(ability); final String amount = cost.getAmount();
Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, isEffect(), c, null); CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);
return PaymentDecision.card(list); return PaymentDecision.card(list);
} }
@@ -446,7 +540,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (cost.payCostFromSource()) if (cost.payCostFromSource())
return PaymentDecision.card(source); return PaymentDecision.card(source);
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
CardCollectionView res = ComputerUtil.chooseReturnType(player, cost.getType(), source, ability.getTargetCard(), c, ability); CardCollectionView res = ComputerUtil.chooseReturnType(player, cost.getType(), source, ability.getTargetCard(), c, ability);
return res.isEmpty() ? null : PaymentDecision.card(res); return res.isEmpty() ? null : PaymentDecision.card(res);
@@ -472,22 +569,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null; return null;
} }
if (cost.getRevealFrom().get(0).equals(ZoneType.Exile)) { if (cost.getRevealFrom().equals(ZoneType.Exile)) {
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability); hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
return PaymentDecision.card(getBestCreatureAI(hand)); return PaymentDecision.card(getBestCreatureAI(hand));
} }
int c = cost.getAbilityAmount(ability); Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
return PaymentDecision.card(aic.getCardsToDiscard(c, type.split(";"), ability)); return PaymentDecision.card(aic.getCardsToDiscard(c, type.split(";"), ability));
} }
@Override
public PaymentDecision visit(CostRevealChosenPlayer cost) {
return PaymentDecision.number(1);
}
protected int removeCounter(GameEntityCounterTable table, List<Card> prefs, CounterEnumType cType, int stillToRemove) { protected int removeCounter(GameEntityCounterTable table, List<Card> prefs, CounterEnumType cType, int stillToRemove) {
int removed = 0; int removed = 0;
if (!prefs.isEmpty() && stillToRemove > 0) { if (!prefs.isEmpty() && stillToRemove > 0) {
@@ -501,7 +596,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove); int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) { if (thisRemove > 0) {
removed += thisRemove; removed += thisRemove;
table.put(null, prefCard, CounterType.get(cType), thisRemove); table.put(prefCard, CounterType.get(cType), thisRemove);
} }
} }
} }
@@ -510,8 +605,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostRemoveAnyCounter cost) { public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability); final String amount = cost.getAmount();
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source); final int c = AbilityUtils.calculateAmount(source, amount, ability);
final Card originalHost = ability.getOriginalOrHost();
if (c <= 0) { if (c <= 0) {
return null; return null;
@@ -532,6 +628,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
// currently if amount is bigger than one, // currently if amount is bigger than one,
// it tries to remove all counters from one source and type at once // it tries to remove all counters from one source and type at once
int toRemove = 0; int toRemove = 0;
final GameEntityCounterTable table = new GameEntityCounterTable(); final GameEntityCounterTable table = new GameEntityCounterTable();
@@ -565,7 +662,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(card.getCounters(ctype), c - toRemove); int thisRemove = Math.min(card.getCounters(ctype), c - toRemove);
if (thisRemove > 0) { if (thisRemove > 0) {
toRemove += thisRemove; toRemove += thisRemove;
table.put(null, card, ctype, thisRemove); table.put(card, ctype, thisRemove);
} }
} }
} }
@@ -593,7 +690,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(e.getValue(), c - toRemove); int over = Math.min(e.getValue(), c - toRemove);
if (over > 0) { if (over > 0) {
toRemove += over; toRemove += over;
table.put(null, crd, e.getKey(), over); table.put(crd, e.getKey(), over);
} }
} }
} }
@@ -623,7 +720,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(e.getValue(), c - toRemove); int over = Math.min(e.getValue(), c - toRemove);
if (over > 0) { if (over > 0) {
toRemove += over; toRemove += over;
table.put(null, crd, e.getKey(), over); table.put(crd, e.getKey(), over);
} }
} }
} }
@@ -663,7 +760,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove); int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) { if (over > 0) {
toRemove += over; toRemove += over;
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over); table.put(crd, CounterType.get(CounterEnumType.QUEST), over);
} }
} }
} }
@@ -676,6 +773,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
toRemove += removeCounter(table, prefs, CounterEnumType.LORE, c - toRemove); toRemove += removeCounter(table, prefs, CounterEnumType.LORE, c - toRemove);
} }
// TODO add logic to remove positive counters? // TODO add logic to remove positive counters?
if (c > toRemove && cost.counter != null) { if (c > toRemove && cost.counter != null) {
// TODO add logic for Ooze Flux, should probably try to make a token as big as possible // TODO add logic for Ooze Flux, should probably try to make a token as big as possible
@@ -686,7 +784,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(card.getCounters(cost.counter), c - toRemove); int thisRemove = Math.min(card.getCounters(cost.counter), c - toRemove);
if (thisRemove > 0) { if (thisRemove > 0) {
toRemove += thisRemove; toRemove += thisRemove;
table.put(null, card, cost.counter, thisRemove); table.put(card, cost.counter, thisRemove);
} }
} }
} }
@@ -700,7 +798,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(e.getValue(), c - toRemove); int thisRemove = Math.min(e.getValue(), c - toRemove);
if (thisRemove > 0) { if (thisRemove > 0) {
toRemove += thisRemove; toRemove += thisRemove;
table.put(null, card, e.getKey(), thisRemove); table.put(card, e.getKey(), thisRemove);
} }
} }
} }
@@ -713,10 +811,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostRemoveCounter cost) { public PaymentDecision visit(CostRemoveCounter cost) {
final String amount = cost.getAmount(); final String amount = cost.getAmount();
Integer c = cost.convertAmount();
final String type = cost.getType(); final String type = cost.getType();
int c; if (c == null) {
final String sVar = ability.getSVar(amount); final String sVar = ability.getSVar(amount);
if (amount.equals("All")) { if (amount.equals("All")) {
c = source.getCounters(cost.counter); c = source.getCounters(cost.counter);
@@ -730,7 +828,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
} }
} }
} else { } else {
c = cost.getAbilityAmount(ability); c = AbilityUtils.calculateAmount(source, amount, ability);
}
} }
if (!cost.payCostFromSource()) { if (!cost.payCostFromSource()) {
@@ -758,7 +857,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override @Override
public PaymentDecision visit(CostUntapType cost) { public PaymentDecision visit(CostUntapType cost) {
int c = cost.getAbilityAmount(ability); final String amount = cost.getAmount();
Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
CardCollectionView list = ComputerUtil.chooseUntapType(player, cost.getType(), source, cost.canUntapSource, c, ability); CardCollectionView list = ComputerUtil.chooseUntapType(player, cost.getType(), source, cost.canUntapSource, c, ability);
@@ -790,3 +893,4 @@ public class AiCostDecision extends CostDecisionMakerBase {
return false; return false;
} }
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ public class ComputerUtilAbility {
public boolean apply(final Card c) { public boolean apply(final Card c) {
if (!c.getSVar("NeedsToPlay").isEmpty()) { if (!c.getSVar("NeedsToPlay").isEmpty()) {
final String needsToPlay = c.getSVar("NeedsToPlay"); 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()) { if (list.isEmpty()) {
return false; return false;
} }
@@ -72,8 +72,10 @@ public class ComputerUtilAbility {
if (!player.getCardsIn(ZoneType.Library).isEmpty()) { if (!player.getCardsIn(ZoneType.Library).isEmpty()) {
all.add(player.getCardsIn(ZoneType.Library).get(0)); all.add(player.getCardsIn(ZoneType.Library).get(0));
} }
all.addAll(game.getPlayers().getCardsIn(ZoneType.Exile)); for(Player p : game.getPlayers()) {
all.addAll(game.getPlayers().getCardsIn(ZoneType.Battlefield)); all.addAll(p.getCardsIn(ZoneType.Exile));
all.addAll(p.getCardsIn(ZoneType.Battlefield));
}
return all; return all;
} }
@@ -169,12 +171,8 @@ public class ComputerUtilAbility {
return sa; return sa;
} }
public static Card getAbilitySource(SpellAbility sa) {
return sa.getOriginalHost() != null ? sa.getOriginalHost() : sa.getHostCard();
}
public static String getAbilitySourceName(SpellAbility sa) { public static String getAbilitySourceName(SpellAbility sa) {
final Card c = getAbilitySource(sa); final Card c = sa.getOriginalOrHost();
return c != null ? c.getName() : ""; return c != null ? c.getName() : "";
} }
@@ -212,15 +210,4 @@ public class ComputerUtilAbility {
return targeted; 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;
}
} }

View File

@@ -1,15 +1,16 @@
package forge.ai; package forge.ai;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import forge.card.mana.ManaCost;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair; import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -39,12 +40,13 @@ import forge.game.card.CardFactory;
import forge.game.card.CardFactoryUtil; import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostPayEnergy; import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection; import forge.game.keyword.KeywordCollection;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
@@ -62,7 +64,6 @@ import forge.item.PaperCard;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.Expressions; import forge.util.Expressions;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil;
public class ComputerUtilCard { public class ComputerUtilCard {
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) { public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
@@ -75,7 +76,7 @@ public class ComputerUtilCard {
} }
}); });
} }
return getMostExpensivePermanentAI(all); return ComputerUtilCard.getMostExpensivePermanentAI(all);
} }
/** /**
@@ -88,7 +89,7 @@ public class ComputerUtilCard {
*/ */
public static void sortByEvaluateCreature(final CardCollection list) { public static void sortByEvaluateCreature(final CardCollection list) {
Collections.sort(list, ComputerUtilCard.EvaluateCreatureComparator); Collections.sort(list, ComputerUtilCard.EvaluateCreatureComparator);
} } // sortByEvaluateCreature()
// The AI doesn't really pick the best artifact, just the most expensive. // The AI doesn't really pick the best artifact, just the most expensive.
/** /**
@@ -136,56 +137,6 @@ public class ComputerUtilCard {
return Aggregates.itemWithMin(all, CardPredicates.Accessors.fnGetCmc); return Aggregates.itemWithMin(all, CardPredicates.Accessors.fnGetCmc);
} }
public static Card getBestPlaneswalkerToDamage(final List<Card> pws) {
Card bestTgt = null;
// As of right now, ranks planeswalkers by their Current Loyalty * 10 + Big buff if close to "Ultimate"
int bestScore = 0;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
int pwScore = curLoyalty * 10;
for (SpellAbility sa : pw.getSpellAbilities()) {
if (sa.hasParam("Ultimate")) {
Integer loyaltyCost = 0;
CostRemoveCounter remLoyalty = sa.getPayCosts().getCostPartByType(CostRemoveCounter.class);
if (remLoyalty != null) {
// if remLoyalty is null, generally there's an AddCounter<0/LOYALTY> cost, like for Gideon Jura.
loyaltyCost = remLoyalty.convertAmount();
}
if (loyaltyCost != null && loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
// Will ultimate soon
pwScore += 10000;
}
if (pwScore > bestScore) {
bestScore = pwScore;
bestTgt = pw;
}
}
}
}
return bestTgt;
}
public static Card getWorstPlaneswalkerToDamage(final List<Card> pws) {
Card bestTgt = null;
int bestScore = Integer.MAX_VALUE;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
if (curLoyalty < bestScore) {
bestScore = curLoyalty;
bestTgt = pw;
}
}
return bestTgt;
}
// The AI doesn't really pick the best enchantment, just the most expensive. // The AI doesn't really pick the best enchantment, just the most expensive.
/** /**
* <p> * <p>
@@ -203,6 +154,7 @@ public class ComputerUtilCard {
List<Card> all = CardLists.filter(list, CardPredicates.Presets.ENCHANTMENTS); List<Card> all = CardLists.filter(list, CardPredicates.Presets.ENCHANTMENTS);
if (targeted) { if (targeted) {
all = CardLists.filter(all, new Predicate<Card>() { all = CardLists.filter(all, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
return c.canBeTargetedBy(spell); return c.canBeTargetedBy(spell);
@@ -249,7 +201,11 @@ public class ComputerUtilCard {
} }
if (iminBL == Integer.MAX_VALUE) { if (iminBL == Integer.MAX_VALUE) {
// All basic lands have no basic land type. Just return something // All basic lands have no basic land type. Just return something
return Iterables.find(land, CardPredicates.Presets.UNTAPPED, land.get(0)); Iterator<Card> untapped = Iterables.filter(land, CardPredicates.Presets.UNTAPPED).iterator();
if (untapped.hasNext()) {
return untapped.next();
}
return land.get(0);
} }
final List<Card> bLand = CardLists.getType(land, sminBL); final List<Card> bLand = CardLists.getType(land, sminBL);
@@ -258,67 +214,9 @@ public class ComputerUtilCard {
return ut; return ut;
} }
// TODO potentially risky if simulation mode currently able to reach this from triggers
return Aggregates.random(bLand); // random tapped land of least represented type return Aggregates.random(bLand); // random tapped land of least represented type
} }
/**
* <p>
* getWorstLand.
* </p>
*
* @param lands
* @return a {@link forge.game.card.Card} object.
*/
public static Card getWorstLand(final List<Card> lands) {
Card worstLand = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
int score = tmp.isTapped() ? 2 : 0;
score += tmp.isBasicLand() ? 1 : 0;
score -= tmp.isCreature() ? 4 : 0;
for (Card aura : tmp.getEnchantedBy()) {
if (aura.getController().isOpponentOf(tmp.getController())) {
score += 5;
} else {
score -= 5;
}
}
if (score == maxScore &&
CardLists.count(lands, CardPredicates.sharesNameWith(tmp)) > CardLists.count(lands, CardPredicates.sharesNameWith(worstLand))) {
worstLand = tmp;
}
if (score > maxScore) {
worstLand = tmp;
maxScore = score;
}
}
return worstLand;
}
public static Card getBestLandToAnimate(final Iterable<Card> lands) {
Card land = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
int score = tmp.isTapped() ? 0 : 2;
score += tmp.isBasicLand() ? 2 : 0;
score -= tmp.isCreature() ? 4 : 0;
score -= 5 * tmp.getEnchantedBy().size();
if (score == maxScore &&
CardLists.count(lands, CardPredicates.sharesNameWith(tmp)) > CardLists.count(lands, CardPredicates.sharesNameWith(land))) {
land = tmp;
}
if (score > maxScore) {
land = tmp;
maxScore = score;
}
}
return land;
}
/** /**
* <p> * <p>
* getCheapestPermanentAI. * getCheapestPermanentAI.
@@ -354,6 +252,7 @@ public class ComputerUtilCard {
} }
return cheapest; return cheapest;
} }
// returns null if list.size() == 0 // returns null if list.size() == 0
@@ -366,15 +265,17 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object. * @return a {@link forge.game.card.Card} object.
*/ */
public static Card getBestAI(final Iterable<Card> list) { public static Card getBestAI(final Iterable<Card> list) {
// Get Best will filter by appropriate getBest list if ALL of the list is of that type // Get Best will filter by appropriate getBest list if ALL of the list
// is of that type
if (Iterables.all(list, CardPredicates.Presets.CREATURES)) { if (Iterables.all(list, CardPredicates.Presets.CREATURES)) {
return getBestCreatureAI(list); return ComputerUtilCard.getBestCreatureAI(list);
} }
if (Iterables.all(list, CardPredicates.Presets.LANDS)) { if (Iterables.all(list, CardPredicates.Presets.LANDS)) {
return getBestLandAI(list); return getBestLandAI(list);
} }
// TODO - Once we get an EvaluatePermanent this should call getBestPermanent() // TODO - Once we get an EvaluatePermanent this should call
return getMostExpensivePermanentAI(list); // getBestPermanent()
return ComputerUtilCard.getMostExpensivePermanentAI(list);
} }
/** /**
@@ -415,7 +316,7 @@ public class ComputerUtilCard {
int biggestvalue = -1; int biggestvalue = -1;
for (Card card : CardLists.filter(list, CardPredicates.Presets.CREATURES)) { for (Card card : CardLists.filter(list, CardPredicates.Presets.CREATURES)) {
int newvalue = evaluateCreature(card); int newvalue = ComputerUtilCard.evaluateCreature(card);
newvalue += card.isToken() ? tokenBonus : 0; // raise the value of tokens newvalue += card.isToken() ? tokenBonus : 0; // raise the value of tokens
if (biggestvalue < newvalue) { if (biggestvalue < newvalue) {
@@ -446,7 +347,7 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object. * @return a {@link forge.game.card.Card} object.
*/ */
public static Card getWorstAI(final Iterable<Card> list) { public static Card getWorstAI(final Iterable<Card> list) {
return getWorstPermanentAI(list, false, false, false, false); return ComputerUtilCard.getWorstPermanentAI(list, false, false, false, false);
} }
/** /**
@@ -482,7 +383,7 @@ public class ComputerUtilCard {
} }
if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) { if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) {
return getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS)); return ComputerUtilCard.getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
} }
final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES); final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES);
@@ -492,7 +393,7 @@ public class ComputerUtilCard {
List<Card> lands = CardLists.filter(list, CardPredicates.Presets.LANDS); List<Card> lands = CardLists.filter(list, CardPredicates.Presets.LANDS);
if (lands.size() > 6) { if (lands.size() > 6) {
return getWorstLand(lands); return ComputerUtilCard.getWorstLand(lands);
} }
if (hasEnchantmants || hasArtifacts) { if (hasEnchantmants || hasArtifacts) {
@@ -509,7 +410,8 @@ public class ComputerUtilCard {
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES)); return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
} }
// Planeswalkers fall through to here, lands will fall through if there aren't very many // Planeswalkers fall through to here, lands will fall through if there
// aren't very many
return getCheapestPermanentAI(list, null, false); return getCheapestPermanentAI(list, null, false);
} }
@@ -542,7 +444,7 @@ public class ComputerUtilCard {
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() { public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
@Override @Override
public int compare(final Card a, final Card b) { public int compare(final Card a, final Card b) {
return evaluateCreature(b) - evaluateCreature(a); return ComputerUtilCard.evaluateCreature(b) - ComputerUtilCard.evaluateCreature(a);
} }
}; };
@@ -611,6 +513,35 @@ public class ComputerUtilCard {
return combat.isAttacking(card); return combat.isAttacking(card);
} }
public static boolean canBeKilledByRoyalAssassin(final Player ai, final Card card) {
boolean wasTapped = card.isTapped();
for (Player opp : ai.getOpponents()) {
for (Card c : opp.getCardsIn(ZoneType.Battlefield)) {
for (SpellAbility sa : c.getSpellAbilities()) {
if (sa.getApi() != ApiType.Destroy) {
continue;
}
if (!ComputerUtilCost.canPayCost(sa, opp)) {
continue;
}
sa.setActivatingPlayer(opp);
if (sa.canTarget(card)) {
continue;
}
// check whether the ability can only target tapped creatures
card.setTapped(true);
if (!sa.canTarget(card)) {
card.setTapped(wasTapped);
continue;
}
card.setTapped(wasTapped);
return true;
}
}
}
return false;
}
/** /**
* Create a mock combat where ai is being attacked and returns the list of likely blockers. * Create a mock combat where ai is being attacked and returns the list of likely blockers.
* @param ai blocking player * @param ai blocking player
@@ -618,8 +549,8 @@ public class ComputerUtilCard {
* @return list of creatures assigned to block in the simulation * @return list of creatures assigned to block in the simulation
*/ */
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) { public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
AiBlockController aiBlk = new AiBlockController(ai, false); AiBlockController aiBlk = new AiBlockController(ai);
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Player opp = ai.getWeakestOpponent();
Combat combat = new Combat(opp); Combat combat = new Combat(opp);
//Use actual attackers if available, else consider all possible attackers //Use actual attackers if available, else consider all possible attackers
Combat currentCombat = ai.getGame().getCombat(); Combat currentCombat = ai.getGame().getCombat();
@@ -658,44 +589,16 @@ public class ComputerUtilCard {
* @param attacker attacking creature to evaluate * @param attacker attacking creature to evaluate
* @return attacker will die * @return attacker will die
*/ */
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) { public static boolean canBeBlockedProfitably(final Player ai, Card attacker) {
AiBlockController aiBlk = new AiBlockController(ai, checkingOther); AiBlockController aiBlk = new AiBlockController(ai);
Combat combat = new Combat(ai); Combat combat = new Combat(ai);
combat.addAttacker(attacker, ai); combat.addAttacker(attacker, ai);
final List<Card> attackers = Lists.newArrayList(attacker); final List<Card> attackers = new ArrayList<>();
attackers.add(attacker);
aiBlk.assignBlockersGivenAttackers(combat, attackers); aiBlk.assignBlockersGivenAttackers(combat, attackers);
return ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, combat); return ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, combat);
} }
public static boolean canBeKilledByRoyalAssassin(final Player ai, final Card card) {
boolean wasTapped = card.isTapped();
for (Player opp : ai.getOpponents()) {
for (Card c : opp.getCardsIn(ZoneType.Battlefield)) {
for (SpellAbility sa : c.getSpellAbilities()) {
if (sa.getApi() != ApiType.Destroy) {
continue;
}
if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) {
continue;
}
sa.setActivatingPlayer(opp);
if (sa.canTarget(card)) {
continue;
}
// check whether the ability can only target tapped creatures
card.setTapped(true);
if (!sa.canTarget(card)) {
card.setTapped(wasTapped);
continue;
}
card.setTapped(wasTapped);
return true;
}
}
}
return false;
}
/** /**
* getMostExpensivePermanentAI. * getMostExpensivePermanentAI.
* *
@@ -708,7 +611,6 @@ public class ComputerUtilCard {
int bigCMC = -1; int bigCMC = -1;
for (final Card card : all) { for (final Card card : all) {
// TODO when PlayAi can consider MDFC this should also look at the back face (if not on stack or battlefield)
int curCMC = card.getCMC(); int curCMC = card.getCMC();
// Add all cost of all auras with the same controller // Add all cost of all auras with the same controller
@@ -737,13 +639,14 @@ public class ComputerUtilCard {
final String name = c.getName(); final String name = c.getName();
Integer currentCnt = map.get(name); Integer currentCnt = map.get(name);
map.put(name, currentCnt == null ? Integer.valueOf(1) : Integer.valueOf(1 + currentCnt)); map.put(name, currentCnt == null ? Integer.valueOf(1) : Integer.valueOf(1 + currentCnt));
} } // for
int max = 0; int max = 0;
String maxName = ""; String maxName = "";
for (final Entry<String, Integer> entry : map.entrySet()) { for (final Entry<String, Integer> entry : map.entrySet()) {
final String type = entry.getKey(); final String type = entry.getKey();
// Log.debug(type + " - " + entry.getValue());
if (max < entry.getValue()) { if (max < entry.getValue()) {
max = entry.getValue(); max = entry.getValue();
@@ -768,6 +671,7 @@ public class ComputerUtilCard {
public static String getMostProminentCreatureType(final CardCollectionView list) { public static String getMostProminentCreatureType(final CardCollectionView list) {
return getMostProminentType(list, CardType.getAllCreatureTypes()); return getMostProminentType(list, CardType.getAllCreatureTypes());
} }
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid) { public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid) {
if (list.size() == 0) { if (list.size() == 0) {
return ""; return "";
@@ -777,6 +681,7 @@ public class ComputerUtilCard {
// TODO JAVA 8 use getOrDefault // TODO JAVA 8 use getOrDefault
for (final Card c : list) { for (final Card c : list) {
// Changeling are all creature types, they are not interesting for // Changeling are all creature types, they are not interesting for
// counting creature types // counting creature types
if (c.hasStartOfKeyword(Keyword.CHANGELING.toString())) { if (c.hasStartOfKeyword(Keyword.CHANGELING.toString())) {
@@ -854,6 +759,7 @@ public class ComputerUtilCard {
for (final Entry<String, Integer> entry : typesInDeck.entrySet()) { for (final Entry<String, Integer> entry : typesInDeck.entrySet()) {
final String type = entry.getKey(); final String type = entry.getKey();
// Log.debug(type + " - " + entry.getValue());
if (max < entry.getValue()) { if (max < entry.getValue()) {
max = entry.getValue(); max = entry.getValue();
@@ -899,13 +805,13 @@ public class ComputerUtilCard {
} }
for (final Card crd : list) { for (final Card crd : list) {
ColorSet color = crd.getColor(); ColorSet color = CardUtil.getColors(crd);
if (color.hasWhite()) map.get(0).setValue(Integer.valueOf(map.get(0).getValue()+1)); if (color.hasWhite()) map.get(0).setValue(Integer.valueOf(map.get(0).getValue()+1));
if (color.hasBlue()) map.get(1).setValue(Integer.valueOf(map.get(1).getValue()+1)); if (color.hasBlue()) map.get(1).setValue(Integer.valueOf(map.get(1).getValue()+1));
if (color.hasBlack()) map.get(2).setValue(Integer.valueOf(map.get(2).getValue()+1)); if (color.hasBlack()) map.get(2).setValue(Integer.valueOf(map.get(2).getValue()+1));
if (color.hasRed()) map.get(3).setValue(Integer.valueOf(map.get(3).getValue()+1)); if (color.hasRed()) map.get(3).setValue(Integer.valueOf(map.get(3).getValue()+1));
if (color.hasGreen()) map.get(4).setValue(Integer.valueOf(map.get(4).getValue()+1)); if (color.hasGreen()) map.get(4).setValue(Integer.valueOf(map.get(4).getValue()+1));
} } // for
Collections.sort(map, new Comparator<Pair<Byte,Integer>>() { Collections.sort(map, new Comparator<Pair<Byte,Integer>>() {
@Override public int compare(Pair<Byte, Integer> o1, Pair<Byte, Integer> o2) { @Override public int compare(Pair<Byte, Integer> o1, Pair<Byte, Integer> o2) {
@@ -922,6 +828,57 @@ public class ComputerUtilCard {
return result; return result;
} }
/**
* <p>
* getWorstLand.
* </p>
*
* @param lands
* @return a {@link forge.game.card.Card} object.
*/
public static Card getWorstLand(final List<Card> lands) {
Card worstLand = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
int score = tmp.isTapped() ? 2 : 0;
score += tmp.isBasicLand() ? 1 : 0;
score -= tmp.isCreature() ? 4 : 0;
for (Card aura : tmp.getEnchantedBy()) {
if (aura.getController().isOpponentOf(tmp.getController())) {
score += 5;
} else {
score -= 5;
}
}
if (score >= maxScore) {
worstLand = tmp;
maxScore = score;
}
}
return worstLand;
} // end getWorstLand
public static Card getBestLandToAnimate(final Iterable<Card> lands) {
Card land = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
// TODO Improve this by choosing basic lands that I have plenty of mana in
int score = tmp.isTapped() ? 0 : 2;
score += tmp.isBasicLand() ? 2 : 0;
score -= tmp.isCreature() ? 4 : 0;
score -= 5 * tmp.getEnchantedBy().size();
if (score >= maxScore) {
land = tmp;
maxScore = score;
}
}
return land;
} // end getBestLandToAnimate
public static final Predicate<Deck> AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate<Deck>() { public static final Predicate<Deck> AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate<Deck>() {
@Override @Override
public boolean apply(Deck d) { public boolean apply(Deck d) {
@@ -934,53 +891,52 @@ public class ComputerUtilCard {
return true; return true;
} }
}; };
public static List<String> chooseColor(SpellAbility sa, int min, int max, List<String> colorChoices) { public static List<String> chooseColor(SpellAbility sa, int min, int max, List<String> colorChoices) {
List<String> chosen = new ArrayList<>(); List<String> chosen = new ArrayList<>();
Player ai = sa.getActivatingPlayer(); Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame(); final Game game = ai.getGame();
Player opp = ai.getStrongestOpponent(); Player opp = ai.getWeakestOpponent();
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic"); final String logic = sa.getParam("AILogic");
if (logic.equals("MostProminentInHumanDeck")) { if (logic.equals("MostProminentInHumanDeck")) {
chosen.add(getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices));
} }
else if (logic.equals("MostProminentInComputerDeck")) { else if (logic.equals("MostProminentInComputerDeck")) {
chosen.add(getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), ai), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), ai), colorChoices));
} }
else if (logic.equals("MostProminentDualInComputerDeck")) { else if (logic.equals("MostProminentDualInComputerDeck")) {
List<String> prominence = getColorByProminence(CardLists.filterControlledBy(game.getCardsInGame(), ai)); List<String> prominence = ComputerUtilCard.getColorByProminence(CardLists.filterControlledBy(game.getCardsInGame(), ai));
chosen.add(prominence.get(0)); chosen.add(prominence.get(0));
chosen.add(prominence.get(1)); chosen.add(prominence.get(1));
} }
else if (logic.equals("MostProminentInGame")) { else if (logic.equals("MostProminentInGame")) {
chosen.add(getMostProminentColor(game.getCardsInGame(), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsInGame(), colorChoices));
} }
else if (logic.equals("MostProminentHumanCreatures")) { else if (logic.equals("MostProminentHumanCreatures")) {
CardCollectionView list = opp.getCreaturesInPlay(); CardCollectionView list = opp.getCreaturesInPlay();
if (list.isEmpty()) { if (list.isEmpty()) {
list = CardLists.filter(CardLists.filterControlledBy(game.getCardsInGame(), opp), CardPredicates.Presets.CREATURES); list = CardLists.filter(CardLists.filterControlledBy(game.getCardsInGame(), opp), CardPredicates.Presets.CREATURES);
} }
chosen.add(getMostProminentColor(list, colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(list, colorChoices));
} }
else if (logic.equals("MostProminentComputerControls")) { else if (logic.equals("MostProminentComputerControls")) {
chosen.add(getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices));
} }
else if (logic.equals("MostProminentHumanControls")) { else if (logic.equals("MostProminentHumanControls")) {
chosen.add(getMostProminentColor(opp.getCardsIn(ZoneType.Battlefield), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(opp.getCardsIn(ZoneType.Battlefield), colorChoices));
} }
else if (logic.equals("MostProminentPermanent")) { else if (logic.equals("MostProminentPermanent")) {
chosen.add(getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices));
} }
else if (logic.equals("MostProminentAttackers") && game.getPhaseHandler().inCombat()) { else if (logic.equals("MostProminentAttackers") && game.getPhaseHandler().inCombat()) {
chosen.add(getMostProminentColor(game.getCombat().getAttackers(), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(game.getCombat().getAttackers(), colorChoices));
} }
else if (logic.equals("MostProminentInActivePlayerHand")) { else if (logic.equals("MostProminentInActivePlayerHand")) {
chosen.add(getMostProminentColor(game.getPhaseHandler().getPlayerTurn().getCardsIn(ZoneType.Hand), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(game.getPhaseHandler().getPlayerTurn().getCardsIn(ZoneType.Hand), colorChoices));
} }
else if (logic.equals("MostProminentInComputerDeckButGreen")) { else if (logic.equals("MostProminentInComputerDeckButGreen")) {
List<String> prominence = getColorByProminence(CardLists.filterControlledBy(game.getCardsInGame(), ai)); List<String> prominence = ComputerUtilCard.getColorByProminence(CardLists.filterControlledBy(game.getCardsInGame(), ai));
if (prominence.get(0).equals(MagicColor.Constant.GREEN)) { if (prominence.get(0).equals(MagicColor.Constant.GREEN)) {
chosen.add(prominence.get(1)); chosen.add(prominence.get(1));
} else { } else {
@@ -991,8 +947,11 @@ public class ComputerUtilCard {
int maxExcess = 0; int maxExcess = 0;
String bestColor = Constant.GREEN; String bestColor = Constant.GREEN;
for (byte color : MagicColor.WUBRG) { for (byte color : MagicColor.WUBRG) {
CardCollectionView ailist = ai.getColoredCardsInPlay(color); CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView opplist = opp.getColoredCardsInPlay(color); CardCollectionView opplist = opp.getCardsIn(ZoneType.Battlefield);
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
int excess = evaluatePermanentList(opplist) - evaluatePermanentList(ailist); int excess = evaluatePermanentList(opplist) - evaluatePermanentList(ailist);
if (excess > maxExcess) { if (excess > maxExcess) {
@@ -1024,7 +983,7 @@ public class ComputerUtilCard {
String devotionCode = "Count$Devotion." + MagicColor.toLongString(c); String devotionCode = "Count$Devotion." + MagicColor.toLongString(c);
int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa); int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa);
if (devotion > curDevotion && Iterables.any(hand, CardPredicates.isColor(c))) { if (devotion > curDevotion && !CardLists.filter(hand, CardPredicates.isColor(c)).isEmpty()) {
curDevotion = devotion; curDevotion = devotion;
chosenColor = MagicColor.toLongString(c); chosenColor = MagicColor.toLongString(c);
} }
@@ -1042,10 +1001,10 @@ public class ComputerUtilCard {
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) { public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
final Player opp = ai.getWeakestOpponent();
final Game game = ai.getGame(); final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler(); final PhaseHandler ph = game.getPhaseHandler();
final PhaseType phaseType = ph.getPhase(); final PhaseType phaseType = ph.getPhase();
final Player opp = ph.getPlayerTurn().isOpponentOf(ai) ? ph.getPlayerTurn() : ai.getStrongestOpponent();
final int costRemoval = sa.getHostCard().getCMC(); final int costRemoval = sa.getHostCard().getCMC();
final int costTarget = c.getCMC(); final int costTarget = c.getCMC();
@@ -1082,7 +1041,7 @@ public class ComputerUtilCard {
for (Card attacker : currCombat.getAttackersBlockedBy(c)) { for (Card attacker : currCombat.getAttackersBlockedBy(c)) {
if (attacker.getShieldCount() == 0 && ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, currCombat)) { if (attacker.getShieldCount() == 0 && ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, currCombat)) {
CardCollection blockers = currCombat.getBlockers(attacker); CardCollection blockers = currCombat.getBlockers(attacker);
sortByEvaluateCreature(blockers); ComputerUtilCard.sortByEvaluateCreature(blockers);
Combat combat = new Combat(ai); Combat combat = new Combat(ai);
combat.addAttacker(attacker, opp); combat.addAttacker(attacker, opp);
for (Card blocker : blockers) { for (Card blocker : blockers) {
@@ -1143,7 +1102,7 @@ public class ComputerUtilCard {
if (c.isEquipped()) { if (c.isEquipped()) {
valueTempo *= 2; valueTempo *= 2;
} }
if (SpellAbilityAi.isSorcerySpeed(sa, ai)) { if (SpellAbilityAi.isSorcerySpeed(sa)) {
valueTempo *= 2; //sorceries have less usage opportunities valueTempo *= 2; //sorceries have less usage opportunities
} }
if (!c.canBeDestroyed()) { if (!c.canBeDestroyed()) {
@@ -1173,10 +1132,10 @@ public class ComputerUtilCard {
float threat = 0; float threat = 0;
if (c.isCreature()) { if (c.isCreature()) {
// the base value for evaluate creature is 100 // the base value for evaluate creature is 100
threat += (-1 + 1.0f * evaluateCreature(c) / 100) / costRemoval; threat += (-1 + 1.0f * ComputerUtilCard.evaluateCreature(c) / 100) / costRemoval;
if (ai.getLife() > 0 && ComputerUtilCombat.canAttackNextTurn(c)) { if (ai.getLife() > 0 && ComputerUtilCombat.canAttackNextTurn(c)) {
Combat combat = game.getCombat(); Combat combat = game.getCombat();
threat += 1.0f * ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) / ai.getLife(); threat += 1.0f * ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true) / ai.getLife();
//TODO:add threat from triggers and other abilities (ie. Master of Cruelties) //TODO:add threat from triggers and other abilities (ie. Master of Cruelties)
} }
if (ph.isPlayerTurn(ai) && phaseType.isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) { if (ph.isPlayerTurn(ai) && phaseType.isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
@@ -1274,6 +1233,7 @@ public class ComputerUtilCard {
final int power, final List<String> keywords) { final int power, final List<String> keywords) {
return shouldPumpCard(ai, sa, c, toughness, power, keywords, false); return shouldPumpCard(ai, sa, c, toughness, power, keywords, false);
} }
public static boolean shouldPumpCard(final Player ai, final SpellAbility sa, final Card c, final int toughness, public static boolean shouldPumpCard(final Player ai, final SpellAbility sa, final Card c, final int toughness,
final int power, final List<String> keywords, boolean immediately) { final int power, final List<String> keywords, boolean immediately) {
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -1287,16 +1247,12 @@ public class ComputerUtilCard {
boolean combatTrick = false; boolean combatTrick = false;
boolean holdCombatTricks = false; boolean holdCombatTricks = false;
int chanceToHoldCombatTricks = -1; int chanceToHoldCombatTricks = -1;
boolean simAI = false;
if (ai.getController().isAI()) { if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
simAI = aic.usesSimulation();
if (!simAI) {
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK); holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK); chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
} }
}
if (!c.canBeTargetedBy(sa)) { if (!c.canBeTargetedBy(sa)) {
return false; return false;
@@ -1321,16 +1277,16 @@ public class ComputerUtilCard {
// will the creature attack (only relevant for sorcery speed)? // will the creature attack (only relevant for sorcery speed)?
if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& phase.isPlayerTurn(ai) && phase.isPlayerTurn(ai)
&& (SpellAbilityAi.isSorcerySpeed(sa, ai) || main1Preferred) && SpellAbilityAi.isSorcerySpeed(sa) || main1Preferred
&& power > 0 && power > 0
&& doesCreatureAttackAI(ai, c)) { && ComputerUtilCard.doesCreatureAttackAI(ai, c)) {
return true; return true;
} }
// buff attacker/blocker using triggered pump (unless it's lethal and we don't want to be reckless) // buff attacker/blocker using triggered pump (unless it's lethal and we don't want to be reckless)
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && !loseCardAtEOT) { if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !loseCardAtEOT) {
if (phase.isPlayerTurn(ai)) { if (phase.isPlayerTurn(ai)) {
if (CombatUtil.canAttack(c) || (phase.inCombat() && c.isAttacking())) { if (CombatUtil.canAttack(c)) {
return true; return true;
} }
} else { } else {
@@ -1348,9 +1304,9 @@ public class ComputerUtilCard {
//create and buff attackers //create and buff attackers
if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai) && opp.getLife() > 0) { if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai) && opp.getLife() > 0) {
//1. become attacker for whatever reason //1. become attacker for whatever reason
if (!doesCreatureAttackAI(ai, c) && doesSpecifiedCreatureAttackAI(ai, pumped)) { if (!ComputerUtilCard.doesCreatureAttackAI(ai, c) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
float threat = 1.0f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife(); float threat = 1.0f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
if (!Iterables.any(oppCreatures, CardPredicates.possibleBlockers(pumped))) { if (CardLists.filter(oppCreatures, CardPredicates.possibleBlockers(pumped)).isEmpty()) {
threat *= 2; threat *= 2;
} }
if (c.getNetPower() == 0 && c == sa.getHostCard() && power > 0 ) { if (c.getNetPower() == 0 && c == sa.getHostCard() && power > 0 ) {
@@ -1370,7 +1326,9 @@ public class ComputerUtilCard {
&& ComputerUtilMana.hasEnoughManaSourcesToCast(sa, ai)) { && ComputerUtilMana.hasEnoughManaSourcesToCast(sa, ai)) {
combatTrick = true; combatTrick = true;
for (String kw : keywords) { final List<String> kws = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & "))
: Lists.newArrayList();
for (String kw : kws) {
if (!kw.equals("Trample") && !kw.equals("First Strike") && !kw.equals("Double Strike")) { if (!kw.equals("Trample") && !kw.equals("First Strike") && !kw.equals("Double Strike")) {
combatTrick = false; combatTrick = false;
break; break;
@@ -1387,22 +1345,22 @@ public class ComputerUtilCard {
for (SpellAbility ab : c.getSpellAbilities()) { for (SpellAbility ab : c.getSpellAbilities()) {
Cost abCost = ab.getPayCosts(); Cost abCost = ab.getPayCosts();
if (abCost != null && abCost.hasTapCost() if (abCost != null && abCost.hasTapCost()
&& (!abCost.hasManaCost() || ComputerUtilMana.canPayManaCost(ab, ai, 0, false))) { && (!abCost.hasManaCost() || ComputerUtilMana.canPayManaCost(ab, ai, 0))) {
nonCombatChance += 0.5f; nonCombatChance += 0.5f;
break; break;
} }
} }
// combat Haste: only grant it if the creature will attack // combat Haste: only grant it if the creature will attack
if (doesSpecifiedCreatureAttackAI(ai, pumped)) { if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
combatChance += 0.5f + (0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife()); combatChance += 0.5f + (0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife());
} }
chance += nonCombatChance + combatChance; chance += nonCombatChance + combatChance;
} }
//3. grant evasive //3. grant evasive
if (Iterables.any(oppCreatures, CardPredicates.possibleBlockers(c))) { if (!CardLists.filter(oppCreatures, CardPredicates.possibleBlockers(c)).isEmpty()) {
if (!Iterables.any(oppCreatures, CardPredicates.possibleBlockers(pumped)) if (CardLists.filter(oppCreatures, CardPredicates.possibleBlockers(pumped)).isEmpty()
&& doesSpecifiedCreatureAttackAI(ai, pumped)) { && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife(); chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
} }
} }
@@ -1475,8 +1433,8 @@ public class ComputerUtilCard {
if (combat.isAttacking(c) && opp.getLife() > 0) { if (combat.isAttacking(c) && opp.getLife() > 0) {
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true); int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true);
int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true); int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true);
int poisonOrig = ComputerUtilCombat.poisonIfUnblocked(c, ai); int poisonOrig = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0;
int poisonPumped = ComputerUtilCombat.poisonIfUnblocked(pumped, ai); int poisonPumped = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
// predict Infect // predict Infect
if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) { if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) {
@@ -1491,7 +1449,7 @@ public class ComputerUtilCard {
} }
if (c.hasKeyword(Keyword.TRAMPLE) || keywords.contains("Trample")) { if (c.hasKeyword(Keyword.TRAMPLE) || keywords.contains("Trample")) {
for (Card b : combat.getBlockers(c)) { for (Card b : combat.getBlockers(c)) {
pumpedDmg -= ComputerUtilCombat.getDamageToKill(b, false); pumpedDmg -= ComputerUtilCombat.getDamageToKill(b);
} }
} else { } else {
pumpedDmg = 0; pumpedDmg = 0;
@@ -1519,7 +1477,7 @@ public class ComputerUtilCard {
if (combat.isBlocked(atk)) { if (combat.isBlocked(atk)) {
// consider Trample damage properly for a blocked creature // consider Trample damage properly for a blocked creature
for (Card blk : combat.getBlockers(atk)) { for (Card blk : combat.getBlockers(atk)) {
totalPowerUnblocked -= ComputerUtilCombat.getDamageToKill(blk, false); totalPowerUnblocked -= ComputerUtilCombat.getDamageToKill(blk);
} }
} }
} }
@@ -1622,7 +1580,7 @@ public class ComputerUtilCard {
} }
} }
return simAI || MyRandom.getRandom().nextFloat() < chance; return MyRandom.getRandom().nextFloat() < chance;
} }
/** /**
@@ -1637,14 +1595,13 @@ public class ComputerUtilCard {
*/ */
public static Card getPumpedCreature(final Player ai, final SpellAbility sa, public static Card getPumpedCreature(final Player ai, final SpellAbility sa,
final Card c, int toughness, int power, final List<String> keywords) { final Card c, int toughness, int power, final List<String> keywords) {
Card pumped = CardFactory.copyCard(c, false); Card pumped = CardFactory.copyCard(c, true);
pumped.setSickness(c.hasSickness()); pumped.setSickness(c.hasSickness());
final long timestamp = c.getGame().getNextTimestamp(); final long timestamp = c.getGame().getNextTimestamp();
final List<String> kws = Lists.newArrayList(); final List<String> kws = new ArrayList<>();
final List<String> hiddenKws = Lists.newArrayList();
for (String kw : keywords) { for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) { if (kw.startsWith("HIDDEN")) {
hiddenKws.add(kw.substring(7)); pumped.addHiddenExtrinsicKeyword(kw);
} else { } else {
kws.add(kw); kws.add(kw);
} }
@@ -1670,17 +1627,14 @@ public class ComputerUtilCard {
} }
} }
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp, 0); pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.setPTBoost(c.getPTBoostTable()); pumped.setPTBoost(c.getPTBoostTable());
pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0); pumped.addPTBoost(power + berserkPower, toughness, timestamp, null);
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
if (!kws.isEmpty()) { Set<CounterType> types = c.getCounters().keySet();
pumped.addChangedCardKeywords(kws, null, false, timestamp, 0); for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true, null);
} }
if (!hiddenKws.isEmpty()) {
pumped.addHiddenExtrinsicKeywords(timestamp, 0, hiddenKws);
}
pumped.setCounters(c.getCounters());
//Copies tap-state and extra keywords (auras, equipment, etc.) //Copies tap-state and extra keywords (auras, equipment, etc.)
if (c.isTapped()) { if (c.isTapped()) {
pumped.setTapped(true); pumped.setTapped(true);
@@ -1689,15 +1643,18 @@ public class ComputerUtilCard {
KeywordCollection copiedKeywords = new KeywordCollection(); KeywordCollection copiedKeywords = new KeywordCollection();
copiedKeywords.insertAll(pumped.getKeywords()); copiedKeywords.insertAll(pumped.getKeywords());
List<KeywordInterface> toCopy = Lists.newArrayList(); List<KeywordInterface> toCopy = Lists.newArrayList();
for (KeywordInterface k : c.getUnhiddenKeywords()) { for (KeywordInterface k : c.getKeywords()) {
KeywordInterface copiedKI = k.copy(c, true); if (!copiedKeywords.contains(k.getOriginal())) {
if (!copiedKeywords.contains(copiedKI.getOriginal())) { if (k.getHidden()) {
toCopy.add(copiedKI); pumped.addHiddenExtrinsicKeyword(k);
} else {
toCopy.add(k);
}
} }
} }
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used? final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
pumped.addChangedCardKeywordsInternal(toCopy, null, false, timestamp2, 0, true); pumped.addChangedCardKeywordsInternal(toCopy, null, false, false, timestamp2, true);
applyStaticContPT(ai.getGame(), pumped, new CardCollection(c)); ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
return pumped; return pumped;
} }
@@ -1721,27 +1678,37 @@ public class ComputerUtilCard {
// remove old boost that might be copied // remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) { for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getTimestamp(), stAb.getId()); vCard.removePTBoost(c.getTimestamp(), stAb.getId());
if (!stAb.getParam("Mode").equals("Continuous")) { final Map<String, String> params = stAb.getMapParams();
if (!params.get("Mode").equals("Continuous")) {
continue; continue;
} }
if (!stAb.hasParam("Affected")) { if (!params.containsKey("Affected")) {
continue; continue;
} }
if (!stAb.hasParam("AddPower") && !stAb.hasParam("AddToughness")) { if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) {
continue; continue;
} }
if (!stAb.matchesValidParam("Affected", vCard)) { final String valid = params.get("Affected");
if (!vCard.isValid(valid, c.getController(), c, null)) {
continue; continue;
} }
int att = 0; int att = 0;
if (stAb.hasParam("AddPower")) { if (params.containsKey("AddPower")) {
String addP = stAb.getParam("AddPower"); String addP = params.get("AddPower");
att = AbilityUtils.calculateAmount(addP.contains("Affected") ? vCard : c, addP, stAb, true); if (addP.equals("AffectedX")) {
att = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addP));
} else {
att = AbilityUtils.calculateAmount(c, addP, stAb);
}
} }
int def = 0; int def = 0;
if (stAb.hasParam("AddToughness")) { if (params.containsKey("AddToughness")) {
String addT = stAb.getParam("AddToughness"); String addT = params.get("AddToughness");
def = AbilityUtils.calculateAmount(addT.contains("Affected") ? vCard : c, addT, stAb, true); if (addT.equals("AffectedY")) {
def = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addT));
} else {
def = AbilityUtils.calculateAmount(c, addT, stAb);
}
} }
vCard.addPTBoost(att, def, c.getTimestamp(), stAb.getId()); vCard.addPTBoost(att, def, c.getTimestamp(), stAb.getId());
} }
@@ -1777,7 +1744,7 @@ public class ComputerUtilCard {
} }
} }
if (!threatenedTargets.isEmpty()) { if (!threatenedTargets.isEmpty()) {
sortByEvaluateCreature(threatenedTargets); ComputerUtilCard.sortByEvaluateCreature(threatenedTargets);
for (Card c : threatenedTargets) { for (Card c : threatenedTargets) {
if (sa.canAddMoreTarget()) { if (sa.canAddMoreTarget()) {
sa.getTargets().add(c); sa.getTargets().add(c);
@@ -1809,9 +1776,6 @@ public class ComputerUtilCard {
} }
public static boolean hasActiveUndyingOrPersist(final Card c) { public static boolean hasActiveUndyingOrPersist(final Card c) {
if (c.isToken()) {
return false;
}
if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterEnumType.P1P1) == 0) { if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterEnumType.P1P1) == 0) {
return true; return true;
} }
@@ -1821,6 +1785,10 @@ public class ComputerUtilCard {
return false; return false;
} }
public static boolean isPresentOnBattlefield(final Game game, final String cardName) {
return !CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(cardName)).isEmpty();
}
public static int getMaxSAEnergyCostOnBattlefield(final Player ai) { public static int getMaxSAEnergyCostOnBattlefield(final Player ai) {
// returns the maximum energy cost of an ability that permanents on the battlefield under AI's control have // returns the maximum energy cost of an ability that permanents on the battlefield under AI's control have
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield); CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);
@@ -1864,7 +1832,7 @@ public class ComputerUtilCard {
return oppCards; return oppCards;
} }
CardCollection aiCreats = ai.getCreaturesInPlay(); CardCollection aiCreats = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
if (temporary) { if (temporary) {
// Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped. // Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped.
oppCards = CardLists.filter(oppCards, CardPredicates.Presets.UNTAPPED); oppCards = CardLists.filter(oppCards, CardPredicates.Presets.UNTAPPED);
@@ -1935,7 +1903,7 @@ public class ComputerUtilCard {
// A special case which checks that this creature will attack if it's the AI's turn // A special case which checks that this creature will attack if it's the AI's turn
if (needsToPlay.equalsIgnoreCase("WillAttack")) { if (needsToPlay.equalsIgnoreCase("WillAttack")) {
if (sa != null && game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) { if (sa != null && game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return doesSpecifiedCreatureAttackAI(sa.getActivatingPlayer(), card) ? return ComputerUtilCard.doesSpecifiedCreatureAttackAI(sa.getActivatingPlayer(), card) ?
AiPlayDecision.WillPlay : AiPlayDecision.BadEtbEffects; AiPlayDecision.WillPlay : AiPlayDecision.BadEtbEffects;
} else { } else {
return AiPlayDecision.WillPlay; // not our turn, skip this check for the possible Flash use etc. return AiPlayDecision.WillPlay; // not our turn, skip this check for the possible Flash use etc.
@@ -1944,7 +1912,7 @@ public class ComputerUtilCard {
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield); CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, needsToPlay, card.getController(), card, sa); list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return AiPlayDecision.MissingNeededCards; return AiPlayDecision.MissingNeededCards;
} }
@@ -1965,60 +1933,6 @@ public class ComputerUtilCard {
return AiPlayDecision.WillPlay; return AiPlayDecision.WillPlay;
} }
public static Cost getTotalWardCost(Card c) {
Cost totalCost = new Cost(ManaCost.NO_COST, false);
for (final KeywordInterface inst : c.getKeywords(Keyword.WARD)) {
final String keyword = inst.getOriginal();
final String[] k = keyword.split(":");
final Cost wardCost = new Cost(k[1], false);
totalCost = totalCost.add(wardCost);
}
return totalCost;
}
public static boolean willUntap(Player ai, Card tapped) {
// TODO use AiLogic on trigger in case card loses all abilities
// if it's from a static need to also check canUntap
for (Card card : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
boolean untapsEachTurn = card.hasSVar("UntapsEachTurn");
boolean untapsEachOtherTurn = card.hasSVar("UntapsEachOtherPlayerTurn");
if (untapsEachTurn || untapsEachOtherTurn) {
String affected = untapsEachTurn ? card.getSVar("UntapsEachTurn")
: card.getSVar("UntapsEachOtherPlayerTurn");
for (String aff : TextUtil.split(affected, ',')) {
if (tapped.isValid(aff, ai, tapped, null)
&& (untapsEachTurn || (untapsEachOtherTurn && ai.equals(card.getController())))) {
return true;
}
}
}
}
return false;
}
// TODO replace most calls to Player.isCardInPlay because they include phased out
public static boolean isNonDisabledCardInPlay(final Player ai, final String cardName) {
for (Card card : ai.getCardsIn(ZoneType.Battlefield, cardName)) {
// TODO - Better logic to determine if a permanent is disabled by local effects
// currently assuming any permanent enchanted by another player
// is disabled and a second copy is necessary
// will need actual logic that determines if the enchantment is able
// to disable the permanent or it's still functional and a duplicate is unneeded.
boolean disabledByEnemy = false;
for (Card card2 : card.getEnchantedBy()) {
if (card2.getOwner() != ai) {
disabledByEnemy = true;
}
}
if (!disabledByEnemy) {
return true;
}
}
return false;
}
// Determine if the AI has an AI:RemoveDeck:All or an AI:RemoveDeck:Random hint specified. // Determine if the AI has an AI:RemoveDeck:All or an AI:RemoveDeck:Random hint specified.
// Includes a NPE guard on getRules() which might otherwise be tripped on some cards (e.g. tokens). // Includes a NPE guard on getRules() which might otherwise be tripped on some cards (e.g. tokens).
public static boolean isCardRemAIDeck(final Card card) { public static boolean isCardRemAIDeck(final Card card) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,49 @@
package forge.ai; package forge.ai;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi; import forge.ai.ability.AnimateAi;
import forge.card.ColorSet; import forge.card.ColorSet;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.card.Card;
import forge.game.card.*; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.*; import forge.game.cost.Cost;
import forge.game.cost.CostDamage;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayLife;
import forge.game.cost.CostPayment;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveAnyCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.cost.CostTapType;
import forge.game.cost.PaymentDecision;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.Spell; import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil; import forge.util.TextUtil;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Set;
public class ComputerUtilCost { public class ComputerUtilCost {
@@ -73,7 +87,7 @@ public class ComputerUtilCost {
if (cost == null) { if (cost == null) {
return true; return true;
} }
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false); final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa);
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) { if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part; final CostRemoveCounter remCounter = (CostRemoveCounter) part;
@@ -123,8 +137,9 @@ public class ComputerUtilCost {
* the source * the source
* @return true, if successful * @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, SpellAbility sa) {
if (cost == null) { if (cost == null || source.hasSVar("AISkipDiscardCostCheck") /* FIXME: should not be needed! */ ) {
return true; return true;
} }
@@ -135,20 +150,14 @@ public class ComputerUtilCost {
final CostDiscard disc = (CostDiscard) part; final CostDiscard disc = (CostDiscard) part;
final String type = disc.getType(); final String type = disc.getType();
if (type.equals("CARDNAME")) { if (type.equals("CARDNAME") && source.getAbilityText().contains("Bloodrush")) {
if (source.getAbilityText().contains("Bloodrush")) {
continue; 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;
} }
} final CardCollection typeList = CardLists.getValidCards(hand, type.split(","), source.getController(), source, sa);
final CardCollection typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) { if (typeList.size() > ai.getMaxHandSize()) {
continue; continue;
} }
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa); int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList); Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
@@ -233,51 +242,6 @@ public class ComputerUtilCost {
return true; 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. * Check creature sacrifice cost.
* *
@@ -306,10 +270,7 @@ public class ComputerUtilCost {
} }
final CardCollection sacList = new CardCollection(); final CardCollection sacList = new CardCollection();
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility); final 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);
int count = 0; int count = 0;
while (count < amount) { while (count < amount) {
@@ -359,14 +320,11 @@ public class ComputerUtilCost {
} }
final CardCollection sacList = new CardCollection(); final CardCollection sacList = new CardCollection();
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility); final 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);
int count = 0; int count = 0;
while (count < amount) { while (count < amount) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList, sourceAbility); Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
if (prefCard == null) { if (prefCard == null) {
return false; return false;
} }
@@ -379,19 +337,6 @@ public class ComputerUtilCost {
return true; 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) { public static boolean isSacrificeSelfCost(final Cost cost) {
if (cost == null) { if (cost == null) {
return false; return false;
@@ -421,8 +366,6 @@ public class ComputerUtilCost {
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostTapType) { if (part instanceof CostTapType) {
String type = part.getType();
/* /*
* Only crew with creatures weaker than vehicle * Only crew with creatures weaker than vehicle
* *
@@ -433,6 +376,7 @@ public class ComputerUtilCost {
if (sa.hasParam("Crew")) { if (sa.hasParam("Crew")) {
Card vehicle = AnimateAi.becomeAnimated(source, sa); Card vehicle = AnimateAi.becomeAnimated(source, sa);
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle); final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
String type = part.getType();
String totalP = type.split("withTotalPowerGE")[1]; String totalP = type.split("withTotalPowerGE")[1];
type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), ""); type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
CardCollection exclude = CardLists.getValidCards( CardCollection exclude = CardLists.getValidCards(
@@ -447,39 +391,23 @@ public class ComputerUtilCost {
return ComputerUtil.chooseTapTypeAccumulatePower(ai, type, sa, true, return ComputerUtil.chooseTapTypeAccumulatePower(ai, type, sa, true,
Integer.parseInt(totalP), exclude) != null; Integer.parseInt(totalP), exclude) != null;
} }
return false;
}
}
return true;
}
// check if we have a valid card to tap (e.g. Jaspera Sentinel) /**
Integer c = part.convertAmount(); * Check sacrifice cost.
if (c == null) { *
c = AbilityUtils.calculateAmount(source, part.getAmount(), sa); * @param cost
} * the cost
CardCollection exclude = new CardCollection(); * @param source
if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST) != null) { * the source
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST)); * @return true, if successful
} */
// trying to produce mana that includes tapping source that will already be tapped public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
if (exclude.contains(source) && cost.hasTapCost()) { return checkSacrificeCost(ai, cost, source, sourceAbility,true);
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 true;
} }
/** /**
@@ -492,8 +420,8 @@ public class ComputerUtilCost {
* @param cost * @param cost
* @return a boolean. * @return a boolean.
*/ */
@Deprecated
public static boolean shouldPayCost(final Player ai, final Card hostCard, final Cost cost) { public static boolean shouldPayCost(final Player ai, final Card hostCard, final Cost cost) {
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) { if (part instanceof CostPayLife) {
if (!ai.cantLoseForZeroOrLessLife()) { if (!ai.cantLoseForZeroOrLessLife()) {
@@ -523,7 +451,7 @@ public class ComputerUtilCost {
* a {@link forge.game.player.Player} object. * a {@link forge.game.player.Player} object.
* @return a boolean. * @return a boolean.
*/ */
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) { public static boolean canPayCost(final SpellAbility sa, final Player player) {
if (sa.getActivatingPlayer() == null) { if (sa.getActivatingPlayer() == null) {
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added. sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
} }
@@ -585,19 +513,7 @@ public class ComputerUtilCost {
if (sa.hasParam("Crew")) { // put under checkTapTypeCost? if (sa.hasParam("Crew")) { // put under checkTapTypeCost?
for (final CostPart part : sa.getPayCosts().getCostParts()) { for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostTapType && part.getType().contains("+withTotalPowerGE")) { if (part instanceof CostTapType && part.getType().contains("+withTotalPowerGE")) {
return new AiCostDecision(player, sa, false).visit((CostTapType)part) != null; return new AiCostDecision(player, sa).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();
}
} }
} }
} }
@@ -632,7 +548,7 @@ public class ComputerUtilCost {
} }
} }
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect) return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa); && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
} // canPayCost() } // canPayCost()
@@ -642,6 +558,7 @@ public class ComputerUtilCost {
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic); boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined"); boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payNever = "Never".equals(aiLogic); boolean payNever = "Never".equals(aiLogic);
boolean shockland = "Shockland".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer); boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; } if (payNever) { return false; }
@@ -653,9 +570,29 @@ public class ComputerUtilCost {
return false; return false;
} }
} else if ("OnlyDontControl".equals(aiLogic)) { } else if ("OnlyDontControl".equals(aiLogic)) {
if (source == null || payer.equals(source.getController())) { if (sa.getHostCard() == null || payer.equals(sa.getHostCard().getController())) {
return false; 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)) { } else if ("Paralyze".equals(aiLogic)) {
final Card c = source.getEnchantingCard(); final Card c = source.getEnchantingCard();
if (c == null || c.isUntapped()) { if (c == null || c.isUntapped()) {
@@ -663,7 +600,7 @@ public class ComputerUtilCost {
} }
} else if ("RiskFactor".equals(aiLogic)) { } else if ("RiskFactor".equals(aiLogic)) {
final Player activator = sa.getActivatingPlayer(); 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; return false;
} }
} else if ("MorePowerful".equals(aiLogic)) { } else if ("MorePowerful".equals(aiLogic)) {
@@ -676,7 +613,7 @@ public class ComputerUtilCost {
// if payer can't lose life its no need to pay unless // if payer can't lose life its no need to pay unless
if (!payer.canLoseLife()) if (!payer.canLoseLife())
return false; return false;
else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substring(6), sa)) { else if (payer.getLife() <= Integer.valueOf(aiLogic.substring(6))) {
return true; return true;
} }
} else if ("WillAttack".equals(aiLogic)) { } else if ("WillAttack".equals(aiLogic)) {
@@ -693,29 +630,6 @@ public class ComputerUtilCost {
return false; 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 // AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) { if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
return false; return false;
@@ -737,6 +651,7 @@ public class ComputerUtilCost {
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) { public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand)); return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
} }
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) { public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) {
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED); CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED);
Set<String> colorsAvailable = Sets.newHashSet(); Set<String> colorsAvailable = Sets.newHashSet();
@@ -767,19 +682,18 @@ public class ComputerUtilCost {
return false; return false;
} }
public static int getMaxXValue(SpellAbility sa, Player ai, final boolean effect) { public static int getMaxXValue(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
SpellAbility root = sa.getRootAbility(); final SpellAbility root = sa.getRootAbility();
final Cost abCost = root.getPayCosts(); final Cost abCost = sa.getPayCosts();
if (abCost == null || !abCost.hasXInAnyCostPart()) { if (abCost == null || !abCost.hasXInAnyCostPart()) {
return 0; return 0;
} }
Integer val = null; Integer val = null;
if (root.costHasManaX()) { if (sa.costHasManaX()) {
val = ComputerUtilMana.determineLeftoverMana(root, ai, effect); val = ComputerUtilMana.determineLeftoverMana(root, ai);
} }
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
@@ -791,11 +705,11 @@ public class ComputerUtilCost {
if (sa.hasParam("AIMaxTgtsCount")) { if (sa.hasParam("AIMaxTgtsCount")) {
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified // 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? // 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, AbilityUtils.calculateAmount(sa.getHostCard(), "Count$" + sa.getParam("AIMaxTgtsCount"), sa));
} }
} }
val = ObjectUtils.min(val, abCost.getMaxForNonManaX(root, ai, false)); val = ObjectUtils.min(val, abCost.getMaxForNonManaX(root, ai));
if (val != null && val > 0) { if (val != null && val > 0) {
// filter cost parts for preferences, don't choose X > than possible preferences // filter cost parts for preferences, don't choose X > than possible preferences
@@ -825,12 +739,4 @@ public class ComputerUtilCost {
} }
return ObjectUtils.defaultIfNull(val, 0); 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 com.google.common.base.Function;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.cost.CostPayEnergy; import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityMustAttack;
import java.util.List;
public class CreatureEvaluator implements Function<Card, Integer> { 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 @Override
public Integer apply(Card c) { public Integer apply(Card c) {
return evaluateCreature(c); return evaluateCreature(c);
@@ -23,30 +27,27 @@ public class CreatureEvaluator implements Function<Card, Integer> {
public int evaluateCreature(final Card c) { public int evaluateCreature(final Card c) {
return evaluateCreature(c, true, true); return evaluateCreature(c, true, true);
} }
public int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) { public int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
int value = 80; int value = 80;
if (!c.isToken()) { if (!c.isToken()) {
value += addValue(20, "non-token"); // tokens should be worth less than actual cards value += addValue(20, "non-token"); // tokens should be worth less than actual cards
} }
int power = c.getNetCombatDamage(); int power = getEffectivePower(c);
final int toughness = c.getNetToughness(); final int toughness = getEffectiveToughness(c);
for (KeywordInterface kw : c.getKeywords()) {
// TODO replace with ReplacementEffect checks String keyword = kw.getOriginal();
if (c.hasKeyword("Prevent all combat damage that would be dealt by CARDNAME.") if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|| c.hasKeyword("Prevent all damage that would be dealt by CARDNAME.") || keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|| c.hasKeyword("Prevent all combat damage that would be dealt to and dealt by CARDNAME.") || keyword.equals("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.")) { || keyword.equals("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
power = 0; power = 0;
break;
}
} }
if (considerPT) { if (considerPT) {
value += addValue(power * 15, "power"); value += addValue(power * 15, "power");
value += addValue(toughness * 10, "toughness: " + toughness); 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) { if (considerCMC) {
value += addValue(c.getCMC() * 5, "cmc"); value += addValue(c.getCMC() * 5, "cmc");
@@ -74,8 +75,8 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword(Keyword.MENACE)) { if (c.hasKeyword(Keyword.MENACE)) {
value += addValue(power * 4, "menace"); value += addValue(power * 4, "menace");
} }
if (c.hasKeyword(Keyword.SKULK)) { if (c.hasStartOfKeyword("CantBeBlockedBy")) {
value += addValue(power * 3, "skulk"); value += addValue(power * 3, "block-restrict");
} }
} }
@@ -98,28 +99,29 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword(Keyword.VIGILANCE)) { if (c.hasKeyword(Keyword.VIGILANCE)) {
value += addValue((power * 5) + (toughness * 5), "vigilance"); value += addValue((power * 5) + (toughness * 5), "vigilance");
} }
if (c.hasKeyword(Keyword.WITHER)) {
value += addValue(power * 10, "Wither");
}
if (c.hasKeyword(Keyword.INFECT)) { if (c.hasKeyword(Keyword.INFECT)) {
value += addValue(power * 15, "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.RAMPAGE), "rampage");
value += addValue(c.getKeywordMagnitude(Keyword.AFFLICT) * 5, "afflict"); 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.ANNIHILATOR) * 50, "eldrazi");
value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb"); value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb");
// Keywords that may produce temporary or permanent buffs over time // Keywords that may produce temporary or permanent buffs over time
if (c.hasKeyword(Keyword.PROWESS)) {
value += addValue(5, "prowess");
}
if (c.hasKeyword(Keyword.OUTLAST)) { if (c.hasKeyword(Keyword.OUTLAST)) {
value += addValue(10, "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 // Defensive Keywords
if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) { if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) {
@@ -142,68 +144,35 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(35, "hexproof"); value += addValue(35, "hexproof");
} else if (c.hasKeyword(Keyword.SHROUD)) { } else if (c.hasKeyword(Keyword.SHROUD)) {
value += addValue(30, "shroud"); value += addValue(30, "shroud");
} else if (c.hasKeyword(Keyword.WARD)) {
value += addValue(10, "ward");
} }
if (c.hasKeyword(Keyword.PROTECTION)) { if (c.hasKeyword(Keyword.PROTECTION)) {
value += addValue(20, "protection"); value += addValue(20, "protection");
} }
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
// paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) {
value += addValue(14, "paired");
}
if (c.hasEncodedCard()) {
value += addValue(24, "encoded");
}
if (ComputerUtilCard.hasActiveUndyingOrPersist(c)) {
value += addValue(30, "revive");
}
// Bad keywords // Bad keywords
if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) { if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) {
value -= subValue((power * 9) + 40, "defender"); value -= subValue((power * 9) + 40, "defender");
} else if (c.getSVar("SacrificeEndCombat").equals("True")) { } else if (c.getSVar("SacrificeEndCombat").equals("True")) {
value -= subValue(40, "sac-end"); value -= subValue(40, "sac-end");
} }
if (c.hasKeyword("CARDNAME can't attack or block.")) { if (c.hasKeyword("CARDNAME can't block.")) {
value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
} else if (c.hasKeyword("CARDNAME can't block.")) {
value -= subValue(10, "cant-block"); value -= subValue(10, "cant-block");
} else { } else if (c.hasKeyword("CARDNAME attacks each turn if able.")
List<GameEntity> mAEnt = StaticAbilityMustAttack.entitiesMustAttack(c); || c.hasKeyword("CARDNAME attacks each combat if able.")) {
if (mAEnt.contains(c)) {
value -= subValue(10, "must-attack"); value -= subValue(10, "must-attack");
} else if (!mAEnt.isEmpty()) { } else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
value -= subValue(10, "must-attack-player"); value -= subValue(10, "must-attack-player");
}/* else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) { } else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
value -= subValue(toughness * 5, "reverse-reach"); value -= subValue(toughness * 5, "reverse-reach");
}//*/
} }
if (c.hasSVar("DestroyWhenDamaged")) { if (c.hasSVar("DestroyWhenDamaged")) {
value -= subValue((toughness - 1) * 9, "dies-to-dmg"); value -= subValue((toughness - 1) * 9, "dies-to-dmg");
} }
if (c.getSVar("Targeting").equals("Dies")) {
value -= subValue(25, "dies");
}
if (c.isUntapped()) { if (c.hasKeyword("CARDNAME can't attack or block.")) {
value += addValue(1, "untapped"); value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
} }
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) { if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
if (c.isTapped()) { if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
@@ -220,20 +189,40 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) { } else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
value -= subValue(10, "echo-unpaid"); 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)) { if (c.hasKeyword(Keyword.FADING)) {
value -= subValue(20, "fading"); value -= subValue(20, "fading");
} }
if (c.hasKeyword(Keyword.VANISHING)) { if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20, "vanishing"); value -= subValue(20, "vanishing");
} }
// use scaling because the creature is only available halfway if (c.getSVar("Targeting").equals("Dies")) {
if (c.hasKeyword(Keyword.PHASING)) { value -= subValue(25, "dies");
value -= subValue(Math.max(20, value / 2), "phasing");
} }
// TODO no longer a KW for (final SpellAbility sa : c.getSpellAbilities()) {
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) { if (sa.isAbility()) {
value -= subValue(20, "upkeep-dmg"); 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");
}
if (!c.getEncodedCards().isEmpty()) {
value += addValue(24, "encoded");
} }
// card-specific evaluation modifier // card-specific evaluation modifier
@@ -255,7 +244,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) { && (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
// Electrostatic Pummeler, can be expanded for similar cards // Electrostatic Pummeler, can be expanded for similar cards
int initPower = sa.getHostCard().getNetPower(); int initPower = getEffectivePower(sa.getHostCard());
int pumpedPower = initPower; int pumpedPower = initPower;
int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY); int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY);
if (energy > 0) { if (energy > 0) {

View File

@@ -3,20 +3,31 @@ package forge.ai;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import com.google.common.collect.*;
import org.apache.commons.lang3.StringUtils; 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.StaticData;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.effects.DetachedCardEffect; import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCloneStates; import forge.game.card.CardCloneStates;
@@ -33,7 +44,9 @@ import forge.game.mana.ManaPool;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart; import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone; import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.item.IPaperCard; import forge.item.IPaperCard;
@@ -50,7 +63,6 @@ public abstract class GameState {
ZONES.put(ZoneType.Library, "library"); ZONES.put(ZoneType.Library, "library");
ZONES.put(ZoneType.Exile, "exile"); ZONES.put(ZoneType.Exile, "exile");
ZONES.put(ZoneType.Command, "command"); ZONES.put(ZoneType.Command, "command");
ZONES.put(ZoneType.Sideboard, "sideboard");
} }
private int humanLife = -1; private int humanLife = -1;
@@ -77,8 +89,7 @@ public abstract class GameState {
private final Map<Card, Integer> markedDamage = new HashMap<>(); private final Map<Card, Integer> markedDamage = new HashMap<>();
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>(); private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>(); private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>();
private final Map<Card, String> cardToChosenType = new HashMap<>(); private final Map<Card, List<String>> cardToChosenTypes = 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>> cardToRememberedId = new HashMap<>();
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>(); private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
private final Map<Card, List<String>> cardToMergedCards = new HashMap<>(); private final Map<Card, List<String>> cardToMergedCards = new HashMap<>();
@@ -154,7 +165,7 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n")); sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n"));
} }
if (!computerManaPool.isEmpty()) { if (!computerManaPool.isEmpty()) {
sb.append(TextUtil.concatNoSpace("aimanapool=", computerManaPool, "\n")); sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n"));
} }
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n")); sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
@@ -315,29 +326,30 @@ public abstract class GameState {
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) { } else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal"); newText.append("|Modal");
} }
if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
}
if (c.getPlayerAttachedTo() != null) { if (c.getPlayerAttachedTo() != null) {
// TODO: improve this for game states with more than two players // TODO: improve this for game states with more than two players
newText.append("|EnchantingPlayer:"); newText.append("|EnchantingPlayer:");
Player p = c.getPlayerAttachedTo(); Player p = c.getPlayerAttachedTo();
newText.append(p.getController().isAI() ? "AI" : "HUMAN"); newText.append(p.getController().isAI() ? "AI" : "HUMAN");
} else if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
} }
if (c.getDamage() > 0) { if (c.getDamage() > 0) {
newText.append("|Damage:").append(c.getDamage()); newText.append("|Damage:").append(c.getDamage());
} }
if (c.hasChosenColor()) { SpellAbility first = c.getFirstSpellAbility();
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ",")); if (first != null) {
if (first.hasChosenColor()) {
newText.append("|ChosenColor:").append(TextUtil.join(first.getChosenColors(), ","));
} }
if (c.hasChosenType()) { if (first.hasChosenType()) {
newText.append("|ChosenType:").append(c.getChosenType()); newText.append("|ChosenType:").append(TextUtil.join(first.getChosenType(), ","));
} }
if (c.hasChosenType2()) {
newText.append("|ChosenType2:").append(c.getChosenType2());
} }
if (!c.getNamedCard().isEmpty()) { if (!c.getNamedCard().isEmpty()) {
newText.append("|NamedCard:").append(c.getNamedCard()); newText.append("|NamedCard:").append(c.getNamedCard());
} }
@@ -400,12 +412,6 @@ public abstract class GameState {
// Need to figure out a better way to detect if it's actually on adventure. // Need to figure out a better way to detect if it's actually on adventure.
newText.append("|OnAdventure"); newText.append("|OnAdventure");
} }
if (c.isForetold()) {
newText.append("|Foretold");
}
if (c.isForetoldThisTurn()) {
newText.append("|ForetoldThisTurn");
}
} }
@@ -573,13 +579,6 @@ public abstract class GameState {
aiCardTexts.put(ZoneType.Command, categoryValue); 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")) { else if (categoryName.startsWith("ability")) {
abilityString.put(categoryName.substring("ability".length()), categoryValue); abilityString.put(categoryName.substring("ability".length()), categoryValue);
} }
@@ -639,8 +638,7 @@ public abstract class GameState {
markedDamage.clear(); markedDamage.clear();
cardToChosenClrs.clear(); cardToChosenClrs.clear();
cardToChosenCards.clear(); cardToChosenCards.clear();
cardToChosenType.clear(); cardToChosenTypes.clear();
cardToChosenType2.clear();
cardToMergedCards.clear(); cardToMergedCards.clear();
cardToScript.clear(); cardToScript.clear();
cardAttackMap.clear(); cardAttackMap.clear();
@@ -707,19 +705,11 @@ public abstract class GameState {
} }
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated 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) { private String processManaPool(ManaPool manaPool) {
StringBuilder mana = new StringBuilder(); StringBuilder mana = new StringBuilder();
for (final byte c : ManaAtom.MANATYPES) { for (final byte c : MagicColor.WUBRGC) {
int amount = manaPool.getAmountOfColor(c); int amount = manaPool.getAmountOfColor(c);
for (int i = 0; i < amount; i++) { for (int i = 0; i < amount; i++) {
mana.append(MagicColor.toShortString(c)).append(" "); mana.append(MagicColor.toShortString(c)).append(" ");
@@ -743,7 +733,7 @@ public abstract class GameState {
if (persistent) { if (persistent) {
produced.put("PersistentMana", "True"); produced.put("PersistentMana", "True");
} }
final AbilityManaPart abMana = new AbilityManaPart(dummy, produced); final AbilityManaPart abMana = new AbilityManaPart(dummy, null, produced);
game.getAction().invoke(new Runnable() { game.getAction().invoke(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -779,10 +769,23 @@ public abstract class GameState {
} }
game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap)); game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap));
if (!combat.getAttackers().isEmpty()) {
List<GameEntity> attackedTarget = Lists.newArrayList();
for (final Card c : combat.getAttackers()) { for (final Card c : combat.getAttackers()) {
CombatUtil.checkDeclaredAttacker(game, c, combat, false); 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.updateCombatForView();
game.fireEvent(new GameEventCombatChanged()); game.fireEvent(new GameEventCombatChanged());
@@ -825,6 +828,7 @@ public abstract class GameState {
Card exiledWith = idToCard.get(Integer.parseInt(id)); Card exiledWith = idToCard.get(Integer.parseInt(id));
c.setExiledWith(exiledWith); c.setExiledWith(exiledWith);
c.setExiledBy(exiledWith.getController()); c.setExiledBy(exiledWith.getController());
exiledWith.addExiledWith(c);
} }
} }
@@ -1052,12 +1056,7 @@ public abstract class GameState {
return; 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); sa.setActivatingPlayer(activator);
handleScriptedTargetingForSA(game, sa, tgtID); handleScriptedTargetingForSA(game, sa, tgtID);
@@ -1088,19 +1087,13 @@ public abstract class GameState {
Card c = entry.getKey(); Card c = entry.getKey();
List<String> colors = entry.getValue(); List<String> colors = entry.getValue();
c.setChosenColors(colors); c.setChosenColors(colors, c.getFirstSpellAbility());
} }
// Chosen type // Chosen type
for (Entry<Card, String> entry : cardToChosenType.entrySet()) { for (Entry<Card, List<String>> entry : cardToChosenTypes.entrySet()) {
Card c = entry.getKey(); Card c = entry.getKey();
c.setChosenType(entry.getValue()); c.setChosenType(entry.getValue(), c.getFirstSpellAbility());
}
// Chosen type 2
for (Entry<Card, String> entry : cardToChosenType2.entrySet()) {
Card c = entry.getKey();
c.setChosenType2(entry.getValue());
} }
// Named card // Named card
@@ -1203,7 +1196,7 @@ public abstract class GameState {
String[] allCounterStrings = counterString.split(","); String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) { for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2); String[] pair = counterPair.split("=", 2);
entity.addCounterInternal(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, null); entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
} }
} }
@@ -1216,8 +1209,6 @@ public abstract class GameState {
p.getZone(zt).removeAllCards(true); p.getZone(zt).removeAllCards(true);
} }
p.setCommanders(Lists.newArrayList());
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<>(ZoneType.class); Map<ZoneType, CardCollectionView> playerCards = new EnumMap<>(ZoneType.class);
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) { for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
String value = kv.getValue(); String value = kv.getValue();
@@ -1228,8 +1219,6 @@ public abstract class GameState {
p.setLandsPlayedThisTurn(landsPlayed); p.setLandsPlayedThisTurn(landsPlayed);
p.setLandsPlayedLastTurn(landsPlayedLastTurn); p.setLandsPlayedLastTurn(landsPlayedLastTurn);
p.clearPaidForSA();
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) { for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey()); PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) { if (kv.getKey() == ZoneType.Battlefield) {
@@ -1247,21 +1236,22 @@ public abstract class GameState {
boolean tapped = c.isTapped(); boolean tapped = c.isTapped();
boolean sickness = c.hasSickness(); boolean sickness = c.hasSickness();
Map<CounterType, Integer> counters = c.getCounters(); Map<CounterType, Integer> counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters var as-is. // Note: Not clearCounters() since we want to keep the counters
// var as-is.
c.setCounters(Maps.newHashMap()); c.setCounters(Maps.newHashMap());
if (c.isAura()) { if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere // dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter) // (will be overridden later, so the actual value shouldn't matter)
//FIXME it shouldn't be able to attach itself //FIXME it shouldn't be able to attach itself
c.setEntityAttachedTo(CardFactory.copyCard(c, true)); c.setEntityAttachedTo(c);
} }
if (cardsWithoutETBTrigs.contains(c)) { if (cardsWithoutETBTrigs.contains(c)) {
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null, null); p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null);
} else { } else {
p.getZone(ZoneType.Hand).add(c); p.getZone(ZoneType.Hand).add(c);
p.getGame().getAction().moveToPlay(c, null, null); p.getGame().getAction().moveToPlay(c, null);
} }
c.setTapped(tapped); c.setTapped(tapped);
@@ -1323,7 +1313,7 @@ public abstract class GameState {
for (final String info : cardinfo) { for (final String info : cardinfo) {
if (info.startsWith("Tapped")) { if (info.startsWith("Tapped")) {
c.tap(false); c.tap();
} else if (info.startsWith("Renowned")) { } else if (info.startsWith("Renowned")) {
c.setRenowned(true); c.setRenowned(true);
} else if (info.startsWith("Monstrous")) { } else if (info.startsWith("Monstrous")) {
@@ -1350,7 +1340,7 @@ public abstract class GameState {
} }
else if (info.startsWith("OnAdventure")) { else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell"; String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell";
SpellAbility saAdventure = AbilityFactory.getAbility(abAdventure, c); AbilitySub saAdventure = (AbilitySub)AbilityFactory.getAbility(abAdventure, c);
StringBuilder sbPlay = new StringBuilder(); StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure"); sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card."); sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
@@ -1361,9 +1351,7 @@ public abstract class GameState {
c.setExiledBy(c.getController()); c.setExiledBy(c.getController());
} else if (info.startsWith("IsCommander")) { } else if (info.startsWith("IsCommander")) {
c.setCommander(true); c.setCommander(true);
List<Card> cmd = Lists.newArrayList(player.getCommanders()); player.setCommanders(Lists.newArrayList(c));
cmd.add(c);
player.setCommanders(cmd);
} else if (info.startsWith("Id:")) { } else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3)); int id = Integer.parseInt(info.substring(3));
idToCard.put(id, c); idToCard.put(id, c);
@@ -1391,9 +1379,7 @@ public abstract class GameState {
} else if (info.startsWith("ChosenColor:")) { } else if (info.startsWith("ChosenColor:")) {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(","))); cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) { } else if (info.startsWith("ChosenType:")) {
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1)); cardToChosenTypes.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType2:")) {
cardToChosenType2.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ChosenCards:")) { } else if (info.startsWith("ChosenCards:")) {
CardCollection chosen = new CardCollection(); CardCollection chosen = new CardCollection();
String[] idlist = info.substring(info.indexOf(':') + 1).split(","); String[] idlist = info.substring(info.indexOf(':') + 1).split(",");
@@ -1425,14 +1411,6 @@ public abstract class GameState {
} }
} else if (info.equals("NoETBTrigs")) { } else if (info.equals("NoETBTrigs")) {
cardsWithoutETBTrigs.add(c); 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

@@ -25,6 +25,7 @@ public class LobbyPlayerAi extends LobbyPlayer implements IGameEntitiesFactory {
public boolean isAllowCheatShuffle() { public boolean isAllowCheatShuffle() {
return allowCheatShuffle; return allowCheatShuffle;
} }
public void setAllowCheatShuffle(boolean allowCheatShuffle) { public void setAllowCheatShuffle(boolean allowCheatShuffle) {
this.allowCheatShuffle = allowCheatShuffle; this.allowCheatShuffle = allowCheatShuffle;
} }
@@ -32,6 +33,7 @@ public class LobbyPlayerAi extends LobbyPlayer implements IGameEntitiesFactory {
public void setAiProfile(String profileName) { public void setAiProfile(String profileName) {
aiProfile = profileName; aiProfile = profileName;
} }
public String getAiProfile() { public String getAiProfile() {
return aiProfile; return aiProfile;
} }

View File

@@ -3,11 +3,11 @@ package forge.ai;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -27,7 +27,6 @@ import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard; import forge.card.mana.ManaCostShard;
import forge.deck.Deck; import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GameObject; import forge.game.GameObject;
@@ -52,6 +51,7 @@ import forge.game.cost.CostPartMana;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana; import forge.game.mana.Mana;
import forge.game.mana.ManaConversionMatrix; import forge.game.mana.ManaConversionMatrix;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.DelayedReveal; import forge.game.player.DelayedReveal;
@@ -79,7 +79,6 @@ import forge.util.MyRandom;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
/** /**
* A prototype for player controller class * A prototype for player controller class
* *
@@ -88,22 +87,12 @@ import forge.util.collect.FCollectionView;
public class PlayerControllerAi extends PlayerController { public class PlayerControllerAi extends PlayerController {
private final AiController brains; private final AiController brains;
private boolean pilotsNonAggroDeck = false;
public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) { public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) {
super(game, p, lp); super(game, p, lp);
brains = new AiController(p, game); 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); brains.allowCheatShuffle(value);
} }
@@ -136,31 +125,8 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, CardCollectionView remaining, int damageDealt, GameEntity defender, boolean overrideOrder) { public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, int damageDealt, GameEntity defender, boolean overrideOrder) {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, remaining, damageDealt, defender, 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 @Override
@@ -179,7 +145,7 @@ public class PlayerControllerAi extends PlayerController {
payingPlayer = ability.getHostCard().getEnchantingCard().getController(); 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.")) { if (logic.startsWith("MaxMana.") || logic.startsWith("PowerLeakMaxMana.")) {
number = Math.min(number, Integer.parseInt(logic.substring(logic.indexOf(".") + 1))); number = Math.min(number, Integer.parseInt(logic.substring(logic.indexOf(".") + 1)));
@@ -254,7 +220,7 @@ public class PlayerControllerAi extends PlayerController {
remaining.remove(selected); remaining.remove(selected);
selecteds.add(selected); selecteds.add(selected);
} }
} while (selected != null && selecteds.size() < num); } while ( (selected != null ) && (selecteds.size() < num) );
return selecteds; return selecteds;
} }
@@ -297,9 +263,9 @@ public class PlayerControllerAi extends PlayerController {
// Store/replace target choices more properly to get this SA cleared. // Store/replace target choices more properly to get this SA cleared.
TargetChoices tc = null; TargetChoices tc = null;
TargetChoices subtc = null; TargetChoices subtc = null;
boolean storeChoices = sa.usesTargeting(); boolean storeChoices = sa.getTargetRestrictions() != null;
final SpellAbility sub = sa.getSubAbility(); final SpellAbility sub = sa.getSubAbility();
boolean storeSubChoices = sub != null && sub.usesTargeting(); boolean storeSubChoices = sub != null && sub.getTargetRestrictions() != null;
boolean ret = true; boolean ret = true;
if (storeChoices) { if (storeChoices) {
@@ -339,7 +305,7 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public List<Card> exertAttackers(List<Card> attackers) { public List<Card> exertAttackers(List<Card> attackers) {
return AiAttackController.exertAttackers(attackers, brains.getAttackAggression()); return AiAttackController.exertAttackers(attackers);
} }
@Override @Override
@@ -370,13 +336,14 @@ public class PlayerControllerAi extends PlayerController {
for (Card c: topN) { for (Card c: topN) {
if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c)) { if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c)) {
toBottom.add(c); toBottom.add(c);
} else { }
else {
toTop.add(c); toTop.add(c);
} }
} }
// put the rest on top in random order // put the rest on top in random order
CardLists.shuffle(toTop); Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toBottom); return ImmutablePair.of(toTop, toBottom);
} }
@@ -402,7 +369,7 @@ public class PlayerControllerAi extends PlayerController {
} }
} }
CardLists.shuffle(toTop); Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toGraveyard); return ImmutablePair.of(toTop, toGraveyard);
} }
@@ -424,13 +391,13 @@ public class PlayerControllerAi extends PlayerController {
if (destinationZone == ZoneType.Graveyard) { if (destinationZone == ZoneType.Graveyard) {
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard // In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
if (Iterables.any(getGame().getCardsInGame(), new Predicate<Card>() { if (!CardLists.filter(getGame().getCardsInGame(), new Predicate<Card>() {
@Override @Override
public boolean apply(Card card) { public boolean apply(Card card) {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB // need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter"); return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter");
} }
})) { }).isEmpty()) {
int bestValue = 0; int bestValue = 0;
Card bestCreature = null; Card bestCreature = null;
for (Card c : cards) { for (Card c : cards) {
@@ -471,7 +438,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 (!p.isOpponentOf(player)) {
if (landsOTB <= 2) { if (landsOTB <= 2) {
@@ -533,7 +500,8 @@ public class PlayerControllerAi extends PlayerController {
if (copySA instanceof Spell) { if (copySA instanceof Spell) {
Spell spell = (Spell) copySA; Spell spell = (Spell) copySA;
((PlayerControllerAi) player.getController()).getAi().canPlayFromEffectAI(spell, true, true); ((PlayerControllerAi) player.getController()).getAi().canPlayFromEffectAI(spell, true, true);
} else { }
else {
getAi().canPlaySa(copySA); getAi().canPlaySa(copySA);
} }
} }
@@ -544,7 +512,7 @@ public class PlayerControllerAi extends PlayerController {
public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) { public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) {
if (canSetupTargets) if (canSetupTargets)
brains.doTrigger(effectSA, true); // first parameter does not matter, since return value won't be used 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, getGame());
} }
@Override @Override
@@ -552,16 +520,15 @@ public class PlayerControllerAi extends PlayerController {
return getAi().chooseCardsToDelve(genericAmount, grave); return getAi().chooseCardsToDelve(genericAmount, grave);
} }
@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 @Override
public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) { public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) {
String [] splitUTypes = uType.split(","); final CardCollectionView cardsOfType = CardLists.getType(hand, uType);
CardCollection cardsOfType = new CardCollection();
for (String part : splitUTypes) {
CardCollection partCards = CardLists.getType(hand, part);
if (!partCards.isEmpty()) {
cardsOfType.addAll(partCards);
}
}
if (!cardsOfType.isEmpty()) { if (!cardsOfType.isEmpty()) {
Card toDiscard = Aggregates.itemWithMin(cardsOfType, CardPredicates.Accessors.fnGetCmc); Card toDiscard = Aggregates.itemWithMin(cardsOfType, CardPredicates.Accessors.fnGetCmc);
return new CardCollection(toDiscard); return new CardCollection(toDiscard);
@@ -569,18 +536,19 @@ public class PlayerControllerAi extends PlayerController {
return getAi().getCardsToDiscard(num, null, sa); return getAi().getCardsToDiscard(num, null, sa);
} }
@Override @Override
public Mana chooseManaFromPool(List<Mana> manaChoices) { public Mana chooseManaFromPool(List<Mana> manaChoices) {
return manaChoices.get(0); // no brains used return manaChoices.get(0); // no brains used
} }
@Override @Override
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes, boolean isOptional) { public List<String> chooseSomeType(String kindOfType, SpellAbility sa, int min, int max, List<String> validTypes) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), validTypes, invalidTypes); List<String> chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), min, max, validTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) { if (chosen.isEmpty()) {
chosen = validTypes.iterator().next(); return validTypes.subList(0, min);
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: chosen");
} }
getGame().getAction().notifyOfValue(sa, player, chosen.toString(), player);
return chosen; return chosen;
} }
@@ -590,8 +558,8 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) { public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question) {
return brains.aiShouldRun(replacementEffect, effectSA, affected); return brains.aiShouldRun(replacementEffect, effectSA);
} }
@Override @Override
@@ -620,7 +588,7 @@ public class PlayerControllerAi extends PlayerController {
hand.removeAll(toReturn); hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS); 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 we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) { if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {
@@ -671,6 +639,7 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public boolean playChosenSpellAbility(SpellAbility sa) { public boolean playChosenSpellAbility(SpellAbility sa) {
// System.out.println("Playing sa: " + sa);
if (sa instanceof LandAbility) { if (sa instanceof LandAbility) {
if (sa.canPlay()) { if (sa.canPlay()) {
sa.resolve(); sa.resolve();
@@ -695,10 +664,8 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public boolean payManaOptional(Card c, Cost cost, SpellAbility sa, String prompt, ManaPaymentPurpose purpose) { 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() {} }; final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
ability.setActivatingPlayer(c.getController()); 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, // 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 // 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 +685,8 @@ public class PlayerControllerAi extends PlayerController {
} }
// - End of hack for Exile a card from library Cumulative Upkeep - // - End of hack for Exile a card from library Cumulative Upkeep -
if (ComputerUtilCost.canPayCost(ability, c.getController(), true)) { if (ComputerUtilCost.canPayCost(ability, c.getController())) {
ComputerUtil.playNoStack(c.getController(), ability, getGame(), true); ComputerUtil.playNoStack(c.getController(), ability, getGame());
return true; return true;
} }
return false; return false;
@@ -780,6 +747,7 @@ public class PlayerControllerAi extends PlayerController {
return allTargets.get(0); return allTargets.get(0);
} }
@Override @Override
public void notifyOfValue(SpellAbility saSource, GameObject realtedTarget, String value) { 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 // AI should take into consideration creature types, numbers and other information (mostly choices) arriving through this channel
@@ -802,7 +770,7 @@ public class PlayerControllerAi extends PlayerController {
return true; return true;
} else { } else {
Card rem = (Card) source.getFirstRemembered(); Card rem = (Card) source.getFirstRemembered();
if (!rem.isInPlay()) { if (!rem.isInZone(ZoneType.Battlefield)) {
return true; return true;
} }
} }
@@ -810,8 +778,7 @@ public class PlayerControllerAi extends PlayerController {
case "BetterTgtThanRemembered": case "BetterTgtThanRemembered":
if (source.getRememberedCount() > 0) { if (source.getRememberedCount() > 0) {
Card rem = (Card) source.getFirstRemembered(); Card rem = (Card) source.getFirstRemembered();
// avoid pumping opponent creature if (!rem.isInZone(ZoneType.Battlefield)) {
if (!rem.isInPlay() || rem.getController().isOpponentOf(source.getController())) {
return true; return true;
} }
for (Card c : source.getController().getCreaturesInPlay()) { for (Card c : source.getController().getCreaturesInPlay()) {
@@ -885,9 +852,10 @@ public class PlayerControllerAi extends PlayerController {
byte chosenColorMask = MagicColor.fromName(c); byte chosenColorMask = MagicColor.fromName(c);
if ((colors.getColor() & chosenColorMask) != 0) { if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask; return chosenColorMask;
} } else {
return Iterables.getFirst(colors, (byte)0); return Iterables.getFirst(colors, (byte)0);
} }
}
@Override @Override
public byte chooseColor(String message, SpellAbility sa, ColorSet colors) { public byte chooseColor(String message, SpellAbility sa, ColorSet colors) {
@@ -905,8 +873,10 @@ public class PlayerControllerAi extends PlayerController {
if ((colors.getColor() & chosenColorMask) != 0) { if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask; return chosenColorMask;
} }
else {
return Iterables.getFirst(colors, MagicColor.WHITE); return Iterables.getFirst(colors, MagicColor.WHITE);
} }
}
@Override @Override
public ICardFace chooseSingleCardFace(SpellAbility sa, String message, public ICardFace chooseSingleCardFace(SpellAbility sa, String message,
@@ -928,10 +898,6 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, String prompt, public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, String prompt,
Map<String, Object> params) { 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(); ApiType api = sa.getApi();
if (null == api) { if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet"); throw new InvalidParameterException("SA is not api-based, this is not supported yet");
@@ -1001,7 +967,10 @@ public class PlayerControllerAi extends PlayerController {
} }
final String logic = sa.getParam("AILogic"); final String logic = sa.getParam("AILogic");
if (logic == null || logic.equals("MostProminentHumanCreatures")) { 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()) { if (list.isEmpty()) {
list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents()); list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents());
} }
@@ -1015,15 +984,12 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) { public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
// TODO replace with EmptySa
final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } }; final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } };
emptyAbility.setActivatingPlayer(player); emptyAbility.setActivatingPlayer(player);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects()); emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setSVars(sa.getSVars()); emptyAbility.setSVars(sa.getSVars());
emptyAbility.setCardState(sa.getCardState()); if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) {
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid()); ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost
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
return true; return true;
} }
return false; return false;
@@ -1040,9 +1006,9 @@ public class PlayerControllerAi extends PlayerController {
if (sa.isCopied()) { if (sa.isCopied()) {
if (sa.isSpell()) { if (sa.isSpell()) {
if (!sa.getHostCard().isInZone(ZoneType.Stack)) { if (!sa.getHostCard().isInZone(ZoneType.Stack)) {
sa.setHostCard(getGame().getAction().moveToStack(sa.getHostCard(), sa)); sa.setHostCard(player.getGame().getAction().moveToStack(sa.getHostCard(), sa));
} else { } else {
getGame().getStackZone().add(sa.getHostCard()); player.getGame().getStackZone().add(sa.getHostCard());
} }
} }
@@ -1053,13 +1019,13 @@ public class PlayerControllerAi extends PlayerController {
*/ */
if (sa.isMayChooseNewTargets() && !sa.setupTargets()) { if (sa.isMayChooseNewTargets() && !sa.setupTargets()) {
if (sa.isSpell()) { if (sa.isSpell()) {
getGame().getAction().ceaseToExist(sa.getHostCard(), false); sa.getHostCard().ceaseToExist();
} }
continue; continue;
} }
} }
// need finally add the new spell to the stack // need finally add the new spell to the stack
getGame().getStack().add(sa); player.getGame().getStack().add(sa);
} }
} }
} }
@@ -1077,7 +1043,7 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public void playTrigger(Card host, WrappedAbility wrapperAbility, boolean isMandatory) { public void playTrigger(Card host, WrappedAbility wrapperAbility, boolean isMandatory) {
if (prepareSingleSa(host, wrapperAbility, isMandatory)) { if (prepareSingleSa(host, wrapperAbility, isMandatory)) {
ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, getGame(), true); ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, getGame());
} }
} }
@@ -1087,13 +1053,13 @@ public class PlayerControllerAi extends PlayerController {
boolean noManaCost = tgtSA.hasParam("WithoutManaCost"); boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell? if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA; Spell spell = (Spell) tgtSA;
// TODO if mandatory AI is only forced to use mana when it's already in the pool if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
if (tgtSA.checkRestrictions(brains.getPlayer()) && (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional)) {
if (noManaCost) { if (noManaCost) {
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame()); return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
} } else {
return ComputerUtil.playStack(tgtSA, player, getGame()); return ComputerUtil.playStack(tgtSA, player, getGame());
} }
} else
return false; // didn't play spell return false; // didn't play spell
} }
return true; return true;
@@ -1104,12 +1070,6 @@ public class PlayerControllerAi extends PlayerController {
return brains.doTrigger(currentAbility, true); 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 @Override
public boolean chooseCardsPile(SpellAbility sa, CardCollectionView pile1, CardCollectionView pile2, String faceUp) { public boolean chooseCardsPile(SpellAbility sa, CardCollectionView pile1, CardCollectionView pile2, String faceUp) {
if (faceUp.equals("True")) { if (faceUp.equals("True")) {
@@ -1123,6 +1083,7 @@ public class PlayerControllerAi extends PlayerController {
boolean allCreatures = Iterables.all(Iterables.concat(pile1, pile2), CardPredicates.Presets.CREATURES); boolean allCreatures = Iterables.all(Iterables.concat(pile1, pile2), CardPredicates.Presets.CREATURES);
int cmc1 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile1) : ComputerUtilCard.evaluatePermanentList(pile1); int cmc1 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile1) : ComputerUtilCard.evaluatePermanentList(pile1);
int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2); int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2);
System.out.println("value:" + cmc1 + " " + cmc2);
// for now, this assumes that the outcome will be bad // for now, this assumes that the outcome will be bad
// TODO: This should really have a ChooseLogic param to // TODO: This should really have a ChooseLogic param to
@@ -1137,15 +1098,7 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public void revealAISkipCards(String message, Map<Player, Map<DeckSection, List<? extends PaperCard>>> deckCards) { public Collection<? extends PaperCard> complainCardsCantPlayWell(Deck myDeck) {
// 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);
return brains.complainCardsCantPlayWell(myDeck); return brains.complainCardsCantPlayWell(myDeck);
} }
@@ -1161,8 +1114,10 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean effect) { public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean isActivatedSa) {
return ComputerUtilMana.payManaCost(player, sa, effect); // TODO Auto-generated method stub
ManaCostBeingPaid cost = isActivatedSa ? ComputerUtilMana.calculateManaCost(sa, false, 0) : new ManaCostBeingPaid(toPay);
return ComputerUtilMana.payManaCost(cost, sa, player);
} }
@Override @Override
@@ -1212,16 +1167,10 @@ public class PlayerControllerAi extends PlayerController {
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) { public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library); 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 Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic"); 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 (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
// If any Conspiracies are present, try not to choose the same name twice // If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name) // (otherwise the AI will spam the same name)
@@ -1310,27 +1259,6 @@ public class PlayerControllerAi extends PlayerController {
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces); 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 @Override
public List<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards) { public List<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards) {
// sort from best to worst // sort from best to worst
@@ -1353,7 +1281,8 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) { public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen,
List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList(); List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy(); Cost costSoFar = chosen.getPayCosts().copy();
@@ -1373,7 +1302,7 @@ public class PlayerControllerAi extends PlayerController {
} }
} }
if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) { if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenOptCosts.add(opt); chosenOptCosts.add(opt);
costSoFar.add(opt.getCost()); costSoFar.add(opt.getCost());
} }
@@ -1389,7 +1318,8 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @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.) // TODO: improve the logic depending on the keyword and the playability of the cost-modified SA (enough targets present etc.)
int chosenAmount = 0; int chosenAmount = 0;
@@ -1398,7 +1328,7 @@ public class PlayerControllerAi extends PlayerController {
for (int i = 0; i < max; i++) { for (int i = 0; i < max; i++) {
costSoFar.add(cost); costSoFar.add(cost);
SpellAbility fullCostSa = sa.copyWithDefinedCost(costSoFar); SpellAbility fullCostSa = sa.copyWithDefinedCost(costSoFar);
if (ComputerUtilCost.canPayCost(fullCostSa, player, sa.isTrigger())) { if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenAmount++; chosenAmount++;
} else { } else {
break; break;

View File

@@ -344,7 +344,7 @@ public class SpecialAiLogic {
return false; return false;
} }
final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source); final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower); final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);

View File

@@ -21,7 +21,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import forge.game.GameEntity;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
@@ -40,6 +39,7 @@ import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CardUtil; import forge.game.card.CardUtil;
@@ -112,63 +112,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 // Chain of Acid
public static class ChainOfAcid { public static class ChainOfAcid {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -216,9 +159,10 @@ public class SpecialCardAi {
final PhaseHandler ph = ai.getGame().getPhaseHandler(); final PhaseHandler ph = ai.getGame().getPhaseHandler();
final Combat combat = ai.getGame().getCombat(); final Combat combat = ai.getGame().getCombat();
Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa.getSubAbility()); Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa);
animated.addType("Creature");
if (sa.getHostCard().canReceiveCounters(CounterEnumType.P1P1)) { if (sa.getHostCard().canReceiveCounters(CounterEnumType.P1P1)) {
animated.addCounterInternal(CounterEnumType.P1P1, 2, ai, false, null); animated.addCounter(CounterEnumType.P1P1, 2, ai, false, null);
} }
boolean isOppEOT = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai; boolean isOppEOT = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated); boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
@@ -226,6 +170,10 @@ public class SpecialCardAi {
return isOppEOT || isValuableAttacker || isValuableBlocker; return isOppEOT || isValuableAttacker || isValuableBlocker;
} }
public static SpellAbility considerAnimating(final Player ai, final SpellAbility sa, final List<SpellAbility> options) {
return ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) ? options.get(0) : options.get(1);
}
} }
// Cursed Scroll // Cursed Scroll
@@ -379,7 +327,7 @@ public class SpecialCardAi {
} }
// Do not activate if damage will be prevented // 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; return false;
} }
@@ -400,7 +348,7 @@ public class SpecialCardAi {
} }
boolean isBlocking = combat.isBlocking(source); 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); CardCollection opposition = isBlocking ? combat.getAttackersBlockedBy(source) : combat.getBlockers(source);
int oppP = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetAttack); int oppP = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetAttack);
@@ -414,7 +362,7 @@ public class SpecialCardAi {
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) { if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY); int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY);
int totalDamageToPW = 0; int totalDamageToPW = 0;
for (Card atk :combat.getAttackersOf(combat.getDefenderByAttacker(source))) { for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) {
if (combat.isUnblocked(atk)) { if (combat.isUnblocked(atk)) {
totalDamageToPW += atk.getNetCombatDamage(); totalDamageToPW += atk.getNetCombatDamage();
} }
@@ -434,7 +382,7 @@ public class SpecialCardAi {
if (c.hasKeyword(Keyword.FIRST_STRIKE) || c.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (c.hasKeyword(Keyword.FIRST_STRIKE) || c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
oppHasFirstStrike = true; oppHasFirstStrike = true;
} }
if (!ComputerUtilCombat.combatantCantBeDestroyed(c.getController(), c)) { if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) {
oppCantDie = false; oppCantDie = false;
} }
} }
@@ -545,32 +493,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 // Force of Will
public static class ForceOfWill { public static class ForceOfWill {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -589,7 +511,7 @@ public class SpecialCardAi {
// Need to have something else in hand that is blue in addition to Force of Will itself, // Need to have something else in hand that is blue in addition to Force of Will itself,
// otherwise the AI will fail to play the card and the card will disappear from the pool // otherwise the AI will fail to play the card and the card will disappear from the pool
return false; return false;
} else if (!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 // We probably need a low-CMC card to exile to it, exiling a higher CMC spell may be suboptimal
// since the AI does not prioritize/value cards vs. permission at the moment. // since the AI does not prioritize/value cards vs. permission at the moment.
return false; return false;
@@ -643,28 +565,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 // Guilty Conscience
public static class GuiltyConscience { public static class GuiltyConscience {
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) { public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
@@ -709,8 +609,7 @@ public class SpecialCardAi {
} }
} }
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("ChangeNum"), sa);
sa.getParamOrDefault("ChangeNum", "1"), sa);
CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library),
Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName()))); Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName())));
Collections.sort(lib, CardLists.CmcComparatorInv); Collections.sort(lib, CardLists.CmcComparatorInv);
@@ -790,7 +689,7 @@ public class SpecialCardAi {
for (Card c1 : lib) { for (Card c1 : lib) {
if (c1.getName().equals(c.getName())) { if (c1.getName().equals(c.getName())) {
if (!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)) { && ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) {
// Try not to search for things we already have in hand or that we can't cast // Try not to search for things we already have in hand or that we can't cast
libPriorityList.add(c1); libPriorityList.add(c1);
@@ -906,7 +805,7 @@ public class SpecialCardAi {
for (Card gate : availableGates) for (Card gate : availableGates)
{ {
if (!Iterables.any(currentGates, CardPredicates.nameEquals(gate.getName()))) if (CardLists.filter(currentGates, CardPredicates.nameEquals(gate.getName())).isEmpty())
{ {
// Diversify our mana base // Diversify our mana base
return gate; return gate;
@@ -925,7 +824,7 @@ public class SpecialCardAi {
public static Card considerCardFromList(final CardCollection fetchList) { public static Card considerCardFromList(final CardCollection fetchList) {
for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) { for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) {
for (SpellAbility ab : c.getSpellAbilities()) { for (SpellAbility ab : c.getSpellAbilities()) {
if (ab.isActivatedAbility()) { if (ab.isAbility() && !ab.isTrigger()) {
Player controller = c.getController(); Player controller = c.getController();
boolean wasCaged = false; boolean wasCaged = false;
for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile), for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile),
@@ -995,7 +894,7 @@ public class SpecialCardAi {
} }
// Set PayX here to maximum value. // Set PayX here to maximum value.
int tokenSize = ComputerUtilCost.getMaxXValue(sa, ai, false); int tokenSize = ComputerUtilCost.getMaxXValue(sa, ai);
// Some basic strategy for Momir // Some basic strategy for Momir
if (tokenSize < 2) { if (tokenSize < 2) {
@@ -1012,39 +911,6 @@ public class SpecialCardAi {
} }
} }
// 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 // Necropotence
public static class Necropotence { public static class Necropotence {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1056,7 +922,7 @@ public class SpecialCardAi {
return false; // nothing to draw from the library 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 // Prefer Yawgmoth's Bargain because AI is generally better with it
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead? // TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
@@ -1074,7 +940,7 @@ public class SpecialCardAi {
} }
// TODO: Any other bad effects like that? // 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) if (ph.getNextTurn().equals(ai) && ph.is(PhaseType.MAIN2)
&& ai.getSpellsCastLastTurn() == 0 && ai.getSpellsCastLastTurn() == 0
@@ -1149,7 +1015,7 @@ public class SpecialCardAi {
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) { for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana(); ManaCost cost = testSa.getPayCosts().getTotalMana();
boolean canPayWithAvailableColors = cost.canBePaidWithAvailable(ColorSet.fromNames( boolean canPayWithAvailableColors = cost.canBePaidWithAvaliable(ColorSet.fromNames(
ComputerUtilCost.getAvailableManaColors(ai, sa.getHostCard())).getColor()); ComputerUtilCost.getAvailableManaColors(ai, sa.getHostCard())).getColor());
byte colorProfile = cost.getColorProfile(); byte colorProfile = cost.getColorProfile();
@@ -1207,33 +1073,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 // Price of Progress
public static class PriceOfProgress { public static class PriceOfProgress {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1328,51 +1167,6 @@ 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 // Sorin, Vengeful Bloodlord
public static class SorinVengefulBloodlord { public static class SorinVengefulBloodlord {
public static boolean consider(final Player ai, final SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1605,7 +1399,7 @@ public class SpecialCardAi {
if (topGY == null if (topGY == null
|| !topGY.isCreature() || !topGY.isCreature()
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) { || 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);
} }
} }
@@ -1694,7 +1488,7 @@ public class SpecialCardAi {
int maxHandSize = ai.getMaxHandSize(); int maxHandSize = ai.getMaxHandSize();
// TODO: Any other bad effects like that? // TODO: Any other bad effects like that?
boolean blackViseOTB = 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), // TODO: Consider effects like "whenever a player draws a card, he loses N life" (e.g. Nekusar, the Mindraiser),
// and effects that draw an additional card whenever a card is drawn. // and effects that draw an additional card whenever a card is drawn.
@@ -1770,7 +1564,7 @@ public class SpecialCardAi {
int CMC = ab.getPayCosts().getTotalMana() != null ? ab.getPayCosts().getTotalMana().getCMC() : 0; int CMC = ab.getPayCosts().getTotalMana() != null ? ab.getPayCosts().getTotalMana().getCMC() : 0;
int Xcount = ab.getPayCosts().getTotalMana() != null ? ab.getPayCosts().getTotalMana().countX() : 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()) { 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 // 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 // castable cards in graveyard requirements

View File

@@ -1,7 +1,12 @@
package forge.ai; package forge.ai;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.ICardFace; import forge.card.ICardFace;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
@@ -19,13 +24,8 @@ import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.SpellAbilityCondition;
import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/** /**
* Base class for API-specific AI logic * Base class for API-specific AI logic
* <p> * <p>
@@ -58,11 +58,6 @@ public abstract class SpellAbilityAi {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Cost cost = sa.getPayCosts(); 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())) { if (!checkConditions(ai, sa, sa.getConditions())) {
SpellAbility sub = sa.getSubAbility(); SpellAbility sub = sa.getSubAbility();
if (sub != null && !checkConditions(ai, sub, sub.getConditions())) { if (sub != null && !checkConditions(ai, sub, sub.getConditions())) {
@@ -72,12 +67,10 @@ public abstract class SpellAbilityAi {
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("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)) { if (!checkAiLogic(ai, sa, logic)) {
return false; return false;
} }
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) { if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return false; return false;
} }
} else { } else {
@@ -104,7 +97,7 @@ public abstract class SpellAbilityAi {
if (!con.getManaSpent().isEmpty()) { if (!con.getManaSpent().isEmpty()) {
// need to use ManaCostBeingPaid check, can't use Cost#canPay // need to use ManaCostBeingPaid check, can't use Cost#canPay
ManaCostBeingPaid paid = new ManaCostBeingPaid(new ManaCost(new ManaCostParser(con.getManaSpent()))); 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(""); con.setManaSpent("");
} }
} }
@@ -168,21 +161,22 @@ public abstract class SpellAbilityAi {
} }
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { 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) && !mandatory) {
if (!ComputerUtilCost.canPayCost(sa, aiPlayer, true) && !mandatory) {
return false; return false;
} }
// a mandatory SpellAbility with targeting but without candidates, // a mandatory SpellAbility with targeting but without candidates,
// does not need to go any deeper // does not need to go any deeper
if (sa.usesTargeting() && mandatory && sa.getTargetRestrictions().getNumCandidates(sa, true) == 0) { if (sa.usesTargeting() && mandatory && !sa.isTargetNumberValid()
return sa.isTargetNumberValid(); && !sa.getTargetRestrictions().hasCandidates(sa, true)) {
return false;
} }
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory); 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"))) { if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
return false; return false;
} }
@@ -194,7 +188,7 @@ public abstract class SpellAbilityAi {
* Handles the AI decision to play a triggered SpellAbility * Handles the AI decision to play a triggered SpellAbility
*/ */
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { 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; return true;
} }
@@ -213,7 +207,7 @@ public abstract class SpellAbilityAi {
// try to target opponent, then ally, then itself // try to target opponent, then ally, then itself
for (final Player p : players) { for (final Player p : players) {
if (sa.canTarget(p)) { if (p.canBeTargetedBy(sa) && sa.canTarget(p)) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(p); sa.getTargets().add(p);
return true; return true;
@@ -232,7 +226,7 @@ public abstract class SpellAbilityAi {
// sub-SpellAbility might use targets too // sub-SpellAbility might use targets too
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
// no Candidates, no adding to Stack // no Candidates, no adding to Stack
if (!sa.getTargetRestrictions().hasCandidates(sa)) { if (!sa.getTargetRestrictions().hasCandidates(sa, true)) {
return false; return false;
} }
// but if it does, it should override this function // but if it does, it should override this function
@@ -251,11 +245,11 @@ public abstract class SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean. * @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()) return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRestrictions().isSorcerySpeed()) || (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery()) || (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai)); || (sa.isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
} }
/** /**
@@ -291,6 +285,7 @@ public abstract class SpellAbilityAi {
return false; return false;
} }
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai); return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
} }

View File

@@ -34,7 +34,6 @@ public enum SpellApiToAi {
.put(ApiType.BidLife, BidLifeAi.class) .put(ApiType.BidLife, BidLifeAi.class)
.put(ApiType.Bond, BondAi.class) .put(ApiType.Bond, BondAi.class)
.put(ApiType.Branch, AlwaysPlayAi.class) .put(ApiType.Branch, AlwaysPlayAi.class)
.put(ApiType.Camouflage, ChooseCardAi.class)
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class) .put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
.put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class)
.put(ApiType.ChangeX, AlwaysPlayAi.class) .put(ApiType.ChangeX, AlwaysPlayAi.class)
@@ -50,17 +49,14 @@ public enum SpellApiToAi {
.put(ApiType.ChooseSource, ChooseSourceAi.class) .put(ApiType.ChooseSource, ChooseSourceAi.class)
.put(ApiType.ChooseType, ChooseTypeAi.class) .put(ApiType.ChooseType, ChooseTypeAi.class)
.put(ApiType.Clash, ClashAi.class) .put(ApiType.Clash, ClashAi.class)
.put(ApiType.ClassLevelUp, AlwaysPlayAi.class)
.put(ApiType.Cleanup, AlwaysPlayAi.class) .put(ApiType.Cleanup, AlwaysPlayAi.class)
.put(ApiType.Clone, CloneAi.class) .put(ApiType.Clone, CloneAi.class)
.put(ApiType.CompanionChoose, ChooseCompanionAi.class)
.put(ApiType.CopyPermanent, CopyPermanentAi.class) .put(ApiType.CopyPermanent, CopyPermanentAi.class)
.put(ApiType.CopySpellAbility, CopySpellAbilityAi.class) .put(ApiType.CopySpellAbility, CopySpellAbilityAi.class)
.put(ApiType.ControlPlayer, CannotPlayAi.class) .put(ApiType.ControlPlayer, CannotPlayAi.class)
.put(ApiType.ControlSpell, CannotPlayAi.class) .put(ApiType.ControlSpell, CannotPlayAi.class)
.put(ApiType.Counter, CounterAi.class) .put(ApiType.Counter, CounterAi.class)
.put(ApiType.DamageAll, DamageAllAi.class) .put(ApiType.DamageAll, DamageAllAi.class)
.put(ApiType.DayTime, DayTimeAi.class)
.put(ApiType.DealDamage, DamageDealAi.class) .put(ApiType.DealDamage, DamageDealAi.class)
.put(ApiType.Debuff, DebuffAi.class) .put(ApiType.Debuff, DebuffAi.class)
.put(ApiType.DeclareCombatants, CannotPlayAi.class) .put(ApiType.DeclareCombatants, CannotPlayAi.class)
@@ -87,7 +83,6 @@ public enum SpellApiToAi {
.put(ApiType.Explore, ExploreAi.class) .put(ApiType.Explore, ExploreAi.class)
.put(ApiType.Fight, FightAi.class) .put(ApiType.Fight, FightAi.class)
.put(ApiType.FlipACoin, FlipACoinAi.class) .put(ApiType.FlipACoin, FlipACoinAi.class)
.put(ApiType.FlipOntoBattlefield, FlipOntoBattlefieldAi.class)
.put(ApiType.Fog, FogAi.class) .put(ApiType.Fog, FogAi.class)
.put(ApiType.GainControl, ControlGainAi.class) .put(ApiType.GainControl, ControlGainAi.class)
.put(ApiType.GainControlVariant, ControlGainVariantAi.class) .put(ApiType.GainControlVariant, ControlGainVariantAi.class)
@@ -99,10 +94,8 @@ public enum SpellApiToAi {
.put(ApiType.Haunt, HauntAi.class) .put(ApiType.Haunt, HauntAi.class)
.put(ApiType.ImmediateTrigger, ImmediateTriggerAi.class) .put(ApiType.ImmediateTrigger, ImmediateTriggerAi.class)
.put(ApiType.Investigate, InvestigateAi.class) .put(ApiType.Investigate, InvestigateAi.class)
.put(ApiType.Learn, LearnAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class) .put(ApiType.LoseLife, LifeLoseAi.class)
.put(ApiType.LosesGame, GameLossAi.class) .put(ApiType.LosesGame, GameLossAi.class)
.put(ApiType.MakeCard, AlwaysPlayAi.class)
.put(ApiType.Mana, ManaEffectAi.class) .put(ApiType.Mana, ManaEffectAi.class)
.put(ApiType.ManaReflected, CannotPlayAi.class) .put(ApiType.ManaReflected, CannotPlayAi.class)
.put(ApiType.Manifest, ManifestAi.class) .put(ApiType.Manifest, ManifestAi.class)
@@ -111,6 +104,7 @@ public enum SpellApiToAi {
.put(ApiType.MoveCounter, CountersMoveAi.class) .put(ApiType.MoveCounter, CountersMoveAi.class)
.put(ApiType.MultiplePiles, CannotPlayAi.class) .put(ApiType.MultiplePiles, CannotPlayAi.class)
.put(ApiType.MultiplyCounter, CountersMultiplyAi.class) .put(ApiType.MultiplyCounter, CountersMultiplyAi.class)
.put(ApiType.MustAttack, MustAttackAi.class)
.put(ApiType.MustBlock, MustBlockAi.class) .put(ApiType.MustBlock, MustBlockAi.class)
.put(ApiType.Mutate, MutateAi.class) .put(ApiType.Mutate, MutateAi.class)
.put(ApiType.NameCard, ChooseCardNameAi.class) .put(ApiType.NameCard, ChooseCardNameAi.class)
@@ -139,17 +133,12 @@ public enum SpellApiToAi {
.put(ApiType.RemoveCounter, CountersRemoveAi.class) .put(ApiType.RemoveCounter, CountersRemoveAi.class)
.put(ApiType.RemoveCounterAll, CannotPlayAi.class) .put(ApiType.RemoveCounterAll, CannotPlayAi.class)
.put(ApiType.RemoveFromCombat, RemoveFromCombatAi.class) .put(ApiType.RemoveFromCombat, RemoveFromCombatAi.class)
.put(ApiType.RemoveFromGame, AlwaysPlayAi.class)
.put(ApiType.RemoveFromMatch, AlwaysPlayAi.class)
.put(ApiType.ReorderZone, AlwaysPlayAi.class) .put(ApiType.ReorderZone, AlwaysPlayAi.class)
.put(ApiType.Repeat, RepeatAi.class) .put(ApiType.Repeat, RepeatAi.class)
.put(ApiType.RepeatEach, RepeatEachAi.class) .put(ApiType.RepeatEach, RepeatEachAi.class)
.put(ApiType.ReplaceCounter, AlwaysPlayAi.class)
.put(ApiType.ReplaceEffect, AlwaysPlayAi.class) .put(ApiType.ReplaceEffect, AlwaysPlayAi.class)
.put(ApiType.ReplaceDamage, ReplaceDamageAi.class) .put(ApiType.ReplaceDamage, AlwaysPlayAi.class)
.put(ApiType.ReplaceMana, AlwaysPlayAi.class) .put(ApiType.ReplaceSplitDamage, AlwaysPlayAi.class)
.put(ApiType.ReplaceSplitDamage, ReplaceDamageAi.class)
.put(ApiType.ReplaceToken, AlwaysPlayAi.class)
.put(ApiType.RestartGame, RestartGameAi.class) .put(ApiType.RestartGame, RestartGameAi.class)
.put(ApiType.Reveal, RevealAi.class) .put(ApiType.Reveal, RevealAi.class)
.put(ApiType.RevealHand, RevealHandAi.class) .put(ApiType.RevealHand, RevealHandAi.class)
@@ -179,7 +168,6 @@ public enum SpellApiToAi {
.put(ApiType.UnattachAll, UnattachAllAi.class) .put(ApiType.UnattachAll, UnattachAllAi.class)
.put(ApiType.Untap, UntapAi.class) .put(ApiType.Untap, UntapAi.class)
.put(ApiType.UntapAll, UntapAllAi.class) .put(ApiType.UntapAll, UntapAllAi.class)
.put(ApiType.Venture, VentureAi.class)
.put(ApiType.Vote, VoteAi.class) .put(ApiType.Vote, VoteAi.class)
.put(ApiType.WinsGame, GameWinAi.class) .put(ApiType.WinsGame, GameWinAi.class)

View File

@@ -19,15 +19,17 @@ public class ActivateAbilityAi extends SpellAbilityAi {
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
// AI cannot use this properly until he can use SAs during Humans turn // AI cannot use this properly until he can use SAs during Humans turn
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); 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")); List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;
} }
if (!sa.usesTargeting()) { if (tgt == null) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (!defined.contains(opp)) { if (!defined.contains(opp)) {
@@ -35,20 +37,15 @@ public class ActivateAbilityAi extends SpellAbilityAi {
} }
} else { } else {
sa.resetTargets(); sa.resetTargets();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else {
return false;
}
} }
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return randomReturn; return randomReturn;
} }
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { 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 TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -61,6 +58,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
return defined.contains(opp); return defined.contains(opp);
} }
} else { } else {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(opp); sa.getTargets().add(opp);
@@ -72,11 +70,12 @@ public class ActivateAbilityAi extends SpellAbilityAi {
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) { public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI cannot use this properly until he can use SAs during Humans turn // AI cannot use this properly until he can use SAs during Humans turn
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
boolean randomReturn = true; boolean randomReturn = true;
if (!sa.usesTargeting()) { if (tgt == null) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(ai)) { if (defined.contains(ai)) {

View File

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

View File

@@ -24,7 +24,7 @@ import forge.game.zone.ZoneType;
public class AmassAi extends SpellAbilityAi { public class AmassAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) { protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army"); CardCollection aiArmies = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army"));
Card host = sa.getHostCard(); Card host = sa.getHostCard();
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -34,7 +34,7 @@ public class AmassAi extends SpellAbilityAi {
final String tokenScript = "b_0_0_zombie_army"; final String tokenScript = "b_0_0_zombie_army";
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa); 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, false);
if (token == null) { if (token == null) {
return false; return false;
@@ -98,3 +98,4 @@ public class AmassAi extends SpellAbilityAi {
return ComputerUtilCard.getBestAI(better); return ComputerUtilCard.getBestAI(better);
} }
} }

View File

@@ -1,30 +1,41 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.*;
import forge.ai.AiCardMemory;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardType; import forge.card.CardType;
import forge.card.ColorSet;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.AnimateEffectBase; import forge.game.ability.effects.AnimateEffectBase;
import forge.game.card.*; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardTraitChanges;
import forge.game.card.CardUtil;
import forge.game.cost.CostPutCounter; import forge.game.cost.CostPutCounter;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous; import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer; import forge.game.staticability.StaticAbilityLayer;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
* AbilityFactoryAnimate class. * AbilityFactoryAnimate class.
@@ -66,18 +77,19 @@ public class AnimateAi extends SpellAbilityAi {
SpellAbility topStack = game.getStack().peekAbility(); SpellAbility topStack = game.getStack().peekAbility();
if (topStack.getApi() == ApiType.Sacrifice) { if (topStack.getApi() == ApiType.Sacrifice) {
final String valid = topStack.getParamOrDefault("SacValid", "Card.Self"); 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); 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); ai.getWeakestOpponent(), topStack.getHostCard(), topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack, true)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
ComputerUtilCard.sortByEvaluateCreature(list); 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); Card animatedCopy = becomeAnimated(source, sa);
list.add(animatedCopy); list.add(animatedCopy);
list = CardLists.getValidCards(list, valid, ai.getWeakestOpponent(), topStack.getHostCard(), list = CardLists.getValidCards(list, valid.split(","), ai.getWeakestOpponent(), topStack.getHostCard(),
topStack); 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)) if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
&& list.contains(animatedCopy)) { && list.contains(animatedCopy)) {
return true; return true;
@@ -86,31 +98,31 @@ public class AnimateAi extends SpellAbilityAi {
} }
} }
// Don't use instant speed animate abilities before AI's COMBAT_BEGIN // 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) if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !"Permanent".equals(sa.getParam("Duration"))) { && !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false; return false;
} }
// Don't use instant speed animate abilities outside human's // Don't use instant speed animate abilities outside human's
// COMBAT_DECLARE_ATTACKERS or if no attackers // 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.is(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| ph.inCombat() && game.getCombat().getAttackersOf(ai).isEmpty())) { || game.getCombat() != null && game.getCombat().getAttackersOf(ai).isEmpty())) {
return false; return false;
} }
// Don't activate during MAIN2 unless this effect is permanent // 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; return false;
} }
// Don't animate if the AI won't attack anyway or use as a potential blocker // Don't animate if the AI won't attack anyway or use as a potential blocker
Player opponent = ai.getWeakestOpponent(); Player opponent = ai.getWeakestOpponent();
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise // Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
// the AI will waste resources // the AI will waste resources
boolean activateAsPotentialBlocker = "UntilYourNextTurn".equals(sa.getParam("Duration")) boolean activateAsPotentialBlocker = sa.hasParam("UntilYourNextTurn")
&& game.getPhaseHandler().getNextTurn() != ai && ai.getGame().getPhaseHandler().getNextTurn() != ai
&& source.isPermanent(); && source.isPermanent();
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6 if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
&& opponent.getZone(ZoneType.Battlefield).contains(CardPredicates.Presets.CREATURES) && Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
&& !sa.hasParam("AILogic") && !"Permanent".equals(sa.getParam("Duration")) && !activateAsPotentialBlocker) { && !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) {
return false; return false;
} }
return true; return true;
@@ -125,22 +137,14 @@ public class AnimateAi extends SpellAbilityAi {
return false; // what is this for? return false; // what is this for?
} }
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) { if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
if (!isAnimatedThisTurn(aiPlayer, source)) { if (!AnimateAi.isAnimatedThisTurn(aiPlayer, source)) {
rememberAnimatedThisTurn(aiPlayer, source); this.rememberAnimatedThisTurn(aiPlayer, source);
return true; // interrupt sacrifice return true; // interrupt sacrifice
} }
} }
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa)) { 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()) { if (!sa.usesTargeting()) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false; boolean bFlag = false;
@@ -151,7 +155,7 @@ public class AnimateAi extends SpellAbilityAi {
&& !c.isEquipping(); && !c.isEquipping();
// for creatures that could be improved (like Figure of Destiny) // 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; int power = -5;
if (sa.hasParam("Power")) { if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa); power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
@@ -168,13 +172,13 @@ public class AnimateAi extends SpellAbilityAi {
} }
} }
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) { 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; 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()) { if (sa.hasParam("Crew") && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature // Do not try to crew a vehicle which is already a creature
return false; return false;
@@ -193,7 +197,7 @@ public class AnimateAi extends SpellAbilityAi {
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() > if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) { c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) { 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; bFlag = true;
} }
} }
@@ -201,7 +205,7 @@ public class AnimateAi extends SpellAbilityAi {
} }
} }
if (bFlag) { if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard()); this.rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
} }
return bFlag; // All of the defined stuff is animated, not very useful return bFlag; // All of the defined stuff is animated, not very useful
} else { } else {
@@ -233,24 +237,18 @@ public class AnimateAi extends SpellAbilityAi {
return false; return false;
} }
Card toAnimate = ComputerUtilCard.getWorstAI(list); Card toAnimate = ComputerUtilCard.getWorstAI(list);
rememberAnimatedThisTurn(aiPlayer, toAnimate); this.rememberAnimatedThisTurn(aiPlayer, toAnimate);
sa.getTargets().add(toAnimate); sa.getTargets().add(toAnimate);
} }
return true; 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) { private boolean animateTgtAI(final SpellAbility sa) {
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler(); final PhaseHandler ph = ai.getGame().getPhaseHandler();
final String logic = sa.getParamOrDefault("AILogic", ""); final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker")
final boolean alwaysActivatePWAbility = sa.isPwAbility()
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class) && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
&& sa.usesTargeting() && sa.getTargetRestrictions() != null
&& sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0; && sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0;
final CardType types = new CardType(true); final CardType types = new CardType(true);
@@ -274,7 +272,7 @@ public class AnimateAi extends SpellAbilityAi {
Map<Card, Integer> data = Maps.newHashMap(); Map<Card, Integer> data = Maps.newHashMap();
for (final Card c : list) { for (final Card c : list) {
// don't use Permanent animate on something that would leave the field // don't use Permanent animate on something that would leave the field
if (c.hasSVar("EndOfTurnLeavePlay") && "Permanent".equals(sa.getParam("Duration"))) { if (c.hasSVar("EndOfTurnLeavePlay") && sa.hasParam("Permanent")) {
continue; continue;
} }
@@ -308,9 +306,9 @@ public class AnimateAi extends SpellAbilityAi {
// if its player turn, // if its player turn,
// check if its Permanent or that creature would attack // check if its Permanent or that creature would attack
if (ph.isPlayerTurn(ai)) { if (ph.isPlayerTurn(ai)) {
if (!"Permanent".equals(sa.getParam("Duration")) if (!sa.hasParam("Permanent")
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy) && !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
&& !"UntilHostLeavesPlay".equals(sa.getParam("Duration"))) { && !sa.hasParam("UntilHostLeavesPlay")) {
continue; continue;
} }
} }
@@ -341,35 +339,23 @@ public class AnimateAi extends SpellAbilityAi {
if (worst != null) { if (worst != null) {
if (worst.isLand()) { if (worst.isLand()) {
// e.g. Clan Guildmage, make sure we're not using the same land we want to animate to activate the ability // e.g. Clan Guildmage, make sure we're not using the same land we want to animate to activate the ability
holdAnimatedTillMain2(ai, worst); this.holdAnimatedTillMain2(ai, worst);
if (!ComputerUtilMana.canPayManaCost(sa, ai, 0, sa.isTrigger())) { if (!ComputerUtilMana.canPayManaCost(sa, ai, 0)) {
releaseHeldTillMain2(ai, worst); this.releaseHeldTillMain2(ai, worst);
return false; return false;
} }
} }
rememberAnimatedThisTurn(ai, worst); this.rememberAnimatedThisTurn(ai, worst);
sa.getTargets().add(worst); sa.getTargets().add(worst);
} }
return true; 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 // This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things // two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until // that animate a target. Those can just use AI:RemoveDeck:All until
// 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; return false;
} }
@@ -415,7 +401,7 @@ public class AnimateAi extends SpellAbilityAi {
// allow ChosenType - overrides anything else specified // allow ChosenType - overrides anything else specified
if (types.hasSubtype("ChosenType")) { if (types.hasSubtype("ChosenType")) {
types.clear(); types.clear();
types.add(source.getChosenType()); types.addAll(sa.getChosenType());
} }
final List<String> keywords = Lists.newArrayList(); final List<String> keywords = Lists.newArrayList();
@@ -442,15 +428,16 @@ public class AnimateAi extends SpellAbilityAi {
} }
// colors to be added or changed to // colors to be added or changed to
ColorSet finalColors = ColorSet.getNullColor(); String tmpDesc = "";
if (sa.hasParam("Colors")) { if (sa.hasParam("Colors")) {
final String colors = sa.getParam("Colors"); final String colors = sa.getParam("Colors");
if (colors.equals("ChosenColor")) { if (colors.equals("ChosenColor")) {
finalColors = ColorSet.fromNames(source.getChosenColors()); tmpDesc = CardUtil.getShortColorsString(sa.getChosenColors());
} else { } 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 // abilities to add to the animated being
final List<String> abilities = Lists.newArrayList(); final List<String> abilities = Lists.newArrayList();
@@ -482,13 +469,13 @@ public class AnimateAi extends SpellAbilityAi {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(","))); sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
} }
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalColors, AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc,
keywords, removeKeywords, hiddenKeywords, keywords, removeKeywords, hiddenKeywords,
abilities, triggers, replacements, stAbs, abilities, triggers, replacements, stAbs,
timestamp); timestamp);
// check if animate added static Abilities // check if animate added static Abilities
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0); CardTraitChanges traits = card.getChangedCardTraits().get(timestamp);
if (traits != null) { if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) { for (StaticAbility stAb : traits.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) { if ("Continuous".equals(stAb.getParam("Mode"))) {

View File

@@ -29,4 +29,4 @@ public class AnimateAllAi extends SpellAbilityAi {
return mandatory || canPlayAI(aiPlayer, sa); return mandatory || canPlayAI(aiPlayer, sa);
} }
} } // end class AbilityFactoryAnimate

View File

@@ -1,14 +1,16 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.ai.AiAttackController;
import forge.ai.AiCardMemory; import forge.ai.AiCardMemory;
import forge.ai.AiController; import forge.ai.AiController;
import forge.ai.AiProps; import forge.ai.AiProps;
@@ -49,7 +51,6 @@ import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
public class AttachAi extends SpellAbilityAi { public class AttachAi extends SpellAbilityAi {
@@ -113,8 +114,9 @@ public class AttachAi extends SpellAbilityAi {
} }
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) { if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. (Endless Scream and Venarian Gold) // Set PayX here to maximum value. (Endless Scream and Venarian
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); // Gold)
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
if (xPay == 0) { if (xPay == 0) {
return false; return false;
@@ -125,9 +127,15 @@ public class AttachAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source); final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
effectExile.setActivatingPlayer(ai); final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0);
final TargetRestrictions exile_tgt = effectExile.getTargetRestrictions(); 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(); return !targets.isEmpty();
} }
@@ -227,7 +235,7 @@ public class AttachAi extends SpellAbilityAi {
boolean hasFloatMana = ai.getManaPool().totalMana() > 0; boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
boolean willDiscardNow = game.getPhaseHandler().is(PhaseType.END_OF_TURN, ai) 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 willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
boolean willRespondToStack = canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack); boolean willRespondToStack = canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack);
boolean willCastEarly = MyRandom.percentTrue(chanceToCastEarly); boolean willCastEarly = MyRandom.percentTrue(chanceToCastEarly);
@@ -347,14 +355,14 @@ public class AttachAi extends SpellAbilityAi {
public boolean apply(final Card c) { public boolean apply(final Card c) {
//Check for cards that can be sacrificed in response //Check for cards that can be sacrificed in response
for (final SpellAbility ability : c.getAllSpellAbilities()) { for (final SpellAbility ability : c.getAllSpellAbilities()) {
if (ability.isActivatedAbility()) { if (ability.isAbility()) {
final Cost cost = ability.getPayCosts(); final Cost cost = ability.getPayCosts();
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (!(part instanceof CostSacrifice)) { if (!(part instanceof CostSacrifice)) {
continue; continue;
} }
CostSacrifice sacCost = (CostSacrifice) part; CostSacrifice sacCost = (CostSacrifice) part;
if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController(), false)) { if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController())) {
return false; return false;
} }
} }
@@ -425,7 +433,8 @@ public class AttachAi extends SpellAbilityAi {
SpellAbility auraSA = aura.getSpells().get(0); SpellAbility auraSA = aura.getSpells().get(0);
if (auraSA.getApi() == ApiType.Attach) { if (auraSA.getApi() == ApiType.Attach) {
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) { 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; return false;
} }
} }
@@ -446,15 +455,22 @@ public class AttachAi extends SpellAbilityAi {
/** /**
* Attach to player ai preferences. * Attach to player ai preferences.
*
* @param sa * @param sa
* the sa * the sa
* @param mandatory * @param mandatory
* the mandatory * the mandatory
* @param newParam TODO
*
* @return the player * @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 ("Curse".equals(sa.getParam("AILogic"))) {
if (!mandatory) { if (!mandatory) {
targetable.removeAll(aiPlayer.getAllies()); targetable.removeAll(aiPlayer.getAllies());
@@ -538,7 +554,12 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; 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()) { if (!evenBetterList.isEmpty()) {
betterList = evenBetterList; betterList = evenBetterList;
} }
@@ -712,15 +733,17 @@ public class AttachAi extends SpellAbilityAi {
*/ */
private static Card attachAISpecificCardPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, private static Card attachAISpecificCardPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource) { 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 Player ai = sa.getActivatingPlayer();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
Card chosen = null; Card chosen = null;
if ("Guilty Conscience".equals(sourceName)) { if ("Guilty Conscience".equals(sourceName)) {
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list); chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
} else if (sa.hasParam("AIValid")) { } else if ("Bonds of Faith".equals(sourceName)) {
// TODO: Make the AI recognize which cards to pump based on the card's abilities alone chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
chosen = doPumpOrCurseAILogic(ai, sa, list, sa.getParam("AIValid")); } else if ("Clutch of Undeath".equals(sourceName)) {
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
} }
// If Mandatory (brought directly into play without casting) gotta // If Mandatory (brought directly into play without casting) gotta
@@ -744,7 +767,7 @@ public class AttachAi extends SpellAbilityAi {
int powerBuff = 0; int powerBuff = 0;
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) { for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) { 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())) { if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
@@ -793,6 +816,7 @@ public class AttachAi extends SpellAbilityAi {
// grab Planeswalker first, then Creature // grab Planeswalker first, then Creature
// if Life < 5 grab Creature first, then Planeswalker. Lands, // if Life < 5 grab Creature first, then Planeswalker. Lands,
// Enchantments and Artifacts are probably "not good enough" // Enchantments and Artifacts are probably "not good enough"
} }
final Card c = ComputerUtilCard.getBestAI(list); final Card c = ComputerUtilCard.getBestAI(list);
@@ -910,7 +934,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, new Predicate<Card>() { prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
return c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c); return ComputerUtilCombat.canAttackNextTurn(c) && c.getNetPower() > 0;
} }
}); });
} }
@@ -949,6 +973,7 @@ public class AttachAi extends SpellAbilityAi {
return acceptableChoice(c, mandatory); return acceptableChoice(c, mandatory);
} }
/** /**
* Attach do trigger ai. * Attach do trigger ai.
* @param sa * @param sa
@@ -965,9 +990,9 @@ public class AttachAi extends SpellAbilityAi {
List<GameObject> targets = new ArrayList<>(); List<GameObject> targets = new ArrayList<>();
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) { if (tgt == null) {
targets = AbilityUtils.getDefinedObjects(card, sa.getParam("Defined"), sa); targets = AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
} else { } else {
attachPreference(sa, tgt, mandatory); AttachAi.attachPreference(sa, tgt, mandatory);
targets = sa.getTargets(); targets = sa.getTargets();
} }
@@ -1008,13 +1033,7 @@ public class AttachAi extends SpellAbilityAi {
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) { private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
GameObject o; GameObject o;
if (tgt.canTgtPlayer()) { if (tgt.canTgtPlayer()) {
List<Player> targetable = new ArrayList<>(); o = attachToPlayerAIPreferences(sa.getActivatingPlayer(), sa, mandatory);
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) {
targetable.add(player);
}
}
o = attachToPlayerAIPreferences(sa.getActivatingPlayer(), sa, mandatory, targetable);
} else { } else {
o = attachToCardAIPreferences(sa.getActivatingPlayer(), sa, mandatory); o = attachToCardAIPreferences(sa.getActivatingPlayer(), sa, mandatory);
} }
@@ -1104,12 +1123,14 @@ public class AttachAi extends SpellAbilityAi {
list.removeAll(toRemove); list.removeAll(toRemove);
if (magnetList != null) { if (magnetList != null) {
// Look for Heroic triggers // Look for Heroic triggers
if (magnetList.isEmpty() && sa.isSpell()) { if (magnetList.isEmpty() && sa.isSpell()) {
for (Card target : list) { for (Card target : list) {
for (Trigger t : target.getTriggers()) { for (Trigger t : target.getTriggers()) {
if (t.getMode() == TriggerType.SpellCast) { 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); magnetList.add(target);
break; break;
} }
@@ -1167,7 +1188,7 @@ public class AttachAi extends SpellAbilityAi {
if (affected == null) { if (affected == null) {
continue; continue;
} }
if (affected.contains(stCheck) || affected.contains("AttachedBy")) { if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility); totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility); totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
@@ -1272,7 +1293,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.isCreature()) { if (!c.isCreature()) {
return true; return true;
} }
return powerBonus + c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c); return ComputerUtilCombat.canAttackNextTurn(c) && powerBonus + c.getNetPower() > 0;
} }
}); });
} }
@@ -1317,28 +1338,6 @@ public class AttachAi extends SpellAbilityAi {
return null; 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 // is no attachment so no using attach
if (!attachSource.isAttachment()) { if (!attachSource.isAttachment()) {
return null; return null;
@@ -1352,9 +1351,21 @@ public class AttachAi extends SpellAbilityAi {
CardCollection list = null; CardCollection list = null;
if (tgt == null) { if (tgt == null) {
list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa); list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
} else { } 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()) { if (list.isEmpty()) {
@@ -1439,22 +1450,12 @@ public class AttachAi extends SpellAbilityAi {
* the logic * the logic
* @return the card * @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) { final Card attachSource, final String logic) {
// AI logic types that do not require a prefList and that evaluate the Player prefPlayer = ai.getWeakestOpponent();
// usefulness of attach action autonomously if ("Pump".equals(logic) || "Animate".equals(logic) || "Curiosity".equals(logic)) {
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)) {
prefPlayer = ai; prefPlayer = ai;
} else {
prefPlayer = AiAttackController.choosePreferredDefenderPlayer(ai);
} }
// Some ChangeType cards are beneficial, and PrefPlayer should be // Some ChangeType cards are beneficial, and PrefPlayer should be
// changed to represent that // changed to represent that
final List<Card> prefList; final List<Card> prefList;
@@ -1466,18 +1467,25 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filterControlledBy(list, prefPlayer); 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 there are no preferred cards, and not mandatory bail out
if (logic == null || prefList.isEmpty()) { if (prefList.isEmpty()) {
return chooseUnpreferred(mandatory, list); 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; Card c = null;
if ("GainControl".equals(logic)) { if ("GainControl".equals(logic)) {
c = attachAIControlPreference(sa, prefList, mandatory, attachSource); c = attachAIControlPreference(sa, prefList, mandatory, attachSource);
} else if ("Curse".equals(logic)) { } else if ("Curse".equals(logic)) {
c = attachAICursePreference(sa, prefList, mandatory, attachSource, ai); c = attachAICursePreference(sa, prefList, mandatory, attachSource, ai);
} else if ("Pump".equals(logic) || logic.startsWith("Move")) { } else if ("Pump".equals(logic)) {
c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource); c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource);
} else if ("Curiosity".equals(logic)) { } else if ("Curiosity".equals(logic)) {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource); c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
@@ -1555,10 +1563,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("Intimidate") || keyword.equals("Shadow")
|| keyword.equals("Flying") || keyword.equals("Horsemanship") || 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 // give evasive keywords to creatures that can attack and deal damage
boolean canBeBlocked = false; boolean canBeBlocked = false;
@@ -1570,8 +1579,8 @@ public class AttachAi extends SpellAbilityAi {
if (evasive) { if (evasive) {
return card.getNetCombatDamage() + powerBonus > 0 return card.getNetCombatDamage() + powerBonus > 0
&& canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card)
&& ComputerUtilCombat.canAttackNextTurn(card); && canBeBlocked;
} else if (keyword.equals("Haste")) { } else if (keyword.equals("Haste")) {
return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped() return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped()
&& card.getNetCombatDamage() + powerBonus > 0 && card.getNetCombatDamage() + powerBonus > 0
@@ -1587,12 +1596,12 @@ public class AttachAi extends SpellAbilityAi {
return card.getNetCombatDamage() + powerBonus > 0 return card.getNetCombatDamage() + powerBonus > 0
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true)); && (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.equals("First Strike")) { } 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)); && (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.startsWith("Flanking")) { } else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0 return card.getNetCombatDamage() + powerBonus > 0
&& canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card)
&& ComputerUtilCombat.canAttackNextTurn(card); && canBeBlocked;
} else if (keyword.startsWith("Bushido")) { } else if (keyword.startsWith("Bushido")) {
return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card)) return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card))
|| CombatUtil.canBlock(card, true); || CombatUtil.canBlock(card, true);
@@ -1634,8 +1643,10 @@ public class AttachAi extends SpellAbilityAi {
if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender") if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender")
|| keyword.endsWith("CARDNAME can't attack or block.")) { || keyword.endsWith("CARDNAME can't attack or block.")) {
return card.getNetCombatDamage() >= 1 && ComputerUtilCombat.canAttackNextTurn(card); return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
} else if (keyword.endsWith("CARDNAME can't block.")) { } 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); return CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) { } else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
for (SpellAbility ability : card.getSpellAbilities()) { for (SpellAbility ability : card.getSpellAbilities()) {
@@ -1645,10 +1656,10 @@ public class AttachAi extends SpellAbilityAi {
} }
return false; return false;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")) { } 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.") } else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) { || keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card); return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 2;
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) { } else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped(); return !card.isUntapped();
} }
@@ -1688,7 +1699,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.getController().equals(ai)) { if (!c.getController().equals(ai)) {
return false; return false;
} }
return c.isValid(type, ai, sa.getHostCard(), sa); return c.getType().hasCreatureType(type);
} }
}); });
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() { List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
@@ -1698,7 +1709,7 @@ public class AttachAi extends SpellAbilityAi {
if (c.getController().equals(ai)) { if (c.getController().equals(ai)) {
return false; return false;
} }
return !c.isValid(type, ai, sa.getHostCard(), sa) && !ComputerUtilCard.isUselessCreature(ai, c); return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
} }
}); });
@@ -1715,26 +1726,6 @@ public class AttachAi extends SpellAbilityAi {
return chosen; 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 @Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true; return true;
@@ -1742,11 +1733,11 @@ public class AttachAi extends SpellAbilityAi {
@Override @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, Map<String, Object> params) {
return attachGeneralAI(ai, sa, (List<Card>)options, !isOptional, sa.getHostCard(), sa.getParam("AILogic")); return attachToCardAIPreferences(ai, sa, true);
} }
@Override @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, Map<String, Object> params) {
return attachToPlayerAIPreferences(ai, sa, true, (List<Player>)options); return attachToPlayerAIPreferences(ai, sa, true);
} }
} }

View File

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

View File

@@ -33,7 +33,7 @@ public class BecomesBlockedAi extends SpellAbilityAi {
list = CardLists.getTargetableCards(list, sa); list = CardLists.getTargetableCards(list, sa);
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE); list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
while (sa.canAddMoreTarget()) { while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) {
Card choice = null; Card choice = null;
if (list.isEmpty()) { if (list.isEmpty()) {

View File

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

View File

@@ -50,6 +50,7 @@ public final class BondAi extends SpellAbilityAi {
return true; return true;
} // end bondCanPlayAI() } // end bondCanPlayAI()
@Override @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, Map<String, Object> params) {
return ComputerUtilCard.getBestCreatureAI(options); return ComputerUtilCard.getBestCreatureAI(options);

View File

@@ -1,5 +1,9 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -32,4 +36,11 @@ public class CanPlayAsDrawbackAi extends SpellAbilityAi {
return false; 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

@@ -10,7 +10,6 @@ import forge.game.card.Card;
import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
public class ChangeTargetsAi extends SpellAbilityAi { public class ChangeTargetsAi extends SpellAbilityAi {
@@ -41,20 +40,18 @@ public class ChangeTargetsAi extends SpellAbilityAi {
// nothing on stack, so nothing to target // nothing on stack, so nothing to target
return false; return false;
} }
final TargetChoices topTargets = topSa.getTargets();
final Card topHost = topSa.getHostCard();
if (sa.getTargets().size() != 0 && sa.isTrigger()) { if (sa.getTargets().size() != 0) {
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed // something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
return true; 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 // if this does not target at all or already targets host, no need to redirect it again
return false; return false;
} }
for (Card tgt : topTargets.getTargetCards()) { for (Card tgt : topSa.getTargets().getTargetCards()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) { 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), // 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 // 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 // make sure not to redirect our own abilities
return false; return false;
} }
@@ -81,24 +78,14 @@ public class ChangeTargetsAi extends SpellAbilityAi {
// e.g. Spellskite or a creature receiving its ability that requires Phyrexian mana P/U // e.g. Spellskite or a creature receiving its ability that requires Phyrexian mana P/U
int potentialDmg = ComputerUtil.predictDamageFromSpell(topSa, aiPlayer); int potentialDmg = ComputerUtil.predictDamageFromSpell(topSa, aiPlayer);
ManaCost normalizedMana = manaCost.getNormalizedMana(); 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 if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
&& topTargets.contains(aiPlayer)) { && topSa.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 // 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; 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.resetTargets();
sa.getTargets().add(topSa); sa.getTargets().add(topSa);
return true; return true;

View File

@@ -1,28 +1,55 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.*;
import forge.card.CardType; import forge.ai.AiBlockController;
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.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialAiLogic;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.GlobalRuleChange; import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.*; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostDiscard; import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter; import forge.game.cost.CostPutCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -30,14 +57,8 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollection;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
public class ChangeZoneAi extends SpellAbilityAi { public class ChangeZoneAi extends SpellAbilityAi {
/* /*
@@ -53,9 +74,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override @Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) { Card host = sa.getHostCard();
if (host != null && host.hasSVar("AIPreferenceOverride")) {
// currently used by SacAndUpgrade logic, might need simplification // currently used by SacAndUpgrade logic, might need simplification
sa.getHostCard().removeSVar("AIPreferenceOverride"); host.removeSVar("AIPreferenceOverride");
} }
if (aiLogic.equals("BeforeCombat")) { if (aiLogic.equals("BeforeCombat")) {
@@ -69,7 +92,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (aiLogic.equals("PriorityOptionalCost")) { } else if (aiLogic.equals("PriorityOptionalCost")) {
boolean highPriority = false; boolean highPriority = false;
// if we have more than one of these in hand, might not be worth waiting for optional cost payment on the additional copy // if we have more than one of these in hand, might not be worth waiting for optional cost payment on the additional copy
highPriority |= CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(sa.getHostCard().getName())) > 1; highPriority |= CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(host.getName())).size() > 1;
// if we are in danger in combat, no need to wait to pay the optional cost // if we are in danger in combat, no need to wait to pay the optional cost
highPriority |= ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS) highPriority |= ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()); && ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat());
@@ -104,6 +127,40 @@ public class ChangeZoneAi extends SpellAbilityAi {
return true; return true;
} else if (aiLogic.equals("Pongify")) { } else if (aiLogic.equals("Pongify")) {
return SpecialAiLogic.doPongifyLogic(ai, sa); return SpecialAiLogic.doPongifyLogic(ai, sa);
} else if (aiLogic.equals("Ashiok")) {
final int loyalty = host.getCurrentLoyalty() - 1;
CardCollectionView choices = new CardCollection();
for (int i = loyalty; i >= 0; i--) {
sa.setXManaCostPaid(i);
choices = ai.getGame().getCardsIn(ZoneType.listValueOf(sa.getParam("Origin")));
choices = CardLists.getValidCards(choices, sa.getParam("ChangeType"), host.getController(), host, sa);
if (!choices.isEmpty()) {
return true;
}
}
return false;
} else if (aiLogic.equals("BestCard")) {
CardCollectionView choices = ai.getGame().getCardsIn(ZoneType.listValueOf(sa.getParam("Origin")));
choices = CardLists.getValidCards(choices, sa.getParam("ChangeType"), host.getController(), host, sa);
if (!choices.isEmpty()) {
return true;
}
} else if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(host)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// minimum card advantage unless the hand will be fully reloaded
int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
if (numExiledWithSrc > curHandSize) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(host)) {
// 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
return true;
}
}
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
} }
return super.checkAiLogic(ai, sa, aiLogic); return super.checkAiLogic(ai, sa, aiLogic);
@@ -119,13 +176,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic.equals("Always")) { if (aiLogic.equals("Always")) {
return true; return true;
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc. } else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa); return this.doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc. } else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
return doSacAndReturnFromGraveLogic(aiPlayer, sa); return this.doSacAndReturnFromGraveLogic(aiPlayer, sa);
} else if (aiLogic.equals("Necropotence")) { } else if (aiLogic.equals("Necropotence")) {
return SpecialCardAi.Necropotence.consider(aiPlayer, sa); return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
} else if (aiLogic.equals("SameName")) { // Declaration in Stone } else if (aiLogic.equals("SameName")) { // Declaration in Stone
return doSameNameLogic(aiPlayer, sa); return this.doSameNameLogic(aiPlayer, sa);
} else if (aiLogic.equals("ReanimateAll")) { } else if (aiLogic.equals("ReanimateAll")) {
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa); return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
} else if (aiLogic.equals("TheScarabGod")) { } else if (aiLogic.equals("TheScarabGod")) {
@@ -140,9 +197,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa); return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
} else if (aiLogic.equals("Pongify")) { } else if (aiLogic.equals("Pongify")) {
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
} else if (aiLogic.equals("Ashiok")) {
return true; // If checkAiLogic returns true, then we should be good to go
} else if (aiLogic.equals("BestCard")) {
return true; // If checkAiLogic returns true, then we should be good to go
} else if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
return true; // If checkAiLogic returns true, then we should be good to go
} }
} }
if (sa.isHidden()) {
if (isHidden(sa)) {
return hiddenOriginCanPlayAI(aiPlayer, sa); return hiddenOriginCanPlayAI(aiPlayer, sa);
} }
return knownOriginCanPlayAI(aiPlayer, sa); return knownOriginCanPlayAI(aiPlayer, sa);
@@ -159,12 +223,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/ */
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.isHidden()) { if (isHidden(sa)) {
return hiddenOriginPlayDrawbackAI(aiPlayer, sa); return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
} }
return knownOriginPlayDrawbackAI(aiPlayer, sa); return knownOriginPlayDrawbackAI(aiPlayer, sa);
} }
private static boolean isHidden(SpellAbility sa) {
boolean hidden = sa.hasParam("Hidden");
if (!hidden && sa.hasParam("Origin")) {
hidden = ZoneType.isHidden(sa.getParam("Origin"));
}
return hidden;
}
/** /**
* <p> * <p>
* changeZoneTriggerAINoCost. * changeZoneTriggerAINoCost.
@@ -178,16 +251,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (sa.isReplacementAbility() && "Command".equals(sa.getParam("Destination")) && "ReplacedCard".equals(sa.getParam("Defined"))) { if (sa.isReplacementAbility() && "Command".equals(sa.getParam("Destination")) && "ReplacedCard".equals(sa.getParam("Defined"))) {
// Process the commander replacement effect ("return to Command zone instead") // Process the commander replacement effect ("return to Command zone instead")
return doReturnCommanderLogic(sa, aiPlayer); return doReturnCommanderLogic(sa, aiPlayer);
} }
if ("Always".equals(aiLogic)) { if ("Always".equals(sa.getParam("AILogic"))) {
return true; return true;
} else if ("IfNotBuffed".equals(aiLogic)) { } else if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) { if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
return true; // debuffed by opponent's auras to the level that it becomes useless return true; // debuffed by opponent's auras to the level that it becomes useless
} }
@@ -200,11 +271,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
return delta <= 0; return delta <= 0;
} else if ("SaviorOfOllenbock".equals(aiLogic)) {
return SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa);
} }
if (sa.isHidden()) { if (isHidden(sa)) {
return hiddenTriggerAI(aiPlayer, sa, mandatory); return hiddenTriggerAI(aiPlayer, sa, mandatory);
} }
return knownOriginTriggerAI(aiPlayer, sa, mandatory); return knownOriginTriggerAI(aiPlayer, sa, mandatory);
@@ -237,7 +306,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
ZoneType origin = null; ZoneType origin = null;
final Player opponent = AiAttackController.choosePreferredDefenderPlayer(ai); final Player opponent = ai.getWeakestOpponent();
boolean activateForCost = ComputerUtil.activateForCost(sa, ai); boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) { if (sa.hasParam("Origin")) {
@@ -278,7 +347,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.hasParam("Ninjutsu")) { if (sa.hasParam("Ninjutsu")) {
if (source.getType().isLegendary() if (source.getType().isLegendary()
&& !ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { && !ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
if (ai.getZone(ZoneType.Battlefield).contains(CardPredicates.nameEquals(source.getName()))) { final CardCollectionView list = ai.getCardsIn(ZoneType.Battlefield);
if (Iterables.any(list, CardPredicates.nameEquals(source.getName()))) {
return false; return false;
} }
} }
@@ -324,21 +394,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (tgt != null && tgt.canTgtPlayer()) { if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse(); boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opponent)) { if (isCurse && sa.canTarget(opponent)) {
sa.resetTargets();
sa.getTargets().add(opponent); sa.getTargets().add(opponent);
} else if (!isCurse && sa.canTarget(ai)) { } else if (!isCurse && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai); sa.getTargets().add(ai);
} }
if (!sa.isTargetNumberValid()) {
return false;
}
pDefined = sa.getTargets().getTargetPlayers(); pDefined = sa.getTargets().getTargetPlayers();
} else { } else {
if (sa.hasParam("DefinedPlayer")) { if (sa.hasParam("DefinedPlayer")) {
pDefined = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa); pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("DefinedPlayer"), sa);
} else { } else {
pDefined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
} }
} }
@@ -346,7 +411,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (type != null) { if (type != null) {
if (type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { if (type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. // Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(xPay); sa.setXManaCostPaid(xPay);
type = type.replace("X", Integer.toString(xPay)); type = type.replace("X", Integer.toString(xPay));
} }
@@ -387,14 +452,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
String num = sa.getParamOrDefault("ChangeNum", "1"); String num = sa.getParam("ChangeNum");
if (num != null) {
if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. // Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
if (xPay == 0) return false;
xPay = Math.min(xPay, list.size()); xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay); sa.setXManaCostPaid(xPay);
} }
}
if (sourceName.equals("Temur Sabertooth")) { if (sourceName.equals("Temur Sabertooth")) {
// activated bounce + pump // activated bounce + pump
@@ -408,6 +474,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
return canBouncePermanent(ai, sa, list) != null; return canBouncePermanent(ai, sa, list) != null;
} }
} }
if (ComputerUtil.playImmediately(ai, sa)) { if (ComputerUtil.playImmediately(ai, sa)) {
@@ -447,7 +514,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// if putting cards from hand to library and parent is drawing cards // if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something: // make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); final Player opp = aiPlayer.getWeakestOpponent();
if (tgt != null && tgt.canTgtPlayer()) { if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse(); boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opp)) { if (isCurse && sa.canTarget(opp)) {
@@ -499,14 +566,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
final String type = sa.getParam("ChangeType"); final String type = sa.getParam("ChangeType");
if (type != null && type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { if (type != null && type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. // Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(xPay); sa.setXManaCostPaid(xPay);
} }
Iterable<Player> pDefined; Iterable<Player> pDefined;
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if ((tgt != null) && tgt.canTgtPlayer()) { if ((tgt != null) && tgt.canTgtPlayer()) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Player opp = ai.getWeakestOpponent();
if (sa.isCurse()) { if (sa.isCurse()) {
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
@@ -624,7 +691,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return Card * @return Card
*/ */
private static Card chooseCreature(final Player ai, CardCollection list) { private static Card chooseCreature(final Player ai, CardCollection list) {
if (ComputerUtil.aiLifeInDanger(ai, false, 0)) { // Creating a new combat for testing purposes.
final Player opponent = ai.getWeakestOpponent();
Combat combat = new Combat(opponent);
for (Card att : opponent.getCreaturesInPlay()) {
combat.addAttacker(att, ai);
}
AiBlockController block = new AiBlockController(ai);
block.assignBlockersForCombat(combat);
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
// need something AI can cast now // need something AI can cast now
ComputerUtilCard.sortByEvaluateCreature(list); ComputerUtilCard.sortByEvaluateCreature(list);
for (Card c : list) { for (Card c : list) {
@@ -731,19 +807,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
} }
// predict Legendary cards already present
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
boolean nothingWillReturn = true;
for (final Card c : retrieval) {
if (!(c.getType().isLegendary() && ai.isCardInPlay(c.getName()))) {
nothingWillReturn = false;
break;
}
}
if (nothingWillReturn) {
return false;
}
}
} }
} }
@@ -766,7 +829,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN); return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
} }
if (sa.isHidden()) { if (isHidden(sa)) {
return true; return true;
} }
@@ -868,10 +931,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
// X controls the minimum targets // X controls the minimum targets
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) { if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. // Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
// TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore?
sa.setXManaCostPaid(xPay); sa.setXManaCostPaid(xPay);
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
} }
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
@@ -892,7 +956,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.isSpell()) { if (sa.isSpell()) {
list.remove(source); // spells can't target their own source, because it's actually in the stack zone list.remove(source); // spells can't target their own source, because it's actually in the stack zone
} }
//System.out.println("isPreferredTarget " + list);
if (sa.hasParam("AttachedTo")) { if (sa.hasParam("AttachedTo")) {
//System.out.println("isPreferredTarget att " + list);
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
@@ -904,26 +970,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
}); });
} //System.out.println("isPreferredTarget ok " + list);
if (sa.hasParam("AttachAfter")) {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
if (card.isValid(sa.getParam("AttachAfter"), ai, c, sa)) {
return true;
}
}
return false;
}
});
} }
if (list.size() < sa.getMinTargets()) { if (list.size() < sa.getMinTargets()) {
return false; return false;
} }
immediately = immediately || ComputerUtil.playImmediately(ai, sa); immediately |= ComputerUtil.playImmediately(ai, sa);
// Narrow down the list: // Narrow down the list:
if (origin.contains(ZoneType.Battlefield)) { if (origin.contains(ZoneType.Battlefield)) {
@@ -1021,9 +1075,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
if (blink) { if (blink) {
return c.isToken(); return c.isToken();
} } else {
return c.isToken() || c.getCMC() > 0; return c.isToken() || c.getCMC() > 0;
} }
}
}); });
} }
// TODO: Blink permanents with ETB triggers // TODO: Blink permanents with ETB triggers
@@ -1057,7 +1112,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
if (!immediately && (!game.getPhaseHandler().getNextTurn().equals(ai) if (!immediately && (!game.getPhaseHandler().getNextTurn().equals(ai)
|| game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN)) || game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa, ai) && !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa)
&& !ComputerUtil.activateForCost(sa, ai)) { && !ComputerUtil.activateForCost(sa, ai)) {
return false; return false;
} }
@@ -1097,6 +1152,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
if (!sa.hasParam("AITgtOwnCards")) { if (!sa.hasParam("AITgtOwnCards")) {
list = CardLists.filterControlledBy(list, ai.getOpponents()); list = CardLists.filterControlledBy(list, ai.getOpponents());
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@@ -1127,7 +1183,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
boolean doWithoutTarget = sa.isPwAbility() && sa.usesTargeting() boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.usesTargeting()
&& sa.getMinTargets() == 0 && sa.getMinTargets() == 0
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class); && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class);
@@ -1150,11 +1206,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!list.isEmpty()) { if (!list.isEmpty()) {
if (destination.equals(ZoneType.Battlefield) || origin.contains(ZoneType.Battlefield)) { if (destination.equals(ZoneType.Battlefield) || origin.contains(ZoneType.Battlefield)) {
// filter by MustTarget requirement final Card mostExpensive = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false);
CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
final Card mostExpensive = ComputerUtilCard.getMostExpensivePermanentAI(list);
if (mostExpensive.isCreature()) { if (mostExpensive.isCreature()) {
// if a creature is most expensive take the best one // if a creature is most expensive take the best one
if (destination.equals(ZoneType.Exile)) { if (destination.equals(ZoneType.Exile)) {
@@ -1182,11 +1234,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
} }
// Restore original list for next loop if filtered by MustTarget requirement
if (mustTargetFiltered) {
list = originalList;
}
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) { } else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
List<Card> nonLands = CardLists.getNotType(list, "Land"); List<Card> nonLands = CardLists.getNotType(list, "Land");
// Prefer to pull a creature, generally more useful for AI. // Prefer to pull a creature, generally more useful for AI.
@@ -1194,6 +1241,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (choice == null) { // Could not find a creature. if (choice == null) { // Could not find a creature.
if (ai.getLife() <= 5) { // Desperate? if (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon. // Get something AI can cast soon.
System.out.println("5 Life or less, trying to find something castable.");
CardLists.sortByCmcDesc(nonLands); CardLists.sortByCmcDesc(nonLands);
for (Card potentialCard : nonLands) { for (Card potentialCard : nonLands) {
if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), ai)) { if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), ai)) {
@@ -1203,6 +1251,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} else { } else {
// Get the best card in there. // Get the best card in there.
System.out.println("No creature and lots of life, finding something good.");
choice = ComputerUtilCard.getBestAI(nonLands); choice = ComputerUtilCard.getBestAI(nonLands);
} }
} }
@@ -1303,12 +1352,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static Card canBouncePermanent(final Player ai, SpellAbility sa, CardCollectionView list) { private static Card canBouncePermanent(final Player ai, SpellAbility sa, CardCollectionView list) {
Game game = ai.getGame(); Game game = ai.getGame();
// filter out untargetables // filter out untargetables
CardCollectionView aiPermanents = CardLists.filterControlledBy(list, ai); CardCollectionView aiPermanents = CardLists
.filterControlledBy(list, ai);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS); CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
// Felidar Guardian + Saheeli Rai combo support // Felidar Guardian + Saheeli Rai combo support
if (sa.getHostCard().getName().equals("Felidar Guardian")) { if (sa.getHostCard().getName().equals("Felidar Guardian")) {
CardCollectionView saheeli = ai.getCardsIn(ZoneType.Battlefield, "Saheeli Rai"); CardCollection saheeli = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Saheeli Rai"));
if (!saheeli.isEmpty()) { if (!saheeli.isEmpty()) {
return saheeli.get(0); return saheeli.get(0);
} }
@@ -1317,7 +1367,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Don't blink cards that will die. // Don't blink cards that will die.
aiPermanents = ComputerUtil.getSafeTargets(ai, sa, aiPermanents); aiPermanents = ComputerUtil.getSafeTargets(ai, sa, aiPermanents);
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, sa); final List<GameObject> objects = ComputerUtil
.predictThreatenedObjects(ai, sa);
final List<Card> threatenedTargets = new ArrayList<>(); final List<Card> threatenedTargets = new ArrayList<>();
@@ -1374,57 +1425,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
} }
// Reload Undying and Persist, get rid of -1/-1 counters, get rid of enemy auras if able
Card bestChoice = null;
int bestEval = 0;
for (Card c : aiPermanents) {
if (c.isCreature()) {
boolean hasValuableAttachments = false;
boolean hasOppAttachments = false;
int numNegativeCounters = 0;
int numTotalCounters = 0;
for (Card attached : c.getAttachedCards()) {
if (attached.isAura()) {
if (attached.getController() == c.getController()) {
hasValuableAttachments = true;
} else if (attached.getController().isOpponentOf(c.getController())) {
hasOppAttachments = true;
}
}
}
Map<CounterType, Integer> counters = c.getCounters();
for (CounterType ct : counters.keySet()) {
int amount = counters.get(ct);
if (ComputerUtil.isNegativeCounter(ct, c)) {
numNegativeCounters += amount;
}
numTotalCounters += amount;
}
if (hasValuableAttachments || (ComputerUtilCard.isUselessCreature(ai, c) && !hasOppAttachments)) {
continue;
}
Card considered = null;
if ((c.hasKeyword(Keyword.PERSIST) || c.hasKeyword(Keyword.UNDYING))
&& !ComputerUtilCard.hasActiveUndyingOrPersist(c)) {
considered = c;
} else if (hasOppAttachments || (numTotalCounters > 0 && numNegativeCounters > numTotalCounters / 2)) {
considered = c;
}
if (considered != null) {
int eval = ComputerUtilCard.evaluateCreature(c);
if (eval > bestEval) {
bestEval = eval;
bestChoice = considered;
}
}
}
}
if (bestChoice != null) {
return bestChoice;
}
return null; return null;
} }
@@ -1454,7 +1454,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
Card choice = null; Card choice = null;
if (!list.isEmpty()) { if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list); Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false);
if (mostExpensivePermanent.isCreature() if (mostExpensivePermanent.isCreature()
&& (destination.equals(ZoneType.Battlefield) || tgt.getZone().contains(ZoneType.Battlefield))) { && (destination.equals(ZoneType.Battlefield) || tgt.getZone().contains(ZoneType.Battlefield))) {
// if a creature is most expensive take the best // if a creature is most expensive take the best
@@ -1468,6 +1468,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (choice == null) { // Could not find a creature. if (choice == null) { // Could not find a creature.
if (ai.getLife() <= 5) { // Desperate? if (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon. // Get something AI can cast soon.
System.out.println("5 Life or less, trying to find something castable.");
CardLists.sortByCmcDesc(nonLands); CardLists.sortByCmcDesc(nonLands);
for (Card potentialCard : nonLands) { for (Card potentialCard : nonLands) {
if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), ai)) { if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), ai)) {
@@ -1477,6 +1478,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} else { } else {
// Get the best card in there. // Get the best card in there.
System.out.println("No creature and lots of life, finding something good.");
choice = ComputerUtilCard.getBestAI(nonLands); choice = ComputerUtilCard.getBestAI(nonLands);
} }
} }
@@ -1529,7 +1531,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return doExileCombatThreatLogic(ai, sa); return doExileCombatThreatLogic(ai, sa);
} }
if (!sa.usesTargeting()) { if (sa.getTargetRestrictions() == null) {
// Just in case of Defined cases // Just in case of Defined cases
if (!mandatory && sa.hasParam("AttachedTo")) { if (!mandatory && sa.hasParam("AttachedTo")) {
final List<Card> list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("AttachedTo"), sa); final List<Card> list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("AttachedTo"), sa);
@@ -1551,7 +1553,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return null; return null;
} }
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
String logic = sa.getParamOrDefault("AILogic", ""); String logic = sa.getParam("AILogic");
if ("NeverBounceItself".equals(logic)) { if ("NeverBounceItself".equals(logic)) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
if (fetchList.contains(source) && (fetchList.size() > 1 || !sa.getRootAbility().isMandatory())) { if (fetchList.contains(source) && (fetchList.size() > 1 || !sa.getRootAbility().isMandatory())) {
@@ -1574,8 +1576,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
multipleCardsToChoose.remove(0); multipleCardsToChoose.remove(0);
return choice; return choice;
} }
} else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList);
} }
} }
if (fetchList.isEmpty()) { if (fetchList.isEmpty()) {
@@ -1653,16 +1653,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Does AI need a land? // Does AI need a land?
CardCollectionView hand = decider.getCardsIn(ZoneType.Hand); CardCollectionView hand = decider.getCardsIn(ZoneType.Hand);
if (!Iterables.any(hand, Presets.LANDS) && CardLists.count(decider.getCardsIn(ZoneType.Battlefield), Presets.LANDS) < 4) { if (CardLists.filter(hand, Presets.LANDS).isEmpty() && CardLists.filter(decider.getCardsIn(ZoneType.Battlefield), Presets.LANDS).size() < 4) {
boolean canCastSomething = false; boolean canCastSomething = false;
for (Card cardInHand : hand) { for (Card cardInHand : hand) {
canCastSomething = canCastSomething || ComputerUtilMana.hasEnoughManaSourcesToCast(cardInHand.getFirstSpellAbility(), decider); canCastSomething |= ComputerUtilMana.hasEnoughManaSourcesToCast(cardInHand.getFirstSpellAbility(), decider);
} }
if (!canCastSomething) { if (!canCastSomething) {
System.out.println("Pulling a land as there are none in hand, less than 4 on the board, and nothing in hand is castable.");
c = basicManaFixing(decider, fetchList); c = basicManaFixing(decider, fetchList);
} }
} }
if (c == null) { if (c == null) {
System.out.println("Don't need a land or none available; trying for a creature.");
fetchList = CardLists.getNotType(fetchList, "Land"); fetchList = CardLists.getNotType(fetchList, "Land");
// Prefer to pull a creature, generally more useful for AI. // Prefer to pull a creature, generally more useful for AI.
c = chooseCreature(decider, CardLists.filter(fetchList, CardPredicates.Presets.CREATURES)); c = chooseCreature(decider, CardLists.filter(fetchList, CardPredicates.Presets.CREATURES));
@@ -1670,6 +1672,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (c == null) { // Could not find a creature. if (c == null) { // Could not find a creature.
if (decider.getLife() <= 5) { // Desperate? if (decider.getLife() <= 5) { // Desperate?
// Get something AI can cast soon. // Get something AI can cast soon.
System.out.println("5 Life or less, trying to find something castable.");
CardLists.sortByCmcDesc(fetchList); CardLists.sortByCmcDesc(fetchList);
for (Card potentialCard : fetchList) { for (Card potentialCard : fetchList) {
if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), decider)) { if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), decider)) {
@@ -1679,6 +1682,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} else { } else {
// Get the best card in there. // Get the best card in there.
System.out.println("No creature and lots of life, finding something good.");
c = ComputerUtilCard.getBestAI(fetchList); c = ComputerUtilCard.getBestAI(fetchList);
} }
} }
@@ -1696,21 +1700,19 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (card.isToken()) { if (card.isToken()) {
return false; return false;
} }
if (card.isCreature() && ComputerUtilCard.isUselessCreature(decider, card)) { if (card.isCreature() && ComputerUtilCard.isUselessCreature(decider, card)) {
return true; return true;
} } else if (card.isEquipped()) {
if (card.isEquipped()) {
return false; return false;
} } else if (card.isEnchanted()) {
if (card.isEnchanted()) {
for (Card enc : card.getEnchantedBy()) { for (Card enc : card.getEnchantedBy()) {
if (enc.getOwner().isOpponentOf(decider)) { if (enc.getOwner().isOpponentOf(decider)) {
return true; return true;
} }
} }
return false; return false;
} } else if (card.hasCounters()) {
if (card.hasCounters()) {
if (card.isPlaneswalker()) { if (card.isPlaneswalker()) {
int maxLoyaltyToConsider = 2; int maxLoyaltyToConsider = 2;
int loyaltyDiff = 2; int loyaltyDiff = 2;
@@ -1751,10 +1753,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
return true; return true;
} }
@Override @Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) { public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Called when looking for creature to attach aura or equipment // Called when looking for creature to attach aura or equipment
return AttachAi.attachGeneralAI(ai, sa, (List<Card>)options, !isOptional, sa.getHostCard(), sa.getParam("AILogic")); return ComputerUtilCard.getBestAI(options);
} }
/* (non-Javadoc) /* (non-Javadoc)
@@ -1762,20 +1765,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/ */
@Override @Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) { public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
// Called when attaching Aura to player or adding creature to combat // Currently only used by Curse of Misfortunes, so this branch should never get hit
if (params != null && params.containsKey("Attacker")) { // But just in case it does, just select the first option
return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options)); return Iterables.getFirst(options, null);
}
return AttachAi.attachToPlayerAIPreferences(ai, sa, true, (List<Player>)options);
}
@Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) {
return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options));
}
// should not be reached
return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params);
} }
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) { private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
@@ -1897,7 +1889,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
List<String> toRemove = Lists.newArrayList(); List<String> toRemove = Lists.newArrayList();
for (final String name : values.keySet()) { for (final String name : values.keySet()) {
if (!Iterables.any(oppList, CardPredicates.nameEquals(name))) { if (CardLists.filter(oppList, CardPredicates.nameEquals(name)).isEmpty()) {
toRemove.add(name); toRemove.add(name);
} }
} }
@@ -1912,7 +1904,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
})); }));
} }
if (!data.isEmpty()) {
// JAVA 1.8 use Map.Entry.comparingByValue() somehow // JAVA 1.8 use Map.Entry.comparingByValue() somehow
Map.Entry<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() { Map.Entry<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() {
@Override @Override
@@ -1931,7 +1922,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.getTargets().add(c); sa.getTargets().add(c);
return true; return true;
} }
}
return false; return false;
} }
@@ -1981,7 +1971,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
int highestEval = -1; int highestEval = -1;
if (combat.getAttackingPlayer().isOpponentOf(aiPlayer)) { if (combat.getAttackingPlayer().isOpponentOf(aiPlayer)) {
for (Card attacker : combat.getAttackers()) { for (Card attacker : combat.getAttackers()) {
if (sa.canTarget(attacker)) { if (sa.canTarget(attacker) && attacker.canBeTargetedBy(sa)) {
int eval = ComputerUtilCard.evaluateCreature(attacker); int eval = ComputerUtilCard.evaluateCreature(attacker);
if (combat.isUnblocked(attacker)) { if (combat.isUnblocked(attacker)) {
eval += 100; // TODO: make this smarter eval += 100; // TODO: make this smarter
@@ -1995,7 +1985,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else { } else {
// either the current AI player or one of its teammates is attacking, the opponent(s) are blocking // either the current AI player or one of its teammates is attacking, the opponent(s) are blocking
for (Card blocker : combat.getAllBlockers()) { for (Card blocker : combat.getAllBlockers()) {
if (sa.canTarget(blocker)) { if (sa.canTarget(blocker) && blocker.canBeTargetedBy(sa)) {
if (blocker.getController().isOpponentOf(aiPlayer)) { // TODO: unnecessary sanity check? if (blocker.getController().isOpponentOf(aiPlayer)) { // TODO: unnecessary sanity check?
int eval = ComputerUtilCard.evaluateCreature(blocker); int eval = ComputerUtilCard.evaluateCreature(blocker);
if (eval > highestEval) { if (eval > highestEval) {
@@ -2014,143 +2004,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
public static Card doExilePreferenceLogic(final Player aiPlayer, final SpellAbility sa, CardCollection fetchList) {
// Filter by preference. If nothing is preferred, choose the best/worst/random target for the opponent
// or for the AI depending on the settings. This logic must choose at least something if at all possible,
// since it's called from chooseSingleCard.
if (fetchList.isEmpty()) {
return null; // there was nothing to choose at all
}
final Card host = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
final String valid = logic.split(":")[1];
final boolean isCurse = logic.contains("Curse");
final boolean isOwnOnly = logic.contains("OwnOnly");
final boolean isWorstChoice = logic.contains("Worst");
final boolean isRandomChoice = logic.contains("Random");
if (logic.endsWith("HighestCMC")) {
return ComputerUtilCard.getMostExpensivePermanentAI(fetchList);
} else if (logic.contains("MostProminent")) {
CardCollection scanList = new CardCollection();
if (logic.endsWith("OwnType")) {
scanList.addAll(aiPlayer.getCardsIn(ZoneType.Library));
scanList.addAll(aiPlayer.getCardsIn(ZoneType.Hand));
} else if (logic.endsWith("OppType")) {
// this assumes that the deck list is known to the AI before the match starts,
// so it's possible to figure out what remains in library/hand if you know what's
// in graveyard, exile, etc.
scanList.addAll(aiPlayer.getOpponents().getCardsIn(ZoneType.Library));
scanList.addAll(aiPlayer.getOpponents().getCardsIn(ZoneType.Hand));
}
if (logic.contains("NonLand")) {
scanList = CardLists.filter(scanList, Predicates.not(Presets.LANDS));
}
if (logic.contains("NonExiled")) {
CardCollection exiledBy = new CardCollection();
for (Card exiled : aiPlayer.getGame().getCardsIn(ZoneType.Exile)) {
if (exiled.getExiledWith() != null && exiled.getExiledWith().getName().equals(host.getName())) {
exiledBy.add(exiled);
}
}
scanList = CardLists.filter(scanList, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
if (exiledBy.isEmpty()) {
return true;
}
for (Card c : exiledBy) {
return !c.getType().sharesCardTypeWith(card.getType());
}
return true;
}
});
}
CardCollectionView graveyardList = aiPlayer.getGame().getCardsIn(ZoneType.Graveyard);
Set<CardType.CoreType> presentTypes = new HashSet<>();
for (Card inGrave : graveyardList) {
for(CardType.CoreType type : inGrave.getType().getCoreTypes()) {
presentTypes.add(type);
}
}
final Map<CardType.CoreType, Integer> typesInDeck = Maps.newHashMap();
for (final Card c : scanList) {
for (CardType.CoreType ct : c.getType().getCoreTypes()) {
if (presentTypes.contains(ct)) {
Integer count = typesInDeck.get(ct);
if (count == null) {
count = 0;
}
typesInDeck.put(ct, count + 1);
}
}
}
int max = 0;
CardType.CoreType maxType = CardType.CoreType.Land;
for (final Map.Entry<CardType.CoreType, Integer> entry : typesInDeck.entrySet()) {
final CardType.CoreType type = entry.getKey();
if (max < entry.getValue()) {
max = entry.getValue();
maxType = type;
}
}
final CardType.CoreType determinedMaxType = maxType;
CardCollection preferredList = CardLists.filter(fetchList, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return card.getType().hasType(determinedMaxType);
}
});
CardCollection preferredOppList = CardLists.filter(preferredList, CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()));
if (!preferredOppList.isEmpty()) {
return Aggregates.random(preferredOppList);
} else if (!preferredList.isEmpty()) {
return Aggregates.random(preferredList);
}
return Aggregates.random(fetchList);
}
CardCollection preferredList = CardLists.filter(fetchList, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
boolean playerPref = true;
if (isCurse) {
playerPref = card.getController().isOpponentOf(aiPlayer);
} else if (isOwnOnly) {
playerPref = card.getController().equals(aiPlayer) || !card.getController().isOpponentOf(aiPlayer);
}
if (!playerPref) {
return false;
}
return card.isValid(valid, aiPlayer, host, sa); // for things like ExilePreference:Land.Basic
}
});
if (!preferredList.isEmpty()) {
if (isRandomChoice) {
return Aggregates.random(preferredList);
}
return isWorstChoice ? ComputerUtilCard.getWorstAI(preferredList) : ComputerUtilCard.getBestAI(preferredList);
} else {
if (isRandomChoice) {
return Aggregates.random(preferredList);
}
return isWorstChoice ? ComputerUtilCard.getWorstAI(fetchList) : ComputerUtilCard.getBestAI(fetchList);
}
}
private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) { private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) {
// Determines if the controller of each potential target can negate the ChangeZone effect // Determines if the controller of each potential target can negate the ChangeZone effect
// by paying the Unless cost. Returns the list of targets that can be saved that way. // by paying the Unless cost. Returns the list of targets that can be saved that way.
@@ -2168,12 +2021,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean setPayX = false; boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true; setPayX = true;
toPay = ComputerUtilCost.getMaxXValue(sa, ai, true); // TODO use ComputerUtilCost.getMaxXValue if able
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else { } else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
} }
if (toPay == 0 || toPay <= usableManaSources) { if (toPay == 0) {
canBeSaved.add(potentialTgt);
}
if (toPay <= usableManaSources) {
canBeSaved.add(potentialTgt); canBeSaved.add(potentialTgt);
} }

View File

@@ -106,16 +106,17 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} }
return false; return false;
} else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) { } else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) {
PlayerCollection players = ai.getOpponents(); PlayerCollection players = new PlayerCollection();
players.addAll(ai.getOpponents());
players.add(ai); players.add(ai);
int maxSize = 1; int maxSize = 1;
for (Player player : players) { for (Player player : players) {
Player bestTgt = null; Player bestTgt = null;
if (player.canBeTargetedBy(sa)) { if (player.canBeTargetedBy(sa)) {
int numGY = CardLists.count(player.getCardsIn(ZoneType.Graveyard), CardCollectionView cardsGY = CardLists.filter(player.getCardsIn(ZoneType.Graveyard),
CardPredicates.Presets.CREATURES); CardPredicates.Presets.CREATURES);
if (numGY > maxSize) { if (cardsGY.size() > maxSize) {
maxSize = numGY; maxSize = cardsGY.size();
bestTgt = player; bestTgt = player;
} }
} }
@@ -137,17 +138,13 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
return true; return true;
} else { } else {
// search targetable Opponents // search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(), PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
}
// get the one with the most handsize // 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 // set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) { if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(oppTarget); sa.getTargets().add(oppTarget);
} else { } else {
@@ -155,24 +152,24 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} }
} }
} else if (origin.equals(ZoneType.Battlefield)) { } else if (origin.equals(ZoneType.Battlefield)) {
// this statement is assuming the AI is trying to use this spell offensively // this statement is assuming the AI is trying to use this spell
// if the AI is using it defensively, then something else needs to occur // 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 only creatures are affected evaluate both lists and pass only
// if human creatures are more valuable // if human creatures are more valuable
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
// search targetable Opponents // search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
}
// get the one with the most in graveyard // get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most // 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 // set the target
if (oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) { if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(oppTarget); sa.getTargets().add(oppTarget);
} else { } else {
@@ -237,7 +234,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa)); AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target // set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) { if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(oppTarget); sa.getTargets().add(oppTarget);
} else { } else {
@@ -248,26 +245,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
&& !ComputerUtil.isPlayingReanimator(ai); && !ComputerUtil.isPlayingReanimator(ai);
} }
} else if (origin.equals(ZoneType.Exile)) { } else if (origin.equals(ZoneType.Exile)) {
String logic = sa.getParam("AILogic"); // TODO: nothing to do here at the moment
return false;
if (logic != null && logic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// 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 (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
return true;
}
}
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
}
} else if (origin.equals(ZoneType.Stack)) { } else if (origin.equals(ZoneType.Stack)) {
// time stop can do something like this: // time stop can do something like this:
// Origin$ Stack | Destination$ Exile | SubAbility$ DBSkip // Origin$ Stack | Destination$ Exile | SubAbility$ DBSkip
@@ -279,8 +258,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (destination.equals(ZoneType.Battlefield)) { if (destination.equals(ZoneType.Battlefield)) {
if (sa.getParam("GainControl") != null) { if (sa.getParam("GainControl") != null) {
// Check if the cards are valuable enough // Check if the cards are valuable enough
if (CardLists.getNotType(oppType, "Creature").size() == 0 if ((CardLists.getNotType(oppType, "Creature").size() == 0)
&& CardLists.getNotType(computerType, "Creature").size() == 0) { && (CardLists.getNotType(computerType, "Creature").size() == 0)) {
if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
.evaluateCreatureList(oppType)) < 400) { .evaluateCreatureList(oppType)) < 400) {
return false; return false;
@@ -370,7 +349,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's // TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and // Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
// there is no specific AI to support playing it in a smarter way. Feel free to expand. // there is no specific AI to support playing it in a smarter way. Feel free to expand.
return 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()); CardCollectionView humanType = CardLists.filterControlledBy(ai.getGame().getCardsIn(origin), ai.getOpponents());
@@ -384,22 +363,15 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) { if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
// search targetable Opponents // search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return true;
}
return false;
}
// get the one with the most handsize // 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 // set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty() || mandatory) { if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(oppTarget); sa.getTargets().add(oppTarget);
} else { } else {
@@ -429,24 +401,16 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} else if (origin.equals(ZoneType.Graveyard)) { } else if (origin.equals(ZoneType.Graveyard)) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
// search targetable Opponents // search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); final Iterable<Player> oppList = Iterables.filter(ai.getOpponents(),
PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return true;
}
return sa.isTargetNumberValid();
}
// get the one with the most in graveyard // get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most // 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)); AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target // set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty() || mandatory) { if (oppTarget != null && !oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(oppTarget); sa.getTargets().add(oppTarget);
} else { } else {

View File

@@ -1,6 +1,5 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -24,23 +23,21 @@ import forge.util.collect.FCollection;
public class CharmAi extends SpellAbilityAi { public class CharmAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); // sa is Entwined, no need for extra logic
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
Collections.shuffle(choices);
final int num;
final int min;
if (sa.isEntwine()) { if (sa.isEntwine()) {
num = min = choices.size(); return true;
} else {
num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num;
} }
final Card source = sa.getHostCard();
final int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now? boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
// Reset the chosen list otherwise it will be locked in forever by earlier calls // Reset the chosen list otherwise it will be locked in forever by earlier calls
sa.setChosenList(null); sa.setChosenList(null);
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
List<AbilitySub> chosenList; List<AbilitySub> chosenList;
if (!ai.equals(sa.getActivatingPlayer())) { if (!ai.equals(sa.getActivatingPlayer())) {
@@ -69,9 +66,6 @@ public class CharmAi extends SpellAbilityAi {
if (timingRight) { if (timingRight) {
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null // Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
chosenList = chooseOptionsAi(choices, ai, true, num, min, sa.hasParam("CanRepeatModes")); chosenList = chooseOptionsAi(choices, ai, true, num, min, sa.hasParam("CanRepeatModes"));
if (chosenList.isEmpty() && min != 0) {
return false;
}
} else { } else {
return false; return false;
} }
@@ -98,7 +92,7 @@ public class CharmAi extends SpellAbilityAi {
} }
} }
if (isTrigger && chosenList.size() < min) { 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); choices.removeAll(chosenList);
for (AbilitySub sub : choices) { for (AbilitySub sub : choices) {
sub.setActivatingPlayer(ai); sub.setActivatingPlayer(ai);
@@ -165,7 +159,7 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(allyTainted ? gain : lose); chosenList.add(allyTainted ? gain : lose);
} else if (oppTainted || ai.getGame().isCardInPlay("Rain of Gore")) { } else if (oppTainted || ai.getGame().isCardInPlay("Rain of Gore")) {
// Rain of Gore does negate lifegain, so don't benefit the others // 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 // but if ai cant gain life, the effects are negated
chosenList.add(ai.canGainLife() ? lose : gain); chosenList.add(ai.canGainLife() ? lose : gain);
} else if (ai.getGame().isCardInPlay("Sulfuric Vortex")) { } else if (ai.getGame().isCardInPlay("Sulfuric Vortex")) {
@@ -183,13 +177,13 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(gain); 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 // 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); chosenList.add(oppTainted ? lose : gain);
} else if (allyTainted) { } else if (allyTainted) {
// Tainted Remedy negation logic, try gain instead of lose // Tainted Remedy negation logic, try gain instead of lose
// because negation does turn it into lose for opponents // because negation does turn it into lose for opponents
boolean oppCritical = false; 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. // but only if ai doesn't kill itself with that.
if (aiLife != 14) { if (aiLife != 14) {
for (Player p : opponents) { for (Player p : opponents) {
@@ -203,7 +197,7 @@ public class CharmAi extends SpellAbilityAi {
} else { } else {
// normal logic, try to gain life if its critical // normal logic, try to gain life if its critical
boolean oppCritical = false; 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. // but only if ai doesn't kill itself with that.
if (aiLife != 12) { if (aiLife != 12) {
for (Player p : opponents) { for (Player p : opponents) {
@@ -230,8 +224,6 @@ public class CharmAi extends SpellAbilityAi {
goodChoice = sub; goodChoice = sub;
} else { } else {
// Standard canPlayAi() // Standard canPlayAi()
sub.setActivatingPlayer(ai);
sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone());
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) { if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub); chosenList.add(sub);
if (chosenList.size() == min) { if (chosenList.size() == min) {
@@ -254,23 +246,4 @@ public class CharmAi extends SpellAbilityAi {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents, Map<String, Object> params) { public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents, Map<String, Object> params) {
return Aggregates.random(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

@@ -7,8 +7,8 @@ import java.util.Map;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
@@ -20,10 +20,8 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
@@ -41,7 +39,8 @@ public class ChooseCardAi extends SpellAbilityAi {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
// search targetable Opponents // 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()) { if (oppList.isEmpty()) {
return false; return false;
@@ -63,7 +62,7 @@ public class ChooseCardAi extends SpellAbilityAi {
if (sa.hasParam("ChoiceZone")) { if (sa.hasParam("ChoiceZone")) {
choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone")); choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
} }
CardCollectionView choices = game.getCardsIn(choiceZone); CardCollectionView choices = ai.getGame().getCardsIn(choiceZone);
if (sa.hasParam("Choices")) { 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, sa);
} }
@@ -74,8 +73,10 @@ public class ChooseCardAi extends SpellAbilityAi {
return !choices.isEmpty(); return !choices.isEmpty();
} else if (aiLogic.equals("AtLeast2") || aiLogic.equals("BestBlocker")) { } else if (aiLogic.equals("AtLeast2") || aiLogic.equals("BestBlocker")) {
return choices.size() >= 2; return choices.size() >= 2;
} else if (aiLogic.equals("Clone")) { } else if (aiLogic.equals("Clone") || aiLogic.equals("Vesuva")) {
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary"; final String filter = aiLogic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
choices = CardLists.getValidCards(choices, filter, host.getController(), host, sa); choices = CardLists.getValidCards(choices, filter, host.getController(), host, sa);
return !choices.isEmpty(); return !choices.isEmpty();
} else if (aiLogic.equals("Never")) { } else if (aiLogic.equals("Never")) {
@@ -95,24 +96,12 @@ public class ChooseCardAi extends SpellAbilityAi {
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > ref; return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > ref;
} }
}); });
return !choices.isEmpty();
} else if (aiLogic.equals("Ashiok")) {
final int loyalty = host.getCounters(CounterEnumType.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);
if (!choices.isEmpty()) {
return true;
}
}
return !choices.isEmpty(); return !choices.isEmpty();
} else if (aiLogic.equals("RandomNonLand")) { } 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, sa).isEmpty();
} else if (aiLogic.equals("Duneblast")) { } else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay(); CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = AiAttackController.choosePreferredDefenderPlayer(ai).getCreaturesInPlay(); CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE); aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE); oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
@@ -145,16 +134,6 @@ public class ChooseCardAi extends SpellAbilityAi {
return checkApiLogic(ai, sa); 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) /* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/ */
@@ -176,23 +155,28 @@ public class ChooseCardAi extends SpellAbilityAi {
} }
choice = ComputerUtilCard.getBestAI(ownChoices); choice = ComputerUtilCard.getBestAI(ownChoices);
} else if (logic.equals("BestBlocker")) { } else if (logic.equals("BestBlocker")) {
if (Iterables.any(options, Presets.UNTAPPED)) { if (!CardLists.filter(options, Presets.UNTAPPED).isEmpty()) {
options = CardLists.filter(options, Presets.UNTAPPED); options = CardLists.filter(options, Presets.UNTAPPED);
} }
choice = ComputerUtilCard.getBestCreatureAI(options); choice = ComputerUtilCard.getBestCreatureAI(options);
} else if (logic.equals("Clone")) { } else if (logic.equals("Clone") || logic.equals("Vesuva")) {
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary"; final String filter = logic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary" :
CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa); "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) { if (!newOptions.isEmpty()) {
options = newOptions; options = newOptions;
} }
choice = ComputerUtilCard.getBestAI(options); choice = ComputerUtilCard.getBestAI(options);
if (logic.equals("Vesuva") && "Vesuva".equals(choice.getName())) {
choice = null;
}
} else if ("RandomNonLand".equals(logic)) { } else if ("RandomNonLand".equals(logic)) {
options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host, sa); options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host, sa);
choice = Aggregates.random(options); choice = Aggregates.random(options);
} else if (logic.equals("Untap")) { } else if (logic.equals("Untap")) {
final String filter = "Permanent.YouCtrl,Permanent.tapped"; 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()) { if (!newOptions.isEmpty()) {
options = newOptions; options = newOptions;
} }
@@ -255,6 +239,7 @@ public class ChooseCardAi extends SpellAbilityAi {
return true; return true;
} }
}); });
System.out.println("Tangle Wire" + options + " - " + betterList);
if (!betterList.isEmpty()) { if (!betterList.isEmpty()) {
choice = betterList.get(0); choice = betterList.get(0);
} else { } else {

View File

@@ -7,7 +7,6 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.StaticData; import forge.StaticData;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi; import forge.ai.SpecialCardAi;
@@ -35,7 +34,9 @@ public class ChooseCardNameAi extends SpellAbilityAi {
} }
String logic = sa.getParam("AILogic"); 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); return SpecialCardAi.CursedScroll.consider(ai, sa);
} }
@@ -43,7 +44,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) { if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(AiAttackController.choosePreferredDefenderPlayer(ai)); sa.getTargets().add(ai.getWeakestOpponent());
} else { } else {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} }
@@ -63,6 +64,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
*/ */
@Override @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, Map<String, Object> params) {
return ComputerUtilCard.getBestAI(options); return ComputerUtilCard.getBestAI(options);
} }

View File

@@ -44,7 +44,7 @@ public class ChooseColorAi extends SpellAbilityAi {
return false; return false;
} }
// Set PayX here to maximum value. // Set PayX here to maximum value.
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false)); sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai));
return true; return true;
} }
@@ -54,8 +54,11 @@ public class ChooseColorAi extends SpellAbilityAi {
if (logic.equals("MostExcessOpponentControls")) { if (logic.equals("MostExcessOpponentControls")) {
for (byte color : MagicColor.WUBRG) { for (byte color : MagicColor.WUBRG) {
CardCollectionView ailist = ai.getColoredCardsInPlay(color); CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView opplist = ai.getStrongestOpponent().getColoredCardsInPlay(color); 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); int excess = ComputerUtilCard.evaluatePermanentList(opplist) - ComputerUtilCard.evaluatePermanentList(ailist);
if (excess > 4) { if (excess > 4) {

View File

@@ -1,9 +1,9 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom; import forge.util.MyRandom;
public class ChooseEvenOddAi extends SpellAbilityAi { public class ChooseEvenOddAi extends SpellAbilityAi {
@@ -13,9 +13,10 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
if (!sa.hasParam("AILogic")) { if (!sa.hasParam("AILogic")) {
return false; return false;
} }
if (sa.usesTargeting()) { TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); Player opp = aiPlayer.getWeakestOpponent();
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {
@@ -32,3 +33,4 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
} }
} }

View File

@@ -8,6 +8,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
@@ -27,11 +28,11 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantBeCast;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
@@ -53,9 +54,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
} }
} else if ("GideonBlackblade".equals(aiLogic)) { } else if ("GideonBlackblade".equals(aiLogic)) {
return SpecialCardAi.GideonBlackblade.consider(ai, sa); return SpecialCardAi.GideonBlackblade.consider(ai, sa);
} else if ("AtOppEOT".equals(aiLogic)) { } else if ("SoulEcho".equals(aiLogic)) {
PhaseHandler ph = ai.getGame().getPhaseHandler(); return doTriggerAINoCost(ai, sa, true);
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
} else if ("Always".equals(aiLogic)) { } else if ("Always".equals(aiLogic)) {
return true; return true;
} }
@@ -72,7 +72,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
*/ */
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa); return checkApiLogic(aiPlayer, sa);
} }
@Override @Override
@@ -121,7 +121,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
SpellAbility paycost = new SpellAbility.EmptySa(sa.getHostCard(), player); SpellAbility paycost = new SpellAbility.EmptySa(sa.getHostCard(), player);
paycost.setPayCosts(unless); paycost.setPayCosts(unless);
if (ComputerUtilCost.willPayUnlessCost(sp, player, unless, false, new FCollection<>(player)) if (ComputerUtilCost.willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
&& ComputerUtilCost.canPayCost(paycost, player, true)) { && ComputerUtilCost.canPayCost(paycost, player)) {
return sp; return sp;
} }
} }
@@ -160,22 +160,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
} }
} }
return others; 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)) { } else if ("Fatespinner".equals(logic)) {
SpellAbility skipDraw = null, /*skipMain = null,*/ skipCombat = null; SpellAbility skipDraw = null, /*skipMain = null,*/ skipCombat = null;
for (final SpellAbility sp : spells) { for (final SpellAbility sp : spells) {
@@ -199,6 +183,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// Todo if hand is empty or mostly empty, skip main phase // Todo if hand is empty or mostly empty, skip main phase
// Todo if hand has gas, skip draw // Todo if hand has gas, skip draw
return Aggregates.random(spells); return Aggregates.random(spells);
} else if ("SinProdder".equals(logic)) { } else if ("SinProdder".equals(logic)) {
SpellAbility allow = null, deny = null; SpellAbility allow = null, deny = null;
for (final SpellAbility sp : spells) { for (final SpellAbility sp : spells) {
@@ -245,12 +230,11 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
return allow; return allow;
} }
//if Iona does prevent from casting, allow it to draw SpellAbility firstSpell = imprinted.getFirstSpellAbility();
for (final Card io : player.getCardsIn(ZoneType.Battlefield, "Iona, Shield of Emeria")) { // check if something would prevent it from casting
if (imprinted.getColor().hasAnyColor(MagicColor.fromName(io.getChosenColor()))) { if (firstSpell == null || StaticAbilityCantBeCast.cantBeCastAbility(firstSpell, imprinted, owner)) {
return allow; return allow;
} }
}
if (dmg == 0) { if (dmg == 0) {
// If CMC = 0, mill it! // If CMC = 0, mill it!
@@ -294,7 +278,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// if host would put into the battlefield attacking // if host would put into the battlefield attacking
if (combat != null && combat.isAttacking(host)) { if (combat != null && combat.isAttacking(host)) {
final Player defender = combat.getDefenderPlayerByAttacker(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 counterSA;
} }
return tokenSA; return tokenSA;
@@ -305,7 +289,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
for (final Player opp : player.getOpponents()) { for (final Player opp : player.getOpponents()) {
if (CombatUtil.canAttack(copy, opp) && if (CombatUtil.canAttack(copy, opp) &&
opp.canLoseLife() && opp.canLoseLife() &&
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true)) !ComputerUtilCard.canBeBlockedProfitably(opp, copy))
return counterSA; return counterSA;
} }
} }
@@ -361,7 +345,9 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
int bestGuessDamage = totalCMC * 3 / revealedCards.size(); int bestGuessDamage = totalCMC * 3 / revealedCards.size();
return life <= bestGuessDamage ? spells.get(0) : spells.get(1); return life <= bestGuessDamage ? spells.get(0) : spells.get(1);
} else if ("SoulEcho".equals(logic)) { } else if ("SoulEcho".equals(logic)) {
return sa.getHostCard().getController().getLife() < 10 ? spells.get(0) : Aggregates.random(spells); Player target = sa.getTargetingPlayer();
int life = target.getLife();
return life < 10 ? spells.get(0) : Aggregates.random(spells);
} else if ("Pump".equals(logic) || "BestOption".equals(logic)) { } else if ("Pump".equals(logic) || "BestOption".equals(logic)) {
List<SpellAbility> filtered = Lists.newArrayList(); List<SpellAbility> filtered = Lists.newArrayList();
// filter first for the spells which can be done // filter first for the spells which can be done
@@ -378,6 +364,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
} else if ("Riot".equals(logic)) { } else if ("Riot".equals(logic)) {
SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1); SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
return preferHasteForRiot(sa, player) ? hasteSA : counterSA; return preferHasteForRiot(sa, player) ? hasteSA : counterSA;
} else if ("CrawlingBarrens".equals(logic)) {
return SpecialCardAi.CrawlingBarrens.considerAnimating(player, sa, spells);
} }
return spells.get(0); // return first choice if no logic found return spells.get(0); // return first choice if no logic found
} }

View File

@@ -1,51 +1,22 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom; import forge.util.MyRandom;
public class ChooseNumberAi extends SpellAbilityAi { public class ChooseNumberAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", ""); if (!sa.hasParam("AILogic")) {
if (aiLogic.isEmpty()) {
return false; 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;
} }
} TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
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()) {
sa.resetTargets(); sa.resetTargets();
Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); Player opp = aiPlayer.getWeakestOpponent();
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {

View File

@@ -8,7 +8,6 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -26,6 +25,7 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
@@ -51,9 +51,10 @@ public class ChooseSourceAi extends SpellAbilityAi {
} }
} }
if (sa.usesTargeting()) { final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); Player opp = ai.getWeakestOpponent();
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {
@@ -65,7 +66,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (sa.getParam("AILogic").equals("NeedsPrevention")) { if (sa.getParam("AILogic").equals("NeedsPrevention")) {
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
final SpellAbility topStack = game.getStack().peekAbility(); 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; return false;
} }
final ApiType threatApi = topStack.getApi(); final ApiType threatApi = topStack.getApi();
@@ -109,6 +110,8 @@ public class ChooseSourceAi extends SpellAbilityAi {
return true; return true;
} }
@Override @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, Map<String, Object> params) {
if ("NeedsPrevention".equals(sa.getParam("AILogic"))) { if ("NeedsPrevention".equals(sa.getParam("AILogic"))) {
@@ -174,7 +177,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
final Card source = si.getSourceCard(); final Card source = si.getSourceCard();
final SpellAbility abilityOnStack = si.getSpellAbility(true); 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; continue;
} }
final ApiType threatApi = abilityOnStack.getApi(); final ApiType threatApi = abilityOnStack.getApi();

View File

@@ -1,12 +1,10 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.AiCardMemory; import forge.ai.AiCardMemory;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -23,7 +21,6 @@ import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
@@ -37,7 +34,6 @@ public class ChooseTypeAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
return doMirrorEntityLogic(aiPlayer, sa); return doMirrorEntityLogic(aiPlayer, sa);
} }
return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty();
} else if ("MostProminentOppControls".equals(sa.getParam("AILogic"))) { } else if ("MostProminentOppControls".equals(sa.getParam("AILogic"))) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty(); return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
} }
@@ -58,7 +54,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
return false; return false;
} }
int maxX = ComputerUtilMana.determineLeftoverMana(sa, aiPlayer, false); int maxX = ComputerUtilMana.determineLeftoverMana(sa, aiPlayer);
int avgPower = 0; int avgPower = 0;
// predict the opposition // predict the opposition
@@ -107,42 +103,12 @@ public class ChooseTypeAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean isCurse = sa.isCurse();
if (sa.usesTargeting()) { 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(); 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); 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?
}
} else { } else {
for (final Player p : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa)) { 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; return false;
} }
} }

View File

@@ -33,6 +33,7 @@ public class ClashAi extends SpellAbilityAi {
return legalAction; return legalAction;
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
@@ -92,7 +93,7 @@ public class ClashAi extends SpellAbilityAi {
if ("Creature".equals(valid)) { if ("Creature".equals(valid)) {
// Springjack Knight // Springjack Knight
// TODO: Whirlpool Whelm also uses creature targeting but it's trickier to support // TODO: Whirlpool Whelm also uses creature targeting but it's trickier to support
CardCollectionView aiCreats = ai.getCreaturesInPlay(); CardCollectionView aiCreats = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
CardCollectionView oppCreats = CardLists.filter(ai.getOpponents().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); Card tgt = aiCreats.isEmpty() ? ComputerUtilCard.getWorstCreatureAI(oppCreats) : ComputerUtilCard.getBestCreatureAI(aiCreats);

View File

@@ -3,17 +3,13 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -42,7 +38,7 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer // TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?" // "Can I use this to block something?"
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) { if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false; return false;
} }
@@ -53,10 +49,10 @@ public class CloneAi extends SpellAbilityAi {
boolean bFlag = false; boolean bFlag = false;
for (final Card c : defined) { 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) // 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; int power = -5;
if (sa.hasParam("Power")) { if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa); power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
@@ -72,7 +68,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; return false;
} }
} else { } else {
@@ -92,23 +89,17 @@ public class CloneAi extends SpellAbilityAi {
chance = cloneTgtAI(sa); chance = cloneTgtAI(sa);
} }
return chance; return chance;
} }
@Override @Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
Card host = sa.getHostCard();
boolean chance = true; boolean chance = true;
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
chance = cloneTgtAI(sa); 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: // Improve AI for triggers. If source is a creature with:
@@ -180,25 +171,25 @@ public class CloneAi extends SpellAbilityAi {
@Override @Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
Player targetedPlayer, Map<String, Object> params) { Player targetedPlayer, Map<String, Object> params) {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final String name = host.getName();
final Player ctrl = host.getController(); final Player ctrl = host.getController();
final Card cloneTarget = getCloneTarget(sa); final Card cloneTarget = getCloneTarget(sa);
final boolean isOpp = cloneTarget.getController().isOpponentOf(sa.getActivatingPlayer()); 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")); final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.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 // TODO: rewrite this block so that this is done somehow more elegantly
if (canCloneLegendary) { if (canCloneLegendary) {
filter = filter.replace(".nonLegendary+", ".").replace(".nonLegendary", ""); 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()) { if (!newOptions.isEmpty()) {
options = newOptions; options = newOptions;
} }
@@ -210,13 +201,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); Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);
if (isVesuva && "Vesuva".equals(choice.getName())) {
choice = null;
}
return choice; return choice;
} }
@@ -243,8 +233,8 @@ public class CloneAi extends SpellAbilityAi {
// don't use instant speed clone abilities outside computers // don't use instant speed clone abilities outside computers
// Combat_Begin step // Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN) if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa, ai) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && sa.hasParam("Duration")) { && !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false; return false;
} }
@@ -255,6 +245,6 @@ public class CloneAi extends SpellAbilityAi {
} }
// don't activate during main2 unless this effect is permanent // 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 com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -30,8 +29,9 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
CardCollection list = CardCollection list =
CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); 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 // AI won't try to grab cards that are filtered out of AI decks on
// purpose
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
@@ -65,7 +65,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
} }
} else { } else {
if (mandatory) { if (mandatory) {
return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid(); return chkAIDrawback(sa, aiPlayer);
} else { } else {
return canPlayAI(aiPlayer, sa); return canPlayAI(aiPlayer, sa);
} }
@@ -81,10 +81,6 @@ public class ControlExchangeAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); 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 // for TrigTwoTargets logic, only get the opponents' cards for the first target
CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ? CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ?
aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) : aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) :
@@ -101,12 +97,8 @@ public class ControlExchangeAi extends SpellAbilityAi {
Card best = ComputerUtilCard.getBestAI(list); 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 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); final Card object = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa).get(0);
// TODO add evaluate Land if able // TODO add evaluate Land if able
final Card realBest = ComputerUtilCard.getBestAI(Lists.newArrayList(best, object)); final Card realBest = ComputerUtilCard.getBestAI(Lists.newArrayList(best, object));
@@ -117,6 +109,9 @@ public class ControlExchangeAi extends SpellAbilityAi {
} }
} }
// add best Target
sa.getTargets().add(best);
// second target needed (the AI's own worst) // second target needed (the AI's own worst)
if ("TrigTwoTargets".equals(sa.getParam("AILogic"))) { if ("TrigTwoTargets".equals(sa.getParam("AILogic"))) {
return doTrigTwoTargetsLogic(aiPlayer, sa, best); return doTrigTwoTargetsLogic(aiPlayer, sa, best);

View File

@@ -26,7 +26,6 @@ import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -40,7 +39,6 @@ import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
@@ -69,6 +67,7 @@ import forge.util.Aggregates;
public class ControlGainAi extends SpellAbilityAi { public class ControlGainAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(final Player ai, final SpellAbility sa) { protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
final List<String> lose = Lists.newArrayList(); final List<String> lose = Lists.newArrayList();
if (sa.hasParam("LoseControl")) { if (sa.hasParam("LoseControl")) {
@@ -110,7 +109,8 @@ public class ControlGainAi extends SpellAbilityAi {
} }
} }
// 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") if (lose.contains("EOT")
&& ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !sa.isTrigger()) { && !sa.isTrigger()) {
@@ -122,7 +122,10 @@ public class ControlGainAi extends SpellAbilityAi {
return true; 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); 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>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
if (!sa.canTarget(c)) { if (!c.canBeTargetedBy(sa)) {
return false; return false;
} }
if (sa.isTrigger()) { if (sa.isTrigger()) {
return true; return true;
} }
if (!c.canBeControlledBy(ai)) {
return false;
}
// do not take perm control on something that leaves the play end of turn // do not take perm control on something that leaves the play end of turn
if (!lose.contains("EOT") && c.hasSVar("EndOfTurnLeavePlay")) { if (!lose.contains("EOT") && c.hasSVar("EndOfTurnLeavePlay")) {
return false; return false;
@@ -198,7 +197,7 @@ public class ControlGainAi extends SpellAbilityAi {
} }
} }
while (sa.canAddMoreTarget()) { while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
Card t = null; Card t = null;
if (list.isEmpty()) { if (list.isEmpty()) {
@@ -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) { while (t == null) {
// filter by MustTarget requirement
CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
if (planeswalkers > 0) { if (planeswalkers > 0) {
t = ComputerUtilCard.getBestPlaneswalkerAI(list); t = ComputerUtilCard.getBestPlaneswalkerAI(list);
} else if (creatures > 0) { } else if (creatures > 0) {
@@ -226,9 +220,9 @@ public class ControlGainAi extends SpellAbilityAi {
} else if (lands > 0) { } else if (lands > 0) {
t = ComputerUtilCard.getBestLandAI(list); t = ComputerUtilCard.getBestLandAI(list);
} else if (enchantments > 0) { } else if (enchantments > 0) {
t = ComputerUtilCard.getBestEnchantmentAI(list, sa, false); t = ComputerUtilCard.getBestEnchantmentAI(list, sa, true);
} else { } else {
t = ComputerUtilCard.getMostExpensivePermanentAI(list); t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
} }
if (t != null) { if (t != null) {
@@ -244,11 +238,6 @@ public class ControlGainAi extends SpellAbilityAi {
enchantments--; enchantments--;
} }
// Restore original list for next loop if filtered by MustTarget requirement
if (mustTargetFiltered) {
list = originalList;
}
if (!sa.canTarget(t)) { if (!sa.canTarget(t)) {
list.remove(t); list.remove(t);
t = null; t = null;
@@ -265,25 +254,17 @@ public class ControlGainAi extends SpellAbilityAi {
} }
return true; return true;
} }
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) { if (sa.getTargetRestrictions() == null) {
if (mandatory) { if (mandatory) {
return true; return true;
} }
} else { } else {
if(sa.hasParam("TargetingPlayer") || (!this.canPlayAI(ai, sa) && mandatory)) { 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;
}
List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;
@@ -298,15 +279,6 @@ public class ControlGainAi extends SpellAbilityAi {
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, final Player ai) { public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
final Game game = ai.getGame(); 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.usesTargeting()) {
if (sa.hasParam("AllValid")) { if (sa.hasParam("AllValid")) {
CardCollectionView tgtCards = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); CardCollectionView tgtCards = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
@@ -326,7 +298,8 @@ public class ControlGainAi extends SpellAbilityAi {
} else { } else {
return this.canPlayAI(ai, sa); return this.canPlayAI(ai, sa);
} }
}
} // pumpDrawbackAI()
@Override @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, Map<String, Object> params) {

View File

@@ -12,12 +12,9 @@ import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi; import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -34,11 +31,11 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollection;
public class CopyPermanentAi extends SpellAbilityAi { public class CopyPermanentAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// TODO - I'm sure someone can do this AI better
Card source = sa.getHostCard(); Card source = sa.getHostCard();
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
String aiLogic = sa.getParamOrDefault("AILogic", ""); String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -47,9 +44,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
return false; return false;
} }
if ("MomirAvatar".equals(aiLogic)) { if ("MimicVat".equals(aiLogic)) {
return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
} else if ("MimicVat".equals(aiLogic)) {
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa); return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
} else if ("AtEOT".equals(aiLogic)) { } else if ("AtEOT".equals(aiLogic)) {
return ph.is(PhaseType.END_OF_TURN); return ph.is(PhaseType.END_OF_TURN);
@@ -62,7 +57,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
} }
} }
if (sa.hasParam("AtEOT") && !ph.is(PhaseType.MAIN1)) { if (sa.hasParam("AtEOT") && !aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1)) {
return false; return false;
} }
@@ -80,25 +75,18 @@ public class CopyPermanentAi extends SpellAbilityAi {
} }
} }
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. (Osgir)
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
sa.setXManaCostPaid(xPay);
}
if (sa.usesTargeting() && sa.hasParam("TargetingPlayer")) { if (sa.usesTargeting() && sa.hasParam("TargetingPlayer")) {
sa.resetTargets(); sa.resetTargets();
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer); sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa); return targetingPlayer.getController().chooseTargetsFor(sa);
} else if (sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer()) { } else if (sa.getTargetRestrictions() != null && sa.getTargetRestrictions().canTgtPlayer()) {
if (!sa.isCurse()) { if (!sa.isCurse()) {
if (sa.canTarget(aiPlayer)) { if (sa.canTarget(aiPlayer)) {
sa.getTargets().add(aiPlayer); sa.getTargets().add(aiPlayer);
return true; return true;
} else { } else {
for (Player p : aiPlayer.getYourTeam()) { for (Player p : aiPlayer.getTeamMates(true)) {
if (sa.canTarget(p)) { if (sa.canTarget(p)) {
sa.getTargets().add(p); sa.getTargets().add(p);
return true; return true;
@@ -116,7 +104,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
return false; return false;
} }
} else { } else {
return doTriggerAINoCost(aiPlayer, sa, false); return this.doTriggerAINoCost(aiPlayer, sa, false);
} }
} }
@@ -128,25 +116,20 @@ public class CopyPermanentAi extends SpellAbilityAi {
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final boolean canCopyLegendary = sa.hasParam("NonLegendary"); final boolean canCopyLegendary = sa.hasParam("NonLegendary");
// ////
// Targeting
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa)); CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa));
list = CardLists.filter(list, Predicates.not(CardPredicates.isRemAIDeck()));
//Nothing to target //Nothing to target
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;
} }
CardCollection betterList = CardLists.filter(list, Predicates.not(CardPredicates.isRemAIDeck()));
if (betterList.isEmpty()) {
if (!mandatory) {
return false;
}
} else {
list = betterList;
}
// Saheeli Rai + Felidar Guardian combo support // Saheeli Rai + Felidar Guardian combo support
if ("Saheeli Rai".equals(sourceName)) { if ("Saheeli Rai".equals(sourceName)) {
CardCollection felidarGuardian = CardLists.filter(list, CardPredicates.nameEquals("Felidar Guardian")); CardCollection felidarGuardian = CardLists.filter(list, CardPredicates.nameEquals("Felidar Guardian"));
@@ -160,7 +143,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
// target loop // target loop
while (sa.canAddMoreTarget()) { while (sa.canAddMoreTarget()) {
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
sa.resetTargets(); sa.resetTargets();
return false; return false;
} else { } else {
@@ -176,18 +159,18 @@ public class CopyPermanentAi extends SpellAbilityAi {
} }
}); });
Card choice; Card choice;
if (Iterables.any(list, Presets.CREATURES)) { if (!CardLists.filter(list, Presets.CREATURES).isEmpty()) {
if (sa.hasParam("TargetingPlayer")) { if (sa.hasParam("TargetingPlayer")) {
choice = ComputerUtilCard.getWorstCreatureAI(list); choice = ComputerUtilCard.getWorstCreatureAI(list);
} else { } else {
choice = ComputerUtilCard.getBestCreatureAI(list); choice = ComputerUtilCard.getBestCreatureAI(list);
} }
} else { } else {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list); choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
} }
if (choice == null) { // can't find anything left if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
sa.resetTargets(); sa.resetTargets();
return false; return false;
} else { } else {
@@ -248,26 +231,14 @@ public class CopyPermanentAi extends SpellAbilityAi {
final boolean canCopyLegendary = sa.hasParam("NonLegendary"); final boolean canCopyLegendary = sa.hasParam("NonLegendary");
final String filter = canCopyLegendary ? "Permanent" : "Permanent.YouDontCtrl,Permanent.nonLegendary"; final String filter = canCopyLegendary ? "Permanent" : "Permanent.YouDontCtrl,Permanent.nonLegendary";
// TODO add filter to not select Legendary from Other Player when ai already have a Legendary with that name // TODO add filter to not select Legendary from Other Player when ai already have a Legendary with that name
return CardLists.getValidCards(options, filter, ctrl, host, sa); return CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
} }
@Override @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, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) {
return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options));
}
final List<Card> cards = new PlayerCollection(options).getCreaturesInPlay(); final List<Card> cards = new PlayerCollection(options).getCreaturesInPlay();
Card chosen = ComputerUtilCard.getBestCreatureAI(cards); Card chosen = ComputerUtilCard.getBestCreatureAI(cards);
return chosen != null ? chosen.getController() : Iterables.getFirst(options, null); return chosen != null ? chosen.getController() : Iterables.getFirst(options, null);
} }
@Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
if (params != null && params.containsKey("Attacker")) {
return ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options));
}
// should not be reached
return super.chooseSinglePlayerOrPlaneswalker(ai, sa, options, params);
}
} }

View File

@@ -16,6 +16,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell; import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom; import forge.util.MyRandom;
public class CopySpellAbilityAi extends SpellAbilityAi { public class CopySpellAbilityAi extends SpellAbilityAi {
@@ -57,7 +58,9 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
} }
} }
if (sa.usesTargeting()) { final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
// Filter AI-specific targets if provided // Filter AI-specific targets if provided
if ("OnlyOwned".equals(sa.getParam("AITgts"))) { if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
if (!top.getActivatingPlayer().equals(aiPlayer)) { if (!top.getActivatingPlayer().equals(aiPlayer)) {
@@ -145,3 +148,4 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
} }
} }

View File

@@ -2,7 +2,6 @@ package forge.ai.ability;
import java.util.Iterator; import java.util.Iterator;
import forge.game.ability.effects.CounterEffect;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -27,6 +26,7 @@ import forge.game.cost.CostSacrifice;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
@@ -62,14 +62,14 @@ public class CounterAi extends SpellAbilityAi {
} }
} }
if (sa.usesTargeting()) { final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa); final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || topSA.getActivatingPlayer() == ai if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || topSA.getActivatingPlayer() == ai
|| ai.getAllies().contains(topSA.getActivatingPlayer())) { || ai.getAllies().contains(topSA.getActivatingPlayer())) {
// might as well check for player's friendliness // might as well check for player's friendliness
return false; return false;
} else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) {
return false;
} }
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided // check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided
@@ -113,7 +113,8 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false; boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true; setPayX = true;
toPay = Math.min(ComputerUtilCost.getMaxXValue(sa, ai, true), usableManaSources + 1); // TODO use ComputerUtilCost.getMaxXValue
toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
} else { } else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
} }
@@ -123,7 +124,8 @@ public class CounterAi extends SpellAbilityAi {
} }
if (toPay <= usableManaSources) { if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of the time // If this is a reusable Resource, feel free to play it most of
// the time
if (!SpellAbilityAi.playReusable(ai,sa)) { if (!SpellAbilityAi.playReusable(ai,sa)) {
return false; return false;
} }
@@ -244,9 +246,10 @@ public class CounterAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = ai.getGame(); final Game game = ai.getGame();
if (sa.usesTargeting()) { if (tgt != null) {
if (game.getStack().isEmpty()) { if (game.getStack().isEmpty()) {
return false; return false;
} }
@@ -275,7 +278,7 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false; boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) { if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true; setPayX = true;
toPay = ComputerUtilCost.getMaxXValue(sa, ai, true); toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else { } else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
} }

View File

@@ -20,20 +20,15 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType; import forge.game.card.CounterEnumType;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
@@ -45,7 +40,7 @@ import forge.util.Aggregates;
* @author Forge * @author Forge
* @version $Id$ * @version $Id$
*/ */
public abstract class CountersAi extends SpellAbilityAi { public abstract class CountersAi {
// An AbilityFactory subclass for Putting or Removing Counters on Cards. // An AbilityFactory subclass for Putting or Removing Counters on Cards.
/** /**
@@ -59,17 +54,10 @@ public abstract class CountersAi extends SpellAbilityAi {
* a {@link java.lang.String} object. * a {@link java.lang.String} object.
* @param amount * @param amount
* a int. * a int.
* @param newParam TODO
* @return a {@link forge.game.card.Card} object. * @return a {@link forge.game.card.Card} object.
*/ */
public static Card chooseCursedTarget(final CardCollectionView list, final String type, final int amount, final Player ai) { public static Card chooseCursedTarget(final CardCollectionView list, final String type, final int amount) {
Card choice; Card choice;
// opponent can always order it so that he gets 0
if (amount == 1 && Iterables.any(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
return null;
}
if (type.equals("M1M1")) { if (type.equals("M1M1")) {
// try to kill the best killable creature, or reduce the best one // try to kill the best killable creature, or reduce the best one
// but try not to target a Undying Creature // but try not to target a Undying Creature
@@ -99,9 +87,7 @@ public abstract class CountersAi extends SpellAbilityAi {
*/ */
public static Card chooseBoonTarget(final CardCollectionView list, final String type) { public static Card chooseBoonTarget(final CardCollectionView list, final String type) {
Card choice = null; Card choice = null;
if (type.equals("P1P1")) { if (type.equals("P1P1")) {
// TODO look for modified
choice = ComputerUtilCard.getBestCreatureAI(list); choice = ComputerUtilCard.getBestCreatureAI(list);
if (choice == null) { if (choice == null) {
@@ -115,7 +101,7 @@ public abstract class CountersAi extends SpellAbilityAi {
return c.getCounters(CounterEnumType.DIVINITY) == 0; return c.getCounters(CounterEnumType.DIVINITY) == 0;
} }
}); });
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon); choice = ComputerUtilCard.getMostExpensivePermanentAI(boon, null, false);
} else if (CounterType.get(type).isKeywordCounter()) { } else if (CounterType.get(type).isKeywordCounter()) {
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type)); choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
} else { } else {

View File

@@ -29,6 +29,7 @@ import forge.util.collect.FCollection;
public class CountersMoveAi extends SpellAbilityAi { public class CountersMoveAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (!moveTgtAI(ai, sa)) { if (!moveTgtAI(ai, sa)) {
@@ -82,8 +83,9 @@ public class CountersMoveAi extends SpellAbilityAi {
return true; return true;
} }
// something you can't block, try to reduce its attack // something you can't block, try to reduce its
if (!ComputerUtilCard.canBeBlockedProfitably(ai, cpy, false)) { // attack
if (!ComputerUtilCard.canBeBlockedProfitably(ai, cpy)) {
return true; return true;
} }
} }
@@ -117,6 +119,7 @@ public class CountersMoveAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
@@ -234,6 +237,7 @@ public class CountersMoveAi extends SpellAbilityAi {
} }
private boolean moveTgtAI(final Player ai, final SpellAbility sa) { private boolean moveTgtAI(final Player ai, final SpellAbility sa) {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final Game game = ai.getGame(); final Game game = ai.getGame();
final String type = sa.getParam("CounterType"); final String type = sa.getParam("CounterType");
@@ -279,7 +283,8 @@ public class CountersMoveAi extends SpellAbilityAi {
// cant use substract on Copy // cant use substract on Copy
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount); srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
// do not steal a P1P1 from Undying if it would die this way // do not steal a P1P1 from Undying if it would die
// this way
if (cType != null && cType.is(CounterEnumType.P1P1) && srcCardCpy.getNetToughness() <= 0) { if (cType != null && cType.is(CounterEnumType.P1P1) && srcCardCpy.getNetToughness() <= 0) {
return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken(); return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken();
} }
@@ -367,14 +372,6 @@ public class CountersMoveAi extends SpellAbilityAi {
} }
} }
Card lki = CardUtil.getLKICopy(src);
if (cType == null) {
lki.clearCounters();
} else {
lki.setCounters(cType, 0);
}
// go for opponent when higher value implies debuff
if (ComputerUtilCard.evaluateCreature(src) > ComputerUtilCard.evaluateCreature(lki)) {
List<Card> aiList = CardLists.filterControlledBy(tgtCards, ai); List<Card> aiList = CardLists.filterControlledBy(tgtCards, ai);
if (!aiList.isEmpty()) { if (!aiList.isEmpty()) {
List<Card> best = CardLists.filter(aiList, new Predicate<Card>() { List<Card> best = CardLists.filter(aiList, new Predicate<Card>() {
@@ -395,7 +392,7 @@ public class CountersMoveAi extends SpellAbilityAi {
if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) { if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) {
return false; return false;
} }
if (cType.is(CounterEnumType.M1M1)) { if (cType.is(CounterEnumType.M1M1) && card.hasKeyword(Keyword.PERSIST)) {
return false; return false;
} }
@@ -418,16 +415,11 @@ public class CountersMoveAi extends SpellAbilityAi {
return true; return true;
} }
} }
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (!isMandatoryTrigger) {
// no good target
return false;
}
}
// move counter to opponents creature but only if you can not steal them // move counter to opponents creature but only if you can not steal
// try to move to something useless or something that would leave play // them
// try to move to something useless or something that would leave
// play
List<Card> oppList = CardLists.filterControlledBy(tgtCards, ai.getOpponents()); List<Card> oppList = CardLists.filterControlledBy(tgtCards, ai.getOpponents());
if (!oppList.isEmpty()) { if (!oppList.isEmpty()) {
List<Card> best = CardLists.filter(oppList, new Predicate<Card>() { List<Card> best = CardLists.filter(oppList, new Predicate<Card>() {
@@ -449,7 +441,7 @@ public class CountersMoveAi extends SpellAbilityAi {
}); });
if (best.isEmpty()) { if (best.isEmpty()) {
best = oppList; best = aiList;
} }
Card card = ComputerUtilCard.getBestCreatureAI(best); Card card = ComputerUtilCard.getBestCreatureAI(best);
@@ -463,7 +455,7 @@ public class CountersMoveAi extends SpellAbilityAi {
} }
} }
// used for multiple sources -> defined // used for multiple sources -> defied
// or for source -> multiple defined // or for source -> multiple defined
@Override @Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
@@ -492,10 +484,4 @@ public class CountersMoveAi extends SpellAbilityAi {
// like keeping the last counter on a 0/0 creature // like keeping the last counter on a 0/0 creature
return max; return max;
} }
@Override
public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, Map<String, Object> params) {
// TODO
return super.chooseCounterType(options, sa, params);
}
} }

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