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/launch.json
.factorypath
# Ignore NetBeans config files
@@ -33,8 +32,6 @@ bin
gen
*.log
# Ignore macOS Spotlight rubbish
.DS_Store
# 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

116
README.md
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
@echo off
pushd %~dp0
java -version 1>nul 2>nul || (
echo no java installed
popd
exit /b 2
)
for /f tokens^=2^ delims^=.-_^+^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j"
if %jver% GEQ 17 (
java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd
exit /b 0
)
if %jver% GEQ 11 (
java --illegal-access=permit -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd
exit /b 0
)
java -Xmx4096m -Dfile.encoding=UTF-8 -jar $project.build.finalName$
popd

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -6,16 +6,15 @@ import java.util.Collections;
import java.util.List;
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.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.CardType;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
@@ -23,12 +22,43 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
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.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Localizer;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
@@ -39,8 +69,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
private final CardCollection discarded;
private final CardCollection tapped;
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
super(ai0, effect);
public AiCostDecision(Player ai0, SpellAbility sa) {
super(ai0);
ability = sa;
source = ability.getHostCard();
@@ -50,16 +80,27 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
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);
}
@Override
public PaymentDecision visit(CostChooseCreatureType cost) {
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(),
Lists.newArrayList());
return PaymentDecision.type(choice);
Integer amount = cost.convertAmount();
Iterable<String> choices = player.getController().chooseSomeType(
Localizer.getInstance().getMessage("lblCreature"), ability, amount, amount, Lists.newArrayList(CardType.getAllCreatureTypes()));
if (choices == null || Iterables.isEmpty(choices)) {
return null;
}
return PaymentDecision.types(choices);
}
@Override
@@ -72,13 +113,15 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
return PaymentDecision.card(player.getLastDrawnCard());
} else if (cost.payCostFromSource()) {
}
else if (cost.payCostFromSource()) {
if (!hand.contains(source)) {
return null;
}
return PaymentDecision.card(source);
} else if (type.equals("Hand")) {
}
else if (type.equals("Hand")) {
if (hand.size() > 1 && ability.getActivatingPlayer() != null) {
hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard, ability);
}
@@ -88,7 +131,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (type.contains("WithSameName")) {
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")) {
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);
}
return PaymentDecision.card(randomSubset);
} else if (type.equals("DifferentNames")) {
}
else if (type.equals("DifferentNames")) {
CardCollection differentNames = new CardCollection();
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
while (c > 0) {
@@ -113,7 +160,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
c--;
}
return PaymentDecision.card(differentNames);
} else {
}
else {
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollection result = aic.getCardsToDiscard(c, type.split(";"), ability, discarded);
@@ -126,23 +174,24 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
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);
}
@Override
public PaymentDecision visit(CostDraw cost) {
if (!cost.canPay(ability, player, isEffect())) {
return null;
Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
int c = cost.getAbilityAmount(ability);
List<Player> res = cost.getPotentialPlayers(player, ability);
PaymentDecision decision = PaymentDecision.players(res);
decision.c = c;
return decision;
return PaymentDecision.number(c);
}
@Override
@@ -158,7 +207,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
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)) {
return PaymentDecision.card(player.getCardsIn(ZoneType.Library, c));
@@ -166,7 +218,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
else if (cost.sameZone) {
// TODO Determine exile from same zone for AI
return null;
} else {
}
else {
CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability);
return null == chosen ? null : PaymentDecision.card(chosen);
}
@@ -174,6 +227,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostExileFromStack cost) {
Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
List<SpellAbility> chosen = Lists.newArrayList();
for (SpellAbilityStackInstance si :source.getGame().getStack()) {
SpellAbility sp = si.getSpellAbility(true).getRootAbility();
@@ -186,9 +244,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostExiledMoveToGrave cost) {
Integer c = cost.convertAmount();
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);
@@ -196,7 +257,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
CardLists.sortByPowerDesc(typeList);
CardLists.sortByPowerAsc(typeList);
Collections.reverse(typeList);
for (int i = 0; i < c; i++) {
chosen.add(typeList.get(i));
@@ -211,7 +273,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
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);
@@ -230,13 +295,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostFlipCoin cost) {
int c = cost.getAbilityAmount(ability);
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostRollDice cost) {
int c = cost.getAbilityAmount(ability);
Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
return PaymentDecision.number(c);
}
@@ -246,7 +308,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
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);
@@ -263,11 +328,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
return res.isEmpty() ? null : PaymentDecision.card(res);
}
@Override
public PaymentDecision visit(CostGainLife cost) {
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()) {
oppsThatCanGainLife.add(opp);
}
@@ -280,9 +346,13 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.players(oppsThatCanGainLife);
}
@Override
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);
return topLib.size() < c ? null : PaymentDecision.number(c);
@@ -295,8 +365,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostPayLife cost) {
int c = cost.getAbilityAmount(ability);
if (!player.canPayLife(c, isEffect(), ability)) {
Integer c = cost.convertAmount();
if (c == null) {
// Generalize cost
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
if (!player.canPayLife(c)) {
return null;
}
// activator.payLife(c, null);
@@ -305,29 +379,37 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
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)) {
return null;
}
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostPutCardToLib cost) {
if (cost.payCostFromSource()) {
return PaymentDecision.card(source);
}
Integer c = cost.convertAmount();
final Game game = player.getGame();
CardCollection chosen = new CardCollection();
CardCollectionView list;
if (cost.isSameZone()) {
list = new CardCollection(game.getCardsIn(cost.getFrom()));
} else {
}
else {
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);
@@ -343,7 +425,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
}
chosen = chosen.subList(0, c);
} else {
}
else {
chosen = ComputerUtil.choosePutToLibraryFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability);
}
return chosen.isEmpty() ? null : PaymentDecision.card(chosen);
@@ -361,12 +444,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
Card card;
if (cost.getType().equals("Creature.YouCtrl")) {
card = ComputerUtilCard.getWorstCreatureAI(typeList);
} else {
}
else {
card = ComputerUtilCard.getWorstPermanentAI(typeList, false, false, false, false);
}
return PaymentDecision.card(card);
}
@Override
public PaymentDecision visit(CostTap cost) {
return PaymentDecision.number(0);
@@ -374,12 +459,17 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostTapType cost) {
final String amount = cost.getAmount();
Integer c = cost.convertAmount();
String type = cost.getType();
boolean isVehicle = type.contains("+withTotalPowerGE");
CardCollection exclude = new CardCollection();
exclude.addAll(tapped);
if (c == null && !isVehicle) {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
if (type.contains("sharesCreatureTypeWith")) {
return null;
}
@@ -409,7 +499,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), exclude);
} else {
int c = cost.getAbilityAmount(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);
}
@Override
public PaymentDecision visit(CostSacrifice cost) {
if (cost.payCostFromSource()) {
return PaymentDecision.card(source);
}
if (cost.getType().equals("OriginalHost")) {
return PaymentDecision.card(ability.getOriginalHost());
return PaymentDecision.card(ability.getHostCard());
}
if (cost.getAmount().equals("All")) {
// Does the AI want to use Sacrifice All?
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();
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, isEffect(), c, null);
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);
return PaymentDecision.card(list);
}
@@ -446,7 +540,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (cost.payCostFromSource())
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);
return res.isEmpty() ? null : PaymentDecision.card(res);
@@ -472,22 +569,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
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);
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();
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) {
int removed = 0;
if (!prefs.isEmpty() && stillToRemove > 0) {
@@ -501,7 +596,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
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
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
final String amount = cost.getAmount();
final int c = AbilityUtils.calculateAmount(source, amount, ability);
final Card originalHost = ability.getOriginalOrHost();
if (c <= 0) {
return null;
@@ -532,6 +628,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
// currently if amount is bigger than one,
// it tries to remove all counters from one source and type at once
int toRemove = 0;
final GameEntityCounterTable table = new GameEntityCounterTable();
@@ -565,7 +662,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(card.getCounters(ctype), c - toRemove);
if (thisRemove > 0) {
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);
if (over > 0) {
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);
if (over > 0) {
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);
if (over > 0) {
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);
}
// TODO add logic to remove positive counters?
if (c > toRemove && cost.counter != null) {
// 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);
if (thisRemove > 0) {
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);
if (thisRemove > 0) {
toRemove += thisRemove;
table.put(null, card, e.getKey(), thisRemove);
table.put(card, e.getKey(), thisRemove);
}
}
}
@@ -713,24 +811,25 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostRemoveCounter cost) {
final String amount = cost.getAmount();
Integer c = cost.convertAmount();
final String type = cost.getType();
int c;
final String sVar = ability.getSVar(amount);
if (amount.equals("All")) {
c = source.getCounters(cost.counter);
} else if (sVar.equals("Targeted$CardManaCost")) {
c = 0;
if (ability.getTargets().size() > 0) {
for (Card tgt : ability.getTargets().getTargetCards()) {
if (tgt.getManaCost() != null) {
c += tgt.getManaCost().getCMC();
if (c == null) {
final String sVar = ability.getSVar(amount);
if (amount.equals("All")) {
c = source.getCounters(cost.counter);
} else if (sVar.equals("Targeted$CardManaCost")) {
c = 0;
if (ability.getTargets().size() > 0) {
for (Card tgt : ability.getTargets().getTargetCards()) {
if (tgt.getManaCost() != null) {
c += tgt.getManaCost().getCMC();
}
}
}
} else {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
} else {
c = cost.getAbilityAmount(ability);
}
if (!cost.payCostFromSource()) {
@@ -758,7 +857,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
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);
@@ -790,3 +893,4 @@ public class AiCostDecision extends CostDecisionMakerBase {
return false;
}
}

View File

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,49 @@
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.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.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.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
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.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Set;
public class ComputerUtilCost {
@@ -73,7 +87,7 @@ public class ComputerUtilCost {
if (cost == null) {
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()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
@@ -123,8 +137,9 @@ public class ComputerUtilCost {
* the source
* @return true, if successful
*/
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;
}
@@ -135,20 +150,14 @@ public class ComputerUtilCost {
final CostDiscard disc = (CostDiscard) part;
final String type = disc.getType();
if (type.equals("CARDNAME")) {
if (source.getAbilityText().contains("Bloodrush")) {
continue;
} else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
// Better do something than just discard stuff
return true;
}
if (type.equals("CARDNAME") && source.getAbilityText().contains("Bloodrush")) {
continue;
}
final CardCollection typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
final CardCollection typeList = CardLists.getValidCards(hand, type.split(","), source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) {
continue;
}
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
@@ -233,51 +242,6 @@ public class ComputerUtilCost {
return true;
}
public static boolean checkForManaSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility, final boolean effect) {
// TODO cheating via autopay can still happen, need to get the real ai player from controlledBy
if (cost == null || !ai.isAI()) {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
CardCollection list = new CardCollection();
final CardCollection exclude = new CardCollection();
if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_SAC_COST) != null) {
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_SAC_COST));
}
if (part.payCostFromSource()) {
list.add(source);
} else if (part.getType().equals("OriginalHost")) {
list.add(sourceAbility.getOriginalHost());
} else if (part.getAmount().equals("All")) {
// Does the AI want to use Sacrifice All?
return false;
} else {
final String amount = part.getAmount();
Integer c = part.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, amount, sourceAbility);
}
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
if (choices != null) {
list.addAll(choices);
}
}
list.removeAll(exclude);
if (list.isEmpty()) {
return false;
}
for (Card choice : list) {
AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_SAC_COST);
}
return true;
}
}
return true;
}
/**
* Check creature sacrifice cost.
*
@@ -306,10 +270,7 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
int count = 0;
while (count < amount) {
@@ -359,14 +320,11 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
int count = 0;
while (count < amount) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList, sourceAbility);
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
if (prefCard == null) {
return false;
}
@@ -379,31 +337,18 @@ public class ComputerUtilCost {
return true;
}
/**
* Check sacrifice cost.
*
* @param cost
* the cost
* @param source
* the source
* @return true, if successful
*/
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, sourceAbility, true);
}
public static boolean isSacrificeSelfCost(final Cost cost) {
if (cost == null) {
return false;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
if ("CARDNAME".equals(part.getType())) {
return true;
}
}
}
return false;
if (cost == null) {
return false;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
if ("CARDNAME".equals(part.getType())) {
return true;
}
}
}
return false;
}
/**
@@ -421,18 +366,17 @@ public class ComputerUtilCost {
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostTapType) {
String type = part.getType();
/*
* Only crew with creatures weaker than vehicle
*
*
* Possible improvements:
* - block against evasive (flyers, intimidate, etc.)
* - break board stall by racing with evasive vehicle
*/
if (sa.hasParam("Crew")) {
Card vehicle = AnimateAi.becomeAnimated(source, sa);
Card vehicle = AnimateAi.becomeAnimated(source, sa);
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
String type = part.getType();
String totalP = type.split("withTotalPowerGE")[1];
type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
CardCollection exclude = CardLists.getValidCards(
@@ -447,41 +391,25 @@ public class ComputerUtilCost {
return ComputerUtil.chooseTapTypeAccumulatePower(ai, type, sa, true,
Integer.parseInt(totalP), exclude) != null;
}
// check if we have a valid card to tap (e.g. Jaspera Sentinel)
Integer c = part.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, part.getAmount(), sa);
}
CardCollection exclude = new CardCollection();
if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST) != null) {
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST));
}
// trying to produce mana that includes tapping source that will already be tapped
if (exclude.contains(source) && cost.hasTapCost()) {
return false;
}
// if we want to pay for an ability with tapping the source can't be chosen
if (sa.getPayCosts().hasTapCost()) {
exclude.add(sa.getHostCard());
}
CardCollection tapChoices = ComputerUtil.chooseTapType(ai, type, source, cost.hasTapCost(), c, exclude, sa);
if (tapChoices != null) {
for (Card choice : tapChoices) {
AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_TAP_COST);
}
// if manasource gets tapped to produce it also can't help paying another
if (cost.hasTapCost()) {
AiCardMemory.rememberCard(ai, source, MemorySet.PAYS_TAP_COST);
}
return true;
}
return false;
return false;
}
}
return true;
}
/**
* Check sacrifice cost.
*
* @param cost
* the cost
* @param source
* the source
* @return true, if successful
*/
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, sourceAbility,true);
}
/**
* <p>
* shouldPayCost.
@@ -492,8 +420,8 @@ public class ComputerUtilCost {
* @param cost
* @return a boolean.
*/
@Deprecated
public static boolean shouldPayCost(final Player ai, final Card hostCard, final Cost cost) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
if (!ai.cantLoseForZeroOrLessLife()) {
@@ -523,7 +451,7 @@ public class ComputerUtilCost {
* a {@link forge.game.player.Player} object.
* @return a boolean.
*/
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
public static boolean canPayCost(final SpellAbility sa, final Player player) {
if (sa.getActivatingPlayer() == null) {
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
}
@@ -551,9 +479,9 @@ public class ComputerUtilCost {
}
}
for (Card c : player.getCardsIn(ZoneType.Command)) {
if (cannotBeCountered) {
continue;
}
if (cannotBeCountered) {
continue;
}
final String snem = c.getSVar("SpellsNeedExtraManaEffect");
if (!StringUtils.isBlank(snem)) {
if (StringUtils.isNumeric(snem)) {
@@ -585,19 +513,7 @@ public class ComputerUtilCost {
if (sa.hasParam("Crew")) { // put under checkTapTypeCost?
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostTapType && part.getType().contains("+withTotalPowerGE")) {
return new AiCostDecision(player, sa, false).visit((CostTapType)part) != null;
}
}
}
// Ward - will be accounted for when rechecking a targeted ability
if (!(sa instanceof WrappedAbility) && sa.usesTargeting()) {
for (Card tgt : sa.getTargets().getTargetCards()) {
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
extraManaNeeded += wardCost.getTotalMana().getCMC();
}
return new AiCostDecision(player, sa).visit((CostTapType)part) != null;
}
}
}
@@ -632,8 +548,8 @@ public class ComputerUtilCost {
}
}
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
} // canPayCost()
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
@@ -642,6 +558,7 @@ public class ComputerUtilCost {
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payNever = "Never".equals(aiLogic);
boolean shockland = "Shockland".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
@@ -653,9 +570,29 @@ public class ComputerUtilCost {
return false;
}
} else if ("OnlyDontControl".equals(aiLogic)) {
if (source == null || payer.equals(source.getController())) {
if (sa.getHostCard() == null || payer.equals(sa.getHostCard().getController())) {
return false;
}
} else if (shockland) {
if (payer.getLife() > 3 && payer.canPayLife(2)) {
final int landsize = payer.getLandsInPlay().size() + 1;
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
// if the new land size would equal the CMC of a card in AIs hand, consider playing it untapped,
// otherwise don't bother running other checks
if (landsize != c.getCMC()) {
continue;
}
// try to determine in the AI is actually planning to play a spell ability from the card
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
// try to determine if the mana shards provided by the lands would be applicable to pay the mana cost
boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
if (canPay && willPlay) {
return true;
}
}
}
return false;
} else if ("Paralyze".equals(aiLogic)) {
final Card c = source.getEnchantingCard();
if (c == null || c.isUntapped()) {
@@ -663,7 +600,7 @@ public class ComputerUtilCost {
}
} else if ("RiskFactor".equals(aiLogic)) {
final Player activator = sa.getActivatingPlayer();
if (!activator.canDraw()) {
if (!activator.canDraw() || activator.hasKeyword("You can't draw more than one card each turn.")) {
return false;
}
} else if ("MorePowerful".equals(aiLogic)) {
@@ -673,10 +610,10 @@ public class ComputerUtilCost {
return false;
}
} else if (aiLogic != null && aiLogic.startsWith("LifeLE")) {
// if payer can't lose life its no need to pay unless
if (!payer.canLoseLife())
return false;
else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substring(6), sa)) {
// if payer can't lose life its no need to pay unless
if (!payer.canLoseLife())
return false;
else if (payer.getLife() <= Integer.valueOf(aiLogic.substring(6))) {
return true;
}
} else if ("WillAttack".equals(aiLogic)) {
@@ -693,29 +630,6 @@ public class ComputerUtilCost {
return false;
}
// Check for shocklands and similar ETB replacement effects
if (sa.hasParam("ETB") && sa.getApi().equals(ApiType.Tap)) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
final CostPayLife lifeCost = (CostPayLife) part;
Integer amount = lifeCost.convertAmount();
if (payer.getLife() > (amount + 1) && payer.canPayLife(amount, true, sa)) {
final int landsize = payer.getLandsInPlay().size() + 1;
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
// Check if the AI has enough lands to play the card
if (landsize != c.getCMC()) {
continue;
}
// Check if the AI intends to play the card and if it can pay for it with the mana it has
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
boolean canPay = c.getManaCost().canBePaidWithAvailable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
return canPay && willPlay;
}
}
}
}
}
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
return false;
@@ -725,18 +639,19 @@ public class ComputerUtilCost {
// Didn't have any of the data on the original SA to pay dependant costs
return checkLifeCost(payer, cost, source, 4, sa)
&& checkDamageCost(payer, cost, source, 4)
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source, sa))
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
&& checkDamageCost(payer, cost, source, 4)
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source, sa))
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
}
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
}
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) {
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED);
Set<String> colorsAvailable = Sets.newHashSet();
@@ -767,19 +682,18 @@ public class ComputerUtilCost {
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();
SpellAbility root = sa.getRootAbility();
final Cost abCost = root.getPayCosts();
final SpellAbility root = sa.getRootAbility();
final Cost abCost = sa.getPayCosts();
if (abCost == null || !abCost.hasXInAnyCostPart()) {
return 0;
}
Integer val = null;
if (root.costHasManaX()) {
val = ComputerUtilMana.determineLeftoverMana(root, ai, effect);
if (sa.costHasManaX()) {
val = ComputerUtilMana.determineLeftoverMana(root, ai);
}
if (sa.usesTargeting()) {
@@ -791,11 +705,11 @@ public class ComputerUtilCost {
if (sa.hasParam("AIMaxTgtsCount")) {
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified
// TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable?
val = ObjectUtils.min(val, AbilityUtils.calculateAmount(source, "Count$" + sa.getParam("AIMaxTgtsCount"), sa));
val = ObjectUtils.min(val, 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) {
// filter cost parts for preferences, don't choose X > than possible preferences
@@ -825,12 +739,4 @@ public class ComputerUtilCost {
}
return ObjectUtils.defaultIfNull(val, 0);
}
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
if (source.usesTargeting()) {
final CardCollection targets = new CardCollection(source.getTargets().getTargetCards());
choices = Iterables.filter(choices, Predicates.not(Predicates.and(CardPredicates.isController(ai), Predicates.in(targets))));
}
return new CardCollection(choices);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3,20 +3,31 @@ package forge.ai;
import java.io.BufferedReader;
import java.io.InputStream;
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.Set;
import com.google.common.collect.*;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.Card;
import forge.game.card.CardCloneStates;
@@ -33,7 +44,9 @@ import forge.game.mana.ManaPool;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
import forge.item.IPaperCard;
@@ -50,7 +63,6 @@ public abstract class GameState {
ZONES.put(ZoneType.Library, "library");
ZONES.put(ZoneType.Exile, "exile");
ZONES.put(ZoneType.Command, "command");
ZONES.put(ZoneType.Sideboard, "sideboard");
}
private int humanLife = -1;
@@ -77,8 +89,7 @@ public abstract class GameState {
private final Map<Card, Integer> markedDamage = new HashMap<>();
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>();
private final Map<Card, String> cardToChosenType = new HashMap<>();
private final Map<Card, String> cardToChosenType2 = new HashMap<>();
private final Map<Card, List<String>> cardToChosenTypes = new HashMap<>();
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
private final Map<Card, List<String>> cardToMergedCards = new HashMap<>();
@@ -154,7 +165,7 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n"));
}
if (!computerManaPool.isEmpty()) {
sb.append(TextUtil.concatNoSpace("aimanapool=", computerManaPool, "\n"));
sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n"));
}
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
@@ -315,29 +326,30 @@ public abstract class GameState {
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
}
if (c.getPlayerAttachedTo() != null) {
// TODO: improve this for game states with more than two players
newText.append("|EnchantingPlayer:");
Player p = c.getPlayerAttachedTo();
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
} else if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
}
if (c.getDamage() > 0) {
newText.append("|Damage:").append(c.getDamage());
}
if (c.hasChosenColor()) {
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ","));
}
if (c.hasChosenType()) {
newText.append("|ChosenType:").append(c.getChosenType());
}
if (c.hasChosenType2()) {
newText.append("|ChosenType2:").append(c.getChosenType2());
SpellAbility first = c.getFirstSpellAbility();
if (first != null) {
if (first.hasChosenColor()) {
newText.append("|ChosenColor:").append(TextUtil.join(first.getChosenColors(), ","));
}
if (first.hasChosenType()) {
newText.append("|ChosenType:").append(TextUtil.join(first.getChosenType(), ","));
}
}
if (!c.getNamedCard().isEmpty()) {
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.
newText.append("|OnAdventure");
}
if (c.isForetold()) {
newText.append("|Foretold");
}
if (c.isForetoldThisTurn()) {
newText.append("|ForetoldThisTurn");
}
}
@@ -435,7 +441,7 @@ public abstract class GameState {
boolean first = true;
StringBuilder counterString = new StringBuilder();
for (Entry<CounterType, Integer> kv : counters.entrySet()) {
for(Entry<CounterType, Integer> kv : counters.entrySet()) {
if (!first) {
counterString.append(",");
}
@@ -471,7 +477,7 @@ public abstract class GameState {
}
public void parse(List<String> lines) {
for (String line : lines) {
for(String line : lines) {
parseLine(line);
}
}
@@ -573,13 +579,6 @@ public abstract class GameState {
aiCardTexts.put(ZoneType.Command, categoryValue);
}
else if (categoryName.endsWith("sideboard")) {
if (isHuman)
humanCardTexts.put(ZoneType.Sideboard, categoryValue);
else
aiCardTexts.put(ZoneType.Sideboard, categoryValue);
}
else if (categoryName.startsWith("ability")) {
abilityString.put(categoryName.substring("ability".length()), categoryValue);
}
@@ -639,8 +638,7 @@ public abstract class GameState {
markedDamage.clear();
cardToChosenClrs.clear();
cardToChosenCards.clear();
cardToChosenType.clear();
cardToChosenType2.clear();
cardToChosenTypes.clear();
cardToMergedCards.clear();
cardToScript.clear();
cardAttackMap.clear();
@@ -707,19 +705,11 @@ public abstract class GameState {
}
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
// Set negative or zero life after state effects if need be, important for some puzzles that rely on
// pre-setting negative life (e.g. PS_NEO4).
if (humanLife <= 0) {
human.setLife(humanLife, null);
} else if (computerLife <= 0) {
ai.setLife(computerLife, null);
}
}
private String processManaPool(ManaPool manaPool) {
StringBuilder mana = new StringBuilder();
for (final byte c : ManaAtom.MANATYPES) {
for (final byte c : MagicColor.WUBRGC) {
int amount = manaPool.getAmountOfColor(c);
for (int i = 0; i < amount; i++) {
mana.append(MagicColor.toShortString(c)).append(" ");
@@ -743,7 +733,7 @@ public abstract class GameState {
if (persistent) {
produced.put("PersistentMana", "True");
}
final AbilityManaPart abMana = new AbilityManaPart(dummy, produced);
final AbilityManaPart abMana = new AbilityManaPart(dummy, null, produced);
game.getAction().invoke(new Runnable() {
@Override
public void run() {
@@ -779,10 +769,23 @@ public abstract class GameState {
}
game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap));
for (final Card c : combat.getAttackers()) {
CombatUtil.checkDeclaredAttacker(game, c, combat, false);
if (!combat.getAttackers().isEmpty()) {
List<GameEntity> attackedTarget = Lists.newArrayList();
for (final Card c : combat.getAttackers()) {
attackedTarget.add(combat.getDefenderByAttacker(c));
}
final Map<AbilityKey, Object> runParams = Maps.newEnumMap(AbilityKey.class);
runParams.put(AbilityKey.Attackers, combat.getAttackers());
runParams.put(AbilityKey.AttackingPlayer, combat.getAttackingPlayer());
runParams.put(AbilityKey.AttackedTarget, attackedTarget);
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
}
for (final Card c : combat.getAttackers()) {
CombatUtil.checkDeclaredAttacker(game, c, combat);
}
game.getTriggerHandler().resetActiveTriggers();
game.updateCombatForView();
game.fireEvent(new GameEventCombatChanged());
@@ -825,6 +828,7 @@ public abstract class GameState {
Card exiledWith = idToCard.get(Integer.parseInt(id));
c.setExiledWith(exiledWith);
c.setExiledBy(exiledWith.getController());
exiledWith.addExiledWith(c);
}
}
@@ -1052,12 +1056,7 @@ public abstract class GameState {
return;
}
if (!c.getName().equals(spellDef) && c.hasAlternateState() && spellDef.equals(c.getAlternateState().getName())) {
sa = c.getAlternateState().getFirstSpellAbility();
} else {
sa = c.getFirstSpellAbility();
}
sa = c.getFirstSpellAbility();
sa.setActivatingPlayer(activator);
handleScriptedTargetingForSA(game, sa, tgtID);
@@ -1088,19 +1087,13 @@ public abstract class GameState {
Card c = entry.getKey();
List<String> colors = entry.getValue();
c.setChosenColors(colors);
c.setChosenColors(colors, c.getFirstSpellAbility());
}
// Chosen type
for (Entry<Card, String> entry : cardToChosenType.entrySet()) {
for (Entry<Card, List<String>> entry : cardToChosenTypes.entrySet()) {
Card c = entry.getKey();
c.setChosenType(entry.getValue());
}
// Chosen type 2
for (Entry<Card, String> entry : cardToChosenType2.entrySet()) {
Card c = entry.getKey();
c.setChosenType2(entry.getValue());
c.setChosenType(entry.getValue(), c.getFirstSpellAbility());
}
// Named card
@@ -1124,13 +1117,13 @@ public abstract class GameState {
private void handleCardAttachments() {
// Unattach all permanents first
for (Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
attachedTo.unAttachAllCards();
}
// Attach permanents by ID
for (Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
Card attacher = entry.getKey();
if (attacher.isAttachment()) {
@@ -1139,7 +1132,7 @@ public abstract class GameState {
}
// Enchant players by ID
for (Entry<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
for(Entry<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
// TODO: improve this for game states with more than two players
Card attacher = entry.getKey();
Game game = attacher.getGame();
@@ -1150,9 +1143,9 @@ public abstract class GameState {
}
private void handleMergedCards() {
for (Entry<Card, List<String>> entry : cardToMergedCards.entrySet()) {
for(Entry<Card, List<String>> entry : cardToMergedCards.entrySet()) {
Card mergedTo = entry.getKey();
for (String mergedCardName : entry.getValue()) {
for(String mergedCardName : entry.getValue()) {
Card c;
PaperCard pc = StaticData.instance().getCommonCards().getCard(mergedCardName. replace("^", ","));
if (pc == null) {
@@ -1203,7 +1196,7 @@ public abstract class GameState {
String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2);
entity.addCounterInternal(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, null);
entity.addCounter(CounterType.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.setCommanders(Lists.newArrayList());
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<>(ZoneType.class);
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
String value = kv.getValue();
@@ -1228,8 +1219,6 @@ public abstract class GameState {
p.setLandsPlayedThisTurn(landsPlayed);
p.setLandsPlayedLastTurn(landsPlayedLastTurn);
p.clearPaidForSA();
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) {
@@ -1247,21 +1236,22 @@ public abstract class GameState {
boolean tapped = c.isTapped();
boolean sickness = c.hasSickness();
Map<CounterType, Integer> counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters var as-is.
// Note: Not clearCounters() since we want to keep the counters
// var as-is.
c.setCounters(Maps.newHashMap());
if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter)
//FIXME it shouldn't be able to attach itself
c.setEntityAttachedTo(CardFactory.copyCard(c, true));
c.setEntityAttachedTo(c);
}
if (cardsWithoutETBTrigs.contains(c)) {
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null, null);
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null);
} else {
p.getZone(ZoneType.Hand).add(c);
p.getGame().getAction().moveToPlay(c, null, null);
p.getGame().getAction().moveToPlay(c, null);
}
c.setTapped(tapped);
@@ -1281,7 +1271,7 @@ public abstract class GameState {
* <p>
* processCardsForZone.
* </p>
*
*
* @param data
* an array of {@link java.lang.String} objects.
* @param player
@@ -1300,7 +1290,7 @@ public abstract class GameState {
break;
}
}
Card c;
boolean hasSetCurSet = false;
if (cardinfo[0].startsWith("t:")) {
@@ -1323,7 +1313,7 @@ public abstract class GameState {
for (final String info : cardinfo) {
if (info.startsWith("Tapped")) {
c.tap(false);
c.tap();
} else if (info.startsWith("Renowned")) {
c.setRenowned(true);
} else if (info.startsWith("Monstrous")) {
@@ -1347,10 +1337,10 @@ public abstract class GameState {
c.setState(CardStateName.Meld, true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
}
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell";
SpellAbility saAdventure = AbilityFactory.getAbility(abAdventure, c);
AbilitySub saAdventure = (AbilitySub)AbilityFactory.getAbility(abAdventure, c);
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
@@ -1361,9 +1351,7 @@ public abstract class GameState {
c.setExiledBy(c.getController());
} else if (info.startsWith("IsCommander")) {
c.setCommander(true);
List<Card> cmd = Lists.newArrayList(player.getCommanders());
cmd.add(c);
player.setCommanders(cmd);
player.setCommanders(Lists.newArrayList(c));
} else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3));
idToCard.put(id, c);
@@ -1391,9 +1379,7 @@ public abstract class GameState {
} else if (info.startsWith("ChosenColor:")) {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) {
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ChosenType2:")) {
cardToChosenType2.put(c, info.substring(info.indexOf(':') + 1));
cardToChosenTypes.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenCards:")) {
CardCollection chosen = new CardCollection();
String[] idlist = info.substring(info.indexOf(':') + 1).split(",");
@@ -1425,14 +1411,6 @@ public abstract class GameState {
}
} else if (info.equals("NoETBTrigs")) {
cardsWithoutETBTrigs.add(c);
} else if (info.equals("Foretold")) {
c.setForetold(true);
c.turnFaceDown(true);
c.addMayLookTemp(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) {
c.setForetoldThisTurn(true);
} else if (info.equals("IsToken")) {
c.setToken(true);
}
}

View File

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

View File

@@ -3,11 +3,11 @@ package forge.ai;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
@@ -27,7 +27,6 @@ import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard;
import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
@@ -52,6 +51,7 @@ import forge.game.cost.CostPartMana;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
import forge.game.mana.ManaConversionMatrix;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.DelayedReveal;
@@ -79,35 +79,24 @@ import forge.util.MyRandom;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
/**
/**
* A prototype for player controller class
*
*
* Handles phase skips for now.
*/
public class PlayerControllerAi extends PlayerController {
private final AiController brains;
private boolean pilotsNonAggroDeck = false;
public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) {
super(game, p, lp);
brains = new AiController(p, game);
}
public boolean pilotsNonAggroDeck() {
return pilotsNonAggroDeck;
}
public void setupAutoProfile(Deck deck) {
pilotsNonAggroDeck = deck.getName().contains("Control") || Deck.getAverageCMC(deck) > 3;
}
public void allowCheatShuffle(boolean value) {
public void allowCheatShuffle(boolean value){
brains.allowCheatShuffle(value);
}
public void setUseSimulation(boolean value) {
brains.setUseSimulation(value);
}
@@ -136,31 +125,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, CardCollectionView remaining, int damageDealt, GameEntity defender, boolean overrideOrder) {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, remaining, damageDealt, defender, overrideOrder);
}
@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;
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, int damageDealt, GameEntity defender, boolean overrideOrder) {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, damageDealt, defender, overrideOrder);
}
@Override
@@ -179,7 +145,7 @@ public class PlayerControllerAi extends PlayerController {
payingPlayer = ability.getHostCard().getEnchantingCard().getController();
}
int number = ComputerUtilMana.determineLeftoverMana(ability, player, false);
int number = ComputerUtilMana.determineLeftoverMana(ability, player);
if (logic.startsWith("MaxMana.") || logic.startsWith("PowerLeakMaxMana.")) {
number = Math.min(number, Integer.parseInt(logic.substring(logic.indexOf(".") + 1)));
@@ -189,7 +155,7 @@ public class PlayerControllerAi extends PlayerController {
case BidLife:
return 0;
default:
return null;
return null;
}
}
return null; // return incorrect value to indicate that
@@ -229,16 +195,16 @@ public class PlayerControllerAi extends PlayerController {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
FCollection<T> remaining = new FCollection<>(optionList);
List<T> selecteds = new ArrayList<>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer, params);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
FCollection<T> remaining = new FCollection<>(optionList);
List<T> selecteds = new ArrayList<>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer, params);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
return selecteds;
}
@@ -250,11 +216,11 @@ public class PlayerControllerAi extends PlayerController {
SpellAbility selected;
do {
selected = chooseSingleSpellForEffect(remaining, sa, title, params);
if (selected != null) {
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while (selected != null && selecteds.size() < num);
} while ( (selected != null ) && (selecteds.size() < num) );
return selecteds;
}
@@ -272,7 +238,7 @@ public class PlayerControllerAi extends PlayerController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return getAi().confirmAction(sa, mode, message);
}
@Override
public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, String string,
int bid, Player winner) {
@@ -297,9 +263,9 @@ public class PlayerControllerAi extends PlayerController {
// Store/replace target choices more properly to get this SA cleared.
TargetChoices tc = null;
TargetChoices subtc = null;
boolean storeChoices = sa.usesTargeting();
boolean storeChoices = sa.getTargetRestrictions() != null;
final SpellAbility sub = sa.getSubAbility();
boolean storeSubChoices = sub != null && sub.usesTargeting();
boolean storeSubChoices = sub != null && sub.getTargetRestrictions() != null;
boolean ret = true;
if (storeChoices) {
@@ -339,7 +305,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List<Card> exertAttackers(List<Card> attackers) {
return AiAttackController.exertAttackers(attackers, brains.getAttackAggression());
return AiAttackController.exertAttackers(attackers);
}
@Override
@@ -370,13 +336,14 @@ public class PlayerControllerAi extends PlayerController {
for (Card c: topN) {
if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c)) {
toBottom.add(c);
} else {
}
else {
toTop.add(c);
}
}
// put the rest on top in random order
CardLists.shuffle(toTop);
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toBottom);
}
@@ -402,7 +369,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
CardLists.shuffle(toTop);
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toGraveyard);
}
@@ -424,13 +391,13 @@ public class PlayerControllerAi extends PlayerController {
if (destinationZone == ZoneType.Graveyard) {
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
if (Iterables.any(getGame().getCardsInGame(), new Predicate<Card>() {
if (!CardLists.filter(getGame().getCardsInGame(), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter");
}
})) {
}).isEmpty()) {
int bestValue = 0;
Card bestCreature = null;
for (Card c : cards) {
@@ -471,7 +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 (landsOTB <= 2) {
@@ -533,7 +500,8 @@ public class PlayerControllerAi extends PlayerController {
if (copySA instanceof Spell) {
Spell spell = (Spell) copySA;
((PlayerControllerAi) player.getController()).getAi().canPlayFromEffectAI(spell, true, true);
} else {
}
else {
getAi().canPlaySa(copySA);
}
}
@@ -544,7 +512,7 @@ public class PlayerControllerAi extends PlayerController {
public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) {
if (canSetupTargets)
brains.doTrigger(effectSA, true); // first parameter does not matter, since return value won't be used
ComputerUtil.playNoStack(player, effectSA, getGame(), true);
ComputerUtil.playNoStack(player, effectSA, getGame());
}
@Override
@@ -552,16 +520,15 @@ public class PlayerControllerAi extends PlayerController {
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
public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) {
String [] splitUTypes = uType.split(",");
CardCollection cardsOfType = new CardCollection();
for (String part : splitUTypes) {
CardCollection partCards = CardLists.getType(hand, part);
if (!partCards.isEmpty()) {
cardsOfType.addAll(partCards);
}
}
final CardCollectionView cardsOfType = CardLists.getType(hand, uType);
if (!cardsOfType.isEmpty()) {
Card toDiscard = Aggregates.itemWithMin(cardsOfType, CardPredicates.Accessors.fnGetCmc);
return new CardCollection(toDiscard);
@@ -569,18 +536,19 @@ public class PlayerControllerAi extends PlayerController {
return getAi().getCardsToDiscard(num, null, sa);
}
@Override
public Mana chooseManaFromPool(List<Mana> manaChoices) {
return manaChoices.get(0); // no brains used
}
@Override
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes, boolean isOptional) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), validTypes, invalidTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) {
chosen = validTypes.iterator().next();
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: chosen");
public List<String> chooseSomeType(String kindOfType, SpellAbility sa, int min, int max, List<String> validTypes) {
List<String> chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), min, max, validTypes);
if (chosen.isEmpty()) {
return validTypes.subList(0, min);
}
getGame().getAction().notifyOfValue(sa, player, chosen.toString(), player);
return chosen;
}
@@ -590,8 +558,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
return brains.aiShouldRun(replacementEffect, effectSA, affected);
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question) {
return brains.aiShouldRun(replacementEffect, effectSA);
}
@Override
@@ -620,7 +588,7 @@ public class PlayerControllerAi extends PlayerController {
hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.count(toReturn, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.filter(toReturn, Presets.LANDS).size();
// If we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {
@@ -668,9 +636,10 @@ public class PlayerControllerAi extends PlayerController {
public List<SpellAbility> chooseSpellAbilityToPlay() {
return brains.chooseSpellAbilityToPlay();
}
@Override
public boolean playChosenSpellAbility(SpellAbility sa) {
// System.out.println("Playing sa: " + sa);
if (sa instanceof LandAbility) {
if (sa.canPlay()) {
sa.resolve();
@@ -680,7 +649,7 @@ public class PlayerControllerAi extends PlayerController {
ComputerUtil.handlePlayingSpellAbility(player, sa, getGame());
}
return true;
}
}
@Override
public CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard) {
@@ -695,10 +664,8 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean payManaOptional(Card c, Cost cost, SpellAbility sa, String prompt, ManaPaymentPurpose purpose) {
// TODO replace with EmptySa
final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
ability.setActivatingPlayer(c.getController());
ability.setCardState(sa.getCardState());
// FIXME: This is a hack to check if the AI can play the "exile from library" pay costs (Cumulative Upkeep,
// e.g. Thought Lash). We have to do it and bail early if the AI can't pay, because otherwise the AI will
@@ -718,8 +685,8 @@ public class PlayerControllerAi extends PlayerController {
}
// - End of hack for Exile a card from library Cumulative Upkeep -
if (ComputerUtilCost.canPayCost(ability, c.getController(), true)) {
ComputerUtil.playNoStack(c.getController(), ability, getGame(), true);
if (ComputerUtilCost.canPayCost(ability, c.getController())) {
ComputerUtil.playNoStack(c.getController(), ability, getGame());
return true;
}
return false;
@@ -735,7 +702,7 @@ public class PlayerControllerAi extends PlayerController {
public int chooseNumber(SpellAbility sa, String title, int min, int max) {
return brains.chooseNumber(sa, title, min, max);
}
@Override
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map<String, Object> params) {
ApiType api = sa.getApi();
@@ -780,6 +747,7 @@ public class PlayerControllerAi extends PlayerController {
return allTargets.get(0);
}
@Override
public void notifyOfValue(SpellAbility saSource, GameObject realtedTarget, String value) {
// AI should take into consideration creature types, numbers and other information (mostly choices) arriving through this channel
@@ -802,7 +770,7 @@ public class PlayerControllerAi extends PlayerController {
return true;
} else {
Card rem = (Card) source.getFirstRemembered();
if (!rem.isInPlay()) {
if (!rem.isInZone(ZoneType.Battlefield)) {
return true;
}
}
@@ -810,8 +778,7 @@ public class PlayerControllerAi extends PlayerController {
case "BetterTgtThanRemembered":
if (source.getRememberedCount() > 0) {
Card rem = (Card) source.getFirstRemembered();
// avoid pumping opponent creature
if (!rem.isInPlay() || rem.getController().isOpponentOf(source.getController())) {
if (!rem.isInZone(ZoneType.Battlefield)) {
return true;
}
for (Card c : source.getController().getCreaturesInPlay()) {
@@ -837,7 +804,7 @@ public class PlayerControllerAi extends PlayerController {
/*
* (non-Javadoc)
*
*
* @see
* forge.game.player.PlayerController#chooseBinary(forge.game.spellability.
* SpellAbility, java.lang.String,
@@ -878,15 +845,16 @@ public class PlayerControllerAi extends PlayerController {
}
return sa.getChosenList();
}
@Override
public byte chooseColorAllowColorless(String message, Card card, ColorSet colors) {
final String c = ComputerUtilCard.getMostProminentColor(player.getCardsIn(ZoneType.Hand));
byte chosenColorMask = MagicColor.fromName(c);
if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask;
} else {
return Iterables.getFirst(colors, (byte)0);
}
return Iterables.getFirst(colors, (byte)0);
}
@Override
@@ -905,7 +873,9 @@ public class PlayerControllerAi extends PlayerController {
if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask;
}
return Iterables.getFirst(colors, MagicColor.WHITE);
else {
return Iterables.getFirst(colors, MagicColor.WHITE);
}
}
@Override
@@ -921,17 +891,13 @@ public class PlayerControllerAi extends PlayerController {
/*
* (non-Javadoc)
*
*
* @see forge.game.player.PlayerController#chooseCounterType(java.util.List,
* forge.game.spellability.SpellAbility, java.lang.String, java.util.Map)
*/
@Override
public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, String prompt,
Map<String, Object> params) {
// short cut if there is no options to choose
if (options.size() <= 1) {
return Iterables.getFirst(options, null);
}
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
@@ -941,7 +907,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean confirmPayment(CostPart costPart, String prompt, SpellAbility sa) {
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
}
@Override
@@ -1001,7 +967,10 @@ public class PlayerControllerAi extends PlayerController {
}
final String logic = sa.getParam("AILogic");
if (logic == null || logic.equals("MostProminentHumanCreatures")) {
CardCollection list = player.getOpponents().getCreaturesInPlay();
CardCollection list = new CardCollection();
for (Player opp : player.getOpponents()) {
list.addAll(opp.getCreaturesInPlay());
}
if (list.isEmpty()) {
list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents());
}
@@ -1015,15 +984,12 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) {
final Card source = sa.getHostCard();
// TODO replace with EmptySa
final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } };
emptyAbility.setActivatingPlayer(player);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setSVars(sa.getSVars());
emptyAbility.setCardState(sa.getCardState());
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player, true)) {
ComputerUtil.playNoStack(player, emptyAbility, getGame(), true); // AI needs something to resolve to pay that cost
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) {
ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost
return true;
}
return false;
@@ -1040,9 +1006,9 @@ public class PlayerControllerAi extends PlayerController {
if (sa.isCopied()) {
if (sa.isSpell()) {
if (!sa.getHostCard().isInZone(ZoneType.Stack)) {
sa.setHostCard(getGame().getAction().moveToStack(sa.getHostCard(), sa));
sa.setHostCard(player.getGame().getAction().moveToStack(sa.getHostCard(), sa));
} else {
getGame().getStackZone().add(sa.getHostCard());
player.getGame().getStackZone().add(sa.getHostCard());
}
}
@@ -1053,18 +1019,18 @@ public class PlayerControllerAi extends PlayerController {
*/
if (sa.isMayChooseNewTargets() && !sa.setupTargets()) {
if (sa.isSpell()) {
getGame().getAction().ceaseToExist(sa.getHostCard(), false);
sa.getHostCard().ceaseToExist();
}
continue;
}
}
// need finally add the new spell to the stack
getGame().getStack().add(sa);
player.getGame().getStack().add(sa);
}
}
}
private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory) {
private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory){
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(host, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
@@ -1077,7 +1043,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public void playTrigger(Card host, WrappedAbility wrapperAbility, boolean isMandatory) {
if (prepareSingleSa(host, wrapperAbility, isMandatory)) {
ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, getGame(), true);
ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, getGame());
}
}
@@ -1087,14 +1053,14 @@ public class PlayerControllerAi extends PlayerController {
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
Spell spell = (Spell) tgtSA;
// TODO if mandatory AI is only forced to use mana when it's already in the pool
if (tgtSA.checkRestrictions(brains.getPlayer()) && (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional)) {
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
if (noManaCost) {
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
} else {
return ComputerUtil.playStack(tgtSA, player, getGame());
}
return ComputerUtil.playStack(tgtSA, player, getGame());
}
return false; // didn't play spell
} else
return false; // didn't play spell
}
return true;
}
@@ -1104,12 +1070,6 @@ public class PlayerControllerAi extends PlayerController {
return brains.doTrigger(currentAbility, true);
}
@Override
public TargetChoices chooseNewTargetsFor(SpellAbility ability, Predicate<GameObject> filter, boolean optional) {
// AI currently can't do this. But when it can it will need to be based on Ability API
return null;
}
@Override
public boolean chooseCardsPile(SpellAbility sa, CardCollectionView pile1, CardCollectionView pile2, String faceUp) {
if (faceUp.equals("True")) {
@@ -1123,6 +1083,7 @@ public class PlayerControllerAi extends PlayerController {
boolean allCreatures = Iterables.all(Iterables.concat(pile1, pile2), CardPredicates.Presets.CREATURES);
int cmc1 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile1) : ComputerUtilCard.evaluatePermanentList(pile1);
int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2);
System.out.println("value:" + cmc1 + " " + cmc2);
// for now, this assumes that the outcome will be bad
// TODO: This should really have a ChooseLogic param to
@@ -1135,17 +1096,9 @@ public class PlayerControllerAi extends PlayerController {
public void revealAnte(String message, Multimap<Player, PaperCard> removedAnteCards) {
// Ai won't understand that anyway
}
@Override
public void revealAISkipCards(String message, Map<Player, Map<DeckSection, List<? extends PaperCard>>> deckCards) {
// Ai won't understand that anyway
}
@Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto
setupAutoProfile(myDeck);
public Collection<? extends PaperCard> complainCardsCantPlayWell(Deck myDeck) {
return brains.complainCardsCantPlayWell(myDeck);
}
@@ -1161,8 +1114,10 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean effect) {
return ComputerUtilMana.payManaCost(player, sa, effect);
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean isActivatedSa) {
// TODO Auto-generated method stub
ManaCostBeingPaid cost = isActivatedSa ? ComputerUtilMana.calculateManaCost(sa, false, 0) : new ManaCostBeingPaid(toPay);
return ComputerUtilMana.payManaCost(cost, sa, player);
}
@Override
@@ -1189,7 +1144,7 @@ public class PlayerControllerAi extends PlayerController {
return new HashMap<>();
}
}
//Do not convoke potential blockers until after opponent's attack
final CardCollectionView blockers = ComputerUtilCard.getLikelyBlockers(ai, null);
if ((ph.isPlayerTurn(ai) && ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)) ||
@@ -1212,16 +1167,10 @@ public class PlayerControllerAi extends PlayerController {
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) {
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = player.getStrongestOpponent().getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = player.getWeakestOpponent().getCardsIn(ZoneType.Library);
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic");
// Filter for valid options only
if (!valid.isEmpty()) {
aiLibrary = CardLists.getValidCards(aiLibrary, valid, source.getController(), source, sa);
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
}
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
// If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name)
@@ -1310,27 +1259,6 @@ public class PlayerControllerAi extends PlayerController {
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces);
}
@Override
public Card chooseDungeon(Player ai, List<PaperCard> dungeonCards, String message) {
// TODO: improve the conditions that define which dungeon is a viable option to choose
List<String> dungeonNames = Lists.newArrayList();
for (PaperCard pc : dungeonCards) {
dungeonNames.add(pc.getName());
}
// Don't choose Tomb of Annihilation when life in danger unless we can win right away or can't lose for 0 life
if (ai.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player?
int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
if ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife())
&& !(ai.getLife() > 1 && ai.getWeakestOpponent().getLife() == 1)) {
dungeonNames.remove("Tomb of Annihilation");
}
}
int i = MyRandom.getRandom().nextInt(dungeonNames.size());
return Card.fromPaperCard(dungeonCards.get(i), ai);
}
@Override
public List<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards) {
// sort from best to worst
@@ -1353,7 +1281,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) {
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen,
List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();
@@ -1373,7 +1302,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) {
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
}
@@ -1389,7 +1318,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public int chooseNumberForKeywordCost(SpellAbility sa, Cost cost, KeywordInterface keyword, String prompt, int max) {
public int chooseNumberForKeywordCost(SpellAbility sa, Cost cost, KeywordInterface keyword, String prompt,
int max) {
// TODO: improve the logic depending on the keyword and the playability of the cost-modified SA (enough targets present etc.)
int chosenAmount = 0;
@@ -1398,7 +1328,7 @@ public class PlayerControllerAi extends PlayerController {
for (int i = 0; i < max; i++) {
costSoFar.add(cost);
SpellAbility fullCostSa = sa.copyWithDefinedCost(costSoFar);
if (ComputerUtilCost.canPayCost(fullCostSa, player, sa.isTrigger())) {
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenAmount++;
} else {
break;

View File

@@ -344,7 +344,7 @@ public class SpecialAiLogic {
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 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.List;
import forge.game.GameEntity;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
@@ -40,6 +39,7 @@ import forge.game.ability.ApiType;
import forge.game.card.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;
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
public static class ChainOfAcid {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -216,9 +159,10 @@ public class SpecialCardAi {
final PhaseHandler ph = ai.getGame().getPhaseHandler();
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)) {
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 isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
@@ -226,6 +170,10 @@ public class SpecialCardAi {
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
@@ -379,7 +327,7 @@ public class SpecialCardAi {
}
// Do not activate if damage will be prevented
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
if (source.staticDamagePrevention(predictedPT.getLeft(), source, true, true) == 0) {
return false;
}
@@ -400,7 +348,7 @@ public class SpecialCardAi {
}
boolean isBlocking = combat.isBlocking(source);
boolean cantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
boolean cantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
CardCollection opposition = isBlocking ? combat.getAttackersBlockedBy(source) : combat.getBlockers(source);
int oppP = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetAttack);
@@ -414,7 +362,7 @@ public class SpecialCardAi {
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY);
int totalDamageToPW = 0;
for (Card atk :combat.getAttackersOf(combat.getDefenderByAttacker(source))) {
for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) {
if (combat.isUnblocked(atk)) {
totalDamageToPW += atk.getNetCombatDamage();
}
@@ -434,7 +382,7 @@ public class SpecialCardAi {
if (c.hasKeyword(Keyword.FIRST_STRIKE) || c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
oppHasFirstStrike = true;
}
if (!ComputerUtilCombat.combatantCantBeDestroyed(c.getController(), c)) {
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) {
oppCantDie = false;
}
}
@@ -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
public static class ForceOfWill {
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,
// otherwise the AI will fail to play the card and the card will disappear from the pool
return false;
} else if (!Iterables.any(blueCards, CardPredicates.lessCMC(3))) {
} else if (CardLists.filter(blueCards, CardPredicates.lessCMC(3)).isEmpty()) {
// We probably need a low-CMC card to exile to it, exiling a higher CMC spell may be suboptimal
// since the AI does not prioritize/value cards vs. permission at the moment.
return false;
@@ -643,28 +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
public static class GuiltyConscience {
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(),
sa.getParamOrDefault("ChangeNum", "1"), sa);
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("ChangeNum"), sa);
CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library),
Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName())));
Collections.sort(lib, CardLists.CmcComparatorInv);
@@ -790,7 +689,7 @@ public class SpecialCardAi {
for (Card c1 : lib) {
if (c1.getName().equals(c.getName())) {
if (!Iterables.any(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName()))
if (CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName())).isEmpty()
&& ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) {
// Try not to search for things we already have in hand or that we can't cast
libPriorityList.add(c1);
@@ -906,7 +805,7 @@ public class SpecialCardAi {
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
return gate;
@@ -925,7 +824,7 @@ public class SpecialCardAi {
public static Card considerCardFromList(final CardCollection fetchList) {
for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) {
for (SpellAbility ab : c.getSpellAbilities()) {
if (ab.isActivatedAbility()) {
if (ab.isAbility() && !ab.isTrigger()) {
Player controller = c.getController();
boolean wasCaged = false;
for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile),
@@ -995,7 +894,7 @@ public class SpecialCardAi {
}
// Set PayX here to maximum value.
int tokenSize = ComputerUtilCost.getMaxXValue(sa, ai, false);
int tokenSize = ComputerUtilCost.getMaxXValue(sa, ai);
// Some basic strategy for Momir
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
public static class Necropotence {
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
}
if (Iterables.any(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
if (!CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain")).isEmpty()) {
// Prefer Yawgmoth's Bargain because AI is generally better with it
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
@@ -1074,7 +940,7 @@ public class SpecialCardAi {
}
// TODO: Any other bad effects like that?
boolean blackViseOTB = Iterables.any(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise"));
boolean blackViseOTB = !CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).isEmpty();
if (ph.getNextTurn().equals(ai) && ph.is(PhaseType.MAIN2)
&& ai.getSpellsCastLastTurn() == 0
@@ -1149,7 +1015,7 @@ public class SpecialCardAi {
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana();
boolean canPayWithAvailableColors = cost.canBePaidWithAvailable(ColorSet.fromNames(
boolean canPayWithAvailableColors = cost.canBePaidWithAvaliable(ColorSet.fromNames(
ComputerUtilCost.getAvailableManaColors(ai, sa.getHostCard())).getColor());
byte colorProfile = cost.getColorProfile();
@@ -1207,33 +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
public static class PriceOfProgress {
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
public static class SorinVengefulBloodlord {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1605,7 +1399,7 @@ public class SpecialCardAi {
if (topGY == null
|| !topGY.isCreature()
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false);
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0);
}
}
@@ -1694,7 +1488,7 @@ public class SpecialCardAi {
int maxHandSize = ai.getMaxHandSize();
// TODO: Any other bad effects like that?
boolean blackViseOTB = Iterables.any(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise"));
boolean blackViseOTB = !CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).isEmpty();
// TODO: Consider effects like "whenever a player draws a card, he loses N life" (e.g. Nekusar, the Mindraiser),
// and effects that draw an additional card whenever a card is drawn.
@@ -1770,7 +1564,7 @@ public class SpecialCardAi {
int CMC = ab.getPayCosts().getTotalMana() != null ? ab.getPayCosts().getTotalMana().getCMC() : 0;
int Xcount = ab.getPayCosts().getTotalMana() != null ? ab.getPayCosts().getTotalMana().countX() : 0;
if ((Xcount == 0 && CMC == 0) || ComputerUtilMana.canPayManaCost(ab, ai, selfCMC + minManaAdj, false)) {
if ((Xcount == 0 && CMC == 0) || ComputerUtilMana.canPayManaCost(ab, ai, selfCMC + minManaAdj)) {
if (src.isInstant() || src.isSorcery()) {
// instants and sorceries are one-shot, so only treat them as 1/2 value for the purpose of meeting minimum
// castable cards in graveyard requirements

View File

@@ -1,7 +1,12 @@
package forge.ai;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.card.ICardFace;
import forge.card.mana.ManaCost;
@@ -19,13 +24,8 @@ import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Base class for API-specific AI logic
* <p>
@@ -58,11 +58,6 @@ public abstract class SpellAbilityAi {
final Card source = sa.getHostCard();
final Cost cost = sa.getPayCosts();
if (sa.hasParam("AICheckCanPlayWithDefinedX")) {
// FIXME: can this somehow be simplified without the need for an extra AI hint?
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
}
if (!checkConditions(ai, sa, sa.getConditions())) {
SpellAbility sub = sa.getSubAbility();
if (sub != null && !checkConditions(ai, sub, sub.getConditions())) {
@@ -72,12 +67,10 @@ public abstract class SpellAbilityAi {
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
final boolean alwaysOnDiscard = "AlwaysOnDiscard".equals(logic) && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
if (!checkAiLogic(ai, sa, logic)) {
return false;
}
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return false;
}
} else {
@@ -104,7 +97,7 @@ public abstract class SpellAbilityAi {
if (!con.getManaSpent().isEmpty()) {
// need to use ManaCostBeingPaid check, can't use Cost#canPay
ManaCostBeingPaid paid = new ManaCostBeingPaid(new ManaCost(new ManaCostParser(con.getManaSpent())));
if (ComputerUtilMana.canPayManaCost(paid, sa, ai, sa.isTrigger())) {
if (ComputerUtilMana.canPayManaCost(paid, sa, ai)) {
con.setManaSpent("");
}
}
@@ -162,39 +155,40 @@ public abstract class SpellAbilityAi {
*/
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop
return false; // prevent infinite loop
}
return MyRandom.getRandom().nextFloat() < .8f; // random success
return MyRandom.getRandom().nextFloat() < .8f; // random success
}
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
// this evaluation order is currently intentional as it does more stuff that helps avoiding some crashes
if (!ComputerUtilCost.canPayCost(sa, aiPlayer, true) && !mandatory) {
if (!ComputerUtilCost.canPayCost(sa, aiPlayer) && !mandatory) {
return false;
}
// a mandatory SpellAbility with targeting but without candidates,
// does not need to go any deeper
if (sa.usesTargeting() && mandatory && sa.getTargetRestrictions().getNumCandidates(sa, true) == 0) {
return sa.isTargetNumberValid();
if (sa.usesTargeting() && mandatory && !sa.isTargetNumberValid()
&& !sa.getTargetRestrictions().hasCandidates(sa, true)) {
return false;
}
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory);
}
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory)
{
if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
return false;
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
}
/**
* Handles the AI decision to play a triggered SpellAbility
*/
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
if (canPlayWithoutRestrict(aiPlayer, sa)) {
return true;
}
@@ -213,7 +207,7 @@ public abstract class SpellAbilityAi {
// try to target opponent, then ally, then itself
for (final Player p : players) {
if (sa.canTarget(p)) {
if (p.canBeTargetedBy(sa) && sa.canTarget(p)) {
sa.resetTargets();
sa.getTargets().add(p);
return true;
@@ -232,7 +226,7 @@ public abstract class SpellAbilityAi {
// sub-SpellAbility might use targets too
if (sa.usesTargeting()) {
// no Candidates, no adding to Stack
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
if (!sa.getTargetRestrictions().hasCandidates(sa, true)) {
return false;
}
// but if it does, it should override this function
@@ -251,11 +245,11 @@ public abstract class SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
protected static boolean isSorcerySpeed(final SpellAbility sa) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|| (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."));
}
/**
@@ -290,6 +284,7 @@ public abstract class SpellAbilityAi {
if (sa.isSpell() && !sa.isBuyBackAbility()) {
return false;
}
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
}
@@ -304,7 +299,7 @@ public abstract class SpellAbilityAi {
final AbilitySub subAb = ab.getSubAbility();
return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
System.err.println("Warning: default (ie. inherited from base class) implementation of confirmAction is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,16 @@
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.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiAttackController;
import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.AiProps;
@@ -49,7 +51,6 @@ import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
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")) {
// Set PayX here to maximum value. (Endless Scream and Venarian Gold)
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
// Set PayX here to maximum value. (Endless Scream and Venarian
// Gold)
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
if (xPay == 0) {
return false;
@@ -125,9 +127,15 @@ public class AttachAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
effectExile.setActivatingPlayer(ai);
final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0);
final TargetRestrictions exile_tgt = effectExile.getTargetRestrictions();
final List<Card> targets = CardUtil.getValidCardsToTarget(exile_tgt, effectExile);
final CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(origin), exile_tgt.getValidTgts(), ai, source, effectExile);
final CardCollection targets = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
}
});
return !targets.isEmpty();
}
@@ -227,7 +235,7 @@ public class AttachAi extends SpellAbilityAi {
boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
boolean willDiscardNow = game.getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
&& ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
boolean willRespondToStack = canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack);
boolean willCastEarly = MyRandom.percentTrue(chanceToCastEarly);
@@ -347,14 +355,14 @@ public class AttachAi extends SpellAbilityAi {
public boolean apply(final Card c) {
//Check for cards that can be sacrificed in response
for (final SpellAbility ability : c.getAllSpellAbilities()) {
if (ability.isActivatedAbility()) {
if (ability.isAbility()) {
final Cost cost = ability.getPayCosts();
for (final CostPart part : cost.getCostParts()) {
if (!(part instanceof CostSacrifice)) {
continue;
}
CostSacrifice sacCost = (CostSacrifice) part;
if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController(), false)) {
if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController())) {
return false;
}
}
@@ -425,7 +433,8 @@ public class AttachAi extends SpellAbilityAi {
SpellAbility auraSA = aura.getSpells().get(0);
if (auraSA.getApi() == ApiType.Attach) {
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) {
// Don't attach multiple KeepTapped Auras to one card
// Don't attach multiple KeepTapped Auras to one
// card
return false;
}
}
@@ -446,15 +455,22 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach to player ai preferences.
*
* @param sa
* the sa
* @param mandatory
* the mandatory
* @param newParam TODO
*
* @return the player
*/
public static Player attachToPlayerAIPreferences(final Player aiPlayer, final SpellAbility sa, final boolean mandatory, List<Player> targetable) {
private static Player attachToPlayerAIPreferences(final Player aiPlayer, final SpellAbility sa,
final boolean mandatory) {
List<Player> targetable = new ArrayList<>();
for (final Player player : aiPlayer.getGame().getPlayers()) {
if (sa.canTarget(player)) {
targetable.add(player);
}
}
if ("Curse".equals(sa.getParam("AILogic"))) {
if (!mandatory) {
targetable.removeAll(aiPlayer.getAllies());
@@ -538,7 +554,12 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
evenBetterList = CardLists.filter(betterList, CardPredicates.Presets.UNTAPPED);
evenBetterList = CardLists.filter(betterList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.isUntapped();
}
});
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
@@ -712,15 +733,17 @@ public class AttachAi extends SpellAbilityAi {
*/
private static Card attachAISpecificCardPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource) {
// I know this isn't much better than Hardcoding, but some cards need it for now
final Player ai = sa.getActivatingPlayer();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
Card chosen = null;
if ("Guilty Conscience".equals(sourceName)) {
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
} else if (sa.hasParam("AIValid")) {
// TODO: Make the AI recognize which cards to pump based on the card's abilities alone
chosen = doPumpOrCurseAILogic(ai, sa, list, sa.getParam("AIValid"));
} else if ("Bonds of Faith".equals(sourceName)) {
chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
} else if ("Clutch of Undeath".equals(sourceName)) {
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
}
// If Mandatory (brought directly into play without casting) gotta
@@ -744,7 +767,7 @@ public class AttachAi extends SpellAbilityAi {
int powerBuff = 0;
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) {
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), stAb);
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), null);
}
}
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
@@ -793,6 +816,7 @@ public class AttachAi extends SpellAbilityAi {
// grab Planeswalker first, then Creature
// if Life < 5 grab Creature first, then Planeswalker. Lands,
// Enchantments and Artifacts are probably "not good enough"
}
final Card c = ComputerUtilCard.getBestAI(list);
@@ -910,7 +934,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
return ComputerUtilCombat.canAttackNextTurn(c) && c.getNetPower() > 0;
}
});
}
@@ -949,6 +973,7 @@ public class AttachAi extends SpellAbilityAi {
return acceptableChoice(c, mandatory);
}
/**
* Attach do trigger ai.
* @param sa
@@ -965,9 +990,9 @@ public class AttachAi extends SpellAbilityAi {
List<GameObject> targets = new ArrayList<>();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
targets = AbilityUtils.getDefinedObjects(card, sa.getParam("Defined"), sa);
targets = AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
} else {
attachPreference(sa, tgt, mandatory);
AttachAi.attachPreference(sa, tgt, mandatory);
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) {
GameObject o;
if (tgt.canTgtPlayer()) {
List<Player> targetable = new ArrayList<>();
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) {
targetable.add(player);
}
}
o = attachToPlayerAIPreferences(sa.getActivatingPlayer(), sa, mandatory, targetable);
o = attachToPlayerAIPreferences(sa.getActivatingPlayer(), sa, mandatory);
} else {
o = attachToCardAIPreferences(sa.getActivatingPlayer(), sa, mandatory);
}
@@ -1104,12 +1123,14 @@ public class AttachAi extends SpellAbilityAi {
list.removeAll(toRemove);
if (magnetList != null) {
// Look for Heroic triggers
if (magnetList.isEmpty() && sa.isSpell()) {
for (Card target : list) {
for (Trigger t : target.getTriggers()) {
if (t.getMode() == TriggerType.SpellCast) {
if ("Card.Self".equals(t.getParam("TargetsValid")) && "You".equals(t.getParam("ValidActivatingPlayer"))) {
final Map<String, String> params = t.getMapParams();
if ("Card.Self".equals(params.get("TargetsValid")) && "You".equals(params.get("ValidActivatingPlayer"))) {
magnetList.add(target);
break;
}
@@ -1167,7 +1188,7 @@ public class AttachAi extends SpellAbilityAi {
if (affected == null) {
continue;
}
if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
@@ -1272,7 +1293,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.isCreature()) {
return true;
}
return powerBonus + c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
return ComputerUtilCombat.canAttackNextTurn(c) && powerBonus + c.getNetPower() > 0;
}
});
}
@@ -1317,28 +1338,6 @@ public class AttachAi extends SpellAbilityAi {
return null;
}
// Is a SA that moves target attachment
if ("MoveTgtAura".equals(sa.getParam("AILogic"))) {
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa));
list = CardLists.filter(list, Predicates.or(CardPredicates.isControlledByAnyOf(aiPlayer.getOpponents()), new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
return ComputerUtilCard.isUselessCreature(aiPlayer, card.getAttachedTo());
}
}));
return !list.isEmpty() ? ComputerUtilCard.getBestAI(list) : null;
} else if ("Unenchanted".equals(sa.getParam("AILogic"))) {
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(tgt, sa));
CardCollection preferred = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
return !card.hasCardAttachments();
}
});
return preferred.isEmpty() ? Aggregates.random(list) : Aggregates.random(preferred);
}
// is no attachment so no using attach
if (!attachSource.isAttachment()) {
return null;
@@ -1352,9 +1351,21 @@ public class AttachAi extends SpellAbilityAi {
CardCollection list = null;
if (tgt == null) {
list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa);
list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
} else {
list = CardLists.filter(CardUtil.getValidCardsToTarget(tgt, sa), CardPredicates.canBeAttached(attachSource));
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
list = CardLists.filter(list, CardPredicates.canBeAttached(attachSource));
// TODO If Attaching without casting, don't need to actually target.
// I believe this is the only case where mandatory will be true, so just
// check that when starting that work
// But we shouldn't attach to things with Protection
if (!mandatory) {
list = CardLists.getTargetableCards(list, sa);
} else {
list = CardLists.filter(list, Predicates.not(CardPredicates.isProtectedFrom(attachSource)));
}
}
if (list.isEmpty()) {
@@ -1439,22 +1450,12 @@ public class AttachAi extends SpellAbilityAi {
* the logic
* @return the card
*/
public static Card attachGeneralAI(final Player ai, final SpellAbility sa, final List<Card> list, final boolean mandatory,
private static Card attachGeneralAI(final Player ai, final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource, final String logic) {
// AI logic types that do not require a prefList and that evaluate the
// usefulness of attach action autonomously
if ("InstantReequipPowerBuff".equals(logic)) {
return attachAIInstantReequipPreference(sa, attachSource);
}
Player prefPlayer;
if ("Pump".equals(logic) || "Animate".equals(logic) || "Curiosity".equals(logic) || "MoveTgtAura".equals(logic)
|| "MoveAllAuras".equals(logic)) {
Player prefPlayer = ai.getWeakestOpponent();
if ("Pump".equals(logic) || "Animate".equals(logic) || "Curiosity".equals(logic)) {
prefPlayer = ai;
} else {
prefPlayer = AiAttackController.choosePreferredDefenderPlayer(ai);
}
// Some ChangeType cards are beneficial, and PrefPlayer should be
// changed to represent that
final List<Card> prefList;
@@ -1466,18 +1467,25 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filterControlledBy(list, prefPlayer);
}
// AI logic types that do not require a prefList and that evaluate the
// usefulness of attach action autonomously
if ("InstantReequipPowerBuff".equals(logic)) {
return attachAIInstantReequipPreference(sa, attachSource);
}
// If there are no preferred cards, and not mandatory bail out
if (logic == null || prefList.isEmpty()) {
if (prefList.isEmpty()) {
return chooseUnpreferred(mandatory, list);
}
// Preferred list has at least one card in it to make to the actual Logic
// Preferred list has at least one card in it to make to the actual
// Logic
Card c = null;
if ("GainControl".equals(logic)) {
c = attachAIControlPreference(sa, prefList, mandatory, attachSource);
} else if ("Curse".equals(logic)) {
c = attachAICursePreference(sa, prefList, mandatory, attachSource, ai);
} else if ("Pump".equals(logic) || logic.startsWith("Move")) {
} else if ("Pump".equals(logic)) {
c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource);
} else if ("Curiosity".equals(logic)) {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
@@ -1555,10 +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("Flying") || keyword.equals("Horsemanship")
|| keyword.endsWith("walk") || keyword.equals("All creatures able to block CARDNAME do so.");
|| keyword.endsWith("walk") || keyword.startsWith("CantBeBlockedBy")
|| keyword.equals("All creatures able to block CARDNAME do so."));
// give evasive keywords to creatures that can attack and deal damage
boolean canBeBlocked = false;
@@ -1570,8 +1579,8 @@ public class AttachAi extends SpellAbilityAi {
if (evasive) {
return card.getNetCombatDamage() + powerBonus > 0
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
} else if (keyword.equals("Haste")) {
return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped()
&& card.getNetCombatDamage() + powerBonus > 0
@@ -1587,12 +1596,12 @@ public class AttachAi extends SpellAbilityAi {
return card.getNetCombatDamage() + powerBonus > 0
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.equals("First Strike")) {
return card.getNetCombatDamage() + powerBonus > 0 && !card.hasDoubleStrike()
return card.getNetCombatDamage() + powerBonus > 0 && !card.hasKeyword(Keyword.DOUBLE_STRIKE)
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
} else if (keyword.startsWith("Bushido")) {
return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card))
|| CombatUtil.canBlock(card, true);
@@ -1634,8 +1643,10 @@ public class AttachAi extends SpellAbilityAi {
if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender")
|| keyword.endsWith("CARDNAME can't attack or block.")) {
return card.getNetCombatDamage() >= 1 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME can't block.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
} else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty();
} else if (keyword.endsWith("CARDNAME can't block.") || keyword.contains("CantBlock")) {
return CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
for (SpellAbility ability : card.getSpellAbilities()) {
@@ -1645,10 +1656,10 @@ public class AttachAi extends SpellAbilityAi {
}
return false;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 1 && ComputerUtilCombat.canAttackNextTurn(card);
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 2;
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
}
@@ -1688,7 +1699,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.getController().equals(ai)) {
return false;
}
return c.isValid(type, ai, sa.getHostCard(), sa);
return c.getType().hasCreatureType(type);
}
});
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
@@ -1698,7 +1709,7 @@ public class AttachAi extends SpellAbilityAi {
if (c.getController().equals(ai)) {
return false;
}
return !c.isValid(type, ai, sa.getHostCard(), sa) && !ComputerUtilCard.isUselessCreature(ai, c);
return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
}
});
@@ -1715,26 +1726,6 @@ public class AttachAi extends SpellAbilityAi {
return chosen;
}
@Override
public boolean chkAIDrawback(final SpellAbility sa, final Player ai) {
// TODO for targeting optional Halvar trigger, needs to be coordinated with PumpAi to make it playable
if (sa.isTrigger() && sa.usesTargeting()) {
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
if (tgt != null) {
sa.resetTargets();
sa.getTargets().add(tgt);
}
return sa.isTargetNumberValid();
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return true;
}
return false;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
@@ -1742,11 +1733,11 @@ public class AttachAi extends SpellAbilityAi {
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
return attachGeneralAI(ai, sa, (List<Card>)options, !isOptional, sa.getHostCard(), sa.getParam("AILogic"));
return attachToCardAIPreferences(ai, sa, true);
}
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
return attachToPlayerAIPreferences(ai, sa, true, (List<Player>)options);
return attachToPlayerAIPreferences(ai, sa, true);
}
}

View File

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

View File

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

View File

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

View File

@@ -49,6 +49,7 @@ public final class BondAi extends SpellAbilityAi {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
} // end bondCanPlayAI()
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {

View File

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

View File

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

View File

@@ -1,28 +1,55 @@
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.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
import forge.card.CardType;
import forge.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.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -30,14 +57,8 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
public class ChangeZoneAi extends SpellAbilityAi {
/*
@@ -53,9 +74,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override
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
sa.getHostCard().removeSVar("AIPreferenceOverride");
host.removeSVar("AIPreferenceOverride");
}
if (aiLogic.equals("BeforeCombat")) {
@@ -69,7 +92,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (aiLogic.equals("PriorityOptionalCost")) {
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
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
highPriority |= ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat());
@@ -104,6 +127,40 @@ public class ChangeZoneAi extends SpellAbilityAi {
return true;
} else if (aiLogic.equals("Pongify")) {
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);
@@ -119,13 +176,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic.equals("Always")) {
return true;
} 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.
return doSacAndReturnFromGraveLogic(aiPlayer, sa);
return this.doSacAndReturnFromGraveLogic(aiPlayer, sa);
} else if (aiLogic.equals("Necropotence")) {
return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
} else if (aiLogic.equals("SameName")) { // Declaration in Stone
return doSameNameLogic(aiPlayer, sa);
return this.doSameNameLogic(aiPlayer, sa);
} else if (aiLogic.equals("ReanimateAll")) {
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
} else if (aiLogic.equals("TheScarabGod")) {
@@ -140,9 +197,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
} else if (aiLogic.equals("Pongify")) {
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 knownOriginCanPlayAI(aiPlayer, sa);
@@ -159,12 +223,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.isHidden()) {
if (isHidden(sa)) {
return hiddenOriginPlayDrawbackAI(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>
* changeZoneTriggerAINoCost.
@@ -178,16 +251,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/
@Override
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"))) {
// Process the commander replacement effect ("return to Command zone instead")
return doReturnCommanderLogic(sa, aiPlayer);
}
if ("Always".equals(aiLogic)) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
} else if ("IfNotBuffed".equals(aiLogic)) {
} else if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
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;
} else if ("SaviorOfOllenbock".equals(aiLogic)) {
return SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa);
}
if (sa.isHidden()) {
if (isHidden(sa)) {
return hiddenTriggerAI(aiPlayer, sa, mandatory);
}
return knownOriginTriggerAI(aiPlayer, sa, mandatory);
@@ -237,7 +306,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
ZoneType origin = null;
final Player opponent = AiAttackController.choosePreferredDefenderPlayer(ai);
final Player opponent = ai.getWeakestOpponent();
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) {
@@ -278,7 +347,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.hasParam("Ninjutsu")) {
if (source.getType().isLegendary()
&& !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;
}
}
@@ -324,21 +394,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opponent)) {
sa.resetTargets();
sa.getTargets().add(opponent);
} else if (!isCurse && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
}
if (!sa.isTargetNumberValid()) {
return false;
}
pDefined = sa.getTargets().getTargetPlayers();
} else {
if (sa.hasParam("DefinedPlayer")) {
pDefined = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa);
pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("DefinedPlayer"), sa);
} 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.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// 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);
type = type.replace("X", Integer.toString(xPay));
}
@@ -387,13 +452,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false;
}
String num = sa.getParamOrDefault("ChangeNum", "1");
if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) return false;
xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay);
String num = sa.getParam("ChangeNum");
if (num != null) {
if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay);
}
}
if (sourceName.equals("Temur Sabertooth")) {
@@ -408,6 +474,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
return canBouncePermanent(ai, sa, list) != null;
}
}
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
// make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
final Player opp = aiPlayer.getWeakestOpponent();
if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opp)) {
@@ -499,14 +566,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
final String type = sa.getParam("ChangeType");
if (type != null && type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// 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);
}
Iterable<Player> pDefined;
final TargetRestrictions tgt = sa.getTargetRestrictions();
if ((tgt != null) && tgt.canTgtPlayer()) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Player opp = ai.getWeakestOpponent();
if (sa.isCurse()) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
@@ -624,7 +691,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return Card
*/
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
ComputerUtilCard.sortByEvaluateCreature(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);
}
if (sa.isHidden()) {
if (isHidden(sa)) {
return true;
}
@@ -868,10 +931,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
// X controls the minimum targets
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
// 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?
sa.setXManaCostPaid(xPay);
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
}
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
@@ -892,7 +956,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.isSpell()) {
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")) {
//System.out.println("isPreferredTarget att " + list);
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -904,26 +970,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false;
}
});
}
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;
}
});
//System.out.println("isPreferredTarget ok " + list);
}
if (list.size() < sa.getMinTargets()) {
return false;
}
immediately = immediately || ComputerUtil.playImmediately(ai, sa);
immediately |= ComputerUtil.playImmediately(ai, sa);
// Narrow down the list:
if (origin.contains(ZoneType.Battlefield)) {
@@ -1021,8 +1075,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (blink) {
return c.isToken();
} else {
return c.isToken() || c.getCMC() > 0;
}
return c.isToken() || c.getCMC() > 0;
}
});
}
@@ -1057,7 +1112,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (!immediately && (!game.getPhaseHandler().getNextTurn().equals(ai)
|| 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)) {
return false;
}
@@ -1097,6 +1152,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false;
}
if (!sa.hasParam("AITgtOwnCards")) {
list = CardLists.filterControlledBy(list, ai.getOpponents());
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.getPayCosts().hasSpecificCostType(CostPutCounter.class);
@@ -1150,11 +1206,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!list.isEmpty()) {
if (destination.equals(ZoneType.Battlefield) || origin.contains(ZoneType.Battlefield)) {
// filter by MustTarget requirement
CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
final Card mostExpensive = ComputerUtilCard.getMostExpensivePermanentAI(list);
final Card mostExpensive = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false);
if (mostExpensive.isCreature()) {
// if a creature is most expensive take the best one
if (destination.equals(ZoneType.Exile)) {
@@ -1182,11 +1234,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
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)) {
List<Card> nonLands = CardLists.getNotType(list, "Land");
// 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 (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon.
System.out.println("5 Life or less, trying to find something castable.");
CardLists.sortByCmcDesc(nonLands);
for (Card potentialCard : nonLands) {
if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), ai)) {
@@ -1203,6 +1251,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
} else {
// Get the best card in there.
System.out.println("No creature and lots of life, finding something good.");
choice = ComputerUtilCard.getBestAI(nonLands);
}
}
@@ -1303,12 +1352,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static Card canBouncePermanent(final Player ai, SpellAbility sa, CardCollectionView list) {
Game game = ai.getGame();
// filter out untargetables
CardCollectionView aiPermanents = CardLists.filterControlledBy(list, ai);
CardCollectionView aiPermanents = CardLists
.filterControlledBy(list, ai);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
// Felidar Guardian + Saheeli Rai combo support
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()) {
return saheeli.get(0);
}
@@ -1317,7 +1367,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Don't blink cards that will die.
aiPermanents = ComputerUtil.getSafeTargets(ai, sa, aiPermanents);
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<>();
@@ -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;
}
@@ -1454,7 +1454,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
Card choice = null;
if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false);
if (mostExpensivePermanent.isCreature()
&& (destination.equals(ZoneType.Battlefield) || tgt.getZone().contains(ZoneType.Battlefield))) {
// 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 (ai.getLife() <= 5) { // Desperate?
// Get something AI can cast soon.
System.out.println("5 Life or less, trying to find something castable.");
CardLists.sortByCmcDesc(nonLands);
for (Card potentialCard : nonLands) {
if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), ai)) {
@@ -1477,6 +1478,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
} else {
// Get the best card in there.
System.out.println("No creature and lots of life, finding something good.");
choice = ComputerUtilCard.getBestAI(nonLands);
}
}
@@ -1529,7 +1531,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return doExileCombatThreatLogic(ai, sa);
}
if (!sa.usesTargeting()) {
if (sa.getTargetRestrictions() == null) {
// Just in case of Defined cases
if (!mandatory && sa.hasParam("AttachedTo")) {
final List<Card> list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("AttachedTo"), sa);
@@ -1551,7 +1553,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return null;
}
if (sa.hasParam("AILogic")) {
String logic = sa.getParamOrDefault("AILogic", "");
String logic = sa.getParam("AILogic");
if ("NeverBounceItself".equals(logic)) {
Card source = sa.getHostCard();
if (fetchList.contains(source) && (fetchList.size() > 1 || !sa.getRootAbility().isMandatory())) {
@@ -1574,8 +1576,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
multipleCardsToChoose.remove(0);
return choice;
}
} else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList);
}
}
if (fetchList.isEmpty()) {
@@ -1653,16 +1653,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Does AI need a land?
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;
for (Card cardInHand : hand) {
canCastSomething = canCastSomething || ComputerUtilMana.hasEnoughManaSourcesToCast(cardInHand.getFirstSpellAbility(), decider);
canCastSomething |= ComputerUtilMana.hasEnoughManaSourcesToCast(cardInHand.getFirstSpellAbility(), decider);
}
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);
}
}
if (c == null) {
System.out.println("Don't need a land or none available; trying for a creature.");
fetchList = CardLists.getNotType(fetchList, "Land");
// Prefer to pull a creature, generally more useful for AI.
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 (decider.getLife() <= 5) { // Desperate?
// Get something AI can cast soon.
System.out.println("5 Life or less, trying to find something castable.");
CardLists.sortByCmcDesc(fetchList);
for (Card potentialCard : fetchList) {
if (ComputerUtilMana.hasEnoughManaSourcesToCast(potentialCard.getFirstSpellAbility(), decider)) {
@@ -1679,6 +1682,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
} else {
// Get the best card in there.
System.out.println("No creature and lots of life, finding something good.");
c = ComputerUtilCard.getBestAI(fetchList);
}
}
@@ -1696,21 +1700,19 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (card.isToken()) {
return false;
}
if (card.isCreature() && ComputerUtilCard.isUselessCreature(decider, card)) {
return true;
}
if (card.isEquipped()) {
} else if (card.isEquipped()) {
return false;
}
if (card.isEnchanted()) {
} else if (card.isEnchanted()) {
for (Card enc : card.getEnchantedBy()) {
if (enc.getOwner().isOpponentOf(decider)) {
return true;
}
}
return false;
}
if (card.hasCounters()) {
} else if (card.hasCounters()) {
if (card.isPlaneswalker()) {
int maxLoyaltyToConsider = 2;
int loyaltyDiff = 2;
@@ -1751,10 +1753,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
return true;
}
@Override
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
return AttachAi.attachGeneralAI(ai, sa, (List<Card>)options, !isOptional, sa.getHostCard(), sa.getParam("AILogic"));
return ComputerUtilCard.getBestAI(options);
}
/* (non-Javadoc)
@@ -1762,20 +1765,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/
@Override
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
if (params != null && params.containsKey("Attacker")) {
return (Player) ComputerUtilCombat.addAttackerToCombat(sa, (Card) params.get("Attacker"), new FCollection<GameEntity>(options));
}
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);
// Currently only used by Curse of Misfortunes, so this branch should never get hit
// But just in case it does, just select the first option
return Iterables.getFirst(options, null);
}
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
@@ -1871,7 +1863,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
source, sa);
list = CardLists.filterControlledBy(list, aiPlayer.getOpponents());
if (list.isEmpty()) {
return false; // no valid targets
return false; // no valid targets
}
Map<Player, Map.Entry<String, Integer>> data = Maps.newHashMap();
@@ -1897,7 +1889,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
List<String> toRemove = Lists.newArrayList();
for (final String name : values.keySet()) {
if (!Iterables.any(oppList, CardPredicates.nameEquals(name))) {
if (CardLists.filter(oppList, CardPredicates.nameEquals(name)).isEmpty()) {
toRemove.add(name);
}
}
@@ -1912,25 +1904,23 @@ public class ChangeZoneAi extends SpellAbilityAi {
}));
}
if (!data.isEmpty()) {
// 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>>>() {
@Override
public int compare(Map.Entry<Player, Map.Entry<String, Integer>> o1, Map.Entry<Player, Map.Entry<String, Integer>> o2) {
return o1.getValue().getValue() - o2.getValue().getValue();
}
});
// filter list again by the opponent and a creature of the wanted name that can be targeted
list = CardLists.filter(CardLists.filterControlledBy(list, max.getKey()),
CardPredicates.nameEquals(max.getValue().getKey()), CardPredicates.isTargetableBy(sa));
// list should contain only one element or zero
sa.resetTargets();
for (Card c : list) {
sa.getTargets().add(c);
return true;
// 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>>>() {
@Override
public int compare(Map.Entry<Player, Map.Entry<String, Integer>> o1, Map.Entry<Player, Map.Entry<String, Integer>> o2) {
return o1.getValue().getValue() - o2.getValue().getValue();
}
});
// filter list again by the opponent and a creature of the wanted name that can be targeted
list = CardLists.filter(CardLists.filterControlledBy(list, max.getKey()),
CardPredicates.nameEquals(max.getValue().getKey()), CardPredicates.isTargetableBy(sa));
// list should contain only one element or zero
sa.resetTargets();
for (Card c : list) {
sa.getTargets().add(c);
return true;
}
return false;
@@ -1981,7 +1971,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
int highestEval = -1;
if (combat.getAttackingPlayer().isOpponentOf(aiPlayer)) {
for (Card attacker : combat.getAttackers()) {
if (sa.canTarget(attacker)) {
if (sa.canTarget(attacker) && attacker.canBeTargetedBy(sa)) {
int eval = ComputerUtilCard.evaluateCreature(attacker);
if (combat.isUnblocked(attacker)) {
eval += 100; // TODO: make this smarter
@@ -1995,7 +1985,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else {
// either the current AI player or one of its teammates is attacking, the opponent(s) are blocking
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?
int eval = ComputerUtilCard.evaluateCreature(blocker);
if (eval > highestEval) {
@@ -2014,143 +2004,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
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) {
// 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.
@@ -2168,12 +2021,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = ComputerUtilCost.getMaxXValue(sa, ai, true);
// TODO use ComputerUtilCost.getMaxXValue if able
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}
if (toPay == 0 || toPay <= usableManaSources) {
if (toPay == 0) {
canBeSaved.add(potentialTgt);
}
if (toPay <= usableManaSources) {
canBeSaved.add(potentialTgt);
}

View File

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

View File

@@ -1,6 +1,5 @@
package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -24,23 +23,21 @@ import forge.util.collect.FCollection;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
Collections.shuffle(choices);
final int num;
final int min;
// sa is Entwined, no need for extra logic
if (sa.isEntwine()) {
num = min = choices.size();
} else {
num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num;
return true;
}
final 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?
// Reset the chosen list otherwise it will be locked in forever by earlier calls
sa.setChosenList(null);
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
List<AbilitySub> chosenList;
if (!ai.equals(sa.getActivatingPlayer())) {
@@ -69,9 +66,6 @@ public class CharmAi extends SpellAbilityAi {
if (timingRight) {
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
chosenList = chooseOptionsAi(choices, ai, true, num, min, sa.hasParam("CanRepeatModes"));
if (chosenList.isEmpty() && min != 0) {
return false;
}
} else {
return false;
}
@@ -93,12 +87,12 @@ public class CharmAi extends SpellAbilityAi {
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == num) {
return chosenList; // maximum choices reached
return chosenList; // maximum choices reached
}
}
}
if (isTrigger && chosenList.size() < min) {
// Second pass using doTrigger(false) to fulfill minimum choice
// Second pass using doTrigger(false) to fulfil minimum choice
choices.removeAll(chosenList);
for (AbilitySub sub : choices) {
sub.setActivatingPlayer(ai);
@@ -165,7 +159,7 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(allyTainted ? gain : lose);
} else if (oppTainted || ai.getGame().isCardInPlay("Rain of Gore")) {
// Rain of Gore does negate lifegain, so don't benefit the others
// same for if a opponent does control Tainted Remedy
// same for if a oppoent does control Tainted Remedy
// but if ai cant gain life, the effects are negated
chosenList.add(ai.canGainLife() ? lose : gain);
} else if (ai.getGame().isCardInPlay("Sulfuric Vortex")) {
@@ -181,15 +175,15 @@ public class CharmAi extends SpellAbilityAi {
} else if (ai.canGainLife() && aiLife <= 5) {
// critical Life try to gain more
chosenList.add(gain);
} else if (!ai.canGainLife() && aiLife == 14) {
} else if(!ai.canGainLife() && aiLife == 14 ) {
// ai cant gain life, but try to avoid falling to 13
// but if a opponent does control Tainted Remedy its irrelevant
// but if a oppoent does control Tainted Remedy its irrelevant
chosenList.add(oppTainted ? lose : gain);
} else if (allyTainted) {
// Tainted Remedy negation logic, try gain instead of lose
// because negation does turn it into lose for opponents
boolean oppCritical = false;
// an opponent is Critical = 14, and can't gain life, try to lose life instead
// an oppoent is Critical = 14, and can't gain life, try to lose life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 14) {
for (Player p : opponents) {
@@ -203,7 +197,7 @@ public class CharmAi extends SpellAbilityAi {
} else {
// normal logic, try to gain life if its critical
boolean oppCritical = false;
// an opponent is Critical = 12, and can gain life, try to gain life instead
// an oppoent is Critical = 12, and can gain life, try to gain life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 12) {
for (Player p : opponents) {
@@ -230,12 +224,10 @@ public class CharmAi extends SpellAbilityAi {
goodChoice = sub;
} else {
// Standard canPlayAi()
sub.setActivatingPlayer(ai);
sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone());
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == min) {
break; // enough choices
break; // enough choices
}
}
}
@@ -254,23 +246,4 @@ public class CharmAi extends SpellAbilityAi {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents, Map<String, Object> params) {
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.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
@@ -20,10 +20,8 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
@@ -41,7 +39,8 @@ public class ChooseCardAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
sa.resetTargets();
// search targetable Opponents
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
final List<Player> oppList = Lists.newArrayList(Iterables.filter(
ai.getOpponents(), PlayerPredicates.isTargetableBy(sa)));
if (oppList.isEmpty()) {
return false;
@@ -63,7 +62,7 @@ public class ChooseCardAi extends SpellAbilityAi {
if (sa.hasParam("ChoiceZone")) {
choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
}
CardCollectionView choices = game.getCardsIn(choiceZone);
CardCollectionView choices = ai.getGame().getCardsIn(choiceZone);
if (sa.hasParam("Choices")) {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa);
}
@@ -74,8 +73,10 @@ public class ChooseCardAi extends SpellAbilityAi {
return !choices.isEmpty();
} else if (aiLogic.equals("AtLeast2") || aiLogic.equals("BestBlocker")) {
return choices.size() >= 2;
} else if (aiLogic.equals("Clone")) {
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
} else if (aiLogic.equals("Clone") || aiLogic.equals("Vesuva")) {
final String filter = aiLogic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
choices = CardLists.getValidCards(choices, filter, host.getController(), host, sa);
return !choices.isEmpty();
} else if (aiLogic.equals("Never")) {
@@ -95,24 +96,12 @@ public class ChooseCardAi extends SpellAbilityAi {
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();
} else if (aiLogic.equals("RandomNonLand")) {
return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty();
} else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = AiAttackController.choosePreferredDefenderPlayer(ai).getCreaturesInPlay();
CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
@@ -145,16 +134,6 @@ public class ChooseCardAi extends SpellAbilityAi {
return checkApiLogic(ai, sa);
}
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("AtOppEOT")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
}
return super.checkPhaseRestrictions(ai, sa, ph);
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@@ -176,23 +155,28 @@ public class ChooseCardAi extends SpellAbilityAi {
}
choice = ComputerUtilCard.getBestAI(ownChoices);
} else if (logic.equals("BestBlocker")) {
if (Iterables.any(options, Presets.UNTAPPED)) {
if (!CardLists.filter(options, Presets.UNTAPPED).isEmpty()) {
options = CardLists.filter(options, Presets.UNTAPPED);
}
choice = ComputerUtilCard.getBestCreatureAI(options);
} else if (logic.equals("Clone")) {
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
} else if (logic.equals("Clone") || logic.equals("Vesuva")) {
final String filter = logic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary" :
"Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) {
options = newOptions;
}
choice = ComputerUtilCard.getBestAI(options);
if (logic.equals("Vesuva") && "Vesuva".equals(choice.getName())) {
choice = null;
}
} else if ("RandomNonLand".equals(logic)) {
options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host, sa);
choice = Aggregates.random(options);
} else if (logic.equals("Untap")) {
final String filter = "Permanent.YouCtrl,Permanent.tapped";
CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) {
options = newOptions;
}
@@ -200,7 +184,7 @@ public class ChooseCardAi extends SpellAbilityAi {
} else if (logic.equals("NeedsPrevention")) {
final Game game = ai.getGame();
final Combat combat = game.getCombat();
CardCollectionView better = CardLists.filter(options, new Predicate<Card>() {
CardCollectionView better = CardLists.filter(options, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (combat == null || !combat.isAttacking(c, ai) || !combat.isUnblocked(c)) {
@@ -255,6 +239,7 @@ public class ChooseCardAi extends SpellAbilityAi {
return true;
}
});
System.out.println("Tangle Wire" + options + " - " + betterList);
if (!betterList.isEmpty()) {
choice = betterList.get(0);
} else {

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom;
public class ChooseEvenOddAi extends SpellAbilityAi {
@@ -13,9 +13,10 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
if (!sa.hasParam("AILogic")) {
return false;
}
if (sa.usesTargeting()) {
TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
Player opp = aiPlayer.getWeakestOpponent();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
@@ -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.Sets;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
@@ -27,11 +28,11 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantBeCast;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.collect.FCollection;
@@ -53,9 +54,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
} else if ("GideonBlackblade".equals(aiLogic)) {
return SpecialCardAi.GideonBlackblade.consider(ai, sa);
} else if ("AtOppEOT".equals(aiLogic)) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
} else if ("SoulEcho".equals(aiLogic)) {
return doTriggerAINoCost(ai, sa, true);
} else if ("Always".equals(aiLogic)) {
return true;
}
@@ -72,7 +72,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
return checkApiLogic(aiPlayer, sa);
}
@Override
@@ -121,7 +121,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
SpellAbility paycost = new SpellAbility.EmptySa(sa.getHostCard(), player);
paycost.setPayCosts(unless);
if (ComputerUtilCost.willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
&& ComputerUtilCost.canPayCost(paycost, player, true)) {
&& ComputerUtilCost.canPayCost(paycost, player)) {
return sp;
}
}
@@ -160,22 +160,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
}
return others;
} else if ("Counters".equals(logic)) {
// TODO: this code will need generalization if this logic is used for cards other
// than Elspeth Conquers Death with different choice parameters
SpellAbility p1p1 = null, loyalty = null;
for (final SpellAbility sp : spells) {
if (("P1P1").equals(sp.getParam("CounterType"))) {
p1p1 = sp;
} else {
loyalty = sp;
}
}
if (sa.getParent().getTargetCard() != null && sa.getParent().getTargetCard().isPlaneswalker()) {
return loyalty;
} else {
return p1p1;
}
} else if ("Fatespinner".equals(logic)) {
SpellAbility skipDraw = null, /*skipMain = null,*/ skipCombat = null;
for (final SpellAbility sp : spells) {
@@ -199,6 +183,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// Todo if hand is empty or mostly empty, skip main phase
// Todo if hand has gas, skip draw
return Aggregates.random(spells);
} else if ("SinProdder".equals(logic)) {
SpellAbility allow = null, deny = null;
for (final SpellAbility sp : spells) {
@@ -245,11 +230,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
return allow;
}
//if Iona does prevent from casting, allow it to draw
for (final Card io : player.getCardsIn(ZoneType.Battlefield, "Iona, Shield of Emeria")) {
if (imprinted.getColor().hasAnyColor(MagicColor.fromName(io.getChosenColor()))) {
return allow;
}
SpellAbility firstSpell = imprinted.getFirstSpellAbility();
// check if something would prevent it from casting
if (firstSpell == null || StaticAbilityCantBeCast.cantBeCastAbility(firstSpell, imprinted, owner)) {
return allow;
}
if (dmg == 0) {
@@ -294,7 +278,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// if host would put into the battlefield attacking
if (combat != null && combat.isAttacking(host)) {
final Player defender = combat.getDefenderPlayerByAttacker(host);
if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true)) {
if (defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy)) {
return counterSA;
}
return tokenSA;
@@ -305,7 +289,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
for (final Player opp : player.getOpponents()) {
if (CombatUtil.canAttack(copy, opp) &&
opp.canLoseLife() &&
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
!ComputerUtilCard.canBeBlockedProfitably(opp, copy))
return counterSA;
}
}
@@ -354,14 +338,16 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
int totalCMC = 0;
for (Card c : revealedCards) {
for(Card c : revealedCards) {
totalCMC += c.getCMC();
}
int bestGuessDamage = totalCMC * 3 / revealedCards.size();
return life <= bestGuessDamage ? spells.get(0) : spells.get(1);
} else if ("SoulEcho".equals(logic)) {
return sa.getHostCard().getController().getLife() < 10 ? spells.get(0) : Aggregates.random(spells);
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)) {
List<SpellAbility> filtered = Lists.newArrayList();
// filter first for the spells which can be done
@@ -378,6 +364,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
} else if ("Riot".equals(logic)) {
SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
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
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,17 +3,13 @@ package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -42,7 +38,7 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?"
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false;
}
@@ -53,10 +49,10 @@ public class CloneAi extends SpellAbilityAi {
boolean bFlag = false;
for (final Card c : defined) {
bFlag |= !c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn());
bFlag |= (!c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn()));
// for creatures that could be improved (like Figure of Destiny)
if (c.isCreature() && (!sa.hasParam("Duration") || (!c.isTapped() && !c.isSick()))) {
if (c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
@@ -72,7 +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;
}
} else {
@@ -92,23 +89,17 @@ public class CloneAi extends SpellAbilityAi {
chance = cloneTgtAI(sa);
}
return chance;
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
Card host = sa.getHostCard();
boolean chance = true;
if (sa.usesTargeting()) {
chance = cloneTgtAI(sa);
} else {
if (sa.hasParam("Choices")) {
CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield),
sa.getParam("Choices"), host.getController(), host, sa);
chance = !choices.isEmpty();
}
}
// Improve AI for triggers. If source is a creature with:
@@ -180,25 +171,25 @@ public class CloneAi extends SpellAbilityAi {
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
Player targetedPlayer, Map<String, Object> params) {
final Card host = sa.getHostCard();
final String name = host.getName();
final Player ctrl = host.getController();
final Card cloneTarget = getCloneTarget(sa);
final boolean isOpp = cloneTarget.getController().isOpponentOf(sa.getActivatingPlayer());
final boolean isVesuva = "Vesuva".equals(name) || "Sculpting Steel".equals(name);
final boolean isVesuva = "Vesuva".equals(host.getName());
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name;
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
// TODO: rewrite this block so that this is done somehow more elegantly
if (canCloneLegendary) {
filter = filter.replace(".nonLegendary+", ".").replace(".nonLegendary", "");
}
CardCollection newOptions = CardLists.getValidCards(options, filter, ctrl, host, sa);
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) {
options = newOptions;
}
@@ -210,13 +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);
if (isVesuva && "Vesuva".equals(choice.getName())) {
choice = null;
}
return choice;
}
@@ -243,8 +233,8 @@ public class CloneAi extends SpellAbilityAi {
// don't use instant speed clone abilities outside computers
// Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("ActivationPhases") && sa.hasParam("Duration")) {
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false;
}
@@ -255,6 +245,6 @@ public class CloneAi extends SpellAbilityAi {
}
// don't activate during main2 unless this effect is permanent
return !ph.is(PhaseType.MAIN2) || !sa.hasParam("Duration");
return !ph.is(PhaseType.MAIN2) || sa.hasParam("Permanent");
}
}

View File

@@ -4,7 +4,6 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -30,8 +29,9 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.resetTargets();
CardCollection list =
CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on purpose
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on
// purpose
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -65,7 +65,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
}
} else {
if (mandatory) {
return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid();
return chkAIDrawback(sa, aiPlayer);
} else {
return canPlayAI(aiPlayer, sa);
}
@@ -81,10 +81,6 @@ public class ControlExchangeAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if ("PowerStruggle".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.PowerStruggle.considerSecondTarget(aiPlayer, sa);
}
// for TrigTwoTargets logic, only get the opponents' cards for the first target
CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ?
aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) :
@@ -101,12 +97,8 @@ public class ControlExchangeAi extends SpellAbilityAi {
Card best = ComputerUtilCard.getBestAI(list);
// add best Target:
// do it here already even if we don't want to play this as it might be for targeting a trigger
sa.getTargets().add(best);
// if Param has Defined, check if the best Target is better than the Defined
if (sa.hasParam("Defined") && (!sa.isTrigger() || sa.getRootAbility().isOptionalTrigger())) {
if (sa.hasParam("Defined")) {
final Card object = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa).get(0);
// TODO add evaluate Land if able
final Card realBest = ComputerUtilCard.getBestAI(Lists.newArrayList(best, object));
@@ -117,6 +109,9 @@ public class ControlExchangeAi extends SpellAbilityAi {
}
}
// add best Target
sa.getTargets().add(best);
// second target needed (the AI's own worst)
if ("TrigTwoTargets".equals(sa.getParam("AILogic"))) {
return doTrigTwoTargetsLogic(aiPlayer, sa, best);

View File

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

View File

@@ -12,12 +12,9 @@ import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -34,11 +31,11 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollection;
public class CopyPermanentAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// TODO - I'm sure someone can do this AI better
Card source = sa.getHostCard();
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -47,9 +44,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
return false;
}
if ("MomirAvatar".equals(aiLogic)) {
return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
} else if ("MimicVat".equals(aiLogic)) {
if ("MimicVat".equals(aiLogic)) {
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
} else if ("AtEOT".equals(aiLogic)) {
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;
}
@@ -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")) {
sa.resetTargets();
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
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.canTarget(aiPlayer)) {
sa.getTargets().add(aiPlayer);
return true;
} else {
for (Player p : aiPlayer.getYourTeam()) {
for (Player p : aiPlayer.getTeamMates(true)) {
if (sa.canTarget(p)) {
sa.getTargets().add(p);
return true;
@@ -116,7 +104,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
return false;
}
} 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 boolean canCopyLegendary = sa.hasParam("NonLegendary");
// ////
// Targeting
if (sa.usesTargeting()) {
sa.resetTargets();
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa));
list = CardLists.filter(list, Predicates.not(CardPredicates.isRemAIDeck()));
//Nothing to target
if (list.isEmpty()) {
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
if ("Saheeli Rai".equals(sourceName)) {
CardCollection felidarGuardian = CardLists.filter(list, CardPredicates.nameEquals("Felidar Guardian"));
@@ -160,7 +143,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
// target loop
while (sa.canAddMoreTarget()) {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -176,18 +159,18 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
});
Card choice;
if (Iterables.any(list, Presets.CREATURES)) {
if (!CardLists.filter(list, Presets.CREATURES).isEmpty()) {
if (sa.hasParam("TargetingPlayer")) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = ComputerUtilCard.getBestCreatureAI(list);
}
} else {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
}
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -241,33 +224,21 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
return ComputerUtilCard.getBestAI(options);
}
private CardCollection getBetterOptions(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional) {
final Card host = sa.getHostCard();
final Player ctrl = host.getController();
final boolean canCopyLegendary = sa.hasParam("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
return CardLists.getValidCards(options, filter, ctrl, host, sa);
return CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
}
@Override
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();
Card chosen = ComputerUtilCard.getBestCreatureAI(cards);
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.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom;
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
if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
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 forge.game.ability.effects.CounterEffect;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
@@ -27,6 +26,7 @@ import forge.game.cost.CostSacrifice;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.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);
if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || topSA.getActivatingPlayer() == ai
|| ai.getAllies().contains(topSA.getActivatingPlayer())) {
// might as well check for player's friendliness
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
@@ -113,7 +113,8 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
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 {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}
@@ -123,8 +124,9 @@ public class CounterAi extends SpellAbilityAi {
}
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of the time
if (!SpellAbilityAi.playReusable(ai, sa)) {
// If this is a reusable Resource, feel free to play it most of
// the time
if (!SpellAbilityAi.playReusable(ai,sa)) {
return false;
}
}
@@ -244,9 +246,10 @@ public class CounterAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = ai.getGame();
if (sa.usesTargeting()) {
if (tgt != null) {
if (game.getStack().isEmpty()) {
return false;
}
@@ -275,7 +278,7 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = ComputerUtilCost.getMaxXValue(sa, ai, true);
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}
@@ -316,7 +319,7 @@ public class CounterAi extends SpellAbilityAi {
Iterator<SpellAbilityStackInstance> it = game.getStack().iterator();
SpellAbilityStackInstance si = null;
while (it.hasNext()) {
while(it.hasNext()) {
si = it.next();
tgtSA = si.getSpellAbility(true);
if (!sa.canTargetSpellAbility(tgtSA)) {

View File

@@ -20,20 +20,15 @@ package forge.ai.ability;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
@@ -45,7 +40,7 @@ import forge.util.Aggregates;
* @author Forge
* @version $Id$
*/
public abstract class CountersAi extends SpellAbilityAi {
public abstract class CountersAi {
// 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.
* @param amount
* a int.
* @param newParam TODO
* @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;
// 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")) {
// try to kill the best killable creature, or reduce the best one
// 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) {
Card choice = null;
if (type.equals("P1P1")) {
// TODO look for modified
choice = ComputerUtilCard.getBestCreatureAI(list);
if (choice == null) {
@@ -115,7 +101,7 @@ public abstract class CountersAi extends SpellAbilityAi {
return c.getCounters(CounterEnumType.DIVINITY) == 0;
}
});
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon, null, false);
} else if (CounterType.get(type).isKeywordCounter()) {
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
} else {

View File

@@ -29,6 +29,7 @@ import forge.util.collect.FCollection;
public class CountersMoveAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!moveTgtAI(ai, sa)) {
@@ -82,8 +83,9 @@ public class CountersMoveAi extends SpellAbilityAi {
return true;
}
// something you can't block, try to reduce its attack
if (!ComputerUtilCard.canBeBlockedProfitably(ai, cpy, false)) {
// something you can't block, try to reduce its
// attack
if (!ComputerUtilCard.canBeBlockedProfitably(ai, cpy)) {
return true;
}
}
@@ -117,6 +119,7 @@ public class CountersMoveAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -234,6 +237,7 @@ public class CountersMoveAi extends SpellAbilityAi {
}
private boolean moveTgtAI(final Player ai, final SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = ai.getGame();
final String type = sa.getParam("CounterType");
@@ -279,7 +283,8 @@ public class CountersMoveAi extends SpellAbilityAi {
// cant use substract on Copy
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) {
return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken();
}
@@ -367,67 +372,54 @@ 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);
if (!aiList.isEmpty()) {
List<Card> best = CardLists.filter(aiList, new Predicate<Card>() {
List<Card> aiList = CardLists.filterControlledBy(tgtCards, ai);
if (!aiList.isEmpty()) {
List<Card> best = CardLists.filter(aiList, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
// gain from useless
if (ComputerUtilCard.isUselessCreature(ai, card)) {
return false;
}
// source would leave the game
if (card.hasSVar("EndOfTurnLeavePlay")) {
return false;
}
if (cType != null) {
if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) {
return false;
}
if (cType.is(CounterEnumType.M1M1)) {
return false;
}
if (!card.canReceiveCounters(cType)) {
return false;
}
}
return true;
@Override
public boolean apply(Card card) {
// gain from useless
if (ComputerUtilCard.isUselessCreature(ai, card)) {
return false;
}
});
if (best.isEmpty()) {
best = aiList;
}
// source would leave the game
if (card.hasSVar("EndOfTurnLeavePlay")) {
return false;
}
Card card = ComputerUtilCard.getBestCreatureAI(best);
if (cType != null) {
if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) {
return false;
}
if (cType.is(CounterEnumType.M1M1) && card.hasKeyword(Keyword.PERSIST)) {
return false;
}
if (card != null) {
sa.getTargets().add(card);
if (!card.canReceiveCounters(cType)) {
return false;
}
}
return true;
}
});
if (best.isEmpty()) {
best = aiList;
}
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (!isMandatoryTrigger) {
// no good target
return false;
Card card = ComputerUtilCard.getBestCreatureAI(best);
if (card != null) {
sa.getTargets().add(card);
return true;
}
}
// move counter to opponents creature but only if you can not steal them
// try to move to something useless or something that would leave play
// move counter to opponents creature but only if you can not steal
// them
// try to move to something useless or something that would leave
// play
List<Card> oppList = CardLists.filterControlledBy(tgtCards, ai.getOpponents());
if (!oppList.isEmpty()) {
List<Card> best = CardLists.filter(oppList, new Predicate<Card>() {
@@ -449,7 +441,7 @@ public class CountersMoveAi extends SpellAbilityAi {
});
if (best.isEmpty()) {
best = oppList;
best = aiList;
}
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
@Override
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
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