Compare commits

..

7 Commits

Author SHA1 Message Date
Hanmac
da886202c0 SacrificeEffect now use Remember on SpellAbility 2018-12-26 10:30:56 +01:00
Hanmac
4ab45ae42b AbilityUtils: calcAmount use new getRemembered 2018-12-25 21:37:43 +01:00
Hanmac
bae8819630 Manifest: RememberManifested to SpellAbility 2018-12-25 21:37:43 +01:00
Hanmac
19fbe18235 CountersRemoveAllEffect: use SpellAbility Remember 2018-12-25 21:37:43 +01:00
Hanmac
5e7d542d36 CounterRemove: move remember to SA 2018-12-25 21:37:43 +01:00
Hanmac
5e32834fb8 WrappedAbility: fix SpellAbility remember for trigger 2018-12-25 21:36:54 +01:00
Hanmac
738e68f468 SpellAbility: add Remember to SpellAbility 2018-12-25 21:36:54 +01:00
13825 changed files with 72990 additions and 309459 deletions

7
.classpath Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

286
.gitignore vendored
View File

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

View File

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

View File

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

12
.project Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>forge</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,7 @@
add_header=true
add_todo=false
eclipse.preferences.version=1
header_text=/*\n * Forge\: Play Magic\: the Gathering.\n * Copyright (C) 2011 Forge Team\n *\n * This program is free software\: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n * \n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n * \n * You should have received a copy of the GNU General Public License\n * along with this program. If not, see <http\://www.gnu.org/licenses/>.\n */
project_specific_settings=true
replacements=<?xml version\="1.0" standalone\="yes"?>\n\n<replacements>\n<replacement key\="get" scope\="1" mode\="0">Gets the</replacement>\n<replacement key\="set" scope\="1" mode\="0">Sets the</replacement>\n<replacement key\="add" scope\="1" mode\="0">Adds the</replacement>\n<replacement key\="edit" scope\="1" mode\="0">Edits the</replacement>\n<replacement key\="remove" scope\="1" mode\="0">Removes the</replacement>\n<replacement key\="init" scope\="1" mode\="0">Inits the</replacement>\n<replacement key\="parse" scope\="1" mode\="0">Parses the</replacement>\n<replacement key\="create" scope\="1" mode\="0">Creates the</replacement>\n<replacement key\="build" scope\="1" mode\="0">Builds the</replacement>\n<replacement key\="is" scope\="1" mode\="0">Checks if is</replacement>\n<replacement key\="print" scope\="1" mode\="0">Prints the</replacement>\n<replacement key\="has" scope\="1" mode\="0">Checks for</replacement>\n</replacements>\n\n
visibility_private=false

View File

@@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8

View File

@@ -0,0 +1,284 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.7
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_assignment=0
org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
org.eclipse.jdt.core.formatter.blank_lines_after_package=1
org.eclipse.jdt.core.formatter.blank_lines_before_field=0
org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
org.eclipse.jdt.core.formatter.blank_lines_before_method=1
org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
org.eclipse.jdt.core.formatter.blank_lines_before_package=0
org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
org.eclipse.jdt.core.formatter.comment.format_block_comments=true
org.eclipse.jdt.core.formatter.comment.format_header=false
org.eclipse.jdt.core.formatter.comment.format_html=true
org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
org.eclipse.jdt.core.formatter.comment.format_line_comments=true
org.eclipse.jdt.core.formatter.comment.format_source_code=true
org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert
org.eclipse.jdt.core.formatter.comment.line_length=80
org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
org.eclipse.jdt.core.formatter.compact_else_if=true
org.eclipse.jdt.core.formatter.continuation_indentation=2
org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
org.eclipse.jdt.core.formatter.indent_empty_lines=false
org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
org.eclipse.jdt.core.formatter.indentation.size=4
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
org.eclipse.jdt.core.formatter.join_lines_in_comments=true
org.eclipse.jdt.core.formatter.join_wrapped_lines=true
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
org.eclipse.jdt.core.formatter.lineSplit=120
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
org.eclipse.jdt.core.formatter.tabulation.char=space
org.eclipse.jdt.core.formatter.tabulation.size=4
org.eclipse.jdt.core.formatter.use_on_off_tags=false
org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true
org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

674
LICENSE
View File

@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

230
README.md
View File

@@ -1,230 +0,0 @@
# Forge
Gitlab repo is found [here](https://git.cardforge.org/core-developers/forge).
Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated)
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
# Requirements / Tools
- Java IDE such as IntelliJ or Eclipse
- Java JDK 8 or later
- Git
- Git client (optional)
- Maven
- Gitlab account
- Libgdx (optional: familiarity with this library is helpful for mobile platform development)
- Android SDK (optional: for Android releases)
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
# Project Quick Setup
- Log in to gitlab with your user account and fork the project.
- Clone your forked project to your local machine
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
# Eclipse
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
## Project Setup
- Follow the instructions for cloning from Gitlab. You'll need a Gitlab account setup and an SSH key defined.
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your Gitlab profile under
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing Gitlab.
- Fork the Forge git repo to your Gitlab account.
- Clone your forked repo to your local machine.
- Make sure the Java SDK is installed -- not just the JRE. Java 8 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 1.8 or later.
- Install Eclipse 2018-12 or later for Java. Launch it.
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
ensure everything is checked > Finish.
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
for this first time through.
- Once everything builds, all errors should disappear. You can now advance to Project launch.
## Project Launch
### Desktop
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!
### 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.
- 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!
## Eclipse / Android SDK Integration
Google no longer supports Android SDK releases for Eclipse. That said, it is still possible to build and debug Android platforms.
### Android SDK
Reference SO for obtaining a specific release: https://stackoverflow.com/questions/27043522/where-can-i-download-an-older-version-of-the-android-sdk
#### Windows
Download the following archived version of the Android SDK: http://dl-ssl.google.com/android/repository/tools_r25.2.3-windows.zip. Install it somewhere on your machine. This is referenced
in the following instructions as your 'Android SDK Install' path.
#### Linux / Mac OSX
TBD
### Android Plugin for Eclipse
Google's last plugin release does not work completely with target's running Android 7.0 or later. Download the ADT-24.2.0-20160729.zip plugin
from: https://github.com/khaledev/ADT/releases
In Eclipse go to: Help > Install New Software... > Add > Name: ADT Update, Click on the "Archive:" button and navigate to the downloaded ADT-24.2.0-20160729.zip file > Add. Install all "Developer Tools". Eclipse
should restart and prompt you to run the SDK Manager. Launch it and continue to the next steps below.
### Android Platform
In Eclipse, if the SDK Manager is not already running, go to Window > Android SDK Manager. Install the following options / versions:
- Android SDK Build-tools 26.0.1
- Android 7.1.1 (API 25) SDK Platform
- Google USB Driver 11
Note that this will populate additional tools in the Android SDK install path extracted above.
### Proguard update
The Proguard included with the Android SDK Build-tools is outdated and does not work with Java 1.8. Download Proguard 6.0.3 from https://sourceforge.net/projects/proguard/files/proguard/6.0/.
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard4.7/.
- Extract Proguard 6.0.3 to the Android SDK install path under tools/. You will need to rename the dir proguard6.0.3/ to proguard/.
### Android Build
The Eclipse plug-ins do NOT support building things for Android. They do however allow you to use the debugger so you can still set breakpoints and trace
things out. The steps below show how to generate a debug Android build.
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
- On the Main tab, set Goals: clean install
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
3) Right-click on the forge-gui-android project. Run as.. > Maven build...
- On the Main tab, set Goals: install, Profiles: android-debug
- On the Environment tab, you may need to define the variable ANDROID_HOME with the value containing the path to your Android SDK installation. For example, Variable: ANDROID_HOME, Value: Your Android SDK install path here.
4) Run the forge-gui-android Maven build. This may take a few minutes. If everything worked, you should see "BUILD SUCCESS" in the Console View.
Assuming you got this far, you should have an Android forge-android-[version].apk in the forge-gui-android/target path.
### Android Deploy
You'll need to have the Android SDK install path platform-tools/ path in your command search path to easily deploy builds.
- Open a command prompt. Navigate to the forge-gui-android/target/ path.
- Connect your Android device to your dev machine.
- Ensure the device is visible using `adb devices`
- Remove the old Forge install if present: `adb uninstall forge.app`
- Install the new apk: `adb install forge-android-[version].apk`
### Android Debugging
Assuming the apk is installed, launch it from the device.
In Eclipse, launch the DDMS. Window > Perspective > Open Perspective > Other... > DDMS. You should see the forge app in the list. Highlight the app, click on the green debug button and a
green debug button should appear next to the app's name. You can now set breakpoints and step through the source code.
## Windows / Linux SNAPSHOT build
SNAPSHOT builds can be built via the Maven integration in Eclipse.
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
# IntelliJ
Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/wikis/intellij-setup).
# Card Scripting
Visit [this page](https://www.slightlymagic.net/wiki/Forge_API) for information on scripting.
Card scripting resources are found in the forge-gui/res/ path.
# General Notes
## Project Hierarchy
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
- forge-ai
- forge-core
- forge-game
- forge-gui
The platform-specific projects are:
- forge-gui-android
- forge-gui-desktop
- forge-gui-ios
- forge-gui-mobile
- forge-gui-mobile-dev
### forge-ai
### forge-core
### forge-game
### forge-gui
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.
### 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.
### forge-gui-ios
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.
### forge-gui-mobile-dev
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.

10
forge-ai/.classpath Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry combineaccessrules="false" kind="src" path="/forge-core"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

24
forge-ai/.project Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>forge-ai</name>
<comment></comment>
<projects>
<project>forge-game</project>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,3 @@
eclipse.preferences.version=1
encoding//src/main/java=ISO-8859-1
encoding/<project>=UTF-8

View File

@@ -0,0 +1,5 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.7

View File

@@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

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

View File

@@ -1,5 +1,5 @@
package forge.ai;
public enum AIOption {
USE_SIMULATION
USE_SIMULATION;
}

View File

@@ -17,26 +17,16 @@
*/
package forge.ai;
import java.util.ArrayList;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi;
import forge.card.CardTypeView;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
import forge.game.ability.effects.ProtectEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.combat.GlobalAttackRestrictions;
@@ -47,12 +37,14 @@ import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import java.util.ArrayList;
import java.util.List;
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
/**
@@ -76,7 +68,6 @@ public class AiAttackController {
private Player defendingOpponent;
private int aiAggression = 0; // added by Masher, how aggressive the ai is attack will be depending on circumstances
private final boolean nextTurn;
/**
@@ -86,40 +77,34 @@ public class AiAttackController {
*
*/
public AiAttackController(final Player ai) {
this(ai, false);
} // constructor
public AiAttackController(final Player ai, boolean nextTurn) {
this.ai = ai;
this.defendingOpponent = choosePreferredDefenderPlayer();
this.oppList = getOpponentCreatures(this.defendingOpponent);
this.myList = ai.getCreaturesInPlay();
this.attackers = new ArrayList<>();
this.attackers = new ArrayList<Card>();
for (Card c : myList) {
if (nextTurn && CombatUtil.canAttackNextTurn(c, this.defendingOpponent) ||
CombatUtil.canAttack(c, this.defendingOpponent)) {
if (CombatUtil.canAttack(c, this.defendingOpponent)) {
attackers.add(c);
}
}
this.blockers = getPossibleBlockers(oppList, this.attackers);
this.nextTurn = nextTurn;
} // overloaded constructor to evaluate attackers that should attack next turn
} // constructor
public AiAttackController(final Player ai, Card attacker) {
this.ai = ai;
this.defendingOpponent = choosePreferredDefenderPlayer();
this.oppList = getOpponentCreatures(this.defendingOpponent);
this.myList = ai.getCreaturesInPlay();
this.attackers = new ArrayList<>();
this.attackers = new ArrayList<Card>();
if (CombatUtil.canAttack(attacker, this.defendingOpponent)) {
attackers.add(attacker);
}
this.blockers = getPossibleBlockers(oppList, this.attackers);
this.nextTurn = false;
} // overloaded constructor to evaluate single specified attacker
public static List<Card> getOpponentCreatures(final Player defender) {
List<Card> defenders = new ArrayList<>(defender.getCreaturesInPlay());
List<Card> defenders = new ArrayList<Card>();
defenders.addAll(defender.getCreaturesInPlay());
Predicate<Card> canAnimate = new Predicate<Card>() {
@Override
public boolean apply(Card c) {
@@ -147,14 +132,6 @@ public class AiAttackController {
this.oppList.remove(blocker);
}
private boolean canAttackWrapper(final Card attacker, final GameEntity defender) {
if (nextTurn) {
return CombatUtil.canAttackNextTurn(attacker, defender);
} else {
return CombatUtil.canAttack(attacker, defender);
}
}
/** Choose opponent for AI to attack here. Expand as necessary. */
private Player choosePreferredDefenderPlayer() {
Player defender = ai.getWeakestOpponent(); //Gets opponent with the least life
@@ -174,7 +151,7 @@ public class AiAttackController {
*
*/
public final static List<Card> sortAttackers(final List<Card> in) {
final List<Card> list = new ArrayList<>();
final List<Card> list = new ArrayList<Card>();
// Cards with triggers should come first (for Battle Cry)
for (final Card attacker : in) {
@@ -214,48 +191,14 @@ public class AiAttackController {
return false;
}
// the attacker will die to a triggered ability (e.g. Sarkhan the Masterless)
for (Card c : ai.getOpponents().getCardsIn(ZoneType.Battlefield)) {
for (Trigger t : c.getTriggers()) {
if (t.getMode() == TriggerType.Attacks) {
SpellAbility sa = t.ensureAbility();
if (sa == null) {
continue;
}
if (sa.getApi() == ApiType.EachDamage && "TriggeredAttacker".equals(sa.getParam("DefinedPlayers"))) {
List<Card> valid = CardLists.getValidCards(c.getController().getCreaturesInPlay(), sa.getParam("ValidCards"), c.getController(), c, sa);
// TODO: this assumes that 1 damage is dealt per creature. Improve this to check the parameter/X to determine
// how much damage is dealt by each of the creatures in the valid list.
if (attacker.getNetToughness() <= valid.size()) {
return false;
}
}
}
}
}
if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) {
return true;
}
final Player opp = this.defendingOpponent;
// Damage opponent if unblocked
final int dmgIfUnblocked = ComputerUtilCombat.damageIfUnblocked(attacker, opp, combat, true);
if (dmgIfUnblocked > 0) {
boolean onlyIfExalted = false;
if (combat.getAttackers().isEmpty() && ai.countExaltedBonus() > 0
&& dmgIfUnblocked - ai.countExaltedBonus() == 0) {
// Make sure we're not counting on the Exalted bonus when the AI is planning to attack with more than one creature
onlyIfExalted = true;
}
if (!onlyIfExalted || this.attackers.size() == 1 || this.aiAggression == 6 /* 6 is Exalted attack */) {
if (ComputerUtilCombat.damageIfUnblocked(attacker, opp, combat, true) > 0) {
return true;
}
}
// Poison opponent if unblocked
if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) {
return true;
}
@@ -270,7 +213,7 @@ public class AiAttackController {
final CardCollectionView controlledByCompy = ai.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES);
for (final Card c : controlledByCompy) {
for (final Trigger trigger : c.getTriggers()) {
if (ComputerUtilCombat.combatTriggerWillTrigger(attacker, null, trigger, combat, this.attackers)) {
if (ComputerUtilCombat.combatTriggerWillTrigger(attacker, null, trigger, combat)) {
return true;
}
}
@@ -279,7 +222,7 @@ public class AiAttackController {
}
public final static List<Card> getPossibleBlockers(final List<Card> blockers, final List<Card> attackers) {
List<Card> possibleBlockers = new ArrayList<>(blockers);
List<Card> possibleBlockers = new ArrayList<Card>(blockers);
possibleBlockers = CardLists.filter(possibleBlockers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -290,7 +233,7 @@ public class AiAttackController {
}
public final static boolean canBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) {
final List<Card> attackerList = new ArrayList<>(attackers);
final List<Card> attackerList = new ArrayList<Card>(attackers);
if (!c.isCreature()) {
return false;
}
@@ -303,7 +246,7 @@ public class AiAttackController {
}
public final static Card getCardCanBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) {
final List<Card> attackerList = new ArrayList<>(attackers);
final List<Card> attackerList = new ArrayList<Card>(attackers);
if (!c.isCreature()) {
return null;
}
@@ -318,9 +261,9 @@ public class AiAttackController {
// this checks to make sure that the computer player doesn't lose when the human player attacks
// this method is used by getAttackers()
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) {
final List<Card> notNeededAsBlockers = new ArrayList<>(attackers);
final List<Card> notNeededAsBlockers = new ArrayList<Card>(attackers);
int fixedBlockers = 0;
final List<Card> vigilantes = new ArrayList<>();
final List<Card> vigilantes = new ArrayList<Card>();
//check for time walks
if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) {
return attackers;
@@ -359,7 +302,7 @@ public class AiAttackController {
}
}
List<Card> opponentsAttackers = new ArrayList<>(oppList);
List<Card> opponentsAttackers = new ArrayList<Card>(oppList);
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
@@ -488,7 +431,7 @@ public class AiAttackController {
final CardCollectionView beastions = ai.getCardsIn(ZoneType.Battlefield, "Beastmaster Ascension");
int minCreatures = 7;
for (final Card beastion : beastions) {
final int counters = beastion.getCounters(CounterEnumType.QUEST);
final int counters = beastion.getCounters(CounterType.QUEST);
minCreatures = Math.min(minCreatures, 7 - counters);
}
if (this.attackers.size() >= minCreatures) {
@@ -571,7 +514,8 @@ public class AiAttackController {
remainingAttackers.removeAll(unblockedAttackers);
for (Card blocker : this.blockers) {
if (blocker.canBlockAny()) {
if (blocker.hasKeyword("CARDNAME can block any number of creatures.")
|| blocker.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
for (Card attacker : this.attackers) {
if (CombatUtil.canBlock(attacker, blocker)) {
remainingAttackers.remove(attacker);
@@ -587,19 +531,14 @@ public class AiAttackController {
if (remainingAttackers.isEmpty() || maxBlockersAfterCrew == 0) {
break;
}
int numExtraBlocks = blocker.canBlockAdditional();
if (numExtraBlocks > 0) {
while (numExtraBlocks-- > 0 && !remainingAttackers.isEmpty()) {
if (blocker.hasKeyword("CARDNAME can block an additional creature each combat.")) {
blockedAttackers.add(remainingAttackers.get(0));
remainingAttackers.remove(0);
maxBlockersAfterCrew--;
}
}
if (remainingAttackers.isEmpty()) {
break;
}
}
blockedAttackers.add(remainingAttackers.get(0));
remainingAttackers.remove(0);
maxBlockersAfterCrew--;
@@ -705,12 +644,10 @@ public class AiAttackController {
final boolean bAssault = this.doAssault(ai);
// TODO: detect Lightmine Field by presence of a card with a specific trigger
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
// TODO: detect Season of the Witch by presence of a card with a specific trigger
final boolean seasonOfTheWitch = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Season of the Witch");
// Determine who will be attacked
GameEntity defender = this.chooseDefender(combat, bAssault);
List<Card> attackersLeft = new ArrayList<>(this.attackers);
List<Card> attackersLeft = new ArrayList<Card>(this.attackers);
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
@@ -727,9 +664,6 @@ public class AiAttackController {
// Attackers that don't really have a choice
int numForcedAttackers = 0;
// nextTurn is now only used by effect from Oracle en-Vec, which can skip check must attack,
// because creatures not chosen can't attack.
if (!nextTurn) {
for (final Card attacker : this.attackers) {
if (!CombatUtil.canAttack(attacker, defender)) {
attackersLeft.remove(attacker);
@@ -743,9 +677,6 @@ public class AiAttackController {
} else if (attacker.hasSVar("EndOfTurnLeavePlay")
&& isEffectiveAttacker(ai, attacker, combat)) {
mustAttack = true;
} else if (seasonOfTheWitch) {
// TODO: if there are other ways to tap this creature (like mana creature), then don't need to attack
mustAttack = true;
} else {
for (KeywordInterface inst : attacker.getKeywords()) {
String s = inst.getOriginal();
@@ -766,17 +697,11 @@ public class AiAttackController {
if (attackersLeft.isEmpty()) {
return;
}
}
// Lightmine Field: make sure the AI doesn't wipe out its own creatures
if (lightmineField) {
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro);
}
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily
if (!doRevengeOfRavensAttackLogic(ai, defender, attackersLeft, numForcedAttackers, attackMax)) {
return;
}
if (bAssault) {
if (LOG_AI_ATTACKS)
@@ -787,7 +712,7 @@ public class AiAttackController {
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
return;
if (canAttackWrapper(attacker, defender) && this.isEffectiveAttacker(ai, attacker, combat)) {
if (CombatUtil.canAttack(attacker, defender) && this.isEffectiveAttacker(ai, attacker, combat)) {
combat.addAttacker(attacker, defender);
}
}
@@ -828,7 +753,7 @@ public class AiAttackController {
System.out.println("Exalted");
this.aiAggression = 6;
for (Card attacker : this.attackers) {
if (canAttackWrapper(attacker, defender) && this.shouldAttack(ai, attacker, this.blockers, combat)) {
if (CombatUtil.canAttack(attacker, defender) && this.shouldAttack(ai, attacker, this.blockers, combat)) {
combat.addAttacker(attacker, defender);
return;
}
@@ -844,7 +769,7 @@ public class AiAttackController {
// reached max, breakup
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
break;
if (canAttackWrapper(attacker, defender) && this.shouldAttack(ai, attacker, this.blockers, combat)) {
if (CombatUtil.canAttack(attacker, defender) && this.shouldAttack(ai, attacker, this.blockers, combat)) {
combat.addAttacker(attacker, defender);
}
}
@@ -862,12 +787,12 @@ public class AiAttackController {
int humanForcesForAttritionalAttack = 0;
// examine the potential forces
final List<Card> nextTurnAttackers = new ArrayList<>();
final List<Card> nextTurnAttackers = new ArrayList<Card>();
int candidateCounterAttackDamage = 0;
final Player opp = this.defendingOpponent;
// get the potential damage and strength of the AI forces
final List<Card> candidateAttackers = new ArrayList<>();
final List<Card> candidateAttackers = new ArrayList<Card>();
int candidateUnblockedDamage = 0;
for (final Card pCard : this.myList) {
// if the creature can attack then it's a potential attacker this
@@ -926,7 +851,7 @@ public class AiAttackController {
final int outNumber = computerForces - humanForces;
for (Card blocker : this.blockers) {
if (blocker.canBlockAny()) {
if (blocker.hasKeyword("CARDNAME can block any number of creatures.")) {
aiLifeToPlayerDamageRatio--;
}
}
@@ -949,7 +874,7 @@ public class AiAttackController {
// get player life total
int humanLife = opp.getLife();
// get the list of attackers up to the first blocked one
final List<Card> attritionalAttackers = new ArrayList<>();
final List<Card> attritionalAttackers = new ArrayList<Card>();
for (int x = 0; x < (this.attackers.size() - humanForces); x++) {
attritionalAttackers.add(this.attackers.get(x));
}
@@ -1062,7 +987,7 @@ public class AiAttackController {
} // stay at home to block
if ( LOG_AI_ATTACKS )
System.out.println(this.aiAggression + " = ai aggression");
System.out.println(String.valueOf(this.aiAggression) + " = ai aggression");
// ****************
// Evaluation the end
@@ -1085,7 +1010,7 @@ public class AiAttackController {
continue;
}
if (this.shouldAttack(ai, attacker, this.blockers, combat) && canAttackWrapper(attacker, defender)) {
if (this.shouldAttack(ai, attacker, this.blockers, combat) && CombatUtil.canAttack(attacker, defender)) {
combat.addAttacker(attacker, defender);
// check if attackers are enough to finish the attacked planeswalker
if (defender instanceof Card) {
@@ -1103,7 +1028,7 @@ public class AiAttackController {
}
}
// if enough damage: switch to next planeswalker or player
if (damage >= pw.getCounters(CounterEnumType.LOYALTY)) {
if (damage >= pw.getCounters(CounterType.LOYALTY)) {
List<Card> pwDefending = combat.getDefendingPlaneswalkers();
boolean found = false;
// look for next planeswalker
@@ -1173,6 +1098,7 @@ public class AiAttackController {
// TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?)
if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) {
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine!
boolean wantability = false;
for (SpellAbility sa : attacker.getSpellAbilities()) {
// Do not attack if we can afford using the ability.
if (sa.isAbility()) {
@@ -1193,24 +1119,8 @@ public class AiAttackController {
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
// contains only the defender's blockers that can actually block the attacker
CardCollection validBlockers = CardLists.filter(defenders, new Predicate<Card>() {
@Override
public boolean apply(Card defender) {
return CombatUtil.canBlock(attacker, defender);
}
});
boolean canTrampleOverDefenders = attacker.hasKeyword(Keyword.TRAMPLE) && attacker.getNetCombatDamage() > Aggregates.sum(validBlockers, CardPredicates.Accessors.fnGetNetToughness);
// used to check that CanKillAllDangerous check makes sense in context where creatures with dangerous abilities are present
boolean dangerousBlockersPresent = !CardLists.filter(validBlockers, Predicates.or(
CardPredicates.hasKeyword(Keyword.WITHER), CardPredicates.hasKeyword(Keyword.INFECT),
CardPredicates.hasKeyword(Keyword.LIFELINK))).isEmpty();
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
int defPower = CardLists.getTotalPower(validBlockers, true, false);
int defPower = CardLists.getTotalPower(defenders, true);
if (!hasCombatEffect) {
for (KeywordInterface inst : attacker.getKeywords()) {
@@ -1227,12 +1137,13 @@ public class AiAttackController {
// number of factors about the attacking
// context that will be relevant to the attackers decision according to
// the selected strategy
for (final Card defender : validBlockers) {
for (final Card defender : defenders) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
if ((isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2)
&& CombatUtil.canBlock(attacker, defender)) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterEnumType.P1P1) == 0)) {
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
@@ -1267,7 +1178,7 @@ public class AiAttackController {
// - our creature will die for sure (chump attack)
// - our attack will not do anything special (no attack/combat effect to proc)
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
if (attackerWillDie || (avoidAttackingIntoBlock && uselessAttack && noContributionToAttack)) {
if (attackerWillDie || (avoidAttackingIntoBlock && (uselessAttack || noContributionToAttack))) {
canKillAllDangerous = false;
}
}
@@ -1293,10 +1204,6 @@ public class AiAttackController {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking because they can't block, expecting to kill or damage player");
return true;
} else if (!canBeKilled && !dangerousBlockersPresent && canTrampleOverDefenders) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = expecting to survive and get some Trample damage through");
return true;
}
if (numberOfPossibleBlockers > 2
@@ -1320,9 +1227,8 @@ public class AiAttackController {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = all out attacking");
return true;
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
|| (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
case 4: // expecting to at least trade with something
if (canKillAll || (canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
return true;
@@ -1330,7 +1236,7 @@ public class AiAttackController {
break;
case 3: // expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers)
|| (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|| ((canKillAllDangerous || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|| !canBeBlocked) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
@@ -1339,7 +1245,7 @@ public class AiAttackController {
break;
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
(canKillAllDangerous || !canBeKilled))) {
if (LOG_AI_ATTACKS)
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
return true;
@@ -1380,9 +1286,9 @@ public class AiAttackController {
if (!TriggerType.Exerted.equals(t.getMode())) {
continue;
}
SpellAbility sa = t.ensureAbility();
SpellAbility sa = t.getOverridingAbility();
if (sa == null) {
continue;
sa = AbilityFactory.getAbility(c, t.getParam("Execute"));
}
if (sa.usesTargeting()) {
sa.setActivatingPlayer(c.getController());
@@ -1409,12 +1315,21 @@ public class AiAttackController {
if (c.hasSVar("AIExertCondition")) {
if (!c.getSVar("AIExertCondition").isEmpty()) {
final String needsToExert = c.getSVar("AIExertCondition");
int x = 0;
int y = 0;
String sVar = needsToExert.split(" ")[0];
String comparator = needsToExert.split(" ")[1];
String compareTo = comparator.substring(2);
int x = AbilityUtils.calculateAmount(c, sVar, null);
int y = AbilityUtils.calculateAmount(c, compareTo, null);
try {
x = Integer.parseInt(sVar);
} catch (final NumberFormatException e) {
x = CardFactoryUtil.xCount(c, c.getSVar(sVar));
}
try {
y = Integer.parseInt(compareTo);
} catch (final NumberFormatException e) {
y = CardFactoryUtil.xCount(c, c.getSVar(compareTo));
}
if (Expressions.compare(x, comparator, y)) {
shouldExert = true;
}
@@ -1495,7 +1410,7 @@ public class AiAttackController {
if (artifact != null) {
return artifact;
}
return null;//should never get here
return null; //should never get here
}
private void doLightmineFieldAttackLogic(List<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
@@ -1528,39 +1443,4 @@ public class AiAttackController {
attackersLeft.removeAll(attUnsafe);
}
private boolean doRevengeOfRavensAttackLogic(Player ai, GameEntity defender, List<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false;
if (defender instanceof Player) {
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
} else if (defender instanceof Card) {
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
}
if (!revengeOfRavens) {
return true;
}
int life = ai.canLoseLife() && !ai.cantLoseForZeroOrLessLife() ? ai.getLife() : Integer.MAX_VALUE;
maxAttack = maxAttack < 0 ? Integer.MAX_VALUE - 1 : maxAttack;
if (Math.min(maxAttack, numForcedAttackers) >= life) {
return false;
}
// Remove all 1-power attackers since they usually only hurt the attacker
// TODO: improve to account for possible combat effects coming from attackers like that
CardCollection attUnsafe = new CardCollection();
for (Card attacker : attackersLeft) {
if (attacker.getNetCombatDamage() <= 1) {
attUnsafe.add(attacker);
}
}
attackersLeft.removeAll(attUnsafe);
if (Math.min(maxAttack, attackersLeft.size()) >= life) {
return false;
}
return true;
}
} // end class ComputerUtil_Attack2

View File

@@ -17,23 +17,13 @@
*/
package forge.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardStateName;
import forge.game.CardTraitBase;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.GlobalRuleChange;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword;
@@ -44,6 +34,8 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import java.util.*;
/**
* <p>
@@ -79,8 +71,7 @@ public class AiBlockController {
for (final Card blocker : blockersLeft) {
// if the blocker can block a creature with lure it can't block a creature without
if (CombatUtil.canBlock(attacker, blocker, combat)) {
boolean cantBlockAlone = blocker.hasKeyword("CARDNAME can't attack or block alone.") || blocker.hasKeyword("CARDNAME can't block alone.");
if (solo && cantBlockAlone) {
if (solo && blocker.hasKeyword("CARDNAME can't attack or block alone.")) {
continue;
}
blockers.add(blocker);
@@ -157,7 +148,9 @@ public class AiBlockController {
final CardCollection attackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers);
sortedAttackers.addAll(attackers);
for (final Card c : attackers) {
sortedAttackers.add(c);
}
} else if (defender instanceof Player && defender.equals(ai)) {
firstAttacker = combat.getAttackersOf(defender);
}
@@ -170,7 +163,9 @@ public class AiBlockController {
}
} else {
// add creatures attacking the Player to the back of the list
sortedAttackers.addAll(firstAttacker);
for (final Card c : firstAttacker) {
sortedAttackers.add(c);
}
}
return sortedAttackers;
}
@@ -238,9 +233,9 @@ public class AiBlockController {
// 3.Blockers that can destroy the attacker and have an upside when dying
killingBlockers = getKillingBlockers(combat, attacker, blockers);
for (Card b : killingBlockers) {
if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterEnumType.P1P1) == 0) || b.hasSVar("SacMe")
|| (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0)
if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe")
|| (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|| b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
break;
@@ -272,14 +267,14 @@ public class AiBlockController {
if (mode == TriggerType.DamageDone) {
if ((!trigParams.containsKey("ValidSource")
|| trigger.matchesValid(attacker, trigParams.get("ValidSource").split(",")))
|| CardTraitBase.matchesValid(attacker, trigParams.get("ValidSource").split(","), attacker))
&& attacker.getNetCombatDamage() > 0
&& (!trigParams.containsKey("ValidTarget")
|| trigger.matchesValid(combat.getDefenderByAttacker(attacker), trigParams.get("ValidTarget").split(",")))) {
|| CardTraitBase.matchesValid(combat.getDefenderByAttacker(attacker), trigParams.get("ValidTarget").split(","), attacker))) {
value += 50;
}
} else if (mode == TriggerType.AttackerUnblocked) {
if (trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
if (CardTraitBase.matchesValid(attacker, trigParams.get("ValidCard").split(","), attacker)) {
value += 50;
}
}
@@ -309,8 +304,8 @@ public class AiBlockController {
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
for (Card b : blockers) {
if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterEnumType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterEnumType.FADE) == 0)
if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|| b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
@@ -486,7 +481,8 @@ public class AiBlockController {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, secondBlocker, combat, false);
List<Card> usableBlockersAsThird = new ArrayList<>(usableBlockers);
List<Card> usableBlockersAsThird = new ArrayList<>();
usableBlockersAsThird.addAll(usableBlockers);
usableBlockersAsThird.remove(secondBlocker);
// loop over the remaining blockers in search of a good third blocker candidate
@@ -777,12 +773,14 @@ public class AiBlockController {
blockers.removeAll(combat.getBlockers(attacker));
// Don't add any blockers that won't kill the attacker because the damage would be prevented by a static effect
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
blockers = CardLists.filter(blockers, new Predicate<Card>() {
@Override
public boolean apply(Card blocker) {
return !ComputerUtilCombat.isCombatDamagePrevented(blocker, attacker, blocker.getNetCombatDamage());
}
});
}
// Try to use safe blockers first
if (blockers.size() > 0) {
@@ -861,7 +859,7 @@ public class AiBlockController {
damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true);
}
}
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterEnumType.LOYALTY)) {
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= ((Card) def).getCounters(CounterType.LOYALTY)) {
threatenedPWs.add((Card) def);
}
}
@@ -881,7 +879,7 @@ public class AiBlockController {
if (!chumpPWDefenders.isEmpty()) {
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains(def)) {
if (def instanceof Card && threatenedPWs.contains((Card) def)) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
// don't bother trying to chump a trampling creature
continue;
@@ -916,10 +914,10 @@ public class AiBlockController {
pwDefenders.addAll(combat.getBlockers(pwAtk));
} else {
isFullyBlocked = false;
damageToPW += ComputerUtilCombat.predictDamageTo(pw, pwAtk.getNetCombatDamage(), pwAtk, true);
damageToPW += ComputerUtilCombat.predictDamageTo((Card) pw, pwAtk.getNetCombatDamage(), pwAtk, true);
}
}
if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterEnumType.LOYALTY)) {
if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterType.LOYALTY)) {
for (Card chump : pwDefenders) {
if (chosenChumpBlockers.contains(chump)) {
combat.removeFromCombat(chump);
@@ -1291,7 +1289,6 @@ public class AiBlockController {
oppCreatureCount = ComputerUtil.countUsefulCreatures(attackersLeft.get(0).getController());
}
if (attacker != null && attacker.getOwner() != null)
if (attacker.getOwner().equals(ai) && "6".equals(attacker.getSVar("SacMe"))) {
// Temporarily controlled object - don't trade with it
// TODO: find a more reliable way to figure out that control will be reestablished next turn
@@ -1332,9 +1329,13 @@ public class AiBlockController {
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
if (((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
&& powerParityOrHigher
&& (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand)
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker);
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker)) {
return true;
}
return false;
}
}

View File

@@ -18,13 +18,13 @@
package forge.ai;
import forge.game.card.Card;
import forge.game.player.Player;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import forge.game.card.Card;
import forge.game.player.Player;
/**
* <p>
* AiCardMemory class.
@@ -137,7 +137,7 @@ public class AiCardMemory {
Set<Card> memorySet = getMemorySet(set);
return memorySet != null && memorySet.contains(c);
return memorySet == null ? false : memorySet.contains(c);
}
/**
@@ -150,8 +150,6 @@ public class AiCardMemory {
*/
public boolean isRememberedCardByName(String cardName, MemorySet set) {
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
@@ -160,7 +158,6 @@ public class AiCardMemory {
return true;
}
}
}
return false;
}
@@ -177,8 +174,6 @@ public class AiCardMemory {
*/
public boolean isRememberedCardByName(String cardName, MemorySet set, Player owner) {
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
@@ -187,7 +182,6 @@ public class AiCardMemory {
return true;
}
}
}
return false;
}
@@ -203,12 +197,7 @@ public class AiCardMemory {
if (c == null)
return false;
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
memorySet.add(c);
}
getMemorySet(set).add(c);
return true;
}
@@ -227,12 +216,7 @@ public class AiCardMemory {
return false;
}
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
memorySet.remove(c);
}
getMemorySet(set).remove(c);
return true;
}
@@ -245,8 +229,6 @@ public class AiCardMemory {
*/
public boolean forgetAnyCardWithName(String cardName, MemorySet set) {
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
@@ -255,7 +237,6 @@ public class AiCardMemory {
return forgetCard(c, set);
}
}
}
return false;
}
@@ -270,8 +251,6 @@ public class AiCardMemory {
*/
public boolean forgetAnyCardWithName(String cardName, MemorySet set, Player owner) {
Set<Card> memorySet = getMemorySet(set);
if (memorySet != null) {
Iterator<Card> it = memorySet.iterator();
while (it.hasNext()) {
@@ -280,7 +259,6 @@ public class AiCardMemory {
return forgetCard(c, set);
}
}
}
return false;
}
@@ -291,17 +269,15 @@ public class AiCardMemory {
* @return true, if the given memory set contains no remembered cards.
*/
public boolean isMemorySetEmpty(MemorySet set) {
return set == null || getMemorySet(set).isEmpty();
return getMemorySet(set).isEmpty();
}
/**
* Clears the given memory set.
*/
public void clearMemorySet(MemorySet set) {
if (set != null) {
getMemorySet(set).clear();
}
}
/**
* Clears all memory sets stored in this card memory for the given player.

View File

@@ -17,19 +17,13 @@
*/
package forge.ai;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.esotericsoftware.minlog.Log;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.ExploreAi;
import forge.ai.simulation.SpellAbilityPicker;
@@ -38,38 +32,17 @@ import forge.card.mana.ManaCost;
import forge.deck.CardPool;
import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.CardTraitBase;
import forge.game.CardTraitPredicates;
import forge.game.Direction;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.*;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPlayOption;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.card.CardPredicates.Accessors;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostPayLife;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseType;
@@ -77,16 +50,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplaceMoved;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.SpellPermanent;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -94,13 +58,16 @@ import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.Aggregates;
import forge.util.ComparatorUtil;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder;
import java.security.InvalidParameterException;
import java.util.*;
import java.util.Map.Entry;
/**
* <p>
* AiController class.
@@ -114,7 +81,6 @@ public class AiController {
private final Game game;
private final AiCardMemory memory;
private Combat predictedCombat;
private Combat predictedCombatNextTurn;
private boolean cheatShuffle;
private boolean useSimulation;
private SpellAbilityPicker simPicker;
@@ -159,15 +125,6 @@ public class AiController {
return predictedCombat;
}
public Combat getPredictedCombatNextTurn() {
if (predictedCombatNextTurn == null) {
AiAttackController aiAtk = new AiAttackController(player, true);
predictedCombatNextTurn = new Combat(player);
aiAtk.declareAttackers(predictedCombatNextTurn);
}
return predictedCombatNextTurn;
}
public AiController(final Player computerPlayer, final Game game0) {
player = computerPlayer;
game = game0;
@@ -220,7 +177,7 @@ public class AiController {
&& CardFactoryUtil.isCounterable(host)) {
return true;
} else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)
&& host.getCMC() == c.getCounters(CounterEnumType.CHARGE)) {
&& host.getCMC() == c.getCounters(CounterType.CHARGE)) {
return true;
} else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) {
String hostName = host.getName();
@@ -247,22 +204,24 @@ public class AiController {
return api == null;
}
boolean rightapi = false;
String battlefield = ZoneType.Battlefield.toString();
Player activatingPlayer = sa.getActivatingPlayer();
// Trigger play improvements
for (final Trigger tr : card.getTriggers()) {
// These triggers all care for ETB effects
final Map<String, String> params = tr.getMapParams();
if (tr.getMode() != TriggerType.ChangesZone) {
continue;
}
if (!ZoneType.Battlefield.toString().equals(tr.getParam("Destination"))) {
if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (tr.hasParam("ValidCard")) {
String validCard = tr.getParam("ValidCard");
if (params.containsKey("ValidCard")) {
String validCard = params.get("ValidCard");
if (!validCard.contains("Self")) {
continue;
}
@@ -286,11 +245,21 @@ public class AiController {
}
// if trigger is not mandatory - no problem
if (tr.hasParam("OptionalDecider") && api == null) {
if (params.get("OptionalDecider") != null && api == null) {
continue;
}
SpellAbility exSA = tr.ensureAbility().copy(activatingPlayer);
SpellAbility exSA = tr.getOverridingAbility();
if (exSA == null) {
// Maybe better considerations
if (!params.containsKey("Execute")) {
continue;
}
exSA = AbilityFactory.getAbility(card, params.get("Execute"));
} else {
exSA = exSA.copy();
}
if (api != null) {
if (exSA.getApi() != api) {
@@ -304,7 +273,13 @@ public class AiController {
}
}
exSA.setTrigger(tr);
if (sa != null) {
exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
}
exSA.setTrigger(true);
// for trigger test, need to ignore the conditions
SpellAbilityCondition cons = exSA.getConditions();
@@ -329,16 +304,18 @@ public class AiController {
// Replacement effects
for (final ReplacementEffect re : card.getReplacementEffects()) {
// These Replacements all care for ETB effects
final Map<String, String> params = re.getMapParams();
if (!(re instanceof ReplaceMoved)) {
continue;
}
if (!ZoneType.Battlefield.toString().equals(re.getParam("Destination"))) {
if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (re.hasParam("ValidCard")) {
String validCard = re.getParam("ValidCard");
if (params.containsKey("ValidCard")) {
String validCard = params.get("ValidCard");
if (!validCard.contains("Self")) {
continue;
}
@@ -360,18 +337,27 @@ public class AiController {
if (!re.requirementsCheck(game)) {
continue;
}
SpellAbility exSA = re.getOverridingAbility();
final SpellAbility exSA = re.getOverridingAbility();
if (exSA != null) {
exSA = exSA.copy(activatingPlayer);
if (sa != null) {
exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
}
if (exSA.getActivatingPlayer() == null) {
throw new InvalidParameterException("Executing SpellAbility for Replacement Effect has no activating player");
}
}
// ETBReplacement uses overriding abilities.
// These checks only work if the Executing SpellAbility is an Ability_Sub.
if ((exSA instanceof AbilitySub) && !doTrigger(exSA, false)) {
if (exSA != null && (exSA instanceof AbilitySub) && !doTrigger(exSA, false)) {
return false;
}
}
}
return true;
}
@@ -420,9 +406,7 @@ public class AiController {
@Override
public boolean apply(final Card c) {
CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
if (canPlaySpellBasic(c, null) != AiPlayDecision.WillPlay) {
return false;
}
canPlaySpellBasic(c, null);
String name = c.getName();
if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) {
if (Iterables.any(battlefield, CardPredicates.nameEquals(name))) {
@@ -476,7 +460,8 @@ public class AiController {
byte color = MagicColor.fromName(c);
for (Card land : landList) {
for (final SpellAbility m : ComputerUtilMana.getAIPlayableMana(land)) {
if (m.canProduce(MagicColor.toShortString(color))) {
AbilityManaPart mp = m.getManaPart();
if (mp.canProduce(MagicColor.toShortString(color), m)) {
return land;
}
}
@@ -529,7 +514,8 @@ public class AiController {
return land;
}
for (final SpellAbility m : ComputerUtilMana.getAIPlayableMana(land)) {
if (m.canProduce(MagicColor.toShortString(color))) {
AbilityManaPart mp = m.getManaPart();
if (mp.canProduce(MagicColor.toShortString(color), m)) {
return land;
}
}
@@ -624,15 +610,7 @@ public class AiController {
ComputerUtilAbility.getAvailableCards(game, player);
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, player);
try {
Collections.sort(all, saComparator); // put best spells first
}
catch (IllegalArgumentException ex) {
System.err.println(ex.getMessage());
String assertex = ComparatorUtil.verifyTransitivity(saComparator, all);
Sentry.capture(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
ApiType saApi = sa.getApi();
@@ -679,10 +657,10 @@ public class AiController {
return false;
}
AiCardMemory.MemorySet memSet = null;
AiCardMemory.MemorySet memSet;
if (phaseType == null && forNextSpell) {
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL;
} else if (phaseType != null) {
} else {
switch (phaseType) {
case MAIN2:
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
@@ -742,8 +720,6 @@ public class AiController {
public AiPlayDecision canPlaySa(SpellAbility sa) {
final Card card = sa.getHostCard();
final boolean isRightTiming = sa.canCastTiming(player);
if (!checkAiSpecificRestrictions(sa)) {
return AiPlayDecision.CantPlayAi;
}
@@ -785,18 +761,17 @@ public class AiController {
return AiPlayDecision.CantPlayAi;
}
}
else {
else if (sa.getPayCosts() != null){
Cost payCosts = sa.getPayCosts();
if(payCosts != null) {
ManaCost mana = payCosts.getTotalMana();
if (mana != null) {
if(mana.countX() > 0) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, player);
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, player);
if (xPay <= 0) {
return AiPlayDecision.CantAffordX;
}
sa.setXManaCostPaid(xPay);
card.setSVar("PayX", Integer.toString(xPay));
} else if (mana.isZero()) {
// if mana is zero, but card mana cost does have X, then something is wrong
ManaCost cardCost = card.getManaCost();
@@ -806,24 +781,10 @@ public class AiController {
}
}
}
}
if (checkCurseEffects(sa)) {
return AiPlayDecision.CurseEffects;
}
Card spellHost = card;
if (sa.isSpell()) {
spellHost = CardUtil.getLKICopy(spellHost);
spellHost.setLKICMC(-1); // to reset the cmc
spellHost.setLastKnownZone(game.getStackZone()); // need to add to stack to make check Restrictions respect stack cmc
spellHost.setCastFrom(card.getZone().getZoneType());
}
if (!sa.checkRestrictions(spellHost, player)) {
return AiPlayDecision.AnotherTime;
}
if (sa instanceof SpellPermanent) {
if (!isRightTiming) {
return AiPlayDecision.AnotherTime;
}
return canPlayFromEffectAI((SpellPermanent)sa, false, true);
}
if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
@@ -836,14 +797,8 @@ public class AiController {
&& !player.cantLoseForZeroOrLessLife() && player.canLoseLife()) {
return AiPlayDecision.CurseEffects;
}
if (!isRightTiming) {
return AiPlayDecision.AnotherTime;
}
return canPlaySpellBasic(card, sa);
}
if (!isRightTiming) {
return AiPlayDecision.AnotherTime;
}
return AiPlayDecision.WillPlay;
}
@@ -895,7 +850,7 @@ public class AiController {
int neededMana = 0;
boolean dangerousRecurringCost = false;
Cost costWithBuyback = sa.getPayCosts().copy();
Cost costWithBuyback = sa.getPayCosts() != null ? sa.getPayCosts().copy() : Cost.Zero;
for (OptionalCostValue opt : GameActionUtil.getOptionalCostValues(sa)) {
if (opt.getType() == OptionalCost.Buyback) {
costWithBuyback.add(opt.getCost());
@@ -944,8 +899,8 @@ public class AiController {
public int compare(final SpellAbility a, final SpellAbility b) {
// sort from highest cost to lowest
// we want the highest costs first
int a1 = a.getPayCosts().getTotalMana().getCMC();
int b1 = b.getPayCosts().getTotalMana().getCMC();
int a1 = a.getPayCosts() == null ? 0 : a.getPayCosts().getTotalMana().getCMC();
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
// deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs
if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) {
@@ -964,12 +919,12 @@ public class AiController {
// deprioritize pump spells with pure energy cost (can be activated last,
// since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler)
int a2 = 0, b2 = 0;
if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) {
if (a.getApi() == ApiType.Pump && a.getPayCosts() != null && a.getPayCosts().getCostEnergy() != null) {
if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
a2 = a.getPayCosts().getCostEnergy().convertAmount();
}
}
if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) {
if (b.getApi() == ApiType.Pump && b.getPayCosts() != null && b.getPayCosts().getCostEnergy() != null) {
if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
b2 = b.getPayCosts().getCostEnergy().convertAmount();
}
@@ -993,7 +948,8 @@ public class AiController {
return 1;
}
if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) {
if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()
&& a.getPayCosts() != null && b.getPayCosts() != null) {
// Cheaper Spectacle costs should be preferred
// FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not?
// (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs)
@@ -1034,10 +990,6 @@ public class AiController {
if (source.isEquipment() && noCreatures) {
p -= 9;
}
// don't equip stuff in main 2 if there's more stuff to cast at the moment
if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
p -= 1;
}
// 1. increase chance of using Surge effects
// 2. non-surged versions are usually inefficient
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
@@ -1060,7 +1012,7 @@ public class AiController {
p += 9;
}
// sort planeswalker abilities with most costly first
if (sa.isPwAbility()) {
if (sa.getRestrictions().isPwAbility()) {
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
if (cost instanceof CostRemoveCounter) {
p += cost.convertAmount() == null ? 1 : cost.convertAmount();
@@ -1092,10 +1044,9 @@ public class AiController {
}
public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa, final CardCollectionView exclude) {
boolean noFiltering = (sa != null) && "DiscardCMCX".equals(sa.getParam("AILogic")); // list AI logic for which filtering is taken care of elsewhere
CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand));
hand.removeAll(exclude);
if ((uTypes != null) && (sa != null) && !noFiltering) {
if ((uTypes != null) && (sa != null)) {
hand = CardLists.getValidCards(hand, uTypes, sa.getActivatingPlayer(), sa.getHostCard(), sa);
}
return getCardsToDiscard(numDiscard, numDiscard, hand, sa);
@@ -1115,13 +1066,6 @@ public class AiController {
min = 1;
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
} else if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
final int cmc = sa.getXManaCostPaid();
CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc));
if (discards.isEmpty()) {
return null;
}
return new CardCollection(ComputerUtilCard.getWorstAI(discards));
}
if (sa.hasParam("AnyNumber")) {
@@ -1267,7 +1211,7 @@ public class AiController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
ApiType api = sa.getApi();
// Abilities without api may also use this routine, However they should provide a unique mode value ?? How could this work?
// Abilities without api may also use this routine, However they should provide a unique mode value
if (api == null) {
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
mode);
@@ -1408,8 +1352,6 @@ public class AiController {
// Reset cached predicted combat, as it may be stale. It will be
// re-created if needed and used for any AI logic that needs it.
predictedCombat = null;
// Also reset predicted combat for next turn here
predictedCombatNextTurn = null;
// Reset priority mana reservation that's meant to work for one spell only
AiCardMemory.clearMemorySet(player, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL);
@@ -1444,7 +1386,6 @@ public class AiController {
final List<SpellAbility> abilities = Lists.newArrayList();
LandAbility la = new LandAbility(land, player, null);
la.setCardState(land.getCurrentState());
if (la.canPlay()) {
abilities.add(la);
}
@@ -1452,7 +1393,6 @@ public class AiController {
// add mayPlay option
for (CardPlayOption o : land.mayPlay(player)) {
la = new LandAbility(land, player, o.getAbility());
la.setCardState(land.getCurrentState());
if (la.canPlay()) {
abilities.add(la);
}
@@ -1505,11 +1445,9 @@ public class AiController {
int totalCMCInHand = Aggregates.sum(inHand, CardPredicates.Accessors.fnGetCmc);
int minCMCInHand = Aggregates.min(inHand, CardPredicates.Accessors.fnGetCmc);
if (minCMCInHand == Integer.MAX_VALUE)
minCMCInHand = 0;
int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && minCMCInHand > 0 && !isTapLand;
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && !isTapLand;
boolean cantCastAnythingNow = predictedMana < minCMCInHand;
boolean hasRelevantAbsOTB = !CardLists.filter(otb, new Predicate<Card>() {
@@ -1524,7 +1462,7 @@ public class AiController {
}
for (SpellAbility sa : card.getSpellAbilities()) {
if (sa.isAbility()
if (sa.getPayCosts() != null && sa.isAbility()
&& sa.getPayCosts().getCostMana() != null
&& sa.getPayCosts().getCostMana().getMana().getCMC() > 0
&& (!sa.getPayCosts().hasTapCost() || !isTapLand)
@@ -1589,11 +1527,11 @@ public class AiController {
top = game.getStack().peekAbility();
}
final boolean topOwnedByAI = top != null && top.getActivatingPlayer().equals(player);
final boolean mustRespond = top != null && top.hasParam("AIRespondsToOwnAbility");
if (topOwnedByAI) {
// AI's own spell: should probably let my stuff resolve first, but may want to copy the SA or respond to it
// in a scripted timed fashion.
final boolean mustRespond = top.hasParam("AIRespondsToOwnAbility");
if (!mustRespond) {
saList = ComputerUtilAbility.getSpellAbilities(cards, player); // get the SA list early to check for copy SAs
@@ -1617,19 +1555,8 @@ public class AiController {
saList = ComputerUtilAbility.getSpellAbilities(cards, player);
}
Iterables.removeIf(saList, new Predicate<SpellAbility>() {
@Override
public boolean apply(final SpellAbility spellAbility) {
return spellAbility instanceof LandAbility;
}
});
SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true);
if (topOwnedByAI && !mustRespond && chosenSa != ComputerUtilAbility.getFirstCopySASpell(saList)) {
return null; // not planning to copy the spell and not marked as something the AI would respond to
}
return chosenSa;
}
@@ -1637,14 +1564,7 @@ public class AiController {
if (all == null || all.isEmpty())
return null;
try {
Collections.sort(all, saComparator); // put best spells first
}
catch (IllegalArgumentException ex) {
System.err.println(ex.getMessage());
String assertex = ComparatorUtil.verifyTransitivity(saComparator, all);
Sentry.capture(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
// Don't add Counterspells to the "normal" playcard lookups
@@ -1662,18 +1582,10 @@ public class AiController {
}
sa.setActivatingPlayer(player);
SpellAbility root = sa.getRootAbility();
if (root.isSpell() || root.isTrigger() || root.isReplacementAbility()) {
sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard());
}
AiPlayDecision opinion = canPlayAndPayFor(sa);
// reset LastStateBattlefield
sa.setLastStateBattlefield(CardCollection.EMPTY);
sa.setLastStateGraveyard(CardCollection.EMPTY);
// PhaseHandler ph = game.getPhaseHandler();
// System.out.printf("Ai thinks '%s' of %s -> %s @ %s %s >>> \n", opinion, sa.getHostCard(), sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase());
@@ -1726,6 +1638,7 @@ public class AiController {
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
return true;
}
return false;
}
@@ -1741,14 +1654,14 @@ public class AiController {
hostCard = game.getCardState(hostCard);
}
if (effect.hasParam("AICheckSVar")) {
if (effect.getMapParams().containsKey("AICheckSVar")) {
System.out.println("aiShouldRun?" + sa);
final String svarToCheck = effect.getParam("AICheckSVar");
final String svarToCheck = effect.getMapParams().get("AICheckSVar");
String comparator = "GE";
int compareTo = 1;
if (effect.hasParam("AISVarCompare")) {
final String fullCmp = effect.getParam("AISVarCompare");
if (effect.getMapParams().containsKey("AISVarCompare")) {
final String fullCmp = effect.getMapParams().get("AISVarCompare");
comparator = fullCmp.substring(0, 2);
final String strCmpTo = fullCmp.substring(2);
try {
@@ -1770,11 +1683,16 @@ public class AiController {
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
}
System.out.println("aiShouldRun?" + left + comparator + compareTo);
return Expressions.compare(left, comparator, compareTo);
} else if (effect.hasParam("AICheckDredge")) {
if (Expressions.compare(left, comparator, compareTo)) {
return true;
}
} else if (effect.getMapParams().containsKey("AICheckDredge")) {
return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac");
} else return sa != null && doTrigger(sa, false);
} else if (sa != null && doTrigger(sa, false)) {
return true;
}
return false;
}
public List<SpellAbility> chooseSaToActivateFromOpeningHand(List<SpellAbility> usableFromOpeningHand) {
@@ -1853,9 +1771,7 @@ public class AiController {
+ MyRandom.getRandom().nextInt(3);
return Math.max(remaining, min) / 2;
} else if ("LowestLoseLife".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
} else if ("HighestLoseLife".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, ComputerUtil.getOpponentFor(player).getLife())) + 1;
} else if ("HighestGetCounter".equals(logic)) {
return MyRandom.getRandom().nextInt(3);
} else if (source.hasSVar("EnergyToPay")) {
@@ -1868,14 +1784,80 @@ public class AiController {
throw new UnsupportedOperationException("AI is not supposed to reach this code at the moment");
}
public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional, Map<String, Object> params) {
public Map<GameEntity, CounterType> chooseProliferation(final SpellAbility sa) {
final Map<GameEntity, CounterType> result = Maps.newHashMap();
final List<Player> allies = player.getAllies();
allies.add(player);
final List<Player> enemies = player.getOpponents();
final Function<Card, CounterType> predProliferate = new Function<Card, CounterType>() {
@Override
public CounterType apply(Card crd) {
//fast way out, no need to check other stuff
if (!crd.hasCounters()) {
return null;
}
// cards controlled by ai or ally with Vanishing or Fading
// and exaclty one counter of the specifice type gets high priority to keep the card
if (allies.contains(crd.getController())) {
// except if its a Chronozoa, because it WANTS to be removed to make more
if (crd.hasKeyword(Keyword.VANISHING) && !"Chronozoa".equals(crd.getName())) {
if (crd.getCounters(CounterType.TIME) == 1) {
return CounterType.TIME;
}
} else if (crd.hasKeyword(Keyword.FADING)) {
if (crd.getCounters(CounterType.FADE) == 1) {
return CounterType.FADE;
}
}
}
for (final Entry<CounterType, Integer> c1 : crd.getCounters().entrySet()) {
// if card can not recive the given counter, try another one
if (!crd.canReceiveCounters(c1.getKey())) {
continue;
}
if (ComputerUtil.isNegativeCounter(c1.getKey(), crd) && enemies.contains(crd.getController())) {
return c1.getKey();
}
if (!ComputerUtil.isNegativeCounter(c1.getKey(), crd) && allies.contains(crd.getController())) {
return c1.getKey();
}
}
return null;
}
};
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
CounterType ct = predProliferate.apply(c);
if (ct != null)
result.put(c, ct);
}
for (Player e : enemies) {
// TODO In the future check of enemies can get poison counters and give them some other bad counter type
if (e.getCounters(CounterType.POISON) > 0) {
result.put(e, CounterType.POISON);
}
}
for (Player pl : allies) {
if (pl.getCounters(CounterType.EXPERIENCE) > 0) {
result.put(pl, CounterType.EXPERIENCE);
} else if (pl.getCounters(CounterType.ENERGY) > 0) {
result.put(pl, CounterType.ENERGY);
}
}
return result;
}
public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional) {
if (sa == null || sa.getApi() == null) {
throw new UnsupportedOperationException();
}
CardCollection result = new CardCollection();
if (sa.hasParam("AIMaxAmount")) {
max = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("AIMaxAmount"), sa);
}
switch(sa.getApi()) {
case TwoPiles:
// TODO: improve AI
@@ -1904,7 +1886,7 @@ public class AiController {
default:
CardCollection editablePool = new CardCollection(pool);
for (int i = 0; i < max; i++) {
Card c = player.getController().chooseSingleEntityForEffect(editablePool, sa, null, isOptional, params);
Card c = player.getController().chooseSingleEntityForEffect(editablePool, sa, null, isOptional);
if (c != null) {
result.add(c);
editablePool.remove(c);
@@ -1915,7 +1897,7 @@ public class AiController {
// Special case for Bow to My Command which simulates a complex tap cost via ChooseCard
// TODO: consider enhancing support for tapXType<Any/...> in UnlessCost to get rid of this hack
if ("BowToMyCommand".equals(sa.getParam("AILogic"))) {
if (!sa.getHostCard().isInZone(ZoneType.Command)) {
if (!sa.getHostCard().getZone().is(ZoneType.Command)) {
// Make sure that other opponents do not tap for an already abandoned scheme
result.clear();
break;
@@ -2055,35 +2037,6 @@ public class AiController {
return MyRandom.getRandom().nextBoolean();
}
public boolean chooseEvenOdd(SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("AlwaysEven")) {
return false; // false is Even
} else if (aiLogic.equals("AlwaysOdd")) {
return true; // true is Odd
} else if (aiLogic.equals("Random")) {
return MyRandom.getRandom().nextBoolean();
} else if (aiLogic.equals("CMCInHand")) {
CardCollectionView hand = sa.getActivatingPlayer().getCardsIn(ZoneType.Hand);
int numEven = CardLists.filter(hand, CardPredicates.evenCMC()).size();
int numOdd = CardLists.filter(hand, CardPredicates.oddCMC()).size();
return numOdd > numEven;
} else if (aiLogic.equals("CMCOppControls")) {
CardCollectionView hand = sa.getActivatingPlayer().getOpponents().getCardsIn(ZoneType.Battlefield);
int numEven = CardLists.filter(hand, CardPredicates.evenCMC()).size();
int numOdd = CardLists.filter(hand, CardPredicates.oddCMC()).size();
return numOdd > numEven;
} else if (aiLogic.equals("CMCOppControlsByPower")) {
// TODO: improve this to check for how dangerous those creatures actually are relative to host card
CardCollectionView hand = sa.getActivatingPlayer().getOpponents().getCardsIn(ZoneType.Battlefield);
int powerEven = Aggregates.sum(CardLists.filter(hand, CardPredicates.evenCMC()), Accessors.fnGetNetPower);
int powerOdd = Aggregates.sum(CardLists.filter(hand, CardPredicates.oddCMC()), Accessors.fnGetNetPower);
return powerOdd > powerEven;
}
return MyRandom.getRandom().nextBoolean(); // outside of any specific logic, choose randomly
}
public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa,
CardCollection fetchList, Player player2, Player decider) {
if (useSimulation) {
@@ -2169,9 +2122,9 @@ public class AiController {
return filterList(list, CardTraitPredicates.hasParam("AiLogic", logic));
}
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
if (simPicker != null) {
return simPicker.chooseModeForAbility(sa, possible, min, num, allowRepeat);
return simPicker.chooseModeForAbility(sa, min, num, allowRepeat);
}
return null;
}
@@ -2187,22 +2140,24 @@ public class AiController {
// AI-specific restrictions specified as activation parameters in spell abilities
if (sa.hasParam("AILifeThreshold")) {
return player.getLife() > Integer.parseInt(sa.getParam("AILifeThreshold"));
if (player.getLife() <= Integer.parseInt(sa.getParam("AILifeThreshold"))) {
return false;
}
}
return true;
}
public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> list) {
public ReplacementEffect chooseSingleReplacementEffect(List<ReplacementEffect> list,
Map<String, Object> runParams) {
// no need to choose anything
if (list.size() <= 1) {
return Iterables.getFirst(list, null);
}
ReplacementType mode = Iterables.getFirst(list, null).getMode();
if (runParams.containsKey("Event")) {
// replace lifegain effects
if (mode.equals(ReplacementType.GainLife)) {
if ("GainLife".equals(runParams.get("Event"))) {
List<ReplacementEffect> noGain = filterListByAiLogic(list, "NoLife");
List<ReplacementEffect> loseLife = filterListByAiLogic(list, "LoseLife");
List<ReplacementEffect> doubleLife = filterListByAiLogic(list, "DoubleLife");
@@ -2221,7 +2176,7 @@ public class AiController {
// other than that, do double life
return Iterables.getFirst(doubleLife, null);
}
} else if (mode.equals(ReplacementType.DamageDone)) {
} else if ("DamageDone".equals(runParams.get("Event"))) {
List<ReplacementEffect> prevention = filterList(list, CardTraitPredicates.hasParam("Prevention"));
// TODO when Protection is done as ReplacementEffect do them
@@ -2230,6 +2185,7 @@ public class AiController {
return Iterables.getFirst(prevention, null);
}
}
}
// AI logic for choosing which replacement effect to apply
// happens here.

View File

@@ -1,64 +1,27 @@
package forge.ai;
import static forge.ai.ComputerUtilCard.getBestCreatureAI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.CardType;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterType;
import forge.game.cost.CostAddMana;
import forge.game.cost.CostChooseCreatureType;
import forge.game.cost.CostDamage;
import forge.game.cost.CostDecisionMakerBase;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostDraw;
import forge.game.cost.CostExert;
import forge.game.cost.CostExile;
import forge.game.cost.CostExileFromStack;
import forge.game.cost.CostExiledMoveToGrave;
import forge.game.cost.CostFlipCoin;
import forge.game.cost.CostGainControl;
import forge.game.cost.CostGainLife;
import forge.game.cost.CostMill;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostPayLife;
import forge.game.cost.CostPutCardToLib;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveAnyCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostReturn;
import forge.game.cost.CostReveal;
import forge.game.cost.CostSacrifice;
import forge.game.cost.CostTap;
import forge.game.cost.CostTapType;
import forge.game.cost.CostUnattach;
import forge.game.cost.CostUntap;
import forge.game.cost.CostUntapType;
import forge.game.cost.PaymentDecision;
import forge.game.keyword.Keyword;
import forge.game.cost.*;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Localizer;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
@@ -92,15 +55,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostChooseCreatureType cost) {
Integer amount = cost.convertAmount();
Iterable<String> choices = player.getController().chooseSomeType(
Localizer.getInstance().getMessage("lblCreature"), ability, amount, amount, Lists.newArrayList(CardType.getAllCreatureTypes()));
if (choices == null || Iterables.isEmpty(choices)) {
return null;
}
return PaymentDecision.types(choices);
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(),
Lists.<String>newArrayList());
return PaymentDecision.type(choice);
}
@Override
@@ -123,7 +80,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
else if (type.equals("Hand")) {
if (hand.size() > 1 && ability.getActivatingPlayer() != null) {
hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard, ability);
hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard);
}
return PaymentDecision.card(hand);
}
@@ -133,34 +90,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
if (sVar.equals("XChoice")) {
return null;
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
if (type.equals("Random")) {
CardCollectionView randomSubset = CardLists.getRandomSubList(new CardCollection(hand), c);
if (randomSubset.size() > 1 && ability.getActivatingPlayer() != null) {
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard);
}
return PaymentDecision.card(randomSubset);
}
else if (type.equals("DifferentNames")) {
CardCollection differentNames = new CardCollection();
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
while (c > 0) {
Card chosen;
if (!discardMe.isEmpty()) {
chosen = Aggregates.random(discardMe);
discardMe = CardLists.filter(discardMe, Predicates.not(CardPredicates.sharesNameWith(chosen)));
} else {
final Card worst = ComputerUtilCard.getWorstAI(hand);
chosen = worst != null ? worst : Aggregates.random(hand);
}
differentNames.add(chosen);
hand = CardLists.filter(hand, Predicates.not(CardPredicates.sharesNameWith(chosen)));
c--;
}
return PaymentDecision.card(differentNames);
}
else {
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
@@ -177,8 +120,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
return null; // cannot pay
} else {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
}
return PaymentDecision.number(c);
}
@@ -209,6 +158,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
return null;
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
@@ -220,7 +174,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
else {
CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability);
CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c);
return null == chosen ? null : PaymentDecision.card(chosen);
}
}
@@ -230,6 +184,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
return null;
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
List<SpellAbility> chosen = Lists.newArrayList();
@@ -275,6 +234,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
if (c == null) {
if (ability.getSVar(cost.getAmount()).equals("XChoice")) {
return null;
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
@@ -297,6 +260,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
public PaymentDecision visit(CostFlipCoin cost) {
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
return null;
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
return PaymentDecision.number(c);
@@ -351,11 +319,17 @@ public class AiCostDecision extends CostDecisionMakerBase {
public PaymentDecision visit(CostMill cost) {
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
return null;
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
CardCollectionView topLib = player.getCardsIn(ZoneType.Library, c);
return topLib.size() < c ? null : PaymentDecision.number(c);
return topLib.size() < c ? null : PaymentDecision.card(topLib);
}
@Override
@@ -367,9 +341,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
public PaymentDecision visit(CostPayLife cost) {
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
if (source.getName().equals("Maralen of the Mornsong Avatar")) {
return PaymentDecision.number(2);
}
if (source.getName().equals("Necrologia")) {
return PaymentDecision.number(Integer.parseInt(ability.getSVar("ChosenX")));
}
return null;
} else {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
}
if (!player.canPayLife(c)) {
return null;
}
@@ -381,8 +366,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
public PaymentDecision visit(CostPayEnergy cost) {
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
return null;
} else {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
}
if (!player.canPayEnergy(c)) {
return null;
}
@@ -408,6 +399,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
// Generalize cost
if (sVar.equals("XChoice")) {
return null;
}
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
@@ -427,7 +423,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
chosen = chosen.subList(0, c);
}
else {
chosen = ComputerUtil.choosePutToLibraryFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability);
chosen = ComputerUtil.choosePutToLibraryFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c);
}
return chosen.isEmpty() ? null : PaymentDecision.card(chosen);
}
@@ -467,9 +463,25 @@ public class AiCostDecision extends CostDecisionMakerBase {
CardCollection exclude = new CardCollection();
exclude.addAll(tapped);
if (c == null && !isVehicle) {
if (c == null) {
final String sVar = ability.getSVar(amount);
if (sVar.equals("XChoice")) {
CardCollectionView typeList =
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"),
ability.getActivatingPlayer(), ability.getHostCard(), ability);
typeList = CardLists.filter(typeList, Presets.UNTAPPED);
c = typeList.size();
// account for the fact that the activated card may be tapped in the process
if (ability.getPayCosts().hasTapCost() && typeList.contains(ability.getHostCard())) {
c--;
}
source.setSVar("ChosenX", "Number$" + Integer.toString(c));
} else {
if (!isVehicle) {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
}
}
if (type.contains("sharesCreatureTypeWith")) {
return null;
}
@@ -482,7 +494,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public boolean apply(Card card) {
for (final SpellAbility sa : card.getSpellAbilities()) {
if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) {
if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
return true;
}
}
@@ -499,7 +511,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), exclude);
} else {
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, exclude, ability);
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, exclude);
}
if (totap == null) {
@@ -516,19 +528,34 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (cost.payCostFromSource()) {
return PaymentDecision.card(source);
}
if (cost.getType().equals("OriginalHost")) {
return PaymentDecision.card(ability.getHostCard());
}
if (cost.getAmount().equals("All")) {
// Does the AI want to use Sacrifice All?
return null;
}
final String amount = cost.getAmount();
Integer c = cost.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, amount, ability);
if (ability.getSVar(cost.getAmount()).equals("XChoice")) {
String logic = ability.getParamOrDefault("AILogic", "");
if ("SacToReduceCost".equals(logic)) {
// e.g. Torgaar, Famine Incarnate
// TODO: currently returns an empty list, so the AI doesn't sacrifice anything. Trying to make
// the AI decide on creatures to sac makes the AI sacrifice them, but the cost is not reduced and the
// AI pays the full mana cost anyway (despite sacrificing creatures).
return PaymentDecision.card(new CardCollection());
} else if (!logic.isEmpty() && !logic.equals("Never")) {
// If at least some other AI logic is specified, assume that the AI for that API knows how
// to define ChosenX and thus honor that value.
// Cards which have no special logic for this yet but which do work in a simple/suboptimal way
// are currently conventionally flagged with AILogic$ DoSacrifice.
c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null);
} else {
// Other cards are assumed to be flagged AI:RemoveDeck:All for now
return null;
}
} else {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
}
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);
@@ -545,14 +572,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
CardCollectionView res = ComputerUtil.chooseReturnType(player, cost.getType(), source, ability.getTargetCard(), c, ability);
CardCollectionView res = ComputerUtil.chooseReturnType(player, cost.getType(), source, ability.getTargetCard(), c);
return res.isEmpty() ? null : PaymentDecision.card(res);
}
@Override
public PaymentDecision visit(CostReveal cost) {
final String type = cost.getType();
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
if (cost.payCostFromSource()) {
if (!hand.contains(source)) {
@@ -569,176 +596,75 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
if (cost.getRevealFrom().equals(ZoneType.Exile)) {
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
return PaymentDecision.card(getBestCreatureAI(hand));
}
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(cost.getAmount());
if (sVar.equals("XChoice")) {
c = hand.size();
} else {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
}
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
return PaymentDecision.card(aic.getCardsToDiscard(c, type.split(";"), ability));
}
protected int removeCounter(GameEntityCounterTable table, List<Card> prefs, CounterEnumType cType, int stillToRemove) {
int removed = 0;
if (!prefs.isEmpty() && stillToRemove > 0) {
Collections.sort(prefs, CardPredicates.compareByCounterType(cType));
for (Card prefCard : prefs) {
// already enough removed
if (stillToRemove <= removed) {
break;
}
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
removed += thisRemove;
table.put(prefCard, CounterType.get(cType), thisRemove);
}
}
}
return removed;
}
@Override
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final String amount = cost.getAmount();
final int c = AbilityUtils.calculateAmount(source, amount, ability);
final Card originalHost = ability.getOriginalOrHost();
final String type = cost.getType();
if (c <= 0) {
return null;
}
CardCollectionView typeList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, source, ability);
// only cards with counters are of interest
typeList = CardLists.filter(typeList, CardPredicates.hasCounters());
CardCollectionView typeList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"), player, source, ability);
// no target
if (typeList.isEmpty()) {
return null;
}
// TODO fill up a GameEntityCounterTable
// cost now has counter type or null
// the amount might be different from 1, could be X
// currently if amount is bigger than one,
// it tries to remove all counters from one source and type at once
int toRemove = 0;
final GameEntityCounterTable table = new GameEntityCounterTable();
// currently the only one using remove any counter using a type uses p1p1
// the first things are benefit from removing counters
// try to remove -1/-1 counter from persist creature
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.M1M1))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.M1M1), CardPredicates.hasKeyword(Keyword.PERSIST));
toRemove += removeCounter(table, prefs, CounterEnumType.M1M1, c - toRemove);
}
// try to remove +1/+1 counter from undying creature
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.P1P1))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.P1P1), CardPredicates.hasKeyword(Keyword.UNDYING));
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.P1P1, c),
CardPredicates.hasKeyword("Undying"));
toRemove += removeCounter(table, prefs, CounterEnumType.P1P1, c - toRemove);
if (!prefs.isEmpty()) {
Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.P1P1));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.P1P1;
return result;
}
if (c > toRemove && cost.counter == null && originalHost.hasSVar("AIRemoveCounterCostPriority") && !"ANY".equalsIgnoreCase(originalHost.getSVar("AIRemoveCounterCostPriority"))) {
String[] counters = TextUtil.split(originalHost.getSVar("AIRemoveCounterCostPriority"), ',');
// try to remove -1/-1 counter from persist creature
prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.M1M1, c),
CardPredicates.hasKeyword("Persist"));
for (final String ctr : counters) {
CounterType ctype = CounterType.getType(ctr);
// ctype == null means any type
// any type is just used to return null for this
for (Card card : CardLists.filter(typeList, CardPredicates.hasCounter(ctype))) {
int thisRemove = Math.min(card.getCounters(ctype), c - toRemove);
if (thisRemove > 0) {
toRemove += thisRemove;
table.put(card, ctype, thisRemove);
}
}
}
if (!prefs.isEmpty()) {
Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.M1M1));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.M1M1;
return result;
}
// filter for negative counters
if (c > toRemove && cost.counter == null) {
List<Card> negatives = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (CounterType cType : table.filterToRemove(crd).keySet()) {
if (ComputerUtil.isNegativeCounter(cType, crd)) {
return true;
}
}
return false;
}
});
// try to remove Time counter from Chronozoa, it will generate more
prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.TIME, c),
CardPredicates.nameEquals("Chronozoa"));
if (!negatives.isEmpty()) {
// TODO sort negatives to remove from best Cards first?
for (final Card crd : negatives) {
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(crd).entrySet()) {
if (ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
int over = Math.min(e.getValue(), c - toRemove);
if (over > 0) {
toRemove += over;
table.put(crd, e.getKey(), over);
}
}
}
}
}
}
// filter for useless counters
// they have no effect on the card, if they are there or removed
if (c > toRemove && cost.counter == null) {
List<Card> useless = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (CounterType ctype : table.filterToRemove(crd).keySet()) {
if (ComputerUtil.isUselessCounter(ctype, crd)) {
return true;
}
}
return false;
}
});
if (!useless.isEmpty()) {
for (final Card crd : useless) {
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(crd).entrySet()) {
if (ComputerUtil.isUselessCounter(e.getKey(), crd)) {
int over = Math.min(e.getValue(), c - toRemove);
if (over > 0) {
toRemove += over;
table.put(crd, e.getKey(), over);
}
}
}
}
}
}
// try to remove Time counter from Chronozoa, it will generate more token
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.TIME))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.TIME), CardPredicates.nameEquals("Chronozoa"));
toRemove += removeCounter(table, prefs, CounterEnumType.TIME, c - toRemove);
if (!prefs.isEmpty()) {
Collections.sort(prefs, CardPredicates.compareByCounterType(CounterType.TIME));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.TIME;
return result;
}
// try to remove Quest counter on something with enough counters for the
// effect to continue
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.QUEST))) {
List<Card> prefs = CardLists.filter(typeList, new Predicate<Card>() {
prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterType.QUEST, c));
if (!prefs.isEmpty()) {
prefs = CardLists.filter(prefs, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
// a Card without MaxQuestEffect doesn't need any Quest
@@ -747,65 +673,130 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (crd.hasSVar("MaxQuestEffect")) {
e = Integer.parseInt(crd.getSVar("MaxQuestEffect"));
}
return crd.getCounters(CounterEnumType.QUEST) > e;
return crd.getCounters(CounterType.QUEST) >= e + c;
}
});
Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST)));
for (final Card crd : prefs) {
int e = 0;
if (crd.hasSVar("MaxQuestEffect")) {
e = Integer.parseInt(crd.getSVar("MaxQuestEffect"));
}
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) {
toRemove += over;
table.put(crd, CounterType.get(CounterEnumType.QUEST), over);
}
}
Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterType.QUEST)));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.QUEST;
return result;
}
// remove Lore counters from Sagas to keep them longer
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.LORE))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.LORE), CardPredicates.isType("Saga"));
// TODO add Svars and other stuff to keep the Sagas on specific levels
// also add a way for the AI to respond to the last Chapter ability to keep the Saga on the field if wanted
toRemove += removeCounter(table, prefs, CounterEnumType.LORE, c - toRemove);
// filter for only cards with enough counters
typeList = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Integer i : crd.getCounters().values()) {
if (i >= c) {
return true;
}
}
return false;
}
});
// nothing with enough counters of any type
if (typeList.isEmpty()) {
return null;
}
// filter for negative counters
List<Card> negatives = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
return true;
}
}
return false;
}
});
// TODO add logic to remove positive counters?
if (c > toRemove && cost.counter != null) {
// TODO add logic for Ooze Flux, should probably try to make a token as big as possible
// without killing own non undying creatures in the process
// the amount of X should probably be tweaked for this
List<Card> withCtr = CardLists.filter(typeList, CardPredicates.hasCounter(cost.counter));
for (Card card : withCtr) {
int thisRemove = Math.min(card.getCounters(cost.counter), c - toRemove);
if (thisRemove > 0) {
toRemove += thisRemove;
table.put(card, cost.counter, thisRemove);
if (!negatives.isEmpty()) {
final Card card = ComputerUtilCard.getBestAI(negatives);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isNegativeCounter(e.getKey(), card)) {
result.ct = e.getKey();
break;
}
}
return result;
}
// Used to not return null
// Special part for CostPriority Any
if (c > toRemove && cost.counter == null && originalHost.hasSVar("AIRemoveCounterCostPriority") && "ANY".equalsIgnoreCase(originalHost.getSVar("AIRemoveCounterCostPriority"))) {
for (Card card : typeList) {
// TODO try not to remove to much positive counters from the same card
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(card).entrySet()) {
int thisRemove = Math.min(e.getValue(), c - toRemove);
if (thisRemove > 0) {
toRemove += thisRemove;
table.put(card, e.getKey(), thisRemove);
// filter for useless counters
// they have no effect on the card, if they are there or removed
List<Card> useless = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isUselessCounter(e.getKey())) {
return true;
}
}
return false;
}
});
if (!useless.isEmpty()) {
final Card card = useless.get(0);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isUselessCounter(e.getKey())) {
result.ct = e.getKey();
break;
}
}
return result;
}
// if table is empty, than no counter was removed
return table.isEmpty() ? null : PaymentDecision.counters(table);
// try a way to pay unless cost
if ("Chisei, Heart of Oceans".equals(ComputerUtilAbility.getAbilitySourceName(ability))) {
final Card card = ComputerUtilCard.getWorstAI(typeList);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c) {
result.ct = e.getKey();
break;
}
}
return result;
}
// check if the card defines its own priorities for counter removal as cost
if (source.hasSVar("AIRemoveCounterCostPriority")) {
String[] counters = TextUtil.split(source.getSVar("AIRemoveCounterCostPriority"), ',');
for (final String ctr : counters) {
List<Card> withCtr = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey() == CounterType.valueOf(ctr))) {
return true;
}
}
return false;
}
});
if (!withCtr.isEmpty()) {
final Card card = withCtr.get(0);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey() == CounterType.valueOf(ctr))) {
result.ct = e.getKey();
break;
}
}
return result;
}
}
}
return null;
}
@Override
@@ -816,11 +807,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (c == null) {
final String sVar = ability.getSVar(amount);
if (amount.equals("All")) {
if (sVar.equals("XChoice")) {
c = AbilityUtils.calculateAmount(source, "ChosenX", ability);
source.setSVar("ChosenX", "Number$" + String.valueOf(c));
} else if (amount.equals("All")) {
c = source.getCounters(cost.counter);
} else if (sVar.equals("Targeted$CardManaCost")) {
c = 0;
if (ability.getTargets().size() > 0) {
if (ability.getTargets().getNumTargeted() > 0) {
for (Card tgt : ability.getTargets().getTargetCards()) {
if (tgt.getManaCost() != null) {
c += tgt.getManaCost().getCMC();
@@ -860,10 +854,22 @@ public class AiCostDecision extends CostDecisionMakerBase {
final String amount = cost.getAmount();
Integer c = cost.convertAmount();
if (c == null) {
final String sVar = ability.getSVar(amount);
if (sVar.equals("XChoice")) {
CardCollection typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Battlefield),
cost.getType().split(";"), player, ability.getHostCard(), ability);
if (!cost.canUntapSource) {
typeList.remove(source);
}
typeList = CardLists.filter(typeList, Presets.TAPPED);
c = typeList.size();
source.setSVar("ChosenX", "Number$" + Integer.toString(c));
} else {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
}
CardCollectionView list = ComputerUtil.chooseUntapType(player, cost.getType(), source, cost.canUntapSource, c, ability);
CardCollectionView list = ComputerUtil.chooseUntapType(player, cost.getType(), source, cost.canUntapSource, c);
if (list == null) {
System.out.println("Couldn't find a valid card to untap for: " + source.getName());

View File

@@ -17,5 +17,5 @@ public enum AiPlayDecision {
WouldBecomeZeroToughnessCreature,
WouldDestroyWorldEnchantment,
BadEtbEffects,
CurseEffects
CurseEffects;
}

View File

@@ -17,19 +17,19 @@
*/
package forge.ai;
import forge.LobbyPlayer;
import forge.util.Aggregates;
import forge.util.FileUtil;
import forge.util.TextUtil;
import org.apache.commons.lang3.ArrayUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import forge.LobbyPlayer;
import forge.util.Aggregates;
import forge.util.FileUtil;
import forge.util.TextUtil;
/**
* Holds default AI personality profile values in an enum.
* Loads profile from the given text file when setProfile is called.
@@ -39,7 +39,7 @@ import forge.util.TextUtil;
* @version $Id: AIProfile.java 20169 2013-03-08 08:24:17Z Agetian $
*/
public class AiProfileUtil {
private static Map<String, Map<AiProps, String>> loadedProfiles = new HashMap<>();
private static Map<String, Map<AiProps, String>> loadedProfiles = new HashMap<String, Map<AiProps, String>>();
private static String AI_PROFILE_DIR;
private static final String AI_PROFILE_EXT = ".ai";
@@ -74,7 +74,7 @@ public class AiProfileUtil {
* @param profileName a profile to load.
*/
private static final Map<AiProps, String> loadProfile(final String profileName) {
Map<AiProps, String> profileMap = new HashMap<>();
Map<AiProps, String> profileMap = new HashMap<AiProps, String>();
List<String> lines = FileUtil.readFile(buildFileName(profileName));
for (String line : lines) {
@@ -122,7 +122,7 @@ public class AiProfileUtil {
*/
public static List<String> getAvailableProfiles()
{
final List<String> availableProfiles = new ArrayList<>();
final List<String> availableProfiles = new ArrayList<String>();
final File dir = new File(AI_PROFILE_DIR);
final String[] children = dir.list();
@@ -146,7 +146,7 @@ public class AiProfileUtil {
* available profiles including special random profile tags.
*/
public static List<String> getProfilesDisplayList() {
final List<String> availableProfiles = new ArrayList<>();
final List<String> availableProfiles = new ArrayList<String>();
availableProfiles.add(AI_PROFILE_RANDOM_MATCH);
availableProfiles.add(AI_PROFILE_RANDOM_DUEL);
availableProfiles.addAll(getAvailableProfiles());

View File

@@ -35,7 +35,6 @@ public enum AiProps { /** */
MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"),
MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"),
PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"),
SAC_TO_REATTACH_TARGET_EVAL_THRESHOLD ("400"),
PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */
RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */
PLAY_AGGRO ("false"),
@@ -75,7 +74,6 @@ public enum AiProps { /** */
ALWAYS_COPY_SPELL_IF_CMC_DIFF ("2"), /** */
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
ACTIVELY_PROTECT_VS_CURSE_AURAS("false"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
@@ -131,10 +129,7 @@ public enum AiProps { /** */
FLASH_USE_BUFF_AURAS_AS_COMBAT_TRICKS("true"),
FLASH_BUFF_AURA_CHANCE_TO_CAST_EARLY("1"),
FLASH_BUFF_AURA_CHANCE_CAST_AT_EOT("5"),
FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"),
BLINK_RELOAD_PLANESWALKER_CHANCE("30"), /** */
BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY("2"), /** */
BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"); /** */
FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"); /** */
// Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
// <-- There are no experimental options here -->

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,7 @@
package forge.ai;
import java.util.Iterator;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GameActionUtil;
@@ -21,6 +17,9 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import java.util.Iterator;
import java.util.List;
public class ComputerUtilAbility {
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
@@ -82,28 +81,31 @@ public class ComputerUtilAbility {
public static List<SpellAbility> getSpellAbilities(final CardCollectionView l, final Player player) {
final List<SpellAbility> spellAbilities = Lists.newArrayList();
for (final Card c : l) {
spellAbilities.addAll(c.getAllPossibleAbilities(player, false));
for (final SpellAbility sa : c.getSpellAbilities()) {
spellAbilities.add(sa);
}
if (c.isFaceDown() && c.isInZone(ZoneType.Exile) && !c.mayPlay(player).isEmpty()) {
for (final SpellAbility sa : c.getState(CardStateName.Original).getSpellAbilities()) {
spellAbilities.add(sa);
}
}
}
return spellAbilities;
}
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
final List<SpellAbility> newAbilities = Lists.newArrayList();
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
for (SpellAbility sa : originList) {
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
sa.setActivatingPlayer(player);
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
}
for (SpellAbility sa : originListWithAddCosts) {
// determine which alternative costs are cheaper than the original and prioritize them
List<SpellAbility> saAltCosts = GameActionUtil.getAlternativeCosts(sa, player);
List<SpellAbility> priorityAltSa = Lists.newArrayList();
List<SpellAbility> otherAltSa = Lists.newArrayList();
for (SpellAbility altSa : saAltCosts) {
if (sa.getPayCosts().isOnlyManaCost()
if (altSa.getPayCosts() == null || sa.getPayCosts() == null) {
otherAltSa.add(altSa);
} else if (sa.getPayCosts().isOnlyManaCost()
&& altSa.getPayCosts().isOnlyManaCost() && sa.getPayCosts().getTotalMana().compareTo(altSa.getPayCosts().getTotalMana()) == 1) {
// the alternative cost is strictly cheaper, so why not? (e.g. Omniscience etc.)
priorityAltSa.add(altSa);
@@ -171,8 +173,12 @@ public class ComputerUtilAbility {
return sa;
}
public static Card getAbilitySource(SpellAbility sa) {
return sa.getOriginalHost() != null ? sa.getOriginalHost() : sa.getHostCard();
}
public static String getAbilitySourceName(SpellAbility sa) {
final Card c = sa.getOriginalOrHost();
final Card c = getAbilitySource(sa);
return c != null ? c.getName() : "";
}

View File

@@ -1,27 +1,11 @@
package forge.ai;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.card.CardStateName;
import forge.card.CardType;
import forge.card.ColorSet;
import forge.card.MagicColor;
@@ -31,18 +15,10 @@ import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
@@ -64,6 +40,12 @@ import forge.item.PaperCard;
import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
public class ComputerUtilCard {
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
@@ -190,22 +172,21 @@ public class ComputerUtilCard {
// if no non-basic lands, target the least represented basic land type
String sminBL = "";
int iminBL = Integer.MAX_VALUE;
int iminBL = 20000; // hopefully no one will ever have more than 20000
// lands of one type....
int n = 0;
for (String name : MagicColor.Constant.BASIC_LANDS) {
n = CardLists.getType(land, name).size();
if (n < iminBL && n > 0) {
if ((n < iminBL) && (n > 0)) {
// if two or more are tied, only the
// first
// one checked will be used
iminBL = n;
sminBL = name;
}
}
if (iminBL == Integer.MAX_VALUE) {
// All basic lands have no basic land type. Just return something
Iterator<Card> untapped = Iterables.filter(land, CardPredicates.Presets.UNTAPPED).iterator();
if (untapped.hasNext()) {
return untapped.next();
}
return land.get(0);
if (iminBL == 20000) {
return null; // no basic land was a minimum
}
final List<Card> bLand = CardLists.getType(land, sminBL);
@@ -214,6 +195,7 @@ public class ComputerUtilCard {
return ut;
}
return Aggregates.random(bLand); // random tapped land of least represented type
}
@@ -246,7 +228,7 @@ public class ComputerUtilCard {
Card cheapest = null;
for (Card c : all) {
if (cheapest == null || c.getManaCost().getCMC() <= cheapest.getManaCost().getCMC()) {
if (cheapest == null || cheapest.getManaCost().getCMC() <= cheapest.getManaCost().getCMC()) {
cheapest = c;
}
}
@@ -327,17 +309,6 @@ public class ComputerUtilCard {
return biggest;
}
// For ability of Oracle en-Vec, return the first card that are going to attack next turn
public static Card getBestCreatureToAttackNextTurnAI(final Player aiPlayer, final Iterable<Card> list) {
AiController aic = ((PlayerControllerAi)aiPlayer.getController()).getAi();
for(final Card card : list) {
if (aic.getPredictedCombatNextTurn().isAttacking(card)) {
return card;
}
}
return null;
}
/**
* <p>
* getWorstAI.
@@ -397,7 +368,7 @@ public class ComputerUtilCard {
}
if (hasEnchantmants || hasArtifacts) {
final List<Card> ae = CardLists.filter(list, Predicates.and(Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS), new Predicate<Card>() {
final List<Card> ae = CardLists.filter(list, Predicates.and(Predicates.<Card>or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return !card.hasSVar("DoNotDiscardIfAble");
@@ -550,7 +521,7 @@ public class ComputerUtilCard {
*/
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
AiBlockController aiBlk = new AiBlockController(ai);
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
Combat combat = new Combat(opp);
//Use actual attackers if available, else consider all possible attackers
Combat currentCombat = ai.getGame().getCombat();
@@ -593,7 +564,7 @@ public class ComputerUtilCard {
AiBlockController aiBlk = new AiBlockController(ai);
Combat combat = new Combat(ai);
combat.addAttacker(attacker, ai);
final List<Card> attackers = new ArrayList<>();
final List<Card> attackers = new ArrayList<Card>();
attackers.add(attacker);
aiBlk.assignBlockersGivenAttackers(combat, attackers);
return ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, combat);
@@ -672,7 +643,7 @@ public class ComputerUtilCard {
return getMostProminentType(list, CardType.getAllCreatureTypes());
}
public static String getMostProminentType(final CardCollectionView list, final Collection<String> valid) {
public static String getMostProminentType(final CardCollectionView list, final List<String> valid) {
if (list.size() == 0) {
return "";
}
@@ -727,12 +698,31 @@ public class ComputerUtilCard {
}
// same for Trigger that does make Tokens
for(Trigger t:c.getTriggers()){
SpellAbility sa = t.ensureAbility();
SpellAbility sa = t.getOverridingAbility();
String sTokenTypes = null;
if (sa != null) {
if (sa.getApi() != ApiType.Token || !sa.hasParam("TokenTypes")) {
continue;
}
for (String var : sa.getParam("TokenTypes").split(",")) {
sTokenTypes = sa.getParam("TokenTypes");
} else if (t.hasParam("Execute")) {
String name = t.getParam("Execute");
if (!c.hasSVar(name)) {
continue;
}
Map<String, String> params = AbilityFactory.getMapParams(c.getSVar(name));
if (!params.containsKey("TokenTypes")) {
continue;
}
sTokenTypes = params.get("TokenTypes");
}
if (sTokenTypes == null) {
continue;
}
for (String var : sTokenTypes.split(",")) {
if (!CardType.isACreatureType(var)) {
continue;
}
@@ -743,7 +733,6 @@ public class ComputerUtilCard {
typesInDeck.put(var, count + 1);
}
}
}
// special rule for Fabricate and Servo
if(c.hasStartOfKeyword(Keyword.FABRICATE.toString())){
Integer count = typesInDeck.get("Servo");
@@ -799,7 +788,7 @@ public class ComputerUtilCard {
public static List<String> getColorByProminence(final List<Card> list) {
int cntColors = MagicColor.WUBRG.length;
final List<Pair<Byte,Integer>> map = new ArrayList<>();
final List<Pair<Byte,Integer>> map = new ArrayList<Pair<Byte,Integer>>();
for(int i = 0; i < cntColors; i++) {
map.add(MutablePair.of(MagicColor.WUBRG[i], 0));
}
@@ -820,7 +809,7 @@ public class ComputerUtilCard {
});
// will this part be once dropped?
List<String> result = new ArrayList<>(cntColors);
List<String> result = new ArrayList<String>(cntColors);
for(Pair<Byte, Integer> idx : map) { // fetch color names in the same order
result.add(MagicColor.toLongString(idx.getKey()));
}
@@ -892,10 +881,10 @@ public class ComputerUtilCard {
}
};
public static List<String> chooseColor(SpellAbility sa, int min, int max, List<String> colorChoices) {
List<String> chosen = new ArrayList<>();
List<String> chosen = new ArrayList<String>();
Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
Player opp = ai.getWeakestOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
@@ -937,7 +926,7 @@ public class ComputerUtilCard {
}
else if (logic.equals("MostProminentInComputerDeckButGreen")) {
List<String> prominence = ComputerUtilCard.getColorByProminence(CardLists.filterControlledBy(game.getCardsInGame(), ai));
if (prominence.get(0).equals(MagicColor.Constant.GREEN)) {
if (prominence.get(0) == MagicColor.Constant.GREEN) {
chosen.add(prominence.get(1));
} else {
chosen.add(prominence.get(0));
@@ -975,22 +964,6 @@ public class ComputerUtilCard {
}
chosen.add(chosenColor);
}
else if (logic.equals("HighestDevotionToColor")) {
int curDevotion = 0;
String chosenColor = MagicColor.Constant.WHITE;
CardCollectionView hand = ai.getCardsIn(ZoneType.Hand);
for(byte c : MagicColor.WUBRG) {
String devotionCode = "Count$Devotion." + MagicColor.toLongString(c);
int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa);
if (devotion > curDevotion && !CardLists.filter(hand, CardPredicates.isColor(c)).isEmpty()) {
curDevotion = devotion;
chosenColor = MagicColor.toLongString(c);
}
}
chosen.add(chosenColor);
}
}
if (chosen.isEmpty()) {
chosen.add(MagicColor.Constant.GREEN);
@@ -1001,7 +974,7 @@ public class ComputerUtilCard {
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
final Player ai = sa.getActivatingPlayer();
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final PhaseType phaseType = ph.getPhase();
@@ -1155,14 +1128,14 @@ public class ComputerUtilCard {
// assume it either benefits the player or disrupts the opponent
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic() && !stAb.isTemporary()) {
priority = true;
break;
}
}
if (!priority) {
for (final Trigger t : c.getTriggers()) {
if (t.isIntrinsic()) {
if (t.isIntrinsic() && !t.isTemporary()) {
// has a triggered ability, could be benefitting the opponent or disrupting the AI
priority = true;
break;
@@ -1171,8 +1144,8 @@ public class ComputerUtilCard {
}
// if this thing has AILogic set to "Curse", it's probably meant as some form of disruption
if (!priority) {
for (final String value : c.getSVars().values()) {
if (value.contains("AILogic$ Curse")) {
for (final String sVar : c.getSVars().keySet()) {
if (c.getSVars().get(sVar).contains("AILogic$ Curse")) {
// this is a curse ability, so prioritize its removal
priority = true;
break;
@@ -1214,10 +1187,11 @@ public class ComputerUtilCard {
final float valueNow = Math.max(valueTempo, threat);
if (valueNow < 0.2) { //hard floor to reduce ridiculous odds for instants over time
return false;
}
} else {
final float chance = MyRandom.getRandom().nextFloat();
return chance < valueNow;
}
}
/**
* Decides if the "pump" is worthwhile
@@ -1239,7 +1213,6 @@ public class ComputerUtilCard {
final Game game = ai.getGame();
final PhaseHandler phase = game.getPhaseHandler();
final Combat combat = phase.getCombat();
final boolean main1Preferred = "Main1IfAble".equals(sa.getParam("AILogic")) && phase.is(PhaseType.MAIN1, ai);
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
final boolean loseCardAtEOT = "Sacrifice".equals(sa.getParam("AtEOT")) || "Exile".equals(sa.getParam("AtEOT"))
|| "Destroy".equals(sa.getParam("AtEOT")) || "ExileCombat".equals(sa.getParam("AtEOT"));
@@ -1277,7 +1250,7 @@ public class ComputerUtilCard {
// will the creature attack (only relevant for sorcery speed)?
if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& phase.isPlayerTurn(ai)
&& SpellAbilityAi.isSorcerySpeed(sa) || main1Preferred
&& SpellAbilityAi.isSorcerySpeed(sa)
&& power > 0
&& ComputerUtilCard.doesCreatureAttackAI(ai, c)) {
return true;
@@ -1296,7 +1269,7 @@ public class ComputerUtilCard {
}
}
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
List<Card> oppCreatures = opp.getCreaturesInPlay();
float chance = 0;
@@ -1320,14 +1293,14 @@ public class ComputerUtilCard {
// cast it during Declare Blockers, thus ruining its attacker
if (holdCombatTricks && sa.getApi() == ApiType.Pump
&& sa.hasParam("NumAtt") && sa.getHostCard() != null
&& sa.getHostCard().isInZone(ZoneType.Hand)
&& sa.getHostCard().getZone() != null && sa.getHostCard().getZone().is(ZoneType.Hand)
&& c.getNetPower() > 0 // too obvious if attacking with a 0-power creature
&& sa.getHostCard().isInstant() // only do it for instant speed spells in hand
&& ComputerUtilMana.hasEnoughManaSourcesToCast(sa, ai)) {
combatTrick = true;
final List<String> kws = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & "))
: Lists.newArrayList();
: Lists.<String>newArrayList();
for (String kw : kws) {
if (!kw.equals("Trample") && !kw.equals("First Strike") && !kw.equals("Double Strike")) {
combatTrick = false;
@@ -1433,8 +1406,8 @@ public class ComputerUtilCard {
if (combat.isAttacking(c) && opp.getLife() > 0) {
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true);
int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true);
int poisonOrig = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0;
int poisonPumped = opp.canReceiveCounters(CounterEnumType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
int poisonOrig = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0;
int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
// predict Infect
if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) {
@@ -1457,8 +1430,7 @@ public class ComputerUtilCard {
}
if (pumpedDmg > dmg) {
if ((!c.hasKeyword(Keyword.INFECT) && pumpedDmg >= opp.getLife())
|| (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterEnumType.POISON) && pumpedDmg >= opp.getPoisonCounters())
|| ("PumpForTrample".equals(sa.getParam("AILogic")))) {
|| (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
return true;
}
}
@@ -1484,10 +1456,6 @@ public class ComputerUtilCard {
}
if (totalPowerUnblocked >= opp.getLife()) {
return true;
} else if (totalPowerUnblocked > dmg && sa.getHostCard() != null && sa.getHostCard().isInPlay()) {
if (sa.getPayCosts().hasNoManaCost()) {
return true; // always activate abilities which cost no mana and which can increase unblocked damage
}
}
}
float value = 1.0f * (pumpedDmg - dmg);
@@ -1598,7 +1566,7 @@ public class ComputerUtilCard {
Card pumped = CardFactory.copyCard(c, true);
pumped.setSickness(c.hasSickness());
final long timestamp = c.getGame().getNextTimestamp();
final List<String> kws = new ArrayList<>();
final List<String> kws = new ArrayList<String>();
for (String kw : keywords) {
if (kw.startsWith("HIDDEN")) {
pumped.addHiddenExtrinsicKeyword(kw);
@@ -1628,12 +1596,12 @@ public class ComputerUtilCard {
}
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.setPTBoost(c.getPTBoostTable());
pumped.addPTBoost(power + berserkPower, toughness, timestamp, null);
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness);
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
Set<CounterType> types = c.getCounters().keySet();
for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true, null);
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true);
}
//Copies tap-state and extra keywords (auras, equipment, etc.)
if (c.isTapped()) {
@@ -1675,9 +1643,7 @@ public class ComputerUtilCard {
}
list.add(vCard); // account for the static abilities that may be present on the card itself
for (final Card c : list) {
// remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getTimestamp(), stAb.getId());
final Map<String, String> params = stAb.getMapParams();
if (!params.get("Mode").equals("Continuous")) {
continue;
@@ -1692,25 +1658,26 @@ public class ComputerUtilCard {
if (!vCard.isValid(valid, c.getController(), c, null)) {
continue;
}
int att = 0;
if (params.containsKey("AddPower")) {
String addP = params.get("AddPower");
int att = 0;
if (addP.equals("AffectedX")) {
att = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addP));
} else {
att = AbilityUtils.calculateAmount(c, addP, stAb);
}
vCard.addTempPowerBoost(att);
}
int def = 0;
if (params.containsKey("AddToughness")) {
String addT = params.get("AddToughness");
int def = 0;
if (addT.equals("AffectedY")) {
def = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addT));
} else {
def = AbilityUtils.calculateAmount(c, addT, stAb);
}
vCard.addTempToughnessBoost(def);
}
vCard.addPTBoost(att, def, c.getTimestamp(), stAb.getId());
}
}
}
@@ -1776,10 +1743,10 @@ public class ComputerUtilCard {
}
public static boolean hasActiveUndyingOrPersist(final Card c) {
if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterEnumType.P1P1) == 0) {
if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterType.P1P1) == 0) {
return true;
}
if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterEnumType.M1M1) == 0) {
if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterType.M1M1) == 0) {
return true;
}
return false;
@@ -1796,6 +1763,10 @@ public class ComputerUtilCard {
for (Card c : otb) {
for (SpellAbility sa : c.getSpellAbilities()) {
if (sa.getPayCosts() == null) {
continue;
}
CostPayEnergy energyCost = sa.getPayCosts().getCostEnergy();
if (energyCost != null) {
int amount = energyCost.convertAmount();
@@ -1867,64 +1838,36 @@ public class ComputerUtilCard {
public static AiPlayDecision checkNeedsToPlayReqs(final Card card, final SpellAbility sa) {
Game game = card.getGame();
boolean isRightSplit = sa != null && sa.getCardState().getStateName() == CardStateName.RightSplit;
boolean isRightSplit = sa != null && sa.isRightSplit();
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";
// TODO: if there are ever split cards with Evoke or Kicker, factor in the right split option above
if (sa != null) {
if (sa.isEvoke()) {
// if the spell is evoked, will use NeedsToPlayEvoked if available (otherwise falls back to NeedsToPlay)
if (card.hasSVar("NeedsToPlayEvoked")) {
needsToPlayName = "NeedsToPlayEvoked";
}
if (card.hasSVar("NeedsToPlayEvokedVar")) {
needsToPlayVarName = "NeedsToPlayEvokedVar";
}
} else if (sa.isKicked()) {
// if the spell is kicked, uses NeedsToPlayKicked if able and locks out the regular NeedsToPlay check
// for unkicked spells, uses NeedsToPlay
if (card.hasSVar("NeedsToPlayKicked")) {
needsToPlayName = "NeedsToPlayKicked";
} else {
needsToPlayName = "UNUSED";
}
if (card.hasSVar("NeedsToPlayKickedVar")) {
needsToPlayVarName = "NeedsToPlayKickedVar";
} else {
needsToPlayVarName = "UNUSED";
}
}
}
if (card.hasSVar(needsToPlayName)) {
final String needsToPlay = card.getSVar(needsToPlayName);
// A special case which checks that this creature will attack if it's the AI's turn
if (needsToPlay.equalsIgnoreCase("WillAttack")) {
if (sa != null && game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return ComputerUtilCard.doesSpecifiedCreatureAttackAI(sa.getActivatingPlayer(), card) ?
AiPlayDecision.WillPlay : AiPlayDecision.BadEtbEffects;
} else {
return AiPlayDecision.WillPlay; // not our turn, skip this check for the possible Flash use etc.
}
}
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, sa);
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
if (list.isEmpty()) {
return AiPlayDecision.MissingNeededCards;
}
}
if (card.getSVar(needsToPlayVarName).length() > 0) {
final String needsToPlay = card.getSVar(needsToPlayVarName);
int x = 0;
int y = 0;
String sVar = needsToPlay.split(" ")[0];
String comparator = needsToPlay.split(" ")[1];
String compareTo = comparator.substring(2);
int x = AbilityUtils.calculateAmount(card, sVar, sa);
int y = AbilityUtils.calculateAmount(card, compareTo, sa);
try {
x = Integer.parseInt(sVar);
} catch (final NumberFormatException e) {
x = CardFactoryUtil.xCount(card, card.getSVar(sVar));
}
try {
y = Integer.parseInt(compareTo);
} catch (final NumberFormatException e) {
y = CardFactoryUtil.xCount(card, card.getSVar(compareTo));
}
if (!Expressions.compare(x, comparator, y)) {
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
}
@@ -1941,7 +1884,4 @@ public class ComputerUtilCard {
public static boolean isCardRemRandomDeck(final Card card) {
return card.getRules() != null && card.getRules().getAiHints().getRemRandomDecks();
}
public static boolean isCardRemNonCommanderDeck(final Card card) {
return card.getRules() != null && card.getRules().getAiHints().getRemNonCommanderDecks();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,16 @@
package forge.ai;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostDamage;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayLife;
import forge.game.cost.CostPayment;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveAnyCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.cost.CostTapType;
import forge.game.cost.PaymentDecision;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.Spell;
@@ -44,6 +19,10 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Set;
public class ComputerUtilCost {
@@ -66,7 +45,7 @@ public class ComputerUtilCost {
final CostPutCounter addCounter = (CostPutCounter) part;
final CounterType type = addCounter.getCounter();
if (type.is(CounterEnumType.M1M1)) {
if (type.equals(CounterType.M1M1)) {
return false;
}
}
@@ -74,6 +53,9 @@ public class ComputerUtilCost {
return true;
}
public static boolean checkRemoveCounterCost(final Cost cost, final Card source) {
return checkRemoveCounterCost(cost, source, null);
}
/**
* Check remove counter cost.
*
@@ -87,14 +69,13 @@ public class ComputerUtilCost {
if (cost == null) {
return true;
}
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
if (type.is(CounterEnumType.P1P1)) {
if (CounterType.P1P1.equals(type)) {
return false;
}
continue;
@@ -105,8 +86,19 @@ public class ComputerUtilCost {
return false;
}
// Remove X counters - set ChosenX to max possible value here, the SAs should correct that
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
final String sVar = sa.getSVar(remCounter.getAmount());
if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) {
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
}
}
// check the sa what the PaymentDecision is.
// ignore Loyality abilities with Zero as Cost
if (!type.is(CounterEnumType.LOYALTY)) {
if (sa != null && !CounterType.LOYALTY.equals(type)) {
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa);
PaymentDecision pay = decision.visit(remCounter);
if (pay == null || pay.c <= 0) {
return false;
@@ -114,15 +106,10 @@ public class ComputerUtilCost {
}
//don't kill the creature
if (type.is(CounterEnumType.P1P1) && source.getLethalDamage() <= 1
if (CounterType.P1P1.equals(type) && source.getLethalDamage() <= 1
&& !source.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (part instanceof CostRemoveAnyCounter) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision pay = decision.visit(remCounter);
return pay != null;
}
}
return true;
@@ -137,9 +124,8 @@ public class ComputerUtilCost {
* the source
* @return true, if successful
*/
public static boolean checkDiscardCost(final Player ai, final Cost cost, final Card source, SpellAbility sa) {
if (cost == null || source.hasSVar("AISkipDiscardCostCheck") /* FIXME: should not be needed! */ ) {
public static boolean checkDiscardCost(final Player ai, final Cost cost, final Card source) {
if (cost == null) {
return true;
}
@@ -153,7 +139,7 @@ public class ComputerUtilCost {
if (type.equals("CARDNAME") && source.getAbilityText().contains("Bloodrush")) {
continue;
}
final CardCollection typeList = CardLists.getValidCards(hand, type.split(","), source.getController(), source, sa);
final CardCollection typeList = CardLists.getValidCards(hand, type.split(","), source.getController(), source, null);
if (typeList.size() > ai.getMaxHandSize()) {
continue;
}
@@ -163,12 +149,13 @@ public class ComputerUtilCost {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
if (pref == null) {
return false;
}
} else {
typeList.remove(pref);
hand.remove(pref);
}
}
}
}
return true;
}
@@ -270,7 +257,7 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
int count = 0;
while (count < amount) {
@@ -320,7 +307,7 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
int count = 0;
while (count < amount) {
@@ -428,7 +415,7 @@ public class ComputerUtilCost {
continue;
}
final int remainingLife = ai.getLife();
final int lifeCost = part.convertAmount();
final int lifeCost = ((CostPayLife) part).convertAmount();
if ((remainingLife - lifeCost) < 10) {
return false; //Don't pay life if it would put AI under 10 life
} else if ((remainingLife / lifeCost) < 4) {
@@ -471,9 +458,9 @@ public class ComputerUtilCost {
if(!meetsRestriction)
continue;
if (StringUtils.isNumeric(parts[0])) {
extraManaNeeded += Integer.parseInt(parts[0]);
} else {
try {
extraManaNeeded += Integer.parseInt(snem);
} catch (final NumberFormatException e) {
System.out.println("wrong SpellsNeedExtraMana SVar format on " + c);
}
}
@@ -484,9 +471,9 @@ public class ComputerUtilCost {
}
final String snem = c.getSVar("SpellsNeedExtraManaEffect");
if (!StringUtils.isBlank(snem)) {
if (StringUtils.isNumeric(snem)) {
try {
extraManaNeeded += Integer.parseInt(snem);
} else {
} catch (final NumberFormatException e) {
System.out.println("wrong SpellsNeedExtraManaEffect SVar format on " + c);
}
}
@@ -494,7 +481,7 @@ public class ComputerUtilCost {
}
// Try not to lose Planeswalker if not threatened
if (sa.isPwAbility()) {
if (sa.getRestrictions().isPwAbility()) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
@@ -533,7 +520,7 @@ public class ComputerUtilCost {
public boolean apply(Card card) {
boolean hasManaSa = false;
for (final SpellAbility sa : card.getSpellAbilities()) {
if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) {
if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
hasManaSa = true;
break;
}
@@ -556,7 +543,7 @@ public class ComputerUtilCost {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payOwner = sa.hasParam("UnlessAI") ? aiLogic.startsWith("Defined") : false;
boolean payNever = "Never".equals(aiLogic);
boolean shockland = "Shockland".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
@@ -598,11 +585,6 @@ public class ComputerUtilCost {
if (c == null || c.isUntapped()) {
return false;
}
} else if ("RiskFactor".equals(aiLogic)) {
final Player activator = sa.getActivatingPlayer();
if (!activator.canDraw() || activator.hasKeyword("You can't draw more than one card each turn.")) {
return false;
}
} else if ("MorePowerful".equals(aiLogic)) {
final int sourceCreatures = sa.getActivatingPlayer().getCreaturesInPlay().size();
final int payerCreatures = payer.getCreaturesInPlay().size();
@@ -623,8 +605,7 @@ public class ComputerUtilCost {
if (combat.getAttackers().isEmpty()) {
return false;
}
} else if ("nonToken".equals(aiLogic) && !AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).isEmpty()
&& AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) {
} else if ("nonToken".equals(aiLogic) && AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).get(0).isToken()) {
return false;
} else if ("LowPriority".equals(aiLogic) && MyRandom.getRandom().nextInt(100) < 67) {
return false;
@@ -641,11 +622,11 @@ public class ComputerUtilCost {
return checkLifeCost(payer, cost, source, 4, sa)
&& checkDamageCost(payer, cost, source, 4)
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source))
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
&& (!source.getName().equals("Chain of Vapor") || (ComputerUtil.getOpponentFor(payer).getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
}
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
@@ -681,62 +662,4 @@ public class ComputerUtilCost {
}
return false;
}
public static int getMaxXValue(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard();
final SpellAbility root = sa.getRootAbility();
final Cost abCost = sa.getPayCosts();
if (abCost == null || !abCost.hasXInAnyCostPart()) {
return 0;
}
Integer val = null;
if (sa.costHasManaX()) {
val = ComputerUtilMana.determineLeftoverMana(root, ai);
}
if (sa.usesTargeting()) {
// if announce is used as min targets, check what the max possible number would be
if ("X".equals(sa.getTargetRestrictions().getMinTargets())) {
val = ObjectUtils.min(val, CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa).size());
}
if (sa.hasParam("AIMaxTgtsCount")) {
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified
// TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable?
val = ObjectUtils.min(val, AbilityUtils.calculateAmount(sa.getHostCard(), "Count$" + sa.getParam("AIMaxTgtsCount"), sa));
}
}
val = ObjectUtils.min(val, abCost.getMaxForNonManaX(root, ai));
if (val != null && val > 0) {
// filter cost parts for preferences, don't choose X > than possible preferences
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostSacrifice) {
if (part.payCostFromSource()) {
continue;
}
if (!part.getAmount().equals("X")) {
continue;
}
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), part.getType().split(";"), source.getController(), source, sa);
int count = 0;
while (count < val) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
if (prefCard == null) {
break;
}
typeList.remove(prefCard);
count++;
}
val = ObjectUtils.min(val, count);
}
}
}
return ObjectUtils.defaultIfNull(val, 0);
}
}

View File

@@ -1,26 +1,8 @@
package forge.ai;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.*;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.card.MagicColor;
@@ -28,27 +10,13 @@ import forge.card.mana.ManaAtom;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
import forge.card.mana.ManaCostShard;
import forge.game.CardTraitPredicates;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.card.*;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.cost.*;
import forge.game.mana.Mana;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.mana.ManaPool;
@@ -56,17 +24,16 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
public class ComputerUtilMana {
private final static boolean DEBUG_MANA_PAYMENT = false;
@@ -98,8 +65,6 @@ public class ComputerUtilMana {
// Does not check if mana sources can be used right now, just checks for potential chance.
public static boolean hasEnoughManaSourcesToCast(final SpellAbility sa, final Player ai) {
if(ai == null || sa == null)
return false;
sa.setActivatingPlayer(ai);
return payManaCost(sa, ai, true, 0, false);
}
@@ -149,7 +114,7 @@ public class ComputerUtilMana {
return score;
}
private static void sortManaAbilities(final Multimap<ManaCostShard, SpellAbility> manaAbilityMap, final SpellAbility sa) {
private static void sortManaAbilities(final Multimap<ManaCostShard, SpellAbility> manaAbilityMap) {
final Map<Card, Integer> manaCardMap = Maps.newHashMap();
final List<Card> orderedCards = Lists.newArrayList();
@@ -204,7 +169,8 @@ public class ComputerUtilMana {
}
return ability1.compareTo(ability2);
} else {
}
else {
return preOrder;
}
}
@@ -215,58 +181,6 @@ public class ComputerUtilMana {
}
manaAbilityMap.replaceValues(shard, newAbilities);
// Sort the first N abilities so that the preferred shard is selected, e.g. Adamant
String manaPref = sa.getParamOrDefault("AIManaPref", "");
if (manaPref.isEmpty() && sa.getHostCard() != null && sa.getHostCard().hasSVar("AIManaPref")) {
manaPref = sa.getHostCard().getSVar("AIManaPref");
}
if (!manaPref.isEmpty()) {
final String[] prefShardInfo = manaPref.split(":");
final String preferredShard = prefShardInfo[0];
final int preferredShardAmount = prefShardInfo.length > 1 ? Integer.parseInt(prefShardInfo[1]) : 3;
if (!preferredShard.isEmpty()) {
final List<SpellAbility> prefSortedAbilities = new ArrayList<>(newAbilities);
final List<SpellAbility> otherSortedAbilities = new ArrayList<>(newAbilities);
Collections.sort(prefSortedAbilities, new Comparator<SpellAbility>() {
@Override
public int compare(final SpellAbility ability1, final SpellAbility ability2) {
if (ability1.getManaPart().mana().contains(preferredShard))
return -1;
else if (ability2.getManaPart().mana().contains(preferredShard))
return 1;
return 0;
}
});
Collections.sort(otherSortedAbilities, new Comparator<SpellAbility>() {
@Override
public int compare(final SpellAbility ability1, final SpellAbility ability2) {
if (ability1.getManaPart().mana().contains(preferredShard))
return 1;
else if (ability2.getManaPart().mana().contains(preferredShard))
return -1;
return 0;
}
});
final List<SpellAbility> finalAbilities = new ArrayList<>();
for (int i = 0; i < preferredShardAmount && i < prefSortedAbilities.size(); i++) {
finalAbilities.add(prefSortedAbilities.get(i));
}
for (int i = 0; i < otherSortedAbilities.size(); i++) {
SpellAbility ab = otherSortedAbilities.get(i);
if (!finalAbilities.contains(ab))
finalAbilities.add(ab);
}
manaAbilityMap.replaceValues(shard, finalAbilities);
}
}
}
}
@@ -319,8 +233,8 @@ public class ComputerUtilMana {
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
// to attempt to make the spell uncounterable when possible.
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls") && ma.hasChosenType()
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getChosenType(0))) {
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
continue;
@@ -348,189 +262,6 @@ public class ComputerUtilMana {
return null;
}
public static String predictManaReplacement(SpellAbility saPayment, Player ai, ManaCostShard toPay) {
Card hostCard = saPayment.getHostCard();
Game game = hostCard.getGame();
String manaProduced = toPay.isSnow() && hostCard.isSnow() ? "S" : GameActionUtil.generatedTotalMana(saPayment);
//String originalProduced = manaProduced;
final Map<AbilityKey, Object> repParams = AbilityKey.newMap();
repParams.put(AbilityKey.Mana, manaProduced);
repParams.put(AbilityKey.Affected, hostCard);
repParams.put(AbilityKey.Player, ai);
repParams.put(AbilityKey.AbilityMana, saPayment); // RootAbility
// TODO Damping Sphere might replace later?
// add flags to replacementEffects to filter better?
List<ReplacementEffect> reList = game.getReplacementHandler().getReplacementList(ReplacementType.ProduceMana, repParams, ReplacementLayer.Other);
List<SpellAbility> replaceMana = Lists.newArrayList();
List<SpellAbility> replaceType = Lists.newArrayList();
List<SpellAbility> replaceAmount = Lists.newArrayList(); // currently only multi
// try to guess the color the mana gets replaced to
for (ReplacementEffect re : reList) {
SpellAbility o = re.getOverridingAbility();
if (o == null || o.getApi() != ApiType.ReplaceMana) {
continue;
}
// this one does replace the amount too
if (o.hasParam("ReplaceMana")) {
replaceMana.add(o);
} else if (o.hasParam("ReplaceType") || o.hasParam("ReplaceColor")) {
// this one replaces the color/type
// check if this one can be replaced into wanted mana shard
replaceType.add(o);
} else if (o.hasParam("ReplaceAmount")) {
replaceAmount.add(o);
}
}
// it is better to apply these ones first
if (!replaceMana.isEmpty()) {
for (SpellAbility saMana : replaceMana) {
// one of then has to Any
// one of then has to C
// one of then has to B
String m = saMana.getParam("ReplaceMana");
if ("Any".equals(m)) {
byte rs = MagicColor.GREEN;
for (byte c : MagicColor.WUBRGC) {
if (toPay.canBePaidWithManaOfColor(c)) {
rs = c;
break;
}
}
manaProduced = MagicColor.toShortString(rs);
} else {
manaProduced = m;
}
}
}
// then apply this one
if (!replaceType.isEmpty()) {
for (SpellAbility saMana : replaceAmount) {
if (saMana.hasParam("ReplaceType")) {
// replace color and colorless
String color = saMana.getParam("ReplaceType");
if ("Any".equals(color)) {
byte rs = MagicColor.GREEN;
for (byte c : MagicColor.WUBRGC) {
if (toPay.canBePaidWithManaOfColor(c)) {
rs = c;
break;
}
}
color = MagicColor.toShortString(rs);
}
for (byte c : MagicColor.WUBRGC) {
String s = MagicColor.toShortString(c);
manaProduced = manaProduced.replace(s, color);
}
} else if (saMana.hasParam("ReplaceColor")) {
// replace color
String color = saMana.getParam("ReplaceColor");
if ("Chosen".equals(color)) {
if (saMana.hasChosenColor()) {
color = MagicColor.toShortString(saMana.getChosenColor());
}
}
if (saMana.hasParam("ReplaceOnly")) {
manaProduced = manaProduced.replace(saMana.getParam("ReplaceOnly"), color);
} else {
for (byte c : MagicColor.WUBRG) {
String s = MagicColor.toShortString(c);
manaProduced = manaProduced.replace(s, color);
}
}
}
}
}
// then multiply if able
if (!replaceAmount.isEmpty()) {
int totalAmount = 1;
for (SpellAbility saMana : replaceAmount) {
totalAmount *= Integer.valueOf(saMana.getParam("ReplaceAmount"));
}
manaProduced = StringUtils.repeat(manaProduced, " ", totalAmount);
}
return manaProduced;
}
public static String predictManafromSpellAbility(SpellAbility saPayment, Player ai, ManaCostShard toPay) {
Card hostCard = saPayment.getHostCard();
String manaProduced = predictManaReplacement(saPayment, ai, toPay);
String originalProduced = manaProduced;
if (originalProduced.isEmpty()) {
return manaProduced;
}
// Run triggers like Nissa
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(hostCard);
runParams.put(AbilityKey.Player, ai); // assuming AI would only ever gives itself mana
runParams.put(AbilityKey.AbilityMana, saPayment);
runParams.put(AbilityKey.Produced, manaProduced);
runParams.put(AbilityKey.Activator, ai);
for (Trigger tr : ai.getGame().getTriggerHandler().getActiveTrigger(TriggerType.TapsForMana, runParams)) {
SpellAbility trSA = tr.ensureAbility();
if (trSA == null) {
continue;
}
if (ApiType.Mana.equals(trSA.getApi())) {
int pAmount = AbilityUtils.calculateAmount(trSA.getHostCard(), trSA.getParamOrDefault("Amount", "1"), trSA);
String produced = trSA.getParam("Produced");
if (produced.equals("Chosen")) {
produced = MagicColor.toShortString(trSA.getHostCard().getChosenColor(trSA));
}
manaProduced += " " + StringUtils.repeat(produced, pAmount);
} else if (ApiType.ManaReflected.equals(trSA.getApi())) {
final String colorOrType = trSA.getParamOrDefault("ColorOrType", "Color");
// currently Color or Type, Type is colors + colorless
final String reflectProperty = trSA.getParam("ReflectProperty");
if (reflectProperty.equals("Produced") && !originalProduced.isEmpty()) {
// check if a colorless shard can be paid from the trigger
if (toPay.equals(ManaCostShard.COLORLESS) && colorOrType.equals("Type") && originalProduced.contains("C")) {
manaProduced += " " + "C";
} else if (originalProduced.length() == 1) {
// if length is only one, and it either is equal C == Type
if (colorOrType.equals("Type") || !originalProduced.equals("C")) {
manaProduced += " " + originalProduced;
}
} else {
// should it look for other shards too?
boolean found = false;
for (String s : originalProduced.split(" ")) {
if (colorOrType.equals("Type") || !s.equals("C") && toPay.canBePaidWithManaOfColor(MagicColor.fromName(s))) {
found = true;
manaProduced += " " + s;
break;
}
}
// no good mana found? just add the first generated color
if (!found) {
for (String s : originalProduced.split(" ")) {
if (colorOrType.equals("Type") || !s.equals("C")) {
manaProduced += " " + s;
break;
}
}
}
}
}
}
}
return manaProduced;
}
public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai) {
CardCollection manaSources = new CardCollection();
@@ -575,7 +306,7 @@ public class ComputerUtilMana {
// select which abilities may be used for each shard
Multimap<ManaCostShard, SpellAbility> sourcesForShards = ComputerUtilMana.groupAndOrderToPayShards(ai, manaAbilityMap, cost);
sortManaAbilities(sourcesForShards, sa);
sortManaAbilities(sourcesForShards);
ManaCostShard toPay;
// Loop over mana needed
@@ -589,30 +320,30 @@ public class ComputerUtilMana {
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)) {
if (!toPay.isPhyrexian() || !ai.canPayLife(2)) {
break; // cannot pay
}
if (toPay.isPhyrexian()) {
cost.payPhyrexian();
} else if (lifeInsteadOfBlack) {
cost.decreaseShard(ManaCostShard.BLACK, 1);
}
continue;
}
manaSources.add(saPayment.getHostCard());
setExpressColorChoice(sa, ai, cost, toPay, saPayment);
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
String manaProduced = toPay.isSnow() ? "S" : GameActionUtil.generatedMana(saPayment);
manaProduced = AbilityManaPart.applyManaReplacement(saPayment, manaProduced);
//System.out.println(manaProduced);
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard()));
Iterator<SpellAbility> itSa = sourcesForShards.values().iterator();
while (itSa.hasNext()) {
SpellAbility srcSa = itSa.next();
if (srcSa.getHostCard().equals(saPayment.getHostCard())) {
itSa.remove();
}
}
}
handleOfferingsAI(sa, true, cost.isPaid());
@@ -624,19 +355,9 @@ public class ComputerUtilMana {
private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean test, boolean checkPlayable) {
adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai);
List<Mana> manaSpentToPay = test ? new ArrayList<>() : sa.getPayingMana();
List<Mana> manaSpentToPay = test ? new ArrayList<Mana>() : sa.getPayingMana();
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
int testEnergyPool = ai.getCounters(CounterEnumType.ENERGY);
boolean ignoreColor = false, ignoreType = false;
StaticAbility mayPlay = sa.getMayPlay();
if (mayPlay != null) {
if (mayPlay.hasParam("MayPlayIgnoreColor")) {
ignoreColor = true;
} else if (mayPlay.hasParam("MayPlayIgnoreType")) {
ignoreType = true;
}
}
int testEnergyPool = ai.getCounters(CounterType.ENERGY);
List<SpellAbility> paymentList = Lists.newArrayList();
@@ -646,8 +367,7 @@ public class ComputerUtilMana {
boolean hasConverge = sa.getHostCard().hasConverge();
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test,
checkPlayable, manaSpentToPay, hasConverge, ignoreColor, ignoreType);
checkPlayable, manaSpentToPay, hasConverge);
if (sourcesForShards == null && !purePhyrexian) {
return false; // no mana abilities to use for paying
}
@@ -658,41 +378,13 @@ public class ComputerUtilMana {
// Loop over mana needed
while (!cost.isPaid()) {
while (!cost.isPaid() && !manapool.isEmpty()) {
boolean found = false;
// Apply the color/type conversion matrix if necessary
final CostPayment pay = new CostPayment(sa.getPayCosts(), sa);
if (ignoreType) {
AbilityUtils.applyManaColorConversion(pay, MagicColor.Constant.ANY_TYPE_CONVERSION);
} else if (ignoreColor) {
AbilityUtils.applyManaColorConversion(pay, MagicColor.Constant.ANY_COLOR_CONVERSION);
}
manapool.applyCardMatrix(pay);
for (byte color : MagicColor.WUBRGC) {
if (manapool.tryPayCostWithColor(color, sa, cost)) {
found = true;
break;
}
}
if (!found) {
break;
}
}
if (cost.isPaid()) {
break;
}
toPay = getNextShardToPay(cost);
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
Collection<SpellAbility> saList = null;
if (hasConverge &&
(toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) {
final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION;
for (final byte b : ColorSet.fromMask(unpaidColors)) {
// try and pay other colors for converge
for (final byte b : ColorSet.fromMask(unpaidColors)) { // try and pay other colors for converge
final ManaCostShard shard = ManaCostShard.valueOf(b);
saList = sourcesForShards.get(shard);
if (saList != null && !saList.isEmpty()) {
@@ -700,8 +392,7 @@ public class ComputerUtilMana {
break;
}
}
if (saList == null || saList.isEmpty()) {
// failed to converge, revert to paying generic
if (saList == null || saList.isEmpty()) { // failed to converge, revert to paying generic
saList = sourcesForShards.get(toPay);
hasConverge = false;
}
@@ -721,7 +412,7 @@ public class ComputerUtilMana {
SpellAbility saPayment = saList.isEmpty() ? null : chooseManaAbility(cost, sa, ai, toPay, saList, checkPlayable || !test);
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
if (sa.getTargets() != null && sa.getTargets().isTargeting(saPayment.getHostCard())) {
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
continue;
}
@@ -739,8 +430,7 @@ public class ComputerUtilMana {
}
if (saPayment == null) {
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2)
|| (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
if (!toPay.isPhyrexian() || !ai.canPayLife(2) || (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
break; // cannot pay
}
@@ -755,12 +445,7 @@ public class ComputerUtilMana {
}
}
if (toPay.isPhyrexian()) {
cost.payPhyrexian();
} else if (lifeInsteadOfBlack) {
cost.decreaseShard(ManaCostShard.BLACK, 1);
}
if (!test) {
ai.payLife(2, sa.getHostCard());
}
@@ -781,25 +466,32 @@ public class ComputerUtilMana {
}
}
// FIXME: if we're ignoring color or type, assume that the color/type of the mana produced will fit the case
// for the purpose of testing (since adding appropriate sources for shards in this particular case is handled
// inside getSourcesForShards)
// This is hacky and may be prone to bugs, so better implementation ideas are highly welcome.
String manaProduced = ignoreColor || ignoreType ? MagicColor.toShortString(toPay.getColorMask())
: predictManafromSpellAbility(saPayment, ai, toPay);
// System.out.println(manaProduced);
String manaProduced = toPay.isSnow() ? "S" : GameActionUtil.generatedMana(saPayment);
manaProduced = AbilityManaPart.applyManaReplacement(saPayment, manaProduced);
//System.out.println(manaProduced);
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard()));
Iterator<SpellAbility> itSa = sourcesForShards.values().iterator();
while (itSa.hasNext()) {
SpellAbility srcSa = itSa.next();
if (srcSa.getHostCard().equals(saPayment.getHostCard())) {
itSa.remove();
}
}
}
else {
if (saPayment.getPayCosts() != null) {
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment))) {
saList.remove(saPayment);
continue;
}
}
else {
System.err.println("Ability " + saPayment + " from " + saPayment.getHostCard() + " had NULL as payCost");
saPayment.getHostCard().tap();
}
ai.getGame().getStack().addAndUnfreeze(saPayment);
// subtract mana from mana pool
@@ -808,9 +500,15 @@ public class ComputerUtilMana {
// no need to remove abilities from resource map,
// once their costs are paid and consume resources, they can not be used again
if (hasConverge) {
// hack to prevent converge re-using sources
Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard()));
if (hasConverge) { // hack to prevent converge re-using sources
// remove from available lists
Iterator<SpellAbility> itSa = sourcesForShards.values().iterator();
while (itSa.hasNext()) {
SpellAbility srcSa = itSa.next();
if (srcSa.getHostCard().equals(saPayment.getHostCard())) {
itSa.remove();
}
}
}
}
}
@@ -823,6 +521,15 @@ public class ComputerUtilMana {
// extraMana, sa.getHostCard(), sa.toUnsuppressedString(), StringUtils.join(paymentPlan, "\n\t"));
// }
// See if it's possible to pay with something that was left in the mana pool in corner cases,
// e.g. Gemstone Caverns with a Luck counter on it generating colored mana (which fails to be
// processed correctly on a per-ability basis, leaving floating mana in the pool)
if (!cost.isPaid() && !manapool.isEmpty()) {
for (byte color : MagicColor.WUBRGC) {
manapool.tryPayCostWithColor(color, sa, cost);
}
}
// The cost is still unpaid, so refund the mana and report
if (!cost.isPaid()) {
refundMana(manaSpentToPay, ai, sa);
@@ -836,6 +543,13 @@ public class ComputerUtilMana {
}
}
// Note: manaSpentToPay shouldn't be cleared here, since it needs to remain
// on the SpellAbility in order for effects that check mana spent cost to work.
sa.getHostCard().setColorsPaid(cost.getColorsPaid());
// if (sa instanceof Spell_Permanent) // should probably add this
sa.getHostCard().setSunburstValue(cost.getSunburst());
if (test) {
refundMana(manaSpentToPay, ai, sa);
resetPayment(paymentList);
@@ -850,31 +564,16 @@ public class ComputerUtilMana {
}
}
private static void addAllSourcesForMagicColorRange(final ListMultimap<Integer, SpellAbility> manaAbilityMap, final ListMultimap<ManaCostShard, SpellAbility> sourcesForShards, final byte[] range) {
for (final byte b : range) {
final ManaCostShard shard = ManaCostShard.valueOf(b);
if (!sourcesForShards.containsKey(shard)) {
for (final byte c : range) {
for (SpellAbility saMana : manaAbilityMap.get((int) c)) {
if (!sourcesForShards.get(shard).contains(saMana)) {
sourcesForShards.get(shard).add(saMana);
}
}
}
}
}
}
/**
* Creates a mapping between the required mana shards and the available spell abilities to pay for them
*/
private static ListMultimap<ManaCostShard, SpellAbility> getSourcesForShards(final ManaCostBeingPaid cost,
final SpellAbility sa, final Player ai, final boolean test, final boolean checkPlayable,
List<Mana> manaSpentToPay, final boolean hasConverge, final boolean ignoreColor, final boolean ignoreType) {
List<Mana> manaSpentToPay, final boolean hasConverge) {
// arrange all mana abilities by color produced.
final ListMultimap<Integer, SpellAbility> manaAbilityMap = ComputerUtilMana.groupSourcesByManaColor(ai, checkPlayable);
if (manaAbilityMap.isEmpty()) {
// no mana abilities, bailing out
if (manaAbilityMap.isEmpty()) { // no mana abilities, bailing out
refundMana(manaSpentToPay, ai, sa);
handleOfferingsAI(sa, test, cost.isPaid());
return null;
@@ -885,29 +584,20 @@ public class ComputerUtilMana {
// select which abilities may be used for each shard
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = ComputerUtilMana.groupAndOrderToPayShards(ai, manaAbilityMap, cost);
if (hasConverge) {
// add extra colors for paying converge
if (hasConverge) { // add extra colors for paying converge
final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION;
for (final byte b : ColorSet.fromMask(unpaidColors)) {
final ManaCostShard shard = ManaCostShard.valueOf(b);
if (!sourcesForShards.containsKey(shard)) {
if (ai.getManaPool().canPayForShardWithColor(shard, b)) {
for (SpellAbility saMana : manaAbilityMap.get((int)b)) {
sourcesForShards.get(shard).add(saMana);
sourcesForShards.get(shard).add(sourcesForShards.get(shard).size(), saMana);
}
}
}
}
}
// add all other types/colors if the specific type/color doesn't matter
if (ignoreType) {
addAllSourcesForMagicColorRange(manaAbilityMap, sourcesForShards, MagicColor.WUBRGC);
} else if (ignoreColor) {
addAllSourcesForMagicColorRange(manaAbilityMap, sourcesForShards, MagicColor.WUBRG);
}
sortManaAbilities(sourcesForShards, sa);
sortManaAbilities(sourcesForShards);
if (DEBUG_MANA_PAYMENT) {
System.out.println("DEBUG_MANA_PAYMENT: sourcesForShards = " + sourcesForShards);
}
@@ -1021,7 +711,7 @@ public class ComputerUtilMana {
continue;
}
if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsSpellAndShardRestrictions(saBeingPaidFor, shard, thisMana.getColor())) {
if (thisMana.getManaAbility() != null && !thisMana.getManaAbility().meetsManaRestrictions(saBeingPaidFor)) {
continue;
}
@@ -1117,10 +807,11 @@ 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 (ma.getPayCosts() != null) { // if the AI can't pay the additional costs skip the mana ability
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) {
return false;
}
}
else if (sourceCard.isTapped()) {
return false;
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
@@ -1174,12 +865,10 @@ public class ComputerUtilMana {
// For combat tricks, always obey mana reservation
if (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP) {
if (!(ai.getGame().getPhaseHandler().isPlayerTurn(ai))) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
} else if (!(ai.getGame().getPhaseHandler().isPlayerTurn(ai)) && (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP)) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
}
else
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
} else {
if ((AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) ||
(AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK))) {
@@ -1268,7 +957,7 @@ public class ComputerUtilMana {
choice = abMana.getExpressChoice();
abMana.clearExpressChoice();
byte colorMask = ManaAtom.fromName(choice);
if (manaAb.canProduce(choice) && satisfiesColorChoice(abMana, choiceString, choice) && testCost.isAnyPartPayableWith(colorMask, ai.getManaPool())) {
if (abMana.canProduce(choice, manaAb) && testCost.isAnyPartPayableWith(colorMask, ai.getManaPool())) {
choiceString.append(choice);
payMultipleMana(testCost, choice, ai);
continue;
@@ -1278,7 +967,7 @@ public class ComputerUtilMana {
if (!testCost.isPaid()) {
// Loop over combo colors
for (String color : comboColors) {
if (satisfiesColorChoice(abMana, choiceString, choice) && testCost.isAnyPartPayableWith(ManaAtom.fromName(color), ai.getManaPool())) {
if (testCost.isAnyPartPayableWith(ManaAtom.fromName(color), ai.getManaPool())) {
payMultipleMana(testCost, color, ai);
if (nMana != 1) {
choiceString.append(" ");
@@ -1293,18 +982,14 @@ public class ComputerUtilMana {
}
}
// check if combo mana can produce most common color in hand
String commonColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Hand));
if (!commonColor.isEmpty() && satisfiesColorChoice(abMana, choiceString, MagicColor.toShortString(commonColor)) && abMana.getComboColors().contains(MagicColor.toShortString(commonColor))) {
String commonColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(
ZoneType.Hand));
if (!commonColor.isEmpty() && abMana.getComboColors().contains(MagicColor.toShortString(commonColor))) {
choice = MagicColor.toShortString(commonColor);
}
else {
// default to first available color
for (String c : comboColors) {
if (satisfiesColorChoice(abMana, choiceString, c)) {
choice = c;
break;
}
}
// default to first color
choice = comboColors[0];
}
if (nMana != 1) {
choiceString.append(" ");
@@ -1319,10 +1004,6 @@ public class ComputerUtilMana {
abMana.setExpressChoice(choiceString.toString());
}
private static boolean satisfiesColorChoice(AbilityManaPart abMana, StringBuilder choices, String choice) {
return !abMana.getOrigProduced().contains("Different") || !choices.toString().contains(choice);
}
/**
* <p>
* payMultipleMana.
@@ -1412,7 +1093,7 @@ public class ComputerUtilMana {
* @param extraMana extraMana
* @return ManaCost
*/
public static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final boolean test, final int extraMana) {
static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final boolean test, final int extraMana) {
Card card = sa.getHostCard();
ZoneType castFromBackup = null;
if (test && sa.isSpell()) {
@@ -1431,19 +1112,32 @@ public class ComputerUtilMana {
ManaCostBeingPaid cost = new ManaCostBeingPaid(mana, restriction);
// Tack xMana Payments into mana here if X is a set value
if (cost.getXcounter() > 0 || extraMana > 0) {
if (sa.getPayCosts() != null && (cost.getXcounter() > 0 || extraMana > 0)) {
int manaToAdd = 0;
if (test && extraMana > 0) {
final int multiplicator = Math.max(cost.getXcounter(), 1);
manaToAdd = extraMana * multiplicator;
} else {
manaToAdd = AbilityUtils.calculateAmount(card, "X", sa) * cost.getXcounter();
// For Count$xPaid set PayX in the AFs then use that here
// Else calculate it as appropriate.
final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar)) {
if (xSvar.equals("PayX") && card.hasSVar(xSvar)) {
// X SVar may end up being an empty string when copying a spell with no cost (e.g. Jhoira Avatar)
String xValue = card.getSVar(xSvar);
manaToAdd = xValue.isEmpty() ? 0 : Integer.parseInt(card.getSVar(xSvar)) * cost.getXcounter(); // X
} else {
manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter();
}
}
}
cost.increaseShard(ManaCostShard.parseNonGeneric(sa.getParamOrDefault("XColor", "1")), manaToAdd);
String manaXColor = sa.getParam("XColor");
ManaCostShard shardToGrow = ManaCostShard.parseNonGeneric(manaXColor == null ? "1" : manaXColor);
cost.increaseShard(shardToGrow, manaToAdd);
if (!test) {
sa.setXManaCostPaid(manaToAdd / cost.getXcounter());
card.setXManaCostPaid(manaToAdd / cost.getXcounter());
}
}
@@ -1492,7 +1186,7 @@ public class ComputerUtilMana {
for (SpellAbility ma : src.getManaAbilities()) {
ma.setActivatingPlayer(p);
if (!checkPlayable || ma.canPlay()) {
int costsToActivate = ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
int costsToActivate = ma.getPayCosts() != null && ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length;
int producedAmount = AbilityUtils.calculateAmount(src, ma.getParamOrDefault("Amount", "1"), ma);
@@ -1555,16 +1249,6 @@ public class ComputerUtilMana {
// 3. Use lands that produce any color many
// 4. all other sources (creature, costs, drawback, etc.)
for (Card card : manaSources) {
// exclude creature sources that will tap as a part of an attack declaration
if (card.isCreature()) {
if (card.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)) {
Combat combat = card.getGame().getCombat();
if (combat.getAttackers().indexOf(card) != -1 && !card.hasKeyword(Keyword.VIGILANCE)) {
continue;
}
}
}
if (card.isCreature() || card.isEnchanted()) {
otherManaSources.add(card);
continue; // don't use creatures before other permanents
@@ -1647,6 +1331,19 @@ public class ComputerUtilMana {
final ListMultimap<Integer, SpellAbility> manaMap = ArrayListMultimap.create();
final Game game = ai.getGame();
List<ReplacementEffect> replacementEffects = new ArrayList<ReplacementEffect>();
for (final Player p : game.getPlayers()) {
for (final Card crd : p.getAllCards()) {
for (final ReplacementEffect replacementEffect : crd.getReplacementEffects()) {
if (replacementEffect.requirementsCheck(game)
&& replacementEffect.getMapParams().containsKey("ManaReplacement")
&& replacementEffect.zonesCheck(game.getZoneOf(crd))) {
replacementEffects.add(replacementEffect);
}
}
}
}
// Loop over all current available mana sources
for (final Card sourceCard : getAvailableManaSources(ai, checkPlayable)) {
if (DEBUG_MANA_PAYMENT) {
@@ -1676,80 +1373,49 @@ public class ComputerUtilMana {
}
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
SpellAbility tail = m;
while (tail != null) {
AbilityManaPart mp = m.getManaPart();
if (mp != null && tail.metConditions()) {
// TODO Replacement Check currently doesn't work for reflected colors
// setup produce mana replacement effects
String origin = mp.getOrigProduced();
final Map<AbilityKey, Object> repParams = AbilityKey.newMap();
repParams.put(AbilityKey.Mana, origin);
repParams.put(AbilityKey.Affected, sourceCard);
repParams.put(AbilityKey.Player, ai);
repParams.put(AbilityKey.AbilityMana, m); // RootAbility
final Map<String, Object> repParams = new HashMap<>();
repParams.put("Event", "ProduceMana");
repParams.put("Mana", mp.getOrigProduced());
repParams.put("Affected", sourceCard);
repParams.put("Player", ai);
repParams.put("AbilityMana", m);
List<ReplacementEffect> reList = game.getReplacementHandler().getReplacementList(ReplacementType.ProduceMana, repParams, ReplacementLayer.Other);
for (final ReplacementEffect replacementEffect : replacementEffects) {
if (replacementEffect.canReplace(repParams)) {
Card crd = replacementEffect.getHostCard();
String repType = crd.getSVar(replacementEffect.getMapParams().get("ManaReplacement"));
if (repType.contains("Chosen")) {
repType = TextUtil.fastReplace(repType, "Chosen", MagicColor.toShortString(crd.getChosenColor()));
}
mp.setManaReplaceType(repType);
}
}
if (reList.isEmpty()) {
Set<String> reflectedColors = CardUtil.getReflectableManaColors(m);
// find possible colors
for (byte color : MagicColor.WUBRG) {
if (tail.canThisProduce(MagicColor.toShortString(color)) || reflectedColors.contains(MagicColor.toLongString(color))) {
manaMap.put((int)color, m);
if (mp.canProduce("W", m) || reflectedColors.contains(MagicColor.Constant.WHITE)) {
manaMap.get(ManaAtom.WHITE).add(m);
}
if (mp.canProduce("U", m) || reflectedColors.contains(MagicColor.Constant.BLUE)) {
manaMap.get(ManaAtom.BLUE).add(m);
}
if (m.canThisProduce("C") || reflectedColors.contains(MagicColor.Constant.COLORLESS)) {
manaMap.put(ManaAtom.COLORLESS, m);
if (mp.canProduce("B", m) || reflectedColors.contains(MagicColor.Constant.BLACK)) {
manaMap.get(ManaAtom.BLACK).add(m);
}
} else {
// try to guess the color the mana gets replaced to
for (ReplacementEffect re : reList) {
SpellAbility o = re.getOverridingAbility();
String replaced = origin;
if (o == null || o.getApi() != ApiType.ReplaceMana) {
continue;
if (mp.canProduce("R", m) || reflectedColors.contains(MagicColor.Constant.RED)) {
manaMap.get(ManaAtom.RED).add(m);
}
if (o.hasParam("ReplaceMana")) {
replaced = o.getParam("ReplaceMana");
} else if (o.hasParam("ReplaceType")) {
String color = o.getParam("ReplaceType");
for (byte c : MagicColor.WUBRGC) {
String s = MagicColor.toShortString(c);
replaced = replaced.replace(s, color);
if (mp.canProduce("G", m) || reflectedColors.contains(MagicColor.Constant.GREEN)) {
manaMap.get(ManaAtom.GREEN).add(m);
}
} else if (o.hasParam("ReplaceColor")) {
String color = o.getParam("ReplaceColor");
if (o.hasParam("ReplaceOnly")) {
replaced = replaced.replace(o.getParam("ReplaceOnly"), color);
} else {
for (byte c : MagicColor.WUBRG) {
String s = MagicColor.toShortString(c);
replaced = replaced.replace(s, color);
if (mp.canProduce("C", m) || reflectedColors.contains(MagicColor.Constant.COLORLESS)) {
manaMap.get(ManaAtom.COLORLESS).add(m);
}
}
}
for (byte color : MagicColor.WUBRG) {
if ("Any".equals(replaced) || replaced.contains(MagicColor.toShortString(color))) {
manaMap.put((int)color, m);
}
}
if (replaced.contains("C")) {
manaMap.put(ManaAtom.COLORLESS, m);
}
}
}
}
tail = tail.getSubAbility();
}
if (m.getHostCard().isSnow()) {
manaMap.put(ManaAtom.IS_SNOW, m);
if (mp.isSnow()) {
manaMap.get(ManaAtom.IS_SNOW).add(m);
}
if (DEBUG_MANA_PAYMENT) {
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor manaMap = " + manaMap);
@@ -1871,7 +1537,7 @@ public class ComputerUtilMana {
* @return map between creatures and shards to convoke
*/
public static Map<Card, ManaCostShard> getConvokeOrImproviseFromList(final ManaCost cost, List<Card> list, boolean improvise) {
final Map<Card, ManaCostShard> convoke = new HashMap<>();
final Map<Card, ManaCostShard> convoke = new HashMap<Card, ManaCostShard>();
Card convoked = null;
if (!improvise) {
for (ManaCostShard toPay : cost) {
@@ -1894,4 +1560,31 @@ public class ComputerUtilMana {
}
return convoke;
}
public static int determineMaxAffordableX(Player ai, SpellAbility sa) {
if (sa.getPayCosts() == null || sa.getPayCosts().getCostMana() == null) {
return -1;
}
int numTgts = 0;
int numX = sa.getPayCosts().getCostMana().getAmountOfX();
if (numX == 0) {
return -1;
}
int testX = 1;
while (testX <= 100) {
if (ComputerUtilMana.canPayManaCost(sa, ai, testX)) {
numTgts++;
} else {
break;
}
testX++;
}
numTgts /= numX;
return numTgts;
}
}

View File

@@ -5,7 +5,7 @@ import com.google.common.base.Function;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
@@ -242,11 +242,11 @@ public class CreatureEvaluator implements Function<Card, Integer> {
&& "+X".equals(sa.getParam("NumDef"))
&& !sa.usesTargeting()
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
// Electrostatic Pummeler, can be expanded for similar cards
int initPower = getEffectivePower(sa.getHostCard());
int pumpedPower = initPower;
int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY);
int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY);
if (energy > 0) {
int numActivations = energy / 3;
for (int i = 0; i < numActivations; i++) {

View File

@@ -1,39 +1,19 @@
package forge.ai;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.Card;
import forge.game.card.CardCloneStates;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
import forge.game.card.CounterType;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
@@ -44,7 +24,6 @@ import forge.game.mana.ManaPool;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
@@ -53,9 +32,16 @@ import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
import java.util.Map.Entry;
public abstract class GameState {
private static final Map<ZoneType, String> ZONES = new HashMap<>();
private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>();
static {
ZONES.put(ZoneType.Battlefield, "battlefield");
ZONES.put(ZoneType.Hand, "hand");
@@ -80,21 +66,18 @@ public abstract class GameState {
private boolean puzzleCreatorState = false;
private final Map<ZoneType, String> humanCardTexts = new EnumMap<>(ZoneType.class);
private final Map<ZoneType, String> aiCardTexts = new EnumMap<>(ZoneType.class);
private final Map<ZoneType, String> humanCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
private final Map<ZoneType, String> aiCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
private final Map<Integer, Card> idToCard = new HashMap<>();
private final Map<Card, Integer> cardToAttachId = new HashMap<>();
private final Map<Card, Integer> cardToEnchantPlayerId = new HashMap<>();
private final Map<Card, Integer> markedDamage = new HashMap<>();
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>();
private final Map<Card, List<String>> cardToChosenTypes = new HashMap<>();
private final Map<Card, String> cardToChosenType = new HashMap<>();
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
private final Map<Card, List<String>> cardToMergedCards = new HashMap<>();
private final Map<Card, String> cardToNamedCard = new HashMap<>();
private final Map<Card, String> cardToNamedCard2 = new HashMap<>();
private final Map<Card, String> cardToExiledWithId = new HashMap<>();
private final Map<Card, Card> cardAttackMap = new HashMap<>();
@@ -113,13 +96,8 @@ public abstract class GameState {
private String precastHuman = null;
private String precastAI = null;
private String putOnStackHuman = null;
private String putOnStackAI = null;
private int turn = 1;
private boolean removeSummoningSickness = false;
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
private final int TARGET_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
private final int TARGET_HUMAN = -2;
@@ -233,10 +211,6 @@ public abstract class GameState {
// Remember the IDs of imprinted cards
cardsReferencedByID.add(i);
}
for (Card i : card.getChosenCards()) {
// Remember the IDs of chosen cards
cardsReferencedByID.add(i);
}
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
// Remember the IDs of attacked planeswalkers
GameEntity def = game.getCombat().getDefenderByAttacker(card);
@@ -259,7 +233,7 @@ public abstract class GameState {
if (card instanceof DetachedCardEffect) {
continue;
}
addCard(zone, card.getController() == ai ? aiCardTexts : humanCardTexts, card);
addCard(zone, card.getOwner() == ai ? aiCardTexts : humanCardTexts, card);
}
}
}
@@ -270,19 +244,13 @@ public abstract class GameState {
newText.append(";");
}
if (c.isToken()) {
newText.append("t:").append(new TokenInfo(c).toString());
newText.append("t:" + new TokenInfo(c).toString());
} else {
if (c.getPaperCard() == null) {
return;
}
if (!c.getMergedCards().isEmpty()) {
// we have to go by the current top card name here
newText.append(c.getTopMergedCard().getPaperCard().getName());
} else {
newText.append(c.getPaperCard().getName());
}
}
if (c.isCommander()) {
newText.append("|IsCommander");
}
@@ -292,10 +260,6 @@ public abstract class GameState {
}
if (zoneType == ZoneType.Battlefield) {
if (c.getOwner() != c.getController()) {
// TODO: Handle more than 2-player games.
newText.append("|Owner:" + (c.getOwner().isAI() ? "AI" : "Human"));
}
if (c.isTapped()) {
newText.append("|Tapped");
}
@@ -323,8 +287,6 @@ public abstract class GameState {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
if (c.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
@@ -340,33 +302,15 @@ public abstract class GameState {
newText.append("|Damage:").append(c.getDamage());
}
SpellAbility first = c.getFirstSpellAbility();
if (first != null) {
if (first.hasChosenColor()) {
newText.append("|ChosenColor:").append(TextUtil.join(first.getChosenColors(), ","));
if (!c.getChosenColor().isEmpty()) {
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ","));
}
if (first.hasChosenType()) {
newText.append("|ChosenType:").append(TextUtil.join(first.getChosenType(), ","));
if (!c.getChosenType().isEmpty()) {
newText.append("|ChosenType:").append(c.getChosenType());
}
}
if (!c.getNamedCard().isEmpty()) {
newText.append("|NamedCard:").append(c.getNamedCard());
}
if (!c.getNamedCard2().isEmpty()) {
newText.append("|NamedCard2:").append(c.getNamedCard2());
}
List<String> chosenCardIds = Lists.newArrayList();
for (Object obj : c.getChosenCards()) {
if (obj instanceof Card) {
int id = ((Card)obj).getId();
chosenCardIds.add(String.valueOf(id));
}
}
if (!chosenCardIds.isEmpty()) {
newText.append("|ChosenCards:").append(TextUtil.join(chosenCardIds, ","));
}
List<String> rememberedCardIds = Lists.newArrayList();
for (Object obj : c.getRemembered()) {
@@ -387,17 +331,6 @@ public abstract class GameState {
if (!imprintedCardIds.isEmpty()) {
newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ","));
}
if (!c.getMergedCards().isEmpty()) {
List<String> mergedCardNames = new ArrayList<>();
for (Card merged : c.getMergedCards()) {
if (c.getTopMergedCard() == merged) {
continue;
}
mergedCardNames.add(merged.getPaperCard().getName().replace(",", "^"));
}
newText.append("|MergedCards:").append(TextUtil.join(mergedCardNames, ","));
}
}
if (zoneType == ZoneType.Exile) {
@@ -407,12 +340,6 @@ public abstract class GameState {
if (c.isFaceDown()) {
newText.append("|FaceDown"); // Exiled face down
}
if (c.isAdventureCard() && c.getZone().is(ZoneType.Exile)) {
// TODO: this will basically default all exiled cards with Adventure to being "On Adventure".
// Need to figure out a better way to detect if it's actually on adventure.
newText.append("|OnAdventure");
}
}
if (zoneType == ZoneType.Battlefield || zoneType == ZoneType.Exile) {
@@ -429,7 +356,7 @@ public abstract class GameState {
newText.append("|Attacking");
GameEntity def = c.getGame().getCombat().getDefenderByAttacker(c);
if (def instanceof Card) {
newText.append(":").append(def.getId());
newText.append(":" + def.getId());
}
}
}
@@ -505,10 +432,6 @@ public abstract class GameState {
turn = Integer.parseInt(categoryValue);
}
else if (categoryName.equals("removesummoningsickness")) {
removeSummoningSickness = categoryValue.equalsIgnoreCase("true");
}
else if (categoryName.endsWith("life")) {
if (isHuman)
humanLife = Integer.parseInt(categoryValue);
@@ -590,13 +513,6 @@ public abstract class GameState {
precastAI = categoryValue;
}
else if (categoryName.endsWith("putonstack")) {
if (isHuman)
putOnStackHuman = categoryValue;
else
putOnStackAI = categoryValue;
}
else if (categoryName.endsWith("manapool")) {
if (isHuman)
humanManaPool = categoryValue;
@@ -634,12 +550,9 @@ public abstract class GameState {
cardToEnchantPlayerId.clear();
cardToRememberedId.clear();
cardToExiledWithId.clear();
cardToImprintedId.clear();
markedDamage.clear();
cardToChosenClrs.clear();
cardToChosenCards.clear();
cardToChosenTypes.clear();
cardToMergedCards.clear();
cardToChosenType.clear();
cardToScript.clear();
cardAttackMap.clear();
@@ -672,24 +585,18 @@ public abstract class GameState {
handleCardAttachments();
handleChosenEntities();
handleRememberedEntities();
handleMergedCards();
handleScriptExecution(game);
handlePrecastSpells(game);
handleMarkedDamage();
game.getTriggerHandler().setSuppressAllTriggers(false);
// SAs added to stack cause triggers to fire, as if the relevant SAs were cast
handleAddSAsToStack(game);
// Combat only works for 1v1 matches for now (which are the only matches dev mode supports anyway)
// Note: triggers may fire during combat declarations ("whenever X attacks, ...", etc.)
if (newPhase == PhaseType.COMBAT_DECLARE_ATTACKERS || newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS) {
boolean toDeclareBlockers = newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS;
if (newPlayerTurn != null) {
handleCombat(game, newPlayerTurn, newPlayerTurn.getSingleOpponent(), toDeclareBlockers);
}
}
game.getStack().setResolving(false);
@@ -698,25 +605,19 @@ public abstract class GameState {
game.getPhaseHandler().devAdvanceToPhase(advPhase);
}
if (removeSummoningSickness) {
for (Card card : game.getCardsInGame()) {
card.setSickness(false);
}
}
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
}
private String processManaPool(ManaPool manaPool) {
StringBuilder mana = new StringBuilder();
String mana = "";
for (final byte c : MagicColor.WUBRGC) {
int amount = manaPool.getAmountOfColor(c);
for (int i = 0; i < amount; i++) {
mana.append(MagicColor.toShortString(c)).append(" ");
mana += MagicColor.toShortString(c) + " ";
}
}
return mana.toString().trim();
return mana.trim();
}
private void updateManaPool(Player p, String manaDef, boolean clearPool, boolean persistent) {
@@ -733,7 +634,7 @@ public abstract class GameState {
if (persistent) {
produced.put("PersistentMana", "True");
}
final AbilityManaPart abMana = new AbilityManaPart(dummy, null, produced);
final AbilityManaPart abMana = new AbilityManaPart(dummy, produced);
game.getAction().invoke(new Runnable() {
@Override
public void run() {
@@ -774,10 +675,10 @@ public abstract class GameState {
for (final Card c : combat.getAttackers()) {
attackedTarget.add(combat.getDefenderByAttacker(c));
}
final Map<AbilityKey, Object> runParams = Maps.newEnumMap(AbilityKey.class);
runParams.put(AbilityKey.Attackers, combat.getAttackers());
runParams.put(AbilityKey.AttackingPlayer, combat.getAttackingPlayer());
runParams.put(AbilityKey.AttackedTarget, attackedTarget);
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Attackers", combat.getAttackers());
runParams.put("AttackingPlayer", combat.getAttackingPlayer());
runParams.put("AttackedTarget", attackedTarget);
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
}
@@ -827,8 +728,6 @@ public abstract class GameState {
Card exiledWith = idToCard.get(Integer.parseInt(id));
c.setExiledWith(exiledWith);
c.setExiledBy(exiledWith.getController());
exiledWith.addExiledWith(c);
}
}
@@ -863,12 +762,6 @@ public abstract class GameState {
break;
}
}
if (sa.hasParam("RememberTargets")) {
for (final GameObject o : sa.getTargets()) {
sa.getHostCard().addRemembered(o);
}
}
}
private void handleScriptExecution(final Game game) {
@@ -881,9 +774,6 @@ public abstract class GameState {
}
private void executeScript(Game game, Card c, String sPtr) {
executeScript(game, c, sPtr, false);
}
private void executeScript(Game game, Card c, String sPtr, boolean putOnStack) {
int tgtID = TARGET_NONE;
if (sPtr.contains("->")) {
String tgtDef = sPtr.substring(sPtr.lastIndexOf("->") + 2);
@@ -956,14 +846,9 @@ public abstract class GameState {
}
}
if (sa != null) {
sa.setActivatingPlayer(c.getController());
}
handleScriptedTargetingForSA(game, sa, tgtID);
if (putOnStack) {
game.getStack().addAndUnfreeze(sa);
} else {
sa.resolve();
// resolve subabilities
@@ -973,7 +858,6 @@ public abstract class GameState {
subSa = subSa.getSubAbility();
}
}
}
private void handlePrecastSpells(final Game game) {
Player human = game.getPlayers().get(0);
@@ -993,28 +877,7 @@ public abstract class GameState {
}
}
private void handleAddSAsToStack(final Game game) {
Player human = game.getPlayers().get(0);
Player ai = game.getPlayers().get(1);
if (putOnStackHuman != null) {
String[] spellList = TextUtil.split(putOnStackHuman, ';');
for (String spell : spellList) {
precastSpellFromCard(spell, human, game, true);
}
}
if (putOnStackAI != null) {
String[] spellList = TextUtil.split(putOnStackAI, ';');
for (String spell : spellList) {
precastSpellFromCard(spell, ai, game, true);
}
}
}
private void precastSpellFromCard(String spellDef, final Player activator, final Game game) {
precastSpellFromCard(spellDef, activator, game, false);
}
private void precastSpellFromCard(String spellDef, final Player activator, final Game game, final boolean putOnStack) {
int tgtID = TARGET_NONE;
String scriptID = "";
@@ -1028,17 +891,6 @@ public abstract class GameState {
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
}
Card c = null;
if (StringUtils.isNumeric(spellDef)) {
// Precast from a specific host
c = idToCard.get(Integer.parseInt(spellDef));
if (c == null) {
System.err.println("ERROR: Could not find a card with ID " + spellDef + " to precast!");
return;
}
} else {
// Precast from a card by name
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
if (pc == null) {
@@ -1046,13 +898,11 @@ public abstract class GameState {
return;
}
c = Card.fromPaperCard(pc, activator);
}
Card c = Card.fromPaperCard(pc, activator);
SpellAbility sa = null;
if (!scriptID.isEmpty()) {
executeScript(game, c, scriptID, putOnStack);
executeScript(game, c, scriptID);
return;
}
@@ -1061,12 +911,8 @@ public abstract class GameState {
handleScriptedTargetingForSA(game, sa, tgtID);
if (putOnStack) {
game.getStack().addAndUnfreeze(sa);
} else {
sa.resolve();
}
}
private void handleMarkedDamage() {
for (Entry<Card, Integer> entry : markedDamage.entrySet()) {
@@ -1087,13 +933,13 @@ public abstract class GameState {
Card c = entry.getKey();
List<String> colors = entry.getValue();
c.setChosenColors(colors, c.getFirstSpellAbility());
c.setChosenColors(colors);
}
// Chosen type
for (Entry<Card, List<String>> entry : cardToChosenTypes.entrySet()) {
for (Entry<Card, String> entry : cardToChosenType.entrySet()) {
Card c = entry.getKey();
c.setChosenType(entry.getValue(), c.getFirstSpellAbility());
c.setChosenType(entry.getValue());
}
// Named card
@@ -1101,18 +947,6 @@ public abstract class GameState {
Card c = entry.getKey();
c.setNamedCard(entry.getValue());
}
// Named card 2
for (Entry<Card,String> entry : cardToNamedCard2.entrySet()) {
Card c = entry.getKey();
c.setNamedCard2(entry.getValue());
}
// Chosen cards
for (Entry<Card, CardCollection> entry : cardToChosenCards.entrySet()) {
Card c = entry.getKey();
c.setChosenCards(entry.getValue());
}
}
private void handleCardAttachments() {
@@ -1142,61 +976,12 @@ public abstract class GameState {
}
}
private void handleMergedCards() {
for(Entry<Card, List<String>> entry : cardToMergedCards.entrySet()) {
Card mergedTo = entry.getKey();
for(String mergedCardName : entry.getValue()) {
Card c;
PaperCard pc = StaticData.instance().getCommonCards().getCard(mergedCardName. replace("^", ","));
if (pc == null) {
System.err.println("ERROR: Tried to create a non-existent card named " + mergedCardName + " (as a merged card) when loading game state!");
continue;
}
c = Card.fromPaperCard(pc, mergedTo.getOwner());
emulateMergeViaMutate(mergedTo, c);
}
}
}
private void emulateMergeViaMutate(Card top, Card bottom) {
if (top == null || bottom == null) {
System.err.println("ERROR: Tried to call emulateMergeViaMutate with a null card!");
return;
}
Game game = top.getGame();
bottom.setMergedToCard(top);
if (!top.hasMergedCard()) {
top.addMergedCard(top);
}
top.addMergedCard(bottom);
if (top.getMutatedTimestamp() != -1) {
top.removeCloneState(top.getMutatedTimestamp());
}
final Long ts = game.getNextTimestamp();
top.setMutatedTimestamp(ts);
if (top.getCurrentStateName() != CardStateName.FaceDown) {
final CardCloneStates mutatedStates = CardFactory.getMutatedCloneStates(top, null/*FIXME*/);
top.addCloneState(mutatedStates, ts);
}
bottom.setTapped(top.isTapped());
bottom.setFlipped(top.isFlipped());
top.setTimesMutated(top.getTimesMutated() + 1);
top.updateTokenView();
// TODO: Merged commanders aren't supported yet
}
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
entity.setCounters(Maps.newHashMap());
entity.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2);
entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false);
}
}
@@ -1209,7 +994,7 @@ public abstract class GameState {
p.getZone(zt).removeAllCards(true);
}
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<>(ZoneType.class);
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
String value = kv.getValue();
playerCards.put(kv.getKey(), processCardsForZone(value.isEmpty() ? new String[0] : value.split(";"), p));
@@ -1222,7 +1007,7 @@ public abstract class GameState {
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) {
List<Card> cards = new ArrayList<>();
List<Card> cards = new ArrayList<Card>();
for (final Card c : kv.getValue()) {
if (c.isToken()) {
cards.add(c);
@@ -1238,7 +1023,7 @@ public abstract class GameState {
Map<CounterType, Integer> counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters
// var as-is.
c.setCounters(Maps.newHashMap());
c.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter)
@@ -1262,9 +1047,7 @@ public abstract class GameState {
zone.setCards(kv.getValue());
}
}
for (Card cmd : p.getCommanders()) {
p.getZone(ZoneType.Command).add(Player.createCommanderEffect(p.getGame(), cmd));
}
}
/**
@@ -1325,7 +1108,7 @@ public abstract class GameState {
} else if (info.startsWith("SummonSick")) {
c.setSickness(true);
} else if (info.startsWith("FaceDown")) {
c.turnFaceDown(true);
c.setState(CardStateName.FaceDown, true);
if (info.endsWith("Manifested")) {
c.setManifested(true);
}
@@ -1335,23 +1118,11 @@ public abstract class GameState {
c.setState(CardStateName.Flipped, true);
} else if (info.startsWith("Meld")) {
c.setState(CardStateName.Meld, true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell";
AbilitySub saAdventure = (AbilitySub)AbilityFactory.getAbility(abAdventure, c);
StringBuilder sbPlay = new StringBuilder();
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure");
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
saAdventure.setSVar("Play", sbPlay.toString());
saAdventure.setActivatingPlayer(c.getOwner());
saAdventure.resolve();
c.setExiledWith(c); // This seems to be the way it's set up internally. Potentially not needed here?
c.setExiledBy(c.getController());
} else if (info.startsWith("IsCommander")) {
// TODO: This doesn't seem to properly restore the ability to play the commander. Why?
c.setCommander(true);
player.setCommanders(Lists.newArrayList(c));
player.getZone(ZoneType.Command).add(Player.createCommanderEffect(player.getGame(), c));
} else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3));
idToCard.put(id, c);
@@ -1362,14 +1133,6 @@ public abstract class GameState {
// TODO: improve this for game states with more than two players
String tgt = info.substring(info.indexOf(':') + 1);
cardToEnchantPlayerId.put(c, tgt.equalsIgnoreCase("AI") ? TARGET_AI : TARGET_HUMAN);
} else if (info.startsWith("Owner:")) {
// TODO: improve this for game states with more than two players
Player human = player.getGame().getPlayers().get(0);
Player ai = player.getGame().getPlayers().get(1);
String owner = info.substring(info.indexOf(':') + 1);
Player controller = c.getController();
c.setOwner(owner.equalsIgnoreCase("AI") ? ai : human);
c.setController(controller, c.getGame().getNextTimestamp());
} else if (info.startsWith("Ability:")) {
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
@@ -1379,21 +1142,9 @@ public abstract class GameState {
} else if (info.startsWith("ChosenColor:")) {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) {
cardToChosenTypes.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenCards:")) {
CardCollection chosen = new CardCollection();
String[] idlist = info.substring(info.indexOf(':') + 1).split(",");
for (String id : idlist) {
chosen.add(idToCard.get(Integer.parseInt(id)));
}
cardToChosenCards.put(c, chosen);
} else if (info.startsWith("MergedCards:")) {
List<String> cardNames = Arrays.asList(info.substring(info.indexOf(':') + 1).split(","));
cardToMergedCards.put(c, cardNames);
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("NamedCard:")) {
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("NamedCard2:")) {
cardToNamedCard2.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ExecuteScript:")) {
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("RememberedCards:")) {

View File

@@ -1,23 +1,11 @@
package forge.ai;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import forge.LobbyPlayer;
import forge.ai.ability.ProtectAi;
import forge.card.CardStateName;
@@ -33,43 +21,18 @@ import forge.game.GameObject;
import forge.game.GameType;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CardView;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment;
import forge.game.cost.CostExile;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.keyword.KeywordInterface;
import forge.game.cost.*;
import forge.game.mana.Mana;
import forge.game.mana.ManaConversionMatrix;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.DelayedReveal;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController;
import forge.game.player.PlayerView;
import forge.game.player.*;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.Ability;
import forge.game.spellability.AbilityStatic;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetChoices;
import forge.game.spellability.*;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
@@ -78,6 +41,13 @@ import forge.util.ITriggerEvent;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.security.InvalidParameterException;
import java.util.*;
/**
* A prototype for player controller class
@@ -106,8 +76,10 @@ public class PlayerControllerAi extends PlayerController {
if (abilities.size() == 0) {
return null;
}
else {
return abilities.get(0);
}
}
public AiController getAi() {
return brains;
@@ -119,7 +91,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public List<PaperCard> sideboard(Deck deck, GameType gameType, String message) {
public List<PaperCard> sideboard(Deck deck, GameType gameType) {
// AI does not know how to sideboard
return null;
}
@@ -130,7 +102,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public Integer announceRequirements(SpellAbility ability, String announce) {
public Integer announceRequirements(SpellAbility ability, String announce, boolean allowZero) {
// For now, these "announcements" are made within the AI classes of the appropriate SA effects
if (ability.getApi() != null) {
switch (ability.getApi()) {
@@ -172,12 +144,12 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional, Map<String, Object> params) {
return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional, params);
public CardCollectionView chooseCardsForEffect(CardCollectionView sourceList, SpellAbility sa, String title, int min, int max, boolean isOptional) {
return brains.chooseCardsForEffect(sourceList, sa, min, max, isOptional);
}
@Override
public <T extends GameEntity> T chooseSingleEntityForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public <T extends GameEntity> T chooseSingleEntityForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, boolean isOptional, Player targetedPlayer) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
@@ -185,47 +157,19 @@ public class PlayerControllerAi extends PlayerController {
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer);
}
@Override
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
Player targetedPlayer, Map<String, Object> params) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
FCollection<T> remaining = new FCollection<>(optionList);
List<T> selecteds = new ArrayList<>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer, params);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
return selecteds;
FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title,
Player targetedPlayer) {
// this isn't used
return null;
}
@Override
public List<SpellAbility> chooseSpellAbilitiesForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
int num, Map<String, Object> params) {
List<SpellAbility> remaining = Lists.newArrayList(spells);
List<SpellAbility> selecteds = Lists.newArrayList();
SpellAbility selected;
do {
selected = chooseSingleSpellForEffect(remaining, sa, title, params);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < num) );
return selecteds;
}
@Override
public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
public SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
@@ -251,13 +195,15 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean confirmTrigger(WrappedAbility wrapper) {
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
final SpellAbility sa = wrapper.getWrappedAbility();
//final Trigger regtrig = wrapper.getTrigger();
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true;
}
if (wrapper.isMandatory()) {
if (triggerParams.containsKey("DelayedTrigger") || isMandatory) {
//TODO: The only card with an optional delayed trigger is Shirei, Shizo's Caretaker,
// needs to be expanded when a more difficult cards comes up
return true;
}
// Store/replace target choices more properly to get this SA cleared.
@@ -294,7 +240,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public Player chooseStartingPlayer(boolean isFirstgame) {
public Player chooseStartingPlayer(boolean isFirstGame) {
return this.player; // AI is brave :)
}
@@ -391,11 +337,12 @@ public class PlayerControllerAi extends PlayerController {
if (destinationZone == ZoneType.Graveyard) {
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
if (!CardLists.filter(getGame().getCardsInGame(), new Predicate<Card>() {
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter");
return card.getName().equals("Volrath's Shapeshifter")
|| card.getStates().contains(CardStateName.OriginalText) && card.getState(CardStateName.OriginalText).getName().equals("Volrath's Shapeshifter");
}
}).isEmpty()) {
int bestValue = 0;
@@ -512,7 +459,7 @@ public class PlayerControllerAi extends PlayerController {
public void playSpellAbilityNoStack(SpellAbility effectSA, boolean canSetupTargets) {
if (canSetupTargets)
brains.doTrigger(effectSA, true); // first parameter does not matter, since return value won't be used
ComputerUtil.playNoStack(player, effectSA, getGame());
ComputerUtil.playNoStack(player, effectSA, game);
}
@Override
@@ -521,7 +468,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public TargetChoices chooseNewTargetsFor(SpellAbility ability, Predicate<GameObject> filter, boolean optional) {
public TargetChoices chooseNewTargetsFor(SpellAbility ability) {
// AI currently can't do this. But when it can it will need to be based on Ability API
return null;
}
@@ -533,7 +480,7 @@ public class PlayerControllerAi extends PlayerController {
Card toDiscard = Aggregates.itemWithMin(cardsOfType, CardPredicates.Accessors.fnGetCmc);
return new CardCollection(toDiscard);
}
return getAi().getCardsToDiscard(num, null, sa);
return getAi().getCardsToDiscard(num, (String[])null, sa);
}
@@ -543,18 +490,20 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public List<String> chooseSomeType(String kindOfType, SpellAbility sa, int min, int max, List<String> validTypes) {
List<String> chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), min, max, validTypes);
if (chosen.isEmpty()) {
return validTypes.subList(0, min);
public String chooseSomeType(String kindOfType, SpellAbility sa, List<String> validTypes, List<String> invalidTypes, boolean isOptional) {
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), invalidTypes);
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty())
{
chosen = validTypes.get(0);
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to 1st element: chosen");
}
getGame().getAction().notifyOfValue(sa, player, chosen.toString(), player);
game.getAction().nofityOfValue(sa, player, chosen, player);
return chosen;
}
@Override
public Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes, Player forPlayer) {
return ComputerUtil.vote(player, options, sa, votes, forPlayer);
public Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes) {
return ComputerUtil.vote(player, options, sa, votes);
}
@Override
@@ -562,66 +511,15 @@ public class PlayerControllerAi extends PlayerController {
return brains.aiShouldRun(replacementEffect, effectSA);
}
@Override
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn);
}
@Override
public CardCollectionView getCardsToMulligan(Player firstPlayer) {
if (!ComputerUtil.wantMulligan(player, 0)) {
if (!ComputerUtil.wantMulligan(player)) {
return null;
}
return player.getCardsIn(ZoneType.Hand);
}
@Override
public CardCollectionView londonMulliganReturnCards(final Player mulliganingPlayer, int cardsToReturn) {
// TODO This is better than it was before, but still suboptimal (but fast).
// Maybe score a bunch of hands based on projected hand size and return the "duds"
CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand));
int numLandsDesired = (mulliganingPlayer.getStartingHandSize() - cardsToReturn) / 2;
CardCollection toReturn = new CardCollection();
for (int i = 0; i < cardsToReturn; i++) {
hand.removeAll(toReturn);
CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS);
int numLandsInHand = landsInHand.size() - CardLists.filter(toReturn, Presets.LANDS).size();
// If we're flooding with lands, get rid of the worst land we have
if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) {
CardCollection producingLands = CardLists.filter(landsInHand, Presets.LANDS_PRODUCING_MANA);
CardCollection nonProducingLands = CardLists.filter(landsInHand, Predicates.not(Presets.LANDS_PRODUCING_MANA));
Card worstLand = nonProducingLands.isEmpty() ? ComputerUtilCard.getWorstLand(producingLands)
: ComputerUtilCard.getWorstLand(nonProducingLands);
toReturn.add(worstLand);
continue;
}
// See if we'd scry something to the bottom in this situation. If we want to, probably get rid of it.
CardCollection scryBottom = new CardCollection();
for (Card c : hand) {
// Lands are evaluated separately above, factoring in the number of cards to be returned to the library
if (!c.isLand() && !toReturn.contains(c) && !willPutCardOnTop(c)) {
scryBottom.add(c);
}
}
if (!scryBottom.isEmpty()) {
CardLists.sortByCmcDesc(scryBottom);
toReturn.add(scryBottom.getFirst()); // assume the max CMC one is worse since we're not guaranteed to have lands for it
continue;
}
// If we don't want to scry anything to the bottom, remove the worst card that we have in order to satisfy
// the requirement
toReturn.add(ComputerUtilCard.getWorstAI(hand));
}
return CardCollection.getView(toReturn);
}
@Override
public void declareAttackers(Player attacker, Combat combat) {
brains.declareAttackers(attacker, combat);
@@ -638,22 +536,20 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public boolean playChosenSpellAbility(SpellAbility sa) {
public void playChosenSpellAbility(SpellAbility sa) {
// System.out.println("Playing sa: " + sa);
if (sa instanceof LandAbility) {
if (sa.canPlay()) {
sa.resolve();
getGame().updateLastStateForCard(sa.getHostCard());
}
} else {
ComputerUtil.handlePlayingSpellAbility(player, sa, getGame());
ComputerUtil.handlePlayingSpellAbility(player, sa, game);
}
return true;
}
@Override
public CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard) {
return brains.getCardsToDiscard(numDiscard, null, null);
return brains.getCardsToDiscard(numDiscard, (String[])null, null);
}
@Override
@@ -686,7 +582,7 @@ public class PlayerControllerAi extends PlayerController {
// - End of hack for Exile a card from library Cumulative Upkeep -
if (ComputerUtilCost.canPayCost(ability, c.getController())) {
ComputerUtil.playNoStack(c.getController(), ability, getGame());
ComputerUtil.playNoStack(c.getController(), ability, game);
return true;
}
return false;
@@ -710,7 +606,7 @@ public class PlayerControllerAi extends PlayerController {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseNumber(player, sa, min, max, params);
}
};
@Override
public int chooseNumber(SpellAbility sa, String title, List<Integer> options, Player relatedPlayer) {
@@ -770,15 +666,14 @@ public class PlayerControllerAi extends PlayerController {
return true;
} else {
Card rem = (Card) source.getFirstRemembered();
if (!rem.isInZone(ZoneType.Battlefield)) {
if (!rem.getZone().is(ZoneType.Battlefield)) {
return true;
}
}
break;
case "BetterTgtThanRemembered":
if (source.getRememberedCount() > 0) {
Card rem = (Card) source.getFirstRemembered();
if (!rem.isInZone(ZoneType.Battlefield)) {
if (!rem.getZone().is(ZoneType.Battlefield)) {
return true;
}
for (Card c : source.getController().getCreaturesInPlay()) {
@@ -788,7 +683,6 @@ public class PlayerControllerAi extends PlayerController {
}
return false;
}
break;
default:
break;
}
@@ -796,7 +690,6 @@ public class PlayerControllerAi extends PlayerController {
return defaultVal != null && defaultVal.booleanValue();
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
case LeftOrRight: return brains.chooseDirection(sa);
case OddsOrEvens: return brains.chooseEvenOdd(sa); // false is Odd, true is Even
default:
return MyRandom.getRandom().nextBoolean();
}
@@ -827,8 +720,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {
List<AbilitySub> result = brains.chooseModeForAbility(sa, possible, min, num, allowRepeat);
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) {
List<AbilitySub> result = brains.chooseModeForAbility(sa, min, num, allowRepeat);
if (result != null) {
return result;
}
@@ -859,9 +752,6 @@ public class PlayerControllerAi extends PlayerController {
@Override
public byte chooseColor(String message, SpellAbility sa, ColorSet colors) {
if (colors.countColors() < 2) {
return Iterables.getFirst(colors, MagicColor.WHITE);
}
// You may switch on sa.getApi() here and use sa.getParam("AILogic")
CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
if (sa.getApi() == ApiType.Mana) {
@@ -911,16 +801,16 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public ReplacementEffect chooseSingleReplacementEffect(String prompt, List<ReplacementEffect> possibleReplacers) {
return brains.chooseSingleReplacementEffect(possibleReplacers);
public ReplacementEffect chooseSingleReplacementEffect(String prompt, List<ReplacementEffect> possibleReplacers, Map<String, Object> runParams) {
return brains.chooseSingleReplacementEffect(possibleReplacers, runParams);
}
@Override
public String chooseProtectionType(String string, SpellAbility sa, List<String> choices) {
String choice = choices.get(0);
SpellAbility hostsa = null; //for Protect sub-ability
if (getGame().stack.size() > 1) {
for (SpellAbilityStackInstance si : getGame().getStack()) {
if (game.stack.size() > 1) {
for (SpellAbilityStackInstance si : game.getStack()) {
SpellAbility spell = si.getSpellAbility(true);
if (sa != spell && sa.getHostCard() != spell.getHostCard()) {
String s = ProtectAi.toProtectFrom(spell.getHostCard(), sa);
@@ -931,10 +821,10 @@ public class PlayerControllerAi extends PlayerController {
}
}
}
final Combat combat = getGame().getCombat();
final Combat combat = game.getCombat();
if (combat != null) {
if (getGame().stack.size() == 1) {
SpellAbility topstack = getGame().stack.peekAbility();
if (game.stack.size() == 1) {
SpellAbility topstack = game.stack.peekAbility();
if (topstack.getSubAbility() == sa) {
hostsa = topstack;
}
@@ -957,7 +847,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
}
final PhaseHandler ph = getGame().getPhaseHandler();
final PhaseHandler ph = game.getPhaseHandler();
if (ph.getPlayerTurn() == sa.getActivatingPlayer() && ph.getPhase() == PhaseType.MAIN1 && sa.getTargetCard() != null) {
AiAttackController aiAtk = new AiAttackController(sa.getActivatingPlayer(), sa.getTargetCard());
String s = aiAtk.toProtectAttacker(sa);
@@ -972,7 +862,7 @@ public class PlayerControllerAi extends PlayerController {
list.addAll(opp.getCreaturesInPlay());
}
if (list.isEmpty()) {
list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents());
list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
}
if (!list.isEmpty()) {
choice = ComputerUtilCard.getMostProminentColor(list);
@@ -986,10 +876,8 @@ public class PlayerControllerAi extends PlayerController {
final Card source = sa.getHostCard();
final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } };
emptyAbility.setActivatingPlayer(player);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setSVars(sa.getSVars());
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) {
ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost
ComputerUtil.playNoStack(player, emptyAbility, game); // AI needs something to resolve to pay that cost
return true;
}
return false;
@@ -998,34 +886,8 @@ public class PlayerControllerAi extends PlayerController {
@Override
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {
if (sa.isTrigger()) {
if (prepareSingleSa(sa.getHostCard(), sa, true)) {
ComputerUtil.playStack(sa, player, getGame());
}
} else {
if (sa.isCopied()) {
if (sa.isSpell()) {
if (!sa.getHostCard().isInZone(ZoneType.Stack)) {
sa.setHostCard(player.getGame().getAction().moveToStack(sa.getHostCard(), sa));
} else {
player.getGame().getStackZone().add(sa.getHostCard());
}
}
/* FIXME: the new implementation (below) requires implementing setupNewTargets in the AI controller, among other possible changes, otherwise breaks AI
if (sa.isMayChooseNewTargets()) {
sa.setupNewTargets(player);
}
*/
if (sa.isMayChooseNewTargets() && !sa.setupTargets()) {
if (sa.isSpell()) {
sa.getHostCard().ceaseToExist();
}
continue;
}
}
// need finally add the new spell to the stack
player.getGame().getStack().add(sa);
if (prepareSingleSa(sa.getHostCard(),sa,true)) {
ComputerUtil.playStack(sa, player, game);
}
}
}
@@ -1043,7 +905,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public void playTrigger(Card host, WrappedAbility wrapperAbility, boolean isMandatory) {
if (prepareSingleSa(host, wrapperAbility, isMandatory)) {
ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, getGame());
ComputerUtil.playNoStack(wrapperAbility.getActivatingPlayer(), wrapperAbility, game);
}
}
@@ -1055,9 +917,9 @@ public class PlayerControllerAi extends PlayerController {
Spell spell = (Spell) tgtSA;
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
if (noManaCost) {
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, game);
} else {
return ComputerUtil.playStack(tgtSA, player, getGame());
return ComputerUtil.playStack(tgtSA, player, game);
}
} else
return false; // didn't play spell
@@ -1065,6 +927,11 @@ public class PlayerControllerAi extends PlayerController {
return true;
}
@Override
public Map<GameEntity, CounterType> chooseProliferation(SpellAbility sa) {
return brains.chooseProliferation(sa);
}
@Override
public boolean chooseTargetsFor(SpellAbility currentAbility) {
return brains.doTrigger(currentAbility, true);
@@ -1141,7 +1008,7 @@ public class PlayerControllerAi extends PlayerController {
}
});
} else {
return new HashMap<>();
return new HashMap<Card, ManaCostShard>();
}
}
@@ -1167,7 +1034,7 @@ public class PlayerControllerAi extends PlayerController {
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) {
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = player.getWeakestOpponent().getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = ComputerUtil.getOpponentFor(player).getCardsIn(ZoneType.Library);
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic");
@@ -1189,21 +1056,20 @@ public class PlayerControllerAi extends PlayerController {
} else if (logic.equals("MostProminentInHumanDeck")) {
return ComputerUtilCard.getMostProminentCardName(oppLibrary);
} else if (logic.equals("MostProminentCreatureInComputerDeck")) {
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard(), sa);
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard());
return ComputerUtilCard.getMostProminentCardName(cards);
} else if (logic.equals("BestCreatureInComputerDeck")) {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiLibrary);
return bestCreature != null ? bestCreature.getName() : "Plains";
return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName();
} else if (logic.equals("RandomInComputerDeck")) {
return Aggregates.random(aiLibrary).getName();
} else if (logic.equals("MostProminentSpellInComputerDeck")) {
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Card.Instant,Card.Sorcery", player, sa.getHostCard(), sa);
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Card.Instant,Card.Sorcery", player, sa.getHostCard());
return ComputerUtilCard.getMostProminentCardName(cards);
} else if (logic.equals("CursedScroll")) {
return SpecialCardAi.CursedScroll.chooseCard(player, sa);
}
} else {
CardCollectionView list = CardLists.filterControlledBy(getGame().getCardsInGame(), player.getOpponents());
CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
list = CardLists.filter(list, Predicates.not(Presets.LANDS));
if (!list.isEmpty()) {
return list.get(0).getName();
@@ -1224,7 +1090,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List<Card> chooseCardsForZoneChange(
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max,
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList,
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
// this isn't used
return null;
@@ -1284,24 +1150,12 @@ public class PlayerControllerAi extends PlayerController {
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen,
List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();
Cost costSoFar = chosen.getPayCosts() != null ? chosen.getPayCosts().copy() : Cost.Zero;
for (OptionalCostValue opt : optionalCostValues) {
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
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 = CardUtil.getLKICopy(chosen.getHostCard());
copy.addOptionalCostPaid(opt.getType());
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
continue; // don't choose kickers we don't want to play
}
}
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
@@ -1310,47 +1164,4 @@ public class PlayerControllerAi extends PlayerController {
return chosenOptCosts;
}
@Override
public boolean confirmMulliganScry(Player p) {
// Always true?
return true;
}
@Override
public int chooseNumberForKeywordCost(SpellAbility sa, Cost cost, KeywordInterface keyword, String prompt,
int max) {
// TODO: improve the logic depending on the keyword and the playability of the cost-modified SA (enough targets present etc.)
int chosenAmount = 0;
Cost costSoFar = sa.getPayCosts().copy();
for (int i = 0; i < max; i++) {
costSoFar.add(cost);
SpellAbility fullCostSa = sa.copyWithDefinedCost(costSoFar);
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenAmount++;
} else {
break;
}
}
return chosenAmount;
}
@Override
public CardCollection chooseCardsForEffectMultiple(Map<String, CardCollection> validMap, SpellAbility sa, String title, boolean isOptional) {
CardCollection choices = new CardCollection();
for (String mapKey: validMap.keySet()) {
CardCollection cc = validMap.get(mapKey);
cc.removeAll(choices);
Card chosen = ComputerUtilCard.getBestAI(cc);
if (chosen != null) {
choices.add(chosen);
}
}
return choices;
}
}

View File

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

View File

@@ -17,33 +17,19 @@
*/
package forge.ai;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GameType;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPart;
@@ -54,7 +40,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.SpellPermanent;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
@@ -64,6 +49,11 @@ import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Special logic for individual cards
@@ -104,8 +94,12 @@ public class SpecialCardAi {
int minCMC = isLowCMCDeck ? 3 : 4; // probably not worth wasting a lotus on a low-CMC spell (<4 CMC), except in low-CMC decks, where 3 CMC may be fine
int paidCMC = cost.getConvertedManaCost();
if (paidCMC < minCMC) {
if (paidCMC == 3 && numManaSrcs < 3) {
// if it's a CMC 3 spell and we're more than one mana source short for it, might be worth it anyway
return paidCMC == 3 && numManaSrcs < 3;
return true;
}
return false;
}
return true;
@@ -153,29 +147,6 @@ public class SpecialCardAi {
}
}
// Crawling Barrens
public static class CrawlingBarrens {
public static boolean consider(final Player ai, final SpellAbility sa) {
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final Combat combat = ai.getGame().getCombat();
Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa);
animated.addType("Creature");
if (sa.getHostCard().canReceiveCounters(CounterEnumType.P1P1)) {
animated.addCounter(CounterEnumType.P1P1, 2, ai, false, null);
}
boolean isOppEOT = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated);
return isOppEOT || isValuableAttacker || isValuableBlocker;
}
public static SpellAbility considerAnimating(final Player ai, final SpellAbility sa, final List<SpellAbility> options) {
return ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) ? options.get(0) : options.get(1);
}
}
// Cursed Scroll
public static class CursedScroll {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -201,7 +172,7 @@ public class SpecialCardAi {
}
}
return best != null ? best.getName() : "";
return best.getName();
}
}
@@ -227,7 +198,7 @@ public class SpecialCardAi {
sa.getTargets().add(worstCreat);
}
return sa.getTargets().size() > 0;
return sa.getTargets().getNumTargeted() > 0;
}
}
@@ -247,7 +218,11 @@ public class SpecialCardAi {
}
}
return ai.getLife() <= sa.getHostCard().getNetPower() && !hasUsefulBlocker;
if (ai.getLife() <= sa.getHostCard().getNetPower() && !hasUsefulBlocker) {
return true;
} else {
return false;
}
}
public static int getSacThreshold() {
@@ -360,7 +335,7 @@ public class SpecialCardAi {
boolean canTrample = source.hasKeyword(Keyword.TRAMPLE);
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = combat.getDefenderByAttacker(source).getCounters(CounterEnumType.LOYALTY);
int loyalty = ((Card)combat.getDefenderByAttacker(source)).getCounters(CounterType.LOYALTY);
int totalDamageToPW = 0;
for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) {
if (combat.isUnblocked(atk)) {
@@ -436,11 +411,15 @@ public class SpecialCardAi {
Pair<Integer, Integer> predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness());
int oppT = Aggregates.sum(potentialBlockers, CardPredicates.Accessors.fnGetNetToughness);
return potentialBlockers.isEmpty() || (source.hasKeyword(Keyword.TRAMPLE) && predictedPT.getLeft() - oppT >= oppLife);
if (potentialBlockers.isEmpty() || (source.hasKeyword(Keyword.TRAMPLE) && predictedPT.getLeft() - oppT >= oppLife)) {
return true;
}
return false;
}
public static Pair<Integer, Integer> getPumpedPT(Player ai, int power, int toughness) {
int energy = ai.getCounters(CounterEnumType.ENERGY);
int energy = ai.getCounters(CounterType.ENERGY);
if (energy > 0) {
int numActivations = energy / 3;
for (int i = 0; i < numActivations; i++) {
@@ -522,49 +501,6 @@ public class SpecialCardAi {
}
}
// Gideon Blackblade
public static class GideonBlackblade {
public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
if (!otb.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(otb));
}
return true;
}
public static SpellAbility chooseSpellAbility(final Player ai, final SpellAbility sa, final List<SpellAbility> spells) {
// TODO: generalize and improve this so that it acts in a more reasonable way and can potentially be used for other cards too
List<SpellAbility> best = Lists.newArrayList();
List<SpellAbility> possible = Lists.newArrayList();
Card tgtCard = sa.getTargetCard();
if (tgtCard != null) {
for (SpellAbility sp : spells) {
if (SpellApiToAi.Converter.get(sp.getApi()).canPlayAIWithSubs(ai, sp)) {
best.add(sp); // these SAs are prioritized since the AI sees a reason to play them now
}
final List<String> keywords = sp.hasParam("KW") ? Arrays.asList(sp.getParam("KW").split(" & "))
: Lists.newArrayList();
for (String kw : keywords) {
if (!tgtCard.hasKeyword(kw)) {
if ("Indestructible".equals(kw) && ai.getOpponents().getCreaturesInPlay().isEmpty()) {
continue; // nothing to damage or kill the creature with
}
possible.add(sp); // these SAs at least don't duplicate a keyword on the card
break;
}
}
}
}
if (!best.isEmpty()) {
return Aggregates.random(best);
} else if (!possible.isEmpty()) {
return Aggregates.random(possible);
} else {
return Aggregates.random(spells); // if worst comes to worst, it's a PW +1 ability, so do at least something
}
}
}
// Guilty Conscience
public static class GuiltyConscience {
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
@@ -589,7 +525,10 @@ public class SpecialCardAi {
@Override
public boolean apply(final Card c) {
// Don't enchant creatures that can survive
return c.canBeDestroyed() && c.getNetCombatDamage() >= c.getNetToughness() && !c.isEnchantedBy("Guilty Conscience");
if (!c.canBeDestroyed() || c.getNetCombatDamage() < c.getNetToughness() || c.isEnchantedBy("Guilty Conscience")) {
return false;
}
return true;
}
});
chosen = ComputerUtilCard.getBestCreatureAI(creatures);
@@ -668,7 +607,10 @@ public class SpecialCardAi {
boolean canRetFromGrave = false;
String name = c.getName().replace(',', ';');
for (Trigger t : c.getTriggers()) {
SpellAbility ab = t.ensureAbility();
SpellAbility ab = null;
if (t.hasParam("Execute")) {
ab = AbilityFactory.getAbility(c.getSVar(t.getParam("Execute")), c);
}
if (ab == null) { continue; }
if (ab.getApi() == ApiType.ChangeZone
@@ -738,7 +680,7 @@ public class SpecialCardAi {
// if there's another reanimator card currently suspended, don't cast a new one until the previous
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) {
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterType.TIME) > 0) {
return false;
}
}
@@ -786,37 +728,6 @@ public class SpecialCardAi {
}
}
// Maze's End
public static class MazesEnd {
public static boolean consider(final Player ai, final SpellAbility sa) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty();
}
public static Card considerCardToGet(final Player ai, final SpellAbility sa)
{
CardCollection currentGates = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Gate"));
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
if (availableGates.isEmpty())
return null; // shouldn't get here
for (Card gate : availableGates)
{
if (CardLists.filter(currentGates, CardPredicates.nameEquals(gate.getName())).isEmpty())
{
// Diversify our mana base
return gate;
}
}
// Fetch a random gate if we already have all types
return Aggregates.random(availableGates);
}
}
// Mairsil, the Pretender
public static class MairsilThePretender {
// Scan the fetch list for a card with at least one activated ability.
@@ -828,7 +739,7 @@ public class SpecialCardAi {
Player controller = c.getController();
boolean wasCaged = false;
for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile),
CardPredicates.hasCounter(CounterEnumType.CAGE))) {
CardPredicates.hasCounter(CounterType.CAGE))) {
if (c.getName().equals(caged.getName())) {
wasCaged = true;
break;
@@ -894,7 +805,7 @@ public class SpecialCardAi {
}
// Set PayX here to maximum value.
int tokenSize = ComputerUtilCost.getMaxXValue(sa, ai);
int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai);
// Some basic strategy for Momir
if (tokenSize < 2) {
@@ -905,7 +816,7 @@ public class SpecialCardAi {
tokenSize = 11;
}
sa.setXManaCostPaid(tokenSize);
source.setSVar("PayX", Integer.toString(tokenSize));
return true;
}
@@ -965,6 +876,7 @@ public class SpecialCardAi {
// Only activate in AI's own turn (sans the exception above)
return false;
}
return true;
}
}
@@ -986,7 +898,11 @@ public class SpecialCardAi {
}
// Maybe use it for some important high-impact spells even if there are more cards in hand?
return ai.getCardsIn(ZoneType.Hand).size() <= 1 || hasEnsnaringBridgeEffect;
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && !hasEnsnaringBridgeEffect) {
return false;
}
return true;
}
}
@@ -1000,7 +916,7 @@ public class SpecialCardAi {
return false;
}
String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield));
int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), "Count$Devotion." + prominentColor, sa);
int devotion = CardFactoryUtil.xCount(sa.getHostCard(), "Count$Devotion." + prominentColor);
int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0);
// do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage)
@@ -1134,7 +1050,7 @@ public class SpecialCardAi {
// Sarkhan the Mad
public static class SarkhanTheMad {
public static boolean considerDig(final Player ai, final SpellAbility sa) {
return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1;
return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1;
}
public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) {
@@ -1167,44 +1083,6 @@ public class SpecialCardAi {
}
}
// Sorin, Vengeful Bloodlord
public static class SorinVengefulBloodlord {
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),
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.lessCMC(loyalty - 1), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
final Card copy = CardUtil.getLKICopy(card);
ComputerUtilCard.applyStaticContPT(ai.getGame(), copy, null);
return copy.getNetToughness() > 0;
}
}));
CardLists.sortByCmcDesc(creaturesToGet);
if (creaturesToGet.isEmpty()) {
return false;
}
// pick the best creature that will stay on the battlefield
Card best = creaturesToGet.getFirst();
for (Card c : creaturesToGet) {
if (best != c && ComputerUtilCard.evaluateCreature(c, true, false) >
ComputerUtilCard.evaluateCreature(best, true, false)) {
best = c;
}
}
if (best != null) {
sa.resetTargets();
sa.getTargets().add(best);
return true;
}
return false;
}
}
// Survival of the Fittest
public static class SurvivalOfTheFittest {
public static Card considerDiscardTarget(final Player ai) {
@@ -1272,7 +1150,7 @@ public class SpecialCardAi {
// no options with smaller CMC, so discard the one that is harder to cast for the one that is
// easier to cast right now, but only if the best card in the library is at least CMC 3
// (probably not worth it to grab low mana cost cards this way)
if (maxCMC != null && bestInLib != null && maxCMC.getCMC() < bestInLib.getCMC() && bestInLib.getCMC() >= 3) {
if (maxCMC != null && maxCMC.getCMC() < bestInLib.getCMC() && bestInLib.getCMC() >= 3) {
return maxCMC;
}
// We appear to be playing Reanimator (or we have a reanimator card in hand already), so it's
@@ -1332,7 +1210,7 @@ public class SpecialCardAi {
sa.getTargets().add(worstOwnCreat);
}
return sa.getTargets().size() > 0;
return sa.getTargets().getNumTargeted() > 0;
}
}
@@ -1351,29 +1229,13 @@ public class SpecialCardAi {
}
}
if (aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD) {
// use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand
return aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD;
}
return true;
}
// Timmerian Fiends
public static class TimmerianFiends {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Card targeted = sa.getParentTargetingCard().getTargetCard();
if (targeted == null) {
return false;
}
if (targeted.isCreature()) {
if (ComputerUtil.aiLifeInDanger(ai, true, 0)) {
return true; // do it, hoping to save a valuable potential blocker etc.
}
return ComputerUtilCard.evaluateCreature(targeted) >= 200; // might need tweaking
} else {
// TODO: this currently compares purely by CMC. To be somehow improved, especially for stuff like the Power Nine etc.
return ComputerUtilCard.evaluatePermanentList(new CardCollection(targeted)) >= 3;
}
}
}
// Volrath's Shapeshifter
@@ -1399,7 +1261,9 @@ public class SpecialCardAi {
if (topGY == null
|| !topGY.isCreature()
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0);
if (numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0)) {
return true;
}
}
}
@@ -1426,11 +1290,11 @@ public class SpecialCardAi {
Card source = sa.getHostCard();
Game game = source.getGame();
final int loyalty = source.getCounters(CounterEnumType.LOYALTY);
final int loyalty = source.getCounters(CounterType.LOYALTY);
int x = -1, best = 0;
Card single = null;
for (int i = 0; i < loyalty; i++) {
sa.setXManaCostPaid(i);
sa.setSVar("ChosenX", "Number$" + i);
oppType = CardLists.filterControlledBy(game.getCardsIn(origin), ai.getOpponents());
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(ai.getCardsIn(origin), sa.getParam("ChangeType"), sa);
@@ -1447,8 +1311,13 @@ public class SpecialCardAi {
}
// check if +1 would be sufficient
if (single != null) {
// TODO use better logic to find the right Deal Damage Effect?
SpellAbility ugin_burn = Iterables.find(source.getSpellAbilities(), SpellAbilityPredicates.isApi(ApiType.DealDamage), null);
SpellAbility ugin_burn = null;
for (final SpellAbility s : source.getSpellAbilities()) {
if (s.getApi() == ApiType.DealDamage) {
ugin_burn = s;
break;
}
}
if (ugin_burn != null) {
// basic logic copied from DamageDealAi::dealDamageChooseTgtC
if (ugin_burn.canTarget(single)) {
@@ -1459,17 +1328,17 @@ public class SpecialCardAi {
if (can_kill) {
return false;
}
}
// simple check to burn player instead of exiling planeswalker
if (single.isPlaneswalker() && single.getCurrentLoyalty() <= 3) {
return false;
}
}
}
}
if (x == -1) {
return false;
}
sa.setXManaCostPaid(x);
sa.setSVar("ChosenX", "Number$" + x);
return true;
}
}
@@ -1516,6 +1385,7 @@ public class SpecialCardAi {
// Only activate in AI's own turn (sans the exception above)
return false;
}
return true;
}
}

View File

@@ -1,13 +1,7 @@
package forge.ai;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.card.ICardFace;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
@@ -26,6 +20,10 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
import forge.util.MyRandom;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Base class for API-specific AI logic
* <p>
@@ -79,14 +77,16 @@ public abstract class SpellAbilityAi {
}
}
if (!checkApiLogic(ai, sa)) {
return false;
if (sa.hasParam("AITgtBeforeCostEval")) {
// Cost payment requires a valid target to be specified, e.g. Quillmane Baku, so run the API logic first
// to set the target, then decide on paying costs (slower, so only use for cards where it matters)
return checkApiLogic(ai, sa) && (cost == null || willPayCosts(ai, sa, cost, source));
}
// needs to be after API logic because needs to check possible X Cost?
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return false;
}
return true;
return checkApiLogic(ai, sa);
}
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {
@@ -112,7 +112,7 @@ public abstract class SpellAbilityAi {
if (aiLogic.equals("CheckCondition")) {
SpellAbility saCopy = sa.copy();
saCopy.setActivatingPlayer(ai);
return saCopy.metConditions();
return saCopy.getConditions().areMet(saCopy);
}
return !("Never".equals(aiLogic));
@@ -127,7 +127,7 @@ public abstract class SpellAbilityAi {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
@@ -167,8 +167,7 @@ public abstract class SpellAbilityAi {
// a mandatory SpellAbility with targeting but without candidates,
// does not need to go any deeper
if (sa.usesTargeting() && mandatory && !sa.isTargetNumberValid()
&& !sa.getTargetRestrictions().hasCandidates(sa, true)) {
if (sa.usesTargeting() && mandatory && !sa.getTargetRestrictions().hasCandidates(sa, true)) {
return false;
}
@@ -248,8 +247,7 @@ public abstract class SpellAbilityAi {
protected static boolean isSorcerySpeed(final SpellAbility sa) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|| (sa.isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
|| (sa.getRestrictions().isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
}
/**
@@ -266,7 +264,7 @@ public abstract class SpellAbilityAi {
// TODO probably also consider if winter orb or similar are out
if (sa instanceof AbilitySub) {
if (sa.getPayCosts() == null || sa instanceof AbilitySub) {
return true; // This is only true for Drawbacks and triggers
}
@@ -278,7 +276,7 @@ public abstract class SpellAbilityAi {
return true;
}
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
if (sa.getRestrictions().isPwAbility() && phase.is(PhaseType.MAIN2)) {
return true;
}
if (sa.isSpell() && !sa.isBuyBackAbility()) {
@@ -306,7 +304,7 @@ public abstract class SpellAbilityAi {
}
@SuppressWarnings("unchecked")
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer) {
boolean hasPlayer = false;
boolean hasCard = false;
boolean hasPlaneswalker = false;
@@ -323,11 +321,11 @@ public abstract class SpellAbilityAi {
}
if (hasPlayer && hasPlaneswalker) {
return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection<GameEntity>) options, params);
return (T) chooseSinglePlayerOrPlaneswalker(ai, sa, (Collection<GameEntity>) options);
} else if (hasCard) {
return (T) chooseSingleCard(ai, sa, (Collection<Card>) options, isOptional, targetedPlayer, params);
return (T) chooseSingleCard(ai, sa, (Collection<Card>) options, isOptional, targetedPlayer);
} else if (hasPlayer) {
return (T) chooseSinglePlayer(ai, sa, (Collection<Player>) options, params);
return (T) chooseSinglePlayer(ai, sa, (Collection<Player>) options);
}
return null;
@@ -338,17 +336,17 @@ public abstract class SpellAbilityAi {
return spells.get(0);
}
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleCard is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(options, null);
}
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayer is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(options, null);
}
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options, Map<String, Object> params) {
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSinglePlayerOrPlaneswalker is used for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(options, null);
}

View File

@@ -1,14 +1,13 @@
package forge.ai;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import forge.ai.ability.*;
import forge.game.ability.ApiType;
import forge.util.ReflectionUtil;
import java.util.Map;
public enum SpellApiToAi {
Converter;
@@ -22,7 +21,6 @@ public enum SpellApiToAi {
.put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class)
.put(ApiType.AddPhase, AddPhaseAi.class)
.put(ApiType.AddTurn, AddTurnAi.class)
.put(ApiType.Amass, AmassAi.class)
.put(ApiType.Animate, AnimateAi.class)
.put(ApiType.AnimateAll, AnimateAllAi.class)
.put(ApiType.Attach, AttachAi.class)
@@ -34,16 +32,14 @@ public enum SpellApiToAi {
.put(ApiType.BidLife, BidLifeAi.class)
.put(ApiType.Bond, BondAi.class)
.put(ApiType.Branch, AlwaysPlayAi.class)
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
.put(ApiType.ChangeX, AlwaysPlayAi.class)
.put(ApiType.ChangeZone, ChangeZoneAi.class)
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
.put(ApiType.Charm, CharmAi.class)
.put(ApiType.ChooseCard, ChooseCardAi.class)
.put(ApiType.ChooseColor, ChooseColorAi.class)
.put(ApiType.ChooseDirection, ChooseDirectionAi.class)
.put(ApiType.ChooseEvenOdd, ChooseEvenOddAi.class)
.put(ApiType.ChooseNumber, ChooseNumberAi.class)
.put(ApiType.ChoosePlayer, ChoosePlayerAi.class)
.put(ApiType.ChooseSource, ChooseSourceAi.class)
@@ -64,7 +60,6 @@ public enum SpellApiToAi {
.put(ApiType.Destroy, DestroyAi.class)
.put(ApiType.DestroyAll, DestroyAllAi.class)
.put(ApiType.Dig, DigAi.class)
.put(ApiType.DigMultiple, DigMultipleAi.class)
.put(ApiType.DigUntil, DigUntilAi.class)
.put(ApiType.Discard, DiscardAi.class)
.put(ApiType.DrainMana, DrainManaAi.class)
@@ -72,7 +67,6 @@ public enum SpellApiToAi {
.put(ApiType.EachDamage, DamageEachAi.class)
.put(ApiType.Effect, EffectAi.class)
.put(ApiType.Encode, EncodeAi.class)
.put(ApiType.EndCombatPhase, EndTurnAi.class)
.put(ApiType.EndTurn, EndTurnAi.class)
.put(ApiType.ExchangeLife, LifeExchangeAi.class)
.put(ApiType.ExchangeLifeVariant, LifeExchangeVariantAi.class)
@@ -85,15 +79,14 @@ public enum SpellApiToAi {
.put(ApiType.FlipACoin, FlipACoinAi.class)
.put(ApiType.Fog, FogAi.class)
.put(ApiType.GainControl, ControlGainAi.class)
.put(ApiType.GainControlVariant, ControlGainVariantAi.class)
.put(ApiType.GainControlVariant, AlwaysPlayAi.class)
.put(ApiType.GainLife, LifeGainAi.class)
.put(ApiType.GainOwnership, CannotPlayAi.class)
.put(ApiType.GameDrawn, CannotPlayAi.class)
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
.put(ApiType.Goad, GoadAi.class)
.put(ApiType.Haunt, HauntAi.class)
.put(ApiType.ImmediateTrigger, ImmediateTriggerAi.class)
.put(ApiType.Investigate, InvestigateAi.class)
.put(ApiType.ImmediateTrigger, AlwaysPlayAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class)
.put(ApiType.LosesGame, GameLossAi.class)
.put(ApiType.Mana, ManaEffectAi.class)
@@ -106,7 +99,6 @@ public enum SpellApiToAi {
.put(ApiType.MultiplyCounter, CountersMultiplyAi.class)
.put(ApiType.MustAttack, MustAttackAi.class)
.put(ApiType.MustBlock, MustBlockAi.class)
.put(ApiType.Mutate, MutateAi.class)
.put(ApiType.NameCard, ChooseCardNameAi.class)
.put(ApiType.NoteCounters, AlwaysPlayAi.class)
.put(ApiType.PeekAndReveal, PeekAndRevealAi.class)
@@ -143,9 +135,8 @@ public enum SpellApiToAi {
.put(ApiType.Reveal, RevealAi.class)
.put(ApiType.RevealHand, RevealHandAi.class)
.put(ApiType.ReverseTurnOrder, AlwaysPlayAi.class)
.put(ApiType.RollDice, RollDiceAi.class)
.put(ApiType.RollPlanarDice, RollPlanarDiceAi.class)
.put(ApiType.RunChaos, AlwaysPlayAi.class)
.put(ApiType.RunSVarAbility, AlwaysPlayAi.class)
.put(ApiType.Sacrifice, SacrificeAi.class)
.put(ApiType.SacrificeAll, SacrificeAllAi.class)
.put(ApiType.Scry, ScryAi.class)
@@ -153,10 +144,9 @@ public enum SpellApiToAi {
.put(ApiType.SetLife, LifeSetAi.class)
.put(ApiType.SetState, SetStateAi.class)
.put(ApiType.Shuffle, ShuffleAi.class)
.put(ApiType.SkipPhase, SkipPhaseAi.class)
.put(ApiType.SkipTurn, SkipTurnAi.class)
.put(ApiType.StoreMap, StoreMapAi.class)
.put(ApiType.StoreSVar, StoreSVarAi.class)
.put(ApiType.Subgame, AlwaysPlayAi.class)
.put(ApiType.Surveil, SurveilAi.class)
.put(ApiType.Tap, TapAi.class)
.put(ApiType.TapAll, TapAllAi.class)

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -13,6 +11,9 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@Override
@@ -21,7 +22,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
@@ -45,7 +46,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
@@ -56,9 +57,12 @@ public class ActivateAbilityAi extends SpellAbilityAi {
} else {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
return defined.contains(opp);
if (!defined.contains(opp)) {
return false;
}
}
return true;
} else {
sa.resetTargets();
sa.getTargets().add(opp);
@@ -83,7 +87,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
}
} else {
sa.resetTargets();
sa.getTargets().add(ai.getWeakestOpponent());
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
}
return randomReturn;

View File

@@ -17,15 +17,15 @@
*/
package forge.ai.ability;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
/**
* <p>
* AbilityFactory_Turns class.
@@ -67,8 +67,10 @@ public class AddTurnAi extends SpellAbilityAi {
return false;
}
}
if (!StringUtils.isNumeric(sa.getParam("NumTurns"))) {
// TODO: improve ai for Sage of Hours
return StringUtils.isNumeric(sa.getParam("NumTurns"));
return false;
}
// not sure if the AI should be playing with cards that give the
// Human more turns.
}

View File

@@ -1,101 +0,0 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.token.TokenInfo;
import forge.game.phase.PhaseHandler;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class AmassAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
CardCollection aiArmies = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army"));
Card host = sa.getHostCard();
final Game game = ai.getGame();
if (!aiArmies.isEmpty()) {
return CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterEnumType.P1P1)) > 0;
}
final String tokenScript = "b_0_0_zombie_army";
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
Card token = TokenInfo.getProtoType(tokenScript, sa, false);
if (token == null) {
return false;
}
token.setController(ai, 0);
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
boolean result = true;
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(token);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
if (token.canReceiveCounters(CounterEnumType.P1P1)) {
token.setCounters(CounterEnumType.P1P1, amount);
}
if (token.isCreature() && token.getNetToughness() < 1) {
result = false;
}
//reset static abilities
game.getAction().checkStaticAbilities(false);
return result;
}
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
// TODO: Special check for instant speed logic? Something like Lazotep Plating.
/*
boolean isInstant = sa.getRestrictions().isInstantSpeed();
CardCollection aiArmies = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Army"));
if (isInstant) {
}
*/
return true;
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || checkApiLogic(ai, sa);
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
Iterable<Card> better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
if (Iterables.isEmpty(better)) {
better = options;
}
return ComputerUtilCard.getBestAI(better);
}
}

View File

@@ -1,40 +1,34 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
import forge.card.CardType;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.AiCardMemory;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardType;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.AnimateEffectBase;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardTraitChanges;
import forge.game.card.CardUtil;
import forge.game.cost.CostPutCounter;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.zone.ZoneType;
/**
* <p>
@@ -48,13 +42,15 @@ import forge.game.zone.ZoneType;
public class AnimateAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
if ("Attacking".equals(aiLogic)) { // Launch the Fleet
if (ph.getPlayerTurn().isOpponentOf(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
List<Card> list = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
List<Card> list = CardLists.getValidCards(ai.getCreaturesInPlay(), tgt.getValidTgts(), ai, source, sa);
for (Card c : list) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, c)) {
sa.getTargets().add(c);
@@ -81,13 +77,13 @@ public class AnimateAi extends SpellAbilityAi {
num = (num == null) ? "1" : num;
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
ai.getWeakestOpponent(), topStack.getHostCard(), topStack);
ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
ComputerUtilCard.sortByEvaluateCreature(list);
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
Card animatedCopy = becomeAnimated(source, sa);
list.add(animatedCopy);
list = CardLists.getValidCards(list, valid.split(","), ai.getWeakestOpponent(), topStack.getHostCard(),
list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(),
topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
@@ -113,16 +109,11 @@ public class AnimateAi extends SpellAbilityAi {
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) {
return false;
}
// Don't animate if the AI won't attack anyway or use as a potential blocker
// Don't animate if the AI won't attack anyway
Player opponent = ai.getWeakestOpponent();
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
// the AI will waste resources
boolean activateAsPotentialBlocker = sa.hasParam("UntilYourNextTurn")
&& ai.getGame().getPhaseHandler().getNextTurn() != ai
&& source.isPermanent();
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) {
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent")) {
return false;
}
return true;
@@ -133,7 +124,7 @@ public class AnimateAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
final Game game = aiPlayer.getGame();
final PhaseHandler ph = game.getPhaseHandler();
if (!sa.metConditions() && sa.getSubAbility() == null) {
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
return false; // what is this for?
}
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
@@ -210,16 +201,21 @@ public class AnimateAi extends SpellAbilityAi {
return bFlag; // All of the defined stuff is animated, not very useful
} else {
sa.resetTargets();
return animateTgtAI(sa);
if (!animateTgtAI(sa)) {
return false;
}
}
return true;
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
if (!animateTgtAI(sa)) {
return false;
}
}
return true;
@@ -232,7 +228,10 @@ public class AnimateAi extends SpellAbilityAi {
} else if (sa.usesTargeting() && mandatory) {
// fallback if animate is mandatory
sa.resetTargets();
List<Card> list = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa);
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
CardCollectionView list = aiPlayer.getGame().getCardsIn(tgt.getZone());
list = CardLists.getValidCards(list, tgt.getValidTgts(), aiPlayer, source, sa);
if (list.isEmpty()) {
return false;
}
@@ -246,26 +245,26 @@ public class AnimateAi extends SpellAbilityAi {
private boolean animateTgtAI(final SpellAbility sa) {
final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker")
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
&& sa.getTargetRestrictions() != null
&& sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0;
final CardType types = new CardType(true);
final CardType types = new CardType();
if (sa.hasParam("Types")) {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
// something is used for animate into creature
if (types.isCreature()) {
final Game game = ai.getGame();
CardCollectionView list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
CardCollectionView list = ai.getGame().getCardsIn(tgt.getZone());
list = CardLists.getValidCards(list, tgt.getValidTgts(), ai, source, sa);
// need to targetable
list = CardLists.getTargetableCards(list, sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
// list is empty, no possible targets
if (list.isEmpty() && !alwaysActivatePWAbility) {
if (list.isEmpty()) {
return false;
}
@@ -299,7 +298,7 @@ public class AnimateAi extends SpellAbilityAi {
// evaluate their value to check if it becomes better
if (c.isCreature()) {
int cValue = ComputerUtilCard.evaluateCreature(c);
if (cValue >= aValue)
if (cValue <= aValue)
continue;
}
@@ -318,7 +317,7 @@ public class AnimateAi extends SpellAbilityAi {
}
// data is empty, no good targets
if (data.isEmpty() && !alwaysActivatePWAbility) {
if (data.isEmpty()) {
return false;
}
@@ -336,18 +335,8 @@ public class AnimateAi extends SpellAbilityAi {
// select the worst of the best
final Card worst = ComputerUtilCard.getWorstAI(maxList);
if (worst != null) {
if (worst.isLand()) {
// e.g. Clan Guildmage, make sure we're not using the same land we want to animate to activate the ability
this.holdAnimatedTillMain2(ai, worst);
if (!ComputerUtilMana.canPayManaCost(sa, ai, 0)) {
this.releaseHeldTillMain2(ai, worst);
return false;
}
}
this.rememberAnimatedThisTurn(ai, worst);
sa.getTargets().add(worst);
}
return true;
}
@@ -369,6 +358,7 @@ public class AnimateAi extends SpellAbilityAi {
// duplicating AnimateEffect.resolve
final Card source = sa.getHostCard();
final Game game = sa.getActivatingPlayer().getGame();
final Map<String, String> svars = source.getSVars();
final long timestamp = game.getNextTimestamp();
card.setSickness(hasOriginalCardSickness);
@@ -376,24 +366,18 @@ public class AnimateAi extends SpellAbilityAi {
Integer power = null;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
if (power == 0 && "PTByCMC".equals(sa.getParam("AILogic"))) {
power = card.getManaCost().getCMC();
}
}
Integer toughness = null;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
if (toughness == 0 && "PTByCMC".equals(sa.getParam("AILogic"))) {
toughness = card.getManaCost().getCMC();
}
}
final CardType types = new CardType(true);
final CardType types = new CardType();
if (sa.hasParam("Types")) {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
final CardType removeTypes = new CardType(true);
final CardType removeTypes = new CardType();
if (sa.hasParam("RemoveTypes")) {
removeTypes.addAll(Arrays.asList(sa.getParam("RemoveTypes").split(",")));
}
@@ -401,7 +385,7 @@ public class AnimateAi extends SpellAbilityAi {
// allow ChosenType - overrides anything else specified
if (types.hasSubtype("ChosenType")) {
types.clear();
types.addAll(sa.getChosenType());
types.add(source.getChosenType());
}
final List<String> keywords = Lists.newArrayList();
@@ -421,8 +405,8 @@ public class AnimateAi extends SpellAbilityAi {
// allow SVar substitution for keywords
for (int i = 0; i < keywords.size(); i++) {
final String k = keywords.get(i);
if (source.hasSVar(k)) {
keywords.add(source.getSVar(k));
if (svars.containsKey(k)) {
keywords.add(svars.get(k));
keywords.remove(k);
}
}
@@ -432,7 +416,7 @@ public class AnimateAi extends SpellAbilityAi {
if (sa.hasParam("Colors")) {
final String colors = sa.getParam("Colors");
if (colors.equals("ChosenColor")) {
tmpDesc = CardUtil.getShortColorsString(sa.getChosenColors());
tmpDesc = CardUtil.getShortColorsString(source.getChosenColors());
} else {
tmpDesc = CardUtil.getShortColorsString(Lists.newArrayList(Arrays.asList(colors.split(","))));
}
@@ -469,16 +453,81 @@ public class AnimateAi extends SpellAbilityAi {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
}
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc,
keywords, removeKeywords, hiddenKeywords,
abilities, triggers, replacements, stAbs,
timestamp);
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, keywords, removeKeywords, hiddenKeywords, timestamp);
// check if animate added static Abilities
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp);
if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
// back to duplicating AnimateEffect.resolve
// TODO will all these abilities/triggers/replacements/etc. lead to
// memory leaks or unintended effects?
// remove abilities
final List<SpellAbility> removedAbilities = Lists.newArrayList();
boolean clearAbilities = sa.hasParam("OverwriteAbilities");
boolean clearSpells = sa.hasParam("OverwriteSpells");
boolean removeAll = sa.hasParam("RemoveAllAbilities");
boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
if (clearAbilities || clearSpells || removeAll) {
for (final SpellAbility ab : card.getSpellAbilities()) {
if (removeAll
|| (ab.isIntrinsic() && removeIntrinsic && !ab.isBasicLandAbility())
|| (ab.isAbility() && clearAbilities)
|| (ab.isSpell() && clearSpells)) {
card.removeSpellAbility(ab);
removedAbilities.add(ab);
}
}
}
// give abilities
final List<SpellAbility> addedAbilities = Lists.newArrayList();
if (abilities.size() > 0) {
for (final String s : abilities) {
final String actualAbility = source.getSVar(s);
final SpellAbility grantedAbility = AbilityFactory.getAbility(actualAbility, source);
addedAbilities.add(grantedAbility);
card.addSpellAbility(grantedAbility);
}
}
// Grant triggers
final List<Trigger> addedTriggers = Lists.newArrayList();
if (triggers.size() > 0) {
for (final String s : triggers) {
final String actualTrigger = source.getSVar(s);
final Trigger parsedTrigger = TriggerHandler.parseTrigger(actualTrigger, source, false);
addedTriggers.add(card.addTrigger(parsedTrigger));
}
}
// give replacement effects
final List<ReplacementEffect> addedReplacements = Lists.newArrayList();
if (replacements.size() > 0) {
for (final String s : replacements) {
final String actualReplacement = source.getSVar(s);
final ReplacementEffect parsedReplacement = ReplacementHandler.parseReplacement(actualReplacement,
source, false);
addedReplacements.add(card.addReplacementEffect(parsedReplacement));
}
}
// suppress triggers from the animated card
final List<Trigger> removedTriggers = Lists.newArrayList();
if (sa.hasParam("OverwriteTriggers") || removeAll || removeIntrinsic) {
for (final Trigger trigger : card.getTriggers()) {
if (removeIntrinsic && !trigger.isIntrinsic()) {
continue;
}
trigger.setSuppressed(true);
removedTriggers.add(trigger);
}
}
// give static abilities (should only be used by cards to give
// itself a static ability)
if (stAbs.size() > 0) {
for (final String s : stAbs) {
final String actualAbility = source.getSVar(s);
final StaticAbility stAb = card.addStaticAbility(actualAbility);
if ("Continuous".equals(stAb.getMapParams().get("Mode"))) {
for (final StaticAbilityLayer layer : stAb.getLayers()) {
StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer);
}
@@ -499,6 +548,30 @@ public class AnimateAi extends SpellAbilityAi {
card.setSVar(name, actualsVar);
}
}
// suppress static abilities from the animated card
final List<StaticAbility> removedStatics = Lists.newArrayList();
if (sa.hasParam("OverwriteStatics") || removeAll || removeIntrinsic) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (removeIntrinsic && !stAb.isIntrinsic()) {
continue;
}
stAb.setTemporarilySuppressed(true);
removedStatics.add(stAb);
}
}
// suppress static abilities from the animated card
final List<ReplacementEffect> removedReplacements = Lists.newArrayList();
if (sa.hasParam("OverwriteReplacements") || removeAll || removeIntrinsic) {
for (final ReplacementEffect re : card.getReplacementEffects()) {
if (removeIntrinsic && !re.isIntrinsic()) {
continue;
}
re.setTemporarilySuppressed(true);
removedReplacements.add(re);
}
}
ComputerUtilCard.applyStaticContPT(game, card, null);
}
@@ -509,12 +582,4 @@ public class AnimateAi extends SpellAbilityAi {
public static boolean isAnimatedThisTurn(Player ai, Card c) {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
}
private void holdAnimatedTillMain2(Player ai, Card c) {
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
}
private void releaseHeldTillMain2(Player ai, Card c) {
AiCardMemory.forgetCard(ai, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
}
}

View File

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

View File

@@ -1,39 +1,17 @@
package forge.ai.ability;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
@@ -53,6 +31,11 @@ import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class AttachAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -89,6 +72,11 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !"Curse".equals(sa.getParam("AILogic"))) {
return false;
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
@@ -108,21 +96,20 @@ public class AttachAi extends SpellAbilityAi {
if (ai.getController().isAI()) {
advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC);
}
if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai)))
&& source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
if (source.withFlash(ai) && source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
return false;
}
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
if (abCost.getTotalMana().countX() > 0 && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. (Endless Scream and Venarian
// Gold)
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (xPay == 0) {
return false;
}
sa.setXManaCostPaid(xPay);
source.setSVar("PayX", Integer.toString(xPay));
}
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {
@@ -136,7 +123,9 @@ public class AttachAi extends SpellAbilityAi {
return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
}
});
return !targets.isEmpty();
if (targets.isEmpty()) {
return false;
}
}
return true;
@@ -244,15 +233,10 @@ public class AttachAi extends SpellAbilityAi {
boolean alternativeConsiderations = hasFloatMana || willDiscardNow || willDieNow || willRespondToStack || willCastAtEOT || willCastEarly;
if (!alternativeConsiderations) {
if (combat == null ||
game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
if (!alternativeConsiderations && (combat == null || game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) || (!combat.isAttacking(attachTarget) && !combat.isBlocking(attachTarget))) {
return false;
}
return combat.isAttacking(attachTarget) || combat.isBlocking(attachTarget);
}
return true;
}
@@ -414,7 +398,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.isCreature() && !c.getType().hasSubtype("Vehicle") && !c.isTapped()) {
// try to identify if this thing can actually tap
for (SpellAbility ab : c.getAllSpellAbilities()) {
if (ab.getPayCosts().hasTapCost()) {
if (ab.getPayCosts() != null && ab.getPayCosts().hasTapCost()) {
return true;
}
}
@@ -464,7 +448,7 @@ public class AttachAi extends SpellAbilityAi {
*/
private static Player attachToPlayerAIPreferences(final Player aiPlayer, final SpellAbility sa,
final boolean mandatory) {
List<Player> targetable = new ArrayList<>();
List<Player> targetable = new ArrayList<Player>();
for (final Player player : aiPlayer.getGame().getPlayers()) {
if (sa.canTarget(player)) {
targetable.add(player);
@@ -576,7 +560,7 @@ public class AttachAi extends SpellAbilityAi {
@Override
public boolean apply(final Card c) {
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
if (sa.isAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
return false;
}
}
@@ -854,7 +838,7 @@ public class AttachAi extends SpellAbilityAi {
* @return the card
*/
private static Card attachAICursePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource, final Player ai) {
final Card attachSource) {
// AI For choosing a Card to Curse of.
// TODO Figure out some way to combine The "gathering of data" from
@@ -868,7 +852,7 @@ public class AttachAi extends SpellAbilityAi {
int totToughness = 0;
int totPower = 0;
final List<String> keywords = new ArrayList<>();
final List<String> keywords = new ArrayList<String>();
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams();
@@ -888,11 +872,15 @@ public class AttachAi extends SpellAbilityAi {
String kws = stabMap.get("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
for (final String kw : kws.split(" & ")) {
keywords.add(kw);
}
}
kws = stabMap.get("AddHiddenKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
for (final String kw : kws.split(" & ")) {
keywords.add(kw);
}
}
}
}
@@ -915,7 +903,7 @@ public class AttachAi extends SpellAbilityAi {
Card c = null;
if (prefList == null || prefList.isEmpty()) {
prefList = new ArrayList<>(list);
prefList = new ArrayList<Card>(list);
} else {
c = ComputerUtilCard.getBestAI(prefList);
if (c != null) {
@@ -946,24 +934,6 @@ public class AttachAi extends SpellAbilityAi {
);
}
// If this is already attached and there's a sac cost, make sure we attach to something that's
// seriously better than whatever the attachment is currently attached to (e.g. Bound by Moonsilver)
if (sa.getHostCard().getAttachedTo() != null && sa.getHostCard().getAttachedTo().isCreature()
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
final int oldEvalRating = ComputerUtilCard.evaluateCreature(sa.getHostCard().getAttachedTo());
final int threshold = ai.isAI() ? ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.SAC_TO_REATTACH_TARGET_EVAL_THRESHOLD) : Integer.MAX_VALUE;
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
if (!card.isCreature()) {
return false;
}
return ComputerUtilCard.evaluateCreature(card) >= oldEvalRating + threshold;
}
});
}
c = ComputerUtilCard.getBestAI(prefList);
if (c == null) {
@@ -987,13 +957,13 @@ public class AttachAi extends SpellAbilityAi {
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<>();
List<GameObject> targets = new ArrayList<GameObject>();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
targets = AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
} else {
AttachAi.attachPreference(sa, tgt, mandatory);
targets = sa.getTargets();
targets = sa.getTargets().getTargets();
}
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
@@ -1010,7 +980,9 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
// don't equip creatures that don't gain anything
return !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
if (card.hasSVar("NonStackingAttachEffect") && newTarget.isEquippedBy(card.getName())) {
return false;
}
}
}
@@ -1065,7 +1037,7 @@ public class AttachAi extends SpellAbilityAi {
Card c = null;
List<Card> magnetList = null;
String stCheck = null;
if (attachSource.isAura() || sa.isBestow()) {
if (attachSource.isAura() || sa.hasParam("Bestow")) {
stCheck = "EnchantedBy";
magnetList = CardLists.filter(list, new Predicate<Card>() {
@Override
@@ -1106,7 +1078,12 @@ public class AttachAi extends SpellAbilityAi {
final Map<String, String> params = t.getMapParams();
if ("Card.Self".equals(params.get("ValidCard"))
&& "Battlefield".equals(params.get("Destination"))) {
SpellAbility trigSa = t.ensureAbility();
SpellAbility trigSa = null;
if (t.hasParam("Execute") && attachSource.hasSVar(t.getParam("Execute"))) {
trigSa = AbilityFactory.getAbility(attachSource.getSVar(params.get("Execute")), attachSource);
} else if (t.getOverridingAbility() != null) {
trigSa = t.getOverridingAbility();
}
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
for (Card target : list) {
if (!target.getController().isOpponentOf(ai)) {
@@ -1172,9 +1149,8 @@ public class AttachAi extends SpellAbilityAi {
int totToughness = 0;
int totPower = 0;
final List<String> keywords = new ArrayList<>();
final List<String> keywords = new ArrayList<String>();
boolean grantingAbilities = false;
boolean grantingExtraBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams();
@@ -1193,15 +1169,18 @@ public class AttachAi extends SpellAbilityAi {
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility");
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
String kws = stabMap.get("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
for (final String kw : kws.split(" & ")) {
keywords.add(kw);
}
}
kws = stabMap.get("AddHiddenKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
for (final String kw : kws.split(" & ")) {
keywords.add(kw);
}
}
}
}
@@ -1223,26 +1202,19 @@ public class AttachAi extends SpellAbilityAi {
}
//only add useful keywords unless P/T bonus is significant
if (totToughness + totPower < 4 && (!keywords.isEmpty() || grantingExtraBlock)) {
if (totToughness + totPower < 4 && !keywords.isEmpty()) {
final int pow = totPower;
final boolean extraBlock = grantingExtraBlock;
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (!keywords.isEmpty()) {
for (final String keyword : keywords) {
if (isUsefulAttachKeyword(keyword, c, sa, pow)) {
return true;
}
}
}
if (c.hasKeyword(Keyword.INFECT) && pow >= 2) {
// consider +2 power a significant bonus on Infect creatures
return true;
}
if (extraBlock && CombatUtil.canBlock(c, true) && !c.canBlockAny()) {
return true;
}
return false;
}
@@ -1374,7 +1346,7 @@ public class AttachAi extends SpellAbilityAi {
CardCollection prefList = list;
// Filter AI-specific targets if provided
prefList = ComputerUtil.filterAITgts(sa, aiPlayer, list, true);
prefList = ComputerUtil.filterAITgts(sa, aiPlayer, (CardCollection)list, true);
Card c = attachGeneralAI(aiPlayer, sa, prefList, mandatory, attachSource, sa.getParam("AILogic"));
@@ -1382,7 +1354,7 @@ public class AttachAi extends SpellAbilityAi {
if (c != null && attachSource.isEquipment()
&& attachSource.isEquipping()
&& attachSource.getEquipping().getController() == aiPlayer) {
if (c.equals(attachSource.getEquipping()) && !mandatory) {
if (c.equals(attachSource.getEquipping())) {
// Do not equip if equipping the same card already
return null;
}
@@ -1393,13 +1365,13 @@ public class AttachAi extends SpellAbilityAi {
boolean uselessCreature = ComputerUtilCard.isUselessCreature(aiPlayer, attachSource.getEquipping());
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never") && !mandatory) {
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) {
// Do not equip other creatures if the AI profile does not allow moving equipment around
return null;
} else if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("from_useless_only")) {
// Do not equip other creatures if the AI profile only allows moving equipment from useless creatures
// and the equipped creature is still useful (not non-untapping+tapped and not set to can't attack/block)
if (!uselessCreature && !mandatory) {
if (!uselessCreature) {
return null;
}
}
@@ -1415,13 +1387,13 @@ public class AttachAi extends SpellAbilityAi {
}
// avoid randomly moving the equipment back and forth between several creatures in one turn
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN) && !mandatory) {
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN)) {
return null;
}
// do not equip if the new creature is not significantly better than the previous one (evaluates at least better by evalT)
int evalT = aic.getIntProperty(AiProps.MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD);
if (!decideMoveFromUseless && ComputerUtilCard.evaluateCreature(c) - ComputerUtilCard.evaluateCreature(attachSource.getEquipping()) < evalT && !mandatory) {
if (!decideMoveFromUseless && ComputerUtilCard.evaluateCreature(c) - ComputerUtilCard.evaluateCreature(attachSource.getEquipping()) < evalT) {
return null;
}
}
@@ -1484,7 +1456,7 @@ public class AttachAi extends SpellAbilityAi {
if ("GainControl".equals(logic)) {
c = attachAIControlPreference(sa, prefList, mandatory, attachSource);
} else if ("Curse".equals(logic)) {
c = attachAICursePreference(sa, prefList, mandatory, attachSource, ai);
c = attachAICursePreference(sa, prefList, mandatory, attachSource);
} else if ("Pump".equals(logic)) {
c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource);
} else if ("Curiosity".equals(logic)) {
@@ -1578,51 +1550,86 @@ public class AttachAi extends SpellAbilityAi {
}
if (evasive) {
return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
if (card.getNetCombatDamage() + powerBonus <= 0
|| !ComputerUtilCombat.canAttackNextTurn(card)
|| !canBeBlocked) {
return false;
}
} else if (keyword.equals("Haste")) {
return card.hasSickness() && ph.isPlayerTurn(sa.getActivatingPlayer()) && !card.isTapped()
&& card.getNetCombatDamage() + powerBonus > 0
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& ComputerUtilCombat.canAttackNextTurn(card);
if (!card.hasSickness() || !ph.isPlayerTurn(sa.getActivatingPlayer()) || card.isTapped()
|| card.getNetCombatDamage() + powerBonus <= 0
|| card.hasKeyword("CARDNAME can attack as though it had haste.")
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| !ComputerUtilCombat.canAttackNextTurn(card)) {
return false;
}
} else if (keyword.endsWith("Indestructible")) {
return true;
} else if (keyword.endsWith("Deathtouch") || keyword.endsWith("Wither")) {
return card.getNetCombatDamage() + powerBonus > 0
&& ((canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card))
|| CombatUtil.canBlock(card, true));
if (card.getNetCombatDamage() + powerBonus <= 0
|| ((!canBeBlocked || !ComputerUtilCombat.canAttackNextTurn(card))
&& !CombatUtil.canBlock(card, true))) {
return false;
}
} else if (keyword.equals("Double Strike") || keyword.equals("Lifelink")) {
return card.getNetCombatDamage() + powerBonus > 0
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
if (card.getNetCombatDamage() + powerBonus <= 0
|| (!ComputerUtilCombat.canAttackNextTurn(card) && !CombatUtil.canBlock(card, true))) {
return false;
}
} else if (keyword.equals("First Strike")) {
return card.getNetCombatDamage() + powerBonus > 0 && !card.hasKeyword(Keyword.DOUBLE_STRIKE)
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword(Keyword.DOUBLE_STRIKE)
|| (!ComputerUtilCombat.canAttackNextTurn(card) && !CombatUtil.canBlock(card, true))) {
return false;
}
} else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card)
&& canBeBlocked;
if (card.getNetCombatDamage() + powerBonus <= 0
|| !ComputerUtilCombat.canAttackNextTurn(card)
|| !canBeBlocked) {
return false;
}
} else if (keyword.startsWith("Bushido")) {
return (canBeBlocked && ComputerUtilCombat.canAttackNextTurn(card))
|| CombatUtil.canBlock(card, true);
if ((!canBeBlocked || !ComputerUtilCombat.canAttackNextTurn(card))
&& !CombatUtil.canBlock(card, true)) {
return false;
}
} else if (keyword.equals("Trample")) {
return card.getNetCombatDamage() + powerBonus > 1
&& canBeBlocked
&& ComputerUtilCombat.canAttackNextTurn(card);
if (card.getNetCombatDamage() + powerBonus <= 1
|| !canBeBlocked
|| !ComputerUtilCombat.canAttackNextTurn(card)) {
return false;
}
} else if (keyword.equals("Infect")) {
return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card);
if (card.getNetCombatDamage() + powerBonus <= 0
|| !ComputerUtilCombat.canAttackNextTurn(card)) {
return false;
}
} else if (keyword.equals("Vigilance")) {
return card.getNetCombatDamage() + powerBonus > 0
&& ComputerUtilCombat.canAttackNextTurn(card)
&& CombatUtil.canBlock(card, true);
if (card.getNetCombatDamage() + powerBonus <= 0
|| !ComputerUtilCombat.canAttackNextTurn(card)
|| !CombatUtil.canBlock(card, true)) {
return false;
}
} else if (keyword.equals("Reach")) {
return !card.hasKeyword(Keyword.FLYING) && CombatUtil.canBlock(card, true);
if (card.hasKeyword(Keyword.FLYING) || !CombatUtil.canBlock(card, true)) {
return false;
}
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
if (!CombatUtil.canBlock(card, true) || card.hasKeyword("CARDNAME can block any number of creatures.")
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
return false;
}
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {
return card.hasKeyword(Keyword.DEFENDER) && card.getNetCombatDamage() + powerBonus > 0;
if (!card.hasKeyword(Keyword.DEFENDER) || card.getNetCombatDamage() + powerBonus <= 0) {
return false;
}
} else if (keyword.equals("Shroud") || keyword.equals("Hexproof")) {
return !card.hasKeyword(Keyword.SHROUD) && !card.hasKeyword(Keyword.HEXPROOF);
} else return !keyword.equals("Defender");
if (card.hasKeyword(Keyword.SHROUD) || card.hasKeyword(Keyword.HEXPROOF)) {
return false;
}
} else if (keyword.equals("Defender")) {
return false;
}
return true;
}
/**
@@ -1643,11 +1650,17 @@ public class AttachAi extends SpellAbilityAi {
if (keyword.endsWith("CARDNAME can't attack.") || keyword.equals("Defender")
|| keyword.endsWith("CARDNAME can't attack or block.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
if (!ComputerUtilCombat.canAttackNextTurn(card) || card.getNetCombatDamage() < 1) {
return false;
}
} else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty();
if (!ComputerUtilCombat.canAttackNextTurn(card) || !CombatUtil.canBlock(card, true) || ai.getCreaturesInPlay().isEmpty()) {
return false;
}
} else if (keyword.endsWith("CARDNAME can't block.") || keyword.contains("CantBlock")) {
return CombatUtil.canBlock(card, true);
if (!CombatUtil.canBlock(card, true)) {
return false;
}
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
for (SpellAbility ability : card.getSpellAbilities()) {
if (ability.isAbility()) {
@@ -1656,12 +1669,18 @@ public class AttachAi extends SpellAbilityAi {
}
return false;
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
if (!ComputerUtilCombat.canAttackNextTurn(card) || card.getNetCombatDamage() < 1) {
return false;
}
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 2;
if (!ComputerUtilCombat.canAttackNextTurn(card) || card.getNetCombatDamage() < 2) {
return false;
}
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
if (card.isUntapped()) {
return false;
}
}
return true;
}
@@ -1685,8 +1704,12 @@ public class AttachAi extends SpellAbilityAi {
return true;
}
if (sa.getHostCard().isEquipment() && ComputerUtilCard.isUselessCreature(ai, c)) {
// useless to equip a creature that can't attack or block.
return !sa.getHostCard().isEquipment() || !ComputerUtilCard.isUselessCreature(ai, c);
return false;
}
return true;
}
public static Card doPumpOrCurseAILogic(final Player ai, final SpellAbility sa, final List<Card> list, final String type) {
@@ -1732,12 +1755,12 @@ public class AttachAi extends SpellAbilityAi {
}
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
return attachToCardAIPreferences(ai, sa, true);
}
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
return attachToPlayerAIPreferences(ai, sa, true);
}
}

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
@@ -16,7 +17,7 @@ public class BalanceAi extends SpellAbilityAi {
int diff = 0;
// TODO Add support for multiplayer logic
final Player opp = aiPlayer.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
/* (non-Javadoc)

View File

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

View File

@@ -41,7 +41,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
return false;
}
if (sa.getTargets().size() != 0) {
if (sa.getTargets().getNumTargeted() != 0) {
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
return true;
}
@@ -80,7 +80,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
ManaCost normalizedMana = manaCost.getNormalizedMana();
boolean canPay = ComputerUtilMana.canPayManaCost(new ManaCostBeingPaid(normalizedMana), sa, aiPlayer);
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
&& topSa.getTargets().contains(aiPlayer)) {
&& topSa.getTargets().getTargets().contains(aiPlayer)) {
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
return false;
}

View File

@@ -1,55 +1,23 @@
package forge.ai.ability;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.AiBlockController;
import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialAiLogic;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.ai.*;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -58,7 +26,9 @@ 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 org.apache.commons.lang3.StringUtils;
import java.util.*;
public class ChangeZoneAi extends SpellAbilityAi {
/*
@@ -74,11 +44,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
Card host = sa.getHostCard();
if (host != null && host.hasSVar("AIPreferenceOverride")) {
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
// currently used by SacAndUpgrade logic, might need simplification
host.removeSVar("AIPreferenceOverride");
sa.getHostCard().removeSVar("AIPreferenceOverride");
}
if (aiLogic.equals("BeforeCombat")) {
@@ -92,7 +60,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (aiLogic.equals("PriorityOptionalCost")) {
boolean highPriority = false;
// if we have more than one of these in hand, might not be worth waiting for optional cost payment on the additional copy
highPriority |= CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(host.getName())).size() > 1;
highPriority |= CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(sa.getHostCard().getName())).size() > 1;
// if we are in danger in combat, no need to wait to pay the optional cost
highPriority |= ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat());
@@ -102,65 +70,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false;
}
}
} else if (aiLogic.equals("NoSameCreatureType")) {
final List<ZoneType> origin = Lists.newArrayList();
if (sa.hasParam("Origin")) {
origin.addAll(ZoneType.listValueOf(sa.getParam("Origin")));
} else if (sa.hasParam("TgtZone")) {
origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone")));
}
CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(origin),
sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
final List<String> creatureTypes = Lists.newArrayList();
for (Card c : list) {
creatureTypes.addAll(c.getType().getCreatureTypes());
}
for (String type : creatureTypes) {
int freq = Collections.frequency(creatureTypes, type);
if (freq > 1) {
return false;
}
}
return true;
} else if (aiLogic.equals("Pongify")) {
return SpecialAiLogic.doPongifyLogic(ai, sa);
} else if (aiLogic.equals("Ashiok")) {
final int loyalty = host.getCurrentLoyalty() - 1;
CardCollectionView choices = new CardCollection();
for (int i = loyalty; i >= 0; i--) {
sa.setXManaCostPaid(i);
choices = ai.getGame().getCardsIn(ZoneType.listValueOf(sa.getParam("Origin")));
choices = CardLists.getValidCards(choices, sa.getParam("ChangeType"), host.getController(), host, sa);
if (!choices.isEmpty()) {
return true;
}
}
return false;
} else if (aiLogic.equals("BestCard")) {
CardCollectionView choices = ai.getGame().getCardsIn(ZoneType.listValueOf(sa.getParam("Origin")));
choices = CardLists.getValidCards(choices, sa.getParam("ChangeType"), host.getController(), host, sa);
if (!choices.isEmpty()) {
return true;
}
} else if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(host)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// minimum card advantage unless the hand will be fully reloaded
int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
if (numExiledWithSrc > curHandSize) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(host)) {
// Try to gain some card advantage if the card will die anyway
// TODO: ideally, should evaluate the hand value and not discard good hands to it
return true;
}
}
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
}
return super.checkAiLogic(ai, sa, aiLogic);
@@ -187,25 +96,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
} else if (aiLogic.equals("TheScarabGod")) {
return SpecialCardAi.TheScarabGod.consider(aiPlayer, sa);
} else if (aiLogic.equals("SorinVengefulBloodlord")) {
return SpecialCardAi.SorinVengefulBloodlord.consider(aiPlayer, sa);
} else if (aiLogic.equals("Intuition")) {
// This logic only fills the multiple cards array, the decision to play is made
// separately in hiddenOriginCanPlayAI later.
multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa);
} else if (aiLogic.equals("MazesEnd")) {
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
} else if (aiLogic.equals("Pongify")) {
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
} else if (aiLogic.equals("Ashiok")) {
return true; // If checkAiLogic returns true, then we should be good to go
} else if (aiLogic.equals("BestCard")) {
return true; // If checkAiLogic returns true, then we should be good to go
} else if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
return true; // If checkAiLogic returns true, then we should be good to go
}
}
if (isHidden(sa)) {
return hiddenOriginCanPlayAI(aiPlayer, sa);
}
@@ -306,7 +202,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
ZoneType origin = null;
final Player opponent = ai.getWeakestOpponent();
final Player opponent = ComputerUtil.getOpponentFor(ai);
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) {
@@ -331,7 +227,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostDiscard) {
CostDiscard cd = (CostDiscard) part;
@@ -374,10 +270,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
// don't play if the conditions aren't met, unless it would trigger a beneficial sub-condition
if (!activateForCost && !sa.metConditions()) {
if (!activateForCost && !sa.getConditions().areMet(sa)) {
final AbilitySub abSub = sa.getSubAbility();
if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) {
if (!abSub.metConditions()) {
if (!abSub.getConditions().areMet(abSub)) {
return false;
}
} else {
@@ -409,10 +305,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
String type = sa.getParam("ChangeType");
if (type != null) {
if (type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
if (type.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(xPay);
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
type = type.replace("X", Integer.toString(xPay));
}
}
@@ -420,19 +316,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
for (final Player p : pDefined) {
CardCollectionView list = p.getCardsIn(origin);
// remove cards that won't be seen if library can't be searched
if (!ai.canSearchLibraryWith(sa, p)) {
list = CardLists.filter(list, Predicates.not(CardPredicates.inZone(ZoneType.Library)));
}
if (type != null && p == ai) {
// AI only "knows" about his information
list = CardLists.getValidCards(list, type, source.getController(), source, sa);
list = CardLists.getValidCards(list, type, source.getController(), source);
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (c.getType().isLegendary()) {
return !ai.isCardInPlay(c.getName());
if (ai.isCardInPlay(c.getName())) {
return false;
}
}
return true;
}
@@ -440,7 +333,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
// TODO: prevent ai seaching its own library when Ob Nixilis, Unshackled is in play
if (origin != null && origin.isKnown()) {
list = CardLists.getValidCards(list, type, source.getController(), source, sa);
list = CardLists.getValidCards(list, type, source.getController(), source);
}
if (!activateForCost && list.isEmpty()) {
@@ -454,11 +347,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
String num = sa.getParam("ChangeNum");
if (num != null) {
if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
if (num.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay);
source.setSVar("PayX", Integer.toString(xPay));
}
}
@@ -514,7 +407,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = aiPlayer.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opp)) {
@@ -544,6 +437,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
final Card source = sa.getHostCard();
if (sa.hasParam("AILogic")) {
if (sa.getParam("AILogic").equals("Never")) {
/*
@@ -557,23 +452,23 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
List<ZoneType> origin = new ArrayList<>();
List<ZoneType> origin = new ArrayList<ZoneType>();
if (sa.hasParam("Origin")) {
origin = ZoneType.listValueOf(sa.getParam("Origin"));
}
// this works for hidden because the mana is paid first.
final String type = sa.getParam("ChangeType");
if (type != null && type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
if (type != null && type.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(xPay);
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
}
Iterable<Player> pDefined;
final TargetRestrictions tgt = sa.getTargetRestrictions();
if ((tgt != null) && tgt.canTgtPlayer()) {
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.isCurse()) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
@@ -632,7 +527,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/
private static Card basicManaFixing(final Player ai, final List<Card> list) { // Search for a Basic Land
final CardCollectionView combined = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand));
final List<String> basics = new ArrayList<>();
final List<String> basics = new ArrayList<String>();
// what types can I go get?
for (final String name : MagicColor.Constant.BASIC_LANDS) {
@@ -692,7 +587,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/
private static Card chooseCreature(final Player ai, CardCollection list) {
// Creating a new combat for testing purposes.
final Player opponent = ai.getWeakestOpponent();
final Player opponent = ComputerUtil.getOpponentFor(ai);
Combat combat = new Combat(opponent);
for (Card att : opponent.getCreaturesInPlay()) {
combat.addAttacker(att, ai);
@@ -776,8 +671,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
// only use blink or bounce effects
if (!(destination.equals(ZoneType.Exile)
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
if (!(destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone))
&& !destination.equals(ZoneType.Hand)) {
return false;
}
@@ -811,7 +705,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb);
if (subAb != null && !SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb)) {
return false;
}
return true;
}
/*
@@ -915,6 +813,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = ai.getGame();
final AbilitySub abSub = sa.getSubAbility();
@@ -928,19 +827,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
sa.resetTargets();
// X controls the minimum targets
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
// TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore?
sa.setXManaCostPaid(xPay);
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
}
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, source, sa);
list = CardLists.getTargetableCards(list, sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
@@ -973,7 +864,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
//System.out.println("isPreferredTarget ok " + list);
}
if (list.size() < sa.getMinTargets()) {
if (list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
return false;
}
@@ -998,7 +889,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
// Combat bouncing
if (sa.getMinTargets() <= 1) {
if (tgt.getMinTargets(sa.getHostCard(), sa) <= 1) {
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
Combat currCombat = game.getCombat();
CardCollection attackers = currCombat.getAttackers();
@@ -1011,7 +902,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
&& !currCombat.getBlockers(attacker).isEmpty()) {
ComputerUtilCard.sortByEvaluateCreature(blockers);
Combat combat = new Combat(ai);
combat.addAttacker(attacker, ai.getWeakestOpponent());
combat.addAttacker(attacker, ComputerUtil.getOpponentFor(ai));
for (Card blocker : blockers) {
combat.addBlocker(attacker, blocker);
}
@@ -1040,8 +931,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
// if it's blink or bounce, try to save my about to die stuff
final boolean blink = (destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger
|| "DelayedBlink".equals(sa.getParam("AILogic")) || (subApi == ApiType.ChangeZone && subAffected.equals("Remembered"))));
if ((destination.equals(ZoneType.Hand) || blink) && (sa.getMinTargets() <= 1)) {
|| (subApi == ApiType.ChangeZone && subAffected.equals("Remembered"))));
if ((destination.equals(ZoneType.Hand) || blink) && (tgt.getMinTargets(sa.getHostCard(), sa) <= 1)) {
// save my about to die stuff
Card tobounce = canBouncePermanent(ai, sa, list);
if (tobounce != null) {
@@ -1051,7 +942,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.getTargets().add(tobounce);
boolean saheeliFelidarCombo = ComputerUtilAbility.getAbilitySourceName(sa).equals("Felidar Guardian")
boolean saheeliFelidarCombo = sa.getHostCard().getName().equals("Felidar Guardian")
&& tobounce.getName().equals("Saheeli Rai")
&& CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Felidar Guardian")).size() <
CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.isType("Creature")).size() + ai.getOpponentsGreatestLifeTotal() + 10;
@@ -1071,7 +962,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override
public boolean apply(final Card c) {
for (Card aura : c.getEnchantedBy()) {
return aura.getController().isOpponentOf(ai);
if (aura.getController().isOpponentOf(ai)) {
return true;
} else {
return false;
}
}
if (blink) {
return c.isToken();
@@ -1146,7 +1041,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (destination.equals(ZoneType.Exile) || origin.contains(ZoneType.Battlefield)) {
// don't rush bouncing stuff when not going to attack
if (!immediately && game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
if (!immediately && sa.getPayCosts() != null
&& game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& game.getPhaseHandler().isPlayerTurn(ai)
&& ai.getCreaturesInPlay().isEmpty()) {
return false;
@@ -1177,17 +1073,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Only care about combatants during combat
if (game.getPhaseHandler().inCombat() && origin.contains(ZoneType.Battlefield)) {
CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null, null);
CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null);
if (!newList.isEmpty() || !sa.isTrigger()) {
list = newList;
}
}
boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.usesTargeting()
&& sa.getMinTargets() == 0
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class);
if (list.isEmpty() && !doWithoutTarget) {
if (list.isEmpty()) {
return false;
}
@@ -1195,12 +1087,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
// the Unless cost (for example, Erratic Portal)
list.removeAll(getSafeTargetsIfUnlessCostPaid(ai, sa, list));
if (!mandatory && list.size() < sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa)) {
if (!mandatory && list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
return false;
}
// target loop
while (sa.canAddMoreTarget()) {
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
// AI Targeting
Card choice = null;
@@ -1229,7 +1121,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
//option to hold removal instead only applies for single targeted removal
if (!immediately && sa.getMaxTargets() == 1) {
if (!immediately && tgt.getMaxTargets(source, sa) == 1) {
if (!ComputerUtilCard.useRemovalNow(sa, choice, 0, destination)) {
return false;
}
@@ -1265,21 +1157,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (choice == null) { // can't find anything left
if (sa.getTargets().size() == 0 || !sa.isTargetNumberValid()) {
if (sa.getTargets().getNumTargeted() == 0 || sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (!mandatory) {
sa.resetTargets();
}
if (!doWithoutTarget) {
return false;
} else {
break;
}
} else {
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
boolean aiTgtsOK = false;
if (sa.hasParam("AIMinTgts")) {
int minTgts = Integer.parseInt(sa.getParam("AIMinTgts"));
if (sa.getTargets().size() >= minTgts) {
if (sa.getTargets().getNumTargeted() >= minTgts) {
aiTgtsOK = true;
}
}
@@ -1299,23 +1187,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
// if max power exceeded, do not choose this card (but keep looking for other options)
if (sa.hasParam("MaxTotalTargetPower")) {
if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) {
list.remove(choice);
continue;
}
}
// honor the Same Creature Type restriction
if (sa.getTargetRestrictions().isWithSameCreatureType()) {
Card firstTarget = sa.getTargetCard();
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
list.remove(choice);
continue;
}
}
list.remove(choice);
sa.getTargets().add(choice);
}
@@ -1354,7 +1225,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
// filter out untargetables
CardCollectionView aiPermanents = CardLists
.filterControlledBy(list, ai);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
// Felidar Guardian + Saheeli Rai combo support
if (sa.getHostCard().getName().equals("Felidar Guardian")) {
@@ -1370,7 +1240,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final List<GameObject> objects = ComputerUtil
.predictThreatenedObjects(ai, sa);
final List<Card> threatenedTargets = new ArrayList<>();
final List<Card> threatenedTargets = new ArrayList<Card>();
for (final Card c : aiPermanents) {
if (objects.contains(c)) {
@@ -1398,33 +1268,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
}
// Reload planeswalkers
else if (!aiPlaneswalkers.isEmpty() && (sa.getHostCard().isSorcery() || !game.getPhaseHandler().isPlayerTurn(ai))) {
int maxLoyaltyToConsider = 2;
int loyaltyDiff = 2;
int chance = 30;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
maxLoyaltyToConsider = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY);
loyaltyDiff = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF);
chance = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_CHANCE);
}
if (MyRandom.percentTrue(chance)) {
Collections.sort(aiPlaneswalkers, new Comparator<Card>() {
@Override
public int compare(final Card a, final Card b) {
return a.getCounters(CounterEnumType.LOYALTY) - b.getCounters(CounterEnumType.LOYALTY);
}
});
for (Card pw : aiPlaneswalkers) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
int freshLoyalty = Integer.valueOf(pw.getCurrentState().getBaseLoyalty());
if (freshLoyalty - curLoyalty >= loyaltyDiff && curLoyalty <= maxLoyaltyToConsider) {
return pw;
}
}
}
}
return null;
}
@@ -1436,31 +1279,46 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
final Card source = sa.getHostCard();
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final TargetRestrictions tgt = sa.getTargetRestrictions();
CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), ai, source, sa);
CardCollection list = CardLists.getValidCards(ai.getGame().getCardsIn(origin), tgt.getValidTgts(), ai, source, sa);
// Narrow down the list:
if (origin.equals(ZoneType.Battlefield)) {
// filter out untargetables
list = CardLists.getTargetableCards(list, sa);
list.removeAll(sa.getTargets().getTargetCards());
// if Destination is hand, either bounce opponents dangerous stuff
// or save my about to die stuff
// if Destination is exile, filter out my cards
}
else if (origin.equals(ZoneType.Graveyard)) {
// Retrieve from Graveyard to:
}
for (final Card c : sa.getTargets().getTargetCards()) {
list.remove(c);
}
if (list.isEmpty()) {
return false;
}
// target loop
while (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
// AI Targeting
Card choice = null;
if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false);
if (mostExpensivePermanent.isCreature()
&& (destination.equals(ZoneType.Battlefield) || tgt.getZone().contains(ZoneType.Battlefield))) {
if (ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false).isCreature()
&& (destination.equals(ZoneType.Battlefield) || origin.equals(ZoneType.Battlefield))) {
// if a creature is most expensive take the best
choice = ComputerUtilCard.getBestCreatureToBounceAI(list);
} else if (destination.equals(ZoneType.Battlefield) || tgt.getZone().contains(ZoneType.Battlefield)) {
choice = mostExpensivePermanent;
} else if (destination.equals(ZoneType.Battlefield) || origin.equals(ZoneType.Battlefield)) {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false);
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
List<Card> nonLands = CardLists.getNotType(list, "Land");
// Prefer to pull a creature, generally more useful for AI.
@@ -1492,15 +1350,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (choice == null) { // can't find anything left
if (sa.getTargets().size() == 0 || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (sa.getTargets().getNumTargeted() == 0 || sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
sa.resetTargets();
return false;
}
} else {
if (!ComputerUtil.shouldCastLessThanMax(ai, source)) {
return false;
}
break;
}
}
list.remove(choice);
sa.getTargets().add(choice);
@@ -1521,14 +1380,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean.
*/
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
final String logic = sa.getParamOrDefault("AILogic", "");
if ("DeathgorgeScavenger".equals(logic)) {
if ("DeathgorgeScavenger".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
} else if ("ExtraplanarLens".equals(logic)) {
} else if ("ExtraplanarLens".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
} else if ("ExileCombatThreat".equals(logic)) {
return doExileCombatThreatLogic(ai, sa);
}
if (sa.getTargetRestrictions() == null) {
@@ -1538,12 +1393,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!list.isEmpty()) {
final Card attachedTo = list.get(0);
// This code is for the Dragon auras
return !attachedTo.getController().isOpponentOf(ai);
if (attachedTo.getController().isOpponentOf(ai)) {
return false;
}
}
}
} else if (isPreferredTarget(ai, sa, mandatory, true)) {
// do nothing
} else return isUnpreferredTarget(ai, sa, mandatory);
} else if (!isUnpreferredTarget(ai, sa, mandatory)) {
return false;
}
return true;
}
@@ -1556,20 +1415,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
String logic = sa.getParam("AILogic");
if ("NeverBounceItself".equals(logic)) {
Card source = sa.getHostCard();
if (fetchList.contains(source) && (fetchList.size() > 1 || !sa.getRootAbility().isMandatory())) {
if (fetchList.contains(source) && (fetchList.size() > 1 && !sa.getTriggeringAbility().isMandatory())) {
// For cards that should never be bounced back to hand with their own [e.g. triggered] abilities, such as guild lands.
fetchList.remove(source);
}
} else if ("WorstCard".equals(logic)) {
return ComputerUtilCard.getWorstAI(fetchList);
} else if ("BestCard".equals(logic)) {
return ComputerUtilCard.getBestAI(fetchList); // generally also means the most expensive one or close to it
} else if ("Mairsil".equals(logic)) {
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
} else if ("SurvivalOfTheFittest".equals(logic)) {
return SpecialCardAi.SurvivalOfTheFittest.considerCardToGet(decider, sa);
} else if ("MazesEnd".equals(logic)) {
return SpecialCardAi.MazesEnd.considerCardToGet(decider, sa);
} else if ("Intuition".equals(logic)) {
if (!multipleCardsToChoose.isEmpty()) {
Card choice = multipleCardsToChoose.get(0);
@@ -1597,7 +1452,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override
public boolean apply(final Card c) {
if (c.getType().isLegendary()) {
return !decider.isCardInPlay(c.getName());
if (decider.isCardInPlay(c.getName())) {
return false;
}
}
return true;
}
@@ -1606,7 +1463,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
fetchList = CardLists.filter(fetchList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !ComputerUtilCard.isCardRemAIDeck(c) && !ComputerUtilCard.isCardRemRandomDeck(c);
if (ComputerUtilCard.isCardRemAIDeck(c) || ComputerUtilCard.isCardRemRandomDeck(c)) {
return false;
}
return true;
}
});
}
@@ -1617,14 +1477,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (player.isOpponentOf(decider)) {
c = ComputerUtilCard.getBestAI(fetchList);
} else {
if (!sa.hasParam("Mandatory") && origin.contains(ZoneType.Battlefield) && sa.hasParam("ChangeNum")) {
// exclude tokens, they won't come back, and enchanted stuff, since auras will go away
fetchList = prefilterOwnListForBounceAnyNum(fetchList, decider);
if (fetchList.isEmpty()) {
return null;
}
}
c = ComputerUtilCard.getWorstAI(fetchList);
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Temur Sabertooth")) {
Card tobounce = canBouncePermanent(player, sa, fetchList);
@@ -1693,57 +1545,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
return c;
}
private static CardCollection prefilterOwnListForBounceAnyNum(CardCollection fetchList, Player decider) {
fetchList = CardLists.filter(fetchList, new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
if (card.isToken()) {
return false;
}
if (card.isCreature() && ComputerUtilCard.isUselessCreature(decider, card)) {
return true;
} else if (card.isEquipped()) {
return false;
} else if (card.isEnchanted()) {
for (Card enc : card.getEnchantedBy()) {
if (enc.getOwner().isOpponentOf(decider)) {
return true;
}
}
return false;
} else if (card.hasCounters()) {
if (card.isPlaneswalker()) {
int maxLoyaltyToConsider = 2;
int loyaltyDiff = 2;
int chance = 30;
if (decider.getController().isAI()) {
AiController aic = ((PlayerControllerAi) decider.getController()).getAi();
maxLoyaltyToConsider = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY);
loyaltyDiff = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF);
chance = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_CHANCE);
}
if (MyRandom.percentTrue(chance)) {
int curLoyalty = card.getCounters(CounterEnumType.LOYALTY);
int freshLoyalty = Integer.valueOf(card.getCurrentState().getBaseLoyalty());
if (freshLoyalty - curLoyalty >= loyaltyDiff && curLoyalty <= maxLoyaltyToConsider) {
return true;
}
}
} else if (card.isCreature() && card.getCounters(CounterEnumType.M1M1) > 0) {
return true;
}
return false; // TODO: improve for other counters
} else if (card.isAura()) {
return false;
}
return true;
}
});
return fetchList;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@@ -1755,7 +1556,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
// Called when looking for creature to attach aura or equipment
return ComputerUtilCard.getBestAI(options);
}
@@ -1764,7 +1565,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List)
*/
@Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
// Currently only used by Curse of Misfortunes, so this branch should never get hit
// But just in case it does, just select the first option
return Iterables.getFirst(options, null);
@@ -1774,7 +1575,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
Card source = sa.getHostCard();
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
CardCollection listToSac = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), definedSac, ai, source, sa);
CardCollection listToSac = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.restriction(definedSac.split(","), ai, source, sa));
listToSac.sort(Collections.reverseOrder(CardLists.CmcComparatorInv));
CardCollection listToRet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Presets.CREATURES);
@@ -1811,7 +1612,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
String definedGoal = sa.getParam("ChangeType");
boolean anyCMC = !definedGoal.contains(".cmc");
CardCollection listToSac = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), definedSac, ai, source, sa);
CardCollection listToSac = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.restriction(definedSac.split(","), ai, source, sa));
listToSac.sort(!sacWorst ? CardLists.CmcComparatorInv : Collections.reverseOrder(CardLists.CmcComparatorInv));
for (Card sacCandidate : listToSac) {
@@ -1825,19 +1626,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
curGoal = definedGoal.replace("X", String.format("%d", goalCMC));
}
CardCollection listGoal = CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), curGoal, ai, source, sa);
CardCollection listGoal = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.restriction(curGoal.split(","), ai, source, sa));
if (!anyCMC) {
listGoal = CardLists.getValidCards(listGoal, curGoal, source.getController(), source, sa);
listGoal = CardLists.getValidCards(listGoal, curGoal, source.getController(), source);
} else {
listGoal = CardLists.getValidCards(listGoal, curGoal + (curGoal.contains(".") ? "+" : ".") + "cmcGE" + goalCMC, source.getController(), source, sa);
listGoal = CardLists.getValidCards(listGoal, curGoal + (curGoal.contains(".") ? "+" : ".") + "cmcGE" + goalCMC, source.getController(), source);
}
listGoal = CardLists.filter(listGoal, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (c.getType().isLegendary()) {
return !ai.isCardInPlay(c.getName());
if (ai.isCardInPlay(c.getName())) {
return false;
}
}
return true;
}
@@ -1927,9 +1730,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
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);
Map<String, Object> originalParams = (Map<String, Object>)sa.getReplacingObject("OriginalParams");
SpellAbility causeSa = (SpellAbility)originalParams.get("Cause");
SpellAbility causeSub = null;
// Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
@@ -1944,66 +1746,26 @@ public class ChangeZoneAi extends SpellAbilityAi {
&& "Battlefield".equals(causeSub.getParam("Destination"))) {
// A blink effect implemented using ChangeZone API
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) {
} else if (subApi == ApiType.DelayedTrigger) {
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
if ("Exile".equals(exec.getParam("Origin")) && "Battlefield".equals(exec.getParam("Destination"))) {
// A blink effect implemented using a delayed trigger
return !"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"));
return false;
}
}
} else if (causeSa.getHostCard() != null && causeSa.getHostCard().equals((Card)sa.getReplacingObject("Card"))
&& causeSa.getActivatingPlayer().equals(aiPlayer)) {
// This is an intrinsic effect that blinks the card (e.g. Obzedat, Ghost Council), no need to
// return the commander to the Command zone.
return false;
}
} 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 him later
return true;
}
public static boolean doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
final Combat combat = aiPlayer.getGame().getCombat();
if (combat == null) {
return false;
}
Card choice = null;
int highestEval = -1;
if (combat.getAttackingPlayer().isOpponentOf(aiPlayer)) {
for (Card attacker : combat.getAttackers()) {
if (sa.canTarget(attacker) && attacker.canBeTargetedBy(sa)) {
int eval = ComputerUtilCard.evaluateCreature(attacker);
if (combat.isUnblocked(attacker)) {
eval += 100; // TODO: make this smarter
}
if (eval > highestEval) {
highestEval = eval;
choice = attacker;
}
}
}
} else {
// either the current AI player or one of its teammates is attacking, the opponent(s) are blocking
for (Card blocker : combat.getAllBlockers()) {
if (sa.canTarget(blocker) && blocker.canBeTargetedBy(sa)) {
if (blocker.getController().isOpponentOf(aiPlayer)) { // TODO: unnecessary sanity check?
int eval = ComputerUtilCard.evaluateCreature(blocker);
if (eval > highestEval) {
highestEval = eval;
choice = blocker;
}
}
}
}
}
if (choice != null) {
sa.getTargets().add(choice);
return true;
}
return false;
}
private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) {
// Determines if the controller of each potential target can negate the ChangeZone effect
// by paying the Unless cost. Returns the list of targets that can be saved that way.
@@ -2019,9 +1781,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
// TODO use ComputerUtilCost.getMaxXValue if able
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
@@ -2036,7 +1797,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (setPayX) {
sa.setXManaCostPaid(toPay);
source.setSVar("PayX", Integer.toString(toPay));
}
}
}

View File

@@ -1,29 +1,11 @@
package forge.ai.ability;
import java.util.Collections;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiController;
import forge.ai.AiPlayerPredicates;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -34,6 +16,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.Collections;
public class ChangeZoneAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -51,7 +35,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll");
if (!aiLogicAllowsDiscard) {
@@ -73,11 +57,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
CardCollectionView oppType = CardLists.filterControlledBy(game.getCardsIn(origin), ai.getOpponents());
CardCollectionView computerType = ai.getCardsIn(origin);
// remove cards that won't be seen in AI's own library if it can't be searched
if (!ai.canSearchLibraryWith(sa, ai)) {
computerType = CardLists.filter(computerType, Predicates.not(CardPredicates.inZone(ZoneType.Library)));
}
// Ugin check need to be done before filterListByType because of ChosenX
// Ugin AI: always try to sweep before considering +1
if (sourceName.equals("Ugin, the Spirit Dragon")) {
return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
@@ -245,8 +225,25 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
&& !ComputerUtil.isPlayingReanimator(ai);
}
} else if (origin.equals(ZoneType.Exile)) {
// TODO: nothing to do here at the moment
return false;
String logic = sa.getParam("AILogic");
if (logic != null && logic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// minimum card advantage unless the hand will be fully reloaded
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
if (numExiledWithSrc > curHandSize) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
// Try to gain some card advantage if the card will die anyway
// TODO: ideally, should evaluate the hand value and not discard good hands to it
return true;
}
}
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
}
} else if (origin.equals(ZoneType.Stack)) {
// time stop can do something like this:
// Origin$ Stack | Destination$ Exile | SubAbility$ DBSkip
@@ -332,8 +329,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
return true;
// if AI creature is better than Human Creature
return ComputerUtilCard.evaluateCreatureList(aiCards) >= ComputerUtilCard
.evaluateCreatureList(humanCards);
if (ComputerUtilCard.evaluateCreatureList(aiCards) >= ComputerUtilCard
.evaluateCreatureList(humanCards)) {
return true;
}
return false;
}
return true;
}
@@ -435,21 +435,29 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (sa.getParam("GainControl") != null) {
// Check if the cards are valuable enough
if ((CardLists.getNotType(humanType, "Creature").size() == 0) && (CardLists.getNotType(computerType, "Creature").size() == 0)) {
return (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
.evaluateCreatureList(humanType)) >= 1;
if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
.evaluateCreatureList(humanType)) < 1) {
return false;
}
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
else return (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
.evaluatePermanentList(humanType)) >= 1;
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
.evaluatePermanentList(humanType)) < 1) {
return false;
}
} else {
// don't activate if human gets more back than AI does
if ((CardLists.getNotType(humanType, "Creature").isEmpty()) && (CardLists.getNotType(computerType, "Creature").isEmpty())) {
return ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard
.evaluateCreatureList(humanType);
if (ComputerUtilCard.evaluateCreatureList(computerType) <= ComputerUtilCard
.evaluateCreatureList(humanType)) {
return false;
}
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
else return ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard
.evaluatePermanentList(humanType);
else if (ComputerUtilCard.evaluatePermanentList(computerType) <= ComputerUtilCard
.evaluatePermanentList(humanType)) {
return false;
}
}
}

View File

@@ -1,18 +1,8 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import forge.ai.AiController;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.ai.*;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -20,6 +10,8 @@ import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.List;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
@@ -28,11 +20,8 @@ public class CharmAi extends SpellAbilityAi {
return true;
}
final Card source = sa.getHostCard();
final int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
// Reset the chosen list otherwise it will be locked in forever by earlier calls
@@ -243,7 +232,7 @@ public class CharmAi extends SpellAbilityAi {
}
@Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents, Map<String, Object> params) {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> opponents) {
return Aggregates.random(opponents);
}
}

View File

@@ -2,14 +2,15 @@ package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
@@ -20,6 +21,7 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
@@ -64,21 +66,27 @@ public class ChooseCardAi extends SpellAbilityAi {
}
CardCollectionView choices = ai.getGame().getCardsIn(choiceZone);
if (sa.hasParam("Choices")) {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host);
}
if (sa.hasParam("TargetControls")) {
choices = CardLists.filterControlledBy(choices, ai.getOpponents());
}
if (aiLogic.equals("AtLeast1") || aiLogic.equals("OppPreferred")) {
return !choices.isEmpty();
if (choices.isEmpty()) {
return false;
}
} else if (aiLogic.equals("AtLeast2") || aiLogic.equals("BestBlocker")) {
return choices.size() >= 2;
if (choices.size() < 2) {
return false;
}
} else if (aiLogic.equals("Clone") || aiLogic.equals("Vesuva")) {
final String filter = aiLogic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
choices = CardLists.getValidCards(choices, filter, host.getController(), host, sa);
return !choices.isEmpty();
choices = CardLists.getValidCards(choices, filter, host.getController(), host);
if (choices.isEmpty()) {
return false;
}
} else if (aiLogic.equals("Never")) {
return false;
} else if (aiLogic.equals("NeedsPrevention")) {
@@ -96,12 +104,30 @@ public class ChooseCardAi extends SpellAbilityAi {
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > ref;
}
});
return !choices.isEmpty();
if (choices.isEmpty()) {
return false;
}
} else if (aiLogic.equals("Ashiok")) {
final int loyalty = host.getCounters(CounterType.LOYALTY) - 1;
for (int i = loyalty; i >= 0; i--) {
host.setSVar("ChosenX", "Number$" + i);
choices = ai.getGame().getCardsIn(choiceZone);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host);
if (!choices.isEmpty()) {
return true;
}
}
if (choices.isEmpty()) {
return false;
}
} else if (aiLogic.equals("RandomNonLand")) {
return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty();
if (CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host).isEmpty()) {
return false;
}
} else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay();
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
@@ -114,14 +140,10 @@ public class ChooseCardAi extends SpellAbilityAi {
aiCreatures.remove(chosen);
int minGain = 200;
return (ComputerUtilCard.evaluateCreatureList(aiCreatures) + minGain) < ComputerUtilCard
.evaluateCreatureList(oppCreatures);
} else if (aiLogic.equals("OwnCard")) {
CardCollectionView ownChoices = CardLists.filter(choices, CardPredicates.isController(ai));
if (ownChoices.isEmpty()) {
ownChoices = CardLists.filter(choices, CardPredicates.isControlledByAnyOf(ai.getAllies()));
if ((ComputerUtilCard.evaluateCreatureList(aiCreatures) + minGain) >= ComputerUtilCard
.evaluateCreatureList(oppCreatures)) {
return false;
}
return !ownChoices.isEmpty();
}
return true;
}
@@ -138,7 +160,7 @@ public class ChooseCardAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@Override
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
final Card host = sa.getHostCard();
final Player ctrl = host.getController();
final String logic = sa.getParam("AILogic");
@@ -148,12 +170,6 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = ComputerUtilCard.getBestAI(options);
} else if ("WorstCard".equals(logic)) {
choice = ComputerUtilCard.getWorstAI(options);
} else if ("OwnCard".equals(logic)) {
CardCollectionView ownChoices = CardLists.filter(options, CardPredicates.isController(ai));
if (ownChoices.isEmpty()) {
ownChoices = CardLists.filter(options, CardPredicates.isControlledByAnyOf(ai.getAllies()));
}
choice = ComputerUtilCard.getBestAI(ownChoices);
} else if (logic.equals("BestBlocker")) {
if (!CardLists.filter(options, Presets.UNTAPPED).isEmpty()) {
options = CardLists.filter(options, Presets.UNTAPPED);
@@ -172,7 +188,7 @@ public class ChooseCardAi extends SpellAbilityAi {
choice = null;
}
} else if ("RandomNonLand".equals(logic)) {
options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host, sa);
options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host);
choice = Aggregates.random(options);
} else if (logic.equals("Untap")) {
final String filter = "Permanent.YouCtrl,Permanent.tapped";
@@ -232,7 +248,7 @@ public class ChooseCardAi extends SpellAbilityAi {
return false;
}
for (SpellAbility sa : c.getAllSpellAbilities()) {
if (sa.getPayCosts().hasTapCost()) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
return false;
}
}
@@ -275,8 +291,6 @@ public class ChooseCardAi extends SpellAbilityAi {
// Choose the best (hopefully the fattest, whatever) creature so that hopefully it won't die too easily
choice = ComputerUtilCard.getBestAI(creatures);
}
} else if (logic.equals("NextTurnAttacker")) {
choice = ComputerUtilCard.getBestCreatureToAttackNextTurnAI(ai, options);
} else {
choice = ComputerUtilCard.getBestAI(options);
}

View File

@@ -1,16 +1,12 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.StaticData;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.card.CardDb;
import forge.card.CardRules;
import forge.card.CardSplitType;
@@ -27,6 +23,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
if (sa.hasParam("AILogic")) {
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
@@ -44,7 +41,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
if (tgt != null) {
sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(ai.getWeakestOpponent());
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} else {
sa.getTargets().add(ai);
}
@@ -57,13 +54,13 @@ public class ChooseCardNameAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// TODO - there is no AILogic implemented yet
return mandatory;
return false;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@Override
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
return ComputerUtilCard.getBestAI(options);
}

View File

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

View File

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

View File

@@ -33,7 +33,9 @@ public class ChooseDirectionAi extends SpellAbilityAi {
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Right));
int leftValue = Aggregates.sum(left, CardPredicates.Accessors.fnGetCmc);
int rightValue = Aggregates.sum(right, CardPredicates.Accessors.fnGetCmc);
return aiValue <= leftValue && aiValue <= rightValue;
if (aiValue > leftValue || aiValue > rightValue) {
return false;
}
}
}
return true;
@@ -46,6 +48,6 @@ public class ChooseDirectionAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
return canPlayAI(ai, sa);
}
}

View File

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

View File

@@ -8,11 +8,10 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.card.MagicColor;
@@ -23,7 +22,7 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
@@ -32,7 +31,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantBeCast;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.collect.FCollection;
@@ -52,12 +50,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
return true;
}
}
} else if ("GideonBlackblade".equals(aiLogic)) {
return SpecialCardAi.GideonBlackblade.consider(ai, sa);
} else if ("SoulEcho".equals(aiLogic)) {
return doTriggerAINoCost(ai, sa, true);
} else if ("Always".equals(aiLogic)) {
return true;
}
return false;
}
@@ -72,12 +64,12 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return checkApiLogic(aiPlayer, sa);
return canPlayAI(aiPlayer, sa);
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) {
if ("CombustibleGearhulk".equals(sa.getParam("AILogic"))) {
for (final Player p : aiPlayer.getOpponents()) {
if (p.canBeTargetedBy(sa)) {
sa.resetTargets();
@@ -103,8 +95,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
return spells.get(0);
} else if ("Random".equals(logic)) {
return Aggregates.random(spells);
} else if ("GideonBlackblade".equals(logic)) {
return SpecialCardAi.GideonBlackblade.chooseSpellAbility(player, sa, spells);
} else if ("Phasing".equals(logic)) { // Teferi's Realm : keep aggressive
List<SpellAbility> filtered = Lists.newArrayList(Iterables.filter(spells, new Predicate<SpellAbility>() {
@Override
@@ -120,7 +110,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
Cost unless = new Cost(unlessCost, false);
SpellAbility paycost = new SpellAbility.EmptySa(sa.getHostCard(), player);
paycost.setPayCosts(unless);
if (ComputerUtilCost.willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
if (ComputerUtilCost.willPayUnlessCost(sp, player, unless, false, new FCollection<Player>(player))
&& ComputerUtilCost.canPayCost(paycost, player)) {
return sp;
}
@@ -172,10 +162,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
}
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) {
if (player.hasKeyword("Skip your draw step.")) {
return skipDraw;
}
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) {
if (player.hasKeyword("Skip your next combat phase.")) {
return skipCombat;
}
@@ -230,11 +220,12 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
return allow;
}
SpellAbility firstSpell = imprinted.getFirstSpellAbility();
// check if something would prevent it from casting
if (firstSpell == null || StaticAbilityCantBeCast.cantBeCastAbility(firstSpell, imprinted, owner)) {
//if Iona does prevent from casting, allow it to draw
for (final Card io : player.getCardsIn(ZoneType.Battlefield, "Iona, Shield of Emeria")) {
if (CardUtil.getColors(imprinted).hasAnyColor(MagicColor.fromName(io.getChosenColor()))) {
return allow;
}
}
if (dmg == 0) {
// If CMC = 0, mill it!
@@ -260,7 +251,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
SpellAbility counterSA = spells.get(0), tokenSA = spells.get(1);
// check for something which might prevent the counters to be placed on host
if (!host.canReceiveCounters(CounterEnumType.P1P1)) {
if (!host.canReceiveCounters(CounterType.P1P1)) {
return tokenSA;
}
@@ -272,7 +263,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// need a copy for one with extra +1/+1 counter boost,
// without causing triggers to run
final Card copy = CardUtil.getLKICopy(host);
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + n);
copy.setCounters(CounterType.P1P1, copy.getCounters(CounterType.P1P1) + n);
copy.setZone(host.getZone());
// if host would put into the battlefield attacking
@@ -297,10 +288,10 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
// TODO check for trigger to turn token ETB into +1/+1 counter for host
// TODO check for trigger to turn token ETB into damage or life loss for opponent
// in this cases Token might be prefered even if they would not survive
final Card tokenCard = TokenAi.spawnToken(player, tokenSA);
final Card tokenCard = TokenAi.spawnToken(player, tokenSA, true);
// Token would not survive
if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) {
if (tokenCard.getNetToughness() < 1) {
return counterSA;
}
@@ -344,10 +335,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
int bestGuessDamage = totalCMC * 3 / revealedCards.size();
return life <= bestGuessDamage ? spells.get(0) : spells.get(1);
} else if ("SoulEcho".equals(logic)) {
Player target = sa.getTargetingPlayer();
int life = target.getLife();
return life < 10 ? spells.get(0) : Aggregates.random(spells);
} else if ("Pump".equals(logic) || "BestOption".equals(logic)) {
List<SpellAbility> filtered = Lists.newArrayList();
// filter first for the spells which can be done
@@ -364,8 +351,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
} else if ("Riot".equals(logic)) {
SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
return preferHasteForRiot(sa, player) ? hasteSA : counterSA;
} else if ("CrawlingBarrens".equals(logic)) {
return SpecialCardAi.CrawlingBarrens.considerAnimating(player, sa, spells);
}
return spells.get(0); // return first choice if no logic found
}
@@ -384,7 +369,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
game.getAction().checkStaticAbilities(false);
// can't gain counters, use Haste
if (!copy.canReceiveCounters(CounterEnumType.P1P1)) {
if (!copy.canReceiveCounters(CounterType.P1P1)) {
return true;
}
@@ -407,7 +392,9 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
final Player opp = player.getWeakestOpponent();
if (opp != null) {
// TODO add predict Combat Damage?
return opp.getLife() < copy.getNetPower();
if (opp.getLife() < copy.getNetPower()) {
return true;
}
}
// haste might not be good enough?

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -16,7 +17,7 @@ public class ChooseNumberAi extends SpellAbilityAi {
TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
Player opp = aiPlayer.getWeakestOpponent();
Player opp = ComputerUtil.getOpponentFor(aiPlayer);
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {

View File

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

View File

@@ -1,15 +1,16 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameObject;
@@ -46,7 +47,20 @@ public class ChooseSourceAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
if (abCost != null) {
if (!willPayCosts(ai, sa, abCost, source)) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) {
return false;
}
}
@@ -54,7 +68,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
Player opp = ai.getWeakestOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
@@ -84,14 +98,17 @@ public class ChooseSourceAi extends SpellAbilityAi {
return false;
}
int dmg = AbilityUtils.calculateAmount(threatSource, topStack.getParam("NumDmg"), topStack);
return ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0;
if (ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) <= 0) {
return false;
}
return true;
}
if (game.getPhaseHandler().getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) {
return false;
}
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
if (sa.hasParam("Choices")) {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host);
}
final Combat combat = game.getCombat();
choices = CardLists.filter(choices, new Predicate<Card>() {
@@ -103,7 +120,9 @@ public class ChooseSourceAi extends SpellAbilityAi {
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0;
}
});
return !choices.isEmpty();
if (choices.isEmpty()) {
return false;
}
}
}
@@ -113,7 +132,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
@Override
public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(final Player aiChoser, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
if ("NeedsPrevention".equals(sa.getParam("AILogic"))) {
final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
@@ -141,7 +160,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
Card bestCreature = ComputerUtilCard.getBestCreatureAI(permanentSources);
if (bestCreature != null) {
return bestCreature;
}
} else {
// No optimal creature was found above, so try to broaden the choice.
if (!Iterables.isEmpty(options)) {
List<Card> oppCreatures = CardLists.filter(options, Predicates.and(CardPredicates.Presets.CREATURES,
@@ -162,6 +181,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
// source) in order to choose at least something, or the game will hang.
return game.getStack().peekAbility().getHostCard();
}
}
// Should never get here
System.err.println("Unexpected behavior: The AI was unable to choose anything for AF ChooseSource in "
@@ -204,7 +224,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
private static List<GameObject> getTargets(final SpellAbility sa) {
return sa.usesTargeting() && (!sa.hasParam("Defined"))
? Lists.newArrayList(sa.getTargets())
? Lists.newArrayList(sa.getTargets().getTargets())
: AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
}
}

View File

@@ -1,10 +1,7 @@
package forge.ai.ability;
import java.util.HashSet;
import java.util.Set;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
@@ -12,7 +9,6 @@ import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardType;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
@@ -24,6 +20,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.List;
public class ChooseTypeAi extends SpellAbilityAi {
@Override
@@ -34,8 +31,6 @@ public class ChooseTypeAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
return doMirrorEntityLogic(aiPlayer, sa);
}
} else if ("MostProminentOppControls".equals(sa.getParam("AILogic"))) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
}
return doTriggerAINoCost(aiPlayer, sa, false);
@@ -49,12 +44,28 @@ public class ChooseTypeAi extends SpellAbilityAi {
return false;
}
String chosenType = chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield));
CardCollectionView otb = aiPlayer.getCardsIn(ZoneType.Battlefield);
List<String> valid = Lists.newArrayList(CardType.getAllCreatureTypes());
String chosenType = ComputerUtilCard.getMostProminentType(otb, valid);
if (chosenType.isEmpty()) {
return false;
// Account for the situation when only changelings are on the battlefield
boolean allChangeling = false;
for (Card c : otb) {
if (c.isCreature() && c.hasStartOfKeyword(Keyword.CHANGELING.toString())) {
chosenType = Aggregates.random(valid); // just choose a random type for changelings
allChangeling = true;
break;
}
}
int maxX = ComputerUtilMana.determineLeftoverMana(sa, aiPlayer);
if (!allChangeling) {
// Still empty, probably no creatures on board
return false;
}
}
int maxX = ComputerUtilMana.determineMaxAffordableX(aiPlayer, sa);
int avgPower = 0;
// predict the opposition
@@ -91,7 +102,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
}
if (maxX > avgPower && maxX > maxOppPower && maxX >= maxOppToughness) {
sa.setXManaCostPaid(maxX);
sa.setSVar("PayX", String.valueOf(maxX));
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ANIMATED_THIS_TURN);
return true;
}
@@ -116,40 +127,4 @@ public class ChooseTypeAi extends SpellAbilityAi {
return true;
}
private String chooseType(SpellAbility sa, CardCollectionView cards) {
Set<String> valid = new HashSet<>();
if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.PumpAll
&& sa.getSubAbility().isCurse() && sa.getSubAbility().hasParam("NumDef")) {
final SpellAbility pumpSa = sa.getSubAbility();
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), pumpSa.getParam("NumDef"), pumpSa);
for (Card c : cards) {
if (c.isCreature() && c.getNetToughness() <= -defense) {
valid.addAll(c.getType().getCreatureTypes());
}
}
} else {
valid.addAll(CardType.getAllCreatureTypes());
}
String chosenType = ComputerUtilCard.getMostProminentType(cards, valid);
if (chosenType.isEmpty()) {
// Account for the situation when only changelings are on the battlefield
boolean allChangeling = false;
for (Card c : cards) {
if (c.isCreature() && c.hasStartOfKeyword(Keyword.CHANGELING.toString())) {
chosenType = Aggregates.random(valid); // just choose a random type for changelings
allChangeling = true;
break;
}
}
if (!allChangeling) {
// Still empty, probably no creatures on board
return "";
}
}
return chosenType;
}
}

View File

@@ -1,11 +1,9 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
@@ -58,7 +56,7 @@ public class ClashAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility, java.lang.Iterable)
*/
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
for (Player p : options) {
if (p.getCardsIn(ZoneType.Library).size() == 0)
return p;
@@ -84,7 +82,7 @@ public class ClashAi extends SpellAbilityAi {
PlayerCollection players = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
// use chooseSinglePlayer function to the select player
Player chosen = chooseSinglePlayer(ai, sa, players, null);
Player chosen = chooseSinglePlayer(ai, sa, players);
if (chosen != null) {
sa.resetTargets();
sa.getTargets().add(chosen);
@@ -106,7 +104,7 @@ public class ClashAi extends SpellAbilityAi {
}
}
return sa.getTargets().size() > 0;
return sa.getTargets().getNumTargeted() > 0;
}
}

View File

@@ -1,8 +1,5 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -15,12 +12,16 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.List;
public class CloneAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Game game = source.getGame();
@@ -38,13 +39,27 @@ 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, ai.getGame().getPhaseHandler())) {
PhaseHandler phase = game.getPhaseHandler();
// don't use instant speed clone abilities outside computers
// Combat_Begin step
if (!phase.is(PhaseType.COMBAT_BEGIN)
&& phase.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false;
}
PhaseHandler phase = game.getPhaseHandler();
// don't use instant speed clone abilities outside humans
// Combat_Declare_Attackers_InstantAbility step
if (!phase.is(PhaseType.COMBAT_DECLARE_ATTACKERS) || phase.isPlayerTurn(ai) || game.getCombat().getAttackers().isEmpty()) {
return false;
}
if (!sa.usesTargeting()) {
// don't activate during main2 unless this effect is permanent
if (phase.is(PhaseType.MAIN2) && !sa.hasParam("Permanent")) {
return false;
}
if (null == tgt) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
@@ -124,11 +139,11 @@ public class CloneAi extends SpellAbilityAi {
private boolean cloneTgtAI(final SpellAbility sa) {
// Specific logic for cards
if ("CloneAttacker".equals(sa.getParam("AILogic"))) {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard(), sa);
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
} else if ("CloneBestCreature".equals(sa.getParam("AILogic"))) {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard(), sa);
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
}
@@ -170,39 +185,20 @@ public class CloneAi extends SpellAbilityAi {
*/
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
Player targetedPlayer, Map<String, Object> params) {
Player targetedPlayer) {
final Card host = sa.getHostCard();
final Player ctrl = host.getController();
final Card cloneTarget = getCloneTarget(sa);
final boolean isOpp = cloneTarget.getController().isOpponentOf(sa.getActivatingPlayer());
final boolean isVesuva = "Vesuva".equals(host.getName());
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
final String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
// TODO: rewrite this block so that this is done somehow more elegantly
if (canCloneLegendary) {
filter = filter.replace(".nonLegendary+", ".").replace(".nonLegendary", "");
}
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
if (!newOptions.isEmpty()) {
options = newOptions;
}
if (sa.hasParam("AiChoiceLogic")) {
final String logic = sa.getParam("AiChoiceLogic");
if ("BestOppCtrl".equals(logic)) {
options = CardLists.filterControlledBy(options, ctrl.getOpponents());
}
}
Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);
Card choice = ComputerUtilCard.getBestAI(options);
if (isVesuva && "Vesuva".equals(choice.getName())) {
choice = null;
}
@@ -210,41 +206,4 @@ public class CloneAi extends SpellAbilityAi {
return choice;
}
protected Card getCloneTarget(final SpellAbility sa) {
final Card host = sa.getHostCard();
Card tgtCard = host;
if (sa.hasParam("CloneTarget")) {
final List<Card> cloneTargets = AbilityUtils.getDefinedCards(host, sa.getParam("CloneTarget"), sa);
if (!cloneTargets.isEmpty()) {
tgtCard = cloneTargets.get(0);
}
} else if (sa.hasParam("Choices") && sa.usesTargeting()) {
tgtCard = sa.getTargets().getFirstTargetedCard();
}
return tgtCard;
}
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player, forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler)
*/
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
// don't use instant speed clone abilities outside computers
// Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
return false;
}
// don't use instant speed clone abilities outside humans
// Combat_Declare_Attackers_InstantAbility step
if (!ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS) || ph.isPlayerTurn(ai) || ph.getCombat().getAttackers().isEmpty()) {
return false;
}
// don't activate during main2 unless this effect is permanent
return !ph.is(PhaseType.MAIN2) || sa.hasParam("Permanent");
}
}

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
@@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.resetTargets();
CardCollection list =
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on
// purpose
list = CardLists.filter(list, new Predicate<Card>() {
@@ -81,12 +82,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
// for TrigTwoTargets logic, only get the opponents' cards for the first target
CardCollectionView unfilteredList = "TrigTwoTargets".equals(sa.getParam("AILogic")) ?
aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield) :
aiPlayer.getGame().getCardsIn(ZoneType.Battlefield);
CardCollection list = CardLists.getValidCards(unfilteredList,
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
tgt.getValidTgts(), aiPlayer, sa.getHostCard(), sa);
// only select the cards that can be targeted
@@ -111,51 +107,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
// add best Target
sa.getTargets().add(best);
// second target needed (the AI's own worst)
if ("TrigTwoTargets".equals(sa.getParam("AILogic"))) {
return doTrigTwoTargetsLogic(aiPlayer, sa, best);
}
return true;
}
private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final int creatureThreshold = 100; // TODO: make this configurable from the AI profile
final int nonCreatureThreshold = 2;
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield),
tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// only select the cards that can be targeted
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty()) {
return false;
}
Card aiWorst = ComputerUtilCard.getWorstAI(list);
if (aiWorst == null) {
return false;
}
if (aiWorst != bestFirstTgt) {
if (bestFirstTgt.isCreature() && aiWorst.isCreature()) {
if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return true;
}
} else {
// TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine
if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return true;
}
}
}
sa.clearTargets();
return false;
}
}

View File

@@ -18,7 +18,6 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
@@ -35,12 +34,12 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.collect.FCollectionView;
//AB:GainControl|ValidTgts$Creature|TgtPrompt$Select target legendary creature|LoseControl$Untap,LoseControl|SpellDescription$Gain control of target xxxxxxx
@@ -55,6 +54,8 @@ import forge.util.Aggregates;
// (as a "&"-separated list; like Haste, Sacrifice CARDNAME at EOT, any standard keyword)
// OppChoice - set to True if opponent chooses creature (for Preacher) - not implemented yet
// Untap - set to True if target card should untap when control is taken
// DestroyTgt - actions upon which the tgt should be destroyed. same list as LoseControl
// NoRegen - set if destroyed creature can't be regenerated. used only with DestroyTgt
/**
* <p>
@@ -76,14 +77,16 @@ public class ControlGainAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = ai.getGame();
final PlayerCollection opponents = ai.getOpponents();
final FCollectionView<Player> opponents = ai.getOpponents();
// if Defined, then don't worry about targeting
if (tgt == null) {
if (sa.hasParam("AllValid")) {
CardCollectionView tgtCards = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), opponents);
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
return !tgtCards.isEmpty();
if (tgtCards.isEmpty()) {
return false;
}
}
return true;
} else {
@@ -93,21 +96,20 @@ public class ControlGainAi extends SpellAbilityAi {
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
}
if (tgt.isRandomTarget()) {
sa.getTargets().add(Aggregates.random(tgt.getAllCandidates(sa, false)));
}
if (tgt.canOnlyTgtOpponent()) {
List<Player> oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa));
List<Player> oppList = Lists
.newArrayList(Iterables.filter(opponents, PlayerPredicates.isTargetableBy(sa)));
if (oppList.isEmpty()) {
return false;
}
if (tgt.isRandomTarget()) {
sa.getTargets().add(Aggregates.random(oppList));
} else {
sa.getTargets().add(oppList.get(0));
}
}
}
// Don't steal something if I can't Attack without, or prevent it from
// blocking at least
@@ -197,11 +199,11 @@ public class ControlGainAi extends SpellAbilityAi {
}
}
while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
Card t = null;
if (list.isEmpty()) {
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
if ((sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -245,7 +247,7 @@ public class ControlGainAi extends SpellAbilityAi {
break;
}
}
}
};
if (t != null) {
sa.getTargets().add(t);
@@ -268,10 +270,11 @@ public class ControlGainAi extends SpellAbilityAi {
List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return false;
}
} else {
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
}
}
}
return true;
}
@@ -279,7 +282,7 @@ public class ControlGainAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
final Game game = ai.getGame();
if (!sa.usesTargeting()) {
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (sa.hasParam("AllValid")) {
CardCollectionView tgtCards = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
@@ -293,16 +296,19 @@ public class ControlGainAi extends SpellAbilityAi {
lose.addAll(Lists.newArrayList(sa.getParam("LoseControl").split(",")));
}
return !lose.contains("EOT")
|| !game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
if (lose.contains("EOT")
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
} else {
return this.canPlayAI(ai, sa);
}
return true;
} // pumpDrawbackAI()
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
final List<Card> cards = Lists.newArrayList();
for (Player p : options) {
cards.addAll(p.getCreaturesInPlay());

View File

@@ -1,82 +0,0 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
/**
* <p>
* AbilityFactory_GainControlVariant class.
* </p>
*
* @author Forge
* @version $Id: AbilityFactoryGainControl.java 17764 2012-10-29 11:04:18Z Sloth $
*/
public class ControlGainVariantAi extends SpellAbilityAi {
@Override
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), new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
return crd.isCreature() && !crd.getController().equals(crd.getOwner());
}
});
if (list.isEmpty()) {
return false;
}
for (final Card c : list) {
if (ai.equals(c.getController())) {
return false;
}
}
}
return true;
}
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
Iterable<Card> otherCtrl = CardLists.filter(options, Predicates.not(CardPredicates.isController(ai)));
if (Iterables.isEmpty(otherCtrl)) {
return ComputerUtilCard.getWorstAI(options);
} else {
return ComputerUtilCard.getBestAI(otherCtrl);
}
}
}

View File

@@ -1,29 +1,13 @@
package forge.ai.ability;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -32,11 +16,15 @@ import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import java.util.Collection;
import java.util.List;
public class CopyPermanentAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// Card source = sa.getHostCard();
// TODO - I'm sure someone can do this AI better
Card source = sa.getHostCard();
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -50,11 +38,6 @@ public class CopyPermanentAi extends SpellAbilityAi {
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 false;
}
}
if (sa.hasParam("AtEOT") && !aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1)) {
@@ -63,7 +46,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
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()) {
if (sa.getParam("Defined").equals("Imprinted.ExiledWithSource") && sa.getHostCard().getImprintedCards().isEmpty()) {
return false;
}
}
@@ -77,32 +60,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
if (sa.usesTargeting() && sa.hasParam("TargetingPlayer")) {
sa.resetTargets();
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
} else if (sa.getTargetRestrictions() != null && sa.getTargetRestrictions().canTgtPlayer()) {
if (!sa.isCurse()) {
if (sa.canTarget(aiPlayer)) {
sa.getTargets().add(aiPlayer);
return true;
} else {
for (Player p : aiPlayer.getTeamMates(true)) {
if (sa.canTarget(p)) {
sa.getTargets().add(p);
return true;
}
}
return false;
}
} else {
for (Player p : aiPlayer.getOpponents()) {
if (sa.canTarget(p)) {
sa.getTargets().add(p);
return true;
}
}
return false;
}
} else {
return this.doTriggerAINoCost(aiPlayer, sa, false);
}
@@ -114,7 +74,6 @@ public class CopyPermanentAi extends SpellAbilityAi {
final Player activator = sa.getActivatingPlayer();
final Game game = host.getGame();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final boolean canCopyLegendary = sa.hasParam("NonLegendary");
// ////
@@ -143,7 +102,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
// target loop
while (sa.canAddMoreTarget()) {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
if (!sa.isTargetNumberValid() || (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -155,7 +114,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return (!c.getType().isLegendary() || canCopyLegendary) || !c.getController().equals(aiPlayer);
return !c.getType().isLegendary() || !c.getController().equals(aiPlayer);
}
});
Card choice;
@@ -170,7 +129,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
if (!sa.isTargetNumberValid() || (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -184,7 +143,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
} else if (sa.hasParam("Choices")) {
// only check for options, does not select there
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host, sa);
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host);
Collection<Card> betterChoices = getBetterOptions(aiPlayer, sa, choices, !mandatory);
if (betterChoices.isEmpty()) {
return mandatory;
@@ -193,13 +152,6 @@ public class CopyPermanentAi extends SpellAbilityAi {
// 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 false;
}
}
return true;
}
@@ -216,7 +168,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
// Select a card to attach to
CardCollection betterOptions = getBetterOptions(ai, sa, options, isOptional);
if (!betterOptions.isEmpty()) {
@@ -228,14 +180,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
private CardCollection getBetterOptions(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional) {
final Card host = sa.getHostCard();
final Player ctrl = host.getController();
final boolean canCopyLegendary = sa.hasParam("NonLegendary");
final String filter = canCopyLegendary ? "Permanent" : "Permanent.YouDontCtrl,Permanent.nonLegendary";
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
// TODO add filter to not select Legendary from Other Player when ai already have a Legendary with that name
return CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
}
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
final List<Card> cards = new PlayerCollection(options).getCreaturesInPlay();
Card chosen = ComputerUtilCard.getBestCreatureAI(cards);
return chosen != null ? chosen.getController() : Iterables.getFirst(options, null);

View File

@@ -1,24 +1,19 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.AiCardMemory;
import forge.ai.AiPlayDecision;
import forge.ai.AiProps;
import forge.ai.ComputerUtilCard;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.ApiType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilityActivated;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi {
@Override
@@ -34,8 +29,8 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
final SpellAbility top = game.getStack().peekAbility();
if (top != null
&& top.getPayCosts().getCostMana() != null
&& sa.getPayCosts().getCostMana() != null
&& top.getPayCosts() != null && top.getPayCosts().getCostMana() != null
&& sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null
&& top.getPayCosts().getCostMana().getMana().getCMC() >= sa.getPayCosts().getCostMana().getMana().getCMC() + diff) {
// The copied spell has a significantly higher CMC than the copy spell, consider copying
chance = 100;
@@ -68,7 +63,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
}
if (top.isWrapper() || top.isActivatedAbility()) {
if (top.isWrapper() || !(top instanceof SpellAbility || top instanceof AbilityActivated)) {
// Shouldn't even try with triggered or wrapped abilities at this time, will crash
return false;
} else if (top.getApi() == ApiType.CopySpellAbility) {
@@ -96,7 +91,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
AiPlayDecision decision = AiPlayDecision.CantPlaySa;
if (top instanceof Spell) {
decision = ((PlayerControllerAi) aiPlayer.getController()).getAi().canPlayFromEffectAI((Spell) topCopy, true, true);
} else if (top.isActivatedAbility() && top.getActivatingPlayer().equals(aiPlayer)
} else if (top instanceof AbilityActivated && top.getActivatingPlayer().equals(aiPlayer)
&& logic.contains("CopyActivatedAbilities")) {
decision = AiPlayDecision.WillPlay; // FIXME: we activated it once, why not again? Or bad idea?
}

View File

@@ -1,19 +1,6 @@
package forge.ai.ability;
import java.util.Iterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -29,6 +16,11 @@ import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Iterator;
public class CounterAi extends SpellAbilityAi {
@@ -111,9 +103,8 @@ public class CounterAi extends SpellAbilityAi {
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
// TODO use ComputerUtilCost.getMaxXValue
toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
@@ -132,7 +123,7 @@ public class CounterAi extends SpellAbilityAi {
}
if (setPayX) {
sa.setXManaCostPaid(toPay);
source.setSVar("PayX", Integer.toString(toPay));
}
}
@@ -276,7 +267,7 @@ public class CounterAi extends SpellAbilityAi {
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else {
@@ -298,7 +289,7 @@ public class CounterAi extends SpellAbilityAi {
}
if (setPayX) {
sa.setXManaCostPaid(toPay);
source.setSVar("PayX", Integer.toString(toPay));
}
}
}

View File

@@ -26,7 +26,6 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.keyword.Keyword;
import forge.util.Aggregates;
@@ -98,12 +97,10 @@ public abstract class CountersAi {
final CardCollection boon = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getCounters(CounterEnumType.DIVINITY) == 0;
return c.getCounters(CounterType.DIVINITY) == 0;
}
});
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon, null, false);
} 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.
// Charge counters on things with Charge abilities, etc. Expand

View File

@@ -1,22 +1,13 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -26,6 +17,9 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.List;
import java.util.Map;
public class CountersMoveAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
@@ -48,14 +42,14 @@ public class CountersMoveAi extends SpellAbilityAi {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Card host = sa.getHostCard();
final String type = sa.getParam("CounterType");
final CounterType cType = "Any".equals(type) ? null : CounterType.getType(type);
final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type);
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return false;
}
if (cType != null && cType.is(CounterEnumType.P1P1) && sa.hasParam("Source")) {
if (CounterType.P1P1.equals(cType) && sa.hasParam("Source")) {
int amount = calcAmount(sa, cType);
final List<Card> srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa);
if (ph.getPlayerTurn().isOpponentOf(ai)) {
@@ -96,9 +90,11 @@ public class CountersMoveAi extends SpellAbilityAi {
}
// for Simic Fluxmage and other
return ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN);
if (!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) {
return false;
}
} else if (cType != null && cType.is(CounterEnumType.P1P1) && sa.hasParam("Defined")) {
} else if (CounterType.P1P1.equals(cType) && sa.hasParam("Defined")) {
// something like Cyptoplast Root-kin
if (ph.getPlayerTurn().isOpponentOf(ai)) {
if (ph.inCombat() && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
@@ -111,7 +107,9 @@ public class CountersMoveAi extends SpellAbilityAi {
}
// Make sure that removing the last counter doesn't kill the creature
if ("Self".equals(sa.getParam("Source"))) {
return host == null || host.getNetToughness() - 1 > 0;
if (host != null && host.getNetToughness() - 1 <= 0) {
return false;
}
}
}
return true;
@@ -121,7 +119,6 @@ public class CountersMoveAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!moveTgtAI(ai, sa) && !mandatory) {
return false;
@@ -149,7 +146,7 @@ public class CountersMoveAi extends SpellAbilityAi {
final Card host = sa.getHostCard();
final String type = sa.getParam("CounterType");
final CounterType cType = "Any".equals(type) ? null : CounterType.getType(type);
final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type);
final List<Card> srcCards = AbilityUtils.getDefinedCards(host, sa.getParam("Source"), sa);
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
@@ -196,7 +193,9 @@ public class CountersMoveAi extends SpellAbilityAi {
// check for some specific AI preferences
if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0;
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
return false;
}
}
}
// no target
@@ -208,7 +207,9 @@ public class CountersMoveAi extends SpellAbilityAi {
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if (sa.usesTargeting()) {
sa.resetTargets();
return moveTgtAI(ai, sa);
if (!moveTgtAI(ai, sa)) {
return false;
}
}
return true;
@@ -241,7 +242,7 @@ public class CountersMoveAi extends SpellAbilityAi {
final Card host = sa.getHostCard();
final Game game = ai.getGame();
final String type = sa.getParam("CounterType");
final CounterType cType = "Any".equals(type) || "All".equals(type) ? null : CounterType.getType(type);
final CounterType cType = "Any".equals(type) ? null : CounterType.valueOf(type);
List<Card> tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
@@ -285,8 +286,11 @@ public class CountersMoveAi extends SpellAbilityAi {
// do not steal a P1P1 from Undying if it would die
// this way
if (cType != null && cType.is(CounterEnumType.P1P1) && srcCardCpy.getNetToughness() <= 0) {
return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken();
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken()) {
return true;
}
return false;
}
return true;
}
@@ -328,13 +332,13 @@ public class CountersMoveAi extends SpellAbilityAi {
}
// try to remove P1P1 from undying or evolve
if (cType != null && cType.is(CounterEnumType.P1P1)) {
if (CounterType.P1P1.equals(cType)) {
if (card.hasKeyword(Keyword.UNDYING) || card.hasKeyword(Keyword.EVOLVE)
|| card.hasKeyword(Keyword.ADAPT)) {
return true;
}
}
if (cType != null && cType.is(CounterEnumType.M1M1) && card.hasKeyword(Keyword.PERSIST)) {
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return true;
}
@@ -389,10 +393,10 @@ public class CountersMoveAi extends SpellAbilityAi {
}
if (cType != null) {
if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) {
if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) {
return false;
}
if (cType.is(CounterEnumType.M1M1) && card.hasKeyword(Keyword.PERSIST)) {
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return false;
}
@@ -400,7 +404,7 @@ public class CountersMoveAi extends SpellAbilityAi {
return false;
}
}
return true;
return false;
}
});
@@ -459,7 +463,7 @@ public class CountersMoveAi extends SpellAbilityAi {
// or for source -> multiple defined
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
Player targetedPlayer, Map<String, Object> params) {
Player targetedPlayer) {
if (sa.hasParam("AiLogic")) {
String logic = sa.getParam("AiLogic");

View File

@@ -16,7 +16,6 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -49,7 +48,6 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!c.canReceiveCounters(counterType)) {
return false;
}
} else {
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
// has negative counter it would double
@@ -78,7 +76,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final CounterType counterType = getCounterType(sa);
if (counterType != null && !counterType.is(CounterEnumType.P1P1)) {
if (!CounterType.P1P1.equals(counterType) && counterType != null) {
if (!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)) {
@@ -98,7 +96,10 @@ public class CountersMultiplyAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return !sa.usesTargeting() || setTargets(ai, sa) || mandatory;
if (sa.usesTargeting() && !setTargets(ai, sa) && !mandatory) {
return false;
}
return true;
}
private CounterType getCounterType(SpellAbility sa) {
@@ -148,15 +149,15 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!aiList.isEmpty()) {
// counter type list to check
// first loyalty, then P1P!, then Charge Counter
List<CounterEnumType> typeList = Lists.newArrayList(CounterEnumType.LOYALTY, CounterEnumType.P1P1, CounterEnumType.CHARGE);
for (CounterEnumType type : typeList) {
List<CounterType> typeList = Lists.newArrayList(CounterType.LOYALTY, CounterType.P1P1, CounterType.CHARGE);
for (CounterType type : typeList) {
// enough targets
if (!sa.canAddMoreTarget()) {
break;
}
if (counterType == null || counterType.is(type)) {
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
if (counterType == null || counterType == type) {
addTargetsByCounterType(ai, sa, aiList, type);
}
}
}
@@ -165,7 +166,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!oppList.isEmpty()) {
// not enough targets
if (sa.canAddMoreTarget()) {
final CounterType type = CounterType.get(CounterEnumType.M1M1);
final CounterType type = CounterType.M1M1;
if (counterType == null || counterType == type) {
addTargetsByCounterType(ai, sa, oppList, type);
}
@@ -173,7 +174,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}
// targeting does failed
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
if (!sa.isTargetNumberValid() || sa.getTargets().getNumTargeted() == 0) {
sa.resetTargets();
return false;
}

View File

@@ -1,21 +1,15 @@
package forge.ai.ability;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -33,20 +27,16 @@ public class CountersProliferateAi extends SpellAbilityAi {
for (final Player p : allies) {
// player has experience or energy counter
if (p.getCounters(CounterEnumType.EXPERIENCE) + p.getCounters(CounterEnumType.ENERGY) >= 1) {
if (p.getCounters(CounterType.EXPERIENCE) + p.getCounters(CounterType.ENERGY) >= 1) {
allyExpOrEnergy = true;
}
cperms.addAll(CardLists.filter(p.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
if (!crd.hasCounters()) {
if (crd.hasCounters()) {
return false;
}
if (crd.isPlaneswalker()) {
return true;
}
// iterate only over existing counters
for (final Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= 1 && !ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
@@ -66,11 +56,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
hperms.addAll(CardLists.filter(o.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
if (!crd.hasCounters()) {
return false;
}
if (crd.isPlaneswalker()) {
if (crd.hasCounters()) {
return false;
}
@@ -86,7 +72,10 @@ public class CountersProliferateAi extends SpellAbilityAi {
}
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy;
if (cperms.isEmpty() && hperms.isEmpty() && !opponentPoison && !allyExpOrEnergy) {
return false;
}
return true;
}
@Override
@@ -103,70 +92,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
return canPlayAI(ai, sa);
}
return checkApiLogic(ai, sa);
}
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#chooseSingleEntity(forge.game.player.Player, forge.game.spellability.SpellAbility, java.util.Collection, boolean, forge.game.player.Player)
*/
@SuppressWarnings("unchecked")
@Override
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 = CounterType.get(CounterEnumType.POISON);
// because countertype can't be chosen anymore, only look for posion counters
for (final Player p : Iterables.filter(options, Player.class)) {
if (p.isOpponentOf(ai)) {
if (p.getCounters(poison) > 0 && p.canReceiveCounters(poison)) {
return (T)p;
}
} else {
if (p.getCounters(poison) <= 5 || p.canReceiveCounters(poison)) {
return (T)p;
}
}
}
for (final Card c : Iterables.filter(options, Card.class)) {
// AI planeswalker always, opponent planeswalkers never
if (c.isPlaneswalker()) {
if (c.getController().isOpponentOf(ai)) {
continue;
} else {
return (T)c;
}
}
final Card lki = CardUtil.getLKICopy(c);
// update all the counters there
boolean hasNegative = false;
for (final CounterType ct : c.getCounters().keySet()) {
hasNegative = hasNegative || ComputerUtil.isNegativeCounter(ct, c);
lki.setCounters(ct, lki.getCounters(ct) + 1);
}
// TODO need more logic there?
// it tries to evaluate the creatures
if (c.isCreature()) {
if (c.getController().isOpponentOf(ai) ==
(ComputerUtilCard.evaluateCreature(lki, true, false)
< ComputerUtilCard.evaluateCreature(c, true, false))) {
return (T)c;
}
} else {
if (!c.getController().isOpponentOf(ai) && !hasNegative) {
return (T)c;
}
}
}
return null;
}
}

View File

@@ -1,43 +1,19 @@
package forge.ai.ability;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialAiLogic;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -51,6 +27,10 @@ import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class CountersPutAi extends SpellAbilityAi {
/*
@@ -76,17 +56,17 @@ public class CountersPutAi extends SpellAbilityAi {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType counterType = remCounter.counter;
if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) {
if (counterType.name().equals(type) && !aiLogic.startsWith("MoveCounter")) {
return false;
}
if (!part.payCostFromSource()) {
if (counterType.is(CounterEnumType.P1P1)) {
if (counterType.equals(CounterType.P1P1)) {
return false;
}
continue;
}
// don't kill the creature
if (counterType.is(CounterEnumType.P1P1) && source.getLethalDamage() <= 1) {
if (counterType.equals(CounterType.P1P1) && source.getLethalDamage() <= 1) {
return false;
}
}
@@ -129,11 +109,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
}
int maxLevel = Integer.parseInt(sa.getParam("MaxLevel"));
return source.getCounters(CounterEnumType.LEVEL) < maxLevel;
}
if ("CrawlingBarrens".equals(sa.getParam("AILogic"))) {
return true;
return source.getCounters(CounterType.LEVEL) < maxLevel;
}
return super.checkPhaseRestrictions(ai, sa, ph);
@@ -150,7 +126,7 @@ public class CountersPutAi extends SpellAbilityAi {
Card choice = null;
final String type = sa.getParam("CounterType");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final String logic = sa.getParamOrDefault("AILogic", "");
PhaseHandler ph = ai.getGame().getPhaseHandler();
@@ -170,7 +146,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (abTgt.canTgtPlayer()) {
// try to kill opponent with Poison
PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9));
PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterType.POISON, 9));
if (!poisonList.isEmpty()) {
sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife()));
return true;
@@ -181,13 +157,13 @@ public class CountersPutAi extends SpellAbilityAi {
// try to kill creature with -1/-1 counters if it can
// receive counters, execpt it has undying
CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1));
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
return input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1));
return input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.M1M1);
}
});
@@ -244,10 +220,6 @@ public class CountersPutAi extends SpellAbilityAi {
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 true;
} else if ("PayEnergyConservatively".equals(logic)) {
@@ -268,7 +240,7 @@ public class CountersPutAi extends SpellAbilityAi {
int totBlkPower = Aggregates.sum(blocked, CardPredicates.Accessors.fnGetNetPower);
int totBlkToughness = Aggregates.min(blocked, CardPredicates.Accessors.fnGetNetToughness);
int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
int numActivations = ai.getCounters(CounterType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
if (sa.getHostCard().getNetToughness() + numActivations > totBlkPower
|| sa.getHostCard().getNetPower() + numActivations >= totBlkToughness) {
return true;
@@ -283,7 +255,7 @@ public class CountersPutAi extends SpellAbilityAi {
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return true;
}
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
} else if (ai.getCounters(CounterType.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) {
@@ -312,11 +284,9 @@ public class CountersPutAi extends SpellAbilityAi {
}
} else if (logic.startsWith("MoveCounter")) {
return doMoveCounterLogic(ai, sa, ph);
} else if (logic.equals("CrawlingBarrens")) {
return SpecialCardAi.CrawlingBarrens.consider(ai, sa);
}
if (!sa.metConditions() && sa.getSubAbility() == null) {
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
return false;
}
@@ -341,20 +311,13 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
if (sa.hasParam("Adapt") && source.getCounters(CounterType.P1P1) > 0) {
return false;
}
// TODO handle proper calculation of X values based on Cost
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
if (sa.hasParam("Adapt")) {
Game game = ai.getGame();
Combat combat = game.getCombat();
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);
}
}
if ("Fight".equals(logic)) {
int nPump = 0;
if (type.equals("P1P1")) {
@@ -364,14 +327,14 @@ public class CountersPutAi extends SpellAbilityAi {
}
if (amountStr.equals("X")) {
if (sa.getSVar(amountStr).equals("Count$xPaid")) {
if (source.getSVar(amountStr).equals("Count$xPaid")) {
// By default, set PayX here to maximum value (used for most SAs of this type).
amount = ComputerUtilCost.getMaxXValue(sa, ai);
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (isClockwork) {
// Clockwork Avian and other similar cards: do not tap all mana for X,
// instead only rewind to max allowed value when the power gets low enough.
int curCtrs = source.getCounters(CounterEnumType.P1P0);
int curCtrs = source.getCounters(CounterType.P1P0);
int maxCtrs = Integer.parseInt(sa.getParam("MaxFromEffect"));
// This will "rewind" clockwork cards when they fall to 50% power or below, consider improving
@@ -385,7 +348,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
}
sa.setXManaCostPaid(amount);
source.setSVar("PayX", Integer.toString(amount));
} else if ("ExiledCreatureFromGraveCMC".equals(logic)) {
// e.g. Necropolis
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc);
@@ -424,15 +387,16 @@ public class CountersPutAi extends SpellAbilityAi {
}
if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa)) {
final TargetRestrictions abTgt = sa.getTargetRestrictions();
// only evaluates case where all tokens are placed on a single target
if (sa.usesTargeting() && sa.getMinTargets() < 2) {
if (sa.usesTargeting() && abTgt.getMinTargets(source, sa) < 2) {
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
Card c = sa.getTargets().getFirstTargetedCard();
if (sa.getTargets().size() > 1) {
if (sa.getTargets().getNumTargeted() > 1) {
sa.resetTargets();
sa.getTargets().add(c);
}
sa.addDividedAllocation(sa.getTargetCard(), amount);
abTgt.addDividedAllocation(sa.getTargetCard(), amount);
return true;
} else {
return false;
@@ -460,7 +424,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (sacSelf && c.equals(source)) {
return false;
}
return sa.canTarget(c) && c.canReceiveCounters(CounterType.getType(type));
return sa.canTarget(c) && c.canReceiveCounters(CounterType.valueOf(type));
}
});
@@ -479,14 +443,16 @@ public class CountersPutAi extends SpellAbilityAi {
// but try to do it in Main 2 then so that the AI has a chance to play creatures first.
if (list.isEmpty()
&& sa.hasParam("Planeswalker")
&& sa.getPayCosts() != null
&& sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
&& sa.isTargetNumberValid()
&& sa.getTargets().size() == 0
&& sa.getTargets().getNumTargeted() == 0
&& ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
return true;
}
if (sourceName.equals("Abzan Charm")) {
final TargetRestrictions abTgt = sa.getTargetRestrictions();
// specific AI for instant with distribute two +1/+1 counters
ComputerUtilCard.sortByEvaluateCreature(list);
// maximise the number of targets
@@ -494,13 +460,13 @@ public class CountersPutAi extends SpellAbilityAi {
int left = amount;
for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, i, i,
Lists.newArrayList())) {
Lists.<String>newArrayList())) {
sa.getTargets().add(c);
sa.addDividedAllocation(c, i);
abTgt.addDividedAllocation(c, i);
left -= i;
}
if (left < i || sa.getTargets().size() == sa.getMaxTargets()) {
sa.addDividedAllocation(sa.getTargets().getFirstTargetedCard(), left + i);
if (left < i || sa.getTargets().getNumTargeted() == abTgt.getMaxTargets(source, sa)) {
abTgt.addDividedAllocation(sa.getTargets().getFirstTargetedCard(), left + i);
left = 0;
break;
}
@@ -516,7 +482,7 @@ public class CountersPutAi extends SpellAbilityAi {
// target loop
while (sa.canAddMoreTarget()) {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || (sa.getTargets().size() == 0)) {
if (!sa.isTargetNumberValid() || (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -531,7 +497,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (type.equals("P1P1") && !SpellAbilityAi.isSorcerySpeed(sa)) {
for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount,
Lists.newArrayList())) {
Lists.<String>newArrayList())) {
choice = c;
break;
}
@@ -554,7 +520,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
if (!sa.isTargetNumberValid() || sa.getTargets().getNumTargeted() == 0) {
sa.resetTargets();
return false;
} else {
@@ -566,7 +532,7 @@ public class CountersPutAi extends SpellAbilityAi {
list.remove(choice);
sa.getTargets().add(choice);
if (divided) {
sa.addDividedAllocation(choice, amount);
sa.getTargetRestrictions().addDividedAllocation(choice, amount);
break;
}
choice = null;
@@ -582,7 +548,7 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
final int currCounters = cards.get(0).getCounters(CounterType.valueOf(type));
// each non +1/+1 counter on the card is a 10% chance of not
// activating this ability.
@@ -633,7 +599,7 @@ public class CountersPutAi extends SpellAbilityAi {
final String logic = sa.getParamOrDefault("AILogic", "");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
@@ -661,7 +627,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid()
|| sa.getTargets().size() == 0) {
|| sa.getTargets().getNumTargeted() == 0) {
sa.resetTargets();
return false;
} else {
@@ -685,7 +651,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (choice == null) { // can't find anything left
if ((!sa.isTargetNumberValid())
|| (sa.getTargets().size() == 0)) {
|| (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -696,7 +662,7 @@ public class CountersPutAi extends SpellAbilityAi {
list.remove(choice);
sa.getTargets().add(choice);
if (divided) {
sa.addDividedAllocation(choice, amount);
sa.getTargetRestrictions().addDividedAllocation(choice, amount);
break;
}
}
@@ -707,14 +673,13 @@ public class CountersPutAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard();
// boolean chance = true;
boolean preferred = true;
CardCollection list;
final String type = sa.getParam("CounterType");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.isDividedAsYouChoose();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
int left = amount;
@@ -723,14 +688,14 @@ public class CountersPutAi extends SpellAbilityAi {
list = new CardCollection(AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa));
if (amountStr.equals("X")
&& root.getXManaCostPaid() != null /* SubAbility on something that already had set PayX, e.g. Endless One ETB counters */
&& sa.hasParam(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) {
&& !source.hasSVar("PayX") /* SubAbility on something that already had set PayX, e.g. Endless One ETB counters */
&& ((sa.hasParam(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) || source.getSVar(amountStr).equals("Count$xPaid") )) {
// detect if there's more than one X in the cost (Hangarback Walker, Walking Ballista, etc.)
SpellAbility testSa = sa;
int countX = 0;
int nonXGlyphs = 0;
while (testSa != null && countX == 0) {
while (testSa != null && testSa.getPayCosts() != null && countX == 0) {
countX = testSa.getPayCosts().getTotalMana().countX();
nonXGlyphs = testSa.getPayCosts().getTotalMana().getGlyphCount() - countX;
testSa = testSa.getSubAbility();
@@ -742,7 +707,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
// Spend all remaining mana to add X counters (eg. Hero of Leina Tower)
int payX = ComputerUtilCost.getMaxXValue(sa, ai);
int payX = ComputerUtilMana.determineLeftoverMana(sa, ai);
// Account for the possible presence of additional glyphs in cost (e.g. Mikaeus, the Lunarch; Primordial Hydra)
payX -= nonXGlyphs;
@@ -750,7 +715,7 @@ public class CountersPutAi extends SpellAbilityAi {
// Account for the multiple X in cost
if (countX > 1) { payX /= countX; }
root.setXManaCostPaid(payX);
source.setSVar("PayX", Integer.toString(payX));
}
if (!mandatory) {
@@ -758,21 +723,6 @@ public class CountersPutAi extends SpellAbilityAi {
// put a counter?
// things like Powder Keg, which are way too complex for the AI
}
} else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) {
// can only target opponent
List<Player> playerList = Lists.newArrayList(Iterables.filter(
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
if (playerList.isEmpty() && mandatory) {
return false;
}
// try to choose player with less creatures
Player choice = Collections.min(playerList, PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.CREATURES));
if (choice != null) {
sa.getTargets().add(choice);
}
} else {
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
@@ -842,11 +792,12 @@ public class CountersPutAi extends SpellAbilityAi {
}
}
if (choice != null && divided) {
final TargetRestrictions abTgt = sa.getTargetRestrictions();
int alloc = Math.max(amount / totalTargets, 1);
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
sa.addDividedAllocation(choice, left);
if (sa.getTargets().getNumTargeted() == Math.min(totalTargets, abTgt.getMaxTargets(sa.getHostCard(), sa)) - 1) {
abTgt.addDividedAllocation(choice, left);
} else {
sa.addDividedAllocation(choice, alloc);
abTgt.addDividedAllocation(choice, alloc);
left -= alloc;
}
}
@@ -913,7 +864,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
@Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
// used by Tribute, select player with lowest Life
// TODO add more logic using TributeAILogic
List<Player> list = Lists.newArrayList(options);
@@ -921,20 +872,16 @@ public class CountersPutAi extends SpellAbilityAi {
}
@Override
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
// Bolster does use this
// TODO need more or less logic there?
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) {
return Iterables.getFirst(options, null);
}
final CounterType type = params.containsKey("CounterType") ? (CounterType)params.get("CounterType")
: CounterType.getType(sa.getParam("CounterType"));
final CounterType type = CounterType.valueOf(sa.getParam("CounterType"));
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
@@ -951,7 +898,7 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
if (ComputerUtilCard.isUselessCreature(ai, input))
return false;
if (type.is(CounterEnumType.M1M1) && amount >= input.getNetToughness())
if (CounterType.M1M1.equals(type) && amount >= input.getNetToughness())
return true;
return ComputerUtil.isNegativeCounter(type, input);
}
@@ -975,20 +922,6 @@ public class CountersPutAi extends SpellAbilityAi {
CardCollection filtered = mine;
// Try to filter out keywords that we already have on cards
if (type.isKeywordCounter()) {
Keyword kw = Keyword.smartValueOf(type.getName());
final CardCollection doNotHaveKeyword = CardLists.filter(filtered, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return !card.hasKeyword(kw) && card.canBeTargetedBy(sa) && sa.canTarget(card);
}
});
if (doNotHaveKeyword.size() > 0)
filtered = doNotHaveKeyword;
}
final CardCollection notUseless = CardLists.filter(filtered, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
@@ -1003,26 +936,26 @@ public class CountersPutAi extends SpellAbilityAi {
}
// some special logic to reload Persist/Undying
if (p1p1.equals(type)) {
if (CounterType.P1P1.equals(type)) {
final CardCollection persist = CardLists.filter(filtered, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
if (!input.hasKeyword(Keyword.PERSIST))
return false;
return input.getCounters(m1m1) <= amount;
return input.getCounters(CounterType.M1M1) <= amount;
}
});
if (!persist.isEmpty()) {
filtered = persist;
}
} else if (m1m1.equals(type)) {
} else if (CounterType.M1M1.equals(type)) {
final CardCollection undying = CardLists.filter(filtered, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
if (!input.hasKeyword(Keyword.UNDYING))
return false;
return input.getCounters(p1p1) <= amount && input.getNetToughness() > amount;
return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount;
}
});
@@ -1045,8 +978,8 @@ public class CountersPutAi extends SpellAbilityAi {
if (e instanceof Card) {
Card c = (Card) e;
if (c.getController().isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.M1M1);
if (options.contains(CounterType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.M1M1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, c)) {
@@ -1055,7 +988,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
} else {
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, c) && !ComputerUtil.isUselessCounter(type, c)) {
if (!ComputerUtil.isNegativeCounter(type, c) && !ComputerUtil.isUselessCounter(type)) {
return type;
}
}
@@ -1063,12 +996,12 @@ public class CountersPutAi extends SpellAbilityAi {
} else if (e instanceof Player) {
Player p = (Player) e;
if (p.isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
return CounterType.get(CounterEnumType.POISON);
if (options.contains(CounterType.POISON)) {
return CounterType.POISON;
}
} else {
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
return CounterType.get(CounterEnumType.EXPERIENCE);
if (options.contains(CounterType.EXPERIENCE)) {
return CounterType.EXPERIENCE;
}
}
@@ -1116,39 +1049,4 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) {
if (combat.isAttacking(source)) {
if (!combat.isBlocked(source)) {
return true;
} else {
for (Card blockedBy : combat.getBlockers(source)) {
if (blockedBy.getNetToughness() > source.getNetPower()
&& blockedBy.getNetToughness() <= source.getNetPower() + amount) {
return true;
}
}
int totBlkPower = Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
if (source.getNetToughness() <= totBlkPower
&& source.getNetToughness() + amount > totBlkPower) {
return true;
}
}
} else if (combat.isBlocking(source)) {
for (Card blocked : combat.getAttackersBlockedBy(source)) {
if (blocked.getNetToughness() > source.getNetPower()
&& blocked.getNetToughness() <= source.getNetPower() + amount) {
return true;
}
}
int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), CardPredicates.Accessors.fnGetNetPower);
if (source.getNetToughness() <= totAtkPower
&& source.getNetToughness() + amount > totAtkPower) {
return true;
}
}
return false;
}
}

View File

@@ -1,11 +1,10 @@
package forge.ai.ability;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -21,6 +20,8 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
public class CountersPutAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -38,13 +39,8 @@ public class CountersPutAllAi extends SpellAbilityAi {
final boolean curse = sa.isCurse();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if ("OwnCreatsAndOtherPWs".equals(sa.getParam("AILogic"))) {
hList = CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), "Creature.YouCtrl,Planeswalker.YouCtrl+Other", source.getController(), source, sa);
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.YouCtrl,Planeswalker.YouCtrl+Other", source.getController(), source, sa);
} else {
hList = CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
}
hList = CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
if (abCost != null) {
// AI currently disabled for these costs
@@ -52,7 +48,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
return false;
}
@@ -72,7 +68,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
if (tgt != null) {
Player pl = curse ? ai.getWeakestOpponent() : ai;
Player pl = curse ? ComputerUtil.getOpponentFor(ai) : ai;
sa.getTargets().add(pl);
hList = CardLists.filterControlledBy(hList, pl);
@@ -82,10 +78,10 @@ public class CountersPutAllAi extends SpellAbilityAi {
// TODO improve X value to don't overpay when extra mana won't do
// anything more useful
final int amount;
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
amount = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(amount);
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(amount));
} else {
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
}
@@ -120,7 +116,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
//Check for cards that could profit from the ability
PhaseHandler phase = ai.getGame().getPhaseHandler();
if (type.equals("P1P1") && sa.isAbility() && source.isCreature()
&& sa.getPayCosts().hasTapCost()
&& sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()
&& sa instanceof AbilitySub
&& (!phase.getNextTurn().equals(ai)
|| phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS))) {
@@ -153,7 +149,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return player.getCreaturesInPlay().size() >= player.getWeakestOpponent().getCreaturesInPlay().size();
return player.getCreaturesInPlay().size() >= ComputerUtil.getOpponentFor(player).getCreaturesInPlay().size();
}
@Override
@@ -180,6 +176,6 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
}
return mandatory || canPlayAI(aiPlayer, sa);
return mandatory;
}
}

View File

@@ -17,27 +17,21 @@
*/
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import java.util.List;
import java.util.Map;
/**
* <p>
* AbilityFactory_PutOrRemoveCountersAi class.
@@ -81,7 +75,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (sa.hasParam("CounterType")) {
// currently only Jhoira's Timebug
final CounterType type = CounterType.getType(sa.getParam("CounterType"));
final CounterType type = CounterType.valueOf(sa.getParam("CounterType"));
CardCollection countersList = CardLists.filter(list, CardPredicates.hasCounter(type, amount));
@@ -106,7 +100,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
CardCollectionView depthsList = CardLists.filter(countersList,
CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterEnumType.ICE));
CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterType.ICE));
if (!depthsList.isEmpty()) {
sa.getTargets().add(depthsList.getFirst());
@@ -119,7 +113,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
CardCollection planeswalkerList = CardLists.filter(
CardLists.filterControlledBy(countersList, ai.getOpponents()),
CardPredicates.Presets.PLANESWALKERS,
CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount));
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
if (!planeswalkerList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList));
@@ -129,7 +123,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
// do as M1M1 part
CardCollection aiList = CardLists.filterControlledBy(countersList, ai);
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1));
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
if (!aiPersistList.isEmpty()) {
@@ -142,7 +136,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
// do as P1P1 part
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.P1P1));
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
if (!aiUndyingList.isEmpty()) {
@@ -163,7 +157,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (!ComputerUtil.isNegativeCounter(aType, best)) {
sa.getTargets().add(best);
return true;
} else if (!ComputerUtil.isUselessCounter(aType, best)) {
} else if (!ComputerUtil.isUselessCounter(aType)) {
// whould remove positive counter
if (best.getCounters(aType) <= amount) {
sa.getTargets().add(best);
@@ -205,18 +199,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
Card tgt = (Card) params.get("Target");
// planeswalker has high priority for loyalty counters
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
return CounterType.get(CounterEnumType.LOYALTY);
if (tgt.isPlaneswalker() && options.contains(CounterType.LOYALTY)) {
return CounterType.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(CounterType.get(CounterEnumType.P1P1))) {
return CounterType.get(CounterEnumType.P1P1);
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterType.get(CounterEnumType.M1M1);
if (options.contains(CounterType.P1P1)) {
return CounterType.P1P1;
} else if (options.contains(CounterType.M1M1)) {
return CounterType.M1M1;
}
}
@@ -228,14 +222,14 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
} else {
// this counters are treat first to be removed
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.ICE)) {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
return CounterType.get(CounterEnumType.ICE);
return CounterType.ICE;
}
} 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);
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.P1P1)) {
return CounterType.P1P1;
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.M1M1)) {
return CounterType.M1M1;
}
// fallback logic, select positive counter to add more
@@ -268,19 +262,19 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
if (tgt.getController().isOpponentOf(ai)) {
if (type.is(CounterEnumType.LOYALTY) && tgt.isPlaneswalker()) {
if (type.equals(CounterType.LOYALTY) && tgt.isPlaneswalker()) {
return false;
}
return ComputerUtil.isNegativeCounter(type, tgt);
} else {
if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) {
if (type.equals(CounterType.ICE) && "Dark Depths".equals(tgt.getName())) {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
return false;
}
} else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {
} else if (type.equals(CounterType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {
return false;
} else if (type.is(CounterEnumType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) {
} else if (type.equals(CounterType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) {
return false;
}

View File

@@ -1,25 +1,14 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -28,15 +17,10 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
public class CountersRemoveAi extends SpellAbilityAi {
import java.util.List;
import java.util.Map;
@Override
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
}
return super.canPlayWithoutRestrict(ai, sa);
}
public class CountersRemoveAi extends SpellAbilityAi {
/*
* (non-Javadoc)
@@ -89,7 +73,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
if (!type.matches("Any") && !type.matches("All")) {
final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type));
final int currCounters = sa.getHostCard().getCounters(CounterType.valueOf(type));
if (currCounters < 1) {
return false;
}
@@ -116,7 +100,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
@@ -126,7 +110,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths");
depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa),
CardPredicates.hasCounter(CounterEnumType.ICE, 3));
CardPredicates.hasCounter(CounterType.ICE, 3));
if (!depthsList.isEmpty()) {
sa.getTargets().add(depthsList.getFirst());
@@ -139,7 +123,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS,
CardPredicates.hasCounter(CounterEnumType.LOYALTY, 5));
CardPredicates.hasCounter(CounterType.LOYALTY, 5));
if (!planeswalkerList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList));
@@ -150,8 +134,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
// variable amount for Hex Parasite
int amount;
boolean xPay = false;
if (amountStr.equals("X") && sa.getSVar("X").equals("Count$xPaid")) {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai);
if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) {
final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (manaLeft == 0) {
return false;
@@ -166,15 +150,15 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths");
depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa),
CardPredicates.hasCounter(CounterEnumType.ICE));
CardPredicates.hasCounter(CounterType.ICE));
if (!depthsList.isEmpty()) {
Card depth = depthsList.getFirst();
int ice = depth.getCounters(CounterEnumType.ICE);
int ice = depth.getCounters(CounterType.ICE);
if (amount >= ice) {
sa.getTargets().add(depth);
if (xPay) {
sa.setXManaCostPaid(ice);
source.setSVar("PayX", Integer.toString(ice));
}
return true;
}
@@ -187,13 +171,13 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection planeswalkerList = CardLists.filter(list,
Predicates.and(CardPredicates.Presets.PLANESWALKERS, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
CardPredicates.hasLessCounter(CounterEnumType.LOYALTY, amount));
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
if (!planeswalkerList.isEmpty()) {
Card best = ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList);
sa.getTargets().add(best);
if (xPay) {
sa.setXManaCostPaid(best.getCurrentLoyalty());
source.setSVar("PayX", Integer.toString(best.getCurrentLoyalty()));
}
return true;
}
@@ -203,7 +187,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// do as M1M1 part
CardCollection aiList = CardLists.filterControlledBy(list, ai);
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1));
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
if (!aiPersistList.isEmpty()) {
@@ -216,20 +200,14 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
// do as P1P1 part
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasLessCounter(CounterEnumType.P1P1, amount));
CardCollection aiUndyingList = CardLists.getKeyword(aiP1P1List, Keyword.UNDYING);
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
if (!aiUndyingList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList));
return true;
aiP1P1List = aiUndyingList;
}
// remove P1P1 counters from opposing creatures
CardCollection oppP1P1List = CardLists.filter(list,
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
CardPredicates.hasCounter(CounterEnumType.P1P1));
if (!oppP1P1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));
if (!aiP1P1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiP1P1List));
return true;
}
@@ -251,7 +229,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// no special amount for that one yet
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
CardCollection aiList = CardLists.filterControlledBy(list, ai);
aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterEnumType.M1M1, amount));
aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount));
CardCollection aiPersist = CardLists.getKeyword(aiList, Keyword.PERSIST);
if (!aiPersist.isEmpty()) {
@@ -270,7 +248,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// no special amount for that one yet
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
list = CardLists.filter(list, CardPredicates.hasCounter(CounterEnumType.P1P1, amount));
list = CardLists.filter(list, CardPredicates.hasCounter(CounterType.P1P1, amount));
// currently only logic for Bloodcrazed Hoplite, but add logic for
// targeting ai creatures too
@@ -304,8 +282,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
int amount;
boolean xPay = false;
// Timecrafting has X R
if (amountStr.equals("X") && sa.getSVar("X").equals("Count$xPaid")) {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai);
if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) {
final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (manaLeft == 0) {
return false;
@@ -316,15 +294,15 @@ public class CountersRemoveAi extends SpellAbilityAi {
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
}
CardCollection timeList = CardLists.filter(list, CardPredicates.hasLessCounter(CounterEnumType.TIME, amount));
CardCollection timeList = CardLists.filter(list, CardPredicates.hasLessCounter(CounterType.TIME, amount));
if (!timeList.isEmpty()) {
Card best = ComputerUtilCard.getBestAI(timeList);
int timeCount = best.getCounters(CounterEnumType.TIME);
int timeCount = best.getCounters(CounterType.TIME);
sa.getTargets().add(best);
if (xPay) {
sa.setXManaCostPaid(timeCount);
source.setSVar("PayX", Integer.toString(timeCount));
}
return true;
}
@@ -342,7 +320,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection outlastCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.OUTLAST));
if (!outlastCreats.isEmpty()) {
// outlast cards often benefit from having +1/+1 counters, try not to remove last one
CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterEnumType.P1P1, 2));
CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterType.P1P1, 2));
if (!betterTargets.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
@@ -376,30 +354,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
*/
@Override
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
GameEntity target = (GameEntity) params.get("Target");
CounterType type = (CounterType) params.get("CounterType");
if (target instanceof Card) {
Card targetCard = (Card) target;
if (targetCard.getController().isOpponentOf(player)) {
return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
} else {
if (targetCard.hasKeyword(Keyword.UNDYING) && type.is(CounterEnumType.P1P1)
&& targetCard.getCounters(CounterEnumType.P1P1) >= max) {
return max;
}
return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
}
} else if (target instanceof Player) {
Player targetPlayer = (Player) target;
if (targetPlayer.isOpponentOf(player)) {
return !type.is(CounterEnumType.POISON) ? max : min;
} else {
return type.is(CounterEnumType.POISON) ? max : min;
}
}
// TODO Auto-generated method stub
return super.chooseNumber(player, sa, min, max, params);
}
@@ -415,49 +370,30 @@ public class CountersRemoveAi extends SpellAbilityAi {
return super.chooseCounterType(options, sa, params);
}
Player ai = sa.getActivatingPlayer();
GameEntity target = (GameEntity) params.get("Target");
Card target = (Card) params.get("Target");
if (target instanceof Card) {
Card targetCard = (Card) target;
if (targetCard.getController().isOpponentOf(ai)) {
if (target.getController().isOpponentOf(ai)) {
// if its a Planeswalker try to remove Loyality first
if (targetCard.isPlaneswalker()) {
return CounterType.get(CounterEnumType.LOYALTY);
if (target.isPlaneswalker()) {
return CounterType.LOYALTY;
}
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
if (!ComputerUtil.isNegativeCounter(type, target)) {
return type;
}
}
} else {
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);
if (options.contains(CounterType.M1M1) && target.hasKeyword(Keyword.PERSIST)) {
return CounterType.M1M1;
} else if (options.contains(CounterType.P1P1) && target.hasKeyword(Keyword.UNDYING)) {
return CounterType.M1M1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, targetCard)) {
if (ComputerUtil.isNegativeCounter(type, target)) {
return type;
}
}
}
} else if (target instanceof Player) {
Player targetPlayer = (Player) target;
if (targetPlayer.isOpponentOf(ai)) {
for (CounterType type : options) {
if (!type.is(CounterEnumType.POISON)) {
return type;
}
}
} else {
for (CounterType type : options) {
if (type.is(CounterEnumType.POISON)) {
return type;
}
}
}
}
return super.chooseCounterType(options, sa, params);
}
}

View File

@@ -1,7 +1,7 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -20,7 +20,7 @@ import forge.util.MyRandom;
public abstract class DamageAiBase extends SpellAbilityAi {
protected boolean avoidTargetP(final Player comp, final SpellAbility sa) {
Player enemy = comp.getWeakestOpponent();
Player enemy = ComputerUtil.getOpponentFor(comp);
// Logic for cards that damage owner, like Fireslinger
// Do not target a player if they aren't below 75% of our health.
// Unless Lifelink will cancel the damage to us
@@ -38,7 +38,9 @@ public abstract class DamageAiBase extends SpellAbilityAi {
}
if ("SelfDamage".equals(sa.getParam("AILogic"))) {
if (comp.getLife() * 0.75 < enemy.getLife()) {
return !lifelink;
if (!lifelink) {
return true;
}
}
}
return false;
@@ -52,7 +54,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention, final boolean noPlaneswalkerRedirection) {
int restDamage = d;
final Game game = comp.getGame();
Player enemy = comp.getWeakestOpponent();
Player enemy = ComputerUtil.getOpponentFor(comp);
boolean dmgByCardsInHand = false;
if ("X".equals(sa.getParam("NumDmg")) && sa.getHostCard() != null && sa.hasSVar(sa.getParam("NumDmg")) &&
@@ -67,7 +69,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
if (!sa.canTarget(enemy)) {
return false;
}
if (sa.getTargets() != null && sa.getTargets().contains(enemy)) {
if (sa.getTargets() != null && sa.getTargets().getTargets().contains(enemy)) {
return false;
}

View File

@@ -1,16 +1,12 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
@@ -51,9 +47,13 @@ public class DamageAllAi extends SpellAbilityAi {
dmg = ComputerUtilMana.getConvergeCount(sa, ai);
}
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
x = ComputerUtilCost.getMaxXValue(sa, ai);
x = ComputerUtilMana.determineLeftoverMana(sa, ai);
}
if (damage.equals("ChosenX")) {
x = source.getCounters(CounterType.LOYALTY);
}
if (x == -1) {
Player bestOpp = determineOppToKill(ai, sa, source, dmg);
if (determineOppToKill(ai, sa, source, dmg) != null) {
// we already know we can kill a player, so go for it
return true;
@@ -84,7 +84,10 @@ public class DamageAllAi extends SpellAbilityAi {
if (best_x > 0) {
if (sa.getSVar(damage).equals("Count$xPaid")) {
sa.setXManaCostPaid(best_x);
source.setSVar("PayX", Integer.toString(best_x));
}
if (damage.equals("ChosenX")) {
source.setSVar("ChosenX", "Number$" + best_x);
}
return true;
}
@@ -135,7 +138,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
int minGain = 200; // The minimum gain in destroyed creatures
if (sa.getPayCosts().isReusuableResource()) {
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
if (computerList.isEmpty()) {
minGain = 10; // nothing to lose
// no creatures to lose and player can be damaged
@@ -196,13 +199,12 @@ public class DamageAllAi extends SpellAbilityAi {
String validP = "";
final String damage = sa.getParam("NumDmg");
int dmg;
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
// Set PayX here to maximum value.
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(dmg);
} else {
dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
}
if (sa.hasParam("ValidPlayers")) {
@@ -210,7 +212,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
// Evaluate creatures getting killed
Player enemy = ai.getWeakestOpponent();
Player enemy = ComputerUtil.getOpponentFor(ai);
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -279,14 +281,12 @@ public class DamageAllAi extends SpellAbilityAi {
String validP = "";
final String damage = sa.getParam("NumDmg");
int dmg;
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
// Set PayX here to maximum value.
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(dmg);
} else {
dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
}
if (sa.hasParam("ValidPlayers")) {
@@ -294,7 +294,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
// Evaluate creatures getting killed
Player enemy = ai.getWeakestOpponent();
Player enemy = ComputerUtil.getOpponentFor(ai);
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
final TargetRestrictions tgt = sa.getTargetRestrictions();

View File

@@ -1,39 +1,15 @@
package forge.ai.ability;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostRemoveCounter;
@@ -49,6 +25,12 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class DamageDealAi extends DamageAiBase {
@Override
@@ -64,7 +46,7 @@ public class DamageDealAi extends DamageAiBase {
}
if (damage.equals("X") && sa.getSVar(damage).equals("Count$ChosenNumber")) {
int energy = ai.getCounters(CounterEnumType.ENERGY);
int energy = ai.getCounters(CounterType.ENERGY);
for (SpellAbility s : source.getSpellAbilities()) {
if ("PayEnergy".equals(s.getParam("AILogic"))) {
energy += AbilityUtils.calculateAmount(source, s.getParam("CounterNum"), sa);
@@ -91,13 +73,16 @@ public class DamageDealAi extends DamageAiBase {
}
// Set PayX here to maximum value.
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(dmg);
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.isInZone(ZoneType.Hand)) {
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
}
}
return this.damageTargetAI(ai, sa, dmg, true);
if (!this.damageTargetAI(ai, sa, dmg, true)) {
return false;
}
return true;
}
@Override
@@ -112,7 +97,7 @@ public class DamageDealAi extends DamageAiBase {
if (damage.equals("X")) {
if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
// Try not to waste spells like Blaze or Fireball on early targets, try to do more damage with them if possible
if (ai.getController().isAI()) {
@@ -129,8 +114,8 @@ public class DamageDealAi extends DamageAiBase {
}
// Set PayX here to maximum value. It will be adjusted later depending on the target.
sa.setXManaCostPaid(dmg);
} else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) {
source.setSVar("PayX", Integer.toString(dmg));
} else if (sa.getSVar(damage).contains("InYourHand") && source.getZone().is(ZoneType.Hand)) {
dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
@@ -173,15 +158,15 @@ public class DamageDealAi extends DamageAiBase {
}
}
}
if (!ai.getCreaturesAttackedThisTurn().isEmpty()) {
if (ai.getAttackedWithCreatureThisTurn()) {
dmg = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
}
} else if ("WildHunt".equals(logic)) {
// This dummy ability will just deal 0 damage, but holds the logic for the AI for Master of Wild Hunt
List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source, sa);
List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source);
dmg = Aggregates.sum(wolves, CardPredicates.Accessors.fnGetNetPower);
} else if ("Triskelion".equals(logic)) {
final int n = source.getCounters(CounterEnumType.P1P1);
final int n = source.getCounters(CounterType.P1P1);
if (n > 0) {
if (ComputerUtil.playImmediately(ai, sa)) {
/*
@@ -214,14 +199,14 @@ public class DamageDealAi extends DamageAiBase {
}
if (sourceName.equals("Sorin, Grim Nemesis")) {
int loyalty = source.getCounters(CounterEnumType.LOYALTY);
int loyalty = source.getCounters(CounterType.LOYALTY);
for (; loyalty > 0; loyalty--) {
if (this.damageTargetAI(ai, sa, loyalty, false)) {
dmg = ComputerUtilCombat.getEnoughDamageToKill(sa.getTargetCard(), loyalty, source, false, false);
if (dmg > loyalty || dmg < 1) {
continue; // in case the calculation gets messed up somewhere
}
sa.setXManaCostPaid(dmg);
source.setSVar("ChosenX", "Number$" + dmg);
return true;
}
}
@@ -241,11 +226,11 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) {
return false;
}
if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
return false;
}
@@ -282,30 +267,25 @@ public class DamageDealAi extends DamageAiBase {
}
}
if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) ||
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
sourceName.equals("Crater's Claws")){
// If I can kill my target by paying less mana, do it
if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.isDividedAsYouChoose()) {
int actualPay = dmg;
if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.hasParam("DividedAsYouChoose")) {
int actualPay = 0;
final boolean noPrevention = sa.hasParam("NoPrevention");
for (final Card c : sa.getTargets().getTargetCards()) {
final int adjDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
if (adjDamage < actualPay) {
if ((adjDamage > actualPay) && (adjDamage <= dmg)) {
actualPay = adjDamage;
}
}
if (sourceName.equals("Crater's Claws") && ai.hasFerocious()) {
actualPay = actualPay > 2 ? actualPay - 2 : 0;
}
sa.setXManaCostPaid(actualPay);
source.setSVar("PayX", Integer.toString(actualPay));
}
}
if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
final int cmc = sa.getXManaCostPaid();
return !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc)).isEmpty();
}
return true;
}
@@ -449,19 +429,19 @@ public class DamageDealAi extends DamageAiBase {
// As of right now, ranks planeswalkers by their Current Loyalty * 10 + Big buff if close to "Ultimate"
int bestScore = 0;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
int curLoyalty = pw.getCounters(CounterType.LOYALTY);
int pwScore = curLoyalty * 10;
for (SpellAbility sa : pw.getSpellAbilities()) {
if (sa.hasParam("Ultimate")) {
Integer loyaltyCost = 0;
if (sa.hasParam("Ultimate") && sa.getPayCosts() != null) {
int loyaltyCost = 0;
CostRemoveCounter remLoyalty = sa.getPayCosts().getCostPartByType(CostRemoveCounter.class);
if (remLoyalty != null) {
// if remLoyalty is null, generally there's an AddCounter<0/LOYALTY> cost, like for Gideon Jura.
loyaltyCost = remLoyalty.convertAmount();
}
if (loyaltyCost != null && loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
if (loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
// Will ultimate soon
pwScore += 10000;
}
@@ -477,22 +457,6 @@ public class DamageDealAi extends DamageAiBase {
return bestTgt;
}
private Card getWorstPlaneswalkerToDamage(final List<Card> pws) {
Card bestTgt = null;
int bestScore = Integer.MAX_VALUE;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
if (curLoyalty < bestScore) {
bestScore = curLoyalty;
bestTgt = pw;
}
}
return bestTgt;
}
private List<Card> getTargetableCards(Player ai, SpellAbility sa, Player pl, TargetRestrictions tgt, Player activator, Card source, Game game) {
List<Card> hPlay = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), activator, source, sa);
@@ -501,16 +465,18 @@ public class DamageDealAi extends DamageAiBase {
hPlay = CardLists.filterControlledBy(hPlay, pl);
}
final List<GameObject> objects = Lists.newArrayList(sa.getTargets());
final List<GameObject> objects = Lists.newArrayList(sa.getTargets().getTargets());
if (sa.hasParam("TargetUnique")) {
objects.addAll(sa.getUniqueTargets());
}
for (final Object o : objects) {
if (o instanceof Card) {
final Card c = (Card) o;
if (hPlay.contains(c)) {
hPlay.remove(c);
}
}
}
hPlay = CardLists.getTargetableCards(hPlay, sa);
return hPlay;
}
@@ -564,11 +530,11 @@ public class DamageDealAi extends DamageAiBase {
final boolean noPrevention = sa.hasParam("NoPrevention");
final Game game = source.getGame();
final PhaseHandler phase = game.getPhaseHandler();
final boolean divided = sa.isDividedAsYouChoose();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
final String logic = sa.getParamOrDefault("AILogic", "");
Player enemy = ai.getWeakestOpponent();
Player enemy = ComputerUtil.getOpponentFor(ai);
if ("PowerDmg".equals(logic)) {
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
@@ -577,13 +543,6 @@ public class DamageDealAi extends DamageAiBase {
sa.getTargets().add(enemy);
}
return true;
} else if ("DamageAfterPutCounter".equals(logic)
&& sa.getParent() != null
&& "P1P1".equals(sa.getParent().getParam("CounterType"))) {
// assuming the SA parent is of PutCounter type. Perhaps it's possible to predict counter multipliers here somehow?
final String amountStr = sa.getParent().getParamOrDefault("CounterNum", "1");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
dmg += amount;
}
// AssumeAtLeastOneTarget is used for cards with funky targeting implementation like Fight with Fire which would
@@ -594,10 +553,7 @@ public class DamageDealAi extends DamageAiBase {
immediately |= ComputerUtil.playImmediately(ai, sa);
if (!(sa.getParent() != null && sa.getParent().isTargetNumberValid())) {
sa.resetTargets();
}
// target loop
TargetChoices tcs = sa.getTargets();
@@ -630,7 +586,7 @@ public class DamageDealAi extends DamageAiBase {
if (assignedDamage <= dmg
&& humanCreature.getShieldCount() == 0 && !ComputerUtil.canRegenerate(humanCreature.getController(), humanCreature)) {
tcs.add(humanCreature);
sa.addDividedAllocation(humanCreature, assignedDamage);
tgt.addDividedAllocation(humanCreature, assignedDamage);
lastTgt = humanCreature;
dmg -= assignedDamage;
}
@@ -642,7 +598,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
if (dmg > 0 && lastTgt != null) {
sa.addDividedAllocation(lastTgt, sa.getDividedValue(lastTgt) + dmg);
tgt.addDividedAllocation(lastTgt, tgt.getDividedValue(lastTgt) + dmg);
dmg = 0;
return true;
}
@@ -652,20 +608,20 @@ public class DamageDealAi extends DamageAiBase {
continue;
}
tcs.add(humanCreature);
sa.addDividedAllocation(humanCreature, dmg);
tgt.addDividedAllocation(humanCreature, dmg);
dmg = 0;
return true;
}
}
int totalTargetedSoFar = -1;
while (sa.canAddMoreTarget()) {
if (totalTargetedSoFar == tcs.size()) {
while (tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
if (totalTargetedSoFar == tcs.getNumTargeted()) {
// Avoid looping endlessly when choosing targets for cards with variable target number and type
// like Jaya's Immolating Inferno
break;
}
totalTargetedSoFar = tcs.size();
totalTargetedSoFar = tcs.getNumTargeted();
if (oppTargetsChoice && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) {
// canPlayAI (sa activated by ai)
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
@@ -679,9 +635,10 @@ public class DamageDealAi extends DamageAiBase {
if (c != null && !this.shouldTgtP(ai, sa, dmg, noPrevention, true)) {
tcs.add(c);
if (divided) {
int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
assignedDamage = Math.min(dmg, assignedDamage);
sa.addDividedAllocation(c, assignedDamage);
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
if (assignedDamage <= dmg) {
tgt.addDividedAllocation(c, assignedDamage);
}
dmg = dmg - assignedDamage;
if (dmg <= 0) {
break;
@@ -697,13 +654,13 @@ public class DamageDealAi extends DamageAiBase {
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
tcs.add(enemy);
if (divided) {
sa.addDividedAllocation(enemy, dmg);
tgt.addDividedAllocation(enemy, dmg);
break;
}
continue;
}
if ("RoundedDown".equals(sa.getParam("DivideEvenly"))) {
dmg = dmg * sa.getTargets().size() / (sa.getTargets().size() +1);
dmg = dmg * sa.getTargets().getNumTargeted() / (sa.getTargets().getNumTargeted() +1);
}
// look for creature targets; currently also catches planeswalkers that can be killed immediately
@@ -719,7 +676,7 @@ public class DamageDealAi extends DamageAiBase {
if (divided) {
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
if (assignedDamage <= dmg) {
sa.addDividedAllocation(c, assignedDamage);
tgt.addDividedAllocation(c, assignedDamage);
}
dmg = dmg - assignedDamage;
if (dmg <= 0) {
@@ -739,7 +696,7 @@ public class DamageDealAi extends DamageAiBase {
final Cost abCost = sa.getPayCosts();
boolean freePing = immediately || abCost == null
|| sa.getTargets().size() > 0;
|| sa.getTargets().getNumTargeted() > 0;
if (!source.isSpell()) {
if (phase.is(PhaseType.END_OF_TURN) && sa.isAbility() && abCost.isReusuableResource()) {
@@ -748,7 +705,8 @@ public class DamageDealAi extends DamageAiBase {
}
if (phase.is(PhaseType.MAIN2) && sa.isAbility()) {
if (sa.isPwAbility() || source.hasSVar("EndOfTurnLeavePlay"))
if (sa.getRestrictions().isPwAbility()
|| source.hasSVar("EndOfTurnLeavePlay"))
freePing = true;
}
}
@@ -756,7 +714,7 @@ public class DamageDealAi extends DamageAiBase {
if (freePing && sa.canTarget(enemy) && (!avoidTargetP(ai, sa))) {
tcs.add(enemy);
if (divided) {
sa.addDividedAllocation(enemy, dmg);
tgt.addDividedAllocation(enemy, dmg);
break;
}
}
@@ -774,9 +732,9 @@ public class DamageDealAi extends DamageAiBase {
if (divided) {
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
if (assignedDamage <= dmg) {
sa.addDividedAllocation(c, assignedDamage);
tgt.addDividedAllocation(c, assignedDamage);
} else {
sa.addDividedAllocation(c, dmg);
tgt.addDividedAllocation(c, dmg);
}
dmg = dmg - assignedDamage;
if (dmg <= 0) {
@@ -787,35 +745,36 @@ public class DamageDealAi extends DamageAiBase {
}
} else if ("OppAtTenLife".equals(logic)) {
for (final Player p : ai.getOpponents()) {
if (sa.canTarget(p) && p.getLife() == 10 && tcs.size() < tgt.getMaxTargets(source, sa)) {
if (sa.canTarget(p) && p.getLife() == 10 && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
tcs.add(p);
}
}
}
// TODO: Improve Damage, we shouldn't just target the player just
// because we can
if (sa.canTarget(enemy) && tcs.size() < tgt.getMaxTargets(source, sa)) {
if (sa.canTarget(enemy) && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
|| ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai))
|| immediately || shouldTgtP(ai, sa, dmg, noPrevention)) &&
|| sa.getPayCosts() == null || immediately
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) &&
(!avoidTargetP(ai, sa))) {
tcs.add(enemy);
if (divided) {
sa.addDividedAllocation(enemy, dmg);
tgt.addDividedAllocation(enemy, dmg);
break;
}
continue;
}
}
// fell through all the choices, no targets left?
if (tcs.size() < tgt.getMinTargets(source, sa) || tcs.size() == 0) {
if (tcs.getNumTargeted() < tgt.getMinTargets(source, sa) || tcs.getNumTargeted() == 0) {
if (!mandatory) {
sa.resetTargets();
return false;
} else {
// If the trigger is mandatory, gotta choose my own stuff now
return this.damageChooseRequiredTargets(ai, sa, tgt, dmg);
return this.damageChooseRequiredTargets(ai, sa, tgt, dmg, mandatory);
}
} else {
// TODO is this good enough? for up to amounts?
@@ -892,21 +851,24 @@ public class DamageDealAi extends DamageAiBase {
* a {@link forge.game.spellability.TargetRestrictions} object.
* @param dmg
* a int.
* @param mandatory
* a boolean.
* @return a boolean.
*/
private boolean damageChooseRequiredTargets(final Player ai, final SpellAbility sa, final TargetRestrictions tgt, final int dmg) {
private boolean damageChooseRequiredTargets(final Player ai, final SpellAbility sa, final TargetRestrictions tgt, final int dmg,
final boolean mandatory) {
// this is for Triggered targets that are mandatory
final boolean noPrevention = sa.hasParam("NoPrevention");
final boolean divided = sa.isDividedAsYouChoose();
final Player opp = ai.getWeakestOpponent();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final Player opp = ComputerUtil.getOpponentFor(ai);
while (sa.canAddMoreTarget()) {
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (tgt.canTgtPlaneswalker()) {
final Card c = this.dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, ai, true);
final Card c = this.dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, ai, mandatory);
if (c != null) {
sa.getTargets().add(c);
if (divided) {
sa.addDividedAllocation(c, dmg);
tgt.addDividedAllocation(c, dmg);
break;
}
continue;
@@ -915,11 +877,11 @@ public class DamageDealAi extends DamageAiBase {
// TODO: This currently also catches planeswalkers that can be killed (still necessary? Or can be removed?)
if (tgt.canTgtCreature()) {
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, ai, true);
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, ai, mandatory);
if (c != null) {
sa.getTargets().add(c);
if (divided) {
sa.addDividedAllocation(c, dmg);
tgt.addDividedAllocation(c, dmg);
break;
}
continue;
@@ -929,33 +891,7 @@ public class DamageDealAi extends DamageAiBase {
if (sa.canTarget(opp)) {
if (sa.getTargets().add(opp)) {
if (divided) {
sa.addDividedAllocation(opp, dmg);
break;
}
continue;
}
}
// See if there's an indestructible target that can be used
CardCollection indestructible = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.Presets.PLANESWALKERS, CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE), CardPredicates.isTargetableBy(sa)));
if (!indestructible.isEmpty()) {
Card c = ComputerUtilCard.getWorstPermanentAI(indestructible, false, false, false, false);
sa.getTargets().add(c);
if (divided) {
sa.addDividedAllocation(c, dmg);
break;
}
continue;
}
else if (tgt.canTgtPlaneswalker()) {
// Second pass for planeswalkers: choose AI's worst planeswalker
final Card c = getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.PLANESWALKERS), CardPredicates.isTargetableBy(sa)));
if (c != null) {
sa.getTargets().add(c);
if (divided) {
sa.addDividedAllocation(c, dmg);
tgt.addDividedAllocation(opp, dmg);
break;
}
continue;
@@ -965,7 +901,7 @@ public class DamageDealAi extends DamageAiBase {
if (sa.canTarget(ai)) {
if (sa.getTargets().add(ai)) {
if (divided) {
sa.addDividedAllocation(ai, dmg);
tgt.addDividedAllocation(ai, dmg);
break;
}
continue;
@@ -993,19 +929,22 @@ public class DamageDealAi extends DamageAiBase {
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
// Set PayX here to maximum value.
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(dmg);
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
}
if (!sa.usesTargeting()) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
// If it's not mandatory check a few things
return mandatory || this.damageChooseNontargeted(ai, sa, dmg);
if (!mandatory && !this.damageChooseNontargeted(ai, sa, dmg)) {
return false;
}
} else {
if (!this.damageChoosingTargets(ai, sa, sa.getTargetRestrictions(), dmg, mandatory, true) && !mandatory) {
if (!this.damageChoosingTargets(ai, sa, tgt, dmg, mandatory, true) && !mandatory) {
return false;
}
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) {
if (damage.equals("X") && source.getSVar(damage).equals("Count$xPaid") && !sa.hasParam("DividedAsYouChoose")) {
// If I can kill my target by paying less mana, do it
int actualPay = 0;
final boolean noPrevention = sa.hasParam("NoPrevention");
@@ -1021,7 +960,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
sa.setXManaCostPaid(actualPay);
source.setSVar("PayX", Integer.toString(actualPay));
}
}
@@ -1081,7 +1020,7 @@ public class DamageDealAi extends DamageAiBase {
saTgt.resetTargets();
saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent);
sa.setXManaCostPaid(dmg);
source.setSVar("PayX", Integer.toString(dmg));
return true;
}
@@ -1128,7 +1067,8 @@ public class DamageDealAi extends DamageAiBase {
continue;
}
// currently works only with cards that don't have additional costs (only mana is supported)
if (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class)) {
if (ab.getPayCosts() != null
&& (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class))) {
String dmgDef = "0";
if (ab.getApi() == ApiType.DealDamage) {
dmgDef = ab.getParamOrDefault("NumDmg", "0");
@@ -1140,7 +1080,7 @@ public class DamageDealAi extends DamageAiBase {
continue; // not a toughness debuff
}
}
if (StringUtils.isNumeric(dmgDef)) { // currently doesn't work for X and other dependent costs
if (StringUtils.isNumeric(dmgDef) && ab.canPlay()) { // currently doesn't work for X and other dependent costs
if (sa.usesTargeting() && ab.usesTargeting()) {
// Ensure that the chained spell can target at least the same things (or more) as the current one
TargetRestrictions tgtSa = sa.getTargetRestrictions();
@@ -1152,7 +1092,7 @@ public class DamageDealAi extends DamageAiBase {
}
// FIXME: should it also check restrictions for targeting players?
ManaCost costSa = sa.getPayCosts().getTotalMana();
ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST;
ManaCost costAb = ab.getPayCosts().getTotalMana(); // checked for null above
ManaCost total = ManaCost.combine(costSa, costAb);
SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false));

View File

@@ -48,7 +48,7 @@ public class DamageEachAi extends DamageAiBase {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
return canPlayAI(ai, sa);
}
}

View File

@@ -1,12 +1,6 @@
package forge.ai.ability;
import java.util.ArrayList;
import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
@@ -25,6 +19,9 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.ArrayList;
import java.util.List;
public class DamagePreventAi extends SpellAbilityAi {
@Override
@@ -36,7 +33,20 @@ public class DamagePreventAi extends SpellAbilityAi {
final Cost cost = sa.getPayCosts();
if (!willPayCosts(ai, sa, cost, hostCard)) {
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, hostCard)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) {
return false;
}
@@ -90,7 +100,7 @@ public class DamagePreventAi extends SpellAbilityAi {
tcs.add(ai);
chance = true;
}
final List<Card> threatenedTargets = new ArrayList<>();
final List<Card> threatenedTargets = new ArrayList<Card>();
// filter AIs battlefield by what I can target
List<Card> targetables = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, hostCard, sa);
targetables = CardLists.getTargetableCards(targetables, sa);
@@ -129,15 +139,15 @@ public class DamagePreventAi extends SpellAbilityAi {
ComputerUtilCard.sortByEvaluateCreature(combatants);
for (final Card c : combatants) {
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && tcs.size() < tgt.getMaxTargets(hostCard, sa)) {
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && tcs.getNumTargeted() < tgt.getMaxTargets(hostCard, sa)) {
tcs.add(c);
chance = true;
}
}
}
}
if (sa.usesTargeting() && sa.isDividedAsYouChoose() && !sa.getTargets().isEmpty()) {
sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa));
if (tgt != null && sa.hasParam("DividedAsYouChoose") && sa.getTargets() != null && !sa.getTargets().getTargets().isEmpty()) {
tgt.addDividedAllocation(sa.getTargets().getTargets().get(0), AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa));
}
return chance;
@@ -169,11 +179,12 @@ public class DamagePreventAi extends SpellAbilityAi {
* @return a boolean.
*/
private boolean preventDamageMandatoryTarget(final Player ai, final SpellAbility sa, final boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
sa.resetTargets();
// filter AIs battlefield by what I can target
final Game game = ai.getGame();
CardCollectionView targetables = game.getCardsIn(ZoneType.Battlefield);
targetables = CardLists.getTargetableCards(targetables, sa);
targetables = CardLists.getValidCards(targetables, tgt.getValidTgts(), ai, sa.getHostCard(), sa);
final List<Card> compTargetables = CardLists.filterControlledBy(targetables, ai);
Card target = null;
@@ -204,8 +215,8 @@ public class DamagePreventAi extends SpellAbilityAi {
target = ComputerUtilCard.getCheapestPermanentAI(targetables, sa, true);
}
sa.getTargets().add(target);
if (sa.isDividedAsYouChoose()) {
sa.addDividedAllocation(target, AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa));
if (sa.hasParam("DividedAsYouChoose")) {
tgt.addDividedAllocation(target, AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa));
}
return true;
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.cost.Cost;
@@ -20,7 +22,19 @@ public class DamagePreventAllAi extends SpellAbilityAi {
final Cost cost = sa.getPayCosts();
// temporarily disabled until better AI
if (!willPayCosts(ai, sa, cost, hostCard)) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, hostCard)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) {
return false;
}

View File

@@ -1,13 +1,10 @@
package forge.ai.ability;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
@@ -25,6 +22,10 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class DebuffAi extends SpellAbilityAi {
// *************************************************************************
// ***************************** Debuff ************************************
@@ -50,7 +51,7 @@ public class DebuffAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source)) {
return false;
}
@@ -67,7 +68,7 @@ public class DebuffAi extends SpellAbilityAi {
}
}
if (!sa.usesTargeting()) {
if (!sa.usesTargeting() || !sa.getTargetRestrictions().doesTarget()) {
List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
@@ -93,7 +94,7 @@ public class DebuffAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if (!sa.usesTargeting()) {
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
// TODO - copied from AF_Pump.pumpDrawbackAI() - what should be
// here?
} else {
@@ -124,7 +125,7 @@ public class DebuffAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
sa.resetTargets();
CardCollection list = getCurseCreatures(ai, sa, kws == null ? Lists.newArrayList() : kws);
CardCollection list = getCurseCreatures(ai, sa, kws == null ? Lists.<String>newArrayList() : kws);
list = CardLists.getValidCards(list, tgt.getValidTgts(), sa.getActivatingPlayer(), sa.getHostCard(), sa);
// several uses here:
@@ -138,12 +139,12 @@ public class DebuffAi extends SpellAbilityAi {
return mandatory && debuffMandatoryTarget(ai, sa, mandatory);
}
while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
Card t = null;
// boolean goodt = false;
if (list.isEmpty()) {
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
if ((sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().getNumTargeted() == 0)) {
if (mandatory) {
return debuffMandatoryTarget(ai, sa, mandatory);
}
@@ -176,7 +177,7 @@ public class DebuffAi extends SpellAbilityAi {
* @return a CardCollection.
*/
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
if (!list.isEmpty()) {
list = CardLists.filter(list, new Predicate<Card>() {
@@ -216,11 +217,11 @@ public class DebuffAi extends SpellAbilityAi {
list.remove(c);
}
final CardCollection pref = CardLists.filterControlledBy(list, ai.getWeakestOpponent());
final CardCollection pref = CardLists.filterControlledBy(list, ComputerUtil.getOpponentFor(ai));
final CardCollection forced = CardLists.filterControlledBy(list, ai);
final Card source = sa.getHostCard();
while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) {
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) {
if (pref.isEmpty()) {
break;
}
@@ -237,7 +238,7 @@ public class DebuffAi extends SpellAbilityAi {
sa.getTargets().add(c);
}
while (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (forced.isEmpty()) {
break;
}
@@ -256,7 +257,7 @@ public class DebuffAi extends SpellAbilityAi {
sa.getTargets().add(c);
}
if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
sa.resetTargets();
return false;
}
@@ -266,7 +267,7 @@ public class DebuffAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final List<String> kws = sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : new ArrayList<>();
final List<String> kws = sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : new ArrayList<String>();
if (sa.getTargetRestrictions() == null) {
if (mandatory) {

View File

@@ -1,24 +1,14 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.AiController;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.card.mana.ManaCost;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.ability.AbilityFactory;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class DelayedTriggerAi extends SpellAbilityAi {
@@ -28,14 +18,16 @@ public class DelayedTriggerAi extends SpellAbilityAi {
// TODO: improve ai
return true;
}
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return false;
SpellAbility trigsa = null;
if (sa.hasAdditionalAbility("Execute")) {
trigsa = sa.getAdditionalAbility("Execute");
} else {
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
}
trigsa.setActivatingPlayer(ai);
if (trigsa instanceof AbilitySub) {
return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
return SpellApiToAi.Converter.get(((AbilitySub) trigsa).getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
} else {
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}
@@ -43,11 +35,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return false;
SpellAbility trigsa = null;
if (sa.hasAdditionalAbility("Execute")) {
trigsa = sa.getAdditionalAbility("Execute");
} else {
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
}
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
trigsa.setActivatingPlayer(ai);
@@ -60,95 +53,11 @@ public class DelayedTriggerAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// Card-specific logic
String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("SpellCopy")) {
// fetch Instant or Sorcery and AI has reason to play this turn
// does not try to get itself
final ManaCost costSa = sa.getPayCosts().getTotalMana();
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (!(c.isInstant() || c.isSorcery()) || c.equals(sa.getHostCard())) {
return false;
}
for (SpellAbility ab : c.getSpellAbilities()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab))
|| ab.hasParam("AINoRecursiveCheck")) {
// prevent infinitely recursing mana ritual and other abilities with reentry
continue;
} else if ("SpellCopy".equals(ab.getParam("AILogic")) && ab.getApi() == ApiType.DelayedTrigger) {
// don't copy another copy spell, too complex for the AI
continue;
}
if (!ab.canPlay()) {
continue;
}
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab);
// see if we can pay both for this spell and for the Effect spell we're considering
if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) {
ManaCost costAb = ab.getPayCosts().getTotalMana();
ManaCost total = ManaCost.combine(costSa, costAb);
SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false));
// can we pay both costs?
if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) {
return true;
}
}
}
return false;
}
});
if(count == 0) {
return false;
}
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)) {
// return false;
//}
// fetch Instant or Sorcery without Rebound and AI has reason to play this turn
// only need count, not the list
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (!(c.isInstant() || c.isSorcery()) || c.hasKeyword(Keyword.REBOUND)) {
return false;
}
for (SpellAbility ab : c.getSpellAbilities()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab))
|| ab.hasParam("AINoRecursiveCheck")) {
// prevent infinitely recursing mana ritual and other abilities with reentry
continue;
}
if (!ab.canPlay()) {
continue;
}
AiPlayDecision decision = ((PlayerControllerAi) ai.getController()).getAi().canPlaySa(ab);
if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) {
if (ComputerUtilMana.canPayManaCost(ab, ai, 0)) {
return true;
}
}
}
return false;
}
});
if (count == 0) {
return false;
}
return true;
}
// Generic logic
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return false;
SpellAbility trigsa = null;
if (sa.hasAdditionalAbility("Execute")) {
trigsa = sa.getAdditionalAbility("Execute");
} else {
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
}
trigsa.setActivatingPlayer(ai);
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);

View File

@@ -1,23 +1,10 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialAiLogic;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
@@ -26,6 +13,7 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
public class DestroyAi extends SpellAbilityAi {
@@ -35,19 +23,89 @@ public class DestroyAi extends SpellAbilityAi {
}
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
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 TargetRestrictions abTgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
if (sa.usesTargeting()) {
sa.resetTargets();
if ("MadSarkhanDragon".equals(aiLogic)) {
return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa);
} else if (aiLogic.startsWith("MinLoyalty.")) {
int minLoyalty = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
if (source.getCounters(CounterEnumType.LOYALTY) < minLoyalty) {
final boolean noRegen = sa.hasParam("NoRegen");
final String logic = sa.getParam("AILogic");
boolean hasXCost = false;
CardCollection list;
if (abCost != null) {
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
} else if ("Polymorph".equals(aiLogic)) {
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
return false;
}
hasXCost = abCost.getCostMana() != null ? abCost.getCostMana().getAmountOfX() > 0 : false;
}
if ("AtOpponentsCombatOrAfter".equals(sa.getParam("AILogic"))) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
if (ph.getPlayerTurn() == ai || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
} else if ("AtEOT".equals(sa.getParam("AILogic"))) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
if (!ph.is(PhaseType.END_OF_TURN)) {
return false;
}
} else if ("AtEOTIfNotAttacking".equals(sa.getParam("AILogic"))) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
if (!ph.is(PhaseType.END_OF_TURN) || ai.getAttackedWithCreatureThisTurn()) {
return false;
}
}
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
// Ability that's intended to destroy own useless token to trigger Grave Pacts
// should be fired at end of turn or when under attack after blocking to make opponent sac something
boolean havepact = false;
// TODO replace it with look for a dies -> sacrifice trigger check
havepact |= ai.isCardInPlay("Grave Pact");
havepact |= ai.isCardInPlay("Butcher of Malakir");
havepact |= ai.isCardInPlay("Dictate of Erebos");
if ("Pactivator".equals(logic) && havepact) {
if ((!ai.getGame().getPhaseHandler().isPlayerTurn(ai))
&& ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) || (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)))
&& (ai.getOpponents().getCreaturesInPlay().size() > 0)) {
ai.getController().chooseTargetsFor(sa);
return true;
}
}
// Targeting
if (abTgt != null) {
sa.resetTargets();
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
}
if ("MadSarkhanDragon".equals(logic)) {
return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa);
} else if (logic != null && logic.startsWith("MinLoyalty.")) {
int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
if (source.getCounters(CounterType.LOYALTY) < minLoyalty) {
return false;
}
} else if ("Polymorph".equals(logic)) {
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return false;
}
@@ -66,103 +124,7 @@ public class DestroyAi extends SpellAbilityAi {
}
sa.getTargets().add(worst);
return true;
} else if ("Pongify".equals(aiLogic)) {
return SpecialAiLogic.doPongifyLogic(ai, sa);
}
}
return super.checkAiLogic(ai, sa, aiLogic);
}
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
final String logic) {
if ("AtOpponentsCombatOrAfter".equals(logic)) {
if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
}
} else if ("AtEOT".equals(logic)) {
if (!ph.is(PhaseType.END_OF_TURN)) {
return false;
}
} else if ("AtEOTIfNotAttacking".equals(logic)) {
if (!ph.is(PhaseType.END_OF_TURN) || !ai.getCreaturesAttackedThisTurn().isEmpty()) {
return false;
}
} else if ("Pactivator".equals(logic)) {
// Ability that's intended to destroy own useless token to trigger Grave Pacts
// should be fired at end of turn or when under attack after blocking to make opponent sac something
boolean havepact = false;
// TODO replace it with look for a dies -> sacrifice trigger check
havepact |= ai.isCardInPlay("Grave Pact");
havepact |= ai.isCardInPlay("Butcher of Malakir");
havepact |= ai.isCardInPlay("Dictate of Erebos");
if (havepact) {
if ((!ph.isPlayerTurn(ai))
&& ((ph.is(PhaseType.END_OF_TURN)) || (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)))
&& (ai.getOpponents().getCreaturesInPlay().size() > 0)) {
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
Card worst = ComputerUtilCard.getWorstAI(list);
if (worst != null) {
sa.getTargets().add(worst);
return true;
}
return false;
}
}
}
return true;
}
@Override
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()) {
// Assume there where already enough targets chosen by AI Logic Above
if (!sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
return true;
}
// reset targets before AI Logic part
sa.resetTargets();
int maxTargets;
if (sa.costHasManaX()) {
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai);
// need to set XPaid to get the right number for
sa.setXManaCostPaid(maxTargets);
// need to check for maxTargets
maxTargets = Math.min(maxTargets, sa.getMaxTargets());
} else {
maxTargets = sa.getMaxTargets();
}
if (maxTargets == 0) {
// can't afford X or otherwise target anything
return false;
}
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
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;
@@ -170,7 +132,7 @@ public class DestroyAi extends SpellAbilityAi {
}
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
if (CardLists.getNotType(list, "Creature").isEmpty()) {
@@ -199,7 +161,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
//Check for undying
return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterEnumType.P1P1) > 0);
return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterType.P1P1) > 0);
}
});
}
@@ -222,12 +184,33 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
int maxTargets = abTgt.getMaxTargets(sa.getHostCard(), sa);
if (hasXCost) {
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa));
// X can't be more than the lands we have in our hand for "discard X lands"!
if ("ScorchedEarth".equals(logic)) {
int lands = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
maxTargets = Math.min(maxTargets, lands);
}
}
if (sa.hasParam("AIMaxTgtsCount")) {
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified
// TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable?
maxTargets = Math.min(CardFactoryUtil.xCount(sa.getHostCard(), "Count$" + sa.getParam("AIMaxTgtsCount")), maxTargets);
}
if (maxTargets == 0) {
// can't afford X or otherwise target anything
return false;
}
// target loop
// TODO use can add more Targets
while (sa.getTargets().size() < maxTargets) {
while (sa.getTargets().getNumTargeted() < maxTargets) {
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if ((sa.getTargets().getNumTargeted() < abTgt.getMinTargets(sa.getHostCard(), sa))
|| (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -246,6 +229,19 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
}
if ("Pongify".equals(logic)) {
final Card token = TokenAi.spawnToken(choice.getController(), sa.getSubAbility());
if (token == null) {
return true; // becomes Terminate
} else {
if (source.getGame().getPhaseHandler().getPhase()
.isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) || // prevent surprise combatant
ComputerUtilCard.evaluateCreature(choice) < 1.5
* ComputerUtilCard.evaluateCreature(token)) {
return false;
}
}
}
} else if (CardLists.getNotType(list, "Land").isEmpty()) {
choice = ComputerUtilCard.getBestLandAI(list);
@@ -259,14 +255,15 @@ public class DestroyAi extends SpellAbilityAi {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
}
//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)) {
if (!sa.isTrigger() && abTgt.getMaxTargets(sa.getHostCard(), sa) == 1) {
if (!ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
return false;
}
}
if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if ((sa.getTargets().getNumTargeted() < abTgt.getMinTargets(sa.getHostCard(), sa))
|| (sa.getTargets().getNumTargeted() == 0)) {
sa.resetTargets();
return false;
} else {
@@ -280,7 +277,6 @@ public class DestroyAi extends SpellAbilityAi {
SpellAbility sp = aura.getFirstSpellAbility();
if (sp != null && "GainControl".equals(sp.getParam("AILogic"))
&& aura.getController() != ai && sa.canTarget(aura)) {
list.remove(choice);
choice = aura;
}
}
@@ -292,7 +288,7 @@ public class DestroyAi extends SpellAbilityAi {
} else if (sa.hasParam("Defined")) {
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai)
|| ai.getCreaturesInPlay().size() < ai.getWeakestOpponent().getCreaturesInPlay().size()
|| ai.getCreaturesInPlay().size() < ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size()
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|| ai.getLife() <= 5)) {
// Basic ai logic for Lethal Vapors
@@ -310,19 +306,22 @@ public class DestroyAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final boolean noRegen = sa.hasParam("NoRegen");
if (sa.usesTargeting()) {
if (tgt != null) {
sa.resetTargets();
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty() || list.size() < sa.getMinTargets()) {
return false;
}
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
// Try to avoid targeting creatures that are dead on board
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
if (list.isEmpty() || list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
return false;
}
CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
@@ -343,7 +342,7 @@ public class DestroyAi extends SpellAbilityAi {
}
// Filter AI-specific targets if provided
preferred = ComputerUtil.filterAITgts(sa, ai, preferred, true);
preferred = ComputerUtil.filterAITgts(sa, ai, (CardCollection)preferred, true);
for (final Card c : preferred) {
list.remove(c);
@@ -353,9 +352,10 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
while (sa.canAddMoreTarget()) {
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
if (preferred.isEmpty()) {
if (!sa.isMinTargetChosen()) {
if (sa.getTargets().getNumTargeted() == 0
|| sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (!mandatory) {
sa.resetTargets();
return false;
@@ -379,7 +379,7 @@ public class DestroyAi extends SpellAbilityAi {
}
}
while (sa.canAddMoreTarget()) {
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (list.isEmpty()) {
break;
} else {
@@ -400,11 +400,16 @@ public class DestroyAi extends SpellAbilityAi {
}
}
return sa.isTargetNumberValid();
if (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
return false;
}
} else {
return mandatory;
if (!mandatory) {
return false;
}
}
return true;
}
public boolean doLandForLandRemovalLogic(SpellAbility sa, Player ai, Card tgtLand, String logic) {

View File

@@ -1,26 +1,18 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.ai.AiBlockController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.card.MagicColor;
import forge.ai.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.Combat;
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.game.combat.Combat;
public class DestroyAllAi extends SpellAbilityAi {
@@ -45,7 +37,8 @@ public class DestroyAllAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return doMassRemovalLogic(aiPlayer, sa);
//TODO: Check for bad outcome
return true;
}
@Override
@@ -73,7 +66,7 @@ public class DestroyAllAi extends SpellAbilityAi {
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
Player opponent = ai.getWeakestOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()?
Player opponent = ComputerUtil.getOpponentFor(ai); // TODO: how should this AI logic work for multiplayer and getOpponents()?
final int CREATURE_EVAL_THRESHOLD = 200;
@@ -86,10 +79,10 @@ public class DestroyAllAi extends SpellAbilityAi {
valid = sa.getParam("ValidCards");
}
if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
if (valid.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(xPay);
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
valid = valid.replace("X", Integer.toString(xPay));
}
@@ -114,14 +107,6 @@ public class DestroyAllAi extends SpellAbilityAi {
}
}
// Special handling for Raiding Party
if (logic.equals("RaidingParty")) {
int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size());
int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size());
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()))) {
@@ -186,6 +171,7 @@ public class DestroyAllAi extends SpellAbilityAi {
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
return false;
}
return true;
}
}

View File

@@ -1,25 +1,13 @@
package forge.ai.ability;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
@@ -32,14 +20,10 @@ public class DigAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
Player opp = ai.getWeakestOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
final Card host = sa.getHostCard();
Player libraryOwner = ai;
if (!willPayCosts(ai, sa, sa.getPayCosts(), host)) {
return false;
}
if (sa.usesTargeting()) {
sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) {
@@ -58,9 +42,7 @@ public class DigAi extends SpellAbilityAi {
if ("Never".equals(sa.getParam("AILogic"))) {
return false;
} else if ("AtOppEndOfTurn".equals(sa.getParam("AILogic"))) {
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
return false;
}
return game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN);
}
// don't deck yourself
@@ -78,11 +60,9 @@ public class DigAi extends SpellAbilityAi {
}
final String num = sa.getParam("DigNum");
final boolean payXLogic = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("PayX");
if (num != null && (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) || payXLogic) {
if (num != null && num.equals("X") && host.getSVar(num).equals("Count$xPaid")) {
// By default, set PayX here to maximum value.
SpellAbility root = sa.getRootAbility();
if (root.getXManaCostPaid() == null) {
if (!(sa instanceof AbilitySub) || host.getSVar("PayX").equals("")) {
int manaToSave = 0;
// Special logic that asks the AI to conserve a certain amount of mana when paying X
@@ -90,11 +70,11 @@ public class DigAi extends SpellAbilityAi {
manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]);
}
int numCards = ComputerUtilCost.getMaxXValue(sa, ai) - manaToSave;
int numCards = ComputerUtilMana.determineLeftoverMana(sa, ai) - manaToSave;
if (numCards <= 0) {
return false;
}
root.setXManaCostPaid(numCards);
host.setSVar("PayX", Integer.toString(numCards));
}
}
@@ -119,8 +99,7 @@ public class DigAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.usesTargeting()) {
sa.resetTargets();
if (mandatory && sa.canTarget(opp)) {
@@ -133,37 +112,18 @@ public class DigAi extends SpellAbilityAi {
// Triggers that ask to pay {X} (e.g. Depala, Pilot Exemplar).
if (sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("PayXButSaveMana")) {
int manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]);
int numCards = ComputerUtilCost.getMaxXValue(sa, ai) - manaToSave;
int numCards = ComputerUtilMana.determineLeftoverMana(sa, ai) - manaToSave;
if (numCards <= 0) {
return mandatory;
}
root.setXManaCostPaid(numCards);
sa.getHostCard().setSVar("PayX", Integer.toString(numCards));
}
return true;
}
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> valid, boolean isOptional, Player relatedPlayer, Map<String, Object> params) {
if ("DigForCreature".equals(sa.getParam("AILogic"))) {
Card bestChoice = ComputerUtilCard.getBestCreatureAI(valid);
if (bestChoice == null) {
// no creatures, but maybe there's a morphable card that can be played as a creature?
CardCollection morphs = CardLists.filter(valid, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return card.hasKeyword(Keyword.MORPH);
}
});
if (!morphs.isEmpty()) {
bestChoice = ComputerUtilCard.getBestAI(morphs);
}
}
// still nothing, so return the worst card since it'll be unplayable from exile (e.g. Vivien, Champion of the Wilds)
return bestChoice != null ? bestChoice : ComputerUtilCard.getWorstAI(valid);
}
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> valid, boolean isOptional, Player relatedPlayer) {
if (sa.getActivatingPlayer().isOpponentOf(ai) && relatedPlayer.isOpponentOf(ai)) {
return ComputerUtilCard.getWorstPermanentAI(valid, false, true, false, false);
} else {
@@ -171,15 +131,6 @@ public class DigAi extends SpellAbilityAi {
}
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSinglePlayer(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List)
*/
@Override
public Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options, Map<String, Object> params) {
// an opponent choose a card from
return Iterables.getFirst(options, null);
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/

View File

@@ -1,101 +0,0 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
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 boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
Player opp = ai.getWeakestOpponent();
final Card host = sa.getHostCard();
Player libraryOwner = ai;
if (sa.usesTargeting()) {
sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) {
return false;
} else {
sa.getTargets().add(opp);
}
libraryOwner = opp;
}
// return false if nothing to dig into
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
return false;
}
if ("Never".equals(sa.getParam("AILogic"))) {
return false;
} else if ("AtOppEndOfTurn".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 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 false;
}
if (SpellAbilityAi.playReusable(ai, sa)) {
return true;
}
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
|| game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa)
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
&& !ComputerUtil.activateForCost(sa, ai)) {
return false;
}
return !ComputerUtil.preventRunAwayActivations(sa);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getWeakestOpponent();
if (sa.usesTargeting()) {
sa.resetTargets();
if (mandatory && sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else if (mandatory && sa.canTarget(ai)) {
sa.getTargets().add(ai);
}
}
return true;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
}

View File

@@ -1,8 +1,7 @@
package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
import forge.game.card.CardLists;
@@ -10,10 +9,13 @@ import forge.game.card.CardPredicates;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
public class DigUntilAi extends SpellAbilityAi {
@Override
@@ -34,7 +36,7 @@ public class DigUntilAi extends SpellAbilityAi {
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
Player libraryOwner = ai;
Player opp = ai.getWeakestOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if ("DontMillSelf".equals(logic)) {
// A card that digs for specific things and puts everything revealed before it into graveyard
@@ -75,15 +77,14 @@ public class DigUntilAi extends SpellAbilityAi {
}
final String num = sa.getParam("Amount");
if ((num != null) && num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) {
if ((num != null) && num.equals("X") && source.getSVar(num).equals("Count$xPaid")) {
// Set PayX here to maximum value.
SpellAbility root = sa.getRootAbility();
if (root.getXManaCostPaid() == null) {
int numCards = ComputerUtilCost.getMaxXValue(sa, ai);
if (!(sa instanceof AbilitySub) || source.getSVar("PayX").equals("")) {
int numCards = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (numCards <= 0) {
return false;
}
root.setXManaCostPaid(numCards);
source.setSVar("PayX", Integer.toString(numCards));
}
}

View File

@@ -1,13 +1,6 @@
package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
@@ -22,6 +15,8 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
public class DiscardAi extends SpellAbilityAi {
@Override
@@ -32,11 +27,26 @@ public class DiscardAi extends SpellAbilityAi {
final Cost abCost = sa.getPayCosts();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// temporarily disabled until better AI
if (!willPayCosts(ai, sa, abCost, source)) {
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 (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) {
return false;
}
}
if ("Chandra, Flamecaller".equals(sourceName)) {
final int hand = ai.getCardsIn(ZoneType.Hand).size();
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
@@ -46,7 +56,7 @@ public class DiscardAi extends SpellAbilityAi {
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
}
final boolean humanHasHand = ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).size() > 0;
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
if (tgt != null) {
if (!discardTargetAI(ai, sa)) {
@@ -75,14 +85,14 @@ public class DiscardAi extends SpellAbilityAi {
}
if (sa.hasParam("NumCards")) {
if (sa.getParam("NumCards").equals("X") && sa.getSVar("X").equals("Count$xPaid")) {
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai), ai.getWeakestOpponent()
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
.getCardsIn(ZoneType.Hand).size());
if (cardsToDiscard < 1) {
return false;
}
sa.setXManaCostPaid(cardsToDiscard);
source.setSVar("PayX", Integer.toString(cardsToDiscard));
} else {
if (AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa) < 1) {
return false;
@@ -120,20 +130,12 @@ public class DiscardAi extends SpellAbilityAi {
}
}
// Don't use discard abilities before main 2 if possible
// Don't use draw abilities before main 2 if possible
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& !sa.hasParam("ActivationPhases") && !aiLogic.startsWith("AnyPhase")) {
&& !sa.hasParam("ActivationPhases")) {
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 false;
}
}
}
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return false;
@@ -148,11 +150,9 @@ public class DiscardAi extends SpellAbilityAi {
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
for (Player opp : ai.getOpponents()) {
Player opp = ComputerUtil.getOpponentFor(ai);
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
continue;
} else if (!opp.canDiscardBy(sa)) { // e.g. Tamiyo, Collector of Tales
continue;
return false;
}
if (tgt != null) {
if (sa.canTarget(opp)) {
@@ -160,7 +160,6 @@ public class DiscardAi extends SpellAbilityAi {
return true;
}
}
}
return false;
} // discardTargetAI()
@@ -170,7 +169,7 @@ public class DiscardAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
Player opp = ai.getWeakestOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if (!discardTargetAI(ai, sa)) {
if (mandatory && sa.canTarget(opp)) {
sa.getTargets().add(opp);
@@ -189,11 +188,11 @@ public class DiscardAi extends SpellAbilityAi {
}
}
}
if ("X".equals(sa.getParam("RevealNumber")) && sa.getSVar("X").equals("Count$xPaid")) {
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai), ai.getWeakestOpponent()
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
.getCardsIn(ZoneType.Hand).size());
sa.setXManaCostPaid(cardsToDiscard);
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
}
}

View File

@@ -1,7 +1,6 @@
package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -10,6 +9,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom;
import java.util.List;
public class DrainManaAi extends SpellAbilityAi {
@Override
@@ -18,7 +19,7 @@ public class DrainManaAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
if (tgt == null) {
@@ -40,7 +41,7 @@ public class DrainManaAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getWeakestOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
@@ -51,9 +52,12 @@ public class DrainManaAi extends SpellAbilityAi {
} else {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
return defined.contains(opp);
if (!defined.contains(opp)) {
return false;
}
}
return true;
} else {
sa.resetTargets();
sa.getTargets().add(opp);
@@ -78,7 +82,7 @@ public class DrainManaAi extends SpellAbilityAi {
}
} else {
sa.resetTargets();
sa.getTargets().add(ai.getWeakestOpponent());
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
}
return randomReturn;

View File

@@ -18,26 +18,15 @@
*/
package forge.ai.ability;
import forge.ai.AiCostDecision;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayLife;
import forge.game.cost.PaymentDecision;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -102,7 +91,7 @@ public class DrawAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source,sa)) {
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
AiCostDecision aiDecisions = new AiCostDecision(ai, sa);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard) {
@@ -118,7 +107,7 @@ public class DrawAi extends SpellAbilityAi {
}
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source)) {
return false;
}
@@ -190,7 +179,8 @@ public class DrawAi extends SpellAbilityAi {
int numHand = ai.getCardsIn(ZoneType.Hand).size();
if ("Jace, Vryn's Prodigy".equals(sourceName) && ai.getCardsIn(ZoneType.Graveyard).size() > 3) {
return !ai.isCardInPlay("Jace, Telepath Unbound");
return CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANESWALKERS,
CardPredicates.isType("Jace")).size() <= 0;
}
if (source.isSpell() && ai.getCardsIn(ZoneType.Hand).contains(source)) {
numHand--; // remember to count looter card if it is a spell in hand
@@ -221,19 +211,14 @@ public class DrawAi extends SpellAbilityAi {
private boolean targetAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
final Card source = sa.getHostCard();
final boolean drawback = sa.getParent() != null;
final Game game = ai.getGame();
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean considerPrimary = logic.equals("ConsiderPrimary");
final boolean drawback = (sa.getParent() != null) && !considerPrimary;
boolean assumeSafeX = false; // if true, the AI will assume that the X value has been set to a value that is safe to draw
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
final int computerMaxHandSize = ai.getMaxHandSize();
final SpellAbility root = sa.getRootAbility();
final SpellAbility gainLife = sa.findSubAbilityByType(ApiType.GainLife);
final SpellAbility loseLife = sa.findSubAbilityByType(ApiType.LoseLife);
final SpellAbility getPoison = sa.findSubAbilityByType(ApiType.Poison);
@@ -250,33 +235,47 @@ public class DrawAi extends SpellAbilityAi {
boolean xPaid = false;
final String num = sa.getParam("NumCards");
if (num != null && num.equals("X")) {
if (sa.getSVar(num).equals("Count$xPaid")) {
if (source.getSVar(num).equals("Count$xPaid")) {
// Set PayX here to maximum value.
if (drawback && root.getXManaCostPaid() != null) {
numCards = root.getXManaCostPaid();
if (drawback && !source.getSVar("PayX").equals("")) {
numCards = Integer.parseInt(source.getSVar("PayX"));
} else {
numCards = ComputerUtilCost.getMaxXValue(sa, ai);
// try not to overdraw
int safeDraw = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent
numCards = Math.min(numCards, safeDraw);
numCards = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(numCards));
}
xPaid = true;
}
if (sa.getSVar(num).equals("Count$Converge")) {
numCards = ComputerUtilMana.getConvergeCount(sa, ai);
}
}
// assuming CostPayLife is the one with X
if (num != null && num.equals("ChosenX")) {
if (sa.getSVar("X").equals("XChoice")) {
// Draw up to max hand size but leave at least 3 in library
numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
if (sa.getPayCosts() != null) {
if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
// [Necrologia, Pay X Life : Draw X Cards]
// Don't draw more than what's "safe" and don't risk a near death experience
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
while ((ComputerUtil.aiLifeInDanger(ai, aggroAI, numCards) && (numCards > 0))) {
// Maybe would be better to check for "serious danger" and take more risk?
while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
numCards--;
}
} else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
// [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards]
// TODO: Add special logic to limit/otherwise modify the ChosenX value here
// Skip this ability if nothing is to be chosen for sacrifice
if (numCards <= 0) {
return false;
}
}
}
root.setXManaCostPaid(numCards);
assumeSafeX = true;
}
xPaid = true;
} else if (sa.getSVar(num).equals("Count$Converge")) {
numCards = ComputerUtilMana.getConvergeCount(sa, ai);
sa.setSVar("ChosenX", Integer.toString(numCards));
source.setSVar("ChosenX", Integer.toString(numCards));
}
}
@@ -335,30 +334,16 @@ public class DrawAi extends SpellAbilityAi {
// for drawing and losing life
if (numCards >= oppA.getLife()) {
if (xPaid) {
root.setXManaCostPaid(oppA.getLife());
source.setSVar("PayX", Integer.toString(oppA.getLife()));
}
sa.getTargets().add(oppA);
return true;
}
}
}
// that opponent can gain life and also lose life and that life gain is negative
if (gainLife != null && oppA.canGainLife() && oppA.canLoseLife() && ComputerUtil.lifegainNegative(oppA, source)) {
if (gainLife.hasParam("Defined") && "Targeted".equals(gainLife.getParam("Defined"))) {
if (numCards >= oppA.getLife()) {
if (xPaid) {
root.setXManaCostPaid(oppA.getLife());
}
sa.getTargets().add(oppA);
return true;
}
}
}
// try to make opponent lose to poison
// currently only Caress of Phyrexia
if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (getPoison != null && oppA.canReceiveCounters(CounterType.POISON)) {
if (oppA.getPoisonCounters() + numCards > 9) {
sa.getTargets().add(oppA);
return true;
@@ -402,14 +387,14 @@ public class DrawAi extends SpellAbilityAi {
}
}
if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (getPoison != null && ai.canReceiveCounters(CounterType.POISON)) {
if (numCards + ai.getPoisonCounters() >= 8) {
aiTarget = false;
}
}
if (xPaid) {
root.setXManaCostPaid(numCards);
source.setSVar("PayX", Integer.toString(numCards));
}
}
@@ -417,10 +402,10 @@ public class DrawAi extends SpellAbilityAi {
if (computerHandSize + numCards > computerMaxHandSize && game.getPhaseHandler().isPlayerTurn(ai)) {
if (xPaid) {
numCards = computerMaxHandSize - computerHandSize;
if (sa.getHostCard().isInZone(ZoneType.Hand)) {
if (sa.getHostCard().getZone().is(ZoneType.Hand)) {
numCards++; // the card will be spent
}
root.setXManaCostPaid(numCards);
source.setSVar("PayX", Integer.toString(numCards));
} else {
// Don't draw too many cards and then risk discarding
// cards at EOT
@@ -461,7 +446,7 @@ public class DrawAi extends SpellAbilityAi {
}
// ally would lose because of poison
if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (getPoison != null && ally.canReceiveCounters(CounterType.POISON)) {
if (ally.getPoisonCounters() + numCards > 9) {
continue;
}
@@ -507,8 +492,7 @@ public class DrawAi extends SpellAbilityAi {
if ((computerHandSize + numCards > computerMaxHandSize)
&& game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX) {
&& !sa.isTrigger()) {
// Don't draw too many cards and then risk discarding cards at
// EOT
if (!drawback) {
@@ -522,10 +506,6 @@ public class DrawAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!mandatory && !willPayCosts(ai, sa, sa.getPayCosts(), sa.getHostCard())) {
return false;
}
return targetAI(ai, sa, mandatory);
}

View File

@@ -6,25 +6,15 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.AiCardMemory;
import forge.ai.AiController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.ai.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.Combat;
import forge.game.card.*;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -120,10 +110,89 @@ public class EffectAi extends SpellAbilityAi {
return false;
}
randomReturn = true;
} else if (logic.equals("WillCastCreature") && ai.isAI()) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
SpellAbility saCreature = aic.predictSpellToCastInMain2(ApiType.PermanentCreature);
randomReturn = saCreature != null && ComputerUtilMana.canPayManaCost(saCreature, ai, 0);
} else if (logic.equals("SpellCopy")) {
// fetch Instant or Sorcery and AI has reason to play this turn
// does not try to get itself
final ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST;
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (!(c.isInstant() || c.isSorcery()) || c.equals(sa.getHostCard())) {
return false;
}
for (SpellAbility ab : c.getSpellAbilities()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab))
|| ab.hasParam("AINoRecursiveCheck")) {
// prevent infinitely recursing mana ritual and other abilities with reentry
continue;
} else if ("SpellCopy".equals(ab.getParam("AILogic")) && ab.getApi() == ApiType.Effect) {
// don't copy another copy spell, too complex for the AI
continue;
}
if (!ab.canPlay()) {
continue;
}
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab);
// see if we can pay both for this spell and for the Effect spell we're considering
if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) {
ManaCost costAb = ab.getPayCosts() != null ? ab.getPayCosts().getTotalMana() : ManaCost.NO_COST;
ManaCost total = ManaCost.combine(costSa, costAb);
SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false));
// can we pay both costs?
if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) {
return true;
}
}
}
return false;
}
});
if(count == 0) {
return false;
}
randomReturn = true;
} else if (logic.equals("NarsetRebound")) {
// should be done in Main2, but it might broke for other cards
//if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
// return false;
//}
// fetch Instant or Sorcery without Rebound and AI has reason to play this turn
// only need count, not the list
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (!(c.isInstant() || c.isSorcery()) || c.hasKeyword(Keyword.REBOUND)) {
return false;
}
for (SpellAbility ab : c.getSpellAbilities()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab))
|| ab.hasParam("AINoRecursiveCheck")) {
// prevent infinitely recursing mana ritual and other abilities with reentry
continue;
}
if (!ab.canPlay()) {
continue;
}
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab);
if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) {
if (ComputerUtilMana.canPayManaCost(ab, ai, 0)) {
return true;
}
}
}
return false;
}
});
if(count == 0) {
return false;
}
randomReturn = true;
} else if (logic.equals("Always")) {
randomReturn = true;
} else if (logic.equals("Main1")) {
@@ -198,7 +267,7 @@ public class EffectAi extends SpellAbilityAi {
final SpellAbility saTop = game.getStack().peekAbility();
final Card host = saTop.getHostCard();
if (saTop.getActivatingPlayer() != ai // from opponent
&& host.canDamagePrevented(false) // no prevent damage
&& !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention) // no prevent damage
&& host != null && (host.isInstant() || host.isSorcery())
&& !host.hasKeyword("Prevent all damage that would be dealt by CARDNAME.")) { // valid target
final ApiType type = saTop.getApi();
@@ -214,7 +283,11 @@ public class EffectAi extends SpellAbilityAi {
return false;
}
final SpellAbility topStack = game.getStack().peekAbility();
return topStack.getActivatingPlayer().isOpponentOf(ai) && topStack.getApi() == ApiType.GainLife;
if (topStack.getActivatingPlayer().isOpponentOf(ai) && topStack.getApi() == ApiType.GainLife) {
return true;
} else {
return false;
}
} else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0, 0);
} else if (logic.equals("Burn")) {
@@ -228,9 +301,11 @@ public class EffectAi extends SpellAbilityAi {
return false;
}
if (logic.contains(":")) {
String[] k = logic.split(":");
String k[] = logic.split(":");
Integer i = Integer.valueOf(k[1]);
return ai.getCreaturesInPlay().size() >= i;
if (ai.getCreaturesInPlay().size() < i) {
return false;
}
}
return true;
} else if (logic.equals("CastFromGraveThisTurn")) {
@@ -239,16 +314,6 @@ public class EffectAi extends SpellAbilityAi {
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
return false;
}
} else if (logic.equals("Bribe")) {
Card host = sa.getHostCard();
Combat combat = game.getCombat();
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
&& game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !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 false;
}
} else { //no AILogic
return false;

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