mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-12 08:48:39 +00:00
Compare commits
2 Commits
fix-spm-dr
...
paperCardI
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f14454e992 | ||
|
|
e4799a4f73 |
40
.github/workflows/maven-publish.yml
vendored
40
.github/workflows/maven-publish.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
export _JAVA_OPTIONS="-Xmx2g"
|
||||
d=$(date +%m.%d)
|
||||
# build only desktop and only try to move desktop files
|
||||
mvn -U -B clean -P windows-linux install -DskipTests -Dskip.flatten=true -e -T 1C release:clean release:prepare release:perform
|
||||
mvn -U -B clean -P windows-linux install -e -T 1C release:clean release:prepare release:perform -DskipTests
|
||||
mkdir izpack
|
||||
# move bz2 and jar from work dir to izpack dir
|
||||
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
|
||||
@@ -128,44 +128,6 @@ jobs:
|
||||
removeArtifacts: true
|
||||
makeLatest: true
|
||||
|
||||
- name: 🔧 Install XML tools
|
||||
run: sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: 🔼 Bump versionCode in root POM
|
||||
id: bump_version
|
||||
run: |
|
||||
cd /home/runner/work/forge/forge/
|
||||
|
||||
current_version=$(xmllint --xpath "//*[local-name()='versionCode']/text()" pom.xml)
|
||||
echo "Current versionCode: $current_version"
|
||||
|
||||
IFS='.' read -r major minor patch <<< "${current_version}"
|
||||
new_patch=$(printf "%02d" $((10#$patch + 1)))
|
||||
new_version="${major}.${minor}.${new_patch}"
|
||||
|
||||
sed -i -E "s|<versionCode>.*</versionCode>|<versionCode>${new_version}</versionCode>|" pom.xml
|
||||
|
||||
echo "version_code=${new_version}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: ♻️ Restore {revision} in child POMs
|
||||
run: |
|
||||
find . -name pom.xml ! -path "./pom.xml" | while read -r pom; do
|
||||
sed -i -E 's|<version>2\.0+\.[0-9]+(-SNAPSHOT)?</version>|<version>${revision}</version>|' "$pom"
|
||||
done
|
||||
|
||||
- name: 💾 Commit restored {revision}
|
||||
run: |
|
||||
# Add only pom.xml files
|
||||
find . -name pom.xml -exec git add {} \;
|
||||
|
||||
# Commit if there are changes
|
||||
if git diff --cached --quiet; then
|
||||
echo "No pom.xml changes to commit."
|
||||
else
|
||||
git commit -m "Restore POM files for preparation of next release" || echo "No changes to commit"
|
||||
git push
|
||||
fi
|
||||
|
||||
- name: Send failure notification to Discord
|
||||
if: failure() # This step runs only if the job fails
|
||||
run: |
|
||||
|
||||
2
.github/workflows/test-build.yaml
vendored
2
.github/workflows/test-build.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: ['17', '21']
|
||||
java: [ '17' ]
|
||||
name: Test with Java ${{ matrix.Java }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -66,9 +66,6 @@ forge-gui-mobile-dev/testAssets
|
||||
|
||||
forge-gui/res/cardsfolder/*.bat
|
||||
|
||||
# Generated changelog file
|
||||
forge-gui/release-files/CHANGES.txt
|
||||
|
||||
forge-gui/res/PerSetTrackingResults
|
||||
forge-gui/res/decks
|
||||
forge-gui/res/layouts
|
||||
@@ -90,7 +87,3 @@ forge-gui/tools/PerSetTrackingResults
|
||||
*.tiled-session
|
||||
/forge-gui/res/adventure/*.tiled-project
|
||||
/forge-gui/res/adventure/*.tiled-session
|
||||
|
||||
# Ignore python temporaries
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
33
.gitlab/issue_templates/Bug.md
Normal file
33
.gitlab/issue_templates/Bug.md
Normal file
@@ -0,0 +1,33 @@
|
||||
Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
|
||||
Steps to reproduce
|
||||
|
||||
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
|
||||
|
||||
|
||||
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
|
||||
|
||||
|
||||
What is the current bug behavior?
|
||||
|
||||
(What actually happens)
|
||||
|
||||
|
||||
What is the expected correct behavior?
|
||||
|
||||
(What you should see instead)
|
||||
|
||||
|
||||
Relevant logs and/or screenshots
|
||||
|
||||
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
|
||||
|
||||
|
||||
Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
||||
/label ~needs-investigation
|
||||
15
.gitlab/issue_templates/Feature.md
Normal file
15
.gitlab/issue_templates/Feature.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Summary
|
||||
|
||||
(Summarize the feature you wish concisely)
|
||||
|
||||
|
||||
Example screenshots
|
||||
|
||||
(If this is a UI change, please provide an example screenshot of how this feature might work)
|
||||
|
||||
|
||||
Feature type
|
||||
|
||||
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
|
||||
|
||||
/label ~feature request
|
||||
@@ -6,7 +6,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
||||
|
||||
## Requirements / Tools
|
||||
|
||||
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||
- Java JDK 17 or later
|
||||
- Git
|
||||
- Git client (optional)
|
||||
@@ -22,41 +22,42 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
||||
|
||||
- Clone your forked project to your local machine
|
||||
|
||||
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
||||
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
||||
|
||||
## IntelliJ
|
||||
|
||||
IntelliJ is the recommended IDE for Forge development. Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
|
||||
|
||||
|
||||
## Eclipse
|
||||
|
||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
||||
At this time, Eclipse is not the recommended IDE for Forge development.
|
||||
|
||||
### 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 GitHub. You'll need to setup an account and your SSH key.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
- Clone your forked repo to your local machine.
|
||||
|
||||
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
|
||||
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
|
||||
|
||||
- Install Eclipse 2021-12 or later for Java. Launch it.
|
||||
- Install Eclipse 2021-12 or later for Java. Launch it.
|
||||
|
||||
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
|
||||
- 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
|
||||
- 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.
|
||||
- Once everything builds, all errors should disappear. You can now advance to Project launch.
|
||||
|
||||
### Project Launch
|
||||
|
||||
@@ -66,15 +67,15 @@ This is the standard configuration used for releasing to Windows / Linux / MacOS
|
||||
|
||||
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Ok
|
||||
|
||||
- The familiar Forge splash screen, etc. should appear. Enjoy!
|
||||
- The familiar Forge splash screen, etc. should appear. Enjoy!
|
||||
|
||||
#### Mobile (Desktop dev)
|
||||
|
||||
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
|
||||
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
|
||||
|
||||
- Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Ok.
|
||||
|
||||
- A view similar to a mobile phone should appear. Enjoy!
|
||||
- A view similar to a mobile phone should appear. Enjoy!
|
||||
|
||||
### Eclipse / Android SDK Integration
|
||||
|
||||
@@ -98,7 +99,7 @@ TBD
|
||||
|
||||
#### Android Platform
|
||||
|
||||
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
|
||||
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
|
||||
|
||||
- Android SDK Build-tools 35.0.0
|
||||
- Android 15 (API 35) SDK Platform
|
||||
@@ -123,11 +124,10 @@ TBD
|
||||
|
||||
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...
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -141,7 +141,7 @@ Card scripting resources are found in the forge-gui/res/ path.
|
||||
|
||||
### Project Hierarchy
|
||||
|
||||
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
||||
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
||||
|
||||
- forge-ai
|
||||
- forge-core
|
||||
@@ -158,38 +158,32 @@ The platform-specific projects are:
|
||||
|
||||
#### forge-ai
|
||||
|
||||
The forge-ai project contains the computer opponent logic for gameplay. It includes decision-making algorithms for specific abilities, cards and turn phases.
|
||||
|
||||
#### forge-core
|
||||
|
||||
The forge-core project contains the core game engine, card mechanics, rules engine, and fundamental game logic. It includes the implementation of Magic: The Gathering rules, card interactions, and the game state management system.
|
||||
|
||||
#### forge-game
|
||||
|
||||
The forge-game project handles the game session management, player interactions, and game flow control. It includes implementations for multiplayer support, game modes, matchmaking, and game state persistence. This module bridges the core game engine with the user interface and networking components.
|
||||
|
||||
#### forge-gui
|
||||
|
||||
The forge-gui project contains the user interface components and rendering logic for the game. It includes the main game window, card displays, player interactions, and the scripting resource definitions in the res/ path.
|
||||
The forge-gui project includes the scripting resource definitions in the res/ path.
|
||||
|
||||
#### forge-gui-android
|
||||
|
||||
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
|
||||
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
#### forge-gui-desktop
|
||||
|
||||
Java Swing based GUI targeting desktop machines.
|
||||
|
||||
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
|
||||
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
|
||||
|
||||
#### forge-gui-ios
|
||||
|
||||
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
|
||||
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
#### forge-gui-mobile
|
||||
|
||||
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
|
||||
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
|
||||
|
||||
#### forge-gui-mobile-dev
|
||||
|
||||
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
||||
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ⚔️ Forge: The Magic: The Gathering Rules Engine
|
||||
# ⚔️ Forge: The Magic: The Gathering Rules Engine
|
||||
|
||||
Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
|
||||
@@ -32,7 +32,6 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
|
||||
|
||||
### 📱 Android Installation
|
||||
- _(Note: **Android 11** is the minimum requirements with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
|
||||
- Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
|
||||
|
||||
---
|
||||
@@ -47,13 +46,11 @@ Embark on a thrilling single-player journey where you can:
|
||||
- Challenge diverse AI opponents.
|
||||
- Collect cards and items to boost your abilities.
|
||||
|
||||
<img width="1282" height="752" alt="Shandalar World" src="https://github.com/user-attachments/assets/9af31471-d688-442f-9418-9807d8635b72" />
|
||||

|
||||
|
||||
### 🔍 Quest Modes
|
||||
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
|
||||
|
||||
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" />
|
||||
|
||||
### 🤖 AI Formats
|
||||
Test your skills against AI in multiple formats:
|
||||
- **Sealed**
|
||||
@@ -63,8 +60,6 @@ Test your skills against AI in multiple formats:
|
||||
|
||||
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
|
||||
|
||||
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
|
||||
|
||||
---
|
||||
|
||||
## 💬 Support & Community
|
||||
|
||||
@@ -15,7 +15,7 @@ public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
|
||||
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
|
||||
GuiBase.setDeviceInfo("", "", 0, 0);
|
||||
new EditorMainWindow(Config.instance());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package forge.ai;
|
||||
|
||||
public record AiAbilityDecision(int rating, AiPlayDecision decision) {
|
||||
private static int MIN_RATING = 30;
|
||||
|
||||
public boolean willingToPlay() {
|
||||
return rating > MIN_RATING && decision.willingToPlay();
|
||||
}
|
||||
}
|
||||
@@ -145,15 +145,13 @@ public class AiAttackController {
|
||||
sa.setActivatingPlayer(defender);
|
||||
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
||||
} else if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
||||
continue;
|
||||
}
|
||||
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
|
||||
if (animatedCopy.isCreature()) {
|
||||
// TODO imprecise, only works 100% for colorless mana
|
||||
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
|
||||
sa.getPayCosts().getTotalMana().getCMC() : 0;
|
||||
sa.getPayCosts().getTotalMana().getCMC() : 0; // FIXME: imprecise, only works 100% for colorless mana
|
||||
if (totalMana - manaReserved >= saCMC) {
|
||||
manaReserved += saCMC;
|
||||
defenders.add(animatedCopy);
|
||||
|
||||
@@ -59,6 +59,7 @@ public class AiCardMemory {
|
||||
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
||||
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||
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
|
||||
|
||||
@@ -66,11 +66,13 @@ import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -481,7 +483,7 @@ public class AiController {
|
||||
|
||||
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
|
||||
// don't play MDFC land if other side is spell and enough lands are available
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -909,14 +911,56 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
|
||||
// this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
AiPlayDecision canPlay = canPlaySa(sa);
|
||||
int oldCMC = -1;
|
||||
boolean xCost = sa.costHasX() || host.hasKeyword(Keyword.STRIVE) || sa.getApi() == ApiType.Charm;
|
||||
if (!xCost) {
|
||||
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
|
||||
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
|
||||
if (!sa.getAllTargetChoices().isEmpty()) {
|
||||
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
|
||||
}
|
||||
}
|
||||
|
||||
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
|
||||
if (canPlay != AiPlayDecision.WillPlay) {
|
||||
return canPlay;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
// Account for possible Ward after the spell is fully targeted
|
||||
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
|
||||
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
|
||||
if (!sa.isSpell() || sa.isCounterableBy(null)) {
|
||||
for (TargetChoices tc : sa.getAllTargetChoices()) {
|
||||
for (Card tgt : tc.getTargetCards()) {
|
||||
// TODO some older cards don't use the keyword, so check for trigger instead
|
||||
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
|
||||
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
||||
if (wardCost.hasManaCost()) {
|
||||
xCost |= wardCost.getTotalMana().getCMC() > 0;
|
||||
}
|
||||
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
||||
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
|
||||
return AiPlayDecision.CostNotAcceptable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if some target raised cost
|
||||
if (!xCost && oldCMC > -1) {
|
||||
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
|
||||
if (finalCMC > oldCMC) {
|
||||
xCost = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xCost && !ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
|
||||
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
|
||||
return AiPlayDecision.CantAfford;
|
||||
@@ -929,6 +973,8 @@ public class AiController {
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
|
||||
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
|
||||
// are willing to play the SA
|
||||
return AiPlayDecision.WillPlay;
|
||||
}
|
||||
|
||||
@@ -971,7 +1017,7 @@ public class AiController {
|
||||
Sentry.setExtra("Card", card.getName());
|
||||
Sentry.setExtra("SA", sa.toString());
|
||||
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayWithSubs(player, sa).willingToPlay();
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);
|
||||
|
||||
// remove added extra
|
||||
Sentry.removeExtra("Card");
|
||||
@@ -1349,9 +1395,9 @@ public class AiController {
|
||||
if (spell instanceof SpellApiBased) {
|
||||
boolean chance = false;
|
||||
if (withoutPayingManaCost) {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory).willingToPlay();
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
|
||||
} else {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTrigger(player, spell, mandatory);
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
}
|
||||
if (!chance) {
|
||||
return AiPlayDecision.TargetingFailed;
|
||||
@@ -1664,7 +1710,8 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||
@@ -1742,18 +1789,11 @@ public class AiController {
|
||||
return null;
|
||||
});
|
||||
|
||||
Thread t = new Thread(future);
|
||||
t.start();
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
try {
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
try {
|
||||
t.stop();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||
future.cancel(true);
|
||||
}
|
||||
future.cancel(true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1764,9 +1804,9 @@ public class AiController {
|
||||
|
||||
for (int i = 0; i < numToExile; i++) {
|
||||
Card chosen = null;
|
||||
for (final Card c : grave) {
|
||||
// Exile noncreatures first in case we can revive
|
||||
// Might wanna do some additional checking here for Flashback and the like
|
||||
for (final Card c : grave) { // Exile noncreatures first in
|
||||
// case we can revive. Might wanna do some additional
|
||||
// checking here for Flashback and the like.
|
||||
if (!c.isCreature()) {
|
||||
chosen = c;
|
||||
break;
|
||||
@@ -1787,12 +1827,12 @@ public class AiController {
|
||||
return toExile;
|
||||
}
|
||||
|
||||
public boolean doTrigger(SpellAbility sa, boolean mandatory) {
|
||||
if (sa instanceof WrappedAbility)
|
||||
return doTrigger(((WrappedAbility) sa).getWrappedAbility(), mandatory);
|
||||
if (sa.getApi() != null)
|
||||
return SpellApiToAi.Converter.get(sa).doTrigger(player, sa, mandatory);
|
||||
if (sa.getPayCosts() == Cost.Zero && !sa.usesTargeting()) {
|
||||
public boolean doTrigger(SpellAbility spell, boolean mandatory) {
|
||||
if (spell instanceof WrappedAbility)
|
||||
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
|
||||
if (spell.getApi() != null)
|
||||
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
|
||||
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
|
||||
if (thisRemove > 0) {
|
||||
removed += thisRemove;
|
||||
table.put(null, prefCard, cType, thisRemove);
|
||||
table.put(null, prefCard, CounterType.get(cType), thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,7 +573,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
@Override
|
||||
public PaymentDecision visit(CostRemoveAnyCounter cost) {
|
||||
final int c = cost.getAbilityAmount(ability);
|
||||
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
|
||||
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
|
||||
|
||||
if (c <= 0) {
|
||||
return null;
|
||||
@@ -716,7 +716,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, CounterEnumType.QUEST, over);
|
||||
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -767,12 +767,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
public PaymentDecision visit(CostRemoveCounter cost) {
|
||||
final String amount = cost.getAmount();
|
||||
final String type = cost.getType();
|
||||
final GameEntityCounterTable counterTable = new GameEntityCounterTable();
|
||||
|
||||
// TODO Help AI filter card with most useless counters and put those counters in countertable for things like
|
||||
// Moxite Refinery, similar to CostRemoveAnyCounter
|
||||
// Probably a lot of that decision making can be re-used or pulled out for both PaymentDecisions to use
|
||||
if (cost.counter == null) return null;
|
||||
|
||||
int c;
|
||||
|
||||
@@ -801,8 +795,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
}
|
||||
for (Card card : typeList) {
|
||||
if (card.getCounters(cost.counter) >= c) {
|
||||
counterTable.put(null, card, cost.counter, c);
|
||||
return PaymentDecision.counters(counterTable);
|
||||
return PaymentDecision.card(card, c);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -813,8 +806,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
counterTable.put(null, source, cost.counter, c);
|
||||
return PaymentDecision.counters(counterTable);
|
||||
return PaymentDecision.card(source, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,52 +1,21 @@
|
||||
package forge.ai;
|
||||
|
||||
public enum AiPlayDecision {
|
||||
// Play decision reasons
|
||||
WillPlay,
|
||||
MandatoryPlay,
|
||||
PlayToEmptyHand,
|
||||
ImpactCombat,
|
||||
ResponseToStackResolve,
|
||||
AddBoardPresence,
|
||||
Removal,
|
||||
Tempo,
|
||||
CardAdvantage,
|
||||
|
||||
// Play later decisions
|
||||
WaitForCombat,
|
||||
WaitForMain2,
|
||||
WaitForEndOfTurn,
|
||||
StackNotEmpty,
|
||||
AnotherTime,
|
||||
|
||||
// Don't play decision reasons
|
||||
WillPlay,
|
||||
CantPlaySa,
|
||||
CantPlayAi,
|
||||
CantAfford,
|
||||
CantAffordX,
|
||||
DoesntImpactCombat,
|
||||
DoesntImpactGame,
|
||||
MissingLogic,
|
||||
WaitForMain2,
|
||||
AnotherTime,
|
||||
MissingNeededCards,
|
||||
TimingRestrictions,
|
||||
MissingPhaseRestrictions,
|
||||
ConditionsNotMet,
|
||||
NeedsToPlayCriteriaNotMet,
|
||||
StopRunawayActivations,
|
||||
TargetingFailed,
|
||||
CostNotAcceptable,
|
||||
LifeInDanger,
|
||||
WouldDestroyLegend,
|
||||
WouldDestroyOtherPlaneswalker,
|
||||
WouldBecomeZeroToughnessCreature,
|
||||
WouldDestroyWorldEnchantment,
|
||||
BadEtbEffects,
|
||||
CurseEffects;
|
||||
|
||||
public boolean willingToPlay() {
|
||||
return switch (this) {
|
||||
case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
CurseEffects
|
||||
}
|
||||
@@ -767,7 +767,7 @@ public class ComputerUtil {
|
||||
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
|
||||
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
|
||||
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
|
||||
|
||||
if (untap) {
|
||||
typeList.remove(activate);
|
||||
@@ -864,7 +864,7 @@ public class ComputerUtil {
|
||||
|
||||
// Run non-mandatory trigger.
|
||||
// These checks only work if the Executing SpellAbility is an Ability_Sub.
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTrigger(ai, exSA, false)) {
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
|
||||
// AI would not run this trigger if given the chance
|
||||
return sacrificed;
|
||||
}
|
||||
@@ -1074,80 +1074,6 @@ public class ComputerUtil {
|
||||
return prevented;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card card = sa.getHostCard();
|
||||
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
|
||||
@@ -1319,6 +1245,80 @@ public class ComputerUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility sub = sa.getSubAbility();
|
||||
@@ -1327,6 +1327,7 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cipher spells
|
||||
if (sub != null) {
|
||||
final ApiType api = sub.getApi();
|
||||
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
|
||||
@@ -1384,14 +1385,13 @@ public class ComputerUtil {
|
||||
|
||||
// returns true if the AI should stop using the ability
|
||||
public static boolean preventRunAwayActivations(final SpellAbility sa) {
|
||||
if (!sa.isActivatedAbility()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int activations = sa.getActivationsThisTurn();
|
||||
|
||||
//10 activations should still be acceptable
|
||||
if (activations < 10) {
|
||||
if (!sa.isIntrinsic()) {
|
||||
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
|
||||
}
|
||||
|
||||
if (activations < 10) { //10 activations per turn should still be acceptable
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1621,6 +1621,7 @@ public class ComputerUtil {
|
||||
damage = dmg;
|
||||
}
|
||||
|
||||
// Triggered abilities
|
||||
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
|
||||
for (final Trigger t : c.getTriggers()) {
|
||||
if (TriggerType.Attacks.equals(t.getMode())) {
|
||||
@@ -2542,7 +2543,7 @@ public class ComputerUtil {
|
||||
|
||||
boolean opponent = controller.isOpponentOf(ai);
|
||||
|
||||
final CounterType p1p1Type = CounterEnumType.P1P1;
|
||||
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
|
||||
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return Aggregates.random(options);
|
||||
@@ -2551,7 +2552,7 @@ public class ComputerUtil {
|
||||
String logic = sa.getParam("AILogic");
|
||||
switch (logic) {
|
||||
case "Torture":
|
||||
return options.get(1);
|
||||
return "Torture";
|
||||
case "GraceOrCondemnation":
|
||||
List<ZoneType> graceZones = new ArrayList<ZoneType>();
|
||||
graceZones.add(ZoneType.Battlefield);
|
||||
@@ -2559,12 +2560,12 @@ public class ComputerUtil {
|
||||
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
|
||||
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
|
||||
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
|
||||
return options.get(aiGrace > humanGrace ? 0 : 1);
|
||||
return aiGrace > humanGrace ? "Grace" : "Condemnation";
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
|
||||
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
|
||||
CardCollection computerlist = ai.getCreaturesInPlay();
|
||||
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
|
||||
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
|
||||
case "Judgment":
|
||||
if (votes.isEmpty()) {
|
||||
CardCollection list = new CardCollection();
|
||||
@@ -2578,71 +2579,68 @@ public class ComputerUtil {
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "Protection":
|
||||
if (votes.isEmpty()) {
|
||||
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
|
||||
List<String> restrictedToColors = Lists.newArrayList();
|
||||
for (Object o : options) {
|
||||
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
|
||||
restrictedToColors.put(sp.getOriginalDescription(), sp);
|
||||
if (o instanceof String) {
|
||||
restrictedToColors.add((String) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
|
||||
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
|
||||
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
|
||||
}
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "FeatherOrQuill":
|
||||
SpellAbility feather = (SpellAbility)options.get(0);
|
||||
SpellAbility quill = (SpellAbility)options.get(1);
|
||||
// try to mill opponent with Quill vote
|
||||
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
|
||||
int numQuill = votes.get(quill).size();
|
||||
int numQuill = votes.get("Quill").size();
|
||||
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
|
||||
}
|
||||
}
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
// if source is not on the battlefield anymore, choose +1/+1 ones
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
// if no hand cards, try to mill opponent
|
||||
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return opponent ? quill : feather;
|
||||
return opponent ? "Quill" : "Feather";
|
||||
}
|
||||
|
||||
// AI has something to discard
|
||||
if (ai.equals(controller)) {
|
||||
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
|
||||
return quill;
|
||||
return "Quill";
|
||||
}
|
||||
}
|
||||
|
||||
// default card draw and discard are better than +1/+1 counter
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
case "StrengthOrNumbers":
|
||||
SpellAbility strength = (SpellAbility)options.get(0);
|
||||
SpellAbility numbers = (SpellAbility)options.get(1);
|
||||
// similar to fabricate choose +1/+1 or Token
|
||||
int numStrength = votes.get(strength).size();
|
||||
int numNumbers = votes.get(numbers).size();
|
||||
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
|
||||
int numStrength = votes.get("Strength").size();
|
||||
int numNumbers = votes.get("Numbers").size();
|
||||
|
||||
Card token = TokenAi.spawnToken(controller, numbers);
|
||||
Card token = TokenAi.spawnToken(controller, saToken);
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? strength : numbers;
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? strength : numbers;
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
}
|
||||
|
||||
// token would not survive
|
||||
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
|
||||
return opponent ? numbers : strength;
|
||||
return opponent ? "Numbers" : "Strength";
|
||||
}
|
||||
|
||||
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
|
||||
@@ -2663,40 +2661,35 @@ public class ComputerUtil {
|
||||
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
|
||||
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
|
||||
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
|
||||
case "SproutOrHarvest":
|
||||
SpellAbility sprout = (SpellAbility)options.get(0);
|
||||
SpellAbility harvest = (SpellAbility)options.get(1);
|
||||
// lifegain would hurt or has no effect
|
||||
if (opponent) {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return harvest;
|
||||
return "Harvest";
|
||||
}
|
||||
} else {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return sprout;
|
||||
return "Sprout";
|
||||
}
|
||||
}
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? sprout : harvest;
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? sprout : harvest;
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
}
|
||||
// TODO add Lifegain to +1/+1 counters trigger
|
||||
|
||||
// for now +1/+1 counters are better
|
||||
return opponent ? harvest : sprout;
|
||||
return opponent ? "Harvest" : "Sprout";
|
||||
case "DeathOrTaxes":
|
||||
SpellAbility death = (SpellAbility)options.get(0);
|
||||
SpellAbility taxes = (SpellAbility)options.get(1);
|
||||
|
||||
int numDeath = votes.get(death).size();
|
||||
int numTaxes = votes.get(taxes).size();
|
||||
int numDeath = votes.get("Death").size();
|
||||
int numTaxes = votes.get("Taxes").size();
|
||||
|
||||
if (opponent) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
@@ -2704,29 +2697,29 @@ public class ComputerUtil {
|
||||
// would need to sacrifice more creatures than AI has
|
||||
// sacrifice even more
|
||||
if (aiCreatures.size() <= numDeath) {
|
||||
return death;
|
||||
return "Death";
|
||||
}
|
||||
// would need to discard more cards than it has
|
||||
if (aiCardsInHand.size() <= numTaxes) {
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
}
|
||||
|
||||
// has cards with SacMe or Token
|
||||
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
|
||||
return death;
|
||||
return "Death";
|
||||
}
|
||||
|
||||
// has cards with DiscardMe
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
}
|
||||
|
||||
// discard is probably less worse than sacrifice
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
} else {
|
||||
// ai is first voter or ally of controller
|
||||
// both are not affected, but if opponents control creatures, sacrifice is worse
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
|
||||
}
|
||||
default:
|
||||
return Iterables.getFirst(options, null);
|
||||
|
||||
@@ -345,10 +345,6 @@ public class ComputerUtilAbility {
|
||||
if (source.hasSVar("AIPriorityModifier")) {
|
||||
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
||||
}
|
||||
// try to use it before it's gone
|
||||
if (source.isInPlay() && source.hasSVar("EndOfTurnLeavePlay")) {
|
||||
p += 1;
|
||||
}
|
||||
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
|
||||
p -= 10;
|
||||
}
|
||||
|
||||
@@ -919,14 +919,14 @@ public class ComputerUtilCard {
|
||||
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
|
||||
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
|
||||
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
|
||||
for (byte c : MagicColor.WUBRG) {
|
||||
if ((colors & c) != 0) {
|
||||
return MagicColor.toLongString(c);
|
||||
}
|
||||
}
|
||||
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
|
||||
return restrictedToColors.get(0); // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static List<String> getColorByProminence(final List<Card> list) {
|
||||
@@ -1819,18 +1819,18 @@ public class ComputerUtilCard {
|
||||
* @param sa Pump* or CounterPut*
|
||||
* @return
|
||||
*/
|
||||
public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
||||
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
for (final Card card : cards) {
|
||||
if (objects.contains(card)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -1849,11 +1849,11 @@ public class ComputerUtilCard {
|
||||
}
|
||||
if (!sa.isTargetNumberValid()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isUselessCreature(Player ai, Card c) {
|
||||
|
||||
@@ -974,13 +974,17 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -994,11 +998,12 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
power += pBonus;
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,13 +1107,17 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1122,11 +1131,12 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
@@ -1295,7 +1305,6 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
@@ -1305,8 +1314,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
@@ -1321,14 +1333,13 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
return power;
|
||||
}
|
||||
@@ -1519,14 +1530,16 @@ public class ComputerUtilCombat {
|
||||
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1540,11 +1553,10 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import forge.game.GameObject;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -140,13 +139,11 @@ public class ComputerUtilCost {
|
||||
if (type.equals("CARDNAME")) {
|
||||
if (source.getAbilityText().contains("Bloodrush")) {
|
||||
continue;
|
||||
}
|
||||
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
|
||||
} 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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
|
||||
if (typeList.size() > ai.getMaxHandSize()) {
|
||||
@@ -251,7 +248,11 @@ public class ComputerUtilCost {
|
||||
// Does the AI want to use Sacrifice All?
|
||||
return false;
|
||||
} else {
|
||||
int c = part.getAbilityAmount(sourceAbility);
|
||||
Integer c = part.convertAmount();
|
||||
|
||||
if (c == null) {
|
||||
c = part.getAbilityAmount(sourceAbility);
|
||||
}
|
||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
|
||||
if (choices != null) {
|
||||
@@ -521,12 +522,13 @@ public class ComputerUtilCost {
|
||||
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
|
||||
}
|
||||
|
||||
boolean cannotBeCountered = false;
|
||||
|
||||
// Check for stuff like Nether Void
|
||||
int extraManaNeeded = 0;
|
||||
if (!effect) {
|
||||
boolean cannotBeCountered = !sa.isCounterableBy(null);
|
||||
|
||||
if (sa instanceof Spell) {
|
||||
cannotBeCountered = !sa.isCounterableBy(null);
|
||||
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
||||
if (!StringUtils.isBlank(snem)) {
|
||||
@@ -576,24 +578,12 @@ public class ComputerUtilCost {
|
||||
}
|
||||
}
|
||||
|
||||
// Account for possible Ward after the spell is fully targeted
|
||||
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
|
||||
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
|
||||
if (!sa.isTrigger() && !cannotBeCountered) {
|
||||
Set<GameObject> distinctObjects = Sets.newHashSet();
|
||||
// Ward - will be accounted for when rechecking a targeted ability
|
||||
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
|
||||
for (TargetChoices tc : sa.getAllTargetChoices()) {
|
||||
for (Card tgt : tc.getTargetCards()) {
|
||||
if (!distinctObjects.add(tgt)) {
|
||||
continue;
|
||||
}
|
||||
// TODO some older cards don't use the keyword, so check for trigger instead
|
||||
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
|
||||
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
||||
// don't use API converter since it might have special part logic not meant for Ward cost
|
||||
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
||||
if (!topAI.willPayCosts(player, sa, wardCost, sa.getHostCard())) {
|
||||
return false;
|
||||
}
|
||||
if (wardCost.hasManaCost()) {
|
||||
extraManaNeeded += wardCost.getTotalMana().getCMC();
|
||||
}
|
||||
@@ -617,7 +607,6 @@ public class ComputerUtilCost {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO both of these call CostAdjustment.adjust, try to reuse instead
|
||||
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
|
||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
public static SpellAbility chooseManaAbility(ManaCostBeingPaid cost, SpellAbility sa, Player ai, ManaCostShard toPay,
|
||||
Collection<SpellAbility> maList, boolean checkCosts) {
|
||||
Collection<SpellAbility> saList, boolean checkCosts) {
|
||||
Card saHost = sa.getHostCard();
|
||||
|
||||
// CastTotalManaSpent (AIPreference:ManaFrom$Type or AIManaPref$ Type)
|
||||
@@ -240,12 +240,12 @@ public class ComputerUtilMana {
|
||||
manaSourceType = sa.getParam("AIManaPref");
|
||||
}
|
||||
if (manaSourceType != "") {
|
||||
List<SpellAbility> filteredList = Lists.newArrayList(maList);
|
||||
List<SpellAbility> filteredList = Lists.newArrayList(saList);
|
||||
switch (manaSourceType) {
|
||||
case "Snow":
|
||||
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().isSnow()
|
||||
&& ab2.getHostCard() != null && !ab2.getHostCard().isSnow() ? -1 : 1);
|
||||
maList = filteredList;
|
||||
saList = filteredList;
|
||||
break;
|
||||
case "Treasure":
|
||||
// Try to spend only one Treasure if possible
|
||||
@@ -253,22 +253,22 @@ public class ComputerUtilMana {
|
||||
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
|
||||
SpellAbility first = filteredList.get(0);
|
||||
if (first.getHostCard() != null && first.getHostCard().getType().hasSubtype("Treasure")) {
|
||||
maList.remove(first);
|
||||
saList.remove(first);
|
||||
List<SpellAbility> updatedList = Lists.newArrayList();
|
||||
updatedList.add(first);
|
||||
updatedList.addAll(maList);
|
||||
maList = updatedList;
|
||||
updatedList.addAll(saList);
|
||||
saList = updatedList;
|
||||
}
|
||||
break;
|
||||
case "TreasureMax":
|
||||
// Ok to spend as many Treasures as possible
|
||||
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure")
|
||||
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
|
||||
maList = filteredList;
|
||||
saList = filteredList;
|
||||
break;
|
||||
case "NotSameCard":
|
||||
String hostName = sa.getHostCard().getName();
|
||||
maList = filteredList.stream()
|
||||
saList = filteredList.stream()
|
||||
.filter(saPay -> !saPay.getHostCard().getName().equals(hostName))
|
||||
.collect(Collectors.toList());
|
||||
break;
|
||||
@@ -277,7 +277,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
}
|
||||
|
||||
for (final SpellAbility ma : maList) {
|
||||
for (final SpellAbility ma : saList) {
|
||||
// this rarely seems like a good idea
|
||||
if (ma.getHostCard() == saHost) {
|
||||
continue;
|
||||
@@ -291,12 +291,6 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
|
||||
if (amount <= 0) {
|
||||
// wrong gamestate for variable amount
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sa.getApi() == ApiType.Animate) {
|
||||
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
|
||||
if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined"))
|
||||
@@ -342,7 +336,7 @@ public class ComputerUtilMana {
|
||||
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
|
||||
continue;
|
||||
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
|
||||
for (SpellAbility ab : maList) {
|
||||
for (SpellAbility ab : saList) {
|
||||
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
||||
if (!ab.getHostCard().isTapped()) {
|
||||
paymentChoice = ab;
|
||||
@@ -449,6 +443,7 @@ public class ComputerUtilMana {
|
||||
manaProduced = manaProduced.replace(s, color);
|
||||
}
|
||||
} else if (saMana.hasParam("ReplaceColor")) {
|
||||
// replace color
|
||||
String color = saMana.getParam("ReplaceColor");
|
||||
if ("Chosen".equals(color)) {
|
||||
if (card.hasChosenColor()) {
|
||||
@@ -595,12 +590,12 @@ public class ComputerUtilMana {
|
||||
while (!cost.isPaid()) {
|
||||
toPay = getNextShardToPay(cost, sourcesForShards);
|
||||
|
||||
Collection<SpellAbility> maList = sourcesForShards.get(toPay);
|
||||
if (maList == null) {
|
||||
Collection<SpellAbility> saList = sourcesForShards.get(toPay);
|
||||
if (saList == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, maList, true);
|
||||
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, saList, true);
|
||||
if (saPayment == null) {
|
||||
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
|
||||
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)) {
|
||||
@@ -671,7 +666,6 @@ public class ComputerUtilMana {
|
||||
return true;
|
||||
}
|
||||
|
||||
int phyLifeToPay = 2;
|
||||
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||
boolean hasConverge = sa.getHostCard().hasConverge();
|
||||
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test, checkPlayable, hasConverge);
|
||||
@@ -699,12 +693,13 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (sourcesForShards == null && !purePhyrexian) {
|
||||
// no mana abilities to use for paying
|
||||
break;
|
||||
break; // no mana abilities to use for paying
|
||||
}
|
||||
|
||||
toPay = getNextShardToPay(cost, sourcesForShards);
|
||||
|
||||
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
|
||||
|
||||
Collection<SpellAbility> saList = null;
|
||||
if (hasConverge &&
|
||||
(toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) {
|
||||
@@ -740,8 +735,7 @@ public class ComputerUtilMana {
|
||||
|
||||
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
|
||||
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
|
||||
// not a good idea to sac a card that you're targeting with the SA you're paying for
|
||||
saExcludeList.add(saPayment);
|
||||
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -758,14 +752,9 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (saPayment == null) {
|
||||
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
|
||||
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(phyLifeToPay, false, sa)
|
||||
|| (ai.getLife() <= phyLifeToPay && !ai.cantLoseForZeroOrLessLife())) {
|
||||
// cannot pay
|
||||
break;
|
||||
}
|
||||
if (test) {
|
||||
phyLifeToPay += 2;
|
||||
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)
|
||||
|| (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
|
||||
break; // cannot pay
|
||||
}
|
||||
|
||||
if (sa.hasParam("AIPhyrexianPayment")) {
|
||||
@@ -969,6 +958,7 @@ public class ComputerUtilMana {
|
||||
if (checkCosts) {
|
||||
// Check if AI can still play this mana ability
|
||||
ma.setActivatingPlayer(ai);
|
||||
// if the AI can't pay the additional costs skip the mana ability
|
||||
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) {
|
||||
return false;
|
||||
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
|
||||
@@ -986,9 +976,8 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s))){
|
||||
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1502,7 +1491,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (!cost.isReusuableResource()) {
|
||||
for (CostPart part : cost.getCostParts()) {
|
||||
for(CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
|
||||
unpreferredCost = true;
|
||||
}
|
||||
@@ -1514,7 +1503,7 @@ public class ComputerUtilMana {
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
// We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
|
||||
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
|
||||
continue;
|
||||
}
|
||||
needsLimitedResources = true; // TODO: check for good drawbacks (gainLife)
|
||||
@@ -1593,8 +1582,10 @@ public class ComputerUtilMana {
|
||||
|
||||
// don't use abilities with dangerous drawbacks
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
if (sub != null) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
|
||||
|
||||
@@ -264,14 +264,12 @@ public abstract class GameState {
|
||||
}
|
||||
|
||||
if (c.hasMergedCard()) {
|
||||
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
|
||||
// we have to go by the current top card name here
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
|
||||
} else {
|
||||
String suffix = c.hasPaperFoil() ? "+" : "";
|
||||
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
.append("|Art:").append(c.getPaperCard().getArtIndex());
|
||||
}
|
||||
}
|
||||
@@ -321,21 +319,18 @@ public abstract class GameState {
|
||||
newText.append(":Cloaked");
|
||||
}
|
||||
}
|
||||
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
||||
newText.append("|Transformed");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
newText.append("|Flipped");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||
newText.append("|Meld");
|
||||
if (c.getMeldedWith() != null) {
|
||||
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
|
||||
newText.append(":");
|
||||
newText.append(c.getMeldedWith().getName()).append(suffix);
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
|
||||
if (c.isModal()) {
|
||||
newText.append("|Modal");
|
||||
} else {
|
||||
newText.append("|Transformed");
|
||||
newText.append(c.getMeldedWith().getName());
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
|
||||
newText.append("|Modal");
|
||||
}
|
||||
|
||||
if (c.getPlayerAttachedTo() != null) {
|
||||
@@ -1268,7 +1263,7 @@ public abstract class GameState {
|
||||
} else if (cardinfo[0].startsWith("T:")) {
|
||||
String tokenStr = cardinfo[0].substring(2);
|
||||
PaperToken token = StaticData.instance().getAllTokens().getToken(tokenStr,
|
||||
setCode != null ? setCode : CardEdition.UNKNOWN_CODE);
|
||||
setCode != null ? setCode : CardEdition.UNKNOWN.getName());
|
||||
if (token == null) {
|
||||
System.err.println("ERROR: Tried to create a non-existent token named " + cardinfo[0] + " when loading game state!");
|
||||
continue;
|
||||
@@ -1316,8 +1311,8 @@ public abstract class GameState {
|
||||
if (info.endsWith("Cloaked")) {
|
||||
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
|
||||
}
|
||||
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Backside, true);
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Flipped")) {
|
||||
c.setState(CardStateName.Flipped, true);
|
||||
@@ -1335,6 +1330,9 @@ public abstract class GameState {
|
||||
}
|
||||
c.setState(CardStateName.Meld, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Modal, true);
|
||||
c.setBackSide(true);
|
||||
}
|
||||
else if (info.startsWith("OnAdventure")) {
|
||||
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";
|
||||
|
||||
@@ -1347,11 +1347,6 @@ public class PlayerControllerAi extends PlayerController {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
|
||||
// TODO check if profile detection set to Auto
|
||||
|
||||
@@ -171,7 +171,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ public class SpecialAiLogic {
|
||||
}
|
||||
|
||||
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
|
||||
public static AiAbilityDecision doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
|
||||
public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
|
||||
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||
@@ -222,14 +222,14 @@ public class SpecialAiLogic {
|
||||
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
|
||||
if (numOtherCreats == 0) {
|
||||
// Cut short if there's nothing to sac at all
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the standard Aristocrats logic applies first (if in the right conditions for it)
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
|
||||
if (isDeclareBlockers || isThreatened) {
|
||||
if (doAristocratLogic(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ public class SpecialAiLogic {
|
||||
if (countersSa == null) {
|
||||
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
|
||||
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!");
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
@@ -263,7 +263,7 @@ public class SpecialAiLogic {
|
||||
relevantCreats.remove(source);
|
||||
if (relevantCreats.isEmpty()) {
|
||||
// No relevant creatures to sac
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa);
|
||||
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
@@ -287,20 +287,16 @@ public class SpecialAiLogic {
|
||||
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat))
|
||||
);
|
||||
if (!forcedSacTgts.isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
|
||||
|
||||
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
|
||||
if (source.getNetCombatDamage() < lethalDmg
|
||||
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
return source.getNetCombatDamage() < lethalDmg
|
||||
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg;
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||
@@ -313,7 +309,7 @@ public class SpecialAiLogic {
|
||||
);
|
||||
|
||||
if (sacTgts.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
|
||||
@@ -321,10 +317,7 @@ public class SpecialAiLogic {
|
||||
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
|
||||
|
||||
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
|
||||
if (source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
|
||||
}
|
||||
} else {
|
||||
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||
@@ -336,11 +329,7 @@ public class SpecialAiLogic {
|
||||
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
|
||||
);
|
||||
|
||||
if (sacFodder.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return !sacFodder.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,10 +360,10 @@ public class SpecialAiLogic {
|
||||
// FIXME: We're emulating the UnlessCost on the SA to run the proper checks.
|
||||
// This is hacky, but it works. Perhaps a cleaner way exists?
|
||||
sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost"));
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
|
||||
sa.getMapParams().remove("UnlessCost");
|
||||
} else {
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
|
||||
}
|
||||
return willPlay;
|
||||
}
|
||||
|
||||
@@ -78,17 +78,16 @@ public class SpecialCardAi {
|
||||
|
||||
// Arena and Magus of the Arena
|
||||
public static class Arena {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
|
||||
// TODO This is basically removal, so we may want to play this at other times
|
||||
if (!game.getPhaseHandler().is(PhaseType.END_OF_TURN) || game.getPhaseHandler().getNextTurn() != ai) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
return false; // at opponent's EOT only, to conserve mana
|
||||
}
|
||||
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
if (aiCreatures.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
@@ -112,11 +111,11 @@ public class SpecialCardAi {
|
||||
if (canKillAll) {
|
||||
sa.getTargets().clear();
|
||||
sa.getTargets().add(aiCreature);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Removal);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +203,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Chain of Acid
|
||||
public static class ChainOfAcid {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.LANDS);
|
||||
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
|
||||
@@ -214,22 +213,13 @@ public class SpecialCardAi {
|
||||
// which it can only distinguish by their CMC, considering >CMC higher value).
|
||||
// Currently ensures that the AI will still have lands provided that the human player goes to
|
||||
// destroy all the AI's lands in order (to avoid manalock).
|
||||
if (!OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2) {
|
||||
// If there are enough lands, target the worst non-creature permanent of the opponent
|
||||
Card worstOppPerm = ComputerUtilCard.getWorstAI(OppPerms);
|
||||
if (worstOppPerm != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(worstOppPerm);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return !OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Chain of Smog
|
||||
public static class ChainOfSmog {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
if (ai.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
// to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point)
|
||||
// TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a
|
||||
@@ -245,10 +235,10 @@ public class SpecialCardAi {
|
||||
|
||||
sa.getParent().resetTargets();
|
||||
sa.getParent().getTargets().add(targOpp);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,28 +359,18 @@ public class SpecialCardAi {
|
||||
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
|
||||
|
||||
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
|
||||
Card c = sa.getHostCard();
|
||||
|
||||
// Only check for sacrifice if it's the owner's turn, and it can attack.
|
||||
// TODO: Maybe check if sacrificing a creature allows AI to kill the opponent with the rest on their turn?
|
||||
if (!CombatUtil.canAttack(c) ||
|
||||
!ai.getGame().getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.UNTAPPED.and(
|
||||
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH))));
|
||||
CardPredicates.UNTAPPED.and(
|
||||
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH))));
|
||||
boolean hasUsefulBlocker = false;
|
||||
|
||||
for (Card fc : flyingCreatures) {
|
||||
if (!ComputerUtilCard.isUselessCreature(ai, fc)) {
|
||||
for (Card c : flyingCreatures) {
|
||||
if (!ComputerUtilCard.isUselessCreature(ai, c)) {
|
||||
hasUsefulBlocker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ai.getLife() <= c.getNetPower() && !hasUsefulBlocker;
|
||||
return ai.getLife() <= sa.getHostCard().getNetPower() && !hasUsefulBlocker;
|
||||
}
|
||||
|
||||
public static int getSacThreshold() {
|
||||
@@ -400,7 +380,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Donate
|
||||
public static class Donate {
|
||||
public static AiAbilityDecision considerTargetingOpponent(final Player ai, final SpellAbility sa) {
|
||||
public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) {
|
||||
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
|
||||
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
|
||||
if (donateTarget != null) {
|
||||
@@ -410,7 +390,7 @@ public class SpecialCardAi {
|
||||
|
||||
// All opponents have hexproof or something like that
|
||||
if (Iterables.isEmpty(oppList)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter for player who does not have donate target already
|
||||
@@ -428,30 +408,31 @@ public class SpecialCardAi {
|
||||
if (opp != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
// No targets found to donate, so do nothing.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) {
|
||||
public static boolean considerDonatingPermanent(final Player ai, final SpellAbility sa) {
|
||||
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
|
||||
if (donateTarget != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(donateTarget);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Should never get here because targetOpponent, called before targetPermanentToDonate, should already have made the AI bail
|
||||
System.err.println("Warning: Donate AI failed at SpecialCardAi.Donate#targetPermanentToDonate despite successfully targeting an opponent first.");
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Electrostatic Pummeler
|
||||
public static class ElectrostaticPummeler {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
Game game = ai.getGame();
|
||||
Combat combat = game.getCombat();
|
||||
@@ -464,13 +445,13 @@ public class SpecialCardAi {
|
||||
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
|
||||
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop);
|
||||
if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg)
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not activate if damage will be prevented
|
||||
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Activate Electrostatic Pummeler's pump only as a combat trick
|
||||
@@ -479,14 +460,14 @@ public class SpecialCardAi {
|
||||
// We'll try to deal lethal trample/unblocked damage, so remember the card for attack
|
||||
// and wait until declare blockers step.
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isBlocking = combat.isBlocking(source);
|
||||
@@ -511,11 +492,11 @@ public class SpecialCardAi {
|
||||
}
|
||||
if (totalDamageToPW >= oppT + loyalty) {
|
||||
// Already enough damage to take care of the planeswalker
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
return false;
|
||||
}
|
||||
if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) {
|
||||
// Can pump to kill the planeswalker, go for it
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -536,31 +517,31 @@ public class SpecialCardAi {
|
||||
// We can deal a lot of damage (either a lot of damage directly to the opponent,
|
||||
// or kill the blocker(s) and damage the opponent at the same time, so go for it
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) {
|
||||
// Can't survive first strike or double strike, don't pump
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
return false;
|
||||
}
|
||||
if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) {
|
||||
// Can't pump enough to kill the blockers and survive, don't pump
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
return false;
|
||||
}
|
||||
if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) {
|
||||
// Already enough to kill the blockers and survive, don't overpump
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
return false;
|
||||
}
|
||||
if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.isWitherDamage()
|
||||
&& predictedPT.getLeft() <= oppT) {
|
||||
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we got here, it should be a favorable combat pump, resulting in at least one
|
||||
// opposing creature dying, and hopefully with the Pummeler surviving combat.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) {
|
||||
@@ -637,15 +618,15 @@ public class SpecialCardAi {
|
||||
|
||||
// Fell the Mighty
|
||||
public static class FellTheMighty {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
CardCollection aiList = ai.getCreaturesInPlay();
|
||||
if (aiList.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
CardLists.sortByPowerAsc(aiList);
|
||||
Card lowest = aiList.get(0);
|
||||
if (!sa.canTarget(lowest)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
@@ -655,9 +636,9 @@ public class SpecialCardAi {
|
||||
if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(lowest);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,25 +673,25 @@ public class SpecialCardAi {
|
||||
|
||||
// Gideon Blackblade
|
||||
public static class GideonBlackblade {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
sa.resetTargets();
|
||||
CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
|
||||
if (!otb.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(otb));
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Goblin Polka Band
|
||||
public static class GoblinPolkaBand {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
int maxPotentialTgts = ai.getOpponents().getCreaturesInPlay().filter(CardPredicates.UNTAPPED).size();
|
||||
int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false);
|
||||
|
||||
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
|
||||
if (numTgts == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set Announce
|
||||
@@ -720,7 +701,7 @@ public class SpecialCardAi {
|
||||
List<GameEntity> validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().addAll(Aggregates.random(validTgts, numTgts));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,12 +910,12 @@ public class SpecialCardAi {
|
||||
|
||||
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
|
||||
public static class LivingDeath {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
// if there's another reanimator card currently suspended, don't cast a new one until the previous
|
||||
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
|
||||
for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
|
||||
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -945,7 +926,7 @@ public class SpecialCardAi {
|
||||
|
||||
if (aiCreaturesInGY.isEmpty()) {
|
||||
// nothing in graveyard, so cut short
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Card c : ai.getCreaturesInPlay()) {
|
||||
@@ -977,30 +958,17 @@ public class SpecialCardAi {
|
||||
}
|
||||
|
||||
// if we get more value out of this than our opponent does (hopefully), go for it
|
||||
if ((aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold);
|
||||
}
|
||||
}
|
||||
|
||||
// Maze's End
|
||||
public static class MazesEnd {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
|
||||
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (availableGates.isEmpty()) {
|
||||
// No gates available, so don't activate Maze's End
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty();
|
||||
}
|
||||
|
||||
public static Card considerCardToGet(final Player ai, final SpellAbility sa)
|
||||
@@ -1064,33 +1032,29 @@ public class SpecialCardAi {
|
||||
return exiledWith == null || (tgt != null && ComputerUtilCard.evaluateCreature(tgt) > ComputerUtilCard.evaluateCreature(exiledWith));
|
||||
}
|
||||
|
||||
public static AiAbilityDecision considerCopy(final Player ai, final SpellAbility sa) {
|
||||
public static boolean considerCopy(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Card exiledWith = source.getImprintedCards().isEmpty() ? null : source.getImprintedCards().getFirst();
|
||||
|
||||
if (exiledWith == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
// We want to either be able to attack with the creature, or keep it until our opponent's end of turn as a
|
||||
// potential blocker
|
||||
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
|
||||
return ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
|
||||
|| (ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && ai.getGame().getCombat() != null
|
||||
&& !ai.getGame().getCombat().getAttackers().isEmpty())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
&& !ai.getGame().getCombat().getAttackers().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
// Momir Vig, Simic Visionary Avatar
|
||||
public static class MomirVigAvatar {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
|
||||
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
// In MoJhoSto, prefer Jhoira sorcery ability from time to time
|
||||
@@ -1101,7 +1065,7 @@ public class SpecialCardAi {
|
||||
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA);
|
||||
|
||||
if (ai.getLandsInPlay().size() >= numLandsForJhoira && MyRandom.percentTrue(chanceToPrefJhoira)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1110,7 +1074,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Some basic strategy for Momir
|
||||
if (tokenSize < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tokenSize > 11) {
|
||||
@@ -1119,7 +1083,7 @@ public class SpecialCardAi {
|
||||
|
||||
sa.setXManaCostPaid(tokenSize);
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1158,13 +1122,13 @@ public class SpecialCardAi {
|
||||
|
||||
// Necropotence
|
||||
public static class Necropotence {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Game game = ai.getGame();
|
||||
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
||||
int maxHandSize = ai.getMaxHandSize();
|
||||
|
||||
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false; // nothing to draw from the library
|
||||
}
|
||||
|
||||
if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
|
||||
@@ -1172,7 +1136,7 @@ public class SpecialCardAi {
|
||||
|
||||
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
|
||||
// (not sure how to detect the presence of such effects yet)
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
PhaseHandler ph = game.getPhaseHandler();
|
||||
@@ -1194,33 +1158,23 @@ public class SpecialCardAi {
|
||||
// We're in a situation when we have nothing castable in hand, something needs to be done
|
||||
if (!blackViseOTB) {
|
||||
// exile-loot +1 card when at max hand size, hoping to get a workable spell or land
|
||||
if (computerHandSize + exiledWithNecro - 1 == maxHandSize) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return computerHandSize + exiledWithNecro - 1 == maxHandSize;
|
||||
} else {
|
||||
// Loot to 7 in presence of Black Vise, hoping to find what to do
|
||||
// NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead?
|
||||
if (computerHandSize + exiledWithNecro <= maxHandSize) {
|
||||
// Loot to 7, hoping to find something playable
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// Loot to 8, hoping to find something playable
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return computerHandSize + exiledWithNecro <= maxHandSize;
|
||||
}
|
||||
} else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) {
|
||||
// try not to overdraw in presence of Black Vise
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) {
|
||||
// Only draw until we reach max hand size
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) {
|
||||
// Only activate in AI's own turn (sans the exception above)
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1340,7 +1294,7 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
public static AiAbilityDecision considerSecondTarget(final Player ai, final SpellAbility sa) {
|
||||
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
|
||||
Card firstTgt = sa.getParent().getTargetCard();
|
||||
CardCollection candidates = ai.getOpponents().getCardsIn(ZoneType.Battlefield).filter(
|
||||
CardPredicates.sharesCardTypeWith(firstTgt).and(CardPredicates.isTargetableBy(sa)));
|
||||
@@ -1348,105 +1302,89 @@ public class SpecialCardAi {
|
||||
if (secondTgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(secondTgt);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Price of Progress
|
||||
public static class PriceOfProgress {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
// Don't play in early game - opponent likely still has lands to play
|
||||
if (ai.getGame().getPhaseHandler().getTurn() < 10) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
int aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
|
||||
// TODO Better if we actually calculate the true damage
|
||||
boolean willDieToPCasting = (ai.getLife() <= aiLands * 2);
|
||||
if (!willDieToPCasting) {
|
||||
boolean hasBridge = false;
|
||||
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
|
||||
// Do we have a card in play that makes us want to empty out hand?
|
||||
if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) {
|
||||
hasBridge = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll
|
||||
// even if suboptimal play, but don't waste the card too early even then!
|
||||
if (hasBridge) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.PlayToEmptyHand);
|
||||
boolean hasBridge = false;
|
||||
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
|
||||
// Do we have a card in play that makes us want to empty out hand?
|
||||
if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) {
|
||||
hasBridge = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
boolean willPlay = true;
|
||||
// Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll
|
||||
// even if suboptimal play, but don't waste the card too early even then!
|
||||
if ((hasBridge) && (ai.getGame().getPhaseHandler().getTurn() >= 10)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
int oppLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
|
||||
// Don't if no enemy nonbasic lands
|
||||
if (oppLands == 0) {
|
||||
willPlay = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always if enemy would die and we don't!
|
||||
// TODO : predict actual damage instead of assuming it'll be 2*lands
|
||||
// Don't if we lose, unless we lose anyway to unblocked creatures next turn
|
||||
if (willDieToPCasting &&
|
||||
if ((ai.getLife() <= aiLands * 2) &&
|
||||
(!(ComputerUtil.aiLifeInDanger(ai, true, 0)) && ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2))) {
|
||||
willPlay = false;
|
||||
return false;
|
||||
}
|
||||
// Do if we can win
|
||||
if (opp.getLife() <= oppLands * 2) {
|
||||
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
|
||||
if ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2) {
|
||||
return true;
|
||||
}
|
||||
// Don't if we'd lose a larger percentage of our remaining life than enemy
|
||||
if ((aiLands / ((double) ai.getLife())) >
|
||||
(oppLands / ((double) ai.getOpponentsSmallestLifeTotal()))) {
|
||||
willPlay = false;
|
||||
return false;
|
||||
}
|
||||
// Don't if no enemy nonbasic lands
|
||||
if (oppLands == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't if loss is equal in percentage but we lose more points
|
||||
if (((aiLands / ((double) ai.getLife())) == (oppLands / ((double) ai.getOpponentsSmallestLifeTotal())))
|
||||
&& (aiLands > oppLands)) {
|
||||
willPlay = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
if (willPlay) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Sarkhan the Mad
|
||||
public static class SarkhanTheMad {
|
||||
public static AiAbilityDecision considerDig(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
public static boolean considerDig(final Player ai, final SpellAbility sa) {
|
||||
return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1;
|
||||
}
|
||||
|
||||
public static AiAbilityDecision considerMakeDragon(final Player ai, final SpellAbility sa) {
|
||||
public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) {
|
||||
// TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier?
|
||||
CardCollection creatures = ai.getCreaturesInPlay();
|
||||
boolean hasValidTgt = !CardLists.filter(creatures, t -> t.getNetPower() < 5 && t.getNetToughness() < 5).isEmpty();
|
||||
if (hasValidTgt) {
|
||||
Card worstCreature = ComputerUtilCard.getWorstCreatureAI(creatures);
|
||||
sa.getTargets().add(worstCreature);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.AddBoardPresence);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) {
|
||||
int minLife = weakestOpp.getLife();
|
||||
|
||||
@@ -1502,7 +1440,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Sorin, Vengeful Bloodlord
|
||||
public static class SorinVengefulBloodlord {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
|
||||
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
|
||||
CardPredicates.CREATURES
|
||||
@@ -1516,7 +1454,7 @@ public class SpecialCardAi {
|
||||
CardLists.sortByCmcDesc(creaturesToGet);
|
||||
|
||||
if (creaturesToGet.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// pick the best creature that will stay on the battlefield
|
||||
@@ -1532,10 +1470,10 @@ public class SpecialCardAi {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(best);
|
||||
sa.setXManaCostPaid(best.getCMC());
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1649,27 +1587,23 @@ public class SpecialCardAi {
|
||||
|
||||
// The One Ring
|
||||
public static class TheOneRing {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
|
||||
int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN);
|
||||
|
||||
if (ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
|
||||
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.LifeInDanger);
|
||||
return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
|
||||
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The Scarab God
|
||||
public static class TheScarabGod {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
|
||||
Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
|
||||
|
||||
@@ -1680,19 +1614,13 @@ public class SpecialCardAi {
|
||||
sa.getTargets().add(worstOwnCreat);
|
||||
}
|
||||
|
||||
if (!sa.getTargets().isEmpty()) {
|
||||
// If we have a target, we can play this ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// No valid targets, can't play this ability
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return sa.getTargets().size() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Timetwister
|
||||
public static class Timetwister {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
int maxOppHandSize = 0;
|
||||
|
||||
@@ -1706,14 +1634,7 @@ public class SpecialCardAi {
|
||||
}
|
||||
|
||||
// use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand
|
||||
if (aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD) {
|
||||
// if the AI has less than 3 cards in hand or the opponent has more than 3 cards in hand than the AI
|
||||
// then the AI is willing to play this ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// otherwise, don't play this ability
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1774,12 +1695,12 @@ public class SpecialCardAi {
|
||||
|
||||
// Volrath's Shapeshifter
|
||||
public static class VolrathsShapeshifter {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
|
||||
// try not to do this too early to at least attempt to avoid situations where the AI
|
||||
// would cast a spell which would ruin the shapeshifting
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
|
||||
@@ -1795,15 +1716,11 @@ public class SpecialCardAi {
|
||||
if (topGY == null
|
||||
|| !topGY.isCreature()
|
||||
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
|
||||
if ( numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) {
|
||||
|
||||
@@ -39,75 +39,70 @@ import forge.util.collect.FCollectionView;
|
||||
*/
|
||||
public abstract class SpellAbilityAi {
|
||||
|
||||
public final AiAbilityDecision canPlayWithSubs(final Player aiPlayer, final SpellAbility sa) {
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (!decision.willingToPlay() && !"PlayForSub".equals(sa.getParam("AILogic"))) {
|
||||
return decision;
|
||||
public final boolean canPlayAIWithSubs(final Player aiPlayer, final SpellAbility sa) {
|
||||
if (!canPlayAI(aiPlayer, sa)) {
|
||||
return false;
|
||||
}
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
if (subAb == null) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a "main" SpellAbility
|
||||
*/
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(sa.getHostCard(), sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return canPlayWithoutRestrict(ai, sa);
|
||||
}
|
||||
|
||||
protected AiAbilityDecision canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
|
||||
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
|
||||
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())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
|
||||
return false;
|
||||
}
|
||||
} else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
|
||||
} else if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
|
||||
return false;
|
||||
}
|
||||
|
||||
AiAbilityDecision decision = checkApiLogic(ai, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
return decision;
|
||||
if (!checkApiLogic(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// needs to be after API logic because needs to check possible X Cost
|
||||
// needs to be after API logic because needs to check possible X Cost?
|
||||
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
return false;
|
||||
}
|
||||
|
||||
// for cards like Figure of Destiny
|
||||
// (it's unlikely many valid effect would work like this -
|
||||
// and while in theory AI could turn some conditions true in response that's far too advanced as default)
|
||||
if (!checkConditions(ai, sa)) {
|
||||
SpellAbility sub = sa.getSubAbility();
|
||||
if (sub == null || !checkConditions(ai, sub)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.NeedsToPlayCriteriaNotMet);
|
||||
}
|
||||
}
|
||||
return decision;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean checkConditions(final Player ai, final SpellAbility sa) {
|
||||
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {
|
||||
// copy it to disable some checks that the AI need to check extra
|
||||
SpellAbilityCondition con = (SpellAbilityCondition) sa.getConditions().copy();
|
||||
con = (SpellAbilityCondition) con.copy();
|
||||
|
||||
// if manaspent, check if AI can pay the colored mana as cost
|
||||
if (!con.getManaSpent().isEmpty()) {
|
||||
@@ -121,6 +116,40 @@ public abstract class SpellAbilityAi {
|
||||
return con.areMet(sa);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
||||
*/
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if (aiLogic.equals("CheckCondition")) {
|
||||
SpellAbility saCopy = sa.copy();
|
||||
saCopy.setActivatingPlayer(ai);
|
||||
return saCopy.metConditions();
|
||||
}
|
||||
|
||||
return !("Never".equals(aiLogic));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI is willing to pay for additional costs
|
||||
* <p>
|
||||
* Evaluated costs are: life, discard, sacrifice and counter-removal
|
||||
*/
|
||||
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI will play a SpellAbility based on its phase restrictions
|
||||
*/
|
||||
@@ -130,38 +159,19 @@ public abstract class SpellAbilityAi {
|
||||
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
|
||||
final String logic) {
|
||||
if (logic.equals("AtOppEOT")) {
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
return checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
||||
*/
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if ("Never".equals(aiLogic)) {
|
||||
return false;
|
||||
}
|
||||
if (!"Once".equals(aiLogic)) {
|
||||
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rest of the logic not covered by the canPlayAI template is defined here
|
||||
*/
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
|
||||
// 80% chance to play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false; // prevent infinite loop
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return MyRandom.getRandom().nextFloat() < .8f; // random success
|
||||
}
|
||||
|
||||
public final boolean doTrigger(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
// this evaluation order is currently intentional as it does more stuff that helps avoiding some crashes
|
||||
if (!ComputerUtilCost.canPayCost(sa, aiPlayer, true) && !mandatory) {
|
||||
return false;
|
||||
@@ -173,48 +183,28 @@ public abstract class SpellAbilityAi {
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory).willingToPlay();
|
||||
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory);
|
||||
}
|
||||
|
||||
public final AiAbilityDecision doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
AiAbilityDecision decision = doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
if (!decision.willingToPlay() && !"Always".equals(sa.getParam("AILogic"))) {
|
||||
return decision;
|
||||
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();
|
||||
if (subAb == null) {
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
decision = chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a triggered SpellAbility
|
||||
*/
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
AiAbilityDecision decision = canPlayWithoutRestrict(aiPlayer, sa);
|
||||
if (decision.willingToPlay() && (!mandatory || sa.isTargetNumberValid())) {
|
||||
// This is a weird check. Why do we care if its not mandatory if we WANT to do it?
|
||||
return decision;
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// not mandatory, short way out
|
||||
if (!mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// invalid target might prevent it
|
||||
@@ -230,13 +220,82 @@ public abstract class SpellAbilityAi {
|
||||
if (sa.canTarget(p)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a sub-SpellAbility
|
||||
*/
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
// sub-SpellAbility might use targets too
|
||||
if (sa.usesTargeting()) {
|
||||
// no Candidates, no adding to Stack
|
||||
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
|
||||
return false;
|
||||
}
|
||||
// but if it does, it should override this function
|
||||
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* isSorcerySpeed.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
|
||||
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|
||||
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* playReusable.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler phase = ai.getGame().getPhaseHandler();
|
||||
|
||||
// TODO probably also consider if winter orb or similar are out
|
||||
|
||||
if (sa instanceof AbilitySub) {
|
||||
return true; // This is only true for Drawbacks and triggers
|
||||
}
|
||||
|
||||
if (!sa.getPayCosts().isReusuableResource()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
|
||||
return true;
|
||||
}
|
||||
if (sa.isSpell() && !sa.isBuyback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,35 +304,9 @@ public abstract class SpellAbilityAi {
|
||||
* @param ab
|
||||
* @return
|
||||
*/
|
||||
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
final AbilitySub subAb = ab.getSubAbility();
|
||||
AiAbilityDecision decision = SpellApiToAi.Converter.get(ab).chkDrawback(ab, aiPlayer);
|
||||
if (!decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (subAb == null) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a sub-SpellAbility
|
||||
*/
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
// sub-SpellAbility might use targets too
|
||||
if (sa.usesTargeting()) {
|
||||
// no Candidates, no adding to Stack
|
||||
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
// but if it does, it should override this function
|
||||
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
|
||||
}
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
@@ -281,6 +314,25 @@ public abstract class SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParam("UnlessAI");
|
||||
boolean payNever = "Never".equals(aiLogic);
|
||||
boolean isMine = sa.getActivatingPlayer().equals(payer);
|
||||
|
||||
if (payNever) { return false; }
|
||||
|
||||
// AI will only pay when it's not already payed and only opponents abilities
|
||||
if (alreadyPaid || (payers.size() > 1 && isMine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
|
||||
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
|
||||
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
boolean hasPlayer = false;
|
||||
@@ -360,46 +412,6 @@ public abstract class SpellAbilityAi {
|
||||
return MyRandom.getRandom().nextBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI is willing to pay for additional costs
|
||||
* <p>
|
||||
* Evaluated costs are: life, discard, sacrifice and counter-removal
|
||||
*/
|
||||
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParam("UnlessAI");
|
||||
boolean payNever = "Never".equals(aiLogic);
|
||||
boolean isMine = sa.getActivatingPlayer().equals(payer);
|
||||
|
||||
if (payNever) { return false; }
|
||||
|
||||
// AI will only pay when it's not already payed and only opponents abilities
|
||||
if (alreadyPaid || (payers.size() > 1 && isMine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
|
||||
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
|
||||
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
|
||||
}
|
||||
|
||||
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, Player player, List<OptionalCostValue> optionalCostValues) {
|
||||
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
|
||||
Cost costSoFar = chosen.getPayCosts().copy();
|
||||
@@ -409,14 +421,14 @@ public abstract class SpellAbilityAi {
|
||||
Cost fullCost = opt.getCost().copy().add(costSoFar);
|
||||
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
|
||||
|
||||
// Playability check for Kicker
|
||||
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
|
||||
SpellAbility kickedSaCopy = fullCostSa.copy();
|
||||
kickedSaCopy.addOptionalCost(opt.getType());
|
||||
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
|
||||
copy.setCastSA(kickedSaCopy);
|
||||
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
|
||||
// don't choose kickers we don't want to play
|
||||
continue;
|
||||
continue; // don't choose kickers we don't want to play
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,56 +440,4 @@ public abstract class SpellAbilityAi {
|
||||
|
||||
return chosenOptCosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* isSorcerySpeed.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
|
||||
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|
||||
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* playReusable.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler phase = ai.getGame().getPhaseHandler();
|
||||
|
||||
// TODO probably also consider if winter orb or similar are out
|
||||
|
||||
if (sa instanceof AbilitySub) {
|
||||
return true; // This is only true for Drawbacks and triggers
|
||||
}
|
||||
|
||||
if (!sa.getPayCosts().isReusuableResource()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
|
||||
return true;
|
||||
}
|
||||
if (sa.isSpell() && !sa.isBuyback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -10,6 +8,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -17,69 +16,78 @@ import java.util.Map;
|
||||
public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getStrongestOpponent();
|
||||
|
||||
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (!defined.contains(opp)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getStrongestOpponent();
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (null == tgt) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
if (defined.contains(opp)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return defined.contains(opp);
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
boolean randomReturn = true;
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (defined.contains(ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getWeakestOpponent());
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -13,8 +11,8 @@ import forge.game.spellability.SpellAbility;
|
||||
public class AddPhaseAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.player.Player;
|
||||
@@ -40,7 +38,7 @@ import java.util.List;
|
||||
public class AddTurnAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
|
||||
|
||||
@@ -49,41 +47,41 @@ public class AddTurnAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(ai) && (mandatory || !ai.getGame().getReplacementHandler().wouldExtraTurnBeSkipped(ai))) {
|
||||
sa.getTargets().add(ai);
|
||||
} else if (mandatory) {
|
||||
for (final Player ally : ai.getAllies()) {
|
||||
for (final Player ally : ai.getAllies()) {
|
||||
if (sa.canTarget(ally)) {
|
||||
sa.getTargets().add(ally);
|
||||
break;
|
||||
sa.getTargets().add(ally);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
final List<Player> tgtPlayers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
for (final Player p : tgtPlayers) {
|
||||
if (p.isOpponentOf(ai) && !mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// TODO: improve ai for Sage of Hours
|
||||
if (!StringUtils.isNumeric(sa.getParam("NumTurns"))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return StringUtils.isNumeric(sa.getParam("NumTurns"));
|
||||
// not sure if the AI should be playing with cards that give the
|
||||
// Human more turns.
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return doTriggerAINoCost(aiPlayer, sa, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class AdvanceCrankAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
int nextSprocket = (ai.getCrankCounter() % 3) + 1;
|
||||
int crankCount = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isContraptionOnSprocket(nextSprocket));
|
||||
if (crankCount < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return super.canPlay(ai, sa);
|
||||
//Could evaluate whether we actually want to crank those, but this is probably fine for now.
|
||||
if(crankCount < 2)
|
||||
return false;
|
||||
return super.canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
|
||||
if(logic.equals("AtOppEOT"))
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
|
||||
return super.checkPhaseRestrictions(ai, sa, ph, logic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -18,7 +16,7 @@ import java.util.Map;
|
||||
public class AlterAttributeAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true"));
|
||||
String[] attributes = sa.getParam("Attributes").split(",");
|
||||
@@ -26,7 +24,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
// TODO add targeting logic
|
||||
// needed for Suspected
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
@@ -38,7 +36,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
|
||||
case "Solved":
|
||||
// there is currently no effect that would un-solve something
|
||||
if (!c.isSolved() && activate) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "Suspect":
|
||||
@@ -46,21 +44,21 @@ public class AlterAttributeAi extends SpellAbilityAi {
|
||||
// is Suspected good or bad?
|
||||
// currently Suspected is better
|
||||
if (!activate) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
|
||||
case "Saddle":
|
||||
case "Saddled":
|
||||
// AI should not try to Saddle again?
|
||||
if (c.isSaddled()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
@@ -14,8 +13,8 @@ public class AlwaysPlayAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,8 +3,6 @@ package forge.ai.ability;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -21,28 +19,24 @@ import java.util.Map;
|
||||
|
||||
public class AmassAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army");
|
||||
Card host = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
|
||||
if (!aiArmies.isEmpty()) {
|
||||
if (aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1))) {
|
||||
// If AI has an Army that can receive counters, play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// AI has Armies but none can receive counters, so don't play
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
|
||||
}
|
||||
return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
|
||||
}
|
||||
final String type = sa.getParam("Type");
|
||||
final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army";
|
||||
StringBuilder sb = new StringBuilder("b_0_0_");
|
||||
sb.append(sa.getOriginalParam("Type").toLowerCase()).append("_army");
|
||||
final String tokenScript = sb.toString();
|
||||
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
|
||||
|
||||
Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
|
||||
|
||||
if (token == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
token.setController(ai, 0);
|
||||
@@ -69,11 +63,7 @@ public class AmassAi extends SpellAbilityAi {
|
||||
//reset static abilities
|
||||
game.getAction().checkStaticAbilities(false);
|
||||
|
||||
if (result) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,12 +82,8 @@ public class AmassAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
|
||||
return checkApiLogic(ai, sa);
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -142,129 +142,130 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = aiPlayer.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
if (!sa.metConditions() && sa.getSubAbility() == null) {
|
||||
return false; // what is this for?
|
||||
}
|
||||
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
|
||||
// Should I animate a card before i have to sacrifice something better?
|
||||
if (!isAnimatedThisTurn(aiPlayer, source)) {
|
||||
rememberAnimatedThisTurn(aiPlayer, source);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
return true; // interrupt sacrifice
|
||||
}
|
||||
}
|
||||
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa, new CardCollection())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
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()) {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !c.isEquipping();
|
||||
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !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()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, source)) {
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return false;
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
return false;
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return false;
|
||||
}
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||
}
|
||||
return bFlag; // All of the defined stuff is animated, not very useful
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, source);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
AiAbilityDecision decision;
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
decision = animateTgtAI(sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
} else if (!mandatory) {
|
||||
return decision;
|
||||
} else {
|
||||
if(animateTgtAI(sa))
|
||||
return true;
|
||||
else if (!mandatory)
|
||||
return false;
|
||||
else {
|
||||
// fallback if animate is mandatory
|
||||
sa.resetTargets();
|
||||
List<Card> list = CardUtil.getValidCardsToTarget(sa);
|
||||
if (list.isEmpty()) {
|
||||
return decision;
|
||||
return false;
|
||||
}
|
||||
Card toAnimate = ComputerUtilCard.getWorstAI(list);
|
||||
rememberAnimatedThisTurn(aiPlayer, toAnimate);
|
||||
sa.getTargets().add(toAnimate);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -272,14 +273,9 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2);
|
||||
}
|
||||
|
||||
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
|
||||
if (sa.getMaxTargets() == 0) {
|
||||
// this happens if an optional cost is skipped, e.g. Brave the Wilds
|
||||
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
|
||||
}
|
||||
private boolean animateTgtAI(final SpellAbility sa) {
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final boolean alwaysActivatePWAbility = sa.isPwAbility()
|
||||
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
|
||||
@@ -291,13 +287,15 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
// list is empty, no possible targets
|
||||
if (list.isEmpty() && !alwaysActivatePWAbility) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// something is used for animate into creature
|
||||
@@ -364,7 +362,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
|
||||
// data is empty, no good targets
|
||||
if (data.isEmpty() && !alwaysActivatePWAbility) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the best creature to be animated
|
||||
@@ -387,18 +385,17 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
holdAnimatedTillMain2(ai, worst);
|
||||
if (!ComputerUtilMana.canPayManaCost(sa, ai, 0, sa.isTrigger())) {
|
||||
releaseHeldTillMain2(ai, worst);
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
rememberAnimatedThisTurn(ai, worst);
|
||||
sa.getTargets().add(worst);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
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?
|
||||
// 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);
|
||||
|
||||
@@ -406,7 +403,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
&& (buffed.getNetPower() - worst.getNetPower() >= 3 || !ComputerUtilCard.doesCreatureAttackAI(ai, worst))) {
|
||||
sa.getTargets().add(worst);
|
||||
rememberAnimatedThisTurn(ai, worst);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +415,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
|
||||
boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated);
|
||||
if (isValuableAttacker || isValuableBlocker)
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,23 +425,25 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
if(worst != null) {
|
||||
sa.getTargets().add(worst);
|
||||
rememberAnimatedThisTurn(ai, worst);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts") && !list.isEmpty()) {
|
||||
//No logic, but we do have preferences. Pick the best among those?
|
||||
Card best = ComputerUtilCard.getBestAI(list);
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
if(best != null) {
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||
// 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
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Card becomeAnimated(final Card card, final SpellAbility sa) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -11,30 +9,24 @@ import forge.game.spellability.SpellAbility;
|
||||
public class AnimateAllAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if ("CreatureAdvantage".equals(logic) && !aiPlayer.getCreaturesInPlay().isEmpty()) {
|
||||
// TODO: improve this or implement a better logic for abilities like Oko, the Trickster ultimate
|
||||
for (Card c : aiPlayer.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCard.doesCreatureAttackAI(aiPlayer, c)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("Always".equals(logic)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return "Always".equals(logic);
|
||||
} // end animateAllCanPlayAI()
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(aiPlayer, sa);
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -16,32 +16,30 @@ import java.util.List;
|
||||
|
||||
public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
//Pulls double duty as the OpenAttraction API. Same logic; usually good to do as long as we have the appropriate cards.
|
||||
CardCollectionView deck = getDeck(ai, sa);
|
||||
|
||||
if(deck.isEmpty())
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
|
||||
AiAbilityDecision superDecision = super.canPlay(ai, sa);
|
||||
if (!superDecision.willingToPlay())
|
||||
return superDecision;
|
||||
if(!super.canPlayAI(ai, sa))
|
||||
return false;
|
||||
|
||||
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
xPay = Math.max(xPay, deck.size());
|
||||
if (xPay == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
if(sa.hasParam("DefinedContraption") && sa.usesTargeting()) {
|
||||
if (getGoodReassembleTarget(ai, sa) == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return getGoodReassembleTarget(ai, sa) != null;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CardCollectionView getDeck(Player ai, SpellAbility sa) {
|
||||
@@ -50,11 +48,11 @@ public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
if (xPay == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
@@ -64,7 +62,7 @@ public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
if(target != null)
|
||||
sa.getTargets().add(target);
|
||||
else
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
@@ -86,16 +84,26 @@ public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if(!mandatory && getDeck(aiPlayer, sa).isEmpty())
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
|
||||
if(logic.equals("AtOppEOT"))
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if(getDeck(aiPlayer, sa).isEmpty())
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return super.chkDrawback(sa, aiPlayer);
|
||||
return false;
|
||||
|
||||
return super.chkAIDrawback(sa, aiPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if(!mandatory && getDeck(aiPlayer, sa).isEmpty())
|
||||
return false;
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -12,11 +10,11 @@ import java.util.Map;
|
||||
|
||||
public class AssignGroupAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
|
||||
// otherwise the AI considers the card playable.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
|
||||
|
||||
@@ -45,14 +45,24 @@ public class AttachAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures
|
||||
// and gaining card advantage
|
||||
if (source.hasKeyword("MayFlashSac") && !ai.canCastSorcery()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (source.isAura() && sa.isSpell() && !source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
|
||||
@@ -60,16 +70,20 @@ public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
// TODO: Add some extra checks for where the AI may want to cast a replacement aura
|
||||
// on another creature and keep it when the original enchanted creature is useless
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
|
||||
return false;
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attach spells always have a target
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
AiAbilityDecision attachDecision = attachPreference(sa, tgt, false);
|
||||
if (!attachDecision.willingToPlay()) {
|
||||
return attachDecision;
|
||||
if (!attachPreference(sa, tgt, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +94,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
}
|
||||
if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai)))
|
||||
&& source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
@@ -88,7 +102,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
if (xPay == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.setXManaCostPaid(xPay);
|
||||
@@ -98,10 +112,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
|
||||
effectExile.setActivatingPlayer(ai);
|
||||
final List<Card> targets = CardUtil.getValidCardsToTarget(effectExile);
|
||||
return !targets.isEmpty() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return !targets.isEmpty();
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean doAdvancedFlashAuraLogic(Player ai, SpellAbility sa, Card attachTarget) {
|
||||
@@ -941,8 +955,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
* @return true, if successful
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
final Card card = sa.getHostCard();
|
||||
// Check if there are any valid targets
|
||||
List<GameObject> targets = new ArrayList<>();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt == null) {
|
||||
@@ -954,44 +969,23 @@ public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
|
||||
Card newTarget = (Card) targets.get(0);
|
||||
//don't equip human creatures
|
||||
if (newTarget.getController().isOpponentOf(ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
//don't equip a worse creature
|
||||
if (card.isEquipping()) {
|
||||
Card oldTarget = card.getEquipping();
|
||||
if (ComputerUtilCard.evaluateCreature(oldTarget) > ComputerUtilCard.evaluateCreature(newTarget)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
boolean stacking = !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
|
||||
if (!stacking) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
// don't equip creatures that don't gain anything
|
||||
return !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player ai) {
|
||||
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);
|
||||
}
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
|
||||
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
|
||||
// Living Weapon or similar
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isAuraSpell(final SpellAbility sa) {
|
||||
@@ -1011,13 +1005,13 @@ public class AttachAi extends SpellAbilityAi {
|
||||
* the mandatory
|
||||
* @return true, if successful
|
||||
*/
|
||||
private static AiAbilityDecision attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
|
||||
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
|
||||
GameObject o;
|
||||
boolean spellCanTargetPlayer = false;
|
||||
if (isAuraSpell(sa)) {
|
||||
Card source = sa.getHostCard();
|
||||
if (!source.hasKeyword(Keyword.ENCHANT)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
|
||||
String ko = ki.getOriginal();
|
||||
@@ -1042,11 +1036,11 @@ public class AttachAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (o == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.getTargets().add(o);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1698,6 +1692,25 @@ 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, Map<String, Object> params) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -13,7 +11,7 @@ import forge.util.MyRandom;
|
||||
|
||||
public class BalanceAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
int diff = 0;
|
||||
Player opp = aiPlayer.getWeakestOpponent();
|
||||
@@ -39,7 +37,7 @@ public class BalanceAi extends SpellAbilityAi {
|
||||
|
||||
if (diff < 0) {
|
||||
// Don't sacrifice permanents even if opponent has a ton of cards in hand
|
||||
return new AiAbilityDecision(0, forge.ai.AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
final CardCollectionView humHand = opp.getCardsIn(ZoneType.Hand);
|
||||
@@ -47,7 +45,6 @@ public class BalanceAi extends SpellAbilityAi {
|
||||
diff += 0.5 * (humHand.size() - compHand.size());
|
||||
|
||||
// Larger differential == more chance to actually cast this spell
|
||||
boolean willPlay = diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
|
||||
return new AiAbilityDecision(willPlay ? 100 : 0, willPlay ? forge.ai.AiPlayDecision.WillPlay : AiPlayDecision.StopRunawayActivations);
|
||||
return diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -17,51 +16,55 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
public class BecomesBlockedAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Game game = aiPlayer.getGame();
|
||||
|
||||
if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
|| !game.getPhaseHandler().getPlayerTurn().isOpponentOf(aiPlayer)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
|
||||
sa.resetTargets();
|
||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (choice == null) { // can't find anything left
|
||||
return false;
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// TODO - implement AI
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
boolean chance;
|
||||
|
||||
// TODO - implement AI
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
chance = false;
|
||||
|
||||
return chance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -12,13 +10,14 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BidLifeAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -27,31 +26,31 @@ public class BidLifeAi extends SpellAbilityAi {
|
||||
if (tgt.canTgtCreature()) {
|
||||
List<Card> list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
Card c = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else if (tgt.getZone().contains(ZoneType.Stack)) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
final SpellAbility topSA = game.getStack().peekAbility();
|
||||
if (!topSA.isCounterableBy(sa) || aiPlayer.equals(topSA.getActivatingPlayer())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if (sa.canTargetSpellAbility(topSA)) {
|
||||
sa.getTargets().add(topSA);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -48,9 +46,9 @@ public final class BondAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
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) {
|
||||
@@ -58,7 +56,7 @@ public final class BondAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpecialAiLogic;
|
||||
import forge.ai.SpecialCardAi;
|
||||
@@ -23,18 +21,16 @@ public class BranchAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
if ("GrislySigil".equals(aiLogic)) {
|
||||
boolean result = SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
|
||||
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
return SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
|
||||
} else if ("BranchCounter".equals(aiLogic)) {
|
||||
boolean result = SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa);
|
||||
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
return SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa); // Bring the Ending, Anticognition (hacky implementation)
|
||||
} else if ("TgtAttacker".equals(aiLogic)) {
|
||||
final Combat combat = aiPlayer.getGame().getCombat();
|
||||
if (combat == null || combat.getAttackingPlayer() != aiPlayer) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
final CardCollection attackers = combat.getAttackers();
|
||||
@@ -49,20 +45,16 @@ public class BranchAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(attackers));
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
// TODO: expand for other cases where the AI is needed to make a decision on a branch
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (decision.willingToPlay() || mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -12,15 +10,15 @@ public class CannotPlayAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.player.Player;
|
||||
@@ -17,36 +15,34 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// TODO: Extend this if possible for cards that have this as an activated ability
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (logic.equals("WeakestOppExceptCtrl")) {
|
||||
PlayerCollection targetableOpps = aiPlayer.getOpponents();
|
||||
targetableOpps.remove(sa.getHostCard().getController());
|
||||
if (targetableOpps.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,3 +63,4 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
|
||||
return (T)weakestTargetableOpp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
@@ -18,7 +21,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Game game = sa.getHostCard().getGame();
|
||||
final SpellAbility topSa = game.getStack().isEmpty() ? null
|
||||
: ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
|
||||
@@ -29,50 +32,47 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
|
||||
// The AI can't otherwise play this ability, but should at least not
|
||||
// miss mandatory activations (e.g. triggers).
|
||||
if (sa.isMandatory()) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return sa.isMandatory();
|
||||
}
|
||||
|
||||
private AiAbilityDecision doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
|
||||
private boolean doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
|
||||
// For cards like Spellskite that retarget spells to itself
|
||||
if (topSa == null) {
|
||||
// nothing on stack, so nothing to target
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
final TargetChoices topTargets = topSa.getTargets();
|
||||
final Card topHost = topSa.getHostCard();
|
||||
|
||||
if (!sa.getTargets().isEmpty() && sa.isTrigger()) {
|
||||
if (sa.getTargets().size() != 0 && sa.isTrigger()) {
|
||||
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) {
|
||||
// if this does not target at all or already targets host, no need to redirect it again
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Card tgt : topTargets.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
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) {
|
||||
// make sure not to redirect our own abilities
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if (!topSa.canTarget(sa.getHostCard())) {
|
||||
// don't try targeting it if we can't legally target the host card with it in the first place
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if (!sa.canTarget(topSa)) {
|
||||
// don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face)
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) {
|
||||
@@ -85,22 +85,22 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
|
||||
&& topTargets.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 new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
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 new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
Player firstPlayer = topTargets.getFirstTargetedPlayer();
|
||||
if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(topSa);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,30 +46,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
private static CardCollection multipleCardsToChoose = new CardCollection();
|
||||
|
||||
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
|
||||
if (sa.isHidden()) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)
|
||||
&& !"Battlefield".equals(sa.getParam("Destination")) && !source.isLand()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostDiscard) {
|
||||
CostDiscard cd = (CostDiscard) part;
|
||||
// this is mainly for typecycling
|
||||
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isCraft()) {
|
||||
CardCollection payingCards = new CardCollection();
|
||||
int needed = 0;
|
||||
@@ -153,12 +129,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
// Checks for "return true" unlike checkAiLogic()
|
||||
|
||||
multipleCardsToChoose.clear();
|
||||
String aiLogic = sa.getParam("AILogic");
|
||||
if (aiLogic != null) {
|
||||
if (aiLogic.equals("Always")) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
||||
return doSacAndUpgradeLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
|
||||
@@ -178,18 +156,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (aiLogic.equals("MazesEnd")) {
|
||||
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("Pongify")) {
|
||||
if (sa.isTargetNumberValid()) {
|
||||
// Pre-targeted in checkAiLogic
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
|
||||
} else if (aiLogic.equals("ReturnCastable")) {
|
||||
if (!sa.getHostCard().getExiledCards().isEmpty()
|
||||
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return !sa.getHostCard().getExiledCards().isEmpty()
|
||||
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false);
|
||||
}
|
||||
}
|
||||
if (sa.isHidden()) {
|
||||
@@ -208,7 +178,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if (sa.isHidden()) {
|
||||
return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
|
||||
}
|
||||
@@ -227,7 +197,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (sa.isReplacementAbility() && "Command".equals(sa.getParam("Destination")) && "ReplacedCard".equals(sa.getParam("Defined"))) {
|
||||
@@ -236,10 +206,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if ("Always".equals(aiLogic)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if ("IfNotBuffed".equals(aiLogic)) {
|
||||
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true; // debuffed by opponent's auras to the level that it becomes useless
|
||||
}
|
||||
int delta = 0;
|
||||
for (Card enc : sa.getHostCard().getEnchantedBy()) {
|
||||
@@ -249,17 +219,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
delta++;
|
||||
}
|
||||
}
|
||||
if (delta <= 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return delta <= 0;
|
||||
} else if ("SaviorOfOllenbock".equals(aiLogic)) {
|
||||
if (SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa);
|
||||
}
|
||||
|
||||
if (sa.isHidden()) {
|
||||
@@ -288,8 +250,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
|
||||
private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// have access to more mana
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
@@ -303,35 +267,75 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// This happens when Origin is something like
|
||||
// "Graveyard,Library" (Doomsday)
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final String destination = sa.getParam("Destination");
|
||||
|
||||
if (sa.isNinjutsu()) {
|
||||
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
|
||||
}
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
|
||||
&& !(destination.equals("Battlefield") && !source.isLand())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ai.getGame().getCombat() == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
|
||||
boolean lowerCMC = false;
|
||||
for (Card attacker : attackers) {
|
||||
if (attacker.getCMC() < source.getCMC()) {
|
||||
lowerCMC = true;
|
||||
break;
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
for (final CostPart part : abCost.getCostParts()) {
|
||||
if (part instanceof CostDiscard) {
|
||||
CostDiscard cd = (CostDiscard) part;
|
||||
// this is mainly for typecycling
|
||||
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!lowerCMC) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
if (sa.isNinjutsu()) {
|
||||
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ai.getGame().getCombat() == null) {
|
||||
return false;
|
||||
}
|
||||
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
|
||||
boolean lowerCMC = false;
|
||||
for (Card attacker : attackers) {
|
||||
if (attacker.getCMC() < source.getCMC()) {
|
||||
lowerCMC = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lowerCMC) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't play if the conditions aren't met, unless it would trigger a beneficial sub-condition
|
||||
if (!activateForCost && !sa.metConditions()) {
|
||||
final AbilitySub abSub = sa.getSubAbility();
|
||||
if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) {
|
||||
if (!abSub.metConditions()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Iterable<Player> pDefined = Lists.newArrayList(source.getController());
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null && tgt.canTgtPlayer()) {
|
||||
@@ -343,7 +347,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
if (!sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
pDefined = sa.getTargets().getTargetPlayers();
|
||||
} else {
|
||||
@@ -387,12 +391,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!activateForCost && list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if ("Atarka's Command".equals(sourceName)
|
||||
&& (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) {
|
||||
// be strict on playing lands off charms
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
String num = sa.getParamOrDefault("ChangeNum", "1");
|
||||
@@ -400,60 +404,55 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (sa.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
if (xPay == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
if (xPay == 0) return false;
|
||||
xPay = Math.min(xPay, list.size());
|
||||
sa.setXManaCostPaid(xPay);
|
||||
} else {
|
||||
// Figure out the X amount, bail if it's zero (nothing will change zone).
|
||||
int xValue = AbilityUtils.calculateAmount(source, "X", sa);
|
||||
if (xValue == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceName.equals("Temur Sabertooth")) {
|
||||
// activated bounce + pump
|
||||
boolean pumpDecision = ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible"));
|
||||
AiAbilityDecision saveDecision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility());
|
||||
if (pumpDecision || saveDecision.willingToPlay()) {
|
||||
if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) ||
|
||||
ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility())) {
|
||||
for (Card c : list) {
|
||||
if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(source)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (canBouncePermanent(ai, sa, list) != null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return canBouncePermanent(ai, sa, list) != null;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// don't use fetching to top of library/graveyard before main2
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
||||
&& !sa.hasParam("ActivationPhases")) {
|
||||
if (!destination.equals("Battlefield") && !destination.equals("Hand")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
// Only tutor something in main1 if hand is almost empty
|
||||
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand")
|
||||
&& !aiLogic.equals("AnyMainPhase")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,7 +464,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
private static boolean hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
// if putting cards from hand to library and parent is drawing cards
|
||||
// make sure this will actually do something:
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -477,11 +476,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (!isCurse && sa.canTarget(aiPlayer)) {
|
||||
sa.getTargets().add(aiPlayer);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -495,10 +494,23 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a boolean.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
private static boolean hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// have access to more mana
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if (sa.getParam("AILogic").equals("Never")) {
|
||||
/*
|
||||
* Hack to stop AI from using Aviary Mechanic's "may bounce" trigger.
|
||||
* Ideally it should look for a good bounce target like "Pacifism"-victims
|
||||
* but there is no simple way to check that. It is preferable for the AI
|
||||
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
|
||||
* (bounce useful permanent).
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<ZoneType> origin = new ArrayList<>();
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
@@ -533,15 +545,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
pDefined = sa.getTargets().getTargetPlayers();
|
||||
|
||||
if (Iterables.isEmpty(pDefined)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
}
|
||||
@@ -555,10 +567,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// *********** Utility functions for Hidden ********************
|
||||
@@ -661,7 +673,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Retrieve either this card, or target Cards in Graveyard
|
||||
|
||||
final List<ZoneType> origin = Lists.newArrayList();
|
||||
@@ -673,16 +685,20 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (!isPreferredTarget(ai, sa, false, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// non-targeted retrieval
|
||||
final List<Card> retrieval = sa.knownDetermineDefined(sa.getParam("Defined"));
|
||||
|
||||
if (retrieval == null || retrieval.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// return this card from graveyard: cards like Hammer of Bogardan
|
||||
@@ -693,7 +709,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// (dying or losing control of)
|
||||
if (origin.contains(ZoneType.Battlefield)) {
|
||||
if (ai.getGame().getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
final AbilitySub abSub = sa.getSubAbility();
|
||||
@@ -706,7 +722,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (!(destination.equals(ZoneType.Exile)
|
||||
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
|
||||
&& !destination.equals(ZoneType.Hand)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, sa);
|
||||
@@ -718,13 +734,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (!contains) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (destination == ZoneType.Battlefield) {
|
||||
if (ComputerUtil.isETBprevented(retrieval.get(0))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// predict whether something may put a ETBing creature below zero toughness
|
||||
@@ -734,7 +750,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
final Card copy = CardCopyService.getLKICopy(c);
|
||||
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
|
||||
if (copy.getNetToughness() <= 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,12 +764,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (nothingWillReturn) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -767,7 +784,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.equals("SurvivalOfTheFittest")) {
|
||||
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
|
||||
return true;
|
||||
@@ -826,26 +843,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
if ("MimicVat".equals(sa.getParam("AILogic"))) {
|
||||
if (SpecialCardAi.MimicVat.considerExile(aiPlayer, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isPreferredTarget(aiPlayer, sa, false, true)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else {
|
||||
// if we are here, we have a target
|
||||
// so we can play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return isPreferredTarget(aiPlayer, sa, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1486,7 +1493,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (choice == null) { // can't find anything left
|
||||
if (sa.getTargets().isEmpty() || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
if (sa.getTargets().size() == 0 || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
}
|
||||
@@ -1514,21 +1521,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a boolean.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if ("DeathgorgeScavenger".equals(logic)) {
|
||||
if (SpecialCardAi.DeathgorgeScavenger.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
|
||||
} else if ("ExtraplanarLens".equals(logic)) {
|
||||
if (SpecialCardAi.ExtraplanarLens.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
|
||||
} else if ("ExileCombatThreat".equals(logic)) {
|
||||
return doExileCombatThreatLogic(ai, sa);
|
||||
}
|
||||
@@ -1540,27 +1539,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (!list.isEmpty()) {
|
||||
final Card attachedTo = list.get(0);
|
||||
// This code is for the Dragon auras
|
||||
if (!attachedTo.getController().isOpponentOf(ai)) {
|
||||
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the AI is the controller of the attachedTo card, then it is a valid target.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return !attachedTo.getController().isOpponentOf(ai);
|
||||
}
|
||||
}
|
||||
} else if (isPreferredTarget(ai, sa, mandatory, true)) {
|
||||
// do nothing
|
||||
} else {
|
||||
if (isUnpreferredTarget(ai, sa, mandatory)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else return isUnpreferredTarget(ai, sa, mandatory);
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, Player player, final Player decider) {
|
||||
@@ -1594,7 +1580,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (logic.startsWith("ExilePreference")) {
|
||||
return doExilePreferenceLogic(decider, sa, fetchList);
|
||||
} else if (logic.equals("BounceOwnTrigger")) {
|
||||
return doBounceOwnTriggerLogic(decider, sa, fetchList);
|
||||
return doBounceOwnTriggerLogic(decider, fetchList);
|
||||
}
|
||||
}
|
||||
if (fetchList.isEmpty()) {
|
||||
@@ -1791,7 +1777,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return super.chooseSingleAttackableEntity(ai, sa, options, params);
|
||||
}
|
||||
|
||||
private AiAbilityDecision doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
|
||||
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
|
||||
|
||||
@@ -1810,14 +1796,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestRet);
|
||||
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
private AiAbilityDecision doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
|
||||
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
String logic = sa.getParam("AILogic");
|
||||
@@ -1825,7 +1811,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (!ph.is(PhaseType.MAIN2)) {
|
||||
// Should be given a chance to cast other spells as well as to use a previously upgraded creature
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
return false;
|
||||
}
|
||||
|
||||
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
|
||||
@@ -1864,14 +1850,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (!listGoal.isEmpty()) {
|
||||
// make sure we're upgrading sacCMC->goalCMC
|
||||
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + sacCMC);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
// no candidates to upgrade
|
||||
return false;
|
||||
}
|
||||
|
||||
public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>)sa.getReplacingObject(AbilityKey.OriginalParams);
|
||||
SpellAbility causeSa = (SpellAbility)originalParams.get(AbilityKey.Cause);
|
||||
@@ -1880,13 +1867,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (Objects.equals(ZoneType.Hand, destination)) {
|
||||
// If the commander is being moved to your hand, don't replace since its easier to cast it again
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
|
||||
if (sa.getHostCard().getName().contains("Squee, the Immortal") &&
|
||||
(destination == ZoneType.Graveyard || destination == ZoneType.Exile)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
|
||||
@@ -1895,38 +1882,28 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (subApi == ApiType.ChangeZone && "Exile".equals(causeSub.getParam("Origin"))
|
||||
&& "Battlefield".equals(causeSub.getParam("Destination"))) {
|
||||
// A blink effect implemented using ChangeZone API
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else // This is an intrinsic effect that blinks the card (e.g. Obzedat, Ghost Council), no need to
|
||||
// return the commander to the Command zone.
|
||||
if (subApi == ApiType.DelayedTrigger) {
|
||||
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
|
||||
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
|
||||
// A blink effect implemented using a delayed trigger
|
||||
if (!"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|
||||
|| !causeSa.getActivatingPlayer().equals(aiPlayer)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return !"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"));
|
||||
}
|
||||
} else return causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|
||||
|| !causeSa.getActivatingPlayer().equals(aiPlayer);
|
||||
}
|
||||
|
||||
// Normally we want the commander back in Command zone to recast it later
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
// Normally we want the commander back in Command zone to recast him later
|
||||
return true;
|
||||
}
|
||||
|
||||
public static AiAbilityDecision doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
|
||||
public static boolean doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
|
||||
final Combat combat = aiPlayer.getGame().getCombat();
|
||||
|
||||
if (combat == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
Card choice = null;
|
||||
@@ -1961,9 +1938,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (choice != null) {
|
||||
sa.getTargets().add(choice);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Card doExilePreferenceLogic(final Player aiPlayer, final SpellAbility sa, CardCollection fetchList) {
|
||||
@@ -2158,18 +2135,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
|
||||
}
|
||||
|
||||
private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardCollection choices) {
|
||||
private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
|
||||
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
|
||||
// TODO check for threatened cards
|
||||
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
|
||||
if (!prefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(prefChoices);
|
||||
}
|
||||
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
|
||||
// some extra benefit like First Responder
|
||||
} else if (!unprefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getWorstAI(unprefChoices);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -21,7 +19,7 @@ import java.util.Map;
|
||||
|
||||
public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// Change Zone All, can be any type moving from one zone to another
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -34,14 +32,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll");
|
||||
|
||||
if (!aiLogicAllowsDiscard) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,29 +59,31 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
|
||||
// Ugin AI: always try to sweep before considering +1
|
||||
if (sourceName.equals("Ugin, the Spirit Dragon")) {
|
||||
boolean result = SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
|
||||
}
|
||||
|
||||
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
|
||||
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
if ("LivingDeath".equals(aiLogic)) {
|
||||
// Living Death AI
|
||||
return SpecialCardAi.LivingDeath.consider(ai, sa);
|
||||
} else if ("Timetwister".equals(aiLogic)) {
|
||||
// Timetwister AI
|
||||
return SpecialCardAi.Timetwister.consider(ai, sa);
|
||||
} else if ("RetDiscardedThisTurn".equals(aiLogic)) {
|
||||
boolean result = !ai.getDiscardedThisTurn().isEmpty() && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
// e.g. Shadow of the Grave
|
||||
return ai.getDiscardedThisTurn().size() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
} else if ("ExileGraveyards".equals(aiLogic)) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
|
||||
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.CREATURES);
|
||||
|
||||
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) {
|
||||
PlayerCollection players = ai.getOpponents();
|
||||
players.add(ai);
|
||||
@@ -98,48 +98,68 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
bestTgt = player;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestTgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestTgt);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO improve restrictions on when the AI would want to use this
|
||||
// spBounceAll has some AI we can compare to.
|
||||
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
|
||||
if (!sa.usesTargeting()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
// TODO: improve logic for non-targeted SAs of this type (most are currently AI:RemoveDeck:All, e.g. Memory Jar)
|
||||
return true;
|
||||
} else {
|
||||
// search targetable Opponents
|
||||
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the one with the most handsize
|
||||
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} 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
|
||||
// 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 new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
// set the target
|
||||
if (oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
computerType = new CardCollection();
|
||||
}
|
||||
|
||||
int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units)
|
||||
int nonCreatureEvalThreshold = 3; // CMC difference
|
||||
if (ai.getController().isAI()) {
|
||||
@@ -161,80 +181,103 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
|
||||
// Life is in serious danger, return all creatures from the battlefield to wherever
|
||||
// so they don't deal lethal damage
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluateCreatureList(oppType)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
|
||||
} // mass zone change for non-creatures: evaluate both lists by CMC and pass only if human
|
||||
// permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluatePermanentList(oppType)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cast during main1?
|
||||
if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
|
||||
return false;
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Graveyard)) {
|
||||
if (sa.usesTargeting()) {
|
||||
// search targetable Opponents
|
||||
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the one with the most in graveyard
|
||||
// zone is visible so evaluate which would be hurt the most
|
||||
Player oppTarget = Collections.max(oppList, AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
} else if (destination.equals(ZoneType.Library) && "Card.YouOwn".equals(sa.getParam("ChangeType"))) {
|
||||
boolean result = (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
|
||||
return (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
|
||||
&& !ComputerUtil.isPlayingReanimator(ai);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Exile)) {
|
||||
if (aiLogic.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 = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
|
||||
boolean noDiscard = aiLogic.contains(".noDiscard");
|
||||
|
||||
if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) {
|
||||
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
boolean result = (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Stack)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
if (destination.equals(ZoneType.Battlefield)) {
|
||||
if (sa.hasParam("GainControl")) {
|
||||
// Check if the cards are valuable enough
|
||||
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
|
||||
.evaluateCreatureList(oppType)) < 400) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
.evaluatePermanentList(oppType)) < 6) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// don't activate if human gets more back than AI does
|
||||
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(computerType) <= (ComputerUtilCard
|
||||
.evaluateCreatureList(oppType) + 100)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
|
||||
.evaluatePermanentList(oppType) + 2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean result = ((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance;
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
return (((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,11 +292,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// if putting cards from hand to library and parent is drawing cards
|
||||
// make sure this will actually do something:
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -285,90 +328,127 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, final SpellAbility sa, boolean mandatory) {
|
||||
// Change Zone All, can be any type moving from one zone to another
|
||||
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
|
||||
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) {
|
||||
boolean result = ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
// 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 ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
|
||||
}
|
||||
|
||||
CardCollectionView humanType = ai.getOpponents().getCardsIn(origin);
|
||||
humanType = AbilityUtils.filterListByType(humanType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
CardCollectionView computerType = ai.getCardsIn(origin);
|
||||
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
// TODO improve restrictions on when the AI would want to use this
|
||||
// spBounceAll has some AI we can compare to.
|
||||
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 new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the one with the most handsize
|
||||
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty() || mandatory) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Battlefield)) {
|
||||
// if mandatory, no need to evaluate
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
// 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 (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(computerType) >= ComputerUtilCard.evaluateCreatureList(humanType)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are more valuable
|
||||
else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
|
||||
return false;
|
||||
}
|
||||
} 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 new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return sa.isTargetNumberValid() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
// get the one with the most in graveyard
|
||||
// zone is visible so evaluate which would be hurt the most
|
||||
Player oppTarget = oppList.max(
|
||||
AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty() || mandatory) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Exile)) {
|
||||
|
||||
} else if (origin.equals(ZoneType.Stack)) {
|
||||
// currently only exists indirectly (e.g. Summary Dismissal via PlayAi)
|
||||
}
|
||||
|
||||
if (destination.equals(ZoneType.Battlefield)) {
|
||||
// if mandatory, no need to evaluate
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
if (sa.hasParam("GainControl")) {
|
||||
// Check if the cards are valuable enough
|
||||
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
boolean result = (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
boolean result = (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
return (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
return (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
.evaluatePermanentList(humanType)) >= 1;
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// don't activate if human gets more back than AI does
|
||||
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
boolean result = ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
boolean result = ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
return ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -17,7 +18,7 @@ import java.util.Map;
|
||||
|
||||
public class CharmAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
|
||||
|
||||
@@ -69,10 +70,10 @@ public class CharmAi extends SpellAbilityAi {
|
||||
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
|
||||
chosenList = chooseOptionsAi(sa, choices, ai, true, num, min);
|
||||
if (chosenList.isEmpty() && min != 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ public class CharmAi extends SpellAbilityAi {
|
||||
sa.setChosenList(chosenList);
|
||||
|
||||
if (choiceForOpp) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isSpell()) {
|
||||
@@ -88,7 +89,8 @@ public class CharmAi extends SpellAbilityAi {
|
||||
CharmEffect.chainAbilities(sa, chosenList);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
// prevent run-away activations - first time will always return true
|
||||
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
}
|
||||
|
||||
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
|
||||
@@ -274,10 +276,10 @@ public class CharmAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
// choices were already targeted
|
||||
if (ab.getRootAbility().getChosenList() != null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return super.chkDrawbackWithSubs(aiPlayer, ab);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import forge.game.Game;
|
||||
import forge.game.card.*;
|
||||
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;
|
||||
@@ -25,19 +26,19 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
* The rest of the logic not covered by the canPlayAI template is defined here
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
// search targetable Opponents
|
||||
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.getTargets().add(Iterables.getFirst(oppList, null));
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,12 +135,21 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
if (sa.hasParam("AILogic") && !checkAiLogic(ai, sa, sa.getParam("AILogic"))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
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 checkApiLogic(ai, sa);
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -22,20 +22,16 @@ import java.util.Map;
|
||||
public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
// Don't tap creatures that may be able to block
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
return false;
|
||||
}
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
if (logic.equals("CursedScroll")) {
|
||||
if (SpecialCardAi.CursedScroll.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return SpecialCardAi.CursedScroll.consider(ai, sa);
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -47,13 +43,13 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
if ("PithingNeedle".equals(aiLogic)) {
|
||||
// Make sure theres something in play worth Needlings.
|
||||
@@ -61,27 +57,18 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonManaActivatedAbility", ai, sa.getHostCard(), sa);
|
||||
if (oppPerms.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getBestPlaneswalkerAI(oppPerms);
|
||||
if (card != null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5 percent chance to cast per opposing card with a non mana ability
|
||||
if (MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size();
|
||||
}
|
||||
return mandatory;
|
||||
}
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
|
||||
|
||||
@@ -11,45 +11,40 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public class ChooseColorAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
return false;
|
||||
}
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Nykthos, Shrine to Nyx".equals(sourceName)) {
|
||||
if (SpecialCardAi.NykthosShrineToNyx.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return SpecialCardAi.NykthosShrineToNyx.consider(ai, sa);
|
||||
}
|
||||
|
||||
if ("Oona, Queen of the Fae".equals(sourceName)) {
|
||||
if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
// Set PayX here to maximum value.
|
||||
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("Addle".equals(sourceName)) {
|
||||
// TODO Why is this not in the AI logic?
|
||||
// Why are we specifying the weakest opponent?
|
||||
if (!ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
return !ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
|
||||
}
|
||||
|
||||
if (logic.equals("MostExcessOpponentControls")) {
|
||||
@@ -59,10 +54,10 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
|
||||
int excess = ComputerUtilCard.evaluatePermanentList(opplist) - ComputerUtilCard.evaluatePermanentList(ailist);
|
||||
if (excess > 4) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (logic.equals("MostProminentInComputerDeck")) {
|
||||
if ("Astral Cornucopia".equals(sourceName)) {
|
||||
// activate in Main 2 hoping that the extra mana surplus will make a difference
|
||||
@@ -70,28 +65,22 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
CardCollectionView permanents = CardLists.filter(ai.getCardsIn(ZoneType.Hand),
|
||||
CardPredicates.NONLAND_PERMANENTS);
|
||||
|
||||
if (!permanents.isEmpty() && ph.is(PhaseType.MAIN2, ai)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai);
|
||||
}
|
||||
} else if (logic.equals("HighestDevotionToColor")) {
|
||||
// currently only works more or less reliably in Main2 to cast own spells
|
||||
if (!ph.is(PhaseType.MAIN2, ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Direction;
|
||||
import forge.game.Game;
|
||||
@@ -20,11 +18,11 @@ public class ChooseDirectionAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final String logic = sa.getParam("AILogic");
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
if (logic == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
return false;
|
||||
} else {
|
||||
if ("Aminatou".equals(logic)) {
|
||||
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
|
||||
@@ -35,24 +33,19 @@ public class ChooseDirectionAi extends SpellAbilityAi {
|
||||
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Right));
|
||||
int leftValue = Aggregates.sum(left, Card::getCMC);
|
||||
int rightValue = Aggregates.sum(right, Card::getCMC);
|
||||
if (aiValue <= leftValue && aiValue <= rightValue) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return aiValue <= leftValue && aiValue <= rightValue;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public class ChooseEvenOddAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
return false;
|
||||
}
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -20,17 +19,16 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
@@ -19,6 +20,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class ChooseGenericAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
@@ -27,10 +29,13 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
return true;
|
||||
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
|
||||
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
|
||||
if (SpellApiToAi.Converter.get(sb).canPlayWithSubs(ai, sb).willingToPlay()) {
|
||||
if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if ("AtOppEOT".equals(aiLogic)) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
|
||||
} else if ("Always".equals(aiLogic)) {
|
||||
return true;
|
||||
}
|
||||
@@ -38,46 +43,35 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
// This is equivilant to what was here before but feels bad
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
return sa.hasParam("AILogic");
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
AiAbilityDecision decision;
|
||||
if (sa.isTrigger()) {
|
||||
decision = doTriggerNoCost(aiPlayer, sa, sa.isMandatory());
|
||||
} else {
|
||||
decision = checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
return decision;
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) {
|
||||
for (final Player p : aiPlayer.getOpponents()) {
|
||||
if (p.canBeTargetedBy(sa)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
|
||||
return true; // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
|
||||
}
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -268,7 +262,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
List<SpellAbility> filtered = Lists.newArrayList();
|
||||
// filter first for the spells which can be done
|
||||
for (SpellAbility sp : spells) {
|
||||
if (SpellApiToAi.Converter.get(sp).canPlayWithSubs(player, sp).willingToPlay()) {
|
||||
if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) {
|
||||
filtered.add(sp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
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.util.MyRandom;
|
||||
|
||||
public class ChooseNumberAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
return false;
|
||||
} else if (aiLogic.equals("SweepCreatures")) {
|
||||
int maxChoiceLimit = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Max"), sa);
|
||||
int ownCreatureCount = aiPlayer.getCreaturesInPlay().size();
|
||||
@@ -27,24 +30,17 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (refOpp == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
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) {
|
||||
// we're not pressured and our stuff seems better, don't do it yet
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false; // we're not pressured and our stuff seems better, don't do it yet
|
||||
}
|
||||
|
||||
if (ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit)) {
|
||||
// we have more creatures than the opponent, or we have less than the opponent but more than the max choice limit
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// we have less creatures than the opponent and less than the max choice limit
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -53,17 +49,16 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
@@ -17,18 +15,18 @@ import java.util.Map;
|
||||
|
||||
public class ChoosePlayerAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return canPlay(ai, sa);
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.*;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -11,6 +14,7 @@ import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -28,13 +32,21 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
|
||||
// TODO: AI Support! Currently this is copied from AF ChooseCard.
|
||||
// When implementing AI, I believe AI also needs to be made aware of the damage sources chosen
|
||||
// to be prevented (e.g. so the AI doesn't attack with a creature that will not deal any damage
|
||||
// to the player because a CoP was pre-activated on it - unless, of course, there's another
|
||||
// possible reason to attack with that creature).
|
||||
final Card host = sa.getHostCard();
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (abCost != null) {
|
||||
if (!willPayCosts(ai, sa, abCost, source)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -42,7 +54,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (sa.hasParam("AILogic")) {
|
||||
@@ -51,11 +63,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
if (!game.getStack().isEmpty()) {
|
||||
final SpellAbility topStack = game.getStack().peekAbility();
|
||||
if (sa.hasParam("Choices") && !topStack.matchesValid(topStack.getHostCard(), sa.getParam("Choices").split(","))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
final ApiType threatApi = topStack.getApi();
|
||||
if (threatApi != ApiType.DealDamage && threatApi != ApiType.DamageAll) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card threatSource = topStack.getHostCard();
|
||||
@@ -67,17 +79,13 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!objects.contains(ai) || topStack.hasParam("NoPrevention")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
int dmg = AbilityUtils.calculateAmount(threatSource, topStack.getParam("NumDmg"), topStack);
|
||||
if (ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0;
|
||||
}
|
||||
if (game.getPhaseHandler().getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
|
||||
if (sa.hasParam("Choices")) {
|
||||
@@ -90,13 +98,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
}
|
||||
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0;
|
||||
});
|
||||
if (choices.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return !choices.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,37 +21,23 @@ import java.util.Set;
|
||||
|
||||
public class ChooseTypeAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
return false;
|
||||
} else if ("MostProminentComputerControls".equals(aiLogic)) {
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
|
||||
if (doMirrorEntityLogic(aiPlayer, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return doMirrorEntityLogic(aiPlayer, sa);
|
||||
}
|
||||
return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty();
|
||||
} else if ("MostProminentComputerControlsOrOwns".equals(aiLogic)) {
|
||||
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty()
|
||||
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty();
|
||||
} else if ("MostProminentOppControls".equals(aiLogic)) {
|
||||
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty()
|
||||
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
|
||||
}
|
||||
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
return doTriggerAINoCost(aiPlayer, sa, false);
|
||||
}
|
||||
|
||||
private boolean doMirrorEntityLogic(Player aiPlayer, SpellAbility sa) {
|
||||
@@ -115,7 +101,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
boolean isCurse = sa.isCurse();
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -147,16 +133,16 @@ public class ChooseTypeAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false; // nothing to target?
|
||||
}
|
||||
} else {
|
||||
for (final Player p : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa)) {
|
||||
if (p.isOpponentOf(ai) && !mandatory && !isCurse) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String chooseType(SpellAbility sa, CardCollectionView cards) {
|
||||
|
||||
@@ -2,8 +2,6 @@ package forge.ai.ability;
|
||||
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -24,15 +22,14 @@ public class ClashAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
boolean legalAction = true;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
legalAction = selectTarget(aiPlayer, sa);
|
||||
}
|
||||
|
||||
return legalAction ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return legalAction;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -42,17 +39,14 @@ public class ClashAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
boolean legalAction = true;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
legalAction = selectTarget(ai, sa);
|
||||
if (!legalAction) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return legalAction;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -110,6 +104,7 @@ public class ClashAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return !sa.getTargets().isEmpty();
|
||||
return sa.getTargets().size() > 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.SpellApiToAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -14,8 +12,7 @@ import forge.game.trigger.TriggerType;
|
||||
|
||||
public class ClassLevelUpAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
// TODO does leveling up affect combat? Otherwise wait for Main2
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
Card host = sa.getHostCard();
|
||||
final int level = host.getClassLevel() + 1;
|
||||
for (StaticAbility stAb : host.getStaticAbilities()) {
|
||||
@@ -28,12 +25,12 @@ public class ClassLevelUpAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
SpellAbility effect = t.ensureAbility();
|
||||
if (!SpellApiToAi.Converter.get(effect).doTrigger(aiPlayer, effect, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -20,7 +18,7 @@ import java.util.Map;
|
||||
public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
|
||||
@@ -38,6 +36,10 @@ 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())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PhaseHandler phase = game.getPhaseHandler();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -64,19 +66,18 @@ public class CloneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!bFlag) { // All of the defined stuff is cloned, not very useful
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
useAbility &= cloneTgtAI(sa);
|
||||
}
|
||||
|
||||
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return useAbility;
|
||||
} // end cloneCanPlayAI()
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// AI should only activate this during Human's turn
|
||||
boolean chance = true;
|
||||
|
||||
@@ -84,12 +85,11 @@ public class CloneAi extends SpellAbilityAi {
|
||||
chance = cloneTgtAI(sa);
|
||||
}
|
||||
|
||||
return chance ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return chance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
Card host = sa.getHostCard();
|
||||
boolean chance = true;
|
||||
|
||||
@@ -111,11 +111,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
// Eventually, we can call the trigger of ETB abilities with
|
||||
// not mandatory as part of the checks to cast something
|
||||
|
||||
if (mandatory || chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return chance || mandatory;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
@@ -11,16 +14,16 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
public class ConniveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
if (!ai.canDraw()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false; // can't draw anything
|
||||
}
|
||||
|
||||
Card host = sa.getHostCard();
|
||||
|
||||
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
|
||||
if (num == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false; // Won't do anything
|
||||
}
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -38,7 +41,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if ((list.isEmpty() && sa.isTargetNumberValid() && !sa.getTargets().isEmpty())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
@@ -50,7 +53,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
Card choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
@@ -63,17 +66,13 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
if (!sa.getTargets().isEmpty() && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return !sa.getTargets().isEmpty() && sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!ai.canDraw() && !mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false; // can't draw anything
|
||||
}
|
||||
|
||||
boolean preferred = true;
|
||||
@@ -86,7 +85,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (mandatory) {
|
||||
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (list.isEmpty() && preferred) {
|
||||
@@ -99,13 +98,14 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
// Still an empty list, but we have to choose something (mandatory); expand targeting to
|
||||
// include AI's own cards to see if there's anything targetable (e.g. Plague Belcher).
|
||||
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
preferred = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
Card choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
@@ -118,10 +118,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(
|
||||
sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0,
|
||||
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
@@ -11,6 +13,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public class ControlExchangeAi extends SpellAbilityAi {
|
||||
|
||||
@@ -18,7 +21,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, final SpellAbility sa) {
|
||||
Card object1 = null;
|
||||
Card object2 = null;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -38,40 +41,35 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(object2);
|
||||
}
|
||||
if (object1 == null || object2 == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
if (ComputerUtilCard.evaluateCreature(object1) > ComputerUtilCard.evaluateCreature(object2) + 40) {
|
||||
sa.getTargets().add(object1);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (mandatory) {
|
||||
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return decision;
|
||||
return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid();
|
||||
} else {
|
||||
return canPlay(aiPlayer, sa);
|
||||
return canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if (!sa.usesTargeting()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -92,7 +90,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty())
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
|
||||
Card best = ComputerUtilCard.getBestAI(list);
|
||||
|
||||
@@ -108,7 +106,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
|
||||
// Defined card is better than this one, try to avoid trade
|
||||
if (!best.equals(realBest)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +115,10 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
return doTrigTwoTargetsLogic(aiPlayer, sa, best);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
private AiAbilityDecision doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
|
||||
private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final int creatureThreshold = 100; // TODO: make this configurable from the AI profile
|
||||
final int nonCreatureThreshold = 2;
|
||||
@@ -132,30 +130,30 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
Card aiWorst = ComputerUtilCard.getWorstAI(list);
|
||||
if (aiWorst == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aiWorst != bestFirstTgt) {
|
||||
if (bestFirstTgt.isCreature() && aiWorst.isCreature()) {
|
||||
if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) {
|
||||
sa.getTargets().add(aiWorst);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine
|
||||
if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) {
|
||||
sa.getTargets().add(aiWorst);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa.clearTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ import java.util.Map;
|
||||
*/
|
||||
public class ControlGainAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
final List<String> lose = Lists.newArrayList();
|
||||
|
||||
if (sa.hasParam("LoseControl")) {
|
||||
@@ -81,30 +81,22 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("AllValid")) {
|
||||
CardCollectionView tgtCards = opponents.getCardsIn(ZoneType.Battlefield);
|
||||
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
|
||||
|
||||
if (tgtCards.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return !tgtCards.isEmpty();
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
if (sa.hasParam("TargetingPlayer")) {
|
||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
|
||||
sa.setTargetingPlayer(targetingPlayer);
|
||||
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return targetingPlayer.getController().chooseTargetsFor(sa);
|
||||
}
|
||||
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
List<Player> oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tgt.isRandomTarget()) {
|
||||
@@ -119,12 +111,12 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
if (lose.contains("EOT")
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& !sa.isTrigger()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("Defined")) {
|
||||
// no need to target, we'll pick up the target from Defined
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
CardCollection list = opponents.getCardsIn(ZoneType.Battlefield);
|
||||
@@ -173,7 +165,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
int creatures = 0, artifacts = 0, planeswalkers = 0, lands = 0, enchantments = 0;
|
||||
@@ -202,7 +194,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -265,41 +257,39 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(
|
||||
sa.isTargetNumberValid() ? 100 : 0,
|
||||
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (sa.hasParam("TargetingPlayer") || (mandatory && !this.canPlay(ai, sa).willingToPlay())) {
|
||||
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 new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
sa.getTargets().add(Aggregates.random(oppList));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, final Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
|
||||
final Game game = ai.getGame();
|
||||
|
||||
// Special card logic that is processed elsewhere
|
||||
@@ -315,7 +305,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
CardCollectionView tgtCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
|
||||
if (tgtCards.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final List<String> lose = Lists.newArrayList();
|
||||
@@ -324,14 +314,10 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
lose.addAll(Lists.newArrayList(sa.getParam("LoseControl").split(",")));
|
||||
}
|
||||
|
||||
if (lose.contains("EOT")
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
} else {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return !lose.contains("EOT")
|
||||
|| !game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
|
||||
} else {
|
||||
return this.canPlay(ai, sa);
|
||||
return this.canPlayAI(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -43,22 +41,24 @@ import java.util.Map;
|
||||
*/
|
||||
public class ControlGainVariantAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
|
||||
if ("GainControlOwns".equals(logic)) {
|
||||
List<Card> list = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), crd -> crd.isCreature() && !crd.getController().equals(crd.getOwner()));
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
for (final Card c : list) {
|
||||
if (ai.equals(c.getController())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,52 +22,45 @@ import java.util.function.Predicate;
|
||||
|
||||
public class CopyPermanentAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("MomirAvatar".equals(aiLogic)) {
|
||||
return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
|
||||
} else if ("MimicVat".equals(aiLogic)) {
|
||||
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
|
||||
} else if ("AtEOT".equals(aiLogic)) {
|
||||
if (ph.is(PhaseType.END_OF_TURN)) {
|
||||
if (ph.getPlayerTurn() == aiPlayer) {
|
||||
// If it's the AI's turn, it can activate at EOT
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If it's not the AI's turn, it can't activate at EOT
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// Not at EOT phase
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
} if ("DuplicatePerms".equals(aiLogic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN);
|
||||
} else if ("AtOppEOT".equals(aiLogic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn() != aiPlayer;
|
||||
} else if ("DuplicatePerms".equals(aiLogic)) {
|
||||
final List<Card> valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if (valid.size() < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("AtEOT") && !ph.is(PhaseType.MAIN1)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("Defined")) {
|
||||
// If there needs to be an imprinted card, don't activate the ability if nothing was imprinted yet (e.g. Mimic Vat)
|
||||
if (sa.getParam("Defined").equals("Imprinted.ExiledWithSource") && source.getImprintedCards().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isEmbalm() || sa.isEternalize()) {
|
||||
// E.g. Vizier of Many Faces: check to make sure it makes sense to make the token now
|
||||
AiPlayDecision decision = ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa);
|
||||
|
||||
if (decision != AiPlayDecision.WillPlay) {
|
||||
return new AiAbilityDecision(0, decision);
|
||||
if (ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa) != AiPlayDecision.WillPlay) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,45 +75,37 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
|
||||
sa.setTargetingPlayer(targetingPlayer);
|
||||
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return targetingPlayer.getController().chooseTargetsFor(sa);
|
||||
} else if (sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer()) {
|
||||
if (!sa.isCurse()) {
|
||||
if (sa.canTarget(aiPlayer)) {
|
||||
sa.getTargets().add(aiPlayer);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else {
|
||||
for (Player p : aiPlayer.getYourTeam()) {
|
||||
if (sa.canTarget(p)) {
|
||||
sa.getTargets().add(p);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Player p : aiPlayer.getOpponents()) {
|
||||
if (sa.canTarget(p)) {
|
||||
sa.getTargets().add(p);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
return doTriggerAINoCost(aiPlayer, sa, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Player activator = sa.getActivatingPlayer();
|
||||
final Game game = host.getGame();
|
||||
@@ -143,13 +128,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
|
||||
//Nothing to target
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection betterList = CardLists.filter(list, CardPredicates.isRemAIDeck().negate());
|
||||
if (betterList.isEmpty()) {
|
||||
if (!mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
list = betterList;
|
||||
@@ -161,7 +146,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
if (felidarGuardian.size() > 0) {
|
||||
// can copy a Felidar Guardian and combo off, so let's do it
|
||||
sa.getTargets().add(felidarGuardian.get(0));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,9 +155,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -192,9 +177,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -209,22 +194,20 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host, sa);
|
||||
Collection<Card> betterChoices = getBetterOptions(aiPlayer, sa, choices, !mandatory);
|
||||
if (betterChoices.isEmpty()) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return mandatory;
|
||||
}
|
||||
} else {
|
||||
// if no targeting, it should always be ok
|
||||
}
|
||||
|
||||
if ("TriggeredCardController".equals(sa.getParam("Controller"))) {
|
||||
Card trigCard = (Card)sa.getTriggeringObject(AbilityKey.Card);
|
||||
if (!mandatory && trigCard != null && trigCard.getController().isOpponentOf(aiPlayer)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -17,15 +17,14 @@ import java.util.Map;
|
||||
public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
Game game = aiPlayer.getGame();
|
||||
int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK);
|
||||
int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF);
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (game.getStack().isEmpty()) {
|
||||
boolean result = sa.isMandatory() || "Always".equals(logic);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return sa.isMandatory() || "Always".equals(logic);
|
||||
}
|
||||
|
||||
final SpellAbility top = game.getStack().peekAbility();
|
||||
@@ -42,40 +41,47 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!MyRandom.percentTrue(chance)
|
||||
&& !"Always".equals(logic)
|
||||
&& !"AlwaysIfViable".equals(logic)
|
||||
&& !"OnceIfViable".equals(logic)
|
||||
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("OnceIfViable".equals(logic)) {
|
||||
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
// Filter AI-specific targets if provided
|
||||
if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
|
||||
if (!top.getActivatingPlayer().equals(aiPlayer)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (top.isWrapper() || top.isActivatedAbility()) {
|
||||
// Shouldn't even try with triggered or wrapped abilities at this time, will crash
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (top.getApi() == ApiType.CopySpellAbility) {
|
||||
// Don't try to copy a copy ability, too complex for the AI to handle
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (top.getApi() == ApiType.Mana) {
|
||||
// would lead to Stack Overflow by trying to play this again
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) {
|
||||
if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
|
||||
// If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
|
||||
// it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if (top.hasParam("ConditionManaSpent") || top.getHostCard().hasSVar("AINoCopy")) {
|
||||
// Mana spent is not copied, so these spells generally do nothing when copied.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
|
||||
// Don't try to copy anything you can't understand how to handle
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A copy is necessary to properly test the SA before targeting the copied spell, otherwise the copy SA will fizzle.
|
||||
@@ -93,49 +99,32 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
sa.getTargets().add(top);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, decision);
|
||||
}
|
||||
}
|
||||
|
||||
// the AI should not miss mandatory activations
|
||||
boolean result = sa.isMandatory() || "Always".equals(logic);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return sa.isMandatory() || "Always".equals(logic);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (logic.contains("Always")) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return mandatory || logic.contains("Always"); // this includes logic like AlwaysIfViable
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||
}
|
||||
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||
}
|
||||
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
if (sa.isMandatory()) {
|
||||
return super.chkDrawback(sa, aiPlayer);
|
||||
}
|
||||
}
|
||||
return decision;
|
||||
return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,7 +138,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
// Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then
|
||||
// run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents.
|
||||
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(player, sa).willingToPlay();
|
||||
return SpecialCardAi.ChainOfAcid.consider(player, sa);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,11 +26,13 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
|
||||
public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
boolean toReturn = true;
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final Game game = ai.getGame();
|
||||
@@ -38,12 +40,22 @@ public class CounterAi extends SpellAbilityAi {
|
||||
SpellAbility tgtSA = null;
|
||||
|
||||
if (game.getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ("Force of Will".equals(sourceName)) {
|
||||
if (!SpecialCardAi.ForceOfWill.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,19 +63,19 @@ public class CounterAi extends SpellAbilityAi {
|
||||
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
|
||||
if ((topSA.isSpell() && !topSA.isCounterableBy(sa)) || ai.getYourTeam().contains(topSA.getActivatingPlayer())) {
|
||||
// might as well check for player's friendliness
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided
|
||||
if (sa.hasParam("AITgts") && (topSA.getHostCard() == null
|
||||
|| !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("CounterNoManaSpell") && topSA.getTotalManaSpent() > 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("UnlessCost") && "TargetedController".equals(sa.getParamOrDefault("UnlessPayer", "TargetedController"))) {
|
||||
@@ -72,7 +84,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
CostDiscard discardCost = unlessCost.getCostPartByType(CostDiscard.class);
|
||||
if ("Hand".equals(discardCost.getType())) {
|
||||
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,11 +100,10 @@ public class CounterAi extends SpellAbilityAi {
|
||||
tgtCMC += topSA.getPayCosts().getTotalMana().countX() > 0 ? 3 : 0; // TODO: somehow determine the value of X paid and account for it?
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// This spell doesn't target. Must be a "Coutner All" or "Counter trigger" type of ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
@@ -111,13 +122,13 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (toPay == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
// If this is a reusable Resource, feel free to play it most of the time
|
||||
if (!playReusable(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,15 +147,15 @@ public class CounterAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("Never".equals(logic)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts
|
||||
int minCMC = Integer.parseInt(logic.substring(7));
|
||||
if (tgtCMC < minCMC) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if ("NullBrooch".equals(logic)) {
|
||||
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,40 +234,40 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (dontCounter) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return doTriggerNoCost(aiPlayer, sa, true);
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return doTriggerAINoCost(aiPlayer, sa, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Game game = ai.getGame();
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (mandatory && !sa.canAddMoreTarget()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory);
|
||||
SpellAbility tgtSA = pair.getLeft();
|
||||
|
||||
if (tgtSA == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
sa.getTargets().add(tgtSA);
|
||||
if (!mandatory && !pair.getRight()) {
|
||||
// If not mandatory and not preferred, bail out after setting target
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
@@ -277,13 +288,14 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
if (!mandatory) {
|
||||
if (toPay == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
// If this is a reusable Resource, feel free to play it most of the time
|
||||
// If this is a reusable Resource, feel free to play it most
|
||||
// of the time
|
||||
if (!playReusable(ai,sa) || (MyRandom.getRandom().nextFloat() < .4)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,7 +312,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
// force the Human into making decisions)
|
||||
|
||||
// But really it should be more picky about how it counters things
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Pair<SpellAbility, Boolean> chooseTargetSpellAbility(Game game, SpellAbility sa, Player ai, boolean mandatory) {
|
||||
@@ -350,11 +362,11 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
// ward or human misplay
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
|
||||
for (SpellAbility toBeCountered : spells) {
|
||||
// ward or human misplay
|
||||
if (!toBeCountered.isCounterableBy(sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -369,7 +381,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// no reason to pay if we don't plan to confirm
|
||||
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false).willingToPlay()) {
|
||||
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
|
||||
return false;
|
||||
}
|
||||
// TODO check hasFizzled
|
||||
|
||||
@@ -45,13 +45,13 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
*
|
||||
* @param list
|
||||
* a {@link CardCollectionView} object.
|
||||
* a {@link forge.CardList} object.
|
||||
* @param type
|
||||
* a {@link String} object.
|
||||
* a {@link java.lang.String} object.
|
||||
* @param amount
|
||||
* a int.
|
||||
* @param ai a {@link Player} object.
|
||||
* @return a {@link Card} object.
|
||||
* @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) {
|
||||
Card choice;
|
||||
@@ -65,7 +65,7 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
// try to kill the best killable creature, or reduce the best one
|
||||
// but try not to target a Undying Creature
|
||||
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING);
|
||||
if (!killable.isEmpty()) {
|
||||
if (killable.size() > 0) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(killable);
|
||||
} else {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
@@ -83,10 +83,10 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
*
|
||||
* @param list
|
||||
* a {@link CardCollectionView} object.
|
||||
* a {@link forge.CardList} object.
|
||||
* @param type
|
||||
* a {@link String} object.
|
||||
* @return a {@link Card} object.
|
||||
* a {@link java.lang.String} object.
|
||||
* @return a {@link forge.game.card.Card} object.
|
||||
*/
|
||||
public static Card chooseBoonTarget(final CardCollectionView list, final String type) {
|
||||
Card choice = null;
|
||||
@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
} else if (type.equals("DIVINITY")) {
|
||||
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
|
||||
} else if (CounterType.getType(type).isKeywordCounter()) {
|
||||
} else if (CounterType.get(type).isKeywordCounter()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
|
||||
} else {
|
||||
// The AI really should put counters on cards that can use it.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
@@ -19,25 +21,19 @@ import java.util.Map;
|
||||
|
||||
public class CountersMoveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
AiAbilityDecision decision = new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
decision = moveTgtAI(ai, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
return decision;
|
||||
if (!moveTgtAI(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!playReusable(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MyRandom.getRandom().nextFloat() < .8f) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return MyRandom.getRandom().nextFloat() < .8f; // random success
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,13 +109,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
|
||||
AiAbilityDecision decision = moveTgtAI(ai, sa);
|
||||
if (!decision.willingToPlay() && !mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
if (!moveTgtAI(ai, sa) && !mandatory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sa.isTargetNumberValid() && mandatory) {
|
||||
@@ -127,18 +122,18 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
List<Card> tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
if (tgtCards.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card card = ComputerUtilCard.getWorstAI(tgtCards);
|
||||
sa.getTargets().add(card);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else {
|
||||
// no target Probably something like Graft
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
final Card host = sa.getHostCard();
|
||||
@@ -150,7 +145,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
|
||||
|
||||
if (srcCards.isEmpty() || destCards.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card src = srcCards.get(0);
|
||||
@@ -158,21 +153,21 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
// for such Trigger, do not move counter to another players creature
|
||||
if (!dest.getController().equals(ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (ComputerUtilCard.isUselessCreature(ai, dest)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (dest.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cType != null) {
|
||||
if (!dest.canReceiveCounters(cType)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
final int amount = calcAmount(sa, cType);
|
||||
int a = src.getCounters(cType);
|
||||
if (a < amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card srcCopy = CardCopyService.getLKICopy(src);
|
||||
@@ -186,31 +181,27 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
int newEval = ComputerUtilCard.evaluateCreature(srcCopy) + ComputerUtilCard.evaluateCreature(destCopy);
|
||||
|
||||
if (newEval < oldEval) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for some specific AI preferences
|
||||
if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
|
||||
if (!cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0;
|
||||
}
|
||||
}
|
||||
// no target
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
return moveTgtAI(ai, sa);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int calcAmount(final SpellAbility sa, final CounterType cType) {
|
||||
@@ -235,7 +226,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
return amount;
|
||||
}
|
||||
|
||||
private AiAbilityDecision moveTgtAI(final Player ai, final SpellAbility sa) {
|
||||
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");
|
||||
@@ -253,7 +244,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (destCards.isEmpty()) {
|
||||
// something went wrong
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card dest = destCards.get(0);
|
||||
@@ -262,7 +253,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
tgtCards.remove(dest);
|
||||
|
||||
if (cType != null && !dest.canReceiveCounters(cType)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// prefered logic for this: try to steal counter
|
||||
@@ -294,7 +285,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -338,14 +329,14 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else if (sa.getMaxTargets() == 2) {
|
||||
// TODO
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// SA uses target for Defined
|
||||
// Source => Targeted
|
||||
@@ -353,12 +344,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (srcCards.isEmpty()) {
|
||||
// something went wrong
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Card src = srcCards.get(0);
|
||||
if (cType != null && src.getCounters(cType) <= 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
Card lkiWithCounters = CardCopyService.getLKICopy(src);
|
||||
@@ -411,14 +402,14 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|
||||
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||
if (!isMandatoryTrigger) {
|
||||
// no good target
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,10 +439,10 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
@@ -19,41 +21,42 @@ import java.util.Map;
|
||||
public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
return setTargets(ai, sa);
|
||||
}
|
||||
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final CounterType counterType = getCounterType(sa);
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
if (!sa.usesTargeting()) {
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
} else {
|
||||
return setTargets(ai, sa);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
@@ -82,27 +85,24 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!sa.usesTargeting()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
AiAbilityDecision decision = setTargets(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
if (setTargets(ai, sa)) {
|
||||
return true;
|
||||
} else if (mandatory) {
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
Card safeMatch = list.stream()
|
||||
.filter(CardPredicates.hasCounters().negate())
|
||||
.findFirst().orElse(null);
|
||||
sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch);
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
private CounterType getCounterType(SpellAbility sa) {
|
||||
@@ -117,7 +117,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
return null;
|
||||
}
|
||||
|
||||
private AiAbilityDecision setTargets(Player ai, SpellAbility sa) {
|
||||
private boolean setTargets(Player ai, SpellAbility sa) {
|
||||
final CounterType counterType = getCounterType(sa);
|
||||
|
||||
final Game game = ai.getGame();
|
||||
@@ -154,7 +154,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (counterType == null || counterType.is(type)) {
|
||||
addTargetsByCounterType(ai, sa, aiList, type);
|
||||
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
if (!oppList.isEmpty()) {
|
||||
// not enough targets
|
||||
if (sa.canAddMoreTarget()) {
|
||||
final CounterType type = CounterEnumType.M1M1;
|
||||
final CounterType type = CounterType.get(CounterEnumType.M1M1);
|
||||
if (counterType == null || counterType == type) {
|
||||
addTargetsByCounterType(ai, sa, oppList, type);
|
||||
}
|
||||
@@ -173,10 +173,10 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
// targeting does failed
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addTargetsByCounterType(final Player ai, final SpellAbility sa, final CardCollection list,
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Map;
|
||||
public class CountersProliferateAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final List<Card> cperms = Lists.newArrayList();
|
||||
boolean allyExpOrEnergy = false;
|
||||
|
||||
@@ -68,34 +68,25 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
||||
}));
|
||||
}
|
||||
|
||||
if (!cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy) {
|
||||
// AI will play it if there are any counters to proliferate
|
||||
// or if there are no counters, but AI has experience or energy counters
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
boolean chance = true;
|
||||
|
||||
// TODO Make sure Human has poison counters or there are some counters
|
||||
// we want to proliferate
|
||||
return new AiAbilityDecision(
|
||||
chance ? 100 : 0,
|
||||
chance ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi
|
||||
);
|
||||
return chance;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return checkApiLogic(ai, sa);
|
||||
@@ -110,7 +101,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
||||
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// Proliferate is always optional for all, no need to select best
|
||||
|
||||
final CounterType poison = CounterEnumType.POISON;
|
||||
final CounterType poison = CounterType.get(CounterEnumType.POISON);
|
||||
|
||||
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
// because countertype can't be chosen anymore, only look for poison counters
|
||||
|
||||
@@ -53,7 +53,8 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
// disable moving counters (unless a specialized AI logic supports it)
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter remCounter) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
|
||||
final CounterType counterType = remCounter.counter;
|
||||
if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) {
|
||||
return false;
|
||||
@@ -97,7 +98,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isKeyword(Keyword.LEVEL_UP)) {
|
||||
if (sa.hasParam("LevelUp")) {
|
||||
// creatures enchanted by curse auras have low priority
|
||||
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
for (Card aura : source.getEnchantedBy()) {
|
||||
@@ -118,7 +119,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
@@ -159,7 +160,7 @@ public class CountersPutAi extends CountersAi {
|
||||
PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9));
|
||||
if (!poisonList.isEmpty()) {
|
||||
sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife()));
|
||||
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,12 +171,12 @@ public class CountersPutAi extends CountersAi {
|
||||
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
|
||||
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
|
||||
|
||||
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1));
|
||||
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
|
||||
|
||||
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
|
||||
if (best != null) {
|
||||
sa.getTargets().add(best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
CardCollection aiCreat = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
|
||||
@@ -195,7 +196,7 @@ public class CountersPutAi extends CountersAi {
|
||||
best = ComputerUtilCard.getBestAI(aiCreat);
|
||||
if (best != null) {
|
||||
sa.getTargets().add(best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,22 +205,28 @@ public class CountersPutAi extends CountersAi {
|
||||
if (!ai.getCounters().isEmpty()) {
|
||||
if (!eachExisting || ai.getPoisonCounters() < 5) {
|
||||
sa.getTargets().add(ai);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("AlwaysWithNoTgt".equals(logic)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Never".equals(logic)) {
|
||||
return false;
|
||||
} else if ("AlwaysWithNoTgt".equals(logic)) {
|
||||
return true;
|
||||
} else if ("AristocratCounters".equals(logic)) {
|
||||
return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa);
|
||||
} else if ("PayEnergy".equals(logic)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if ("PayEnergyConservatively".equals(logic)) {
|
||||
boolean onlyInCombat = ai.getController().isAI()
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
|
||||
@@ -228,10 +235,10 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
if (playAggro) {
|
||||
// aggro profiles ignore conservative play for this AI logic
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if (ph.inCombat() && source != null) {
|
||||
if (ai.getGame().getCombat().isAttacking(source) && !onlyDefensive) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
} else if (ai.getGame().getCombat().isBlocking(source)) {
|
||||
// when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker
|
||||
CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(source);
|
||||
@@ -241,27 +248,28 @@ public class CountersPutAi extends CountersAi {
|
||||
int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
|
||||
if (source.getNetToughness() + numActivations > totBlkPower
|
||||
|| source.getNetPower() + numActivations >= totBlkToughness) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (sa.getSubAbility() != null
|
||||
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
|
||||
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
|
||||
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ANIMATED_THIS_TURN)) {
|
||||
// Bristling Hydra: save from death using a ping activation
|
||||
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return true;
|
||||
}
|
||||
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
|
||||
// outside of combat, this logic only works if the relevant AI profile option is enabled
|
||||
// and if there is enough energy saved
|
||||
if (!onlyInCombat) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (logic.equals("MarkOppCreature")) {
|
||||
if (!ph.is(PhaseType.END_OF_TURN)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
return false;
|
||||
}
|
||||
|
||||
Predicate<Card> predicate = CardPredicates.hasCounter(CounterType.getType(type));
|
||||
@@ -273,12 +281,12 @@ public class CountersPutAi extends CountersAi {
|
||||
Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreat);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else if (logic.equals("CheckDFC")) {
|
||||
// for cards like Ludevic's Test Subject
|
||||
if (!source.canTransform(null)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if (logic.startsWith("MoveCounter")) {
|
||||
return doMoveCounterLogic(ai, sa, ph);
|
||||
@@ -287,15 +295,8 @@ public class CountersPutAi extends CountersAi {
|
||||
if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
// don't use this for mana until after combat
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
||||
return new AiAbilityDecision(25, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
|
||||
if (willActivate) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return willActivate;
|
||||
} else if (logic.equals("ChargeToBestCMC")) {
|
||||
return doChargeToCMCLogic(ai, sa);
|
||||
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
|
||||
@@ -304,11 +305,15 @@ public class CountersPutAi extends CountersAi {
|
||||
return SpecialCardAi.TheOneRing.consider(ai, sa);
|
||||
}
|
||||
|
||||
if (!sa.metConditions() && sa.getSubAbility() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence
|
||||
CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility());
|
||||
if (!prot.isEmpty()) {
|
||||
sa.getTargets().add(prot.get(0));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,13 +321,13 @@ public class CountersPutAi extends CountersAi {
|
||||
CardCollection creatsYouCtrl = ai.getCreaturesInPlay();
|
||||
List<Card> leastToughness = Aggregates.listWithMin(creatsYouCtrl, Card::getNetToughness);
|
||||
if (leastToughness.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
// TODO If Creature that would be Bolstered for some reason is useless, also return False
|
||||
}
|
||||
|
||||
if (sa.hasParam("Monstrosity") && source.isMonstrous()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO handle proper calculation of X values based on Cost
|
||||
@@ -336,8 +341,8 @@ public class CountersPutAi extends CountersAi {
|
||||
Game game = ai.getGame();
|
||||
Combat combat = game.getCombat();
|
||||
|
||||
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
|
||||
return false;
|
||||
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return doCombatAdaptLogic(source, amount, combat);
|
||||
}
|
||||
@@ -364,12 +369,12 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
// This will "rewind" clockwork cards when they fall to 50% power or below, consider improving
|
||||
if (curCtrs > Math.ceil(maxCtrs / 2.0)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
amount = Math.min(amount, maxCtrs - curCtrs);
|
||||
if (amount <= 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,14 +386,14 @@ public class CountersPutAi extends CountersAi {
|
||||
.mapToInt(Card::getCMC)
|
||||
.max().orElse(0);
|
||||
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't use it if no counters to add
|
||||
if (amount <= 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Polukranos".equals(logic)) {
|
||||
@@ -415,14 +420,20 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
if (!canSurvive) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ("AtOppEOT".equals(logic)) {
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,18 +444,18 @@ public class CountersPutAi extends CountersAi {
|
||||
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
|
||||
// only evaluates case where all tokens are placed on a single target
|
||||
if (sa.getMinTargets() < 2) {
|
||||
AiAbilityDecision decision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
|
||||
Card c = sa.getTargetCard();
|
||||
if (sa.getTargets().size() > 1) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(c);
|
||||
}
|
||||
sa.addDividedAllocation(c, amount);
|
||||
return decision;
|
||||
} else if (!hasSacCost) {
|
||||
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return decision;
|
||||
return true;
|
||||
} else {
|
||||
if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,7 +498,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
|
||||
@@ -496,9 +507,9 @@ public class CountersPutAi extends CountersAi {
|
||||
&& sa.isPwAbility()
|
||||
&& sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
|
||||
&& sa.isTargetNumberValid()
|
||||
&& sa.getTargets().isEmpty()
|
||||
&& sa.getTargets().size() == 0
|
||||
&& ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sourceName.equals("Abzan Charm")) {
|
||||
@@ -520,11 +531,11 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
if (left == 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
sa.resetTargets();
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// target loop
|
||||
@@ -532,7 +543,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -564,9 +575,10 @@ public class CountersPutAi extends CountersAi {
|
||||
// check if other choice will already be played
|
||||
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
|
||||
}
|
||||
if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|
||||
if (!source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|
||||
|| ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) {
|
||||
if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
if (abCost == null || abCost == Cost.Zero
|
||||
|| (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) {
|
||||
// only use at opponent EOT unless it is free
|
||||
choice = chooseBoonTarget(list, type);
|
||||
}
|
||||
@@ -580,7 +592,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -596,79 +608,66 @@ public class CountersPutAi extends CountersAi {
|
||||
choice = null;
|
||||
}
|
||||
if (sa.getTargets().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
// Don't activate Curse abilities on my cards and non-curse abilities
|
||||
// on my opponents
|
||||
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
|
||||
|
||||
// adding counters would cause counter amount to overflow
|
||||
if (Integer.MAX_VALUE - currCounters <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (type.equals("P1P1")) {
|
||||
if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
|
||||
// each non +1/+1 counter on the card is a 10% chance of not
|
||||
// activating this ability.
|
||||
|
||||
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
// Instant +1/+1
|
||||
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
|
||||
if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false; // only if next turn and cost is reusable
|
||||
}
|
||||
}
|
||||
|
||||
// Useless since the card already has the keyword (or for another reason)
|
||||
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean immediately = ComputerUtil.playImmediately(ai, sa);
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (immediately) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!type.equals("P1P1") && !type.equals("M1M1") && !sa.hasParam("ActivationPhases")) {
|
||||
// Don't use non P1P1/M1M1 counters before main 2 if possible
|
||||
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
return false;
|
||||
}
|
||||
if (ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(final SpellAbility sa, Player ai) {
|
||||
boolean chance = true;
|
||||
final Game game = ai.getGame();
|
||||
Card choice = null;
|
||||
final String type = sa.getParam("CounterType");
|
||||
@@ -682,12 +681,14 @@ public class CountersPutAi extends CountersAi {
|
||||
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list;
|
||||
CardCollection list = null;
|
||||
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty() && isMandatoryTrigger) {
|
||||
@@ -700,11 +701,12 @@ public class CountersPutAi extends CountersAi {
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid()
|
||||
|| sa.getTargets().isEmpty()) {
|
||||
|| sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.isCurse()) {
|
||||
@@ -722,9 +724,9 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if ((!sa.isTargetNumberValid()) || (sa.getTargets().isEmpty())) {
|
||||
if ((!sa.isTargetNumberValid()) || (sa.getTargets().size() == 0)) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -739,14 +741,17 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return chance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
// boolean chance = true;
|
||||
boolean preferred = true;
|
||||
CardCollection list;
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||
@@ -765,14 +770,9 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if ("ChargeToBestCMC".equals(aiLogic)) {
|
||||
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return doChargeToCMCLogic(ai, sa) || mandatory;
|
||||
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
|
||||
return doChargeToOppCtrlCMCLogic(ai, sa) || mandatory;
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -801,7 +801,7 @@ public class CountersPutAi extends CountersAi {
|
||||
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
|
||||
|
||||
if (playerList.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// try to choose player with less creatures
|
||||
@@ -817,30 +817,29 @@ public class CountersPutAi extends CountersAi {
|
||||
if (type.equals("P1P1")) {
|
||||
nPump = amount;
|
||||
}
|
||||
AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
if (FightAi.canFightAi(ai, sa, nPump, nPump)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
Iterable<Card> filteredField;
|
||||
if (sa.isCurse()) {
|
||||
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
filteredField = ai.getCardsIn(ZoneType.Battlefield);
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
CardCollection list = CardLists.getTargetableCards(filteredField, sa);
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
int totalTargets = list.size();
|
||||
boolean preferred = true;
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
int totalTargets = list.size();
|
||||
|
||||
sa.resetTargets();
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (mandatory) {
|
||||
// When things are mandatory, gotta handle a little differently
|
||||
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (list.isEmpty() && preferred) {
|
||||
@@ -860,7 +859,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (list.isEmpty()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
Card choice = null;
|
||||
@@ -872,21 +871,27 @@ public class CountersPutAi extends CountersAi {
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
} else if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else {
|
||||
if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (choice != null && divided) {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
@@ -907,7 +912,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -962,8 +967,8 @@ public class CountersPutAi extends CountersAi {
|
||||
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// Bolster does use this
|
||||
// TODO need more or less logic there?
|
||||
final CounterType m1m1 = CounterEnumType.M1M1;
|
||||
final CounterType p1p1 = CounterEnumType.P1P1;
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
|
||||
// no logic if there is no options or no to choice
|
||||
if (!isOptional && Iterables.size(options) <= 1) {
|
||||
@@ -1081,10 +1086,11 @@ public class CountersPutAi extends CountersAi {
|
||||
Player ai = sa.getActivatingPlayer();
|
||||
GameEntity e = (GameEntity) params.get("Target");
|
||||
// for Card try to select not useless counter
|
||||
if (e instanceof Card c) {
|
||||
if (e instanceof Card) {
|
||||
Card c = (Card) e;
|
||||
if (c.getController().isOpponentOf(ai)) {
|
||||
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.M1M1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, c)) {
|
||||
@@ -1098,14 +1104,15 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e instanceof Player p) {
|
||||
} else if (e instanceof Player) {
|
||||
Player p = (Player) e;
|
||||
if (p.isOpponentOf(ai)) {
|
||||
if (options.contains(CounterEnumType.POISON)) {
|
||||
return CounterEnumType.POISON;
|
||||
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
|
||||
return CounterType.get(CounterEnumType.POISON);
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterEnumType.EXPERIENCE)) {
|
||||
return CounterEnumType.EXPERIENCE;
|
||||
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
|
||||
return CounterType.get(CounterEnumType.EXPERIENCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,7 +1120,7 @@ public class CountersPutAi extends CountersAi {
|
||||
return Iterables.getFirst(options, null);
|
||||
}
|
||||
|
||||
private AiAbilityDecision doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
private boolean doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
// Spikes (Tempest)
|
||||
|
||||
// Try not to do it unless at the end of opponent's turn or the creature is threatened
|
||||
@@ -1126,7 +1133,7 @@ public class CountersPutAi extends CountersAi {
|
||||
|| (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))));
|
||||
|
||||
if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
|
||||
@@ -1144,45 +1151,45 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
if (bestTgt != null) {
|
||||
sa.getTargets().add(bestTgt);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
private AiAbilityDecision doCombatAdaptLogic(Card source, int amount, Combat combat) {
|
||||
private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) {
|
||||
if (combat.isAttacking(source)) {
|
||||
if (!combat.isBlocked(source)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else {
|
||||
for (Card blockedBy : combat.getBlockers(source)) {
|
||||
if (blockedBy.getNetToughness() > source.getNetPower()
|
||||
&& blockedBy.getNetToughness() <= source.getNetPower() + amount) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
|
||||
if (source.getNetToughness() <= totBlkPower
|
||||
&& source.getNetToughness() + amount > totBlkPower) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (combat.isBlocking(source)) {
|
||||
for (Card blocked : combat.getAttackersBlockedBy(source)) {
|
||||
if (blocked.getNetToughness() > source.getNetPower()
|
||||
&& blocked.getNetToughness() <= source.getNetPower() + amount) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower);
|
||||
if (source.getNetToughness() <= totAtkPower
|
||||
&& source.getNetToughness() + amount > totAtkPower) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1193,7 +1200,7 @@ public class CountersPutAi extends CountersAi {
|
||||
return max;
|
||||
}
|
||||
|
||||
private AiAbilityDecision doChargeToCMCLogic(Player ai, SpellAbility sa) {
|
||||
private boolean doChargeToCMCLogic(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
|
||||
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
|
||||
@@ -1208,14 +1215,10 @@ public class CountersPutAi extends CountersAi {
|
||||
optimalCMC = cmc;
|
||||
}
|
||||
}
|
||||
if (numCtrs < optimalCMC) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return numCtrs < optimalCMC;
|
||||
}
|
||||
|
||||
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
|
||||
private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
|
||||
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
|
||||
@@ -1229,11 +1232,6 @@ public class CountersPutAi extends CountersAi {
|
||||
optimalCMC = cmc;
|
||||
}
|
||||
}
|
||||
if (numCtrs < optimalCMC) {
|
||||
// If the AI has less counters than the optimal CMC, it should play the ability.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return numCtrs < optimalCMC;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -16,15 +15,17 @@ import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CountersPutAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
List<Card> hList;
|
||||
List<Card> cList;
|
||||
@@ -43,9 +44,28 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
|
||||
}
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (logic.equals("AtEOTOrBlock")) {
|
||||
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
} else if (logic.equals("AtOppEOT")) {
|
||||
if (!(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,23 +88,26 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
if (curse) {
|
||||
if (type.equals("M1M1")) {
|
||||
final List<Card> killable = CardLists.filter(hList, c -> c.getNetToughness() <= amount);
|
||||
if (killable.size() <= 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
if (!(killable.size() > 2)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// make sure compy doesn't harm his stuff more than human's
|
||||
// stuff
|
||||
if (cList.size() > hList.size()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// human has more things that will benefit, don't play
|
||||
if (hList.size() >= cList.size()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Check for cards that could profit from the ability
|
||||
@@ -102,21 +125,21 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (!combatants) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playReusable(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return chance;
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
return ((MyRandom.getRandom().nextFloat() < .6667) && chance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
}
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
|
||||
@@ -127,7 +150,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
List<Player> players = Lists.newArrayList();
|
||||
if (!sa.isCurse()) {
|
||||
@@ -145,23 +168,11 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
if (preferred) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return preferred || mandatory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
return mandatory || canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
@@ -50,13 +52,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
if (doTgt(ai, sa, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return doTgt(ai, sa, false);
|
||||
}
|
||||
return super.checkApiLogic(ai, sa);
|
||||
}
|
||||
@@ -182,27 +180,11 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
if (doTgt(ai, sa, mandatory)) {
|
||||
// if we can target, then we can play it
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
// if we can't target, then we can't play it
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
if (mandatory) {
|
||||
// if mandatory, just play it
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// if not mandatory, check if we can play it
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return doTgt(ai, sa, mandatory);
|
||||
}
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -218,18 +200,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
Card tgt = (Card) params.get("Target");
|
||||
|
||||
// planeswalker has high priority for loyalty counters
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) {
|
||||
return CounterEnumType.LOYALTY;
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
}
|
||||
|
||||
if (tgt.getController().isOpponentOf(ai)) {
|
||||
// creatures with BaseToughness below or equal zero might be
|
||||
// killed if their counters are removed
|
||||
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
|
||||
if (options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,17 +223,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
// this counters are treat first to be removed
|
||||
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) {
|
||||
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
|
||||
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
|
||||
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
|
||||
|
||||
if (maritEmpty) {
|
||||
return CounterEnumType.ICE;
|
||||
return CounterType.get(CounterEnumType.ICE);
|
||||
}
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
|
||||
// fallback logic, select positive counter to add more
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
@@ -25,6 +23,14 @@ import java.util.function.Predicate;
|
||||
|
||||
public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
return true;
|
||||
}
|
||||
return super.canPlayWithoutRestrict(ai, sa);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
@@ -42,6 +48,24 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player,
|
||||
* forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler,
|
||||
* java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
|
||||
if ("EndOfOpponentsTurn".equals(logic)) {
|
||||
if (!ph.is(PhaseType.END_OF_TURN) || !ph.getNextTurn().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.checkPhaseRestrictions(ai, sa, ph, logic);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
@@ -49,7 +73,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final String type = sa.getParam("CounterType");
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -59,14 +83,14 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (!type.matches("Any") && !type.matches("All")) {
|
||||
final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type));
|
||||
if (currCounters < 1) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
private AiAbilityDecision doTgt(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
|
||||
@@ -79,7 +103,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(tgt.getZone()), sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
@@ -97,7 +121,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardPredicates.hasCounter(CounterEnumType.ICE, 3));
|
||||
if (!depthsList.isEmpty()) {
|
||||
sa.getTargets().add(depthsList.getFirst());
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +134,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!planeswalkerList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else if (type.matches("Any")) {
|
||||
// variable amount for Hex Parasite
|
||||
@@ -120,7 +144,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
if (manaLeft == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
amount = manaLeft;
|
||||
xPay = true;
|
||||
@@ -142,7 +166,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (xPay) {
|
||||
sa.setXManaCostPaid(ice);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +185,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (xPay) {
|
||||
sa.setXManaCostPaid(best.getCurrentLoyalty());
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// some rules only for amount = 1
|
||||
@@ -178,7 +202,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!aiM1M1List.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// do as P1P1 part
|
||||
@@ -187,7 +211,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!aiUndyingList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO stun counters with canRemoveCounters check
|
||||
@@ -198,7 +222,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardPredicates.hasCounter(CounterEnumType.P1P1));
|
||||
if (!oppP1P1List.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// fallback to remove any counter from opponent
|
||||
@@ -210,7 +234,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
for (final CounterType aType : best.getCounters().keySet()) {
|
||||
if (!ComputerUtil.isNegativeCounter(aType, best)) {
|
||||
sa.getTargets().add(best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +255,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!aiList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else if (type.equals("P1P1")) {
|
||||
// no special amount for that one yet
|
||||
@@ -249,7 +273,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
if (!aiList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +287,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!oppList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (type.equals("TIME")) {
|
||||
@@ -274,7 +298,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
if (manaLeft == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
amount = manaLeft;
|
||||
xPay = true;
|
||||
@@ -292,7 +316,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (xPay) {
|
||||
sa.setXManaCostPaid(timeCount);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (mandatory) {
|
||||
@@ -301,7 +325,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
|
||||
if (!adaptCreats.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Outlast nice target
|
||||
@@ -312,27 +336,26 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!betterTargets.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
return doTgt(aiPlayer, sa, mandatory);
|
||||
}
|
||||
return mandatory ? new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -346,7 +369,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
GameEntity target = (GameEntity) params.get("Target");
|
||||
CounterType type = (CounterType) params.get("CounterType");
|
||||
|
||||
if (target instanceof Card targetCard) {
|
||||
if (target instanceof Card) {
|
||||
Card targetCard = (Card) target;
|
||||
if (targetCard.getController().isOpponentOf(player)) {
|
||||
return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
|
||||
} else {
|
||||
@@ -357,7 +381,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
|
||||
}
|
||||
} else if (target instanceof Player targetPlayer) {
|
||||
} else if (target instanceof Player) {
|
||||
Player targetPlayer = (Player) target;
|
||||
if (targetPlayer.isOpponentOf(player)) {
|
||||
return !type.is(CounterEnumType.POISON) ? max : min;
|
||||
} else {
|
||||
@@ -384,7 +409,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (targetCard.getController().isOpponentOf(ai)) {
|
||||
// if its a Planeswalker try to remove Loyality first
|
||||
if (targetCard.isPlaneswalker()) {
|
||||
return CounterEnumType.LOYALTY;
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
@@ -392,10 +417,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterEnumType.M1M1;
|
||||
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.P1P1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
|
||||
@@ -5,24 +5,38 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class DamageAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (MyRandom.getRandom().nextFloat() > Math.pow(.9, sa.getActivationsThisTurn())) {
|
||||
return false;
|
||||
}
|
||||
// abCost stuff that should probably be centralized...
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for some costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// wait until stack is empty (prevents duplicate kills)
|
||||
if (!ai.getGame().getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
|
||||
return false;
|
||||
}
|
||||
|
||||
int x = -1;
|
||||
@@ -37,15 +51,11 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (x == -1) {
|
||||
if (determineOppToKill(ai, sa, source, dmg) != null) {
|
||||
// we already know we can kill a player, so go for it
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
// look for other value in this (damaging creatures or
|
||||
// creatures + player, e.g. Pestilence, etc.)
|
||||
if (evaluateDamageAll(ai, sa, source, dmg) > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return evaluateDamageAll(ai, sa, source, dmg) > 0;
|
||||
} else {
|
||||
int best = -1, best_x = -1;
|
||||
Player bestOpp = determineOppToKill(ai, sa, source, x);
|
||||
@@ -71,9 +81,9 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
sa.setXManaCostPaid(best_x);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +143,9 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
|
||||
// When using Pestilence to hurt players, do it at
|
||||
// the end of the opponent's turn only
|
||||
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|
||||
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
|
||||
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|
||||
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
|
||||
// Need further improvement : if able to kill immediately with repeated activations, do not wait
|
||||
// for phases! Will also need to implement considering repeated activations for killed creatures!
|
||||
// || (ai.sa.getPayCosts(). ??? )
|
||||
@@ -175,7 +185,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String validP = sa.getParamOrDefault("ValidPlayers", "");
|
||||
|
||||
@@ -201,21 +211,21 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
// Don't get yourself killed
|
||||
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// if we can kill human, do it
|
||||
if ((validP.equals("Player") || validP.equals("Opponent") || validP.contains("Targeted"))
|
||||
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) > ComputerUtilCard
|
||||
.evaluateCreatureList(humanList)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,7 +258,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String validP = sa.getParamOrDefault("ValidPlayers", "");
|
||||
|
||||
@@ -277,24 +287,24 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
|
||||
// If it's not mandatory check a few things
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
// Don't get yourself killed
|
||||
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// if we can kill human, do it
|
||||
if ((validP.equals("Player") || validP.contains("Opponent") || validP.contains("Targeted"))
|
||||
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) + 50 >= ComputerUtilCard
|
||||
.evaluateCreatureList(humanList)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import java.util.Map;
|
||||
|
||||
public class DamageDealAi extends DamageAiBase {
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
Card source = sa.getHostCard();
|
||||
@@ -65,19 +65,15 @@ public class DamageDealAi extends DamageAiBase {
|
||||
continue; // in case the calculation gets messed up somewhere
|
||||
}
|
||||
root.setSVar("EnergyToPay", "Number$" + dmg);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
// Life Drain
|
||||
if ("XLifeDrain".equals(logic)) {
|
||||
if (doXLifeDrainLogic(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return doXLifeDrainLogic(ai, sa);
|
||||
}
|
||||
|
||||
// Set PayX here to maximum value.
|
||||
@@ -87,15 +83,11 @@ public class DamageDealAi extends DamageAiBase {
|
||||
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
|
||||
}
|
||||
}
|
||||
if (damageTargetAI(ai, sa, dmg, true)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return damageTargetAI(ai, sa, dmg, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
@@ -116,7 +108,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0);
|
||||
boolean isLethal = sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
|
||||
if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,10 +134,10 @@ public class DamageDealAi extends DamageAiBase {
|
||||
if (shouldTgtP(ai, sa, maxDmg, false)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(maxDamaged);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +154,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
for (Card potentialAtkr : ai.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,24 +175,16 @@ public class DamageDealAi extends DamageAiBase {
|
||||
* Mostly used to ping the player with remaining counters. The issue with
|
||||
* stacked effects might appear here.
|
||||
*/
|
||||
if (damageTargetAI(ai, sa, n, true)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return damageTargetAI(ai, sa, n, true);
|
||||
} else {
|
||||
/*
|
||||
* Only ping when stack is clear to avoid hassle of evaluating stacked effects
|
||||
* like protection/pumps or over-killing target.
|
||||
*/
|
||||
if (ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
|
||||
}
|
||||
return ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false);
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if ("NinThePainArtist".equals(logic)) {
|
||||
// Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard
|
||||
@@ -209,15 +193,11 @@ public class DamageDealAi extends DamageAiBase {
|
||||
if (doTarget) {
|
||||
Card tgt = sa.getTargetCard();
|
||||
if (tgt != null) {
|
||||
if (ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController();
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceName.equals("Sorin, Grim Nemesis")) {
|
||||
@@ -229,35 +209,35 @@ public class DamageDealAi extends DamageAiBase {
|
||||
continue; // in case the calculation gets messed up somewhere
|
||||
}
|
||||
sa.setXManaCostPaid(dmg);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dmg <= 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// temporarily disabled until better AI
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to chain damage/debuff effects
|
||||
@@ -268,13 +248,13 @@ public class DamageDealAi extends DamageAiBase {
|
||||
int extraDmg = chainDmg.getValue();
|
||||
boolean willTargetIfChained = damageTargetAI(ai, sa, dmg + extraDmg, false);
|
||||
if (!willTargetIfChained) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); // won't play it even in chain
|
||||
return false; // won't play it even in chain
|
||||
} else if (willTargetIfChained && chainDmg.getKey().getApi() == ApiType.Pump && sa.getTargets().isTargetingAnyPlayer()) {
|
||||
// we're trying to chain a pump spell to a damage spell targeting a player, that won't work
|
||||
// so run an additional check to ensure that we want to cast the current spell separately
|
||||
sa.resetTargets();
|
||||
if (!damageTargetAI(ai, sa, dmg, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// we are about to decide to play this damage spell; if there's something chained to it, reserve mana for
|
||||
@@ -284,7 +264,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
} else if (!damageTargetAI(ai, sa, dmg, false)) {
|
||||
// simple targeting when there is no spell chaining plan
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) ||
|
||||
@@ -308,12 +288,10 @@ public class DamageDealAi extends DamageAiBase {
|
||||
|
||||
if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
|
||||
final int cmc = sa.getXManaCostPaid();
|
||||
if (!ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc));
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -954,14 +932,14 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
int dmg = calculateDamageAmount(sa, source, damage);
|
||||
|
||||
// Remove all damage
|
||||
if (sa.hasParam("Remove")) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
@@ -972,18 +950,10 @@ public class DamageDealAi extends DamageAiBase {
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
// If it's not mandatory check a few things
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (damageChooseNontargeted(ai, sa, dmg)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return mandatory || damageChooseNontargeted(ai, sa, dmg);
|
||||
} else {
|
||||
if (!damageChoosingTargets(ai, sa, sa.getTargetRestrictions(), dmg, mandatory, true) && !mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) {
|
||||
@@ -1006,7 +976,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int calculateDamageAmount(SpellAbility sa, Card source, String damage) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.player.Player;
|
||||
@@ -16,7 +14,7 @@ public class DamageEachAi extends DamageAiBase {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
@@ -24,41 +22,30 @@ public class DamageEachAi extends DamageAiBase {
|
||||
|
||||
if (sa.usesTargeting() && weakestOpp != null) {
|
||||
if ("MadSarkhanUltimate".equals(logic) && !SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.resetTargets();
|
||||
if (weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife()) {
|
||||
sa.getTargets().add(weakestOpp);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
sa.getTargets().add(weakestOpp);
|
||||
return weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife();
|
||||
}
|
||||
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
|
||||
|
||||
if (shouldTgtP(ai, sa, iDmg, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return shouldTgtP(ai, sa, iDmg, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// check AI life before playing this drawback?
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(ai, sa);
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -20,12 +24,18 @@ import java.util.List;
|
||||
public class DamagePreventAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Card hostCard = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
boolean chance = false;
|
||||
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
if (!willPayCosts(ai, sa, cost, hostCard)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt == null) {
|
||||
// As far as I can tell these Defined Cards will only have one of them
|
||||
@@ -60,7 +70,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
chance = flag;
|
||||
} else { // if nothing on the stack, and it's not declare
|
||||
// blockers. no need to prevent
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // non-targeted
|
||||
@@ -110,7 +120,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
targetables = CardLists.getTargetableCards(targetables, sa);
|
||||
|
||||
if (targetables.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
final CardCollection combatants = CardLists.filter(targetables, CardPredicates.CREATURES);
|
||||
ComputerUtilCard.sortByEvaluateCreature(combatants);
|
||||
@@ -127,15 +137,11 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(hostCard, sa.getParam("Amount"), sa));
|
||||
}
|
||||
|
||||
if (chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return chance;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
boolean chance = false;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt == null) {
|
||||
@@ -145,11 +151,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
chance = preventDamageMandatoryTarget(ai, sa, mandatory);
|
||||
}
|
||||
|
||||
if (chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
|
||||
}
|
||||
return chance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -13,34 +11,24 @@ import java.util.Map;
|
||||
|
||||
public class DayTimeAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
|
||||
if ((sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) || sa.getPayCosts().hasManaCost()) {
|
||||
// If it involves a cost that may put us at a disadvantage, better activate before own turn if possible
|
||||
if (!isSorcerySpeed(sa, aiPlayer)) {
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;
|
||||
} else {
|
||||
if (ph.is(PhaseType.MAIN2, aiPlayer)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return ph.is(PhaseType.MAIN2, aiPlayer); // Give other things a chance to be cast (e.g. Celestus)
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return true; // TODO: more logic if it's ever a bad idea to trigger this (when non-mandatory)
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.*;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -23,27 +26,27 @@ import java.util.List;
|
||||
public class DebuffAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// if there is no target and host card isn't in play, don't activate
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
if (!sa.usesTargeting() && !source.isInPlay()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
// temporarily disabled until AI is improved
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
return false;
|
||||
}
|
||||
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
@@ -55,7 +58,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
// Instant-speed pumps should not be cast outside of combat when the
|
||||
// stack is empty, unless there are specific activation phase requirements
|
||||
if (!isSorcerySpeed(sa, ai) && !sa.hasParam("ActivationPhases")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +66,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
|
||||
final Combat combat = game.getCombat();
|
||||
if (cards.stream().anyMatch(c -> {
|
||||
return cards.stream().anyMatch(c -> {
|
||||
if (c.getController().equals(sa.getActivatingPlayer()) || combat == null)
|
||||
return false;
|
||||
|
||||
@@ -72,34 +75,21 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
}
|
||||
// don't add duplicate negative keywords
|
||||
return sa.hasParam("Keywords") && c.hasAnyKeyword(Arrays.asList(sa.getParam("Keywords").split(" & ")));
|
||||
})) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
if (!sa.usesTargeting()) {
|
||||
// TODO - copied from AF_Pump.pumpDrawbackAI() - what should be here?
|
||||
} else {
|
||||
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
} // debuffDrawbackAI()
|
||||
|
||||
/**
|
||||
@@ -244,24 +234,18 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final List<String> kws = sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : new ArrayList<>();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (debuffTgtAI(ai, sa, kws, mandatory)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return debuffTgtAI(ai, sa, kws, mandatory);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,56 +15,43 @@ import forge.game.zone.ZoneType;
|
||||
public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
// TODO: improve ai
|
||||
return true;
|
||||
}
|
||||
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
|
||||
if (trigsa == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (trigsa instanceof AbilitySub) {
|
||||
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
|
||||
} else {
|
||||
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
|
||||
if (trigsa == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (!sa.hasParam("OptionalDecider")) {
|
||||
if (aic.doTrigger(trigsa, true)) {
|
||||
// If the trigger is mandatory, we can play it
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return aic.doTrigger(trigsa, true);
|
||||
} else {
|
||||
if (aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// Card-specific logic
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
if (logic.equals("SpellCopy")) {
|
||||
@@ -103,9 +90,9 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (count == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if (logic.equals("NarsetRebound")) {
|
||||
// should be done in Main2, but it might broke for other cards
|
||||
//if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
@@ -138,10 +125,10 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (count == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if (logic.equals("SaveCreature")) {
|
||||
CardCollection ownCreatures = ai.getCreaturesInPlay();
|
||||
|
||||
@@ -155,25 +142,19 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
if (!ownCreatures.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(ownCreatures));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generic logic
|
||||
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
|
||||
if (trigsa == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import forge.util.collect.FCollectionView;
|
||||
|
||||
public class DestroyAi extends SpellAbilityAi {
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@@ -103,27 +103,36 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final boolean noRegen = sa.hasParam("NoRegen");
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
CardCollection list;
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Targeting
|
||||
if (sa.usesTargeting()) {
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
// Assume there where already enough targets chosen by AI Logic Above
|
||||
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// reset targets before AI Logic part
|
||||
sa.resetTargets();
|
||||
int maxTargets;
|
||||
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if (sa.getRootAbility().costHasManaX() ||
|
||||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
|
||||
if (sa.getRootAbility().costHasManaX()) {
|
||||
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
||||
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
// need to set XPaid to get the right number for
|
||||
@@ -136,22 +145,23 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|
||||
if (maxTargets == 0) {
|
||||
// can't afford X or otherwise target anything
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("TargetingPlayer")) {
|
||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
|
||||
sa.setTargetingPlayer(targetingPlayer);
|
||||
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return targetingPlayer.getController().chooseTargetsFor(sa);
|
||||
}
|
||||
|
||||
// AI doesn't destroy own cards if it isn't defined in AI logic
|
||||
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
if ("FatalPush".equals(logic)) {
|
||||
final int cmcMax = ai.hasRevolt() ? 4 : 2;
|
||||
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
|
||||
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
|
||||
@@ -196,7 +206,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
// Try to avoid targeting creatures that are dead on board
|
||||
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// target loop
|
||||
@@ -211,7 +221,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -225,7 +235,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if ("OppDestroyYours".equals(logic)) {
|
||||
Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay());
|
||||
if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (CardLists.getNotType(list, "Land").isEmpty()) {
|
||||
@@ -234,7 +244,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) {
|
||||
// Strip Mine, Wasteland - cut short if the relevant logic fails
|
||||
if (!doLandForLandRemovalLogic(sa, ai, choice, logic)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -244,14 +254,14 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
//option to hold removal instead only applies for single targeted removal
|
||||
if (!sa.isTrigger() && sa.getMaxTargets() == 1) {
|
||||
if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -288,22 +298,22 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|
||||
|| ai.getLife() <= 5)) {
|
||||
// Basic ai logic for Lethal Vapors
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if ("Always".equals(logic)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (list.isEmpty()
|
||||
|| !CardLists.filterControlledBy(list, ai).isEmpty()
|
||||
|| CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final boolean noRegen = sa.hasParam("NoRegen");
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -311,7 +321,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
if (list.isEmpty() || list.size() < sa.getMinTargets()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to avoid targeting creatures that are dead on board
|
||||
@@ -339,7 +349,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
list.removeAll(preferred);
|
||||
|
||||
if (preferred.isEmpty() && !mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
@@ -347,12 +357,12 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if (!sa.isMinTargetChosen()) {
|
||||
if (!mandatory) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Card c = ComputerUtilCard.getBestAI(preferred);
|
||||
@@ -387,18 +397,9 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return sa.isTargetNumberValid();
|
||||
} else {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return mandatory;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,23 +23,38 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return doMassRemovalLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return doMassRemovalLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for some costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if ("FellTheMighty".equals(aiLogic)) {
|
||||
@@ -49,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
return doMassRemovalLogic(ai, sa);
|
||||
}
|
||||
|
||||
public static AiAbilityDecision doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
@@ -57,7 +72,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1);
|
||||
|
||||
if (logic.equals("Always")) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
|
||||
}
|
||||
|
||||
String valid = sa.getParamOrDefault("ValidCards", "");
|
||||
@@ -77,7 +92,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
opplist = CardLists.filter(opplist, predicate);
|
||||
ailist = CardLists.filter(ailist, predicate);
|
||||
if (opplist.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -86,7 +101,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(opponent);
|
||||
ailist.clear();
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,35 +110,30 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, ailist.size());
|
||||
int numOppsCanSave = Math.min(CardLists.count(ai.getOpponents().getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, opplist.size());
|
||||
|
||||
if (numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (numAiCanSave < ailist.size() && (opplist.size() - numOppsCanSave < ailist.size() - numAiCanSave)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
|
||||
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// if only creatures are affected evaluate both lists and pass only if human creatures are more valuable
|
||||
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
return false;
|
||||
}
|
||||
|
||||
// test whether the human can kill the ai next turn
|
||||
@@ -136,42 +146,39 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (!containsAttacker) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
AiBlockController block = new AiBlockController(ai, false);
|
||||
block.assignBlockersForCombat(combat);
|
||||
|
||||
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} // only lands involved
|
||||
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
|
||||
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) {
|
||||
// TODO Should care about any land recursion, not just Crucible of Worlds
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
CardCollection oppCreatures = opponent.getCreaturesInPlay();
|
||||
if (!oppCreatures.isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// check if the AI would lose more lands than the opponent would
|
||||
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,21 +20,26 @@ import forge.util.TextUtil;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class DigAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
Player libraryOwner = ai;
|
||||
|
||||
if (!willPayCosts(ai, sa, sa.getPayCosts(), host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!sa.canTarget(opp)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
@@ -42,21 +47,29 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
// return false if nothing to dig into
|
||||
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||
return false;
|
||||
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
|
||||
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// don't deck yourself
|
||||
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
|
||||
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
|
||||
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
final String num = sa.getParam("DigNum");
|
||||
@@ -74,14 +87,14 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()) - manaToSave;
|
||||
if (numCards <= 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
root.setXManaCostPaid(numCards);
|
||||
}
|
||||
}
|
||||
|
||||
if (playReusable(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
|
||||
@@ -89,24 +102,24 @@ public class DigAi extends SpellAbilityAi {
|
||||
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
|
||||
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
|
||||
&& !ComputerUtil.activateForCost(sa, ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("MadSarkhanDigDmg".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
return !ComputerUtil.preventRunAwayActivations(sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// TODO: improve this check in ways that may be specific to a subability
|
||||
return canPlay(aiPlayer, sa);
|
||||
return canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
|
||||
@@ -124,16 +137,12 @@ public class DigAi extends SpellAbilityAi {
|
||||
int manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]);
|
||||
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, true) - manaToSave;
|
||||
if (numCards <= 0) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return mandatory;
|
||||
}
|
||||
root.setXManaCostPaid(numCards);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -194,12 +203,15 @@ public class DigAi extends SpellAbilityAi {
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
Card topc = player.getZone(ZoneType.Library).get(0);
|
||||
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Explorer's Scope")) {
|
||||
// for Explorer's Scope, always put a land on the battlefield tapped
|
||||
// (TODO: might not always be a good idea, e.g. when a land ETBing can have detrimental effects)
|
||||
return true;
|
||||
} else if ("AlwaysConfirm".equals(sa.getParam("AILogic"))) {
|
||||
return true;
|
||||
// AI actions for individual cards (until this AI can be generalized)
|
||||
if (sa.getHostCard() != null) {
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Explorer's Scope")) {
|
||||
// for Explorer's Scope, always put a land on the battlefield tapped
|
||||
// (TODO: might not always be a good idea, e.g. when a land ETBing can have detrimental effects)
|
||||
return true;
|
||||
} else if ("AlwaysConfirm".equals(sa.getParam("AILogic"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// looks like perfect code for Delver of Secrets, but what about other cards?
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -12,12 +14,13 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class DigMultipleAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
@@ -26,7 +29,7 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
@@ -34,29 +37,33 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
|
||||
// return false if nothing to dig into
|
||||
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
|
||||
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// don't deck yourself
|
||||
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
|
||||
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
|
||||
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (playReusable(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
|
||||
@@ -64,14 +71,14 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
|
||||
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
|
||||
&& !ComputerUtil.activateForCost(sa, ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return !ComputerUtil.preventRunAwayActivations(sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -82,7 +89,7 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
@@ -9,6 +11,7 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -16,7 +19,7 @@ import java.util.Map;
|
||||
public class DigUntilAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
double chance = .4; // 40 percent chance with instant speed stuff
|
||||
@@ -39,7 +42,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
// material in the library after using it several times.
|
||||
// TODO: maybe this should happen for any DigUntil SA with RevealedDestination$ Graveyard?
|
||||
if (ai.getCardsIn(ZoneType.Library).size() < 20) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if ("Land.Basic".equals(sa.getParam("Valid"))
|
||||
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.LANDS_PRODUCING_MANA)) {
|
||||
@@ -49,7 +52,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
// This is important for Replenish/Living Death type decks
|
||||
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +60,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!sa.canTarget(opp)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
@@ -65,7 +68,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("Valid")) {
|
||||
final String valid = sa.getParam("Valid");
|
||||
if (CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), valid, source.getController(), source, sa).isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +80,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
if (root.getXManaCostPaid() == null) {
|
||||
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
if (numCards <= 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
root.setXManaCostPaid(numCards);
|
||||
}
|
||||
@@ -85,14 +88,15 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
|
||||
// return false if nothing to dig into
|
||||
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (sa.isCurse()) {
|
||||
@@ -112,7 +116,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -26,29 +26,31 @@ import forge.util.collect.FCollectionView;
|
||||
public class DiscardAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
// temporarily disabled until better AI
|
||||
if (!willPayCosts(ai, sa, abCost, source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Chandra, Flamecaller".equals(sourceName)) {
|
||||
final int hand = ai.getCardsIn(ZoneType.Hand).size();
|
||||
if (MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
|
||||
}
|
||||
|
||||
if (aiLogic.equals("VolrathsShapeshifter")) {
|
||||
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
|
||||
}
|
||||
|
||||
final boolean humanHasHand = !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
|
||||
final boolean humanHasHand = ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).size() > 0;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (!discardTargetAI(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// TODO: Add appropriate restrictions
|
||||
@@ -62,7 +64,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
} else {
|
||||
// defined to the human, so that's fine as long the human has cards
|
||||
if (!humanHasHand) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -76,12 +78,12 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()), ai.getWeakestOpponent()
|
||||
.getCardsIn(ZoneType.Hand).size());
|
||||
if (cardsToDiscard < 1) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.setXManaCostPaid(cardsToDiscard);
|
||||
} else {
|
||||
if (AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa) < 1) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +113,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (numDiscard == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,25 +121,27 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
// Don't use discard abilities before main 2 if possible
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
||||
&& !sa.hasParam("ActivationPhases") && !aiLogic.startsWith("AnyPhase")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aiLogic.equals("AnyPhaseIfFavored")) {
|
||||
if (ai.getGame().getCombat() != null) {
|
||||
if (ai.getCardsIn(ZoneType.Hand).size() < ai.getGame().getCombat().getDefenderPlayerByAttacker(source).getCardsIn(ZoneType.Hand).size()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't tap creatures that may be able to block
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(0.9, sa.getActivationsThisTurn());
|
||||
|
||||
// some other variables here, like handsize vs. maxHandSize
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
||||
@@ -162,7 +166,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
|
||||
@@ -172,7 +176,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
} else if (mandatory && sa.canTarget(ai)) {
|
||||
sa.getTargets().add(ai);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -180,7 +184,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
if ("AtLeast2".equals(sa.getParam("AILogic"))) {
|
||||
final List<Player> players = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
if (players.isEmpty() || players.get(0).getCardsIn(ZoneType.Hand).size() < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,22 +196,18 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
// Drawback AI improvements
|
||||
// if parent draws cards, make sure cards in hand + cards drawn > 0
|
||||
if (sa.usesTargeting()) {
|
||||
if (discardTargetAI(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return discardTargetAI(ai, sa);
|
||||
}
|
||||
// TODO: check for some extra things
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
@@ -13,8 +16,12 @@ import java.util.Map;
|
||||
public class DiscoverAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false; // prevent infinite loop
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,12 +36,8 @@ public class DiscoverAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return checkApiLogic(ai, sa);
|
||||
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
return mandatory || checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DrainManaAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
// assume we are looking to tap human's stuff
|
||||
@@ -25,58 +25,56 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (!defined.contains(opp)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (defined.contains(opp)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return defined.contains(opp);
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
boolean randomReturn = true;
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (defined.contains(ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getWeakestOpponent());
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return randomReturn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -41,41 +42,43 @@ public class DrawAi extends SpellAbilityAi {
|
||||
* @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (!targetAI(ai, sa, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
final Player player = sa.getTargets().getFirstTargetedPlayer();
|
||||
if (player != null && player.isOpponentOf(ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't tap creatures that may be able to block
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canLoot(ai, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) {
|
||||
// Canopy lands and other cards that sacrifice themselves to draw cards
|
||||
if (ai.getCardsIn(ZoneType.Hand).isEmpty()
|
||||
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5)) {
|
||||
// TODO: make this configurable in the AI profile
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return ai.getCardsIn(ZoneType.Hand).isEmpty()
|
||||
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5); // TODO: make this configurable in the AI profile
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -158,6 +161,8 @@ public class DrawAi extends SpellAbilityAi {
|
||||
// LifeLessThan logic presupposes activation as soon as possible in an
|
||||
// attempt to save the AI from dying
|
||||
return true;
|
||||
} else if (logic.equals("AtOppEOT")) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
} else if (logic.equals("RespondToOwnActivation")) {
|
||||
return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
|
||||
} else if ((!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
||||
@@ -170,12 +175,8 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
if (targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,7 +370,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
|
||||
// try to make opponent lose to poison
|
||||
// currently only Caress of Phyrexia
|
||||
if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (oppA.getPoisonCounters() + numCards > 9) {
|
||||
sa.getTargets().add(oppA);
|
||||
return true;
|
||||
@@ -413,7 +414,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (numCards + ai.getPoisonCounters() >= 8) {
|
||||
aiTarget = false;
|
||||
}
|
||||
@@ -471,7 +472,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// ally would lose because of poison
|
||||
if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) {
|
||||
if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -533,16 +534,12 @@ public class DrawAi extends SpellAbilityAi {
|
||||
} // drawTargetAI()
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!mandatory && !willPayCosts(ai, sa, sa.getPayCosts(), sa.getHostCard())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetAI(ai, sa, mandatory)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return targetAI(ai, sa, mandatory);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -35,7 +35,7 @@ import java.util.Map;
|
||||
|
||||
public class EffectAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected boolean canPlayAI(final Player ai,final SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
|
||||
String logic = "";
|
||||
@@ -45,7 +45,12 @@ public class EffectAi extends SpellAbilityAi {
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
if (logic.equals("BeginningOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("EndOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
|
||||
@@ -59,20 +64,20 @@ public class EffectAi extends SpellAbilityAi {
|
||||
worthHolding = true;
|
||||
}
|
||||
if (!worthHolding) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
}
|
||||
} else if (logic.equals("RestrictBlocking")) {
|
||||
if (!phase.isPlayerTurn(ai) || phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)
|
||||
|| phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.getPayCosts().getTotalMana().countX() > 0 && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to half the remaining mana to allow for Main 2 and other combat shenanigans.
|
||||
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai, sa.isTrigger()) / 2;
|
||||
if (xPay == 0) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
|
||||
if (xPay == 0) { return false; }
|
||||
sa.setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
@@ -85,27 +90,23 @@ public class EffectAi extends SpellAbilityAi {
|
||||
int potentialDmg = 0;
|
||||
List<Card> currentAttackers = new ArrayList<>();
|
||||
|
||||
if (possibleBlockers.isEmpty()) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
|
||||
if (possibleBlockers.isEmpty()) { return false; }
|
||||
|
||||
for (final Card creat : possibleAttackers) {
|
||||
if (CombatUtil.canAttack(creat, opp) && possibleBlockers.size() > 1) {
|
||||
potentialDmg += creat.getCurrentPower();
|
||||
if (potentialDmg >= oppLife) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); }
|
||||
if (potentialDmg >= oppLife) { return true; }
|
||||
}
|
||||
if (combat != null && combat.isAttacking(creat)) {
|
||||
currentAttackers.add(creat);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentAttackers.size() > possibleBlockers.size()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return currentAttackers.size() > possibleBlockers.size();
|
||||
} else if (logic.equals("Fog")) {
|
||||
FogAi fogAi = new FogAi();
|
||||
if (!fogAi.canPlay(ai, sa).willingToPlay()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
if (!fogAi.canPlayAI(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -123,14 +124,14 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!canTgt) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
List<Card> list = game.getCombat().getAttackers();
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
Card target = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (target == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.getTargets().add(target);
|
||||
}
|
||||
@@ -138,7 +139,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("ChainVeil")) {
|
||||
if (!phase.isPlayerTurn(ai) || !phase.getPhase().equals(PhaseType.MAIN2) || ai.getPlaneswalkersInPlay().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("WillCastCreature") && ai.isAI()) {
|
||||
@@ -149,17 +150,17 @@ public class EffectAi extends SpellAbilityAi {
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("Main1")) {
|
||||
if (phase.getPhase().isBefore(PhaseType.MAIN1)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("Main2")) {
|
||||
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("Evasion")) {
|
||||
if (!phase.isPlayerTurn(ai)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean shouldPlay = false;
|
||||
@@ -184,10 +185,10 @@ public class EffectAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
|
||||
return shouldPlay ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return shouldPlay;
|
||||
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
boolean threatened = false;
|
||||
for (final SpellAbilityStackInstance stackInst : game.getStack()) {
|
||||
@@ -203,7 +204,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
randomReturn = threatened;
|
||||
} else if (logic.equals("Prevent")) { // prevent burn spell from opponent
|
||||
if (game.getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
final SpellAbility saTop = game.getStack().peekAbility();
|
||||
final Card host = saTop.getHostCard();
|
||||
@@ -214,10 +215,10 @@ public class EffectAi extends SpellAbilityAi {
|
||||
final ApiType type = saTop.getApi();
|
||||
if (type == ApiType.DealDamage || type == ApiType.DamageAll) { // burn spell
|
||||
sa.getTargets().add(saTop);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (logic.equals("NoGain")) {
|
||||
// basic logic to cancel GainLife on stack
|
||||
if (!game.getStack().isEmpty()) {
|
||||
@@ -227,14 +228,14 @@ public class EffectAi extends SpellAbilityAi {
|
||||
while (topStack != null) {
|
||||
if (topStack.getApi() == ApiType.GainLife) {
|
||||
if ("You".equals(topStack.getParam("Defined")) || topStack.isTargeting(activator) || (!topStack.usesTargeting() && !topStack.hasParam("Defined"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else if (topStack.getApi() == ApiType.DealDamage && topStack.getHostCard().hasKeyword(Keyword.LIFELINK)) {
|
||||
Card host = topStack.getHostCard();
|
||||
for (GameEntity target : topStack.getTargets().getTargetEntities()) {
|
||||
if (ComputerUtilCombat.predictDamageTo(target,
|
||||
AbilityUtils.calculateAmount(host, topStack.getParam("NumDmg"), topStack), host, false) > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,11 +249,11 @@ public class EffectAi extends SpellAbilityAi {
|
||||
final Player attackingPlayer = combat.getAttackingPlayer();
|
||||
if (attackingPlayer.isOpponentOf(ai) && attackingPlayer.canGainLife()) {
|
||||
if (ComputerUtilCombat.checkAttackerLifelinkDamage(combat) > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (logic.equals("NonCastCreature")) {
|
||||
// TODO: add support for more cases with more convoluted API setups
|
||||
if (!game.getStack().isEmpty()) {
|
||||
@@ -264,13 +265,13 @@ public class EffectAi extends SpellAbilityAi {
|
||||
boolean reanimator = "true".equalsIgnoreCase(topStack.getSVar("IsReanimatorCard"));
|
||||
if (changeZone && (toBattlefield || reanimator)) {
|
||||
if ("Creature".equals(topStack.getParam("ChangeType")) || topStack.getParamOrDefault("Defined", "").contains("Creature"))
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (logic.equals("Fight")) {
|
||||
return FightAi.canFightAi(ai, sa, 0,0);
|
||||
return FightAi.canFightAi(ai, sa, 0, 0);
|
||||
} else if (logic.equals("Pump")) {
|
||||
sa.resetTargets();
|
||||
List<Card> options = CardUtil.getValidCardsToTarget(sa);
|
||||
@@ -280,55 +281,55 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
if (!options.isEmpty() && phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(options));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (logic.equals("Burn")) {
|
||||
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
|
||||
SpellAbility burn = sa.getSubAbility();
|
||||
return SpellApiToAi.Converter.get(burn).canPlayWithSubs(ai, burn).willingToPlay() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn);
|
||||
} else if (logic.equals("YawgmothsWill")) {
|
||||
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return SpecialCardAi.YawgmothsWill.consider(ai, sa);
|
||||
} else if (logic.startsWith("NeedCreatures")) {
|
||||
// TODO convert to AiCheckSVar
|
||||
if (ai.getCreaturesInPlay().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
if (logic.contains(":")) {
|
||||
String[] k = logic.split(":");
|
||||
int i = Integer.parseInt(k[1]);
|
||||
return ai.getCreaturesInPlay().size() >= i ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return ai.getCreaturesInPlay().size() >= i;
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if (logic.equals("ReplaySpell")) {
|
||||
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Graveyard), sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if (logic.equals("PeaceTalks")) {
|
||||
Player nextPlayer = game.getNextPlayerAfter(ai);
|
||||
|
||||
// If opponent doesn't have creatures, preventing attacks don't mean as much
|
||||
if (nextPlayer.getCreaturesInPlay().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only cast Peace Talks after you attack just in case you have creatures
|
||||
if (!phase.is(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a pseudo combat and see if my life is in danger
|
||||
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return randomReturn;
|
||||
} else if (logic.equals("Bribe")) {
|
||||
Card host = sa.getHostCard();
|
||||
Combat combat = game.getCombat();
|
||||
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
|
||||
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
// ideally needs once per combat or something
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (logic.equals("CantRegenerate")) {
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -349,19 +350,19 @@ public class EffectAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
// TODO check Stack for Effects that would destroy the selected card?
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(list));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if (sa.getParent() != null) {
|
||||
// sub ability should be okay
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
} else if ("Self".equals(sa.getParam("RememberObjects"))) {
|
||||
// the ones affecting itself are Nimbus cards, were opponent can activate this effect
|
||||
Card host = sa.getHostCard();
|
||||
if (!host.canBeDestroyed()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
|
||||
@@ -369,18 +370,18 @@ public class EffectAi extends SpellAbilityAi {
|
||||
List<ReplacementEffect> repDestroyList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
|
||||
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
|
||||
if (repDestroyList.isEmpty() || repDestroyList.stream().anyMatch(CardTraitPredicates.hasParam("Regeneration").negate())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cantRegenerateCheckCombat(host) || cantRegenerateCheckStack(host)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else { //no AILogic
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("False".equals(sa.getParam("Stackable"))) {
|
||||
@@ -389,7 +390,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
name = sa.getHostCard().getName() + "'s Effect";
|
||||
}
|
||||
if (sa.getActivatingPlayer().isCardInCommand(name)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,20 +406,20 @@ public class EffectAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return canTgt ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return canTgt;
|
||||
} else {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
}
|
||||
|
||||
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if (canPlay(aiPlayer, sa).willingToPlay()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
if (canPlayAI(aiPlayer, sa)) {
|
||||
return true; // if false, fall through further to do the mandatory stuff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +431,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
if (!oppPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
@@ -440,14 +441,14 @@ public class EffectAi extends SpellAbilityAi {
|
||||
if (!aiPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
}
|
||||
|
||||
protected boolean cantRegenerateCheckCombat(Card host) {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.combat.CombatUtil;
|
||||
@@ -43,17 +45,19 @@ public final class EncodeAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
*
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -14,22 +12,18 @@ import forge.game.spellability.SpellAbility;
|
||||
public class EndTurnAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { return false; }
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayLife;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -22,39 +22,19 @@ public class EndureAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||
if (bestCreature == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
// Card-specific logic
|
||||
final String num = sa.getParamOrDefault("Num", "1");
|
||||
if ("X".equals(num) && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
|
||||
if (!aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
int curLife = aiPlayer.getLife();
|
||||
int dangerLife = (((PlayerControllerAi) aiPlayer.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
|
||||
if (curLife <= dangerLife) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
int availableMana = ComputerUtilMana.getAvailableManaEstimate(aiPlayer) - 1;
|
||||
int maxEndureX = Math.min(availableMana, curLife - dangerLife);
|
||||
if (maxEndureX > 0) {
|
||||
sa.setXManaCostPaid(maxEndureX);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
|
||||
@@ -141,7 +121,7 @@ public class EndureAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
@@ -149,16 +129,12 @@ public class EndureAi extends SpellAbilityAi {
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,19 @@ public class ExploreAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Explore with a target (e.g. Enter the Unknown)
|
||||
if (sa.usesTargeting()) {
|
||||
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||
if (bestCreature == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean shouldPutInGraveyard(Card topCard, Player ai) {
|
||||
@@ -64,23 +64,19 @@ public class ExploreAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ public class FightAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
sa.resetTargets();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// everything is defined or targeted above, can't do anything there unless a specific logic is set
|
||||
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get creature lists
|
||||
@@ -42,10 +42,8 @@ public class FightAi extends SpellAbilityAi {
|
||||
// Filter MustTarget requirements
|
||||
StaticAbilityMustTarget.filterMustTargetCards(ai, humCreatures, sa);
|
||||
|
||||
//prevent IndexOutOfBoundsException on MOJHOSTO variant
|
||||
if (humCreatures.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
if (humCreatures.isEmpty())
|
||||
return false; //prevent IndexOutOfBoundsException on MOJHOSTO variant
|
||||
|
||||
// assumes the triggered card belongs to the ai
|
||||
if (sa.hasParam("Defined")) {
|
||||
@@ -56,7 +54,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (fighter1List.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
Card fighter1 = fighter1List.get(0);
|
||||
for (Card humanCreature : humCreatures) {
|
||||
@@ -64,11 +62,10 @@ public class FightAi extends SpellAbilityAi {
|
||||
&& !canKill(humanCreature, fighter1, 0)) {
|
||||
// todo: check min/max targets; see if we picked the best matchup
|
||||
sa.getTargets().add(humanCreature);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// bail at this point, otherwise the AI will overtarget and waste the activation
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false; // bail at this point, otherwise the AI will overtarget and waste the activation
|
||||
}
|
||||
|
||||
if (sa.hasParam("TargetsFromDifferentZone")) {
|
||||
@@ -80,12 +77,12 @@ public class FightAi extends SpellAbilityAi {
|
||||
// todo: check min/max targets; see if we picked the best matchup
|
||||
sa.getTargets().add(humanCreature);
|
||||
sa.getTargets().add(aiCreature);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
for (Card creature1 : humCreatures) {
|
||||
for (Card creature2 : humCreatures) {
|
||||
@@ -100,52 +97,42 @@ public class FightAi extends SpellAbilityAi {
|
||||
// todo: check min/max targets; see if we picked the best matchup
|
||||
sa.getTargets().add(creature1);
|
||||
sa.getTargets().add(creature2);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
|
||||
return true; // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
|
||||
}
|
||||
|
||||
return checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
if (aiLogic.equals("Grothama")) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (SpecialCardAi.GrothamaAllDevouring.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return mandatory ? true : SpecialCardAi.GrothamaAllDevouring.consider(ai, sa);
|
||||
}
|
||||
|
||||
AiAbilityDecision decision = checkApiLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
if (checkApiLogic(ai, sa)) {
|
||||
return true;
|
||||
}
|
||||
if (!mandatory) {
|
||||
return decision;
|
||||
return false;
|
||||
}
|
||||
// if mandatory, we have to play it, so we will try to make a good trade or no trade
|
||||
|
||||
//try to make a good trade or no trade
|
||||
final Card source = sa.getHostCard();
|
||||
List<Card> humCreatures = ai.getOpponents().getCreaturesInPlay();
|
||||
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
|
||||
if (humCreatures.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
//assumes the triggered card belongs to the ai
|
||||
if (sa.hasParam("Defined")) {
|
||||
@@ -154,19 +141,19 @@ public class FightAi extends SpellAbilityAi {
|
||||
if (canKill(aiCreature, humanCreature, 0)
|
||||
&& ComputerUtilCard.evaluateCreature(humanCreature) > ComputerUtilCard.evaluateCreature(aiCreature)) {
|
||||
sa.getTargets().add(humanCreature);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (Card humanCreature : humCreatures) {
|
||||
if (!canKill(humanCreature, aiCreature, 0)) {
|
||||
sa.getTargets().add(humanCreature);
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
sa.getTargets().add(humCreatures.get(0));
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
return true;
|
||||
}
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,7 +164,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
* @param power bonus to power
|
||||
* @return true if fight effect should be played, false otherwise
|
||||
*/
|
||||
public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
public static boolean canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
AbilitySub tgtFight = sa.getSubAbility();
|
||||
@@ -209,7 +196,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
ComputerUtilCard.sortByEvaluateCreature(aiCreatures);
|
||||
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
|
||||
if (humCreatures.isEmpty() || aiCreatures.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return false;
|
||||
}
|
||||
// Evaluate creature pairs
|
||||
for (Card humanCreature : humCreatures) {
|
||||
@@ -239,7 +226,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
tgtFight.resetTargets();
|
||||
tgtFight.getTargets().add(humanCreature);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Other cards that use AILogic PowerDmg and a single target
|
||||
@@ -249,7 +236,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
tgtFight.resetTargets();
|
||||
tgtFight.getTargets().add(humanCreature);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -262,12 +249,12 @@ public class FightAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(aiCreature);
|
||||
tgtFight.resetTargets();
|
||||
tgtFight.getTargets().add(humanCreature);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -15,56 +13,52 @@ public class FlipACoinAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String ailogic = sa.getParam("AILogic");
|
||||
if (ailogic.equals("Never")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (ailogic.equals("PhaseOut")) {
|
||||
if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
} else if (ailogic.equals("Bangchuckers")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.resetTargets();
|
||||
for (Player o : ai.getOpponents()) {
|
||||
if (sa.canTarget(o) && o.canLoseLife() && !o.cantLoseForZeroOrLessLife()) {
|
||||
sa.getTargets().add(o);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (Card c : ai.getOpponents().getCreaturesInPlay()) {
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
} else if (ailogic.equals("KillOrcs")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
sa.resetTargets();
|
||||
for (Card c : ai.getOpponents().getCreaturesInPlay()) {
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -16,43 +14,26 @@ import java.util.Map;
|
||||
|
||||
public class FlipOntoBattlefieldAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
PhaseHandler ph = sa.getHostCard().getGame().getPhaseHandler();
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && sa.getPayCosts().hasManaCost()) {
|
||||
if (ph.is(PhaseType.END_OF_TURN)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
return ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
if ("DamageCreatures".equals(logic)) {
|
||||
int maxToughness = Integer.parseInt(sa.getSubAbility().getParam("NumDmg"));
|
||||
CardCollectionView rightToughness = CardLists.filter(aiPlayer.getOpponents().getCreaturesInPlay(), card -> card.getNetToughness() <= maxToughness && card.canBeDestroyed());
|
||||
|
||||
if (rightToughness.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
} else {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return !rightToughness.isEmpty();
|
||||
}
|
||||
|
||||
if (!aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return !aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user