mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-16 02:38:02 +00:00
Merge branch 'master' into 'oracle-updates-nonfunctional'
# Conflicts: # forge-gui/res/cardsfolder/a/artisan_of_forms.txt # forge-gui/res/cardsfolder/c/cemetery_puca.txt # forge-gui/res/cardsfolder/d/dimir_doppelganger.txt # forge-gui/res/cardsfolder/e/evil_twin.txt # forge-gui/res/cardsfolder/h/heat_shimmer.txt # forge-gui/res/cardsfolder/i/increasing_devotion.txt # forge-gui/res/cardsfolder/i/increasing_vengeance.txt # forge-gui/res/cardsfolder/k/kiki_jiki_mirror_breaker.txt # forge-gui/res/cardsfolder/l/lazav_dimir_mastermind.txt # forge-gui/res/cardsfolder/m/minion_reflector.txt # forge-gui/res/cardsfolder/m/mizzium_transreliquat.txt # forge-gui/res/cardsfolder/s/sakashima_the_impostor.txt # forge-gui/res/cardsfolder/s/splinter_twin.txt # forge-gui/res/cardsfolder/t/tempt_with_vengeance.txt # forge-gui/res/cardsfolder/t/thespians_stage.txt # forge-gui/res/cardsfolder/t/twinflame.txt # forge-gui/res/cardsfolder/u/unstable_shapeshifter.txt # forge-gui/res/cardsfolder/v/vesuvan_doppelganger.txt # forge-gui/res/cardsfolder/v/vesuvan_shapeshifter.txt
This commit is contained in:
@@ -1,7 +0,0 @@
|
|||||||
<?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>
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -231,3 +231,8 @@ forge-gui/tools/oracleScript.log
|
|||||||
/release.properties
|
/release.properties
|
||||||
/target
|
/target
|
||||||
/test-output
|
/test-output
|
||||||
|
.settings
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.vscode/settings.json
|
||||||
|
.vscode/launch.json
|
||||||
|
|||||||
33
.gitlab/issue_templates/Bug.md
Normal file
33
.gitlab/issue_templates/Bug.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
Summary
|
||||||
|
|
||||||
|
(Summarize the bug encountered concisely)
|
||||||
|
|
||||||
|
|
||||||
|
Steps to reproduce
|
||||||
|
|
||||||
|
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
|
||||||
|
|
||||||
|
|
||||||
|
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
|
||||||
|
|
||||||
|
|
||||||
|
What is the current bug behavior?
|
||||||
|
|
||||||
|
(What actually happens)
|
||||||
|
|
||||||
|
|
||||||
|
What is the expected correct behavior?
|
||||||
|
|
||||||
|
(What you should see instead)
|
||||||
|
|
||||||
|
|
||||||
|
Relevant logs and/or screenshots
|
||||||
|
|
||||||
|
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
|
||||||
|
|
||||||
|
|
||||||
|
Possible fixes
|
||||||
|
|
||||||
|
(If you can, link to the line of code that might be responsible for the problem)
|
||||||
|
|
||||||
|
/label ~needs-investigation
|
||||||
15
.gitlab/issue_templates/Feature.md
Normal file
15
.gitlab/issue_templates/Feature.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Summary
|
||||||
|
|
||||||
|
(Summarize the feature you wish concisely)
|
||||||
|
|
||||||
|
|
||||||
|
Example screenshots
|
||||||
|
|
||||||
|
(If this is a UI change, please provide an example screenshot of how this feature might work)
|
||||||
|
|
||||||
|
|
||||||
|
Feature type
|
||||||
|
|
||||||
|
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
|
||||||
|
|
||||||
|
/label ~feature request
|
||||||
12
.project
12
.project
@@ -1,12 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
eclipse.preferences.version=1
|
|
||||||
encoding/<project>=UTF-8
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
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
@@ -1,4 +0,0 @@
|
|||||||
activeProfiles=
|
|
||||||
eclipse.preferences.version=1
|
|
||||||
resolveWorkspaceProjects=true
|
|
||||||
version=1
|
|
||||||
230
README.md
Normal file
230
README.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
eclipse.preferences.version=1
|
|
||||||
encoding//src/main/java=ISO-8859-1
|
|
||||||
encoding/<project>=UTF-8
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
activeProfiles=
|
|
||||||
eclipse.preferences.version=1
|
|
||||||
resolveWorkspaceProjects=true
|
|
||||||
version=1
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>forge</artifactId>
|
<artifactId>forge</artifactId>
|
||||||
<groupId>forge</groupId>
|
<groupId>forge</groupId>
|
||||||
<version>1.6.16-SNAPSHOT</version>
|
<version>1.6.27-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>forge-ai</artifactId>
|
<artifactId>forge-ai</artifactId>
|
||||||
@@ -30,30 +30,4 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
|
||||||
<version>3.0.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>checkstyle-validation</id>
|
|
||||||
<phase>validate</phase>
|
|
||||||
<configuration>
|
|
||||||
<configLocation>../checkstyle.xml</configLocation>
|
|
||||||
<includeTestSourceDirectory>true</includeTestSourceDirectory>
|
|
||||||
<encoding>UTF-8</encoding>
|
|
||||||
<consoleOutput>true</consoleOutput>
|
|
||||||
<failsOnError>true</failsOnError>
|
|
||||||
<failOnViolation>true</failOnViolation>
|
|
||||||
</configuration>
|
|
||||||
<goals>
|
|
||||||
<goal>check</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -191,14 +191,48 @@ public class AiAttackController {
|
|||||||
return false;
|
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.getOverridingAbility();
|
||||||
|
if (sa == null && t.hasParam("Execute")) {
|
||||||
|
sa = AbilityFactory.getAbility(c, t.getParam("Execute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sa != null && 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"))) {
|
if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Player opp = this.defendingOpponent;
|
final Player opp = this.defendingOpponent;
|
||||||
if (ComputerUtilCombat.damageIfUnblocked(attacker, opp, combat, true) > 0) {
|
|
||||||
|
// 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 */) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Poison opponent if unblocked
|
||||||
if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) {
|
if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -213,7 +247,7 @@ public class AiAttackController {
|
|||||||
final CardCollectionView controlledByCompy = ai.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES);
|
final CardCollectionView controlledByCompy = ai.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES);
|
||||||
for (final Card c : controlledByCompy) {
|
for (final Card c : controlledByCompy) {
|
||||||
for (final Trigger trigger : c.getTriggers()) {
|
for (final Trigger trigger : c.getTriggers()) {
|
||||||
if (ComputerUtilCombat.combatTriggerWillTrigger(attacker, null, trigger, combat)) {
|
if (ComputerUtilCombat.combatTriggerWillTrigger(attacker, null, trigger, combat, this.attackers)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1119,8 +1153,21 @@ public class AiAttackController {
|
|||||||
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
|
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
|
||||||
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|
||||||
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
|| "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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 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
|
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
|
||||||
int defPower = CardLists.getTotalPower(defenders, true);
|
int defPower = CardLists.getTotalPower(validBlockers, true);
|
||||||
|
|
||||||
if (!hasCombatEffect) {
|
if (!hasCombatEffect) {
|
||||||
for (KeywordInterface inst : attacker.getKeywords()) {
|
for (KeywordInterface inst : attacker.getKeywords()) {
|
||||||
@@ -1137,10 +1184,9 @@ public class AiAttackController {
|
|||||||
// number of factors about the attacking
|
// number of factors about the attacking
|
||||||
// context that will be relevant to the attackers decision according to
|
// context that will be relevant to the attackers decision according to
|
||||||
// the selected strategy
|
// the selected strategy
|
||||||
for (final Card defender : defenders) {
|
for (final Card defender : validBlockers) {
|
||||||
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
|
// 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;
|
numberOfPossibleBlockers += 1;
|
||||||
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
|
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
|
||||||
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) {
|
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) {
|
||||||
@@ -1178,7 +1224,7 @@ public class AiAttackController {
|
|||||||
// - our creature will die for sure (chump attack)
|
// - our creature will die for sure (chump attack)
|
||||||
// - our attack will not do anything special (no attack/combat effect to proc)
|
// - 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
|
// - 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;
|
canKillAllDangerous = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1227,8 +1273,9 @@ public class AiAttackController {
|
|||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = all out attacking");
|
System.out.println(attacker.getName() + " = all out attacking");
|
||||||
return true;
|
return true;
|
||||||
case 4: // expecting to at least trade with something
|
case 4: // expecting to at least trade with something, or can attack "for free", expecting no counterattack
|
||||||
if (canKillAll || (canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked) {
|
if (canKillAll || (dangerousBlockersPresent && canKillAllDangerous && !canBeKilledByOne) || !canBeBlocked
|
||||||
|
|| (defPower == 0 && !ComputerUtilCombat.lifeInDanger(ai, combat))) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
|
System.out.println(attacker.getName() + " = attacking expecting to at least trade with something");
|
||||||
return true;
|
return true;
|
||||||
@@ -1236,7 +1283,7 @@ public class AiAttackController {
|
|||||||
break;
|
break;
|
||||||
case 3: // expecting to at least kill a creature of equal value or not be blocked
|
case 3: // expecting to at least kill a creature of equal value or not be blocked
|
||||||
if ((canKillAll && isWorthLessThanAllKillers)
|
if ((canKillAll && isWorthLessThanAllKillers)
|
||||||
|| ((canKillAllDangerous || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|
|| (((dangerousBlockersPresent && canKillAllDangerous) || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne)
|
||||||
|| !canBeBlocked) {
|
|| !canBeBlocked) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
|
System.out.println(attacker.getName() + " = attacking expecting to kill creature or cause damage, or is unblockable");
|
||||||
@@ -1245,7 +1292,7 @@ public class AiAttackController {
|
|||||||
break;
|
break;
|
||||||
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
|
case 2: // attack expecting to attract a group block or destroying a single blocker and surviving
|
||||||
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
|
if (!canBeBlocked || ((canKillAll || hasAttackEffect || hasCombatEffect) && !canBeKilledByOne &&
|
||||||
(canKillAllDangerous || !canBeKilled))) {
|
((dangerousBlockersPresent && canKillAllDangerous) || !canBeKilled))) {
|
||||||
if (LOG_AI_ATTACKS)
|
if (LOG_AI_ATTACKS)
|
||||||
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
|
System.out.println(attacker.getName() + " = attacking expecting to survive or attract group block");
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.google.common.base.Predicates;
|
|||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.game.CardTraitBase;
|
import forge.game.CardTraitBase;
|
||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
@@ -771,6 +772,16 @@ public class AiBlockController {
|
|||||||
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
|
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
|
||||||
blockers.removeAll(combat.getBlockers(attacker));
|
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
|
// Try to use safe blockers first
|
||||||
if (blockers.size() > 0) {
|
if (blockers.size() > 0) {
|
||||||
safeBlockers = getSafeBlockers(combat, attacker, blockers);
|
safeBlockers = getSafeBlockers(combat, attacker, blockers);
|
||||||
|
|||||||
@@ -40,16 +40,39 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public class AiCardMemory {
|
public class AiCardMemory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the memory set in which the card is remembered
|
||||||
|
* (which, in its turn, defines how the AI utilizes the information
|
||||||
|
* about remembered cards).
|
||||||
|
*/
|
||||||
|
public enum MemorySet {
|
||||||
|
MANDATORY_ATTACKERS, // These creatures must attack this turn
|
||||||
|
TRICK_ATTACKERS, // These creatures will attack to try to provoke the opponent to block them into a combat trick
|
||||||
|
HELD_MANA_SOURCES_FOR_MAIN2, // These mana sources will not be used before Main 2
|
||||||
|
HELD_MANA_SOURCES_FOR_DECLBLK, // These mana sources will not be used before Combat - Declare Blockers
|
||||||
|
HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK, // These mana sources will not be used before the opponent's Combat - Declare Blockers
|
||||||
|
HELD_MANA_SOURCES_FOR_NEXT_SPELL, // These mana sources will not be used until the next time the AI chooses a spell to cast
|
||||||
|
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
||||||
|
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||||
|
BOUNCED_THIS_TURN, // These cards were bounced this turn
|
||||||
|
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
|
||||||
|
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
|
||||||
|
MARKED_TO_AVOID_REENTRY // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
|
||||||
|
//REVEALED_CARDS // stub, not linked to AI code yet
|
||||||
|
}
|
||||||
|
|
||||||
private final Set<Card> memMandatoryAttackers;
|
private final Set<Card> memMandatoryAttackers;
|
||||||
private final Set<Card> memTrickAttackers;
|
private final Set<Card> memTrickAttackers;
|
||||||
private final Set<Card> memHeldManaSources;
|
private final Set<Card> memHeldManaSources;
|
||||||
private final Set<Card> memHeldManaSourcesForCombat;
|
private final Set<Card> memHeldManaSourcesForCombat;
|
||||||
private final Set<Card> memHeldManaSourcesForEnemyCombat;
|
private final Set<Card> memHeldManaSourcesForEnemyCombat;
|
||||||
|
private final Set<Card> memHeldManaSourcesForNextSpell;
|
||||||
private final Set<Card> memAttachedThisTurn;
|
private final Set<Card> memAttachedThisTurn;
|
||||||
private final Set<Card> memAnimatedThisTurn;
|
private final Set<Card> memAnimatedThisTurn;
|
||||||
private final Set<Card> memBouncedThisTurn;
|
private final Set<Card> memBouncedThisTurn;
|
||||||
private final Set<Card> memActivatedThisTurn;
|
private final Set<Card> memActivatedThisTurn;
|
||||||
private final Set<Card> memChosenFogEffect;
|
private final Set<Card> memChosenFogEffect;
|
||||||
|
private final Set<Card> memMarkedToAvoidReentry;
|
||||||
|
|
||||||
public AiCardMemory() {
|
public AiCardMemory() {
|
||||||
this.memMandatoryAttackers = new HashSet<>();
|
this.memMandatoryAttackers = new HashSet<>();
|
||||||
@@ -62,25 +85,8 @@ public class AiCardMemory {
|
|||||||
this.memActivatedThisTurn = new HashSet<>();
|
this.memActivatedThisTurn = new HashSet<>();
|
||||||
this.memTrickAttackers = new HashSet<>();
|
this.memTrickAttackers = new HashSet<>();
|
||||||
this.memChosenFogEffect = new HashSet<>();
|
this.memChosenFogEffect = new HashSet<>();
|
||||||
}
|
this.memMarkedToAvoidReentry = new HashSet<>();
|
||||||
|
this.memHeldManaSourcesForNextSpell = new HashSet<>();
|
||||||
/**
|
|
||||||
* Defines the memory set in which the card is remembered
|
|
||||||
* (which, in its turn, defines how the AI utilizes the information
|
|
||||||
* about remembered cards).
|
|
||||||
*/
|
|
||||||
public enum MemorySet {
|
|
||||||
MANDATORY_ATTACKERS,
|
|
||||||
TRICK_ATTACKERS,
|
|
||||||
HELD_MANA_SOURCES_FOR_MAIN2,
|
|
||||||
HELD_MANA_SOURCES_FOR_DECLBLK,
|
|
||||||
HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK,
|
|
||||||
ATTACHED_THIS_TURN,
|
|
||||||
ANIMATED_THIS_TURN,
|
|
||||||
BOUNCED_THIS_TURN,
|
|
||||||
ACTIVATED_THIS_TURN,
|
|
||||||
CHOSEN_FOG_EFFECT,
|
|
||||||
//REVEALED_CARDS // stub, not linked to AI code yet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Card> getMemorySet(MemorySet set) {
|
private Set<Card> getMemorySet(MemorySet set) {
|
||||||
@@ -95,6 +101,8 @@ public class AiCardMemory {
|
|||||||
return memHeldManaSourcesForCombat;
|
return memHeldManaSourcesForCombat;
|
||||||
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
|
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
|
||||||
return memHeldManaSourcesForEnemyCombat;
|
return memHeldManaSourcesForEnemyCombat;
|
||||||
|
case HELD_MANA_SOURCES_FOR_NEXT_SPELL:
|
||||||
|
return memHeldManaSourcesForNextSpell;
|
||||||
case ATTACHED_THIS_TURN:
|
case ATTACHED_THIS_TURN:
|
||||||
return memAttachedThisTurn;
|
return memAttachedThisTurn;
|
||||||
case ANIMATED_THIS_TURN:
|
case ANIMATED_THIS_TURN:
|
||||||
@@ -105,6 +113,8 @@ public class AiCardMemory {
|
|||||||
return memActivatedThisTurn;
|
return memActivatedThisTurn;
|
||||||
case CHOSEN_FOG_EFFECT:
|
case CHOSEN_FOG_EFFECT:
|
||||||
return memChosenFogEffect;
|
return memChosenFogEffect;
|
||||||
|
case MARKED_TO_AVOID_REENTRY:
|
||||||
|
return memMarkedToAvoidReentry;
|
||||||
//case REVEALED_CARDS:
|
//case REVEALED_CARDS:
|
||||||
// return memRevealedCards;
|
// return memRevealedCards;
|
||||||
default:
|
default:
|
||||||
@@ -273,35 +283,66 @@ public class AiCardMemory {
|
|||||||
* Clears all memory sets stored in this card memory for the given player.
|
* Clears all memory sets stored in this card memory for the given player.
|
||||||
*/
|
*/
|
||||||
public void clearAllRemembered() {
|
public void clearAllRemembered() {
|
||||||
clearMemorySet(MemorySet.MANDATORY_ATTACKERS);
|
for (MemorySet memSet : MemorySet.values()) {
|
||||||
clearMemorySet(MemorySet.TRICK_ATTACKERS);
|
clearMemorySet(memSet);
|
||||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
}
|
||||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
|
|
||||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
|
|
||||||
clearMemorySet(MemorySet.ATTACHED_THIS_TURN);
|
|
||||||
clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
|
|
||||||
clearMemorySet(MemorySet.BOUNCED_THIS_TURN);
|
|
||||||
clearMemorySet(MemorySet.ACTIVATED_THIS_TURN);
|
|
||||||
clearMemorySet(MemorySet.CHOSEN_FOG_EFFECT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static functions to simplify access to AI card memory of a given AI player.
|
// Static functions to simplify access to AI card memory of a given AI player.
|
||||||
public static void rememberCard(Player ai, Card c, MemorySet set) {
|
public static void rememberCard(Player ai, Card c, MemorySet set) {
|
||||||
|
if (!ai.getController().isAI()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().rememberCard(c, set);
|
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().rememberCard(c, set);
|
||||||
}
|
}
|
||||||
|
public static void rememberCard(AiController aic, Card c, MemorySet set) {
|
||||||
|
aic.getCardMemory().rememberCard(c, set);
|
||||||
|
}
|
||||||
public static void forgetCard(Player ai, Card c, MemorySet set) {
|
public static void forgetCard(Player ai, Card c, MemorySet set) {
|
||||||
|
if (!ai.getController().isAI()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().forgetCard(c, set);
|
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().forgetCard(c, set);
|
||||||
}
|
}
|
||||||
|
public static void forgetCard(AiController aic, Card c, MemorySet set) {
|
||||||
|
aic.getCardMemory().forgetCard(c, set);
|
||||||
|
}
|
||||||
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
|
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
|
||||||
|
if (!ai.getController().isAI()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set);
|
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set);
|
||||||
}
|
}
|
||||||
|
public static boolean isRememberedCard(AiController aic, Card c, MemorySet set) {
|
||||||
|
return aic.getCardMemory().isRememberedCard(c, set);
|
||||||
|
}
|
||||||
public static boolean isRememberedCardByName(Player ai, String name, MemorySet set) {
|
public static boolean isRememberedCardByName(Player ai, String name, MemorySet set) {
|
||||||
|
if (!ai.getController().isAI()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCardByName(name, set);
|
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCardByName(name, set);
|
||||||
}
|
}
|
||||||
|
public static boolean isRememberedCardByName(AiController aic, String name, MemorySet set) {
|
||||||
|
return aic.getCardMemory().isRememberedCardByName(name, set);
|
||||||
|
}
|
||||||
public static void clearMemorySet(Player ai, MemorySet set) {
|
public static void clearMemorySet(Player ai, MemorySet set) {
|
||||||
|
if (!ai.getController().isAI()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
|
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
|
||||||
}
|
}
|
||||||
|
public static void clearMemorySet(AiController aic, MemorySet set) {
|
||||||
|
if (!isMemorySetEmpty(aic, set)) {
|
||||||
|
aic.getCardMemory().clearMemorySet(set);
|
||||||
|
}
|
||||||
|
}
|
||||||
public static boolean isMemorySetEmpty(Player ai, MemorySet set) {
|
public static boolean isMemorySetEmpty(Player ai, MemorySet set) {
|
||||||
|
if (!ai.getController().isAI()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isMemorySetEmpty(set);
|
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isMemorySetEmpty(set);
|
||||||
}
|
}
|
||||||
|
public static boolean isMemorySetEmpty(AiController aic, MemorySet set) {
|
||||||
|
return aic.getCardMemory().isMemorySetEmpty(set);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,12 +18,10 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import com.esotericsoftware.minlog.Log;
|
import com.esotericsoftware.minlog.Log;
|
||||||
import com.google.common.base.Function;
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import forge.ai.ability.ChangeZoneAi;
|
import forge.ai.ability.ChangeZoneAi;
|
||||||
import forge.ai.ability.ExploreAi;
|
import forge.ai.ability.ExploreAi;
|
||||||
import forge.ai.simulation.SpellAbilityPicker;
|
import forge.ai.simulation.SpellAbilityPicker;
|
||||||
@@ -38,6 +36,7 @@ import forge.game.ability.AbilityUtils;
|
|||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.ability.SpellApiBased;
|
import forge.game.ability.SpellApiBased;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
|
import forge.game.card.CardPredicates.Accessors;
|
||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
@@ -50,6 +49,7 @@ import forge.game.player.PlayerActionConfirmMode;
|
|||||||
import forge.game.replacement.ReplaceMoved;
|
import forge.game.replacement.ReplaceMoved;
|
||||||
import forge.game.replacement.ReplacementEffect;
|
import forge.game.replacement.ReplacementEffect;
|
||||||
import forge.game.spellability.*;
|
import forge.game.spellability.*;
|
||||||
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.trigger.WrappedAbility;
|
import forge.game.trigger.WrappedAbility;
|
||||||
@@ -59,6 +59,8 @@ import forge.util.Aggregates;
|
|||||||
import forge.util.Expressions;
|
import forge.util.Expressions;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
import io.sentry.Sentry;
|
||||||
|
import io.sentry.event.BreadcrumbBuilder;
|
||||||
|
|
||||||
import java.security.InvalidParameterException;
|
import java.security.InvalidParameterException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -89,6 +91,9 @@ public class AiController {
|
|||||||
this.cheatShuffle = canCheatShuffle;
|
this.cheatShuffle = canCheatShuffle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean usesSimulation() {
|
||||||
|
return this.useSimulation;
|
||||||
|
}
|
||||||
public void setUseSimulation(boolean value) {
|
public void setUseSimulation(boolean value) {
|
||||||
this.useSimulation = value;
|
this.useSimulation = value;
|
||||||
}
|
}
|
||||||
@@ -422,7 +427,8 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
return player.canPlayLand(c);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return landList;
|
return landList;
|
||||||
@@ -432,6 +438,38 @@ public class AiController {
|
|||||||
if (landList.isEmpty()) {
|
if (landList.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
|
||||||
|
|
||||||
|
// Some considerations for Momir/MoJhoSto
|
||||||
|
boolean hasMomir = !CardLists.filter(player.getCardsIn(ZoneType.Command),
|
||||||
|
CardPredicates.nameEquals("Momir Vig, Simic Visionary Avatar")).isEmpty();
|
||||||
|
if (hasMomir && nonLandsInHand.isEmpty()) {
|
||||||
|
// Only do this if we have an all-basic land hand, which covers both stock Momir and MoJhoSto modes
|
||||||
|
// and also a custom Vanguard setup with a customized basic land deck and Momir as the avatar.
|
||||||
|
String landStrategy = getProperty(AiProps.MOMIR_BASIC_LAND_STRATEGY);
|
||||||
|
if (landStrategy.equalsIgnoreCase("random")) {
|
||||||
|
// Pick a completely random basic land
|
||||||
|
return Aggregates.random(landList);
|
||||||
|
} else if (landStrategy.toLowerCase().startsWith("preforder:")) {
|
||||||
|
// Pick a basic land in order of preference, or play a random one if nothing is preferred
|
||||||
|
String order = landStrategy.substring(10);
|
||||||
|
for (char c : order.toCharArray()) {
|
||||||
|
byte color = MagicColor.fromName(c);
|
||||||
|
for (Card land : landList) {
|
||||||
|
for (final SpellAbility m : ComputerUtilMana.getAIPlayableMana(land)) {
|
||||||
|
AbilityManaPart mp = m.getManaPart();
|
||||||
|
if (mp.canProduce(MagicColor.toShortString(color), m)) {
|
||||||
|
return land;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Aggregates.random(landList);
|
||||||
|
}
|
||||||
|
// If nothing is done here, proceeds to the default land picking strategy
|
||||||
|
}
|
||||||
|
|
||||||
//Skip reflected lands.
|
//Skip reflected lands.
|
||||||
CardCollection unreflectedLands = new CardCollection(landList);
|
CardCollection unreflectedLands = new CardCollection(landList);
|
||||||
for (Card l : landList) {
|
for (Card l : landList) {
|
||||||
@@ -443,7 +481,6 @@ public class AiController {
|
|||||||
landList = unreflectedLands;
|
landList = unreflectedLands;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
|
|
||||||
|
|
||||||
//try to skip lands that enter the battlefield tapped
|
//try to skip lands that enter the battlefield tapped
|
||||||
if (!nonLandsInHand.isEmpty()) {
|
if (!nonLandsInHand.isEmpty()) {
|
||||||
@@ -593,16 +630,35 @@ public class AiController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reserveManaSources(SpellAbility sa) {
|
public boolean reserveManaSources(SpellAbility sa) {
|
||||||
reserveManaSources(sa, PhaseType.MAIN2, false);
|
return reserveManaSources(sa, PhaseType.MAIN2, false, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy) {
|
public boolean reserveManaSourcesForNextSpell(SpellAbility sa, SpellAbility exceptForSa) {
|
||||||
|
return reserveManaSources(sa, null, false, true, exceptForSa);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy) {
|
||||||
|
return reserveManaSources(sa, phaseType, enemy, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) {
|
||||||
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
|
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
|
||||||
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
||||||
|
|
||||||
AiCardMemory.MemorySet memSet;
|
// used for chained spells where two spells need to be cast in succession
|
||||||
|
if (exceptForThisSa != null) {
|
||||||
|
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(ComputerUtilMana.calculateManaCost(exceptForThisSa, true, 0), exceptForThisSa, player));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manaSources.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AiCardMemory.MemorySet memSet;
|
||||||
|
if (phaseType == null && forNextSpell) {
|
||||||
|
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL;
|
||||||
|
} else {
|
||||||
switch (phaseType) {
|
switch (phaseType) {
|
||||||
case MAIN2:
|
case MAIN2:
|
||||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||||
@@ -617,10 +673,18 @@ public class AiController {
|
|||||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a simplification, since one mana source can produce more than one mana,
|
||||||
|
// but should work in most circumstances to ensure safety in whatever the AI is using this for.
|
||||||
|
if (manaSources.size() >= cost.getConvertedManaCost()) {
|
||||||
for (Card c : manaSources) {
|
for (Card c : manaSources) {
|
||||||
AiCardMemory.rememberCard(player, c, memSet);
|
AiCardMemory.rememberCard(player, c, memSet);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is for playing spells regularly (no Cascade/Ripple etc.)
|
// This is for playing spells regularly (no Cascade/Ripple etc.)
|
||||||
@@ -629,11 +693,27 @@ public class AiController {
|
|||||||
return AiPlayDecision.CantPlaySa;
|
return AiPlayDecision.CantPlaySa;
|
||||||
}
|
}
|
||||||
|
|
||||||
AiPlayDecision op = canPlaySa(sa);
|
boolean xCost = sa.getPayCosts().hasXInAnyCostPart();
|
||||||
if (op != AiPlayDecision.WillPlay) {
|
if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) {
|
||||||
return op;
|
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
|
||||||
|
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
|
||||||
|
return AiPlayDecision.CantAfford;
|
||||||
}
|
}
|
||||||
return ComputerUtilCost.canPayCost(sa, player) ? AiPlayDecision.WillPlay : AiPlayDecision.CantAfford;
|
|
||||||
|
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||||
|
if (canPlay != AiPlayDecision.WillPlay) {
|
||||||
|
return canPlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xCost && !ComputerUtilCost.canPayCost(sa, player)) {
|
||||||
|
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
|
||||||
|
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
|
||||||
|
return AiPlayDecision.CantAfford;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
|
||||||
|
// are willing to play the SA
|
||||||
|
return AiPlayDecision.WillPlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AiPlayDecision canPlaySa(SpellAbility sa) {
|
public AiPlayDecision canPlaySa(SpellAbility sa) {
|
||||||
@@ -644,8 +724,37 @@ public class AiController {
|
|||||||
if (sa instanceof WrappedAbility) {
|
if (sa instanceof WrappedAbility) {
|
||||||
return canPlaySa(((WrappedAbility) sa).getWrappedAbility());
|
return canPlaySa(((WrappedAbility) sa).getWrappedAbility());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations
|
||||||
|
if (getBooleanProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) {
|
||||||
|
if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) {
|
||||||
|
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When processing a new SA, clear the previously remembered cards that have been marked to avoid re-entry
|
||||||
|
// which might potentially cause a stack overflow.
|
||||||
|
AiCardMemory.clearMemorySet(this, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY);
|
||||||
|
|
||||||
if (sa.getApi() != null) {
|
if (sa.getApi() != null) {
|
||||||
|
|
||||||
|
String msg = "AiController:canPlaySa: AI checks for if can PlaySa";
|
||||||
|
Sentry.getContext().recordBreadcrumb(
|
||||||
|
new BreadcrumbBuilder().setMessage(msg)
|
||||||
|
.withData("Api", sa.getApi().toString())
|
||||||
|
.withData("Card", card.getName()).withData("SA", sa.toString()).build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// add Extra for debugging
|
||||||
|
Sentry.getContext().addExtra("Card", card);
|
||||||
|
Sentry.getContext().addExtra("SA", sa.toString());
|
||||||
|
|
||||||
boolean canPlay = SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(player, sa);
|
boolean canPlay = SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(player, sa);
|
||||||
|
|
||||||
|
// remove added extra
|
||||||
|
Sentry.getContext().removeExtra("Card");
|
||||||
|
Sentry.getContext().removeExtra("SA");
|
||||||
|
|
||||||
if (!canPlay) {
|
if (!canPlay) {
|
||||||
return AiPlayDecision.CantPlayAi;
|
return AiPlayDecision.CantPlayAi;
|
||||||
}
|
}
|
||||||
@@ -700,7 +809,7 @@ public class AiController {
|
|||||||
// will need actual logic that determines if the enchantment is able
|
// will need actual logic that determines if the enchantment is able
|
||||||
// to disable the permanent or it's still functional and a duplicate is unneeded.
|
// to disable the permanent or it's still functional and a duplicate is unneeded.
|
||||||
boolean disabledByEnemy = false;
|
boolean disabledByEnemy = false;
|
||||||
for (Card card2 : card.getEnchantedBy(false)) {
|
for (Card card2 : card.getEnchantedBy()) {
|
||||||
if (card2.getOwner() != player) {
|
if (card2.getOwner() != player) {
|
||||||
disabledByEnemy = true;
|
disabledByEnemy = true;
|
||||||
}
|
}
|
||||||
@@ -717,10 +826,71 @@ public class AiController {
|
|||||||
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
|
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
|
||||||
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add any other necessary logic to play a basic spell here
|
// add any other necessary logic to play a basic spell here
|
||||||
return ComputerUtilCard.checkNeedsToPlayReqs(card, sa);
|
return ComputerUtilCard.checkNeedsToPlayReqs(card, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canPlaySpellWithoutBuyback(Card card, SpellAbility sa) {
|
||||||
|
boolean wasteBuybackAllowed = false;
|
||||||
|
|
||||||
|
// About to lose game : allow
|
||||||
|
if (ComputerUtil.aiLifeInDanger(player, true, 0)) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int copies = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(card.getName())).size();
|
||||||
|
// Have two copies : allow
|
||||||
|
if (copies >= 2) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int neededMana = 0;
|
||||||
|
boolean dangerousRecurringCost = false;
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CostAdjustment.adjust(costWithBuyback, sa);
|
||||||
|
if (costWithBuyback.getCostMana() != null) {
|
||||||
|
neededMana = costWithBuyback.getCostMana().getMana().getCMC();
|
||||||
|
}
|
||||||
|
if (costWithBuyback.hasSpecificCostType(CostPayLife.class)
|
||||||
|
|| costWithBuyback.hasSpecificCostType(CostDiscard.class)
|
||||||
|
|| costWithBuyback.hasSpecificCostType(CostSacrifice.class)) {
|
||||||
|
dangerousRecurringCost = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// won't be able to afford buyback any time soon
|
||||||
|
// if Buyback cost includes sacrifice, life, discard
|
||||||
|
if (dangerousRecurringCost) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory Crystal-like effects need special handling
|
||||||
|
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||||
|
for (StaticAbility s : c.getStaticAbilities()) {
|
||||||
|
if ("ReduceCost".equals(s.getParam("Mode"))
|
||||||
|
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
|
||||||
|
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (neededMana < 0) {
|
||||||
|
neededMana = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false);
|
||||||
|
if (hasMana < neededMana - 1) {
|
||||||
|
wasteBuybackAllowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasteBuybackAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
// not sure "playing biggest spell" matters?
|
// not sure "playing biggest spell" matters?
|
||||||
private final static Comparator<SpellAbility> saComparator = new Comparator<SpellAbility>() {
|
private final static Comparator<SpellAbility> saComparator = new Comparator<SpellAbility>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -730,6 +900,13 @@ public class AiController {
|
|||||||
int a1 = a.getPayCosts() == null ? 0 : a.getPayCosts().getTotalMana().getCMC();
|
int a1 = a.getPayCosts() == null ? 0 : a.getPayCosts().getTotalMana().getCMC();
|
||||||
int b1 = b.getPayCosts() == null ? 0 : b.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")) {
|
||||||
|
return 1;
|
||||||
|
} else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
|
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
|
||||||
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -769,6 +946,20 @@ public class AiController {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (a.isSpectacle() && !b.isSpectacle()
|
||||||
|
&& a.getPayCosts().getTotalMana().getCMC() < b.getPayCosts().getTotalMana().getCMC()) {
|
||||||
|
return 1;
|
||||||
|
} else if (b.isSpectacle() && !a.isSpectacle()
|
||||||
|
&& b.getPayCosts().getTotalMana().getCMC() < a.getPayCosts().getTotalMana().getCMC()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a1 += getSpellAbilityPriority(a);
|
a1 += getSpellAbilityPriority(a);
|
||||||
b1 += getSpellAbilityPriority(b);
|
b1 += getSpellAbilityPriority(b);
|
||||||
|
|
||||||
@@ -790,6 +981,9 @@ public class AiController {
|
|||||||
if (source.isCreature()) {
|
if (source.isCreature()) {
|
||||||
p += 1;
|
p += 1;
|
||||||
}
|
}
|
||||||
|
if (source.hasSVar("AIPriorityModifier")) {
|
||||||
|
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
||||||
|
}
|
||||||
// don't play equipments before having any creatures
|
// don't play equipments before having any creatures
|
||||||
if (source.isEquipment() && noCreatures) {
|
if (source.isEquipment() && noCreatures) {
|
||||||
p -= 9;
|
p -= 9;
|
||||||
@@ -816,7 +1010,7 @@ public class AiController {
|
|||||||
p += 9;
|
p += 9;
|
||||||
}
|
}
|
||||||
// sort planeswalker abilities with most costly first
|
// sort planeswalker abilities with most costly first
|
||||||
if (sa.getRestrictions().isPwAbility()) {
|
if (sa.isPwAbility()) {
|
||||||
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
|
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
|
||||||
if (cost instanceof CostRemoveCounter) {
|
if (cost instanceof CostRemoveCounter) {
|
||||||
p += cost.convertAmount() == null ? 1 : cost.convertAmount();
|
p += cost.convertAmount() == null ? 1 : cost.convertAmount();
|
||||||
@@ -848,9 +1042,10 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa, final CardCollectionView exclude) {
|
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));
|
CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand));
|
||||||
hand.removeAll(exclude);
|
hand.removeAll(exclude);
|
||||||
if ((uTypes != null) && (sa != null)) {
|
if ((uTypes != null) && (sa != null) && !noFiltering) {
|
||||||
hand = CardLists.getValidCards(hand, uTypes, sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
hand = CardLists.getValidCards(hand, uTypes, sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||||
}
|
}
|
||||||
return getCardsToDiscard(numDiscard, numDiscard, hand, sa);
|
return getCardsToDiscard(numDiscard, numDiscard, hand, sa);
|
||||||
@@ -870,6 +1065,14 @@ public class AiController {
|
|||||||
min = 1;
|
min = 1;
|
||||||
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
|
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
|
||||||
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
|
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
|
||||||
|
} else if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
|
||||||
|
final int CMC = Integer.parseInt(sourceCard.getSVar("PayX"));
|
||||||
|
CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(CMC));
|
||||||
|
if (discards.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return new CardCollection(ComputerUtilCard.getWorstAI(discards));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.hasParam("AnyNumber")) {
|
if (sa.hasParam("AnyNumber")) {
|
||||||
@@ -955,10 +1158,14 @@ public class AiController {
|
|||||||
numLandsAvailable++;
|
numLandsAvailable++;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Discard unplayable card
|
// Discard unplayable card (checks by CMC)
|
||||||
|
// But check if there is a card in play that allows casting spells for free!
|
||||||
|
// if yes, nothing is unplayable based on CMC alone
|
||||||
boolean discardedUnplayable = false;
|
boolean discardedUnplayable = false;
|
||||||
|
boolean freeCastAllowed = ComputerUtilCost.isFreeCastAllowedByPermanent(player, null);
|
||||||
|
|
||||||
for (int j = 0; j < validCards.size(); j++) {
|
for (int j = 0; j < validCards.size(); j++) {
|
||||||
if (validCards.get(j).getCMC() > numLandsAvailable && !validCards.get(j).hasSVar("DoNotDiscardIfAble")) {
|
if ((validCards.get(j).getCMC() > numLandsAvailable || freeCastAllowed) && !validCards.get(j).hasSVar("DoNotDiscardIfAble")) {
|
||||||
discardList.add(validCards.get(j));
|
discardList.add(validCards.get(j));
|
||||||
validCards.remove(validCards.get(j));
|
validCards.remove(validCards.get(j));
|
||||||
discardedUnplayable = true;
|
discardedUnplayable = true;
|
||||||
@@ -1011,7 +1218,7 @@ public class AiController {
|
|||||||
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
ApiType api = sa.getApi();
|
ApiType api = sa.getApi();
|
||||||
|
|
||||||
// Abilities without api may also use this routine, However they should provide a unique mode value
|
// Abilities without api may also use this routine, However they should provide a unique mode value ?? How could this work?
|
||||||
if (api == null) {
|
if (api == null) {
|
||||||
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
|
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
|
||||||
mode);
|
mode);
|
||||||
@@ -1153,6 +1360,9 @@ public class AiController {
|
|||||||
// re-created if needed and used for any AI logic that needs it.
|
// re-created if needed and used for any AI logic that needs it.
|
||||||
predictedCombat = null;
|
predictedCombat = 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);
|
||||||
|
|
||||||
if (useSimulation) {
|
if (useSimulation) {
|
||||||
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
|
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
|
||||||
}
|
}
|
||||||
@@ -1174,7 +1384,7 @@ public class AiController {
|
|||||||
if (landsWannaPlay != null) {
|
if (landsWannaPlay != null) {
|
||||||
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
|
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
|
||||||
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
|
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
|
||||||
if (landsWannaPlay != null && !landsWannaPlay.isEmpty() && player.canPlayLand(null)) {
|
if (landsWannaPlay != null && !landsWannaPlay.isEmpty()) {
|
||||||
// TODO search for other land it might want to play?
|
// TODO search for other land it might want to play?
|
||||||
Card land = chooseBestLandToPlay(landsWannaPlay);
|
Card land = chooseBestLandToPlay(landsWannaPlay);
|
||||||
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
|
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
|
||||||
@@ -1316,12 +1526,28 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final SpellAbility getSpellAbilityToPlay() {
|
private final SpellAbility getSpellAbilityToPlay() {
|
||||||
// if top of stack is owned by me
|
final CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
|
||||||
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
|
List<SpellAbility> saList = Lists.newArrayList();
|
||||||
// probably should let my stuff resolve
|
|
||||||
|
SpellAbility top = null;
|
||||||
|
if (!game.getStack().isEmpty()) {
|
||||||
|
top = game.getStack().peekAbility();
|
||||||
|
}
|
||||||
|
final boolean topOwnedByAI = top != null && top.getActivatingPlayer().equals(player);
|
||||||
|
|
||||||
|
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
|
||||||
|
if (ComputerUtilAbility.getFirstCopySASpell(saList) == null) {
|
||||||
|
// Nothing to copy the spell with, so do nothing.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!game.getStack().isEmpty()) {
|
if (!game.getStack().isEmpty()) {
|
||||||
SpellAbility counter = chooseCounterSpell(getPlayableCounters(cards));
|
SpellAbility counter = chooseCounterSpell(getPlayableCounters(cards));
|
||||||
@@ -1332,7 +1558,13 @@ public class AiController {
|
|||||||
return counterETB;
|
return counterETB;
|
||||||
}
|
}
|
||||||
|
|
||||||
return chooseSpellAbilityToPlayFromList(ComputerUtilAbility.getSpellAbilities(cards, player), true);
|
if (saList.isEmpty()) {
|
||||||
|
saList = ComputerUtilAbility.getSpellAbilities(cards, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true);
|
||||||
|
|
||||||
|
return chosenSa;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SpellAbility chooseSpellAbilityToPlayFromList(final List<SpellAbility> all, boolean skipCounter) {
|
private SpellAbility chooseSpellAbilityToPlayFromList(final List<SpellAbility> all, boolean skipCounter) {
|
||||||
@@ -1484,22 +1716,24 @@ public class AiController {
|
|||||||
boolean hasLeyline1 = false;
|
boolean hasLeyline1 = false;
|
||||||
SpellAbility saGemstones = null;
|
SpellAbility saGemstones = null;
|
||||||
|
|
||||||
for(int i = 0; i < result.size(); i++) {
|
List<SpellAbility> toRemove = Lists.newArrayList();
|
||||||
SpellAbility sa = result.get(i);
|
for(SpellAbility sa : result) {
|
||||||
|
|
||||||
String srcName = sa.getHostCard().getName();
|
String srcName = sa.getHostCard().getName();
|
||||||
if ("Gemstone Caverns".equals(srcName)) {
|
if ("Gemstone Caverns".equals(srcName)) {
|
||||||
if (saGemstones == null)
|
if (saGemstones == null)
|
||||||
saGemstones = sa;
|
saGemstones = sa;
|
||||||
else
|
else
|
||||||
result.remove(i--);
|
toRemove.add(sa);
|
||||||
} else if ("Leyline of Singularity".equals(srcName)) {
|
} else if ("Leyline of Singularity".equals(srcName)) {
|
||||||
if (!hasLeyline1)
|
if (!hasLeyline1)
|
||||||
hasLeyline1 = true;
|
hasLeyline1 = true;
|
||||||
else
|
else
|
||||||
result.remove(i--);
|
toRemove.add(sa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for(SpellAbility sa : toRemove) {
|
||||||
|
result.remove(sa);
|
||||||
|
}
|
||||||
|
|
||||||
// Play them last
|
// Play them last
|
||||||
if (saGemstones != null) {
|
if (saGemstones != null) {
|
||||||
@@ -1544,7 +1778,7 @@ public class AiController {
|
|||||||
+ MyRandom.getRandom().nextInt(3);
|
+ MyRandom.getRandom().nextInt(3);
|
||||||
return Math.max(remaining, min) / 2;
|
return Math.max(remaining, min) / 2;
|
||||||
} else if ("LowestLoseLife".equals(logic)) {
|
} else if ("LowestLoseLife".equals(logic)) {
|
||||||
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, ComputerUtil.getOpponentFor(player).getLife())) + 1;
|
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
|
||||||
} else if ("HighestGetCounter".equals(logic)) {
|
} else if ("HighestGetCounter".equals(logic)) {
|
||||||
return MyRandom.getRandom().nextInt(3);
|
return MyRandom.getRandom().nextInt(3);
|
||||||
} else if (source.hasSVar("EnergyToPay")) {
|
} else if (source.hasSVar("EnergyToPay")) {
|
||||||
@@ -1557,75 +1791,6 @@ public class AiController {
|
|||||||
throw new UnsupportedOperationException("AI is not supposed to reach this code at the moment");
|
throw new UnsupportedOperationException("AI is not supposed to reach this code at the moment");
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
public CardCollection chooseCardsForEffect(CardCollectionView pool, SpellAbility sa, int min, int max, boolean isOptional) {
|
||||||
if (sa == null || sa.getApi() == null) {
|
if (sa == null || sa.getApi() == null) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
@@ -1801,6 +1966,12 @@ public class AiController {
|
|||||||
return left.contains(ComputerUtilCard.getBestCreatureAI(all));
|
return left.contains(ComputerUtilCard.getBestCreatureAI(all));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ("Aminatou".equals(sa.getParam("AILogic")) && game.getPlayers().size() > 2) {
|
||||||
|
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.NONLAND_PERMANENTS);
|
||||||
|
CardCollection left = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Left));
|
||||||
|
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Right));
|
||||||
|
return Aggregates.sum(left, Accessors.fnGetCmc) > Aggregates.sum(right, Accessors.fnGetCmc);
|
||||||
|
}
|
||||||
return MyRandom.getRandom().nextBoolean();
|
return MyRandom.getRandom().nextBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -459,6 +459,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
Integer c = cost.convertAmount();
|
Integer c = cost.convertAmount();
|
||||||
String type = cost.getType();
|
String type = cost.getType();
|
||||||
boolean isVehicle = type.contains("+withTotalPowerGE");
|
boolean isVehicle = type.contains("+withTotalPowerGE");
|
||||||
|
|
||||||
|
CardCollection exclude = new CardCollection();
|
||||||
|
exclude.addAll(tapped);
|
||||||
|
|
||||||
if (c == null) {
|
if (c == null) {
|
||||||
final String sVar = ability.getSVar(amount);
|
final String sVar = ability.getSVar(amount);
|
||||||
if (sVar.equals("XChoice")) {
|
if (sVar.equals("XChoice")) {
|
||||||
@@ -482,18 +486,36 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("DontPayTapCostWithManaSources".equals(source.getSVar("AIPaymentPreference"))) {
|
||||||
|
CardCollectionView toExclude =
|
||||||
|
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"),
|
||||||
|
ability.getActivatingPlayer(), ability.getHostCard(), ability);
|
||||||
|
toExclude = CardLists.filter(toExclude, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
for (final SpellAbility sa : card.getSpellAbilities()) {
|
||||||
|
if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
exclude.addAll(toExclude);
|
||||||
|
}
|
||||||
|
|
||||||
String totalP = "";
|
String totalP = "";
|
||||||
CardCollectionView totap;
|
CardCollectionView totap;
|
||||||
if (isVehicle) {
|
if (isVehicle) {
|
||||||
totalP = type.split("withTotalPowerGE")[1];
|
totalP = type.split("withTotalPowerGE")[1];
|
||||||
type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
|
type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
|
||||||
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), tapped);
|
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), exclude);
|
||||||
} else {
|
} else {
|
||||||
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);
|
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, exclude);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totap == null) {
|
if (totap == null) {
|
||||||
// System.out.println("Couldn't find a valid card(s) to tap for: " + source.getName());
|
//System.out.println("Couldn't find a valid card(s) to tap for: " + source.getName());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
tapped.addAll(totap);
|
tapped.addAll(totap);
|
||||||
@@ -528,7 +550,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
// are currently conventionally flagged with AILogic$ DoSacrifice.
|
// are currently conventionally flagged with AILogic$ DoSacrifice.
|
||||||
c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null);
|
c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null);
|
||||||
} else {
|
} else {
|
||||||
// Other cards are assumed to be flagged RemAIDeck for now
|
// Other cards are assumed to be flagged AI:RemoveDeck:All for now
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -787,8 +809,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
final String sVar = ability.getSVar(amount);
|
final String sVar = ability.getSVar(amount);
|
||||||
if (sVar.equals("XChoice")) {
|
if (sVar.equals("XChoice")) {
|
||||||
c = AbilityUtils.calculateAmount(source, "ChosenX", ability);
|
c = AbilityUtils.calculateAmount(source, "ChosenX", ability);
|
||||||
|
source.setSVar("ChosenX", "Number$" + String.valueOf(c));
|
||||||
} else if (amount.equals("All")) {
|
} else if (amount.equals("All")) {
|
||||||
c = source.getCounters(cost.counter);
|
c = source.getCounters(cost.counter);
|
||||||
|
} else if (sVar.equals("Targeted$CardManaCost")) {
|
||||||
|
c = 0;
|
||||||
|
if (ability.getTargets().getNumTargeted() > 0) {
|
||||||
|
for (Card tgt : ability.getTargets().getTargetCards()) {
|
||||||
|
if (tgt.getManaCost() != null) {
|
||||||
|
c += tgt.getManaCost().getCMC();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (sVar.equals("Count$xPaid")) {
|
||||||
|
c = AbilityUtils.calculateAmount(source, "PayX", null);
|
||||||
} else {
|
} else {
|
||||||
c = AbilityUtils.calculateAmount(source, amount, ability);
|
c = AbilityUtils.calculateAmount(source, amount, ability);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public enum AiProps { /** */
|
|||||||
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
|
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
|
||||||
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
|
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
|
||||||
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
|
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
|
||||||
|
TRY_TO_PRESERVE_BUYBACK_SPELLS ("true"), /** */
|
||||||
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
|
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
|
||||||
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
|
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
|
||||||
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
|
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
|
||||||
@@ -69,11 +70,18 @@ public enum AiProps { /** */
|
|||||||
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
|
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
|
||||||
ALWAYS_COUNTER_AURAS ("true"), /** */
|
ALWAYS_COUNTER_AURAS ("true"), /** */
|
||||||
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
|
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
|
||||||
|
CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK ("30"), /** */
|
||||||
|
ALWAYS_COPY_SPELL_IF_CMC_DIFF ("2"), /** */
|
||||||
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
|
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
|
||||||
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
|
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
|
||||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
|
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
|
||||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
|
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
|
||||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
|
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
|
||||||
|
AVOID_TARGETING_CREATS_THAT_WILL_DIE ("true"), /** */
|
||||||
|
DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION ("true"), /** */
|
||||||
|
CHANCE_TO_CHAIN_TWO_DAMAGE_SPELLS("50"), /** */
|
||||||
|
HOLD_X_DAMAGE_SPELLS_FOR_MORE_DAMAGE_CHANCE("100"),
|
||||||
|
HOLD_X_DAMAGE_SPELLS_THRESHOLD("5"), /** */
|
||||||
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
|
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
|
||||||
USE_BERSERK_AGGRESSIVELY ("false"), /** */
|
USE_BERSERK_AGGRESSIVELY ("false"), /** */
|
||||||
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
|
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
|
||||||
@@ -94,6 +102,7 @@ public enum AiProps { /** */
|
|||||||
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
|
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
|
||||||
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
|
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
|
||||||
SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL ("10"), /** */
|
SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL ("10"), /** */
|
||||||
|
SURVEIL_LIFEPERC_AFTER_PAYING_LIFE ("75"), /** */
|
||||||
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
|
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
|
||||||
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
|
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
|
||||||
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */
|
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */
|
||||||
@@ -105,13 +114,27 @@ public enum AiProps { /** */
|
|||||||
INTUITION_ALTERNATIVE_LOGIC ("false"), /** */
|
INTUITION_ALTERNATIVE_LOGIC ("false"), /** */
|
||||||
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
|
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
|
||||||
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE("2"), /** */
|
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE("2"), /** */
|
||||||
|
MOMIR_BASIC_LAND_STRATEGY("default"), /** */
|
||||||
MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA("5"), /** */
|
MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA("5"), /** */
|
||||||
MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR ("50"), /** */
|
MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR ("50"), /** */
|
||||||
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT ("20"), /** */
|
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT ("20"), /** */
|
||||||
// Experimental features, must be removed after extensive testing and, ideally, defaulting
|
AI_IN_DANGER_THRESHOLD("4"), /** */
|
||||||
|
AI_IN_DANGER_MAX_THRESHOLD("4"), /** */
|
||||||
|
FLASH_ENABLE_ADVANCED_LOGIC("true"), /** */
|
||||||
|
FLASH_CHANCE_TO_OBEY_AMBUSHAI("100"), /** */
|
||||||
|
FLASH_CHANCE_TO_CAST_DUE_TO_ETB_EFFECTS("100"), /** */
|
||||||
|
FLASH_CHANCE_TO_CAST_FOR_ETB_BEFORE_MAIN1("10"), /** */
|
||||||
|
FLASH_CHANCE_TO_RESPOND_TO_STACK_WITH_ETB("0"), /** */
|
||||||
|
FLASH_CHANCE_TO_CAST_AS_VALUABLE_BLOCKER("100"),
|
||||||
|
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"); /** */
|
||||||
|
// Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
|
||||||
// <-- There are no experimental options here -->
|
// <-- There are no experimental options here -->
|
||||||
AI_IN_DANGER_THRESHOLD("4"),
|
|
||||||
AI_IN_DANGER_MAX_THRESHOLD("4");
|
|
||||||
|
|
||||||
|
|
||||||
private final String strDefaultVal;
|
private final String strDefaultVal;
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ package forge.ai;
|
|||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.*;
|
import com.google.common.collect.*;
|
||||||
|
import forge.ai.ability.ChooseGenericEffectAi;
|
||||||
import forge.ai.ability.ProtectAi;
|
import forge.ai.ability.ProtectAi;
|
||||||
import forge.ai.ability.TokenAi;
|
import forge.ai.ability.TokenAi;
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
|
import forge.card.ColorSet;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.card.mana.ManaCostShard;
|
import forge.card.mana.ManaCostShard;
|
||||||
import forge.game.CardTraitPredicates;
|
import forge.game.*;
|
||||||
import forge.game.Game;
|
|
||||||
import forge.game.GameObject;
|
|
||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
@@ -47,6 +47,7 @@ import forge.game.spellability.*;
|
|||||||
import forge.game.staticability.StaticAbility;
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
|
import forge.game.trigger.WrappedAbility;
|
||||||
import forge.game.zone.Zone;
|
import forge.game.zone.Zone;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
@@ -73,9 +74,9 @@ public class ComputerUtil {
|
|||||||
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game, Runnable chooseTargets) {
|
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game, Runnable chooseTargets) {
|
||||||
game.getStack().freezeStack();
|
game.getStack().freezeStack();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
source.setSplitStateToPlayAbility(sa);
|
||||||
|
|
||||||
if (sa.isSpell() && !source.isCopiedSpell()) {
|
if (sa.isSpell() && !source.isCopiedSpell()) {
|
||||||
if (source.getType().hasStringType("Arcane")) {
|
|
||||||
sa = AbilityUtils.addSpliceEffects(sa);
|
sa = AbilityUtils.addSpliceEffects(sa);
|
||||||
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
|
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
|
||||||
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
|
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
|
||||||
@@ -87,7 +88,6 @@ public class ComputerUtil {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
source.setCastSA(sa);
|
source.setCastSA(sa);
|
||||||
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
||||||
@@ -408,6 +408,35 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ComputerUtilCost.isFreeCastAllowedByPermanent(ai, "Discard")) {
|
||||||
|
// Dream Halls allows to discard 1 worthless card to cast 1 expensive for free
|
||||||
|
// Do it even if nothing marked for discard in hand, if it's worth doing!
|
||||||
|
int mana = ComputerUtilMana.getAvailableManaEstimate(ai, false);
|
||||||
|
|
||||||
|
boolean cantAffordSoon = activate.getCMC() > mana + 1;
|
||||||
|
boolean wrongColor = !activate.determineColor().hasNoColorsExcept(ColorSet.fromNames(ComputerUtilCost.getAvailableManaColors(ai, ImmutableList.<Card>of())).getColor());
|
||||||
|
|
||||||
|
// Only do this for spells, not activated abilities
|
||||||
|
// We can't pay for this spell even if we play another land, or have wrong colors
|
||||||
|
if (!activate.isInPlay() && (cantAffordSoon || wrongColor)) {
|
||||||
|
CardCollection options = new CardCollection();
|
||||||
|
for (Card c : typeList) {
|
||||||
|
// Try to avoid stupidity by playing cheap spells and paying for them with expensive spells
|
||||||
|
// while the intention was to do things the other way around
|
||||||
|
if (c.isCreature() && activate.isCreature()) {
|
||||||
|
if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(activate)) {
|
||||||
|
options.add(c);
|
||||||
|
}
|
||||||
|
} else if (c.getCMC() <= activate.getCMC()) {
|
||||||
|
options.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!options.isEmpty()) {
|
||||||
|
return ComputerUtilCard.getWorstAI(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Survival of the Fittest logic
|
// Survival of the Fittest logic
|
||||||
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
|
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
|
||||||
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
|
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
|
||||||
@@ -823,7 +852,7 @@ public class ComputerUtil {
|
|||||||
|
|
||||||
if (c != null && c.isEnchanted()) {
|
if (c != null && c.isEnchanted()) {
|
||||||
// TODO: choose "worst" controlled enchanting Aura
|
// TODO: choose "worst" controlled enchanting Aura
|
||||||
for (Card aura : c.getEnchantedBy(false)) {
|
for (Card aura : c.getEnchantedBy()) {
|
||||||
if (aura.getController().equals(c.getController()) && remaining.contains(aura)) {
|
if (aura.getController().equals(c.getController()) && remaining.contains(aura)) {
|
||||||
return aura;
|
return aura;
|
||||||
}
|
}
|
||||||
@@ -958,6 +987,27 @@ public class ComputerUtil {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (card.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) {
|
||||||
|
// Planning to choose Haste for Riot, so do this in Main 1
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have non-persistent mana in our pool, would be good to try to use it and not waste it
|
||||||
|
if (ai.getManaPool().willManaBeLostAtEndOfPhase()) {
|
||||||
|
boolean canUseToPayCost = false;
|
||||||
|
for (byte color : MagicColor.WUBRGC) {
|
||||||
|
if (ai.getManaPool().getAmountOfColor(color) > 0
|
||||||
|
&& ((card.getManaCost().getColorProfile() & color) == color)) {
|
||||||
|
canUseToPayCost = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseToPayCost) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (card.isCreature() && !card.hasKeyword(Keyword.DEFENDER)
|
if (card.isCreature() && !card.hasKeyword(Keyword.DEFENDER)
|
||||||
&& (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) {
|
&& (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) {
|
||||||
return true;
|
return true;
|
||||||
@@ -975,7 +1025,7 @@ public class ComputerUtil {
|
|||||||
playNow = false;
|
playNow = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!playNow && c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c) && c.canBeEquippedBy(card)) {
|
if (!playNow && c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c) && c.canBeAttached(card)) {
|
||||||
playNow = true;
|
playNow = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1025,6 +1075,20 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
} // AntiBuffedBy
|
} // AntiBuffedBy
|
||||||
|
|
||||||
|
// Plane cards that give Haste (e.g. Sokenzan)
|
||||||
|
if (ai.getGame().getRules().hasAppliedVariant(GameType.Planechase)) {
|
||||||
|
for (Card c : ai.getGame().getActivePlanes()) {
|
||||||
|
for (StaticAbility s : c.getStaticAbilities()) {
|
||||||
|
if (s.hasParam("AddKeyword")
|
||||||
|
&& s.getParam("AddKeyword").contains("Haste")
|
||||||
|
&& "Creature".equals(s.getParam("Affected"))
|
||||||
|
&& card.isCreature()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final CardCollectionView vengevines = ai.getCardsIn(ZoneType.Graveyard, "Vengevine");
|
final CardCollectionView vengevines = ai.getCardsIn(ZoneType.Graveyard, "Vengevine");
|
||||||
if (!vengevines.isEmpty()) {
|
if (!vengevines.isEmpty()) {
|
||||||
final CardCollectionView creatures = ai.getCardsIn(ZoneType.Hand);
|
final CardCollectionView creatures = ai.getCardsIn(ZoneType.Hand);
|
||||||
@@ -1190,7 +1254,7 @@ public class ComputerUtil {
|
|||||||
|
|
||||||
// returns true if the AI should stop using the ability
|
// returns true if the AI should stop using the ability
|
||||||
public static boolean preventRunAwayActivations(final SpellAbility sa) {
|
public static boolean preventRunAwayActivations(final SpellAbility sa) {
|
||||||
int activations = sa.getRestrictions().getNumberTurnActivations();
|
int activations = sa.getActivationsThisTurn();
|
||||||
|
|
||||||
if (sa.isTemporary()) {
|
if (sa.isTemporary()) {
|
||||||
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
|
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
|
||||||
@@ -1347,6 +1411,16 @@ public class ComputerUtil {
|
|||||||
if (sa.getApi() != ApiType.Fog) {
|
if (sa.getApi() != ApiType.Fog) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid re-entry for cards already being considered (e.g. in case the AI is considering
|
||||||
|
// Convoke or Improvise for a Fog-like effect)
|
||||||
|
if (c.hasKeyword("Convoke") || c.hasKeyword("Improvise")) {
|
||||||
|
if (AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.canPayCost(sa, ai)) {
|
if (!ComputerUtilCost.canPayCost(sa, ai)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1383,6 +1457,9 @@ public class ComputerUtil {
|
|||||||
if (!ComputerUtilCost.canPayCost(sa, ai)) {
|
if (!ComputerUtilCost.canPayCost(sa, ai)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!GameActionUtil.getOptionalCostValues(sa).isEmpty()) {
|
||||||
|
continue; // we can't rely on the AI being always willing and able to pay the optional cost to deal extra damage
|
||||||
|
}
|
||||||
damage = dmg;
|
damage = dmg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1438,6 +1515,9 @@ public class ComputerUtil {
|
|||||||
// iterate from top of stack to find SpellAbility, including sub-abilities,
|
// iterate from top of stack to find SpellAbility, including sub-abilities,
|
||||||
// that does not match "sa"
|
// that does not match "sa"
|
||||||
SpellAbility spell = si.getSpellAbility(true), sub = spell.getSubAbility();
|
SpellAbility spell = si.getSpellAbility(true), sub = spell.getSubAbility();
|
||||||
|
if (spell.isWrapper()) {
|
||||||
|
spell = ((WrappedAbility) spell).getWrappedAbility();
|
||||||
|
}
|
||||||
while (sub != null && sub != sa) {
|
while (sub != null && sub != sa) {
|
||||||
sub = sub.getSubAbility();
|
sub = sub.getSubAbility();
|
||||||
}
|
}
|
||||||
@@ -1761,6 +1841,68 @@ public class ComputerUtil {
|
|||||||
return threatened;
|
return threatened;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the specified creature will die this turn either from lethal damage in combat
|
||||||
|
* or from a killing spell on stack.
|
||||||
|
* TODO: This currently does not account for the fact that spells on stack can be countered, can be improved.
|
||||||
|
*
|
||||||
|
* @param creature
|
||||||
|
* A creature to check
|
||||||
|
* @return true if the creature dies according to current board position.
|
||||||
|
*/
|
||||||
|
public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa) {
|
||||||
|
final Game game = creature.getGame();
|
||||||
|
|
||||||
|
// a creature will die as a result of combat
|
||||||
|
boolean willDieInCombat = game.getPhaseHandler().inCombat()
|
||||||
|
&& ComputerUtilCombat.combatantWouldBeDestroyed(creature.getController(), creature, game.getCombat());
|
||||||
|
|
||||||
|
// a creature will [hopefully] die from a spell on stack
|
||||||
|
boolean willDieFromSpell = false;
|
||||||
|
boolean noStackCheck = false;
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
|
||||||
|
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
|
||||||
|
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
|
||||||
|
for (SpellAbilityStackInstance si : game.getStack()) {
|
||||||
|
SpellAbility sa = si.getSpellAbility(false);
|
||||||
|
if (sa.getApi() == ApiType.Counter) {
|
||||||
|
noStackCheck = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
willDieFromSpell = !noStackCheck && ComputerUtil.predictThreatenedObjects(creature.getController(), excludeSa).contains(creature);
|
||||||
|
|
||||||
|
return willDieInCombat || willDieFromSpell;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of cards excluding any creatures that will die in active combat or from a spell on stack.
|
||||||
|
* Works only on AI profiles which have AVOID_TARGETING_CREATS_THAT_WILL_DIE enabled, otherwise returns
|
||||||
|
* the original list.
|
||||||
|
*
|
||||||
|
* @param ai
|
||||||
|
* The AI player performing this evaluation
|
||||||
|
* @param list
|
||||||
|
* The list of cards to work with
|
||||||
|
* @return a filtered list with no dying creatures in it
|
||||||
|
*/
|
||||||
|
public static CardCollection filterCreaturesThatWillDieThisTurn(final Player ai, final CardCollection list, final SpellAbility excludeSa) {
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
if (aic.getBooleanProperty(AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) {
|
||||||
|
// Try to avoid targeting creatures that are dead on board
|
||||||
|
List<Card> willBeKilled = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return card.isCreature() && ComputerUtil.predictCreatureWillDieThisTurn(ai, card, excludeSa);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list.removeAll(willBeKilled);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean playImmediately(Player ai, SpellAbility sa) {
|
public static boolean playImmediately(Player ai, SpellAbility sa) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Zone zone = source.getZone();
|
final Zone zone = source.getZone();
|
||||||
@@ -1790,12 +1932,19 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static int scoreHand(CardCollectionView handList, Player ai) {
|
public static int scoreHand(CardCollectionView handList, Player ai, int cardsToReturn) {
|
||||||
|
// TODO Improve hand scoring in relation to cards to return.
|
||||||
|
// If final hand size is 5, score a hand based on what that 5 would be.
|
||||||
|
// Or if this is really really fast, determine what the 5 would be based on scoring
|
||||||
|
// All of the possibilities
|
||||||
|
|
||||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
int currentHandSize = handList.size();
|
||||||
|
int finalHandSize = currentHandSize - cardsToReturn;
|
||||||
|
|
||||||
// don't mulligan when already too low
|
// don't mulligan when already too low
|
||||||
if (handList.size() < aic.getIntProperty(AiProps.MULLIGAN_THRESHOLD)) {
|
if (finalHandSize < aic.getIntProperty(AiProps.MULLIGAN_THRESHOLD)) {
|
||||||
return handList.size();
|
return finalHandSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollectionView library = ai.getZone(ZoneType.Library).getCards();
|
CardCollectionView library = ai.getZone(ZoneType.Library).getCards();
|
||||||
@@ -1803,7 +1952,7 @@ public class ComputerUtil {
|
|||||||
|
|
||||||
// no land deck, can't do anything better
|
// no land deck, can't do anything better
|
||||||
if (landsInDeck == 0) {
|
if (landsInDeck == 0) {
|
||||||
return handList.size();
|
return finalHandSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CardCollectionView lands = CardLists.filter(handList, new Predicate<Card>() {
|
final CardCollectionView lands = CardLists.filter(handList, new Predicate<Card>() {
|
||||||
@@ -1871,9 +2020,9 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Computer mulligans if there are no cards with converted mana cost of 0 in its hand
|
// Computer mulligans if there are no cards with converted mana cost of 0 in its hand
|
||||||
public static boolean wantMulligan(Player ai) {
|
public static boolean wantMulligan(Player ai, int cardsToReturn) {
|
||||||
final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand);
|
final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand);
|
||||||
return scoreHand(handList, ai) <= 0;
|
return scoreHand(handList, ai, cardsToReturn) <= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CardCollection getPartialParisCandidates(Player ai) {
|
public static CardCollection getPartialParisCandidates(Player ai) {
|
||||||
@@ -2708,7 +2857,7 @@ public class ComputerUtil {
|
|||||||
repParams.put("Source", source);
|
repParams.put("Source", source);
|
||||||
|
|
||||||
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
|
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
|
||||||
ReplacementLayer.None);
|
ReplacementLayer.Other);
|
||||||
|
|
||||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2739,7 +2888,7 @@ public class ComputerUtil {
|
|||||||
repParams.put("Source", source);
|
repParams.put("Source", source);
|
||||||
|
|
||||||
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
|
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
|
||||||
ReplacementLayer.None);
|
ReplacementLayer.Other);
|
||||||
|
|
||||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
||||||
// no life gain is not negative
|
// no life gain is not negative
|
||||||
@@ -2769,6 +2918,10 @@ public class ComputerUtil {
|
|||||||
if (ab.getApi() == null) {
|
if (ab.getApi() == null) {
|
||||||
// only API-based SAs are supported, other things may lead to a NPE (e.g. Ancestral Vision Suspend SA)
|
// only API-based SAs are supported, other things may lead to a NPE (e.g. Ancestral Vision Suspend SA)
|
||||||
continue;
|
continue;
|
||||||
|
} else if (ab.getApi() == ApiType.Mana && "ManaRitual".equals(ab.getParam("AILogic"))) {
|
||||||
|
// Mana Ritual cards are too complex for the AI to consider casting through a spell effect and will
|
||||||
|
// lead to a stack overflow. Consider improving.
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
SpellAbility abTest = withoutPayingManaCost ? ab.copyWithNoManaCost() : ab.copy();
|
SpellAbility abTest = withoutPayingManaCost ? ab.copyWithNoManaCost() : ab.copy();
|
||||||
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
|
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
|
||||||
@@ -2845,15 +2998,24 @@ public class ComputerUtil {
|
|||||||
|
|
||||||
if (sa.hasParam("AITgts")) {
|
if (sa.hasParam("AITgts")) {
|
||||||
CardCollection list;
|
CardCollection list;
|
||||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
String aiTgts = sa.getParam("AITgts");
|
||||||
int value = ComputerUtilCard.evaluateCreature(source);
|
if (aiTgts.startsWith("BetterThan")) {
|
||||||
|
int value = 0;
|
||||||
|
if (aiTgts.endsWith("Source")) {
|
||||||
|
value = ComputerUtilCard.evaluateCreature(source);
|
||||||
if (source.isEnchanted()) {
|
if (source.isEnchanted()) {
|
||||||
for (Card enc : source.getEnchantedBy(false)) {
|
for (Card enc : source.getEnchantedBy()) {
|
||||||
if (enc.getController().equals(ai)) {
|
if (enc.getController().equals(ai)) {
|
||||||
value += 100; // is 100 per AI's own aura enough?
|
value += 100; // is 100 per AI's own aura enough?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (aiTgts.contains("EvalRating.")) {
|
||||||
|
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||||
|
} else {
|
||||||
|
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||||
|
value = ComputerUtilCard.evaluateCreature(source);
|
||||||
|
}
|
||||||
final int totalValue = value;
|
final int totalValue = value;
|
||||||
list = CardLists.filter(srcList, new Predicate<Card>() {
|
list = CardLists.filter(srcList, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameActionUtil;
|
import forge.game.GameActionUtil;
|
||||||
@@ -16,10 +12,14 @@ import forge.game.card.CardCollectionView;
|
|||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.OptionalCostValue;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ComputerUtilAbility {
|
public class ComputerUtilAbility {
|
||||||
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
||||||
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
|
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
|
||||||
@@ -95,18 +95,58 @@ public class ComputerUtilAbility {
|
|||||||
|
|
||||||
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
|
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
|
||||||
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
||||||
|
|
||||||
|
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
|
||||||
for (SpellAbility sa : originList) {
|
for (SpellAbility sa : originList) {
|
||||||
|
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player);
|
||||||
//add alternative costs as additional spell abilities
|
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 (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);
|
||||||
|
} else {
|
||||||
|
otherAltSa.add(altSa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add alternative costs as additional spell abilities
|
||||||
|
newAbilities.addAll(priorityAltSa);
|
||||||
newAbilities.add(sa);
|
newAbilities.add(sa);
|
||||||
newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
|
newAbilities.addAll(otherAltSa);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<SpellAbility> result = Lists.newArrayList();
|
final List<SpellAbility> result = Lists.newArrayList();
|
||||||
for (SpellAbility sa : newAbilities) {
|
for (SpellAbility sa : newAbilities) {
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player);
|
||||||
result.addAll(GameActionUtil.getOptionalCosts(sa));
|
|
||||||
|
// Optional cost selection through the AI controller
|
||||||
|
boolean choseOptCost = false;
|
||||||
|
List<OptionalCostValue> list = GameActionUtil.getOptionalCostValues(sa);
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
list = player.getController().chooseOptionalCosts(sa, list);
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
choseOptCost = true;
|
||||||
|
result.add(GameActionUtil.addOptionalCosts(sa, list));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add only one ability: either the one with preferred optional costs, or the original one if there are none
|
||||||
|
if (!choseOptCost) {
|
||||||
|
result.add(sa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +168,17 @@ public class ComputerUtilAbility {
|
|||||||
return tgtSA;
|
return tgtSA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SpellAbility getFirstCopySASpell(List<SpellAbility> spells) {
|
||||||
|
SpellAbility sa = null;
|
||||||
|
for (SpellAbility spell : spells) {
|
||||||
|
if (spell.getApi() == ApiType.CopySpellAbility) {
|
||||||
|
sa = spell;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sa;
|
||||||
|
}
|
||||||
|
|
||||||
public static Card getAbilitySource(SpellAbility sa) {
|
public static Card getAbilitySource(SpellAbility sa) {
|
||||||
return sa.getOriginalHost() != null ? sa.getOriginalHost() : sa.getHostCard();
|
return sa.getOriginalHost() != null ? sa.getOriginalHost() : sa.getHostCard();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import forge.game.ability.ApiType;
|
|||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostPayEnergy;
|
import forge.game.cost.CostPayEnergy;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.keyword.KeywordCollection;
|
import forge.game.keyword.KeywordCollection;
|
||||||
@@ -520,7 +521,7 @@ public class ComputerUtilCard {
|
|||||||
*/
|
*/
|
||||||
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
|
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
|
||||||
AiBlockController aiBlk = new AiBlockController(ai);
|
AiBlockController aiBlk = new AiBlockController(ai);
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
Combat combat = new Combat(opp);
|
Combat combat = new Combat(opp);
|
||||||
//Use actual attackers if available, else consider all possible attackers
|
//Use actual attackers if available, else consider all possible attackers
|
||||||
Combat currentCombat = ai.getGame().getCombat();
|
Combat currentCombat = ai.getGame().getCombat();
|
||||||
@@ -585,7 +586,7 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
// Add all cost of all auras with the same controller
|
// Add all cost of all auras with the same controller
|
||||||
if (card.isEnchanted()) {
|
if (card.isEnchanted()) {
|
||||||
final List<Card> auras = CardLists.filterControlledBy(card.getEnchantedBy(false), card.getController());
|
final List<Card> auras = CardLists.filterControlledBy(card.getEnchantedBy(), card.getController());
|
||||||
curCMC += Aggregates.sum(auras, CardPredicates.Accessors.fnGetCmc) + auras.size();
|
curCMC += Aggregates.sum(auras, CardPredicates.Accessors.fnGetCmc) + auras.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,7 +834,7 @@ public class ComputerUtilCard {
|
|||||||
int score = tmp.isTapped() ? 2 : 0;
|
int score = tmp.isTapped() ? 2 : 0;
|
||||||
score += tmp.isBasicLand() ? 1 : 0;
|
score += tmp.isBasicLand() ? 1 : 0;
|
||||||
score -= tmp.isCreature() ? 4 : 0;
|
score -= tmp.isCreature() ? 4 : 0;
|
||||||
for (Card aura : tmp.getEnchantedBy(false)) {
|
for (Card aura : tmp.getEnchantedBy()) {
|
||||||
if (aura.getController().isOpponentOf(tmp.getController())) {
|
if (aura.getController().isOpponentOf(tmp.getController())) {
|
||||||
score += 5;
|
score += 5;
|
||||||
} else {
|
} else {
|
||||||
@@ -857,7 +858,7 @@ public class ComputerUtilCard {
|
|||||||
int score = tmp.isTapped() ? 0 : 2;
|
int score = tmp.isTapped() ? 0 : 2;
|
||||||
score += tmp.isBasicLand() ? 2 : 0;
|
score += tmp.isBasicLand() ? 2 : 0;
|
||||||
score -= tmp.isCreature() ? 4 : 0;
|
score -= tmp.isCreature() ? 4 : 0;
|
||||||
score -= 5 * tmp.getEnchantedBy(false).size();
|
score -= 5 * tmp.getEnchantedBy().size();
|
||||||
|
|
||||||
if (score >= maxScore) {
|
if (score >= maxScore) {
|
||||||
land = tmp;
|
land = tmp;
|
||||||
@@ -883,7 +884,7 @@ public class ComputerUtilCard {
|
|||||||
List<String> chosen = new ArrayList<String>();
|
List<String> chosen = new ArrayList<String>();
|
||||||
Player ai = sa.getActivatingPlayer();
|
Player ai = sa.getActivatingPlayer();
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
if (sa.hasParam("AILogic")) {
|
if (sa.hasParam("AILogic")) {
|
||||||
final String logic = sa.getParam("AILogic");
|
final String logic = sa.getParam("AILogic");
|
||||||
|
|
||||||
@@ -973,7 +974,7 @@ public class ComputerUtilCard {
|
|||||||
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
|
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
|
||||||
final Player ai = sa.getActivatingPlayer();
|
final Player ai = sa.getActivatingPlayer();
|
||||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final PhaseHandler ph = game.getPhaseHandler();
|
final PhaseHandler ph = game.getPhaseHandler();
|
||||||
final PhaseType phaseType = ph.getPhase();
|
final PhaseType phaseType = ph.getPhase();
|
||||||
@@ -1033,7 +1034,7 @@ public class ComputerUtilCard {
|
|||||||
// interrupt 3: two for one = good
|
// interrupt 3: two for one = good
|
||||||
if (c.isEnchanted()) {
|
if (c.isEnchanted()) {
|
||||||
boolean myEnchants = false;
|
boolean myEnchants = false;
|
||||||
for (Card enc : c.getEnchantedBy(false)) {
|
for (Card enc : c.getEnchantedBy()) {
|
||||||
if (enc.getOwner().equals(ai)) {
|
if (enc.getOwner().equals(ai)) {
|
||||||
myEnchants = true;
|
myEnchants = true;
|
||||||
break;
|
break;
|
||||||
@@ -1268,7 +1269,7 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
|
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
|
||||||
List<Card> oppCreatures = opp.getCreaturesInPlay();
|
List<Card> oppCreatures = opp.getCreaturesInPlay();
|
||||||
float chance = 0;
|
float chance = 0;
|
||||||
@@ -1311,11 +1312,23 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
//2. grant haste
|
//2. grant haste
|
||||||
if (keywords.contains("Haste") && c.hasSickness() && !c.isTapped()) {
|
if (keywords.contains("Haste") && c.hasSickness() && !c.isTapped()) {
|
||||||
chance += 0.5f;
|
double nonCombatChance = 0.0f;
|
||||||
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
double combatChance = 0.0f;
|
||||||
chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
|
// non-combat Haste: has an activated ability with tap cost
|
||||||
|
for (SpellAbility ab : c.getSpellAbilities()) {
|
||||||
|
Cost abCost = ab.getPayCosts();
|
||||||
|
if (abCost != null && abCost.hasTapCost()
|
||||||
|
&& (!abCost.hasManaCost() || ComputerUtilMana.canPayManaCost(ab, ai, 0))) {
|
||||||
|
nonCombatChance += 0.5f;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// combat Haste: only grant it if the creature will attack
|
||||||
|
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
||||||
|
combatChance += 0.5f + (0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife());
|
||||||
|
}
|
||||||
|
chance += nonCombatChance + combatChance;
|
||||||
|
}
|
||||||
|
|
||||||
//3. grant evasive
|
//3. grant evasive
|
||||||
if (!CardLists.filter(oppCreatures, CardPredicates.possibleBlockers(c)).isEmpty()) {
|
if (!CardLists.filter(oppCreatures, CardPredicates.possibleBlockers(c)).isEmpty()) {
|
||||||
@@ -1480,6 +1493,14 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("UntapCombatTrick".equals(sa.getParam("AILogic")) && c.isTapped()) {
|
||||||
|
if (phase.is(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.getPlayerTurn().isOpponentOf(ai)) {
|
||||||
|
chance += 0.5f; // this creature will untap to become a potential blocker
|
||||||
|
} else if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS, ai)) {
|
||||||
|
chance += 1.0f; // untap after tapping for attack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isBerserk) {
|
if (isBerserk) {
|
||||||
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
|
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
|
||||||
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
|
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
|
||||||
@@ -1492,7 +1513,7 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean wantToHoldTrick = holdCombatTricks;
|
boolean wantToHoldTrick = holdCombatTricks && !ai.getCardsIn(ZoneType.Hand).isEmpty();
|
||||||
if (chanceToHoldCombatTricks >= 0) {
|
if (chanceToHoldCombatTricks >= 0) {
|
||||||
// Obey the chance specified in the AI profile for holding combat tricks
|
// Obey the chance specified in the AI profile for holding combat tricks
|
||||||
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
|
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
|
||||||
@@ -1508,14 +1529,18 @@ public class ComputerUtilCard {
|
|||||||
// Attempt to hold combat tricks until blockers are declared, and try to lure the opponent into blocking
|
// Attempt to hold combat tricks until blockers are declared, and try to lure the opponent into blocking
|
||||||
// (The AI will only do it for one attacker at the moment, otherwise it risks running his attackers into
|
// (The AI will only do it for one attacker at the moment, otherwise it risks running his attackers into
|
||||||
// an army of opposing blockers with only one combat trick in hand)
|
// an army of opposing blockers with only one combat trick in hand)
|
||||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
|
||||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
|
|
||||||
// Reserve the mana until Declare Blockers such that the AI doesn't tap out before having a chance to use
|
// Reserve the mana until Declare Blockers such that the AI doesn't tap out before having a chance to use
|
||||||
// the combat trick
|
// the combat trick
|
||||||
|
boolean reserved = false;
|
||||||
if (ai.getController().isAI()) {
|
if (ai.getController().isAI()) {
|
||||||
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, false);
|
reserved = ((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, false);
|
||||||
}
|
// Only proceed with this if we could actually reserve mana
|
||||||
|
if (reserved) {
|
||||||
|
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||||
|
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Don't try to mix "lure" and "precast" paradigms for combat tricks, since that creates issues with
|
// Don't try to mix "lure" and "precast" paradigms for combat tricks, since that creates issues with
|
||||||
// the AI overextending the attack
|
// the AI overextending the attack
|
||||||
@@ -1576,7 +1601,7 @@ public class ComputerUtilCard {
|
|||||||
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
|
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
|
||||||
Set<CounterType> types = c.getCounters().keySet();
|
Set<CounterType> types = c.getCounters().keySet();
|
||||||
for(CounterType ct : types) {
|
for(CounterType ct : types) {
|
||||||
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true);
|
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true, null);
|
||||||
}
|
}
|
||||||
//Copies tap-state and extra keywords (auras, equipment, etc.)
|
//Copies tap-state and extra keywords (auras, equipment, etc.)
|
||||||
if (c.isTapped()) {
|
if (c.isTapped()) {
|
||||||
@@ -1616,6 +1641,7 @@ public class ComputerUtilCard {
|
|||||||
if (exclude != null) {
|
if (exclude != null) {
|
||||||
list.removeAll(exclude);
|
list.removeAll(exclude);
|
||||||
}
|
}
|
||||||
|
list.add(vCard); // account for the static abilities that may be present on the card itself
|
||||||
for (final Card c : list) {
|
for (final Card c : list) {
|
||||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||||
final Map<String, String> params = stAb.getMapParams();
|
final Map<String, String> params = stAb.getMapParams();
|
||||||
@@ -1785,18 +1811,23 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
CardCollection priorityCards = new CardCollection();
|
CardCollection priorityCards = new CardCollection();
|
||||||
for (Card atk : oppCards) {
|
for (Card atk : oppCards) {
|
||||||
|
boolean canBeBlocked = false;
|
||||||
if (isUselessCreature(atk.getController(), atk)) {
|
if (isUselessCreature(atk.getController(), atk)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (Card blk : aiCreats) {
|
for (Card blk : aiCreats) {
|
||||||
if (!CombatUtil.canBlock(atk, blk, true)) {
|
if (CombatUtil.canBlock(atk, blk, true)) {
|
||||||
|
canBeBlocked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!canBeBlocked) {
|
||||||
boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
|
boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
|
||||||
if (!priorityRemovalOnlyInDanger || threat) {
|
if (!priorityRemovalOnlyInDanger || threat) {
|
||||||
priorityCards.add(atk);
|
priorityCards.add(atk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!priorityCards.isEmpty() && priorityCards.size() <= priorityRemovalThreshold) {
|
if (!priorityCards.isEmpty() && priorityCards.size() <= priorityRemovalThreshold) {
|
||||||
return priorityCards;
|
return priorityCards;
|
||||||
@@ -1811,8 +1842,45 @@ public class ComputerUtilCard {
|
|||||||
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
|
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
|
||||||
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";
|
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)) {
|
if (card.hasSVar(needsToPlayName)) {
|
||||||
final String needsToPlay = card.getSVar(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 (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);
|
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
|
||||||
|
|
||||||
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
|
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
|
||||||
@@ -1844,4 +1912,13 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
return AiPlayDecision.WillPlay;
|
return AiPlayDecision.WillPlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if the AI has an AI:RemoveDeck:All or an AI:RemoveDeck:Random hint specified.
|
||||||
|
// Includes a NPE guard on getRules() which might otherwise be tripped on some cards (e.g. tokens).
|
||||||
|
public static boolean isCardRemAIDeck(final Card card) {
|
||||||
|
return card.getRules() != null && card.getRules().getAiHints().getRemAIDecks();
|
||||||
|
}
|
||||||
|
public static boolean isCardRemRandomDeck(final Card card) {
|
||||||
|
return card.getRules() != null && card.getRules().getAiHints().getRemRandomDecks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -769,6 +769,10 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
|
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
|
||||||
Combat combat) {
|
Combat combat) {
|
||||||
|
return combatTriggerWillTrigger(attacker, defender, trigger, combat, null);
|
||||||
|
}
|
||||||
|
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
|
||||||
|
Combat combat, final List<Card> plannedAttackers) {
|
||||||
final Game game = attacker.getGame();
|
final Game game = attacker.getGame();
|
||||||
final Map<String, String> trigParams = trigger.getMapParams();
|
final Map<String, String> trigParams = trigger.getMapParams();
|
||||||
boolean willTrigger = false;
|
boolean willTrigger = false;
|
||||||
@@ -815,6 +819,9 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (trigParams.containsKey("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) {
|
||||||
|
return false; // won't trigger since the AI is planning to attack with more than one creature
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// defender == null means unblocked
|
// defender == null means unblocked
|
||||||
@@ -1057,6 +1064,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
||||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (pBonus > 0) {
|
if (pBonus > 0) {
|
||||||
@@ -1229,6 +1240,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
|
||||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (tBonus > 0) {
|
if (tBonus > 0) {
|
||||||
@@ -1447,6 +1462,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
||||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (pBonus > 0) {
|
if (pBonus > 0) {
|
||||||
@@ -1680,6 +1699,10 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
|
||||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
|
||||||
if (tBonus > 0) {
|
if (tBonus > 0) {
|
||||||
@@ -2104,6 +2127,16 @@ public class ComputerUtilCombat {
|
|||||||
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
|
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
|
||||||
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
|
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
|
||||||
|
|
||||||
|
// Damage prevention might come from a static effect
|
||||||
|
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
|
||||||
|
if (isCombatDamagePrevented(attacker, blocker, attackerDamage)) {
|
||||||
|
attackerDamage = 0;
|
||||||
|
}
|
||||||
|
if (isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
|
||||||
|
defenderDamage = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (combat != null) {
|
if (combat != null) {
|
||||||
for (Card atkr : combat.getAttackersBlockedBy(blocker)) {
|
for (Card atkr : combat.getAttackersBlockedBy(blocker)) {
|
||||||
if (!atkr.equals(attacker)) {
|
if (!atkr.equals(attacker)) {
|
||||||
@@ -2550,7 +2583,7 @@ public class ComputerUtilCombat {
|
|||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isCombatDamagePrevented(final Card attacker, final GameEntity target, final int damage) {
|
public static boolean isCombatDamagePrevented(final Card attacker, final GameEntity target, final int damage) {
|
||||||
final Game game = attacker.getGame();
|
final Game game = attacker.getGame();
|
||||||
|
|
||||||
// first try to replace the damage
|
// first try to replace the damage
|
||||||
@@ -2564,7 +2597,7 @@ public class ComputerUtilCombat {
|
|||||||
// repParams.put("PreventMap", preventMap);
|
// repParams.put("PreventMap", preventMap);
|
||||||
|
|
||||||
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams,
|
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams,
|
||||||
ReplacementLayer.None);
|
ReplacementLayer.Other);
|
||||||
|
|
||||||
return !list.isEmpty();
|
return !list.isEmpty();
|
||||||
}
|
}
|
||||||
@@ -2655,6 +2688,30 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
return attackerAfterTrigs;
|
return attackerAfterTrigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean willKillAtLeastOne(final Player ai, final Card c, final Combat combat) {
|
||||||
|
// This method detects if the attacking or blocking group the card "c" belongs to will kill
|
||||||
|
// at least one creature it's in combat with (either profitably or as a trade),
|
||||||
|
if (combat == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combat.isBlocked(c)) {
|
||||||
|
for (Card blk : combat.getBlockers(c)) {
|
||||||
|
if (ComputerUtilCombat.blockerWouldBeDestroyed(ai, blk, combat)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (combat.isBlocking(c)) {
|
||||||
|
for (Card atk : combat.getAttackersBlockedBy(c)) {
|
||||||
|
if (ComputerUtilCombat.attackerWouldBeDestroyed(ai, atk, combat)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import com.google.common.collect.Lists;
|
|||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import forge.ai.ability.AnimateAi;
|
import forge.ai.ability.AnimateAi;
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardFactoryUtil;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.*;
|
import forge.game.cost.*;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.Spell;
|
import forge.game.spellability.Spell;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -77,7 +75,7 @@ public class ComputerUtilCost {
|
|||||||
|
|
||||||
final CounterType type = remCounter.counter;
|
final CounterType type = remCounter.counter;
|
||||||
if (!part.payCostFromSource()) {
|
if (!part.payCostFromSource()) {
|
||||||
if (type.name().equals("P1P1")) {
|
if (CounterType.P1P1.equals(type)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -92,7 +90,7 @@ public class ComputerUtilCost {
|
|||||||
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
|
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
|
||||||
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
|
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
|
||||||
final String sVar = sa.getSVar(remCounter.getAmount());
|
final String sVar = sa.getSVar(remCounter.getAmount());
|
||||||
if (sVar.equals("XChoice")) {
|
if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) {
|
||||||
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
|
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,9 +106,19 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//don't kill the creature
|
//don't kill the creature
|
||||||
if (type.name().equals("P1P1") && source.getLethalDamage() <= 1) {
|
if (CounterType.P1P1.equals(type) && source.getLethalDamage() <= 1
|
||||||
|
&& !source.hasKeyword(Keyword.UNDYING)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (part instanceof CostRemoveAnyCounter) {
|
||||||
|
if (sa != null) {
|
||||||
|
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
|
||||||
|
|
||||||
|
PaymentDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa).visit(remCounter);
|
||||||
|
return decision != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -145,6 +153,7 @@ public class ComputerUtilCost {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
|
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
|
||||||
|
|
||||||
for (int i = 0; i < num; i++) {
|
for (int i = 0; i < num; i++) {
|
||||||
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
|
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
|
||||||
if (pref == null) {
|
if (pref == null) {
|
||||||
@@ -300,7 +309,7 @@ public class ComputerUtilCost {
|
|||||||
if (!important) {
|
if (!important) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!CardLists.filterControlledBy(source.getEnchantedBy(false), source.getController()).isEmpty()) {
|
if (!CardLists.filterControlledBy(source.getEnchantedBy(), source.getController()).isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -481,7 +490,7 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try not to lose Planeswalker if not threatened
|
// Try not to lose Planeswalker if not threatened
|
||||||
if (sa.getRestrictions().isPwAbility()) {
|
if (sa.isPwAbility()) {
|
||||||
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
if (part instanceof CostRemoveCounter) {
|
if (part instanceof CostRemoveCounter) {
|
||||||
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
|
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
|
||||||
@@ -495,6 +504,7 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KLD vehicle
|
// KLD vehicle
|
||||||
if (sa.hasParam("Crew")) { // put under checkTapTypeCost?
|
if (sa.hasParam("Crew")) { // put under checkTapTypeCost?
|
||||||
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
@@ -504,6 +514,36 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Alternate costs which involve both paying mana and tapping a card, e.g. Zahid, Djinn of the Lamp
|
||||||
|
// Current AI decides on each part separately, thus making it possible for the AI to cheat by
|
||||||
|
// tapping a mana source for mana and for the tap cost at the same time. Until this is improved, AI
|
||||||
|
// will not consider mana sources valid for paying the tap cost to avoid this exact situation.
|
||||||
|
if ("DontPayTapCostWithManaSources".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
|
||||||
|
for (final CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
|
if (part instanceof CostTapType) {
|
||||||
|
CardCollectionView nonManaSources =
|
||||||
|
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
|
||||||
|
sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||||
|
nonManaSources = CardLists.filter(nonManaSources, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
boolean hasManaSa = false;
|
||||||
|
for (final SpellAbility sa : card.getSpellAbilities()) {
|
||||||
|
if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
|
||||||
|
hasManaSa = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !hasManaSa;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (nonManaSources.size() < part.convertAmount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
|
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
|
||||||
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
|
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
|
||||||
} // canPayCost()
|
} // canPayCost()
|
||||||
@@ -595,7 +635,7 @@ public class ComputerUtilCost {
|
|||||||
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
||||||
&& (!source.getName().equals("Perplex") || 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("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
|
||||||
&& (!source.getName().equals("Chain of Vapor") || (ComputerUtil.getOpponentFor(payer).getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||||
@@ -620,4 +660,15 @@ public class ComputerUtilCost {
|
|||||||
|
|
||||||
return colorsAvailable;
|
return colorsAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isFreeCastAllowedByPermanent(Player player, String altCost) {
|
||||||
|
Game game = player.getGame();
|
||||||
|
for (Card cardInPlay : game.getCardsIn(ZoneType.Battlefield)) {
|
||||||
|
if (cardInPlay.hasSVar("AllowFreeCast")) {
|
||||||
|
return altCost == null ? "Always".equals(cardInPlay.getSVar("AllowFreeCast"))
|
||||||
|
: altCost.equals(cardInPlay.getSVar("AllowFreeCast"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.google.common.base.Predicate;
|
|||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.*;
|
import com.google.common.collect.*;
|
||||||
import forge.ai.ability.AnimateAi;
|
import forge.ai.ability.AnimateAi;
|
||||||
|
import forge.card.CardStateName;
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.card.mana.ManaAtom;
|
import forge.card.mana.ManaAtom;
|
||||||
@@ -16,11 +17,7 @@ import forge.game.ability.AbilityUtils;
|
|||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.*;
|
||||||
import forge.game.cost.CostAdjustment;
|
|
||||||
import forge.game.cost.CostPartMana;
|
|
||||||
import forge.game.cost.CostPayEnergy;
|
|
||||||
import forge.game.cost.CostPayment;
|
|
||||||
import forge.game.mana.Mana;
|
import forge.game.mana.Mana;
|
||||||
import forge.game.mana.ManaCostBeingPaid;
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
import forge.game.mana.ManaPool;
|
import forge.game.mana.ManaPool;
|
||||||
@@ -860,6 +857,11 @@ public class ComputerUtilMana {
|
|||||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
|
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
|
||||||
|
|
||||||
|
// Mana reserved for spell synchronization
|
||||||
|
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
||||||
|
|
||||||
// For combat tricks, always obey mana reservation
|
// For combat tricks, always obey mana reservation
|
||||||
@@ -913,6 +915,10 @@ public class ComputerUtilMana {
|
|||||||
// Make mana needed to avoid negative effect a mandatory cost for the AI
|
// Make mana needed to avoid negative effect a mandatory cost for the AI
|
||||||
for (String manaPart : card.getSVar("ManaNeededToAvoidNegativeEffect").split(",")) {
|
for (String manaPart : card.getSVar("ManaNeededToAvoidNegativeEffect").split(",")) {
|
||||||
// convert long color strings to short color strings
|
// convert long color strings to short color strings
|
||||||
|
if (manaPart.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
byte mask = ManaAtom.fromName(manaPart);
|
byte mask = ManaAtom.fromName(manaPart);
|
||||||
|
|
||||||
// make mana mandatory for AI
|
// make mana mandatory for AI
|
||||||
@@ -1116,11 +1122,11 @@ public class ComputerUtilMana {
|
|||||||
// For Count$xPaid set PayX in the AFs then use that here
|
// For Count$xPaid set PayX in the AFs then use that here
|
||||||
// Else calculate it as appropriate.
|
// Else calculate it as appropriate.
|
||||||
final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
|
final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
|
||||||
if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar)) {
|
if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar)) {
|
||||||
if (xSvar.equals("PayX") && card.hasSVar(xSvar)) {
|
if (xSvar.equals("PayX") && (card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar))) {
|
||||||
// X SVar may end up being an empty string when copying a spell with no cost (e.g. Jhoira Avatar)
|
// 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);
|
String xValue = card.hasSVar(xSvar) ? card.getSVar(xSvar) : card.getState(CardStateName.Original).getSVar(xSvar);
|
||||||
manaToAdd = xValue.isEmpty() ? 0 : Integer.parseInt(card.getSVar(xSvar)) * cost.getXcounter(); // X
|
manaToAdd = xValue.isEmpty() ? 0 : Integer.parseInt(xValue) * cost.getXcounter(); // X
|
||||||
} else {
|
} else {
|
||||||
manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter();
|
manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter();
|
||||||
}
|
}
|
||||||
@@ -1509,7 +1515,7 @@ public class ComputerUtilMana {
|
|||||||
final Card offering = sa.getSacrificedAsOffering();
|
final Card offering = sa.getSacrificedAsOffering();
|
||||||
offering.setUsedToPay(false);
|
offering.setUsedToPay(false);
|
||||||
if (costIsPaid && !test) {
|
if (costIsPaid && !test) {
|
||||||
sa.getHostCard().getController().getGame().getAction().sacrifice(offering, sa);
|
sa.getHostCard().getGame().getAction().sacrifice(offering, sa, null);
|
||||||
}
|
}
|
||||||
sa.resetSacrificedAsOffering();
|
sa.resetSacrificedAsOffering();
|
||||||
}
|
}
|
||||||
@@ -1517,7 +1523,7 @@ public class ComputerUtilMana {
|
|||||||
final Card emerge = sa.getSacrificedAsEmerge();
|
final Card emerge = sa.getSacrificedAsEmerge();
|
||||||
emerge.setUsedToPay(false);
|
emerge.setUsedToPay(false);
|
||||||
if (costIsPaid && !test) {
|
if (costIsPaid && !test) {
|
||||||
sa.getHostCard().getController().getGame().getAction().sacrifice(emerge, sa);
|
sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, null);
|
||||||
}
|
}
|
||||||
sa.resetSacrificedAsEmerge();
|
sa.resetSacrificedAsEmerge();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ public abstract class GameState {
|
|||||||
private final Map<Card, Integer> cardToEnchantPlayerId = new HashMap<>();
|
private final Map<Card, Integer> cardToEnchantPlayerId = new HashMap<>();
|
||||||
private final Map<Card, Integer> markedDamage = new HashMap<>();
|
private final Map<Card, Integer> markedDamage = new HashMap<>();
|
||||||
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
|
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
|
||||||
|
private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>();
|
||||||
private final Map<Card, String> cardToChosenType = 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>> cardToRememberedId = new HashMap<>();
|
||||||
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
|
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
|
||||||
@@ -98,6 +99,8 @@ public abstract class GameState {
|
|||||||
|
|
||||||
private int turn = 1;
|
private int turn = 1;
|
||||||
|
|
||||||
|
private boolean removeSummoningSickness = false;
|
||||||
|
|
||||||
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
|
// 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_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
|
||||||
private final int TARGET_HUMAN = -2;
|
private final int TARGET_HUMAN = -2;
|
||||||
@@ -196,9 +199,7 @@ public abstract class GameState {
|
|||||||
cardsReferencedByID.add(card.getExiledWith());
|
cardsReferencedByID.add(card.getExiledWith());
|
||||||
}
|
}
|
||||||
if (zone == ZoneType.Battlefield) {
|
if (zone == ZoneType.Battlefield) {
|
||||||
if (!card.getEnchantedBy(false).isEmpty()
|
if (!card.getAttachedCards().isEmpty()) {
|
||||||
|| !card.getEquippedBy(false).isEmpty()
|
|
||||||
|| !card.getFortifiedBy(false).isEmpty()) {
|
|
||||||
// Remember the ID of cards that have attachments
|
// Remember the ID of cards that have attachments
|
||||||
cardsReferencedByID.add(card);
|
cardsReferencedByID.add(card);
|
||||||
}
|
}
|
||||||
@@ -213,6 +214,10 @@ public abstract class GameState {
|
|||||||
// Remember the IDs of imprinted cards
|
// Remember the IDs of imprinted cards
|
||||||
cardsReferencedByID.add(i);
|
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)) {
|
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
|
||||||
// Remember the IDs of attacked planeswalkers
|
// Remember the IDs of attacked planeswalkers
|
||||||
GameEntity def = game.getCombat().getDefenderByAttacker(card);
|
GameEntity def = game.getCombat().getDefenderByAttacker(card);
|
||||||
@@ -290,17 +295,13 @@ public abstract class GameState {
|
|||||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||||
newText.append("|Meld");
|
newText.append("|Meld");
|
||||||
}
|
}
|
||||||
if (c.getEquipping() != null) {
|
if (c.isAttachedToEntity()) {
|
||||||
newText.append("|Attaching:").append(c.getEquipping().getId());
|
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
|
||||||
} else if (c.getFortifying() != null) {
|
|
||||||
newText.append("|Attaching:").append(c.getFortifying().getId());
|
|
||||||
} else if (c.getEnchantingCard() != null) {
|
|
||||||
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
|
|
||||||
}
|
}
|
||||||
if (c.getEnchantingPlayer() != null) {
|
if (c.getPlayerAttachedTo() != null) {
|
||||||
// TODO: improve this for game states with more than two players
|
// TODO: improve this for game states with more than two players
|
||||||
newText.append("|EnchantingPlayer:");
|
newText.append("|EnchantingPlayer:");
|
||||||
Player p = c.getEnchantingPlayer();
|
Player p = c.getPlayerAttachedTo();
|
||||||
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
|
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +319,17 @@ public abstract class GameState {
|
|||||||
newText.append("|NamedCard:").append(c.getNamedCard());
|
newText.append("|NamedCard:").append(c.getNamedCard());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
List<String> rememberedCardIds = Lists.newArrayList();
|
||||||
for (Object obj : c.getRemembered()) {
|
for (Object obj : c.getRemembered()) {
|
||||||
if (obj instanceof Card) {
|
if (obj instanceof Card) {
|
||||||
@@ -438,6 +450,10 @@ public abstract class GameState {
|
|||||||
turn = Integer.parseInt(categoryValue);
|
turn = Integer.parseInt(categoryValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (categoryName.equals("removesummoningsickness")) {
|
||||||
|
removeSummoningSickness = categoryValue.equalsIgnoreCase("true");
|
||||||
|
}
|
||||||
|
|
||||||
else if (categoryName.endsWith("life")) {
|
else if (categoryName.endsWith("life")) {
|
||||||
if (isHuman)
|
if (isHuman)
|
||||||
humanLife = Integer.parseInt(categoryValue);
|
humanLife = Integer.parseInt(categoryValue);
|
||||||
@@ -558,6 +574,7 @@ public abstract class GameState {
|
|||||||
cardToExiledWithId.clear();
|
cardToExiledWithId.clear();
|
||||||
markedDamage.clear();
|
markedDamage.clear();
|
||||||
cardToChosenClrs.clear();
|
cardToChosenClrs.clear();
|
||||||
|
cardToChosenCards.clear();
|
||||||
cardToChosenType.clear();
|
cardToChosenType.clear();
|
||||||
cardToScript.clear();
|
cardToScript.clear();
|
||||||
cardAttackMap.clear();
|
cardAttackMap.clear();
|
||||||
@@ -611,6 +628,12 @@ public abstract class GameState {
|
|||||||
game.getPhaseHandler().devAdvanceToPhase(advPhase);
|
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
|
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,31 +976,27 @@ public abstract class GameState {
|
|||||||
Card c = entry.getKey();
|
Card c = entry.getKey();
|
||||||
c.setNamedCard(entry.getValue());
|
c.setNamedCard(entry.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chosen cards
|
||||||
|
for (Entry<Card, CardCollection> entry : cardToChosenCards.entrySet()) {
|
||||||
|
Card c = entry.getKey();
|
||||||
|
c.setChosenCards(entry.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCardAttachments() {
|
private void handleCardAttachments() {
|
||||||
// Unattach all permanents first
|
// Unattach all permanents first
|
||||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||||
Card attachedTo = idToCard.get(entry.getValue());
|
Card attachedTo = idToCard.get(entry.getValue());
|
||||||
|
attachedTo.unAttachAllCards();
|
||||||
attachedTo.unEnchantAllCards();
|
|
||||||
attachedTo.unEquipAllCards();
|
|
||||||
for (Card c : attachedTo.getFortifiedBy(true)) {
|
|
||||||
attachedTo.unFortifyCard(c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach permanents by ID
|
// Attach permanents by ID
|
||||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||||
Card attachedTo = idToCard.get(entry.getValue());
|
Card attachedTo = idToCard.get(entry.getValue());
|
||||||
Card attacher = entry.getKey();
|
Card attacher = entry.getKey();
|
||||||
|
if (attacher.isAttachment()) {
|
||||||
if (attacher.isEquipment()) {
|
attacher.attachToEntity(attachedTo);
|
||||||
attacher.equipCard(attachedTo);
|
|
||||||
} else if (attacher.isAura()) {
|
|
||||||
attacher.enchantEntity(attachedTo);
|
|
||||||
} else if (attacher.isFortified()) {
|
|
||||||
attacher.fortifyCard(attachedTo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,7 +1007,7 @@ public abstract class GameState {
|
|||||||
Game game = attacher.getGame();
|
Game game = attacher.getGame();
|
||||||
Player attachedTo = entry.getValue() == TARGET_AI ? game.getPlayers().get(1) : game.getPlayers().get(0);
|
Player attachedTo = entry.getValue() == TARGET_AI ? game.getPlayers().get(1) : game.getPlayers().get(0);
|
||||||
|
|
||||||
attacher.enchantEntity(attachedTo);
|
attacher.attachToEntity(attachedTo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -997,13 +1016,19 @@ public abstract class GameState {
|
|||||||
String[] allCounterStrings = counterString.split(",");
|
String[] allCounterStrings = counterString.split(",");
|
||||||
for (final String counterPair : allCounterStrings) {
|
for (final String counterPair : allCounterStrings) {
|
||||||
String[] pair = counterPair.split("=", 2);
|
String[] pair = counterPair.split("=", 2);
|
||||||
entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false);
|
entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p, final int landsPlayed, final int landsPlayedLastTurn) {
|
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p, final int landsPlayed, final int landsPlayedLastTurn) {
|
||||||
// Lock check static as we setup player state
|
// Lock check static as we setup player state
|
||||||
|
|
||||||
|
// Clear all zones first, this ensures that any lingering cards and effects (e.g. in command zone) get cleared up
|
||||||
|
// before setting up a new state
|
||||||
|
for (ZoneType zt : ZONES.keySet()) {
|
||||||
|
p.getZone(zt).removeAllCards(true);
|
||||||
|
}
|
||||||
|
|
||||||
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
||||||
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
||||||
String value = kv.getValue();
|
String value = kv.getValue();
|
||||||
@@ -1037,7 +1062,9 @@ public abstract class GameState {
|
|||||||
if (c.isAura()) {
|
if (c.isAura()) {
|
||||||
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
|
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
|
||||||
// (will be overridden later, so the actual value shouldn't matter)
|
// (will be overridden later, so the actual value shouldn't matter)
|
||||||
c.setEnchanting(c);
|
|
||||||
|
//FIXME it shouldn't be able to attach itself
|
||||||
|
c.setEntityAttachedTo(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardsWithoutETBTrigs.contains(c)) {
|
if (cardsWithoutETBTrigs.contains(c)) {
|
||||||
@@ -1055,7 +1082,6 @@ public abstract class GameState {
|
|||||||
zone.setCards(kv.getValue());
|
zone.setCards(kv.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1116,7 +1142,7 @@ public abstract class GameState {
|
|||||||
} else if (info.startsWith("SummonSick")) {
|
} else if (info.startsWith("SummonSick")) {
|
||||||
c.setSickness(true);
|
c.setSickness(true);
|
||||||
} else if (info.startsWith("FaceDown")) {
|
} else if (info.startsWith("FaceDown")) {
|
||||||
c.setState(CardStateName.FaceDown, true);
|
c.turnFaceDown(true);
|
||||||
if (info.endsWith("Manifested")) {
|
if (info.endsWith("Manifested")) {
|
||||||
c.setManifested(true);
|
c.setManifested(true);
|
||||||
}
|
}
|
||||||
@@ -1134,7 +1160,7 @@ public abstract class GameState {
|
|||||||
} else if (info.startsWith("Id:")) {
|
} else if (info.startsWith("Id:")) {
|
||||||
int id = Integer.parseInt(info.substring(3));
|
int id = Integer.parseInt(info.substring(3));
|
||||||
idToCard.put(id, c);
|
idToCard.put(id, c);
|
||||||
} else if (info.startsWith("Attaching:")) {
|
} else if (info.startsWith("Attaching:") /*deprecated*/ || info.startsWith("AttachedTo:")) {
|
||||||
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
||||||
cardToAttachId.put(c, id);
|
cardToAttachId.put(c, id);
|
||||||
} else if (info.startsWith("EnchantingPlayer:")) {
|
} else if (info.startsWith("EnchantingPlayer:")) {
|
||||||
@@ -1151,6 +1177,13 @@ public abstract class GameState {
|
|||||||
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||||
} else if (info.startsWith("ChosenType:")) {
|
} else if (info.startsWith("ChosenType:")) {
|
||||||
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
|
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
|
||||||
|
} 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("NamedCard:")) {
|
} else if (info.startsWith("NamedCard:")) {
|
||||||
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
|
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
|
||||||
} else if (info.startsWith("ExecuteScript:")) {
|
} else if (info.startsWith("ExecuteScript:")) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import forge.game.card.CardPredicates.Presets;
|
|||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.*;
|
import forge.game.cost.*;
|
||||||
import forge.game.mana.Mana;
|
import forge.game.mana.Mana;
|
||||||
|
import forge.game.mana.ManaConversionMatrix;
|
||||||
import forge.game.mana.ManaCostBeingPaid;
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -90,7 +91,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PaperCard> sideboard(Deck deck, GameType gameType) {
|
public List<PaperCard> sideboard(Deck deck, GameType gameType, String message) {
|
||||||
// AI does not know how to sideboard
|
// AI does not know how to sideboard
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -161,10 +162,36 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
|
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
|
||||||
FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title,
|
FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
|
||||||
Player targetedPlayer) {
|
Player targetedPlayer) {
|
||||||
// this isn't used
|
if (delayedReveal != null) {
|
||||||
return null;
|
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
|
||||||
|
}
|
||||||
|
FCollection<T> remaining = new FCollection<T>(optionList);
|
||||||
|
List<T> selecteds = new ArrayList<T>();
|
||||||
|
T selected;
|
||||||
|
do {
|
||||||
|
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
|
||||||
|
if ( selected != null ) {
|
||||||
|
remaining.remove(selected);
|
||||||
|
selecteds.add(selected);
|
||||||
|
}
|
||||||
|
} while ( (selected != null ) && (selecteds.size() < max) );
|
||||||
|
return selecteds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2,
|
||||||
|
boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
|
||||||
|
if (delayedReveal != null) {
|
||||||
|
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
|
||||||
|
}
|
||||||
|
T selected1 = chooseSingleEntityForEffect(optionList1, null, sa, title, optional, targetedPlayer);
|
||||||
|
T selected2 = chooseSingleEntityForEffect(optionList2, null, sa, title, optional || selected1!=null, targetedPlayer);
|
||||||
|
List<T> selecteds = new ArrayList<T>();
|
||||||
|
if ( selected1 != null ) { selecteds.add(selected1); }
|
||||||
|
if ( selected2 != null ) { selecteds.add(selected2); }
|
||||||
|
return selecteds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -302,7 +329,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
|
|
||||||
// TODO: Currently this logic uses the same routine as Scry. Possibly differentiate this and implement
|
// TODO: Currently this logic uses the same routine as Scry. Possibly differentiate this and implement
|
||||||
// a specific logic for Surveil (e.g. maybe to interact better with Reanimator strategies etc.).
|
// a specific logic for Surveil (e.g. maybe to interact better with Reanimator strategies etc.).
|
||||||
if (getPlayer().getCardsIn(ZoneType.Hand).size() <= getAi().getIntProperty(AiProps.SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL)) {
|
if (getPlayer().getCardsIn(ZoneType.Library).size() <= getAi().getIntProperty(AiProps.SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL)) {
|
||||||
toTop.addAll(topN);
|
toTop.addAll(topN);
|
||||||
} else {
|
} else {
|
||||||
for (Card c : topN) {
|
for (Card c : topN) {
|
||||||
@@ -330,14 +357,17 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
|
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
|
||||||
//TODO Add more logic for AI ordering here
|
//TODO Add more logic for AI ordering here
|
||||||
|
|
||||||
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
|
if (cards.isEmpty()) {
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
if (destinationZone == ZoneType.Graveyard) {
|
if (destinationZone == ZoneType.Graveyard) {
|
||||||
|
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
|
||||||
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
|
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Card card) {
|
public boolean apply(Card card) {
|
||||||
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
|
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
|
||||||
return card.getName().equals("Volrath's Shapeshifter")
|
return card.getOriginalState(CardStateName.Original).getName().equals("Volrath's Shapeshifter");
|
||||||
|| card.getStates().contains(CardStateName.OriginalText) && card.getState(CardStateName.OriginalText).getName().equals("Volrath's Shapeshifter");
|
|
||||||
}
|
}
|
||||||
}).isEmpty()) {
|
}).isEmpty()) {
|
||||||
int bestValue = 0;
|
int bestValue = 0;
|
||||||
@@ -361,6 +391,61 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
return reordered;
|
return reordered;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (destinationZone == ZoneType.Library) {
|
||||||
|
// Ponder and similar cards
|
||||||
|
Player p = cards.getFirst().getController(); // whose library are we reordering?
|
||||||
|
CardCollection reordered = new CardCollection();
|
||||||
|
|
||||||
|
// Try to use the Scry logic to figure out what should be closer to the top and what should be closer to the bottom
|
||||||
|
CardCollection topLands = new CardCollection(), topNonLands = new CardCollection(), bottom = new CardCollection();
|
||||||
|
for (Card c : cards) {
|
||||||
|
if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(p, c)) {
|
||||||
|
bottom.add(c);
|
||||||
|
} else {
|
||||||
|
if (c.isLand()) {
|
||||||
|
topLands.add(c);
|
||||||
|
} else {
|
||||||
|
topNonLands.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
|
||||||
|
|
||||||
|
if (!p.isOpponentOf(player)) {
|
||||||
|
if (landsOTB <= 2) {
|
||||||
|
// too few lands, add all the lands from the "top" category first
|
||||||
|
reordered.addAll(topLands);
|
||||||
|
topLands.clear();
|
||||||
|
} else {
|
||||||
|
// we would have scried a land to top, so add one land from the "top" category if it's available there, but not more
|
||||||
|
if (!topLands.isEmpty()) {
|
||||||
|
Card first = topLands.getFirst();
|
||||||
|
reordered.add(first);
|
||||||
|
topLands.remove(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add everything that was deemed playable
|
||||||
|
reordered.addAll(topNonLands);
|
||||||
|
// then all the land extras that may be there
|
||||||
|
reordered.addAll(topLands);
|
||||||
|
// and then everything else that was deemed unplayable and thus scriable to the bottom
|
||||||
|
reordered.addAll(bottom);
|
||||||
|
} else {
|
||||||
|
// try to screw the opponent up as much as possible by placing the uncastables first
|
||||||
|
reordered.addAll(bottom);
|
||||||
|
if (landsOTB <= 5) {
|
||||||
|
reordered.addAll(topNonLands);
|
||||||
|
reordered.addAll(topLands);
|
||||||
|
} else {
|
||||||
|
reordered.addAll(topLands);
|
||||||
|
reordered.addAll(topNonLands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(reordered.size() == cards.size());
|
||||||
|
|
||||||
|
return reordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: return with the same order as was passed into this method
|
// Default: return with the same order as was passed into this method
|
||||||
@@ -451,15 +536,66 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
return brains.aiShouldRun(replacementEffect, effectSA);
|
return brains.aiShouldRun(replacementEffect, effectSA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
|
||||||
|
return !ComputerUtil.wantMulligan(player, cardsToReturn);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CardCollectionView getCardsToMulligan(Player firstPlayer) {
|
public CardCollectionView getCardsToMulligan(Player firstPlayer) {
|
||||||
if (!ComputerUtil.wantMulligan(player)) {
|
if (!ComputerUtil.wantMulligan(player, 0)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return player.getCardsIn(ZoneType.Hand);
|
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
|
@Override
|
||||||
public void declareAttackers(Player attacker, Combat combat) {
|
public void declareAttackers(Player attacker, Combat combat) {
|
||||||
brains.declareAttackers(attacker, combat);
|
brains.declareAttackers(attacker, combat);
|
||||||
@@ -867,11 +1003,6 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<GameEntity, CounterType> chooseProliferation(SpellAbility sa) {
|
|
||||||
return brains.chooseProliferation(sa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean chooseTargetsFor(SpellAbility currentAbility) {
|
public boolean chooseTargetsFor(SpellAbility currentAbility) {
|
||||||
return brains.doTrigger(currentAbility, true);
|
return brains.doTrigger(currentAbility, true);
|
||||||
@@ -921,7 +1052,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, boolean isActivatedSa) {
|
public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean isActivatedSa) {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
ManaCostBeingPaid cost = isActivatedSa ? ComputerUtilMana.calculateManaCost(sa, false, 0) : new ManaCostBeingPaid(toPay);
|
ManaCostBeingPaid cost = isActivatedSa ? ComputerUtilMana.calculateManaCost(sa, false, 0) : new ManaCostBeingPaid(toPay);
|
||||||
return ComputerUtilMana.payManaCost(cost, sa, player);
|
return ComputerUtilMana.payManaCost(cost, sa, player);
|
||||||
@@ -974,7 +1105,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
|
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
|
||||||
if (sa.hasParam("AILogic")) {
|
if (sa.hasParam("AILogic")) {
|
||||||
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
|
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
|
||||||
CardCollectionView oppLibrary = ComputerUtil.getOpponentFor(player).getCardsIn(ZoneType.Library);
|
CardCollectionView oppLibrary = player.getWeakestOpponent().getCardsIn(ZoneType.Library);
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String logic = sa.getParam("AILogic");
|
final String logic = sa.getParam("AILogic");
|
||||||
|
|
||||||
@@ -1030,7 +1161,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Card> chooseCardsForZoneChange(
|
public List<Card> chooseCardsForZoneChange(
|
||||||
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList,
|
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max,
|
||||||
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
|
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
|
||||||
// this isn't used
|
// this isn't used
|
||||||
return null;
|
return null;
|
||||||
@@ -1087,9 +1218,39 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility choosen,
|
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen,
|
||||||
List<OptionalCostValue> optionalCostValues) {
|
List<OptionalCostValue> optionalCostValues) {
|
||||||
// TODO Auto-generated method stub
|
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
|
||||||
return null;
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chosenOptCosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean confirmMulliganScry(Player p) {
|
||||||
|
// Always true?
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -501,6 +501,49 @@ 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.<String>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
|
// Guilty Conscience
|
||||||
public static class GuiltyConscience {
|
public static class GuiltyConscience {
|
||||||
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
|
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
|
||||||
@@ -677,6 +720,14 @@ public class SpecialCardAi {
|
|||||||
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
|
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
|
||||||
public static class LivingDeath {
|
public static class LivingDeath {
|
||||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||||
|
// if there's another reanimator card currently suspended, don't cast a new one until the previous
|
||||||
|
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
|
||||||
|
for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
|
||||||
|
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterType.TIME) > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int aiBattlefieldPower = 0, aiGraveyardPower = 0;
|
int aiBattlefieldPower = 0, aiGraveyardPower = 0;
|
||||||
int threshold = 320; // approximately a 4/4 Flying creature worth of extra value
|
int threshold = 320; // approximately a 4/4 Flying creature worth of extra value
|
||||||
|
|
||||||
@@ -821,6 +872,10 @@ public class SpecialCardAi {
|
|||||||
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
||||||
int maxHandSize = ai.getMaxHandSize();
|
int maxHandSize = ai.getMaxHandSize();
|
||||||
|
|
||||||
|
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||||
|
return false; // nothing to draw from the library
|
||||||
|
}
|
||||||
|
|
||||||
if (!CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain")).isEmpty()) {
|
if (!CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain")).isEmpty()) {
|
||||||
// Prefer Yawgmoth's Bargain because AI is generally better with it
|
// Prefer Yawgmoth's Bargain because AI is generally better with it
|
||||||
|
|
||||||
@@ -1071,6 +1126,44 @@ 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(CounterType.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
|
// Survival of the Fittest
|
||||||
public static class SurvivalOfTheFittest {
|
public static class SurvivalOfTheFittest {
|
||||||
public static Card considerDiscardTarget(final Player ai) {
|
public static Card considerDiscardTarget(final Player ai) {
|
||||||
@@ -1337,6 +1430,10 @@ public class SpecialCardAi {
|
|||||||
Game game = ai.getGame();
|
Game game = ai.getGame();
|
||||||
PhaseHandler ph = game.getPhaseHandler();
|
PhaseHandler ph = game.getPhaseHandler();
|
||||||
|
|
||||||
|
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||||
|
return false; // nothing to draw from the library
|
||||||
|
}
|
||||||
|
|
||||||
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
||||||
int maxHandSize = ai.getMaxHandSize();
|
int maxHandSize = ai.getMaxHandSize();
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ public abstract class SpellAbilityAi {
|
|||||||
return false;
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
|
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -169,7 +176,7 @@ public abstract class SpellAbilityAi {
|
|||||||
|
|
||||||
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory)
|
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory)
|
||||||
{
|
{
|
||||||
if (!doTriggerAINoCost(aiPlayer, sa, mandatory)) {
|
if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final AbilitySub subAb = sa.getSubAbility();
|
final AbilitySub subAb = sa.getSubAbility();
|
||||||
@@ -238,9 +245,9 @@ public abstract class SpellAbilityAi {
|
|||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
protected static boolean isSorcerySpeed(final SpellAbility sa) {
|
protected static boolean isSorcerySpeed(final SpellAbility sa) {
|
||||||
return (sa.isSpell() && sa.getHostCard().isSorcery())
|
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||||
|| (sa.isAbility() && sa.getRestrictions().isSorcerySpeed())
|
|| (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|
||||||
|| (sa.getRestrictions().isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
|
|| (sa.isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,7 +276,7 @@ public abstract class SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.getRestrictions().isPwAbility() && phase.is(PhaseType.MAIN2)) {
|
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (sa.isSpell() && !sa.isBuyBackAbility()) {
|
if (sa.isSpell() && !sa.isBuyBackAbility()) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class)
|
.put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class)
|
||||||
.put(ApiType.AddPhase, AddPhaseAi.class)
|
.put(ApiType.AddPhase, AddPhaseAi.class)
|
||||||
.put(ApiType.AddTurn, AddTurnAi.class)
|
.put(ApiType.AddTurn, AddTurnAi.class)
|
||||||
|
.put(ApiType.Amass, AmassAi.class)
|
||||||
.put(ApiType.Animate, AnimateAi.class)
|
.put(ApiType.Animate, AnimateAi.class)
|
||||||
.put(ApiType.AnimateAll, AnimateAllAi.class)
|
.put(ApiType.AnimateAll, AnimateAllAi.class)
|
||||||
.put(ApiType.Attach, AttachAi.class)
|
.put(ApiType.Attach, AttachAi.class)
|
||||||
@@ -34,6 +35,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.Branch, AlwaysPlayAi.class)
|
.put(ApiType.Branch, AlwaysPlayAi.class)
|
||||||
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
|
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
|
||||||
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
||||||
|
.put(ApiType.ChangeX, AlwaysPlayAi.class)
|
||||||
.put(ApiType.ChangeZone, ChangeZoneAi.class)
|
.put(ApiType.ChangeZone, ChangeZoneAi.class)
|
||||||
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
|
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
|
||||||
.put(ApiType.Charm, CharmAi.class)
|
.put(ApiType.Charm, CharmAi.class)
|
||||||
@@ -79,12 +81,14 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.FlipACoin, FlipACoinAi.class)
|
.put(ApiType.FlipACoin, FlipACoinAi.class)
|
||||||
.put(ApiType.Fog, FogAi.class)
|
.put(ApiType.Fog, FogAi.class)
|
||||||
.put(ApiType.GainControl, ControlGainAi.class)
|
.put(ApiType.GainControl, ControlGainAi.class)
|
||||||
|
.put(ApiType.GainControlVariant, AlwaysPlayAi.class)
|
||||||
.put(ApiType.GainLife, LifeGainAi.class)
|
.put(ApiType.GainLife, LifeGainAi.class)
|
||||||
.put(ApiType.GainOwnership, CannotPlayAi.class)
|
.put(ApiType.GainOwnership, CannotPlayAi.class)
|
||||||
.put(ApiType.GameDrawn, CannotPlayAi.class)
|
.put(ApiType.GameDrawn, CannotPlayAi.class)
|
||||||
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
|
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
|
||||||
.put(ApiType.Goad, GoadAi.class)
|
.put(ApiType.Goad, GoadAi.class)
|
||||||
.put(ApiType.Haunt, HauntAi.class)
|
.put(ApiType.Haunt, HauntAi.class)
|
||||||
|
.put(ApiType.ImmediateTrigger, AlwaysPlayAi.class)
|
||||||
.put(ApiType.LoseLife, LifeLoseAi.class)
|
.put(ApiType.LoseLife, LifeLoseAi.class)
|
||||||
.put(ApiType.LosesGame, GameLossAi.class)
|
.put(ApiType.LosesGame, GameLossAi.class)
|
||||||
.put(ApiType.Mana, ManaEffectAi.class)
|
.put(ApiType.Mana, ManaEffectAi.class)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -22,7 +21,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
|
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
|
||||||
@@ -46,7 +45,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
@@ -87,7 +86,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
sa.getTargets().add(ai.getWeakestOpponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
return randomReturn;
|
return randomReturn;
|
||||||
|
|||||||
102
forge-ai/src/main/java/forge/ai/ability/AmassAi.java
Normal file
102
forge-ai/src/main/java/forge/ai/ability/AmassAi.java
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
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.*;
|
||||||
|
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()) {
|
||||||
|
if (CardLists.count(aiArmies, CardPredicates.canReceiveCounters(CounterType.P1P1)) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final String tokenScript = "b_0_0_zombie_army";
|
||||||
|
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
|
||||||
|
|
||||||
|
Card token = TokenInfo.getProtoType(tokenScript, sa);
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
token.setController(ai, 0);
|
||||||
|
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||||
|
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
// need to check what the cards would be on the battlefield
|
||||||
|
// do not attach yet, that would cause Events
|
||||||
|
CardCollection preList = new CardCollection(token);
|
||||||
|
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
|
||||||
|
|
||||||
|
if (token.canReceiveCounters(CounterType.P1P1)) {
|
||||||
|
token.setCounters(CounterType.P1P1, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.isCreature() && token.getNetToughness() < 1) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//reset static abilities
|
||||||
|
game.getAction().checkStaticAbilities(false);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
Iterable<Card> better = CardLists.filter(options, CardPredicates.canReceiveCounters(CounterType.P1P1));
|
||||||
|
if (Iterables.isEmpty(better)) {
|
||||||
|
better = options;
|
||||||
|
}
|
||||||
|
return ComputerUtilCard.getBestAI(better);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import forge.game.ability.AbilityFactory;
|
|||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
|
import forge.game.cost.CostPutCounter;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -77,13 +78,13 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
num = (num == null) ? "1" : num;
|
num = (num == null) ? "1" : num;
|
||||||
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
||||||
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
||||||
ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), topStack);
|
ai.getWeakestOpponent(), topStack.getHostCard(), topStack);
|
||||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||||
ComputerUtilCard.sortByEvaluateCreature(list);
|
ComputerUtilCard.sortByEvaluateCreature(list);
|
||||||
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
|
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
|
||||||
Card animatedCopy = becomeAnimated(source, sa);
|
Card animatedCopy = becomeAnimated(source, sa);
|
||||||
list.add(animatedCopy);
|
list.add(animatedCopy);
|
||||||
list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(),
|
list = CardLists.getValidCards(list, valid.split(","), ai.getWeakestOpponent(), topStack.getHostCard(),
|
||||||
topStack);
|
topStack);
|
||||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||||
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
|
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
|
||||||
@@ -109,11 +110,16 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) {
|
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Don't animate if the AI won't attack anyway
|
// Don't animate if the AI won't attack anyway or use as a potential blocker
|
||||||
Player opponent = ai.getWeakestOpponent();
|
Player opponent = ai.getWeakestOpponent();
|
||||||
|
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
|
||||||
|
// 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
|
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
|
||||||
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
|
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
|
||||||
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent")) {
|
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -149,15 +155,15 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
|
if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
|
||||||
int power = -5;
|
int power = -5;
|
||||||
if (sa.hasParam("Power")) {
|
if (sa.hasParam("Power")) {
|
||||||
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
|
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||||
}
|
}
|
||||||
int toughness = -5;
|
int toughness = -5;
|
||||||
if (sa.hasParam("Toughness")) {
|
if (sa.hasParam("Toughness")) {
|
||||||
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
|
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||||
}
|
}
|
||||||
if (sa.hasParam("Keywords")) {
|
if (sa.hasParam("Keywords")) {
|
||||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||||
if (!source.hasKeyword(keyword)) {
|
if (!c.hasKeyword(keyword)) {
|
||||||
bFlag = true;
|
bFlag = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +194,7 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||||
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
|
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
|
||||||
if (!sa.getHostCard().isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(sa.getHostCard()))) {
|
if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
|
||||||
bFlag = true;
|
bFlag = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +251,11 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
private boolean animateTgtAI(final SpellAbility sa) {
|
private boolean animateTgtAI(final SpellAbility sa) {
|
||||||
final Player ai = sa.getActivatingPlayer();
|
final Player ai = sa.getActivatingPlayer();
|
||||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
|
final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker")
|
||||||
|
&& sa.getPayCosts() != null
|
||||||
|
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
|
||||||
|
&& sa.getTargetRestrictions() != null
|
||||||
|
&& sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0;
|
||||||
|
|
||||||
final CardType types = new CardType();
|
final CardType types = new CardType();
|
||||||
if (sa.hasParam("Types")) {
|
if (sa.hasParam("Types")) {
|
||||||
@@ -264,7 +275,7 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||||
|
|
||||||
// list is empty, no possible targets
|
// list is empty, no possible targets
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty() && !alwaysActivatePWAbility) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +328,7 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// data is empty, no good targets
|
// data is empty, no good targets
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty() && !alwaysActivatePWAbility) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +353,7 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||||
// two are the only things
|
// two are the only things
|
||||||
// that animate a target. Those can just use SVar:RemAIDeck:True until
|
// that animate a target. Those can just use AI:RemoveDeck:All until
|
||||||
// this can do a reasonably
|
// this can do a reasonably
|
||||||
// good job of picking a good target
|
// good job of picking a good target
|
||||||
return false;
|
return false;
|
||||||
@@ -366,10 +377,16 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
Integer power = null;
|
Integer power = null;
|
||||||
if (sa.hasParam("Power")) {
|
if (sa.hasParam("Power")) {
|
||||||
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
|
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
|
||||||
|
if (power == 0 && "PTByCMC".equals(sa.getParam("AILogic"))) {
|
||||||
|
power = card.getManaCost().getCMC();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Integer toughness = null;
|
Integer toughness = null;
|
||||||
if (sa.hasParam("Toughness")) {
|
if (sa.hasParam("Toughness")) {
|
||||||
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
|
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();
|
final CardType types = new CardType();
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ public class AnimateAllAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
|
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} // end animateAllCanPlayAI()
|
} // end animateAllCanPlayAI()
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
|
import forge.game.Game;
|
||||||
import forge.game.GameObject;
|
import forge.game.GameObject;
|
||||||
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
@@ -19,11 +23,13 @@ import forge.game.phase.PhaseType;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.spellability.SpellPermanent;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.staticability.StaticAbility;
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -56,6 +62,16 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)
|
||||||
|
&& source.getType().isLegendary() && sa instanceof SpellPermanent
|
||||||
|
&& ai.isCardInPlay(source.getName())) {
|
||||||
|
// Don't play the second copy of a legendary enchantment already in play
|
||||||
|
|
||||||
|
// TODO: Add some extra checks for where the AI may want to cast a replacement aura
|
||||||
|
// on another creature and keep it when the original enchanted creature is useless
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||||
&& !"Curse".equals(sa.getParam("AILogic"))) {
|
&& !"Curse".equals(sa.getParam("AILogic"))) {
|
||||||
return false;
|
return false;
|
||||||
@@ -75,6 +91,15 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flash logic
|
||||||
|
boolean advancedFlash = false;
|
||||||
|
if (ai.getController().isAI()) {
|
||||||
|
advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC);
|
||||||
|
}
|
||||||
|
if (source.withFlash(ai) && source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (abCost.getTotalMana().countX() > 0 && source.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
|
// Set PayX here to maximum value. (Endless Scream and Venarian
|
||||||
// Gold)
|
// Gold)
|
||||||
@@ -106,6 +131,122 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean doAdvancedFlashAuraLogic(Player ai, SpellAbility sa, Card attachTarget) {
|
||||||
|
Card source = sa.getHostCard();
|
||||||
|
Game game = ai.getGame();
|
||||||
|
Combat combat = game.getCombat();
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
|
||||||
|
if (!aic.getBooleanProperty(AiProps.FLASH_USE_BUFF_AURAS_AS_COMBAT_TRICKS)) {
|
||||||
|
// Currently this only works with buff auras, so if the relevant toggle is disabled, just return true
|
||||||
|
// for instant speed use. To be improved later.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int power = 0, toughness = 0;
|
||||||
|
List<String> keywords = Lists.newArrayList();
|
||||||
|
for (StaticAbility stAb : source.getStaticAbilities()) {
|
||||||
|
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||||
|
if (stAb.hasParam("AddPower")) {
|
||||||
|
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
|
||||||
|
}
|
||||||
|
if (stAb.hasParam("AddToughness")) {
|
||||||
|
toughness += AbilityUtils.calculateAmount(source, stAb.getParam("AddToughness"), stAb);
|
||||||
|
}
|
||||||
|
if (stAb.hasParam("AddKeyword")) {
|
||||||
|
keywords.addAll(Lists.newArrayList(stAb.getParam("AddKeyword").split(" & ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isBuffAura = !sa.isCurse() && (power > 0 || toughness > 0 || !keywords.isEmpty());
|
||||||
|
if (!isBuffAura) {
|
||||||
|
// Currently only works with buff auras, otherwise returns true for instant speed use. To be improved later.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canRespondToStack = false;
|
||||||
|
if (!game.getStack().isEmpty()) {
|
||||||
|
SpellAbility peekSa = game.getStack().peekAbility();
|
||||||
|
Player activator = peekSa.getActivatingPlayer();
|
||||||
|
if (activator != null && activator.isOpponentOf(ai)
|
||||||
|
&& (!peekSa.usesTargeting() || peekSa.getTargets().getTargetCards().contains(attachTarget))) {
|
||||||
|
if (peekSa.getApi() == ApiType.DealDamage || peekSa.getApi() == ApiType.DamageAll) {
|
||||||
|
int dmg = AbilityUtils.calculateAmount(peekSa.getHostCard(), peekSa.getParam("NumDmg"), peekSa);
|
||||||
|
if (dmg < toughness + attachTarget.getNetToughness()) {
|
||||||
|
canRespondToStack = true;
|
||||||
|
}
|
||||||
|
} else if (peekSa.getApi() == ApiType.Destroy || peekSa.getApi() == ApiType.DestroyAll) {
|
||||||
|
if (!attachTarget.hasKeyword(Keyword.INDESTRUCTIBLE) && !ComputerUtil.canRegenerate(ai, attachTarget)
|
||||||
|
&& keywords.contains("Indestructible")) {
|
||||||
|
canRespondToStack = true;
|
||||||
|
}
|
||||||
|
} else if (peekSa.getApi() == ApiType.Pump || peekSa.getApi() == ApiType.PumpAll) {
|
||||||
|
int p = AbilityUtils.calculateAmount(peekSa.getHostCard(), peekSa.getParam("NumAtt"), peekSa);
|
||||||
|
int t = AbilityUtils.calculateAmount(peekSa.getHostCard(), peekSa.getParam("NumDef"), peekSa);
|
||||||
|
if (t < 0 && toughness > 0 && attachTarget.getNetToughness() + t + toughness > 0) {
|
||||||
|
canRespondToStack = true;
|
||||||
|
} else if (p < 0 && power > 0 && attachTarget.getNetPower() + p + power > 0
|
||||||
|
&& attachTarget.getNetToughness() + t + toughness > 0) {
|
||||||
|
// Yep, still need to ensure that the net toughness will be positive here even if buffing for power
|
||||||
|
canRespondToStack = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canSurviveCombat = true;
|
||||||
|
if (combat != null && combat.isBlocked(attachTarget)) {
|
||||||
|
if (!attachTarget.hasKeyword(Keyword.INDESTRUCTIBLE) && !ComputerUtil.canRegenerate(ai, attachTarget)) {
|
||||||
|
boolean dangerous = false;
|
||||||
|
int totalAtkPower = 0;
|
||||||
|
for (Card attacker : combat.getBlockers(attachTarget)) {
|
||||||
|
if (attacker.hasKeyword(Keyword.DEATHTOUCH) || attacker.hasKeyword(Keyword.INFECT)
|
||||||
|
|| attacker.hasKeyword(Keyword.WITHER)) {
|
||||||
|
dangerous = true;
|
||||||
|
}
|
||||||
|
totalAtkPower += attacker.getNetPower();
|
||||||
|
}
|
||||||
|
if (totalAtkPower > attachTarget.getNetToughness() + toughness || dangerous) {
|
||||||
|
canSurviveCombat = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canSurviveCombat || (attachTarget.isCreature() && ComputerUtilCard.isUselessCreature(ai, attachTarget))) {
|
||||||
|
// don't buff anything that will die or get seriously crippled in combat, it's pointless anyway
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chanceToCastAtEOT = aic.getIntProperty(AiProps.FLASH_BUFF_AURA_CHANCE_CAST_AT_EOT);
|
||||||
|
int chanceToCastEarly = aic.getIntProperty(AiProps.FLASH_BUFF_AURA_CHANCE_TO_CAST_EARLY);
|
||||||
|
int chanceToRespondToStack = aic.getIntProperty(AiProps.FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK);
|
||||||
|
|
||||||
|
boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
|
||||||
|
boolean willDiscardNow = game.getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
|
||||||
|
&& ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
|
||||||
|
boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
|
||||||
|
boolean willRespondToStack = canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack);
|
||||||
|
boolean willCastEarly = MyRandom.percentTrue(chanceToCastEarly);
|
||||||
|
boolean willCastAtEOT = game.getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||||
|
&& game.getPhaseHandler().getNextTurn().equals(ai) && MyRandom.percentTrue(chanceToCastAtEOT);
|
||||||
|
|
||||||
|
boolean alternativeConsiderations = hasFloatMana || willDiscardNow || willDieNow || willRespondToStack || willCastAtEOT || willCastEarly;
|
||||||
|
|
||||||
|
if (!alternativeConsiderations) {
|
||||||
|
if (combat == null ||
|
||||||
|
game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(combat.isAttacking(attachTarget) || combat.isBlocking(attachTarget))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acceptable choice.
|
* Acceptable choice.
|
||||||
*
|
*
|
||||||
@@ -276,7 +417,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Iterable<Card> auras = c.getEnchantedBy(false);
|
final Iterable<Card> auras = c.getEnchantedBy();
|
||||||
final Iterator<Card> itr = auras.iterator();
|
final Iterator<Card> itr = auras.iterator();
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
final Card aura = itr.next();
|
final Card aura = itr.next();
|
||||||
@@ -465,8 +606,40 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
private static Card attachAIReanimatePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
private static Card attachAIReanimatePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
||||||
final Card attachSource) {
|
final Card attachSource) {
|
||||||
// AI For choosing a Card to Animate.
|
// AI For choosing a Card to Animate.
|
||||||
// TODO Add some more restrictions for Reanimation Auras
|
final Player ai = sa.getActivatingPlayer();
|
||||||
final Card c = ComputerUtilCard.getBestCreatureAI(list);
|
final Card attachSourceLki = CardUtil.getLKICopy(attachSource);
|
||||||
|
attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||||
|
// Suppress original attach Spell to replace it with another
|
||||||
|
attachSourceLki.getFirstAttachSpell().setSuppressed(true);
|
||||||
|
|
||||||
|
//TODO for Reanimate Auras i need the new Attach Spell, in later versions it might be part of the Enchant Keyword
|
||||||
|
attachSourceLki.addSpellAbility(AbilityFactory.getAbility(attachSourceLki, "NewAttach"));
|
||||||
|
List<Card> betterList = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(final Card c) {
|
||||||
|
final Card lki = CardUtil.getLKICopy(c);
|
||||||
|
// need to fake it as if lki would be on the battlefield
|
||||||
|
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||||
|
|
||||||
|
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
|
||||||
|
attachSourceLki.clearRemembered();
|
||||||
|
attachSourceLki.addRemembered(lki);
|
||||||
|
|
||||||
|
// need to check what the cards would be on the battlefield
|
||||||
|
// do not attach yet, that would cause Events
|
||||||
|
CardCollection preList = new CardCollection(lki);
|
||||||
|
preList.add(attachSourceLki);
|
||||||
|
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
|
||||||
|
boolean result = lki.canBeAttached(attachSourceLki);
|
||||||
|
|
||||||
|
//reset static abilities
|
||||||
|
c.getGame().getAction().checkStaticAbilities(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final Card c = ComputerUtilCard.getBestCreatureAI(betterList);
|
||||||
|
|
||||||
// If Mandatory (brought directly into play without casting) gotta
|
// If Mandatory (brought directly into play without casting) gotta
|
||||||
// choose something
|
// choose something
|
||||||
@@ -905,6 +1078,34 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look for triggers that will damage the creature and remove AI-owned creatures that will die
|
||||||
|
CardCollection toRemove = new CardCollection();
|
||||||
|
for (Trigger t : attachSource.getTriggers()) {
|
||||||
|
if (t.getMode() == TriggerType.ChangesZone) {
|
||||||
|
final Map<String, String> params = t.getMapParams();
|
||||||
|
if ("Card.Self".equals(params.get("ValidCard"))
|
||||||
|
&& "Battlefield".equals(params.get("Destination"))) {
|
||||||
|
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)) {
|
||||||
|
int numDmg = AbilityUtils.calculateAmount(target, trigSa.getParam("NumDmg"), trigSa);
|
||||||
|
if (target.getNetToughness() - target.getDamage() <= numDmg && !target.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
|
toRemove.add(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.removeAll(toRemove);
|
||||||
|
|
||||||
if (magnetList != null) {
|
if (magnetList != null) {
|
||||||
|
|
||||||
// Look for Heroic triggers
|
// Look for Heroic triggers
|
||||||
@@ -992,6 +1193,10 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CardCollection prefList = new CardCollection(list);
|
CardCollection prefList = new CardCollection(list);
|
||||||
|
|
||||||
|
// Filter AI-specific targets if provided
|
||||||
|
prefList = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||||
|
|
||||||
if (totToughness < 0) {
|
if (totToughness < 0) {
|
||||||
// Don't kill my own stuff with Negative toughness Auras
|
// Don't kill my own stuff with Negative toughness Auras
|
||||||
final int tgh = totToughness;
|
final int tgh = totToughness;
|
||||||
@@ -1111,8 +1316,15 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
if (attachSource.hasSVar("DontEquip")) {
|
if (attachSource.hasSVar("DontEquip")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// is no attachment so no using attach
|
||||||
|
if (!attachSource.isAttachment()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't fortify if already fortifying
|
// Don't fortify if already fortifying
|
||||||
if (attachSource.getFortifying() != null && attachSource.getFortifying().getController() == aiPlayer) {
|
if (attachSource.isFortification() && attachSource.getAttachedTo() != null
|
||||||
|
&& attachSource.getAttachedTo().getController() == aiPlayer) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,11 +1334,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
} else {
|
} else {
|
||||||
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
|
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
|
||||||
|
|
||||||
if (attachSource.isAura()) {
|
list = CardLists.filter(list, CardPredicates.canBeAttached(attachSource));
|
||||||
list = CardLists.filter(list, CardPredicates.canBeEnchantedBy(attachSource));
|
|
||||||
} else if (attachSource.isEquipment()) {
|
|
||||||
list = CardLists.filter(list, CardPredicates.canBeEquippedBy(attachSource));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO If Attaching without casting, don't need to actually target.
|
// TODO If Attaching without casting, don't need to actually target.
|
||||||
// I believe this is the only case where mandatory will be true, so just
|
// I believe this is the only case where mandatory will be true, so just
|
||||||
@@ -1327,7 +1535,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Also don't play if it would destroy own Aura
|
// Also don't play if it would destroy own Aura
|
||||||
for (Card c : card.getEnchantedBy(false)) {
|
for (Card c : card.getEnchantedBy()) {
|
||||||
if ((c.getController().equals(ai)) && (c.isOfColor(cc))) {
|
if ((c.getController().equals(ai)) && (c.isOfColor(cc))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
@@ -17,7 +16,7 @@ public class BalanceAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
int diff = 0;
|
int diff = 0;
|
||||||
// TODO Add support for multiplayer logic
|
// TODO Add support for multiplayer logic
|
||||||
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
final Player opp = aiPlayer.getWeakestOpponent();
|
||||||
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
|
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
|
||||||
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
|
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -25,7 +24,7 @@ public class BidLifeAi extends SpellAbilityAi {
|
|||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (tgt.canTgtCreature()) {
|
if (tgt.canTgtCreature()) {
|
||||||
List<Card> list = CardLists.getTargetableCards(ComputerUtil.getOpponentFor(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
|
List<Card> list = CardLists.getTargetableCards(aiPlayer.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), sa);
|
||||||
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import forge.game.combat.Combat;
|
|||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostDiscard;
|
import forge.game.cost.CostDiscard;
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
|
import forge.game.cost.CostPutCounter;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -26,6 +27,7 @@ import forge.game.spellability.AbilitySub;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.MyRandom;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -70,6 +72,29 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return false;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.checkAiLogic(ai, sa, aiLogic);
|
return super.checkAiLogic(ai, sa, aiLogic);
|
||||||
@@ -96,6 +121,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
|
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
|
||||||
} else if (aiLogic.equals("TheScarabGod")) {
|
} else if (aiLogic.equals("TheScarabGod")) {
|
||||||
return SpecialCardAi.TheScarabGod.consider(aiPlayer, sa);
|
return SpecialCardAi.TheScarabGod.consider(aiPlayer, sa);
|
||||||
|
} else if (aiLogic.equals("SorinVengefulBloodlord")) {
|
||||||
|
return SpecialCardAi.SorinVengefulBloodlord.consider(aiPlayer, sa);
|
||||||
} else if (aiLogic.equals("Intuition")) {
|
} else if (aiLogic.equals("Intuition")) {
|
||||||
// This logic only fills the multiple cards array, the decision to play is made
|
// This logic only fills the multiple cards array, the decision to play is made
|
||||||
// separately in hiddenOriginCanPlayAI later.
|
// separately in hiddenOriginCanPlayAI later.
|
||||||
@@ -159,7 +186,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return true; // debuffed by opponent's auras to the level that it becomes useless
|
return true; // debuffed by opponent's auras to the level that it becomes useless
|
||||||
}
|
}
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
for (Card enc : sa.getHostCard().getEnchantedBy(false)) {
|
for (Card enc : sa.getHostCard().getEnchantedBy()) {
|
||||||
if (enc.getController().isOpponentOf(aiPlayer)) {
|
if (enc.getController().isOpponentOf(aiPlayer)) {
|
||||||
delta--;
|
delta--;
|
||||||
} else {
|
} else {
|
||||||
@@ -202,7 +229,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
ZoneType origin = null;
|
ZoneType origin = null;
|
||||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
final Player opponent = ai.getWeakestOpponent();
|
||||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||||
|
|
||||||
if (sa.hasParam("Origin")) {
|
if (sa.hasParam("Origin")) {
|
||||||
@@ -251,6 +278,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
|
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ai.getGame().getCombat() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
|
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
|
||||||
boolean lowerCMC = false;
|
boolean lowerCMC = false;
|
||||||
for (Card attacker : attackers) {
|
for (Card attacker : attackers) {
|
||||||
@@ -312,6 +343,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
for (final Player p : pDefined) {
|
for (final Player p : pDefined) {
|
||||||
CardCollectionView list = p.getCardsIn(origin);
|
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) {
|
if (type != null && p == ai) {
|
||||||
// AI only "knows" about his information
|
// AI only "knows" about his information
|
||||||
list = CardLists.getValidCards(list, type, source.getController(), source);
|
list = CardLists.getValidCards(list, type, source.getController(), source);
|
||||||
@@ -403,7 +439,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
// if putting cards from hand to library and parent is drawing cards
|
// if putting cards from hand to library and parent is drawing cards
|
||||||
// make sure this will actually do something:
|
// make sure this will actually do something:
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
final Player opp = aiPlayer.getWeakestOpponent();
|
||||||
if (tgt != null && tgt.canTgtPlayer()) {
|
if (tgt != null && tgt.canTgtPlayer()) {
|
||||||
boolean isCurse = sa.isCurse();
|
boolean isCurse = sa.isCurse();
|
||||||
if (isCurse && sa.canTarget(opp)) {
|
if (isCurse && sa.canTarget(opp)) {
|
||||||
@@ -464,7 +500,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
Iterable<Player> pDefined;
|
Iterable<Player> pDefined;
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if ((tgt != null) && tgt.canTgtPlayer()) {
|
if ((tgt != null) && tgt.canTgtPlayer()) {
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
if (sa.isCurse()) {
|
if (sa.isCurse()) {
|
||||||
if (sa.canTarget(opp)) {
|
if (sa.canTarget(opp)) {
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
@@ -583,7 +619,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
private static Card chooseCreature(final Player ai, CardCollection list) {
|
private static Card chooseCreature(final Player ai, CardCollection list) {
|
||||||
// Creating a new combat for testing purposes.
|
// Creating a new combat for testing purposes.
|
||||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
final Player opponent = ai.getWeakestOpponent();
|
||||||
Combat combat = new Combat(opponent);
|
Combat combat = new Combat(opponent);
|
||||||
for (Card att : opponent.getCreaturesInPlay()) {
|
for (Card att : opponent.getCreaturesInPlay()) {
|
||||||
combat.addAttacker(att, ai);
|
combat.addAttacker(att, ai);
|
||||||
@@ -667,7 +703,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only use blink or bounce effects
|
// only use blink or bounce effects
|
||||||
if (!(destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone))
|
if (!(destination.equals(ZoneType.Exile)
|
||||||
|
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
|
||||||
&& !destination.equals(ZoneType.Hand)) {
|
&& !destination.equals(ZoneType.Hand)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -898,7 +935,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
&& !currCombat.getBlockers(attacker).isEmpty()) {
|
&& !currCombat.getBlockers(attacker).isEmpty()) {
|
||||||
ComputerUtilCard.sortByEvaluateCreature(blockers);
|
ComputerUtilCard.sortByEvaluateCreature(blockers);
|
||||||
Combat combat = new Combat(ai);
|
Combat combat = new Combat(ai);
|
||||||
combat.addAttacker(attacker, ComputerUtil.getOpponentFor(ai));
|
combat.addAttacker(attacker, ai.getWeakestOpponent());
|
||||||
for (Card blocker : blockers) {
|
for (Card blocker : blockers) {
|
||||||
combat.addBlocker(attacker, blocker);
|
combat.addBlocker(attacker, blocker);
|
||||||
}
|
}
|
||||||
@@ -927,7 +964,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// if it's blink or bounce, try to save my about to die stuff
|
// if it's blink or bounce, try to save my about to die stuff
|
||||||
final boolean blink = (destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger
|
final boolean blink = (destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger
|
||||||
|| (subApi == ApiType.ChangeZone && subAffected.equals("Remembered"))));
|
|| "DelayedBlink".equals(sa.getParam("AILogic")) || (subApi == ApiType.ChangeZone && subAffected.equals("Remembered"))));
|
||||||
if ((destination.equals(ZoneType.Hand) || blink) && (tgt.getMinTargets(sa.getHostCard(), sa) <= 1)) {
|
if ((destination.equals(ZoneType.Hand) || blink) && (tgt.getMinTargets(sa.getHostCard(), sa) <= 1)) {
|
||||||
// save my about to die stuff
|
// save my about to die stuff
|
||||||
Card tobounce = canBouncePermanent(ai, sa, list);
|
Card tobounce = canBouncePermanent(ai, sa, list);
|
||||||
@@ -957,7 +994,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
for (Card aura : c.getEnchantedBy(false)) {
|
for (Card aura : c.getEnchantedBy()) {
|
||||||
if (aura.getController().isOpponentOf(ai)) {
|
if (aura.getController().isOpponentOf(ai)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -1050,7 +1087,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
for (Card aura : c.getEnchantedBy(false)) {
|
for (Card aura : c.getEnchantedBy()) {
|
||||||
if (c.getOwner().isOpponentOf(ai) && aura.getController().equals(ai)) {
|
if (c.getOwner().isOpponentOf(ai) && aura.getController().equals(ai)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1075,7 +1112,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.getTargetRestrictions() != null
|
||||||
|
&& sa.getTargetRestrictions().getMinTargets(source, sa) == 0 && sa.getPayCosts() != null
|
||||||
|
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class);
|
||||||
|
|
||||||
|
if (list.isEmpty() && !doWithoutTarget) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1157,7 +1198,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
if (!mandatory) {
|
if (!mandatory) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
}
|
}
|
||||||
|
if (!doWithoutTarget) {
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
||||||
boolean aiTgtsOK = false;
|
boolean aiTgtsOK = false;
|
||||||
@@ -1183,6 +1228,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// honor the Same Creature Type restriction
|
||||||
|
if (tgt.isWithSameCreatureType()) {
|
||||||
|
Card firstTarget = sa.getTargetCard();
|
||||||
|
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
|
||||||
|
list.remove(choice);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list.remove(choice);
|
list.remove(choice);
|
||||||
sa.getTargets().add(choice);
|
sa.getTargets().add(choice);
|
||||||
}
|
}
|
||||||
@@ -1221,6 +1275,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
// filter out untargetables
|
// filter out untargetables
|
||||||
CardCollectionView aiPermanents = CardLists
|
CardCollectionView aiPermanents = CardLists
|
||||||
.filterControlledBy(list, ai);
|
.filterControlledBy(list, ai);
|
||||||
|
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
|
||||||
|
|
||||||
// Felidar Guardian + Saheeli Rai combo support
|
// Felidar Guardian + Saheeli Rai combo support
|
||||||
if (sa.getHostCard().getName().equals("Felidar Guardian")) {
|
if (sa.getHostCard().getName().equals("Felidar Guardian")) {
|
||||||
@@ -1264,6 +1319,33 @@ 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(CounterType.LOYALTY) - b.getCounters(CounterType.LOYALTY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (Card pw : aiPlaneswalkers) {
|
||||||
|
int curLoyalty = pw.getCounters(CounterType.LOYALTY);
|
||||||
|
int freshLoyalty = pw.getCurrentState().getBaseLoyalty();
|
||||||
|
if (freshLoyalty - curLoyalty >= loyaltyDiff && curLoyalty <= maxLoyaltyToConsider) {
|
||||||
|
return pw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1417,6 +1499,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else if ("WorstCard".equals(logic)) {
|
} else if ("WorstCard".equals(logic)) {
|
||||||
return ComputerUtilCard.getWorstAI(fetchList);
|
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)) {
|
} else if ("Mairsil".equals(logic)) {
|
||||||
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
|
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
|
||||||
} else if ("SurvivalOfTheFittest".equals(logic)) {
|
} else if ("SurvivalOfTheFittest".equals(logic)) {
|
||||||
@@ -1459,7 +1543,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
fetchList = CardLists.filter(fetchList, new Predicate<Card>() {
|
fetchList = CardLists.filter(fetchList, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
if (c.hasSVar("RemAIDeck") || c.hasSVar("RemRandomDeck")) {
|
if (ComputerUtilCard.isCardRemAIDeck(c) || ComputerUtilCard.isCardRemRandomDeck(c)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1730,6 +1814,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
SpellAbility causeSa = (SpellAbility)originalParams.get("Cause");
|
SpellAbility causeSa = (SpellAbility)originalParams.get("Cause");
|
||||||
SpellAbility causeSub = null;
|
SpellAbility causeSub = null;
|
||||||
|
|
||||||
|
// Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
|
||||||
|
if (sa.getHostCard().getName().contains("Squee, the Immortal")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
|
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
|
||||||
ApiType subApi = causeSub.getApi();
|
ApiType subApi = causeSub.getApi();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
@@ -57,6 +58,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
CardCollectionView oppType = CardLists.filterControlledBy(game.getCardsIn(origin), ai.getOpponents());
|
CardCollectionView oppType = CardLists.filterControlledBy(game.getCardsIn(origin), ai.getOpponents());
|
||||||
CardCollectionView computerType = ai.getCardsIn(origin);
|
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 check need to be done before filterListByType because of ChosenX
|
||||||
// Ugin AI: always try to sweep before considering +1
|
// Ugin AI: always try to sweep before considering +1
|
||||||
if (sourceName.equals("Ugin, the Spirit Dragon")) {
|
if (sourceName.equals("Ugin, the Spirit Dragon")) {
|
||||||
@@ -114,7 +120,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
// spBounceAll has some AI we can compare to.
|
// spBounceAll has some AI we can compare to.
|
||||||
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
|
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
|
||||||
if (!sa.usesTargeting()) {
|
if (!sa.usesTargeting()) {
|
||||||
// TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar)
|
// TODO: improve logic for non-targeted SAs of this type (most are currently AI:RemoveDeck:All, e.g. Memory Jar)
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// search targetable Opponents
|
// search targetable Opponents
|
||||||
@@ -220,6 +226,9 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (destination.equals(ZoneType.Library) && "Card.YouOwn".equals(sa.getParam("ChangeType"))) {
|
||||||
|
return (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
|
||||||
|
&& !ComputerUtil.isPlayingReanimator(ai);
|
||||||
}
|
}
|
||||||
} else if (origin.equals(ZoneType.Exile)) {
|
} else if (origin.equals(ZoneType.Exile)) {
|
||||||
String logic = sa.getParam("AILogic");
|
String logic = sa.getParam("AILogic");
|
||||||
@@ -344,8 +353,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) {
|
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) {
|
||||||
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
|
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
|
||||||
// Profaner from exile without paying its mana cost. Otherwise the card is marked RemAIDeck and there is no
|
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
|
||||||
// specific AI to support playing it in a smarter way. Feel free to expand.
|
// there is no specific AI to support playing it in a smarter way. Feel free to expand.
|
||||||
return !CardLists.filter(ai.getOpponents().getCardsIn(origin), CardPredicates.Presets.CREATURES).isEmpty();
|
return !CardLists.filter(ai.getOpponents().getCardsIn(origin), CardPredicates.Presets.CREATURES).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilAbility;
|
import forge.ai.ComputerUtilAbility;
|
||||||
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
@@ -126,7 +126,7 @@ public class ChooseCardAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else if (aiLogic.equals("Duneblast")) {
|
} else if (aiLogic.equals("Duneblast")) {
|
||||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||||
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay();
|
||||||
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
|
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
|
||||||
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
|
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
|
||||||
|
|
||||||
@@ -274,6 +274,22 @@ public class ChooseCardAi extends SpellAbilityAi {
|
|||||||
if (ai.equals(sa.getActivatingPlayer())) {
|
if (ai.equals(sa.getActivatingPlayer())) {
|
||||||
choice = ComputerUtilCard.getBestAI(options);
|
choice = ComputerUtilCard.getBestAI(options);
|
||||||
} // TODO: improve ai
|
} // TODO: improve ai
|
||||||
|
} else if (logic.equals("Phylactery")) {
|
||||||
|
CardCollection aiArtifacts = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS);
|
||||||
|
CardCollection indestructibles = CardLists.filter(aiArtifacts, CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE));
|
||||||
|
CardCollection nonCreatures = CardLists.filter(aiArtifacts, Predicates.not(Presets.CREATURES));
|
||||||
|
CardCollection creatures = CardLists.filter(aiArtifacts, Presets.CREATURES);
|
||||||
|
if (!indestructibles.isEmpty()) {
|
||||||
|
// Choose the worst (smallest) indestructible artifact so that the opponent would have to waste
|
||||||
|
// removal on something unpreferred
|
||||||
|
choice = ComputerUtilCard.getWorstAI(indestructibles);
|
||||||
|
} else if (!nonCreatures.isEmpty()) {
|
||||||
|
// The same as above, but for non-indestructible non-creature artifacts (they can't die in combat)
|
||||||
|
choice = ComputerUtilCard.getWorstAI(nonCreatures);
|
||||||
|
} else if (!creatures.isEmpty()) {
|
||||||
|
// Choose the best (hopefully the fattest, whatever) creature so that hopefully it won't die too easily
|
||||||
|
choice = ComputerUtilCard.getBestAI(creatures);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
choice = ComputerUtilCard.getBestAI(options);
|
choice = ComputerUtilCard.getBestAI(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
|||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (tgt.canOnlyTgtOpponent()) {
|
if (tgt.canOnlyTgtOpponent()) {
|
||||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
sa.getTargets().add(ai.getWeakestOpponent());
|
||||||
} else {
|
} else {
|
||||||
sa.getTargets().add(ai);
|
sa.getTargets().add(ai);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("Addle".equals(sourceName)) {
|
if ("Addle".equals(sourceName)) {
|
||||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).isEmpty()) {
|
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -61,7 +61,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
|||||||
if (logic.equals("MostExcessOpponentControls")) {
|
if (logic.equals("MostExcessOpponentControls")) {
|
||||||
for (byte color : MagicColor.WUBRG) {
|
for (byte color : MagicColor.WUBRG) {
|
||||||
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
||||||
CardCollectionView opplist = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
CardCollectionView opplist = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield);
|
||||||
|
|
||||||
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
||||||
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
|
import forge.game.Direction;
|
||||||
|
import forge.game.Game;
|
||||||
|
import forge.game.card.CardCollection;
|
||||||
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.card.CardPredicates;
|
||||||
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.Aggregates;
|
||||||
|
|
||||||
public class ChooseDirectionAi extends SpellAbilityAi {
|
public class ChooseDirectionAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -12,10 +20,23 @@ public class ChooseDirectionAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final String logic = sa.getParam("AILogic");
|
final String logic = sa.getParam("AILogic");
|
||||||
|
final Game game = sa.getActivatingPlayer().getGame();
|
||||||
if (logic == null) {
|
if (logic == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// TODO: default ai
|
if ("Aminatou".equals(logic)) {
|
||||||
|
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.NONLAND_PERMANENTS);
|
||||||
|
CardCollection aiPermanent = CardLists.filterControlledBy(all, ai);
|
||||||
|
aiPermanent.remove(sa.getHostCard());
|
||||||
|
int aiValue = Aggregates.sum(aiPermanent, CardPredicates.Accessors.fnGetCmc);
|
||||||
|
CardCollection left = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Left));
|
||||||
|
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);
|
||||||
|
if (aiValue > leftValue || aiValue > rightValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import forge.ai.ComputerUtilAbility;
|
import com.google.common.collect.Sets;
|
||||||
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilCard;
|
|
||||||
import forge.ai.ComputerUtilCost;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.ai.SpellApiToAi;
|
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardCollectionView;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.card.CardUtil;
|
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.AbilitySub;
|
import forge.game.spellability.AbilitySub;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -31,6 +21,9 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
public class ChooseGenericEffectAi extends SpellAbilityAi {
|
public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -38,7 +31,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
|||||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||||
if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) {
|
if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) {
|
||||||
return true;
|
return true;
|
||||||
} else if (aiLogic.startsWith("Fabricate")) {
|
} else if (aiLogic.startsWith("Fabricate") || "Riot".equals(aiLogic)) {
|
||||||
return true;
|
return true;
|
||||||
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
|
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
|
||||||
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
|
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
|
||||||
@@ -46,6 +39,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if ("GideonBlackblade".equals(aiLogic)) {
|
||||||
|
return SpecialCardAi.GideonBlackblade.consider(ai, sa);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -91,6 +86,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
|||||||
return spells.get(0);
|
return spells.get(0);
|
||||||
} else if ("Random".equals(logic)) {
|
} else if ("Random".equals(logic)) {
|
||||||
return Aggregates.random(spells);
|
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
|
} else if ("Phasing".equals(logic)) { // Teferi's Realm : keep aggressive
|
||||||
List<SpellAbility> filtered = Lists.newArrayList(Iterables.filter(spells, new Predicate<SpellAbility>() {
|
List<SpellAbility> filtered = Lists.newArrayList(Iterables.filter(spells, new Predicate<SpellAbility>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -344,7 +341,56 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
|||||||
if (!filtered.isEmpty()) {
|
if (!filtered.isEmpty()) {
|
||||||
return filtered.get(0);
|
return filtered.get(0);
|
||||||
}
|
}
|
||||||
|
} else if ("Riot".equals(logic)) {
|
||||||
|
SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
|
||||||
|
return preferHasteForRiot(sa, player) ? hasteSA : counterSA;
|
||||||
}
|
}
|
||||||
return spells.get(0); // return first choice if no logic found
|
return spells.get(0); // return first choice if no logic found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean preferHasteForRiot(SpellAbility sa, Player player) {
|
||||||
|
// returning true means preferring Haste, returning false means preferring a +1/+1 counter
|
||||||
|
final Card host = sa.getHostCard();
|
||||||
|
final Game game = host.getGame();
|
||||||
|
final Card copy = CardUtil.getLKICopy(host);
|
||||||
|
copy.setLastKnownZone(player.getZone(ZoneType.Battlefield));
|
||||||
|
|
||||||
|
// check state it would have on the battlefield
|
||||||
|
CardCollection preList = new CardCollection(copy);
|
||||||
|
game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList);
|
||||||
|
// reset again?
|
||||||
|
game.getAction().checkStaticAbilities(false);
|
||||||
|
|
||||||
|
// can't gain counters, use Haste
|
||||||
|
if (!copy.canReceiveCounters(CounterType.P1P1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// already has Haste, use counter
|
||||||
|
if (copy.hasKeyword(Keyword.HASTE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not AI turn
|
||||||
|
if (!game.getPhaseHandler().isPlayerTurn(player)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not before Combat
|
||||||
|
if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check other opponents too if able
|
||||||
|
final Player opp = player.getWeakestOpponent();
|
||||||
|
if (opp != null) {
|
||||||
|
// TODO add predict Combat Damage?
|
||||||
|
if (opp.getLife() < copy.getNetPower()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// haste might not be good enough?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -17,7 +16,7 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
|||||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
Player opp = aiPlayer.getWeakestOpponent();
|
||||||
if (sa.canTarget(opp)) {
|
if (sa.canTarget(opp)) {
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import com.google.common.base.Predicates;
|
|||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.ai.ComputerUtilCombat;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
@@ -68,7 +67,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
|||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
if (sa.canTarget(opp)) {
|
if (sa.canTarget(opp)) {
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import forge.game.phase.PhaseType;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -21,7 +20,6 @@ public class CloneAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Game game = source.getGame();
|
final Game game = source.getGame();
|
||||||
|
|
||||||
@@ -39,27 +37,13 @@ public class CloneAi extends SpellAbilityAi {
|
|||||||
// TODO - add some kind of check for during human turn to answer
|
// TODO - add some kind of check for during human turn to answer
|
||||||
// "Can I use this to block something?"
|
// "Can I use this to block something?"
|
||||||
|
|
||||||
|
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
PhaseHandler phase = game.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't use instant speed clone abilities outside humans
|
if (!sa.usesTargeting()) {
|
||||||
// Combat_Declare_Attackers_InstantAbility step
|
|
||||||
if (!phase.is(PhaseType.COMBAT_DECLARE_ATTACKERS) || phase.isPlayerTurn(ai) || game.getCombat().getAttackers().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||||
|
|
||||||
boolean bFlag = false;
|
boolean bFlag = false;
|
||||||
@@ -142,14 +126,17 @@ public class CloneAi extends SpellAbilityAi {
|
|||||||
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
|
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
|
||||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
|
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
|
||||||
return true;
|
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.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default:
|
// Default:
|
||||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||||
// two are the only things
|
// two are the only things that clone a target. Those can just use
|
||||||
// that clone a target. Those can just use SVar:RemAIDeck:True until
|
// AI:RemoveDeck:All until this can do a reasonably good job of picking
|
||||||
// this can do a reasonably
|
// a good target
|
||||||
// good job of picking a good target
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +145,18 @@ public class CloneAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
// Didn't confirm in the original code
|
if (sa.hasParam("AILogic") && (!sa.usesTargeting() || sa.isTargetNumberValid())) {
|
||||||
|
// Had a special logic for it and managed to target, so confirm if viable
|
||||||
|
if ("CloneBestCreature".equals(sa.getParam("AILogic"))) {
|
||||||
|
return ComputerUtilCard.evaluateCreature(sa.getTargets().getFirstTargetedCard()) > ComputerUtilCard.evaluateCreature(sa.getHostCard());
|
||||||
|
} else if ("IfDefinedCreatureIsBetter".equals(sa.getParam("AILogic"))) {
|
||||||
|
List<Card> defined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||||
|
Card bestDefined = ComputerUtilCard.getBestCreatureAI(defined);
|
||||||
|
return ComputerUtilCard.evaluateCreature(bestDefined) > ComputerUtilCard.evaluateCreature(sa.getHostCard());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently doesn't confirm anything that's not defined by AI logic
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,19 +170,30 @@ public class CloneAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
|
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
|
||||||
Player targetedPlayer) {
|
Player targetedPlayer) {
|
||||||
|
|
||||||
final Card host = sa.getHostCard();
|
final Card host = sa.getHostCard();
|
||||||
final Player ctrl = host.getController();
|
final Player ctrl = host.getController();
|
||||||
|
|
||||||
final boolean isVesuva = "Vesuva".equals(host.getName());
|
final Card cloneTarget = getCloneTarget(sa);
|
||||||
|
final boolean isOpp = cloneTarget.getController().isOpponentOf(sa.getActivatingPlayer());
|
||||||
|
|
||||||
final String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
|
final boolean isVesuva = "Vesuva".equals(host.getName());
|
||||||
|
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
|
||||||
|
|
||||||
|
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
|
||||||
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
|
: "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);
|
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
|
||||||
if (!newOptions.isEmpty()) {
|
if (!newOptions.isEmpty()) {
|
||||||
options = newOptions;
|
options = newOptions;
|
||||||
}
|
}
|
||||||
Card choice = ComputerUtilCard.getBestAI(options);
|
Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);
|
||||||
|
|
||||||
if (isVesuva && "Vesuva".equals(choice.getName())) {
|
if (isVesuva && "Vesuva".equals(choice.getName())) {
|
||||||
choice = null;
|
choice = null;
|
||||||
}
|
}
|
||||||
@@ -192,4 +201,44 @@ public class CloneAi extends SpellAbilityAi {
|
|||||||
return choice;
|
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
|
||||||
|
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package forge.ai.ability;
|
|||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
@@ -30,13 +29,13 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
|||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
|
|
||||||
CardCollection list =
|
CardCollection list =
|
||||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||||
// AI won't try to grab cards that are filtered out of AI decks on
|
// AI won't try to grab cards that are filtered out of AI decks on
|
||||||
// purpose
|
// purpose
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return !c.hasSVar("RemAIDeck") && c.canBeTargetedBy(sa);
|
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
object1 = ComputerUtilCard.getBestAI(list);
|
object1 = ComputerUtilCard.getBestAI(list);
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not take control on something it doesn't know how to use
|
// do not take control on something it doesn't know how to use
|
||||||
return !c.hasSVar("RemAIDeck");
|
return !ComputerUtilCard.isCardRemAIDeck(c);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
final Player activator = sa.getActivatingPlayer();
|
final Player activator = sa.getActivatingPlayer();
|
||||||
final Game game = host.getGame();
|
final Game game = host.getGame();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
|
final boolean canCopyLegendary = sa.hasParam("NonLegendary");
|
||||||
|
|
||||||
|
|
||||||
// ////
|
// ////
|
||||||
@@ -83,7 +84,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa));
|
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa));
|
||||||
|
|
||||||
list = CardLists.filter(list, Predicates.not(CardPredicates.hasSVar("RemAIDeck")));
|
list = CardLists.filter(list, Predicates.not(CardPredicates.isRemAIDeck()));
|
||||||
//Nothing to target
|
//Nothing to target
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -114,7 +115,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return !c.getType().isLegendary() || !c.getController().equals(aiPlayer);
|
return (!c.getType().isLegendary() || canCopyLegendary) || !c.getController().equals(aiPlayer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Card choice;
|
Card choice;
|
||||||
@@ -180,7 +181,8 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
private CardCollection getBetterOptions(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional) {
|
private CardCollection getBetterOptions(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional) {
|
||||||
final Card host = sa.getHostCard();
|
final Card host = sa.getHostCard();
|
||||||
final Player ctrl = host.getController();
|
final Player ctrl = host.getController();
|
||||||
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
|
final boolean canCopyLegendary = sa.hasParam("NonLegendary");
|
||||||
|
final String filter = canCopyLegendary ? "Permanent" : "Permanent.YouDontCtrl,Permanent.nonLegendary";
|
||||||
// TODO add filter to not select Legendary from Other Player when ai already have a Legendary with that name
|
// 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);
|
return CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.SpecialCardAi;
|
import forge.ai.*;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.game.Game;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
|
import forge.game.spellability.AbilityActivated;
|
||||||
|
import forge.game.spellability.Spell;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -13,27 +18,111 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
|
Game game = aiPlayer.getGame();
|
||||||
return sa.isMandatory() || "Always".equals(sa.getParam("AILogic"));
|
int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK);
|
||||||
|
int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF);
|
||||||
|
String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
|
if (game.getStack().isEmpty()) {
|
||||||
|
return sa.isMandatory();
|
||||||
|
}
|
||||||
|
|
||||||
|
final SpellAbility top = game.getStack().peekAbility();
|
||||||
|
if (top != 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top.getActivatingPlayer().isOpponentOf(aiPlayer)) {
|
||||||
|
chance = 100; // currently the AI will always copy the opponent's spell if viable
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MyRandom.percentTrue(chance)
|
||||||
|
&& !"AlwaysIfViable".equals(logic)
|
||||||
|
&& !"OnceIfViable".equals(logic)
|
||||||
|
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("OnceIfViable".equals(logic)) {
|
||||||
|
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
if (tgt != null) {
|
||||||
|
|
||||||
|
// Filter AI-specific targets if provided
|
||||||
|
if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
|
||||||
|
if (!top.getActivatingPlayer().equals(aiPlayer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Don't try to copy a copy ability, too complex for the AI to handle
|
||||||
|
return false;
|
||||||
|
} else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) {
|
||||||
|
if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
|
||||||
|
// If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
|
||||||
|
// it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (top.hasParam("ConditionManaSpent")) {
|
||||||
|
// Mana spent is not copied, so these spells generally do nothing when copied.
|
||||||
|
return false;
|
||||||
|
} else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
|
||||||
|
// Don't try to copy anything you can't understand how to handle
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A copy is necessary to properly test the SA before targeting the copied spell, otherwise the copy SA will fizzle.
|
||||||
|
final SpellAbility topCopy = top.copy(aiPlayer);
|
||||||
|
topCopy.resetTargets();
|
||||||
|
|
||||||
|
if (top.canBeTargetedBy(sa)) {
|
||||||
|
AiPlayDecision decision = AiPlayDecision.CantPlaySa;
|
||||||
|
if (top instanceof Spell) {
|
||||||
|
decision = ((PlayerControllerAi) aiPlayer.getController()).getAi().canPlayFromEffectAI((Spell) topCopy, true, true);
|
||||||
|
} 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?
|
||||||
|
}
|
||||||
|
if (decision == AiPlayDecision.WillPlay) {
|
||||||
|
sa.getTargets().add(top);
|
||||||
|
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the AI should not miss mandatory activations
|
||||||
|
return sa.isMandatory() || "Always".equals(logic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||||
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
|
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
|
||||||
return mandatory || "Always".equals(sa.getParam("AILogic"));
|
String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
return mandatory || logic.contains("Always"); // this includes logic like AlwaysIfViable
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||||
// NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
|
|
||||||
// generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
|
|
||||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||||
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||||
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.chkAIDrawback(sa, aiPlayer);
|
return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
boolean setPayX = false;
|
boolean setPayX = false;
|
||||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||||
setPayX = true;
|
setPayX = true;
|
||||||
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
|
||||||
} else {
|
} else {
|
||||||
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
|
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for some specific AI preferences
|
// check for some specific AI preferences
|
||||||
if (src.hasStartOfKeyword("Graft") && "DontMoveCounterIfLethal".equals(src.getSVar("AIGraftPreference"))) {
|
if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
|
||||||
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
|
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -333,11 +333,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// try to remove P1P1 from undying or evolve
|
// try to remove P1P1 from undying or evolve
|
||||||
if (CounterType.P1P1.equals(cType)) {
|
if (CounterType.P1P1.equals(cType)) {
|
||||||
if (card.hasKeyword("Undying") || card.hasKeyword("Evolve")) {
|
if (card.hasKeyword(Keyword.UNDYING) || card.hasKeyword(Keyword.EVOLVE)
|
||||||
|
|| card.hasKeyword(Keyword.ADAPT)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
|
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,10 +393,10 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cType != null) {
|
if (cType != null) {
|
||||||
if (CounterType.P1P1.equals(cType) && card.hasKeyword("Undying")) {
|
if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
|
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
|
import forge.game.GameEntity;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.card.CardUtil;
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -33,10 +38,14 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
|||||||
cperms.addAll(CardLists.filter(p.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
|
cperms.addAll(CardLists.filter(p.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card crd) {
|
public boolean apply(final Card crd) {
|
||||||
if (crd.hasCounters()) {
|
if (!crd.hasCounters()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (crd.isPlaneswalker()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// iterate only over existing counters
|
// iterate only over existing counters
|
||||||
for (final Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
|
for (final Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
|
||||||
if (e.getValue() >= 1 && !ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
|
if (e.getValue() >= 1 && !ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
|
||||||
@@ -56,7 +65,11 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
|||||||
hperms.addAll(CardLists.filter(o.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
|
hperms.addAll(CardLists.filter(o.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card crd) {
|
public boolean apply(final Card crd) {
|
||||||
if (crd.hasCounters()) {
|
if (!crd.hasCounters()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crd.isPlaneswalker()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +105,68 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||||
return canPlayAI(ai, sa);
|
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Proliferate is always optional for all, no need to select best
|
||||||
|
|
||||||
|
// 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(CounterType.POISON) > 0 && p.canReceiveCounters(CounterType.POISON)) {
|
||||||
|
return (T)p;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (p.getCounters(CounterType.POISON) <= 5 || p.canReceiveCounters(CounterType.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ import forge.game.GameEntity;
|
|||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.*;
|
||||||
import forge.game.cost.CostPart;
|
|
||||||
import forge.game.cost.CostRemoveCounter;
|
|
||||||
import forge.game.cost.CostSacrifice;
|
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -46,17 +44,19 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
|
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
|
||||||
|
|
||||||
final String type = sa.getParam("CounterType");
|
final String type = sa.getParam("CounterType");
|
||||||
|
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
if (!super.willPayCosts(ai, sa, cost, source)) {
|
if (!super.willPayCosts(ai, sa, cost, source)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable moving counters
|
// disable moving counters (unless a specialized AI logic supports it)
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostRemoveCounter) {
|
if (part instanceof CostRemoveCounter) {
|
||||||
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
|
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
|
||||||
final CounterType counterType = remCounter.counter;
|
final CounterType counterType = remCounter.counter;
|
||||||
if (counterType.name().equals(type)) {
|
if (counterType.name().equals(type) && !aiLogic.startsWith("MoveCounter")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!part.payCostFromSource()) {
|
if (!part.payCostFromSource()) {
|
||||||
@@ -102,7 +102,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
if (sa.hasParam("LevelUp")) {
|
if (sa.hasParam("LevelUp")) {
|
||||||
// creatures enchanted by curse auras have low priority
|
// creatures enchanted by curse auras have low priority
|
||||||
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||||
for (Card aura : source.getEnchantedBy(false)) {
|
for (Card aura : source.getEnchantedBy()) {
|
||||||
if (aura.getController().isOpponentOf(ai)) {
|
if (aura.getController().isOpponentOf(ai)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -220,6 +220,8 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if ("Never".equals(logic)) {
|
if ("Never".equals(logic)) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if ("AristocratCounters".equals(logic)) {
|
||||||
|
return PumpAi.doAristocratWithCountersLogic(sa, ai);
|
||||||
} else if ("PayEnergy".equals(logic)) {
|
} else if ("PayEnergy".equals(logic)) {
|
||||||
return true;
|
return true;
|
||||||
} else if ("PayEnergyConservatively".equals(logic)) {
|
} else if ("PayEnergyConservatively".equals(logic)) {
|
||||||
@@ -282,6 +284,8 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
if (!source.canTransform()) {
|
if (!source.canTransform()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (logic.startsWith("MoveCounter")) {
|
||||||
|
return doMoveCounterLogic(ai, sa, ph);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
|
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
|
||||||
@@ -312,6 +316,17 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
// TODO handle proper calculation of X values based on Cost
|
// TODO handle proper calculation of X values based on Cost
|
||||||
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||||
|
|
||||||
|
if (sa.hasParam("Adapt")) {
|
||||||
|
Game game = ai.getGame();
|
||||||
|
Combat combat = game.getCombat();
|
||||||
|
|
||||||
|
if (!source.canReceiveCounters(CounterType.P1P1) || source.getCounters(CounterType.P1P1) > 0) {
|
||||||
|
return false;
|
||||||
|
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||||
|
return doCombatAdaptLogic(source, amount, combat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("Fight".equals(logic)) {
|
if ("Fight".equals(logic)) {
|
||||||
int nPump = 0;
|
int nPump = 0;
|
||||||
if (type.equals("P1P1")) {
|
if (type.equals("P1P1")) {
|
||||||
@@ -433,6 +448,18 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
|
||||||
|
// 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().getNumTargeted() == 0
|
||||||
|
&& ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceName.equals("Abzan Charm")) {
|
if (sourceName.equals("Abzan Charm")) {
|
||||||
final TargetRestrictions abTgt = sa.getTargetRestrictions();
|
final TargetRestrictions abTgt = sa.getTargetRestrictions();
|
||||||
// specific AI for instant with distribute two +1/+1 counters
|
// specific AI for instant with distribute two +1/+1 counters
|
||||||
@@ -715,6 +742,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
int totalTargets = list.size();
|
int totalTargets = list.size();
|
||||||
|
|
||||||
|
sa.resetTargets();
|
||||||
while (sa.canAddMoreTarget()) {
|
while (sa.canAddMoreTarget()) {
|
||||||
if (mandatory) {
|
if (mandatory) {
|
||||||
// When things are mandatory, gotta handle a little differently
|
// When things are mandatory, gotta handle a little differently
|
||||||
@@ -989,4 +1017,80 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
return Iterables.getFirst(options, null);
|
return Iterables.getFirst(options, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||||
|
// Spikes (Tempest)
|
||||||
|
|
||||||
|
// Try not to do it unless at the end of opponent's turn or the creature is threatened
|
||||||
|
final int creatDiff = sa.getParam("AILogic").contains("IsCounterUser") ? 450 : 1;
|
||||||
|
final Combat combat = ai.getGame().getCombat();
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
|
final boolean threatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source)
|
||||||
|
|| (combat != null && (((combat.isBlocked(source) && ComputerUtilCombat.attackerWouldBeDestroyed(ai, source, combat)) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))
|
||||||
|
|| (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))));
|
||||||
|
|
||||||
|
if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
|
||||||
|
targets.remove(source);
|
||||||
|
|
||||||
|
targets = CardLists.filter(targets, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
boolean tgtThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
|
||||||
|
|| (combat != null && ((combat.isBlocked(card) && ComputerUtilCombat.attackerWouldBeDestroyed(ai, card, combat))
|
||||||
|
|| (combat.isBlocking(card) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, card, combat))));
|
||||||
|
// when threatened, any non-threatened target is good to preserve the counter
|
||||||
|
return !tgtThreatened && (threatened || ComputerUtilCard.evaluateCreature(card, false, false) > ComputerUtilCard.evaluateCreature(source, false, false) + creatDiff);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Card bestTgt = ComputerUtilCard.getBestCreatureAI(targets);
|
||||||
|
|
||||||
|
if (bestTgt != null) {
|
||||||
|
sa.getTargets().add(bestTgt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -39,7 +38,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
final boolean curse = sa.isCurse();
|
final boolean curse = sa.isCurse();
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
|
||||||
hList = CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
hList = CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||||
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||||
|
|
||||||
if (abCost != null) {
|
if (abCost != null) {
|
||||||
@@ -68,7 +67,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
Player pl = curse ? ComputerUtil.getOpponentFor(ai) : ai;
|
Player pl = curse ? ai.getWeakestOpponent() : ai;
|
||||||
sa.getTargets().add(pl);
|
sa.getTargets().add(pl);
|
||||||
|
|
||||||
hList = CardLists.filterControlledBy(hList, pl);
|
hList = CardLists.filterControlledBy(hList, pl);
|
||||||
@@ -149,7 +148,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
return player.getCreaturesInPlay().size() >= ComputerUtil.getOpponentFor(player).getCreaturesInPlay().size();
|
return player.getCreaturesInPlay().size() >= player.getWeakestOpponent().getCreaturesInPlay().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import forge.ai.ComputerUtilCard;
|
|||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.GameEntity;
|
||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
@@ -200,14 +201,20 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do as P1P1 part
|
// do as P1P1 part
|
||||||
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
|
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasLessCounter(CounterType.P1P1, amount));
|
||||||
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
|
CardCollection aiUndyingList = CardLists.getKeyword(aiP1P1List, Keyword.UNDYING);
|
||||||
|
|
||||||
if (!aiUndyingList.isEmpty()) {
|
if (!aiUndyingList.isEmpty()) {
|
||||||
aiP1P1List = aiUndyingList;
|
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if (!aiP1P1List.isEmpty()) {
|
|
||||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiP1P1List));
|
// remove P1P1 counters from opposing creatures
|
||||||
|
CardCollection oppP1P1List = CardLists.filter(list,
|
||||||
|
Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
|
||||||
|
CardPredicates.hasCounter(CounterType.P1P1));
|
||||||
|
if (!oppP1P1List.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +315,30 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mandatory) {
|
if (mandatory) {
|
||||||
|
if (type.equals("P1P1")) {
|
||||||
|
// Try to target creatures with Adapt or similar
|
||||||
|
CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
|
||||||
|
if (!adaptCreats.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outlast nice target
|
||||||
|
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(CounterType.P1P1, 2));
|
||||||
|
|
||||||
|
if (!betterTargets.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -330,7 +361,30 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
|
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
|
||||||
// TODO Auto-generated method stub
|
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 == CounterType.P1P1
|
||||||
|
&& targetCard.getCounters(CounterType.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.equals(CounterType.POISON) ? max : min;
|
||||||
|
} else {
|
||||||
|
return type.equals(CounterType.POISON) ? max : min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return super.chooseNumber(player, sa, min, max, params);
|
return super.chooseNumber(player, sa, min, max, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,30 +400,49 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
return super.chooseCounterType(options, sa, params);
|
return super.chooseCounterType(options, sa, params);
|
||||||
}
|
}
|
||||||
Player ai = sa.getActivatingPlayer();
|
Player ai = sa.getActivatingPlayer();
|
||||||
Card target = (Card) params.get("Target");
|
GameEntity target = (GameEntity) params.get("Target");
|
||||||
|
|
||||||
if (target.getController().isOpponentOf(ai)) {
|
if (target instanceof Card) {
|
||||||
|
Card targetCard = (Card) target;
|
||||||
|
if (targetCard.getController().isOpponentOf(ai)) {
|
||||||
// if its a Planeswalker try to remove Loyality first
|
// if its a Planeswalker try to remove Loyality first
|
||||||
if (target.isPlaneswalker()) {
|
if (targetCard.isPlaneswalker()) {
|
||||||
return CounterType.LOYALTY;
|
return CounterType.LOYALTY;
|
||||||
}
|
}
|
||||||
for (CounterType type : options) {
|
for (CounterType type : options) {
|
||||||
if (!ComputerUtil.isNegativeCounter(type, target)) {
|
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (options.contains(CounterType.M1M1) && target.hasKeyword(Keyword.PERSIST)) {
|
if (options.contains(CounterType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||||
return CounterType.M1M1;
|
|
||||||
} else if (options.contains(CounterType.P1P1) && target.hasKeyword(Keyword.UNDYING)) {
|
|
||||||
return CounterType.M1M1;
|
return CounterType.M1M1;
|
||||||
|
} else if (options.contains(CounterType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||||
|
return CounterType.P1P1;
|
||||||
}
|
}
|
||||||
for (CounterType type : options) {
|
for (CounterType type : options) {
|
||||||
if (ComputerUtil.isNegativeCounter(type, target)) {
|
if (ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (target instanceof Player) {
|
||||||
|
Player targetPlayer = (Player) target;
|
||||||
|
if (targetPlayer.isOpponentOf(ai)) {
|
||||||
|
for (CounterType type : options) {
|
||||||
|
if (!type.equals(CounterType.POISON)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (CounterType type : options) {
|
||||||
|
if (type.equals(CounterType.POISON)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return super.chooseCounterType(options, sa, params);
|
return super.chooseCounterType(options, sa, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.ai.ComputerUtilCombat;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -20,14 +19,14 @@ import forge.util.MyRandom;
|
|||||||
|
|
||||||
public abstract class DamageAiBase extends SpellAbilityAi {
|
public abstract class DamageAiBase extends SpellAbilityAi {
|
||||||
protected boolean avoidTargetP(final Player comp, final SpellAbility sa) {
|
protected boolean avoidTargetP(final Player comp, final SpellAbility sa) {
|
||||||
Player enemy = ComputerUtil.getOpponentFor(comp);
|
Player enemy = comp.getWeakestOpponent();
|
||||||
// Logic for cards that damage owner, like Fireslinger
|
// Logic for cards that damage owner, like Fireslinger
|
||||||
// Do not target a player if they aren't below 75% of our health.
|
// Do not target a player if they aren't below 75% of our health.
|
||||||
// Unless Lifelink will cancel the damage to us
|
// Unless Lifelink will cancel the damage to us
|
||||||
Card hostcard = sa.getHostCard();
|
Card hostcard = sa.getHostCard();
|
||||||
boolean lifelink = hostcard.hasKeyword(Keyword.LIFELINK);
|
boolean lifelink = hostcard.hasKeyword(Keyword.LIFELINK);
|
||||||
if (!lifelink) {
|
if (!lifelink) {
|
||||||
for (Card ench : hostcard.getEnchantedBy(false)) {
|
for (Card ench : hostcard.getEnchantedBy()) {
|
||||||
// Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink.
|
// Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink.
|
||||||
if (ench.hasSVar("LikeLifeLink")) {
|
if (ench.hasSVar("LikeLifeLink")) {
|
||||||
if ("True".equals(ench.getSVar("LikeLifeLink"))) {
|
if ("True".equals(ench.getSVar("LikeLifeLink"))) {
|
||||||
@@ -54,7 +53,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) {
|
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention, final boolean noPlaneswalkerRedirection) {
|
||||||
int restDamage = d;
|
int restDamage = d;
|
||||||
final Game game = comp.getGame();
|
final Game game = comp.getGame();
|
||||||
Player enemy = ComputerUtil.getOpponentFor(comp);
|
Player enemy = comp.getWeakestOpponent();
|
||||||
boolean dmgByCardsInHand = false;
|
boolean dmgByCardsInHand = false;
|
||||||
|
|
||||||
if ("X".equals(sa.getParam("NumDmg")) && sa.getHostCard() != null && sa.hasSVar(sa.getParam("NumDmg")) &&
|
if ("X".equals(sa.getParam("NumDmg")) && sa.getHostCard() != null && sa.hasSVar(sa.getParam("NumDmg")) &&
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate creatures getting killed
|
// Evaluate creatures getting killed
|
||||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
Player enemy = ai.getWeakestOpponent();
|
||||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
@@ -294,7 +294,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate creatures getting killed
|
// Evaluate creatures getting killed
|
||||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
Player enemy = ai.getWeakestOpponent();
|
||||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import com.google.common.base.Predicate;
|
|||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
|
import forge.card.mana.ManaCost;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameObject;
|
import forge.game.GameObject;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostPart;
|
||||||
|
import forge.game.cost.CostPartMana;
|
||||||
import forge.game.cost.CostRemoveCounter;
|
import forge.game.cost.CostRemoveCounter;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
@@ -21,7 +25,11 @@ import forge.game.spellability.TargetChoices;
|
|||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.Aggregates;
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -90,11 +98,26 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
|
|
||||||
if (damage.equals("X")) {
|
if (damage.equals("X")) {
|
||||||
if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
|
if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
|
||||||
// Set PayX here to maximum value.
|
|
||||||
dmg = ComputerUtilMana.determineLeftoverMana(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()) {
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
int holdChance = aic.getIntProperty(AiProps.HOLD_X_DAMAGE_SPELLS_FOR_MORE_DAMAGE_CHANCE);
|
||||||
|
if (MyRandom.percentTrue(holdChance)) {
|
||||||
|
int threshold = aic.getIntProperty(AiProps.HOLD_X_DAMAGE_SPELLS_THRESHOLD);
|
||||||
|
boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0);
|
||||||
|
boolean isLethal = sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
|
||||||
|
if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set PayX here to maximum value. It will be adjusted later depending on the target.
|
||||||
source.setSVar("PayX", Integer.toString(dmg));
|
source.setSVar("PayX", Integer.toString(dmg));
|
||||||
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
|
} else if (sa.getSVar(damage).contains("InYourHand") && source.getZone().is(ZoneType.Hand)) {
|
||||||
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
|
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")) {
|
} 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
|
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
|
||||||
if (sa.getTargetRestrictions().canTgtPlayer()) {
|
if (sa.getTargetRestrictions().canTgtPlayer()) {
|
||||||
@@ -216,19 +239,44 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to chain damage/debuff effects
|
||||||
|
Pair<SpellAbility, Integer> chainDmg = getDamagingSAToChain(ai, sa, damage);
|
||||||
|
|
||||||
|
// test what happens if we chain this to another damaging spell
|
||||||
|
if (chainDmg != null) {
|
||||||
|
int extraDmg = chainDmg.getValue();
|
||||||
|
boolean willTargetIfChained = this.damageTargetAI(ai, sa, dmg + extraDmg, false);
|
||||||
|
if (!willTargetIfChained) {
|
||||||
|
return false; // won't play it even in chain
|
||||||
|
} else if (willTargetIfChained && chainDmg.getKey().getApi() == ApiType.Pump && sa.getTargets().isTargetingAnyPlayer()) {
|
||||||
|
// we're trying to chain a pump spell to a damage spell targeting a player, that won't work
|
||||||
|
// so run an additional check to ensure that we want to cast the current spell separately
|
||||||
|
sa.resetTargets();
|
||||||
if (!this.damageTargetAI(ai, sa, dmg, false)) {
|
if (!this.damageTargetAI(ai, sa, dmg, false)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// we are about to decide to play this damage spell; if there's something chained to it, reserve mana for
|
||||||
|
// the second spell so we don't misplay
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
aic.reserveManaSourcesForNextSpell(chainDmg.getKey(), sa);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// simple targeting when there is no spell chaining plan
|
||||||
|
if (!this.damageTargetAI(ai, sa, dmg, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
|
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
|
||||||
sourceName.equals("Crater's Claws")){
|
sourceName.equals("Crater's Claws")){
|
||||||
// If I can kill my target by paying less mana, do it
|
// If I can kill my target by paying less mana, do it
|
||||||
if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.hasParam("DividedAsYouChoose")) {
|
if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.hasParam("DividedAsYouChoose")) {
|
||||||
int actualPay = 0;
|
int actualPay = dmg;
|
||||||
final boolean noPrevention = sa.hasParam("NoPrevention");
|
final boolean noPrevention = sa.hasParam("NoPrevention");
|
||||||
for (final Card c : sa.getTargets().getTargetCards()) {
|
for (final Card c : sa.getTargets().getTargetCards()) {
|
||||||
final int adjDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
final int adjDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
||||||
if ((adjDamage > actualPay) && (adjDamage <= dmg)) {
|
if (adjDamage < actualPay) {
|
||||||
actualPay = adjDamage;
|
actualPay = adjDamage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,6 +286,24 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
source.setSVar("PayX", Integer.toString(actualPay));
|
source.setSVar("PayX", Integer.toString(actualPay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("XCountersDamage".equals(logic) && sa.getPayCosts() != null) {
|
||||||
|
// Check to ensure that we have enough counters to remove per the defined PayX
|
||||||
|
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
|
if (part instanceof CostRemoveCounter) {
|
||||||
|
if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(source.getSVar("PayX"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
|
||||||
|
final int CMC = Integer.parseInt(source.getSVar("PayX"));
|
||||||
|
return !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(CMC)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +341,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
final Game game = source.getGame();
|
final Game game = source.getGame();
|
||||||
List<Card> hPlay = getTargetableCards(ai, sa, pl, tgt, activator, source, game);
|
List<Card> hPlay = getTargetableCards(ai, sa, pl, tgt, activator, source, game);
|
||||||
|
|
||||||
List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
CardCollection killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return c.getSVar("Targeting").equals("Dies")
|
return c.getSVar("Targeting").equals("Dies")
|
||||||
@@ -286,7 +352,10 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Filter AI-specific targets if provided
|
// Filter AI-specific targets if provided
|
||||||
killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true);
|
killables = ComputerUtil.filterAITgts(sa, ai, killables, true);
|
||||||
|
|
||||||
|
// Try not to target anything which will already be dead by the time the spell resolves
|
||||||
|
killables = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, killables, sa);
|
||||||
|
|
||||||
Card targetCard = null;
|
Card targetCard = null;
|
||||||
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
|
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
|
||||||
@@ -483,7 +552,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
||||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
Player enemy = ai.getWeakestOpponent();
|
||||||
|
|
||||||
if ("PowerDmg".equals(logic)) {
|
if ("PowerDmg".equals(logic)) {
|
||||||
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
|
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
|
||||||
@@ -494,7 +563,9 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tgt.getMaxTargets(source, sa) <= 0) {
|
// AssumeAtLeastOneTarget is used for cards with funky targeting implementation like Fight with Fire which would
|
||||||
|
// otherwise confuse the AI by returning 0 unexpectedly during SA "AI can play" tests.
|
||||||
|
if (tgt.getMaxTargets(source, sa) <= 0 && !logic.equals("AssumeAtLeastOneTarget")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +631,15 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int totalTargetedSoFar = -1;
|
||||||
while (tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
|
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.getNumTargeted();
|
||||||
if (oppTargetsChoice && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) {
|
if (oppTargetsChoice && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) {
|
||||||
// canPlayAI (sa activated by ai)
|
// canPlayAI (sa activated by ai)
|
||||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
|
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
|
||||||
@@ -574,10 +653,9 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
if (c != null && !this.shouldTgtP(ai, sa, dmg, noPrevention, true)) {
|
if (c != null && !this.shouldTgtP(ai, sa, dmg, noPrevention, true)) {
|
||||||
tcs.add(c);
|
tcs.add(c);
|
||||||
if (divided) {
|
if (divided) {
|
||||||
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
||||||
if (assignedDamage <= dmg) {
|
assignedDamage = Math.min(dmg, assignedDamage);
|
||||||
tgt.addDividedAllocation(c, assignedDamage);
|
tgt.addDividedAllocation(c, assignedDamage);
|
||||||
}
|
|
||||||
dmg = dmg - assignedDamage;
|
dmg = dmg - assignedDamage;
|
||||||
if (dmg <= 0) {
|
if (dmg <= 0) {
|
||||||
break;
|
break;
|
||||||
@@ -644,8 +722,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (phase.is(PhaseType.MAIN2) && sa.isAbility()) {
|
if (phase.is(PhaseType.MAIN2) && sa.isAbility()) {
|
||||||
if (sa.getRestrictions().isPwAbility()
|
if (sa.isPwAbility() || source.hasSVar("EndOfTurnLeavePlay"))
|
||||||
|| source.hasSVar("EndOfTurnLeavePlay"))
|
|
||||||
freePing = true;
|
freePing = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -799,7 +876,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
// this is for Triggered targets that are mandatory
|
// this is for Triggered targets that are mandatory
|
||||||
final boolean noPrevention = sa.hasParam("NoPrevention");
|
final boolean noPrevention = sa.hasParam("NoPrevention");
|
||||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
|
|
||||||
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||||
if (tgt.canTgtPlaneswalker()) {
|
if (tgt.canTgtPlaneswalker()) {
|
||||||
@@ -962,4 +1039,90 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
source.setSVar("PayX", Integer.toString(dmg));
|
source.setSVar("PayX", Integer.toString(dmg));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a pair of a SpellAbility (APIType DealDamage or Pump) and damage/debuff amount
|
||||||
|
// The returned spell ability can be chained to "sa" to deal more damage (enough mana is available to cast both
|
||||||
|
// and can be properly reserved).
|
||||||
|
public static Pair<SpellAbility, Integer> getDamagingSAToChain(Player ai, SpellAbility sa, String damage) {
|
||||||
|
if (!ai.getController().isAI()) {
|
||||||
|
return null; // should only work for the actual AI player
|
||||||
|
} else if (((PlayerControllerAi)ai.getController()).getAi().usesSimulation()) {
|
||||||
|
// simulated AI shouldn't use paired decisions, it tries to find complex decisions on its own
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Game game = ai.getGame();
|
||||||
|
int chance = ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_CHAIN_TWO_DAMAGE_SPELLS);
|
||||||
|
|
||||||
|
if (chance > 0 && (ComputerUtilCombat.lifeInDanger(ai, game.getCombat()) || ComputerUtil.aiLifeInDanger(ai, true, 0))) {
|
||||||
|
chance = 100; // in danger, do it even if normally the chance is low (unless chaining is completely disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MyRandom.percentTrue(chance)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sa.getSubAbility() != null || sa.getParent() != null) {
|
||||||
|
// Doesn't work yet for complex decisions where damage is only a part of the decision process
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to chain damage/debuff effects
|
||||||
|
if (StringUtils.isNumeric(damage) || (damage.startsWith("-") && StringUtils.isNumeric(damage.substring(1)))) {
|
||||||
|
// currently only works for predictable numeric damage
|
||||||
|
CardCollection cards = new CardCollection();
|
||||||
|
cards.addAll(ai.getCardsIn(ZoneType.Hand));
|
||||||
|
cards.addAll(ai.getCardsIn(ZoneType.Battlefield));
|
||||||
|
cards.addAll(ai.getCardsActivableInExternalZones(true));
|
||||||
|
for (Card c : cards) {
|
||||||
|
for (SpellAbility ab : c.getSpellAbilities()) {
|
||||||
|
if (ab.equals(sa) || ab.getSubAbility() != null) { // decisions for complex SAs with subs are not supported yet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!ab.canPlay()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// currently works only with cards that don't have additional costs (only mana is supported)
|
||||||
|
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");
|
||||||
|
} else if (ab.getApi() == ApiType.Pump) {
|
||||||
|
dmgDef = ab.getParamOrDefault("NumDef", "0");
|
||||||
|
if (dmgDef.startsWith("-")) {
|
||||||
|
dmgDef = dmgDef.substring(1);
|
||||||
|
} else {
|
||||||
|
continue; // not a toughness debuff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtils.isNumeric(dmgDef)) { // 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();
|
||||||
|
TargetRestrictions tgtAb = sa.getTargetRestrictions();
|
||||||
|
String[] validTgtsSa = tgtSa.getValidTgts();
|
||||||
|
String[] validTgtsAb = tgtAb.getValidTgts();
|
||||||
|
if (!Arrays.asList(validTgtsSa).containsAll(Arrays.asList(validTgtsAb))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: should it also check restrictions for targeting players?
|
||||||
|
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));
|
||||||
|
// can we pay both costs?
|
||||||
|
if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) {
|
||||||
|
return Pair.of(ab, Integer.parseInt(dmgDef));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.google.common.base.Predicate;
|
|||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -177,7 +176,7 @@ public class DebuffAi extends SpellAbilityAi {
|
|||||||
* @return a CardCollection.
|
* @return a CardCollection.
|
||||||
*/
|
*/
|
||||||
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
|
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
|
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@@ -217,7 +216,7 @@ public class DebuffAi extends SpellAbilityAi {
|
|||||||
list.remove(c);
|
list.remove(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
final CardCollection pref = CardLists.filterControlledBy(list, ComputerUtil.getOpponentFor(ai));
|
final CardCollection pref = CardLists.filterControlledBy(list, ai.getWeakestOpponent());
|
||||||
final CardCollection forced = CardLists.filterControlledBy(list, ai);
|
final CardCollection forced = CardLists.filterControlledBy(list, ai);
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to avoid targeting creatures that are dead on board
|
||||||
|
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -271,7 +273,7 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
} else {
|
} else {
|
||||||
// Don't destroy stolen permanents when the stealing aura can be destroyed
|
// Don't destroy stolen permanents when the stealing aura can be destroyed
|
||||||
if (choice.getOwner() == ai) {
|
if (choice.getOwner() == ai) {
|
||||||
for (Card aura : choice.getEnchantedBy(false)) {
|
for (Card aura : choice.getEnchantedBy()) {
|
||||||
SpellAbility sp = aura.getFirstSpellAbility();
|
SpellAbility sp = aura.getFirstSpellAbility();
|
||||||
if (sp != null && "GainControl".equals(sp.getParam("AILogic"))
|
if (sp != null && "GainControl".equals(sp.getParam("AILogic"))
|
||||||
&& aura.getController() != ai && sa.canTarget(aura)) {
|
&& aura.getController() != ai && sa.canTarget(aura)) {
|
||||||
@@ -286,7 +288,7 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
} else if (sa.hasParam("Defined")) {
|
} else if (sa.hasParam("Defined")) {
|
||||||
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||||
if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai)
|
if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai)
|
||||||
|| ai.getCreaturesInPlay().size() < ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size()
|
|| ai.getCreaturesInPlay().size() < ai.getWeakestOpponent().getCreaturesInPlay().size()
|
||||||
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|
||||||
|| ai.getLife() <= 5)) {
|
|| ai.getLife() <= 5)) {
|
||||||
// Basic ai logic for Lethal Vapors
|
// Basic ai logic for Lethal Vapors
|
||||||
@@ -313,6 +315,9 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
|
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
|
||||||
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
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)) {
|
if (list.isEmpty() || list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
|||||||
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
Player opponent = ComputerUtil.getOpponentFor(ai); // TODO: how should this AI logic work for multiplayer and getOpponents()?
|
Player opponent = ai.getWeakestOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()?
|
||||||
|
|
||||||
final int CREATURE_EVAL_THRESHOLD = 200;
|
final int CREATURE_EVAL_THRESHOLD = 200;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicate;
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardCollection;
|
||||||
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
@@ -20,7 +24,7 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
final Card host = sa.getHostCard();
|
final Card host = sa.getHostCard();
|
||||||
Player libraryOwner = ai;
|
Player libraryOwner = ai;
|
||||||
|
|
||||||
@@ -42,7 +46,9 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||||
return false;
|
return false;
|
||||||
} else if ("AtOppEndOfTurn".equals(sa.getParam("AILogic"))) {
|
} else if ("AtOppEndOfTurn".equals(sa.getParam("AILogic"))) {
|
||||||
return game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN);
|
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't deck yourself
|
// don't deck yourself
|
||||||
@@ -60,7 +66,8 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String num = sa.getParam("DigNum");
|
final String num = sa.getParam("DigNum");
|
||||||
if (num != null && num.equals("X") && host.getSVar(num).equals("Count$xPaid")) {
|
final boolean payXLogic = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("PayX");
|
||||||
|
if (num != null && (num.equals("X") && host.getSVar(num).equals("Count$xPaid")) || payXLogic) {
|
||||||
// By default, set PayX here to maximum value.
|
// By default, set PayX here to maximum value.
|
||||||
if (!(sa instanceof AbilitySub) || host.getSVar("PayX").equals("")) {
|
if (!(sa instanceof AbilitySub) || host.getSVar("PayX").equals("")) {
|
||||||
int manaToSave = 0;
|
int manaToSave = 0;
|
||||||
@@ -99,7 +106,7 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (mandatory && sa.canTarget(opp)) {
|
if (mandatory && sa.canTarget(opp)) {
|
||||||
@@ -124,6 +131,25 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> valid, boolean isOptional, Player relatedPlayer) {
|
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> valid, boolean isOptional, Player relatedPlayer) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (sa.getActivatingPlayer().isOpponentOf(ai) && relatedPlayer.isOpponentOf(ai)) {
|
if (sa.getActivatingPlayer().isOpponentOf(ai) && relatedPlayer.isOpponentOf(ai)) {
|
||||||
return ComputerUtilCard.getWorstPermanentAI(valid, false, true, false, false);
|
return ComputerUtilCard.getWorstPermanentAI(valid, false, true, false, false);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -36,7 +35,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
|||||||
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||||
|
|
||||||
Player libraryOwner = ai;
|
Player libraryOwner = ai;
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
|
|
||||||
if ("DontMillSelf".equals(logic)) {
|
if ("DontMillSelf".equals(logic)) {
|
||||||
// A card that digs for specific things and puts everything revealed before it into graveyard
|
// A card that digs for specific things and puts everything revealed before it into graveyard
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
|
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
|
final boolean humanHasHand = ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).size() > 0;
|
||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
if (!discardTargetAI(ai, sa)) {
|
if (!discardTargetAI(ai, sa)) {
|
||||||
@@ -87,7 +87,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
if (sa.hasParam("NumCards")) {
|
if (sa.hasParam("NumCards")) {
|
||||||
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
|
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
|
||||||
// Set PayX here to maximum value.
|
// Set PayX here to maximum value.
|
||||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
|
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getWeakestOpponent()
|
||||||
.getCardsIn(ZoneType.Hand).size());
|
.getCardsIn(ZoneType.Hand).size());
|
||||||
if (cardsToDiscard < 1) {
|
if (cardsToDiscard < 1) {
|
||||||
return false;
|
return false;
|
||||||
@@ -150,9 +150,11 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
for (Player opp : ai.getOpponents()) {
|
||||||
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
|
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
|
||||||
return false;
|
continue;
|
||||||
|
} else if (!opp.canDiscardBy(sa)) { // e.g. Tamiyo, Collector of Tales
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
if (sa.canTarget(opp)) {
|
if (sa.canTarget(opp)) {
|
||||||
@@ -160,6 +162,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} // discardTargetAI()
|
} // discardTargetAI()
|
||||||
|
|
||||||
@@ -169,7 +172,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
if (!discardTargetAI(ai, sa)) {
|
if (!discardTargetAI(ai, sa)) {
|
||||||
if (mandatory && sa.canTarget(opp)) {
|
if (mandatory && sa.canTarget(opp)) {
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
@@ -190,7 +193,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
||||||
// Set PayX here to maximum value.
|
// Set PayX here to maximum value.
|
||||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
|
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getWeakestOpponent()
|
||||||
.getCardsIn(ZoneType.Hand).size());
|
.getCardsIn(ZoneType.Hand).size());
|
||||||
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
|
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -19,7 +18,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
if (tgt == null) {
|
if (tgt == null) {
|
||||||
@@ -41,7 +40,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
@@ -82,7 +81,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
sa.getTargets().add(ai.getWeakestOpponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
return randomReturn;
|
return randomReturn;
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source)) {
|
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +131,8 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
} else if (logic.equals("AlwaysAtOppEOT")) {
|
} else if (logic.equals("AlwaysAtOppEOT")) {
|
||||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||||
|
} else if (logic.equals("RespondToOwnActivation")) {
|
||||||
|
return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't use draw abilities before main 2 if possible
|
// Don't use draw abilities before main 2 if possible
|
||||||
@@ -209,9 +211,11 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
private boolean targetAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
private boolean targetAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final boolean drawback = sa.getParent() != null;
|
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
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();
|
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||||
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
|
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
|
||||||
@@ -239,7 +243,12 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
numCards = Integer.parseInt(source.getSVar("PayX"));
|
numCards = Integer.parseInt(source.getSVar("PayX"));
|
||||||
} else {
|
} else {
|
||||||
numCards = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
numCards = ComputerUtilMana.determineLeftoverMana(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);
|
||||||
source.setSVar("PayX", Integer.toString(numCards));
|
source.setSVar("PayX", Integer.toString(numCards));
|
||||||
|
assumeSafeX = true;
|
||||||
}
|
}
|
||||||
xPaid = true;
|
xPaid = true;
|
||||||
}
|
}
|
||||||
@@ -363,6 +372,10 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
if (numCards >= computerLibrarySize) {
|
if (numCards >= computerLibrarySize) {
|
||||||
if (xPaid) {
|
if (xPaid) {
|
||||||
numCards = computerLibrarySize - 1;
|
numCards = computerLibrarySize - 1;
|
||||||
|
if (numCards <= 0 && !mandatory) {
|
||||||
|
// not drawing anything, so don't do it
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} else if (!ai.isCardInPlay("Laboratory Maniac")) {
|
} else if (!ai.isCardInPlay("Laboratory Maniac")) {
|
||||||
aiTarget = false;
|
aiTarget = false;
|
||||||
}
|
}
|
||||||
@@ -396,6 +409,9 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
if (computerHandSize + numCards > computerMaxHandSize && game.getPhaseHandler().isPlayerTurn(ai)) {
|
if (computerHandSize + numCards > computerMaxHandSize && game.getPhaseHandler().isPlayerTurn(ai)) {
|
||||||
if (xPaid) {
|
if (xPaid) {
|
||||||
numCards = computerMaxHandSize - computerHandSize;
|
numCards = computerMaxHandSize - computerHandSize;
|
||||||
|
if (sa.getHostCard().getZone().is(ZoneType.Hand)) {
|
||||||
|
numCards++; // the card will be spent
|
||||||
|
}
|
||||||
source.setSVar("PayX", Integer.toString(numCards));
|
source.setSVar("PayX", Integer.toString(numCards));
|
||||||
} else {
|
} else {
|
||||||
// Don't draw too many cards and then risk discarding
|
// Don't draw too many cards and then risk discarding
|
||||||
@@ -483,7 +499,8 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if ((computerHandSize + numCards > computerMaxHandSize)
|
if ((computerHandSize + numCards > computerMaxHandSize)
|
||||||
&& game.getPhaseHandler().isPlayerTurn(ai)
|
&& game.getPhaseHandler().isPlayerTurn(ai)
|
||||||
&& !sa.isTrigger()) {
|
&& !sa.isTrigger()
|
||||||
|
&& !assumeSafeX) {
|
||||||
// Don't draw too many cards and then risk discarding cards at
|
// Don't draw too many cards and then risk discarding cards at
|
||||||
// EOT
|
// EOT
|
||||||
if (!drawback) {
|
if (!drawback) {
|
||||||
|
|||||||
@@ -6,17 +6,14 @@ import com.google.common.base.Predicate;
|
|||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.card.mana.ManaCost;
|
||||||
import forge.ai.ComputerUtilCombat;
|
|
||||||
import forge.ai.SpecialCardAi;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.ai.SpellApiToAi;
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
|
import forge.game.cost.Cost;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -116,10 +113,39 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
} else if (logic.equals("SpellCopy")) {
|
} else if (logic.equals("SpellCopy")) {
|
||||||
// fetch Instant or Sorcery and AI has reason to play this turn
|
// fetch Instant or Sorcery and AI has reason to play this turn
|
||||||
// does not try to get itself
|
// 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>() {
|
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return (c.isInstant() || c.isSorcery()) && c != sa.getHostCard() && ComputerUtil.hasReasonToPlayCardThisTurn(ai, 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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,8 +165,26 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
|
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return (c.isInstant() || c.isSorcery()) && !c.hasKeyword(Keyword.REBOUND)
|
if (!(c.isInstant() || c.isSorcery()) || c.hasKeyword(Keyword.REBOUND)) {
|
||||||
&& ComputerUtil.hasReasonToPlayCardThisTurn(ai, c);
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ public class FightAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||||
|
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||||
|
return true; // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
|
||||||
|
}
|
||||||
|
|
||||||
return checkApiLogic(aiPlayer, sa);
|
return checkApiLogic(aiPlayer, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +196,28 @@ public class FightAi extends SpellAbilityAi {
|
|||||||
toughness += bonus;
|
toughness += bonus;
|
||||||
}
|
}
|
||||||
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
|
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
|
||||||
|
if ("2".equals(sa.getParam("TargetMax"))) {
|
||||||
|
// Band Together, uses up to two targets to deal damage to a single target
|
||||||
|
// TODO: Generalize this so that other TargetMax values can be properly accounted for
|
||||||
|
CardCollection aiCreaturesByPower = new CardCollection(aiCreatures);
|
||||||
|
CardLists.sortByPowerDesc(aiCreaturesByPower);
|
||||||
|
Card maxPower = aiCreaturesByPower.getFirst();
|
||||||
|
if (maxPower != null && maxPower != aiCreature) {
|
||||||
|
power += maxPower.getNetPower(); // potential bonus from adding a second target
|
||||||
|
}
|
||||||
|
if (FightAi.canKill(aiCreature, humanCreature, power)) {
|
||||||
|
sa.getTargets().add(aiCreature);
|
||||||
|
if (maxPower != null) {
|
||||||
|
sa.getTargets().add(maxPower);
|
||||||
|
}
|
||||||
|
if (!isChandrasIgnition) {
|
||||||
|
tgtFight.resetTargets();
|
||||||
|
tgtFight.getTargets().add(humanCreature);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other cards that use AILogic PowerDmg and a single target
|
||||||
if (FightAi.canKill(aiCreature, humanCreature, power)) {
|
if (FightAi.canKill(aiCreature, humanCreature, power)) {
|
||||||
sa.getTargets().add(aiCreature);
|
sa.getTargets().add(aiCreature);
|
||||||
if (!isChandrasIgnition) {
|
if (!isChandrasIgnition) {
|
||||||
@@ -200,6 +226,7 @@ public class FightAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (FightAi.shouldFight(aiCreature, humanCreature, power, toughness)) {
|
if (FightAi.shouldFight(aiCreature, humanCreature, power, toughness)) {
|
||||||
if ("Time to Feed".equals(sourceName)) { // flip targets
|
if ("Time to Feed".equals(sourceName)) { // flip targets
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ public class FogAi extends SpellAbilityAi {
|
|||||||
|| (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)))
|
|| (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)))
|
||||||
&& (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
|
&& (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
|
||||||
&& (ComputerUtil.aiLifeInDanger(ai, false, 0))) {
|
&& (ComputerUtil.aiLifeInDanger(ai, false, 0))) {
|
||||||
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, true);
|
boolean reserved = ((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, true);
|
||||||
|
if (reserved) {
|
||||||
AiCardMemory.rememberCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
|
AiCardMemory.rememberCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AI should only activate this during Human's Declare Blockers phase
|
// AI should only activate this during Human's Declare Blockers phase
|
||||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
||||||
@@ -105,7 +107,7 @@ public class FogAi extends SpellAbilityAi {
|
|||||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||||
final Game game = aiPlayer.getGame();
|
final Game game = aiPlayer.getGame();
|
||||||
boolean chance;
|
boolean chance;
|
||||||
if (game.getPhaseHandler().isPlayerTurn(ComputerUtil.getOpponentFor(sa.getActivatingPlayer()))) {
|
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer().getWeakestOpponent())) {
|
||||||
chance = game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE);
|
chance = game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE);
|
||||||
} else {
|
} else {
|
||||||
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -9,7 +8,7 @@ import forge.game.spellability.TargetRestrictions;
|
|||||||
public class GameLossAi extends SpellAbilityAi {
|
public class GameLossAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
if (opp.cantLose()) {
|
if (opp.cantLose()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -34,15 +33,16 @@ public class GameLossAi extends SpellAbilityAi {
|
|||||||
// Phage the Untouchable
|
// Phage the Untouchable
|
||||||
// (Final Fortune would need to attach it's delayed trigger to a
|
// (Final Fortune would need to attach it's delayed trigger to a
|
||||||
// specific turn, which can't be done yet)
|
// specific turn, which can't be done yet)
|
||||||
|
Player opp = ai.getWeakestOpponent();
|
||||||
|
|
||||||
if (!mandatory && ComputerUtil.getOpponentFor(ai).cantLose()) {
|
if (!mandatory && opp.cantLose()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
sa.getTargets().add(opp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -20,7 +19,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
final int myLife = aiPlayer.getLife();
|
final int myLife = aiPlayer.getLife();
|
||||||
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
|
Player opponent = aiPlayer.getWeakestOpponent();
|
||||||
final int hLife = opponent.getLife();
|
final int hLife = opponent.getLife();
|
||||||
|
|
||||||
if (!aiPlayer.canGainLife()) {
|
if (!aiPlayer.canGainLife()) {
|
||||||
@@ -76,7 +75,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
|||||||
final boolean mandatory) {
|
final boolean mandatory) {
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilAbility;
|
|
||||||
import forge.ai.ComputerUtilCombat;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.MagicStack;
|
import forge.game.zone.MagicStack;
|
||||||
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
public class LifeExchangeVariantAi extends SpellAbilityAi {
|
public class LifeExchangeVariantAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -84,18 +84,40 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
|
|||||||
return shouldDo;
|
return shouldDo;
|
||||||
}
|
}
|
||||||
else if ("Evra, Halcyon Witness".equals(sourceName)) {
|
else if ("Evra, Halcyon Witness".equals(sourceName)) {
|
||||||
|
int aiLife = ai.getLife();
|
||||||
|
|
||||||
|
// Offensive use of Evra, try to kill the opponent or deal a lot of damage, and hopefully gain a lot of life too
|
||||||
|
if (game.getCombat() != null && game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||||
|
&& game.getCombat().isAttacking(source) && source.getNetPower() > 0
|
||||||
|
&& source.getNetPower() < aiLife) {
|
||||||
|
Player def = game.getCombat().getDefenderPlayerByAttacker(source);
|
||||||
|
if (game.getCombat().isUnblocked(source) && def.canLoseLife() && aiLife >= def.getLife() && source.getNetPower() < def.getLife()) {
|
||||||
|
// Unblocked Evra which can deal lethal damage
|
||||||
|
return true;
|
||||||
|
} else if (ai.getController().isAI() && aiLife > source.getNetPower() && source.hasKeyword(Keyword.LIFELINK)) {
|
||||||
|
int dangerMin = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
|
||||||
|
int dangerMax = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_MAX_THRESHOLD));
|
||||||
|
int dangerDiff = dangerMax - dangerMin;
|
||||||
|
int lifeInDanger = dangerDiff <= 0 ? dangerMin : MyRandom.getRandom().nextInt(dangerDiff) + dangerMin;
|
||||||
|
if (source.getNetPower() >= lifeInDanger && ai.canGainLife() && ComputerUtil.lifegainPositive(ai, source)) {
|
||||||
|
// Blocked or unblocked Evra which will get bigger *and* we're getting our life back through Lifelink
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive use of Evra, try to debuff Evra to try to gain some life
|
||||||
|
if (source.getNetPower() > aiLife) {
|
||||||
|
// Only makes sense if the AI can actually gain life from this
|
||||||
if (!ai.canGainLife())
|
if (!ai.canGainLife())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
int aiLife = ai.getLife();
|
if (ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
|
||||||
|
|
||||||
if (source.getNetPower() > aiLife) {
|
|
||||||
if (ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat())) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the top of stack
|
// check the top of stack
|
||||||
MagicStack stack = ai.getGame().getStack();
|
MagicStack stack = game.getStack();
|
||||||
if (!stack.isEmpty()) {
|
if (!stack.isEmpty()) {
|
||||||
SpellAbility saTop = stack.peekAbility();
|
SpellAbility saTop = stack.peekAbility();
|
||||||
if (ComputerUtil.predictDamageFromSpell(saTop, ai) >= aiLife) {
|
if (ComputerUtil.predictDamageFromSpell(saTop, ai) >= aiLife) {
|
||||||
@@ -103,6 +125,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -126,7 +149,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
|
|||||||
final boolean mandatory) {
|
final boolean mandatory) {
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ public class LifeLoseAi extends SpellAbilityAi {
|
|||||||
if (tgtPlayers.contains(ai) && amount > 0 && amount + 3 > ai.getLife()) {
|
if (tgtPlayers.contains(ai) && amount > 0 && amount + 3 > ai.getLife()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sa.usesTargeting()) {
|
||||||
|
if (!doTgt(ai, sa, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilAbility;
|
import forge.ai.ComputerUtilAbility;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -20,7 +19,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
// Ability_Cost abCost = sa.getPayCosts();
|
// Ability_Cost abCost = sa.getPayCosts();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final int myLife = ai.getLife();
|
final int myLife = ai.getLife();
|
||||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
final Player opponent = ai.getWeakestOpponent();
|
||||||
final int hlife = opponent.getLife();
|
final int hlife = opponent.getLife();
|
||||||
final String amountStr = sa.getParam("LifeAmount");
|
final String amountStr = sa.getParam("LifeAmount");
|
||||||
|
|
||||||
@@ -107,7 +106,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final int myLife = ai.getLife();
|
final int myLife = ai.getLife();
|
||||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
final Player opponent = ai.getWeakestOpponent();
|
||||||
final int hlife = opponent.getLife();
|
final int hlife = opponent.getLife();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import forge.game.phase.PhaseType;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.Aggregates;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,8 +30,10 @@ public class ManaEffectAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) {
|
protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) {
|
||||||
if ("ManaRitual".equals(aiLogic)) {
|
if (aiLogic.startsWith("ManaRitual")) {
|
||||||
return doManaRitualLogic(ai, sa);
|
return doManaRitualLogic(ai, sa);
|
||||||
|
} else if ("Always".equals(aiLogic)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return super.checkAiLogic(ai, sa, aiLogic);
|
return super.checkAiLogic(ai, sa, aiLogic);
|
||||||
}
|
}
|
||||||
@@ -104,6 +107,7 @@ public class ManaEffectAi extends SpellAbilityAi {
|
|||||||
// Dark Ritual and other similar instants/sorceries that add mana to mana pool
|
// Dark Ritual and other similar instants/sorceries that add mana to mana pool
|
||||||
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
|
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
|
||||||
final Card host = sa.getHostCard();
|
final Card host = sa.getHostCard();
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
|
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||||
int numManaSrcs = manaSources.size();
|
int numManaSrcs = manaSources.size();
|
||||||
@@ -115,7 +119,9 @@ public class ManaEffectAi extends SpellAbilityAi {
|
|||||||
String produced = sa.getParam("Produced");
|
String produced = sa.getParam("Produced");
|
||||||
byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced);
|
byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced);
|
||||||
|
|
||||||
if ("ChosenX".equals(sa.getParam("Amount"))
|
int numCounters = 0;
|
||||||
|
int manaSurplus = 0;
|
||||||
|
if ("XChoice".equals(host.getSVar("X"))
|
||||||
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
|
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
|
||||||
CounterType ctrType = CounterType.KI; // Petalmane Baku
|
CounterType ctrType = CounterType.KI; // Petalmane Baku
|
||||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||||
@@ -124,7 +130,12 @@ public class ManaEffectAi extends SpellAbilityAi {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manaReceived = host.getCounters(ctrType);
|
numCounters = host.getCounters(ctrType);
|
||||||
|
manaReceived = numCounters;
|
||||||
|
if (logic.startsWith("ManaRitualBattery.")) {
|
||||||
|
manaSurplus = Integer.valueOf(logic.substring(18)); // adds an extra mana even if no counters removed
|
||||||
|
manaReceived += manaSurplus;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int searchCMC = numManaSrcs - selfCost + manaReceived;
|
int searchCMC = numManaSrcs - selfCost + manaReceived;
|
||||||
@@ -194,6 +205,13 @@ public class ManaEffectAi extends SpellAbilityAi {
|
|||||||
CardPredicates.lessCMC(searchCMC),
|
CardPredicates.lessCMC(searchCMC),
|
||||||
Predicates.or(CardPredicates.isColorless(), CardPredicates.isColor(producedColor))));
|
Predicates.or(CardPredicates.isColorless(), CardPredicates.isColor(producedColor))));
|
||||||
|
|
||||||
|
if (logic.startsWith("ManaRitualBattery")) {
|
||||||
|
// Don't remove more counters than would be needed to cast the more expensive thing we want to cast,
|
||||||
|
// otherwise the AI grabs too many counters at once.
|
||||||
|
int maxCtrs = Aggregates.max(castableSpells, CardPredicates.Accessors.fnGetCmc) - manaSurplus;
|
||||||
|
sa.setSVar("ChosenX", "Number$" + Math.min(numCounters, maxCtrs));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: this will probably still waste the card from time to time. Somehow improve detection of castable material.
|
// TODO: this will probably still waste the card from time to time. Somehow improve detection of castable material.
|
||||||
return castableSpells.size() > 0;
|
return castableSpells.size() > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.google.common.collect.Maps;
|
|||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.card.CardStateName;
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
@@ -104,7 +103,7 @@ public class ManifestAi extends SpellAbilityAi {
|
|||||||
// check to ensure that there are no replacement effects that prevent creatures ETBing from library
|
// check to ensure that there are no replacement effects that prevent creatures ETBing from library
|
||||||
// (e.g. Grafdigger's Cage)
|
// (e.g. Grafdigger's Cage)
|
||||||
Card topCopy = CardUtil.getLKICopy(library.getFirst());
|
Card topCopy = CardUtil.getLKICopy(library.getFirst());
|
||||||
topCopy.setState(CardStateName.FaceDown, false);
|
topCopy.turnFaceDownNoUpdate();
|
||||||
topCopy.setManifested(true);
|
topCopy.setManifested(true);
|
||||||
|
|
||||||
final Map<String, Object> repParams = Maps.newHashMap();
|
final Map<String, Object> repParams = Maps.newHashMap();
|
||||||
@@ -113,7 +112,7 @@ public class ManifestAi extends SpellAbilityAi {
|
|||||||
repParams.put("Origin", ZoneType.Library);
|
repParams.put("Origin", ZoneType.Library);
|
||||||
repParams.put("Destination", ZoneType.Battlefield);
|
repParams.put("Destination", ZoneType.Battlefield);
|
||||||
repParams.put("Source", sa.getHostCard());
|
repParams.put("Source", sa.getHostCard());
|
||||||
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.None);
|
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.Other);
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ public class MillAi extends SpellAbilityAi {
|
|||||||
} else if ("ExileAndPlayOrDealDamage".equals(sa.getParam("AILogic"))) {
|
} else if ("ExileAndPlayOrDealDamage".equals(sa.getParam("AILogic"))) {
|
||||||
return (ph.is(PhaseType.MAIN1) || ph.is(PhaseType.MAIN2)) && ph.isPlayerTurn(ai); // Chandra, Torch of Defiance and similar
|
return (ph.is(PhaseType.MAIN1) || ph.is(PhaseType.MAIN2)) && ph.isPlayerTurn(ai); // Chandra, Torch of Defiance and similar
|
||||||
}
|
}
|
||||||
|
if (!sa.hasParam("Planeswalker")) { // Planeswalker abilities are only activated at sorcery speed
|
||||||
if ("You".equals(sa.getParam("Defined")) && !(!SpellAbilityAi.isSorcerySpeed(sa) && ph.is(PhaseType.END_OF_TURN)
|
if ("You".equals(sa.getParam("Defined")) && !(!SpellAbilityAi.isSorcerySpeed(sa) && ph.is(PhaseType.END_OF_TURN)
|
||||||
&& ph.getNextTurn().equals(ai))) {
|
&& ph.getNextTurn().equals(ai))) {
|
||||||
return false; // only self-mill at opponent EOT
|
return false; // only self-mill at opponent EOT
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) {
|
if (sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) {
|
||||||
if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai))) {
|
if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai))) {
|
||||||
// creatures with a tap cost to mill (e.g. Doorkeeper) should be activated at the opponent's end step
|
// creatures with a tap cost to mill (e.g. Doorkeeper) should be activated at the opponent's end step
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import com.google.common.collect.Lists;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.game.Game;
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates;
|
import forge.game.card.CardPredicates;
|
||||||
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -23,16 +25,47 @@ public class MustBlockAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
// disabled for the AI until he/she can make decisions about who to make
|
final Card source = sa.getHostCard();
|
||||||
// block
|
final Game game = aiPlayer.getGame();
|
||||||
|
final Combat combat = game.getCombat();
|
||||||
|
final PhaseHandler ph = game.getPhaseHandler();
|
||||||
|
final boolean onlyLethal = !"AllowNonLethal".equals(sa.getParam("AILogic"));
|
||||||
|
|
||||||
|
if (combat == null || !combat.isAttacking(source)) {
|
||||||
|
return false;
|
||||||
|
} else if (AiCardMemory.isRememberedCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||||
|
// The AI can meaningfully do it only to one creature per card yet, trying to do it to multiple cards
|
||||||
|
// may result in overextending and losing the attacker
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TargetRestrictions abTgt = sa.getTargetRestrictions();
|
||||||
|
final List<Card> list = determineGoodBlockers(source, aiPlayer, combat.getDefenderPlayerByAttacker(source), sa, onlyLethal,false);
|
||||||
|
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
|
||||||
|
if (blocker == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sa.getTargets().add(blocker);
|
||||||
|
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||||
|
if (sa.hasParam("DefinedAttacker")) {
|
||||||
|
// The AI can't handle "target creature blocks another target creature" abilities yet
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise it's a standard targeted "target creature blocks CARDNAME" ability, so use the main canPlayAI code path
|
||||||
|
return canPlayAI(aiPlayer, sa);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
@@ -62,27 +95,7 @@ public class MustBlockAi extends SpellAbilityAi {
|
|||||||
boolean chance = false;
|
boolean chance = false;
|
||||||
|
|
||||||
if (abTgt != null) {
|
if (abTgt != null) {
|
||||||
List<Card> list = CardLists.filter(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
final List<Card> list = determineGoodBlockers(definedAttacker, ai, ai.getWeakestOpponent(), sa, true,true);
|
||||||
list = CardLists.getTargetableCards(list, sa);
|
|
||||||
list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
|
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
|
||||||
@Override
|
|
||||||
public boolean apply(final Card c) {
|
|
||||||
boolean tapped = c.isTapped();
|
|
||||||
c.setTapped(false);
|
|
||||||
if (!CombatUtil.canBlock(definedAttacker, c)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (ComputerUtilCombat.canDestroyAttacker(ai, definedAttacker, c, null, false)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ComputerUtilCombat.canDestroyBlocker(ai, c, definedAttacker, null, false)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
c.setTapped(tapped);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -90,6 +103,20 @@ public class MustBlockAi extends SpellAbilityAi {
|
|||||||
if (blocker == null) {
|
if (blocker == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source.hasKeyword(Keyword.PROVOKE) && blocker.isTapped()) {
|
||||||
|
// Don't provoke if the attack is potentially lethal
|
||||||
|
Combat combat = ai.getGame().getCombat();
|
||||||
|
if (combat != null) {
|
||||||
|
Player defender = combat.getDefenderPlayerByAttacker(source);
|
||||||
|
if (defender != null && combat.getAttackingPlayer().equals(ai)
|
||||||
|
&& defender.canLoseLife() && !defender.cantLoseForZeroOrLessLife()
|
||||||
|
&& ComputerUtilCombat.lifeThatWouldRemain(defender, combat) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sa.getTargets().add(blocker);
|
sa.getTargets().add(blocker);
|
||||||
chance = true;
|
chance = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -98,4 +125,40 @@ public class MustBlockAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
return chance;
|
return chance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Card> determineGoodBlockers(final Card attacker, final Player ai, Player defender, SpellAbility sa,
|
||||||
|
final boolean onlyLethal, final boolean testTapped) {
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
final TargetRestrictions abTgt = sa.getTargetRestrictions();
|
||||||
|
|
||||||
|
List<Card> list = Lists.newArrayList();
|
||||||
|
list = CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||||
|
list = CardLists.getTargetableCards(list, sa);
|
||||||
|
list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
|
||||||
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(final Card c) {
|
||||||
|
boolean tapped = c.isTapped();
|
||||||
|
if (testTapped) {
|
||||||
|
c.setTapped(false);
|
||||||
|
}
|
||||||
|
if (!CombatUtil.canBlock(attacker, c)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, c, null, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (onlyLethal && !ComputerUtilCombat.canDestroyBlocker(ai, c, attacker, null, false)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (testTapped) {
|
||||||
|
c.setTapped(tapped);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,23 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
if (card.getType().isLegendary()
|
if (card.getType().isLegendary()
|
||||||
&& !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
|
&& !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
|
||||||
if (ai.isCardInPlay(card.getName())) {
|
if (ai.isCardInPlay(card.getName())) {
|
||||||
|
if (!card.hasSVar("AILegendaryException")) {
|
||||||
// AiPlayDecision.WouldDestroyLegend
|
// AiPlayDecision.WouldDestroyLegend
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
String specialRule = card.getSVar("AILegendaryException");
|
||||||
|
if ("TwoCopiesAllowed".equals(specialRule)) {
|
||||||
|
// One extra copy allowed on the battlefield, e.g. Brothers Yamazaki
|
||||||
|
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())).size() > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("AlwaysAllowed".equals(specialRule)) {
|
||||||
|
// Nothing to do here, check for Legendary is disabled
|
||||||
|
} else {
|
||||||
|
// Unknown hint, assume two copies not allowed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +207,10 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
// be better to have a pristine copy of the card - might not always be a correct assumption, but sounds
|
// be better to have a pristine copy of the card - might not always be a correct assumption, but sounds
|
||||||
// like a reasonable default for some cards).
|
// like a reasonable default for some cards).
|
||||||
for (Card c : ctrld) {
|
for (Card c : ctrld) {
|
||||||
if (c.getEnchantedBy(false).isEmpty()) {
|
if (c.getEnchantedBy().isEmpty()) {
|
||||||
numControlled++;
|
numControlled++;
|
||||||
} else {
|
} else {
|
||||||
for (Card att : c.getEnchantedBy(false)) {
|
for (Card att : c.getEnchantedBy()) {
|
||||||
if (!att.getController().isOpponentOf(ai)) {
|
if (!att.getController().isOpponentOf(ai)) {
|
||||||
numControlled++;
|
numControlled++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import com.google.common.base.Predicate;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilCost;
|
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardUtil;
|
import forge.game.card.CardUtil;
|
||||||
|
import forge.game.combat.Combat;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AbilityFactory for Creature Spells.
|
* AbilityFactory for Creature Spells.
|
||||||
@@ -79,21 +83,122 @@ public class PermanentCreatureAi extends PermanentAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flash logic
|
||||||
|
boolean advancedFlash = false;
|
||||||
|
if (ai.getController().isAI()) {
|
||||||
|
advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC);
|
||||||
|
}
|
||||||
|
if (card.withFlash(ai)) {
|
||||||
|
if (advancedFlash) {
|
||||||
|
return doAdvancedFlashLogic(card, ai, sa);
|
||||||
|
} else {
|
||||||
// save cards with flash for surprise blocking
|
// save cards with flash for surprise blocking
|
||||||
if (card.withFlash(ai)
|
if ((ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
|
||||||
&& (ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
|
|
||||||
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
||||||
&& ai.getManaPool().totalMana() <= 0
|
&& ai.getManaPool().totalMana() <= 0
|
||||||
&& (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
|
&& (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
|
||||||
&& (!card.hasETBTrigger(true) || card.hasSVar("AmbushAI")) && game.getStack().isEmpty()
|
&& (!card.hasETBTrigger(true) && !card.hasSVar("AmbushAI"))
|
||||||
|
&& game.getStack().isEmpty()
|
||||||
&& !ComputerUtil.castPermanentInMain1(ai, sa)) {
|
&& !ComputerUtil.castPermanentInMain1(ai, sa)) {
|
||||||
// AiPlayDecision.AnotherTime;
|
// AiPlayDecision.AnotherTime;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean doAdvancedFlashLogic(Card card, final Player ai, SpellAbility sa) {
|
||||||
|
Game game = ai.getGame();
|
||||||
|
PhaseHandler ph = game.getPhaseHandler();
|
||||||
|
Combat combat = game.getCombat();
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
|
||||||
|
boolean isOppTurn = ph.getPlayerTurn().isOpponentOf(ai);
|
||||||
|
boolean isOwnEOT = ph.is(PhaseType.END_OF_TURN, ai);
|
||||||
|
boolean isEOTBeforeMyTurn = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||||
|
boolean isMyDeclareBlockers = ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, ai) && ai.getGame().getCombat() != null;
|
||||||
|
boolean isOppDeclareAttackers = ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS) && isOppTurn && ai.getGame().getCombat() != null;
|
||||||
|
boolean isMyMain1OrLater = ph.is(PhaseType.MAIN1, ai) || (ph.getPhase().isAfter(PhaseType.MAIN1) && ph.getPlayerTurn().equals(ai));
|
||||||
|
boolean canRespondToStack = false;
|
||||||
|
if (!game.getStack().isEmpty()) {
|
||||||
|
SpellAbility peekSa = game.getStack().peekAbility();
|
||||||
|
Player activator = peekSa.getActivatingPlayer();
|
||||||
|
if (activator != null && activator.isOpponentOf(ai) && peekSa.getApi() != ApiType.DestroyAll
|
||||||
|
&& peekSa.getApi() != ApiType.DamageAll) {
|
||||||
|
canRespondToStack = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasETBTrigger = card.hasETBTrigger(true);
|
||||||
|
boolean hasAmbushAI = card.hasSVar("AmbushAI");
|
||||||
|
boolean defOnlyAmbushAI = hasAmbushAI && "BlockOnly".equals(card.getSVar("AmbushAI"));
|
||||||
|
boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
|
||||||
|
boolean willDiscardNow = isOwnEOT && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
|
||||||
|
boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
|
||||||
|
boolean wantToCastInMain1 = ph.is(PhaseType.MAIN1, ai) && ComputerUtil.castPermanentInMain1(ai, sa);
|
||||||
|
|
||||||
|
// figure out if the card might be a valuable blocker
|
||||||
|
boolean valuableBlocker = false;
|
||||||
|
if (combat != null && combat.getDefendingPlayers().contains(ai)) {
|
||||||
|
// Currently we use a rather simplistic assumption that if we're behind on creature count on board,
|
||||||
|
// a flashed in creature might prove to be good as an additional defender
|
||||||
|
int numUntappedPotentialBlockers = CardLists.filter(ai.getCreaturesInPlay(), new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(final Card card) {
|
||||||
|
return card.isUntapped() && !ComputerUtilCard.isUselessCreature(ai, card);
|
||||||
|
}
|
||||||
|
}).size();
|
||||||
|
|
||||||
|
if (combat.getAttackersOf(ai).size() > numUntappedPotentialBlockers) {
|
||||||
|
valuableBlocker = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int chanceToObeyAmbushAI = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_OBEY_AMBUSHAI);
|
||||||
|
int chanceToAddBlocker = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_CAST_AS_VALUABLE_BLOCKER);
|
||||||
|
int chanceToCastForETB = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_CAST_DUE_TO_ETB_EFFECTS);
|
||||||
|
int chanceToRespondToStack = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_RESPOND_TO_STACK_WITH_ETB);
|
||||||
|
int chanceToProcETBBeforeMain1 = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_CAST_FOR_ETB_BEFORE_MAIN1);
|
||||||
|
boolean canCastAtOppTurn = true;
|
||||||
|
for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||||
|
for (StaticAbility s : c.getStaticAbilities()) {
|
||||||
|
if ("CantBeCast".equals(s.getParam("Mode")) && "True".equals(s.getParam("NonCasterTurn"))) {
|
||||||
|
canCastAtOppTurn = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasFloatMana || willDiscardNow || willDieNow) {
|
||||||
|
// Will lose mana in pool or about to discard a card in cleanup or about to die in combat, so use this opportunity
|
||||||
|
return true;
|
||||||
|
} else if (wantToCastInMain1) {
|
||||||
|
// Would rather cast it in Main 1 or as soon as possible anyway, so go for it
|
||||||
|
return isMyMain1OrLater;
|
||||||
|
} else if (hasAmbushAI && MyRandom.percentTrue(chanceToObeyAmbushAI)) {
|
||||||
|
// Is an ambusher, so try to hold for declare blockers in combat where the AI defends, if possible
|
||||||
|
return defOnlyAmbushAI && canCastAtOppTurn ? isOppDeclareAttackers : (isOppDeclareAttackers || isMyDeclareBlockers);
|
||||||
|
} else if (valuableBlocker && isOppDeclareAttackers && MyRandom.percentTrue(chanceToAddBlocker)) {
|
||||||
|
// Might serve as a valuable blocker in a combat where we are behind on untapped blockers
|
||||||
|
return true;
|
||||||
|
} else if (hasETBTrigger && MyRandom.percentTrue(chanceToCastForETB)) {
|
||||||
|
// Instant speed is good when a card has an ETB trigger, but prolly don't cast in own turn before Main 1 not
|
||||||
|
// to mana lock the AI or lose the chance to consider other options. Try to utilize it as a response to stack
|
||||||
|
// if possible.
|
||||||
|
return isMyMain1OrLater || isOppTurn || MyRandom.percentTrue(chanceToProcETBBeforeMain1);
|
||||||
|
} else if (hasETBTrigger && canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack)) {
|
||||||
|
// Try to do something meaningful in response to an opposing effect on stack. Note that this is currently
|
||||||
|
// too random to likely be meaningful, serious improvement might be needed.
|
||||||
|
return canCastAtOppTurn || ph.getPlayerTurn().equals(ai);
|
||||||
|
} else {
|
||||||
|
// Doesn't have a ETB trigger and doesn't seem to be good as an ambusher, try to surprise the opp before my turn
|
||||||
|
// TODO: maybe implement a way to reserve mana for this
|
||||||
|
return canCastAtOppTurn ? isEOTBeforeMyTurn : isOwnEOT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,14 @@ import forge.card.CardTypeView;
|
|||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameType;
|
import forge.game.GameType;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.Spell;
|
import forge.game.spellability.Spell;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.spellability.SpellPermanent;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
@@ -71,8 +70,25 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that if a ValidZone is specified, there's at least something to choose from in that zone.
|
||||||
|
CardCollectionView validOpts = new CardCollection();
|
||||||
|
if (sa.hasParam("ValidZone")) {
|
||||||
|
validOpts = AbilityUtils.filterListByType(game.getCardsIn(ZoneType.valueOf(sa.getParam("ValidZone"))),
|
||||||
|
sa.getParam("Valid"), sa);
|
||||||
|
if (validOpts.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("ReplaySpell".equals(logic)) {
|
if ("ReplaySpell".equals(logic)) {
|
||||||
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
|
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
|
||||||
|
} else if (logic.startsWith("NeedsChosenCard")) {
|
||||||
|
int minCMC = 0;
|
||||||
|
if (sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null) {
|
||||||
|
minCMC = sa.getPayCosts().getCostMana().getMana().getCMC();
|
||||||
|
}
|
||||||
|
validOpts = CardLists.filter(validOpts, CardPredicates.greaterCMC(minCMC));
|
||||||
|
return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) {
|
if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) {
|
||||||
@@ -138,6 +154,15 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
if (!s.getRestrictions().checkTimingRestrictions(c, s))
|
if (!s.getRestrictions().checkTimingRestrictions(c, s))
|
||||||
continue;
|
continue;
|
||||||
if (sa.hasParam("WithoutManaCost")) {
|
if (sa.hasParam("WithoutManaCost")) {
|
||||||
|
// Try to avoid casting instants and sorceries with X in their cost, since X will be assumed to be 0.
|
||||||
|
if (!(spell instanceof SpellPermanent)) {
|
||||||
|
if (spell.getPayCosts() != null
|
||||||
|
&& spell.getPayCosts().getCostMana() != null
|
||||||
|
&& spell.getPayCosts().getCostMana().getMana().countX() > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
spell = (Spell) spell.copyWithNoManaCost();
|
spell = (Spell) spell.copyWithNoManaCost();
|
||||||
} else if (sa.hasParam("PlayCost")) {
|
} else if (sa.hasParam("PlayCost")) {
|
||||||
Cost abCost;
|
Cost abCost;
|
||||||
@@ -150,6 +175,13 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
spell = (Spell) spell.copyWithDefinedCost(abCost);
|
spell = (Spell) spell.copyWithDefinedCost(abCost);
|
||||||
}
|
}
|
||||||
if( AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
|
if( AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
|
||||||
|
// Before accepting, see if the spell has a valid number of targets (it should at this point).
|
||||||
|
// Proceeding past this point if the spell is not correctly targeted will result
|
||||||
|
// in "Failed to add to stack" error and the card disappearing from the game completely.
|
||||||
|
if (!spell.isTargetNumberValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
@@ -17,7 +16,6 @@ import forge.util.MyRandom;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class PowerExchangeAi extends SpellAbilityAi {
|
public class PowerExchangeAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -32,14 +30,13 @@ public class PowerExchangeAi extends SpellAbilityAi {
|
|||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
|
|
||||||
List<Card> list =
|
List<Card> list =
|
||||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||||
// AI won't try to grab cards that are filtered out of AI decks on
|
// AI won't try to grab cards that are filtered out of AI decks on
|
||||||
// purpose
|
// purpose
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
final Map<String, String> vars = c.getSVars();
|
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa);
|
||||||
return !vars.containsKey("RemAIDeck") && c.canBeTargetedBy(sa);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
CardLists.sortByPowerAsc(list);
|
CardLists.sortByPowerAsc(list);
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ public class ProtectAi extends SpellAbilityAi {
|
|||||||
if (s==null) {
|
if (s==null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
Player opponent = ComputerUtil.getOpponentFor(ai);
|
Player opponent = ai.getWeakestOpponent();
|
||||||
Combat combat = ai.getGame().getCombat();
|
Combat combat = ai.getGame().getCombat();
|
||||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
|
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
|
||||||
float ratio = 1.0f * dmg / opponent.getLife();
|
float ratio = 1.0f * dmg / opponent.getLife();
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import forge.game.phase.PhaseType;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityRestriction;
|
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.staticability.StaticAbility;
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
@@ -288,9 +287,8 @@ public class PumpAi extends PumpAiBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sa.hasParam("ActivationNumberSacrifice")) {
|
if (sa.hasParam("ActivationNumberSacrifice")) {
|
||||||
final SpellAbilityRestriction restrict = sa.getRestrictions();
|
|
||||||
final int sacActivations = Integer.parseInt(sa.getParam("ActivationNumberSacrifice").substring(2));
|
final int sacActivations = Integer.parseInt(sa.getParam("ActivationNumberSacrifice").substring(2));
|
||||||
final int activations = restrict.getNumberTurnActivations();
|
final int activations = sa.getActivationsThisTurn();
|
||||||
// don't risk sacrificing a creature just to pump it
|
// don't risk sacrificing a creature just to pump it
|
||||||
if (activations >= sacActivations - 1) {
|
if (activations >= sacActivations - 1) {
|
||||||
return false;
|
return false;
|
||||||
@@ -316,6 +314,9 @@ public class PumpAi extends PumpAiBase {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa);
|
defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa);
|
||||||
|
if (numDefense.contains("X") && sa.getSVar("X").equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
|
||||||
|
defense--; // the card will be spent casting the spell, so actual toughness is 1 less
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int attack;
|
int attack;
|
||||||
@@ -332,6 +333,9 @@ public class PumpAi extends PumpAiBase {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
|
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
|
||||||
|
if (numAttack.contains("X") && sa.getSVar("X").equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
|
||||||
|
attack--; // the card will be spent casting the spell, so actual power is 1 less
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("ContinuousBonus".equals(aiLogic)) {
|
if ("ContinuousBonus".equals(aiLogic)) {
|
||||||
@@ -481,6 +485,10 @@ public class PumpAi extends PumpAiBase {
|
|||||||
} else if (sa.getParam("AILogic").equals("DonateTargetPerm")) {
|
} else if (sa.getParam("AILogic").equals("DonateTargetPerm")) {
|
||||||
// Donate step 2 - target a donatable permanent.
|
// Donate step 2 - target a donatable permanent.
|
||||||
return SpecialCardAi.Donate.considerDonatingPermanent(ai, sa);
|
return SpecialCardAi.Donate.considerDonatingPermanent(ai, sa);
|
||||||
|
} else if (sa.getParam("AILogic").equals("SacOneEach")) {
|
||||||
|
// each player sacrifices one permanent, e.g. Vaevictis, Asmadi the Dire - grab the worst for allied and
|
||||||
|
// the best for opponents
|
||||||
|
return SacrificeAi.doSacOneEachLogic(ai, sa);
|
||||||
}
|
}
|
||||||
if (isFight) {
|
if (isFight) {
|
||||||
return FightAi.canFightAi(ai, sa, attack, defense);
|
return FightAi.canFightAi(ai, sa, attack, defense);
|
||||||
@@ -796,7 +804,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean doAristocratLogic(final SpellAbility sa, final Player ai) {
|
public static boolean doAristocratLogic(final SpellAbility sa, final Player ai) {
|
||||||
// A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT"
|
// A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT"
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final Combat combat = game.getCombat();
|
final Combat combat = game.getCombat();
|
||||||
@@ -867,7 +875,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
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;
|
final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / (powerBonus != 0 ? powerBonus : 1);
|
||||||
|
|
||||||
if (defTappedOut || numCreatsToSac < numOtherCreats / 2) {
|
if (defTappedOut || numCreatsToSac < numOtherCreats / 2) {
|
||||||
return source.getNetCombatDamage() < lethalDmg
|
return source.getNetCombatDamage() < lethalDmg
|
||||||
@@ -915,7 +923,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean doAristocratWithCountersLogic(final SpellAbility sa, final Player ai) {
|
public static boolean doAristocratWithCountersLogic(final SpellAbility sa, final Player ai) {
|
||||||
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
|
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
|
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
|
||||||
@@ -936,9 +944,19 @@ public class PumpAi extends PumpAiBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if anything is to be gained from the PutCounter subability
|
// 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.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?)
|
// 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 subability!");
|
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,7 +976,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int numCtrs = AbilityUtils.calculateAmount(source, sa.getSubAbility().getParam("CounterNum"), sa.getSubAbility());
|
int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa);
|
||||||
|
|
||||||
if (combat != null && combat.isAttacking(source) && isDeclareBlockers) {
|
if (combat != null && combat.isAttacking(source) && isDeclareBlockers) {
|
||||||
if (combat.getBlockers(source).isEmpty()) {
|
if (combat.getBlockers(source).isEmpty()) {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final Combat combat = game.getCombat();
|
final Combat combat = game.getCombat();
|
||||||
final PhaseHandler ph = game.getPhaseHandler();
|
final PhaseHandler ph = game.getPhaseHandler();
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
final int newPower = card.getNetCombatDamage() + attack;
|
final int newPower = card.getNetCombatDamage() + attack;
|
||||||
//int defense = getNumDefense(sa);
|
//int defense = getNumDefense(sa);
|
||||||
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import forge.game.card.CardLists;
|
|||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -37,6 +38,15 @@ public class PumpAllAi extends PumpAiBase {
|
|||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final Combat combat = game.getCombat();
|
final Combat combat = game.getCombat();
|
||||||
final Cost abCost = sa.getPayCosts();
|
final Cost abCost = sa.getPayCosts();
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
|
if (logic.equals("UntapCombatTrick")) {
|
||||||
|
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
|
if (!(ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, ai)
|
||||||
|
|| (!ph.getPlayerTurn().equals(ai) && ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS)))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
|
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
|
||||||
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
|
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
|
||||||
@@ -58,7 +68,7 @@ public class PumpAllAi extends PumpAiBase {
|
|||||||
valid = sa.getParam("ValidCards");
|
valid = sa.getParam("ValidCards");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||||
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.*;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.game.ability.AbilityUtils;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.card.CardPredicates;
|
||||||
|
import forge.game.phase.PhaseHandler;
|
||||||
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
|
import forge.game.player.PlayerCollection;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||||
/* (non-Javadoc)
|
/* (non-Javadoc)
|
||||||
@@ -13,14 +22,23 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
return sa.isMandatory(); // AI doesn't do anything with this SA yet, but at least it shouldn't miss mandatory triggers
|
// Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
|
||||||
|
final PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
|
if (source.isPermanent() && sa.getRestrictions().isInstantSpeed() && sa.getPayCosts() != null
|
||||||
|
&& (sa.getPayCosts().hasTapCost() || sa.getPayCosts().hasManaCost())) {
|
||||||
|
// If it has an associated cost, try to only do this before own turn
|
||||||
|
if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (non-Javadoc)
|
// Do it once per turn, generally (may be improved later)
|
||||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
if (!sa.isTrigger()
|
||||||
*/
|
&& AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||||
@Override
|
return false;
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
}
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
|
||||||
@@ -28,21 +46,78 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
|||||||
// ability is targeted
|
// ability is targeted
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
|
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = aiPlayer.getWeakestOpponent();
|
||||||
final boolean canTgtHuman = opp.canBeTargetedBy(sa);
|
final boolean canTgtAI = sa.canTarget(aiPlayer);
|
||||||
|
final boolean canTgtHuman = sa.canTarget(opp);
|
||||||
|
|
||||||
if (!canTgtHuman) {
|
if (canTgtHuman && canTgtAI) {
|
||||||
return false;
|
// TODO: maybe some other consideration rather than random?
|
||||||
} else {
|
Player preferredTarget = MyRandom.percentTrue(50) ? aiPlayer : opp;
|
||||||
|
sa.getTargets().add(preferredTarget);
|
||||||
|
} else if (canTgtAI) {
|
||||||
|
sa.getTargets().add(aiPlayer);
|
||||||
|
} else if (canTgtHuman) {
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
|
} else {
|
||||||
|
return false; // could not find a valid target
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canTgtHuman || !canTgtAI) {
|
||||||
|
// can't target another player anyway, remember for no second activation this turn
|
||||||
|
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if it's just defined, no big deal
|
// if it's just defined, no big deal
|
||||||
|
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: the AI currently doesn't do anything with this ability, consider improving.
|
return true;
|
||||||
// For now, "true" is returned (without any action) if the SA is mandatory in order
|
}
|
||||||
// not to miss triggers.
|
|
||||||
return sa.isMandatory();
|
/* (non-Javadoc)
|
||||||
|
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
|
// Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
|
||||||
|
return canPlayAI(ai, sa) || mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
|
// Confirming this action means shuffling the library if asked.
|
||||||
|
|
||||||
|
// First, let's check if we can play the top card of the library
|
||||||
|
PlayerCollection pc = sa.usesTargeting() ? new PlayerCollection(sa.getTargets().getTargetPlayers())
|
||||||
|
: AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||||
|
|
||||||
|
int uncastableCMCThreshold = 2;
|
||||||
|
int minLandsToScryLandsAway = 4;
|
||||||
|
if (player.getController().isAI()) {
|
||||||
|
AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
||||||
|
minLandsToScryLandsAway = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
|
||||||
|
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
Player p = pc.getFirst(); // FIXME: is this always a single target spell?
|
||||||
|
Card top = p.getCardsIn(ZoneType.Library).getFirst();
|
||||||
|
int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
|
||||||
|
int cmc = top.isSplitCard() ? Math.min(top.getCMC(Card.SplitCMCMode.LeftSplitCMC), top.getCMC(Card.SplitCMCMode.RightSplitCMC))
|
||||||
|
: top.getCMC();
|
||||||
|
int maxCastable = ComputerUtilMana.getAvailableManaEstimate(p, false);
|
||||||
|
|
||||||
|
if (!top.isLand() && cmc - maxCastable >= uncastableCMCThreshold) {
|
||||||
|
// Can't cast in the foreseeable future. Shuffle if doing it to ourselves or an ally, otherwise keep it
|
||||||
|
return !p.isOpponentOf(player);
|
||||||
|
} else if (top.isLand() && landsOTB <= minLandsToScryLandsAway){
|
||||||
|
// We don't want to give the opponent a free land if his land count is low
|
||||||
|
return p.isOpponentOf(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usually we don't want to shuffle if we arranged things carefully
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class RepeatAi extends SpellAbilityAi {
|
|||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
if (!opp.canBeTargetedBy(sa)) {
|
if (!opp.canBeTargetedBy(sa)) {
|
||||||
@@ -49,7 +49,7 @@ public class RepeatAi extends SpellAbilityAi {
|
|||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
|
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ai.getWeakestOpponent();
|
||||||
if (sa.canTarget(opp)) {
|
if (sa.canTarget(opp)) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
import forge.ai.AiPlayDecision;
|
import forge.ai.AiPlayDecision;
|
||||||
import forge.ai.PlayerControllerAi;
|
import forge.ai.PlayerControllerAi;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -51,6 +54,32 @@ public class RevealAi extends RevealAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("Kefnet".equals(sa.getParam("AILogic"))) {
|
||||||
|
final Card c = Iterables.getFirst(
|
||||||
|
AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("RevealDefined"), sa), null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (c == null || (!c.isInstant() && !c.isSorcery())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (SpellAbility s : c.getBasicSpells()) {
|
||||||
|
Spell spell = (Spell) s.copy(ai);
|
||||||
|
// timing restrictions still apply
|
||||||
|
if (!s.getRestrictions().checkTimingRestrictions(c, s))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// use hard coded reduce cost
|
||||||
|
spell.getMapParams().put("ReduceCost", "2");
|
||||||
|
|
||||||
|
if (AiPlayDecision.WillPlay == ((PlayerControllerAi) ai.getController()).getAi()
|
||||||
|
.canPlayFromEffectAI(spell, false, false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (!revealHandTargetAI(ai, sa/*, false, mandatory*/)) {
|
if (!revealHandTargetAI(ai, sa/*, false, mandatory*/)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates;
|
import forge.game.card.CardPredicates;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
@@ -61,7 +62,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
|||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final boolean destroy = sa.hasParam("Destroy");
|
final boolean destroy = sa.hasParam("Destroy");
|
||||||
|
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
Player opp = ai.getWeakestOpponent();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (!opp.canBeTargetedBy(sa)) {
|
if (!opp.canBeTargetedBy(sa)) {
|
||||||
@@ -133,7 +134,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
|||||||
List<Card> humanList =
|
List<Card> humanList =
|
||||||
CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||||
|
|
||||||
// Since all of the cards have remAIDeck:True, I enabled 1 for 1
|
// Since all of the cards have AI:RemoveDeck:All, I enabled 1 for 1
|
||||||
// (or X for X) trades for special decks
|
// (or X for X) trades for special decks
|
||||||
if (humanList.size() < amount) {
|
if (humanList.size() < amount) {
|
||||||
return false;
|
return false;
|
||||||
@@ -157,4 +158,41 @@ public class SacrificeAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean doSacOneEachLogic(Player ai, SpellAbility sa) {
|
||||||
|
Game game = ai.getGame();
|
||||||
|
|
||||||
|
sa.resetTargets();
|
||||||
|
for (Player p : game.getPlayers()) {
|
||||||
|
CardCollection targetable = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
|
||||||
|
if (!targetable.isEmpty()) {
|
||||||
|
CardCollection priorityTgts = new CardCollection();
|
||||||
|
if (p.isOpponentOf(ai)) {
|
||||||
|
priorityTgts.addAll(CardLists.filter(targetable, CardPredicates.canBeSacrificedBy(sa)));
|
||||||
|
if (!priorityTgts.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getBestAI(priorityTgts));
|
||||||
|
} else {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getBestAI(targetable));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Card c : targetable) {
|
||||||
|
if (c.canBeSacrificedBy(sa) && (c.hasSVar("SacMe") || (c.isCreature() && ComputerUtilCard.evaluateCreature(c) <= 135)) && !c.equals(sa.getHostCard())) {
|
||||||
|
priorityTgts.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!priorityTgts.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstPermanentAI(priorityTgts, false, false, false, false));
|
||||||
|
} else {
|
||||||
|
targetable.remove(sa.getHostCard());
|
||||||
|
if (!targetable.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getWorstPermanentAI(targetable, true, true, true, false));
|
||||||
|
} else {
|
||||||
|
sa.getTargets().add(sa.getHostCard()); // sac self only as a last resort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
@@ -37,7 +36,7 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CardCollection humanlist =
|
CardCollection humanlist =
|
||||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||||
CardCollection computerlist =
|
CardCollection computerlist =
|
||||||
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||||
|
|
||||||
|
|||||||
@@ -29,33 +29,10 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prevent transform into legendary creature if copy already exists
|
// Prevent transform into legendary creature if copy already exists
|
||||||
// Check first if Legend Rule does still apply
|
if (!isSafeToTransformIntoLegendary(aiPlayer, source)) {
|
||||||
if (!aiPlayer.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
|
|
||||||
if (!source.hasAlternateState()) {
|
|
||||||
System.err.println("Warning: SetState without ALTERNATE on " + source.getName() + ".");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the other side is legendary and if such Card already is in Play
|
|
||||||
final CardState other = source.getAlternateState();
|
|
||||||
|
|
||||||
if (other != null && other.getType().isLegendary() && aiPlayer.isCardInPlay(other.getName())) {
|
|
||||||
if (!other.getType().isCreature()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Card othercard = aiPlayer.getCardsIn(ZoneType.Battlefield, other.getName()).getFirst();
|
|
||||||
|
|
||||||
// for legendary KI counter creatures
|
|
||||||
if (othercard.getCounters(CounterType.KI) >= source.getCounters(CounterType.KI)) {
|
|
||||||
// if the other legendary is useless try to replace it
|
|
||||||
if (!ComputerUtilCard.isUselessCreature(aiPlayer, othercard)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if("Transform".equals(mode) || "Flip".equals(mode)) {
|
if("Transform".equals(mode) || "Flip".equals(mode)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -255,8 +232,44 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
return valueCard <= valueTransformed;
|
return valueCard <= valueTransformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSafeToTransformIntoLegendary(Player aiPlayer, Card source) {
|
||||||
|
// Prevent transform into legendary creature if copy already exists
|
||||||
|
// Check first if Legend Rule does still apply
|
||||||
|
if (!aiPlayer.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
|
||||||
|
if (!source.hasAlternateState()) {
|
||||||
|
System.err.println("Warning: SetState without ALTERNATE on " + source.getName() + ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the other side is legendary and if such Card already is in Play
|
||||||
|
final CardState other = source.getAlternateState();
|
||||||
|
|
||||||
|
if (other != null && other.getType().isLegendary() && aiPlayer.isCardInPlay(other.getName())) {
|
||||||
|
if (!other.getType().isCreature()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Card othercard = aiPlayer.getCardsIn(ZoneType.Battlefield, other.getName()).getFirst();
|
||||||
|
|
||||||
|
// for legendary KI counter creatures
|
||||||
|
if (othercard.getCounters(CounterType.KI) >= source.getCounters(CounterType.KI)) {
|
||||||
|
// if the other legendary is useless try to replace it
|
||||||
|
if (!ComputerUtilCard.isUselessCreature(aiPlayer, othercard)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
// TODO: improve the AI for when it may want to transform something that's optional to transform
|
// TODO: improve the AI for when it may want to transform something that's optional to transform
|
||||||
|
if (!isSafeToTransformIntoLegendary(player, sa.getHostCard())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -8,6 +9,14 @@ import forge.game.spellability.SpellAbility;
|
|||||||
public class ShuffleAi extends SpellAbilityAi {
|
public class ShuffleAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
|
String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
if (logic.equals("Always")) {
|
||||||
|
// We may want to play this for the subability, e.g. Mind's Desire
|
||||||
|
return true;
|
||||||
|
} else if (logic.equals("OwnMain2")) {
|
||||||
|
return aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2, aiPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
// not really sure when the compy would use this; maybe only after a
|
// not really sure when the compy would use this; maybe only after a
|
||||||
// human
|
// human
|
||||||
// deliberately put a card on top of their library
|
// deliberately put a card on top of their library
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.*;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostPayLife;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -68,8 +71,14 @@ public class SurveilAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
if ("Never".equals(aiLogic)) {
|
if ("Never".equals(aiLogic)) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if ("Once".equals(aiLogic)) {
|
||||||
|
if (AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add card-specific Surveil AI logic here when/if necessary
|
// TODO: add card-specific Surveil AI logic here when/if necessary
|
||||||
@@ -84,6 +93,15 @@ public class SurveilAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only Surveil for life when at decent amount of life remaining
|
||||||
|
final Cost cost = sa.getPayCosts();
|
||||||
|
if (cost != null && cost.hasSpecificCostType(CostPayLife.class)) {
|
||||||
|
final int maxLife = ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.SURVEIL_LIFEPERC_AFTER_PAYING_LIFE);
|
||||||
|
if (!ComputerUtilCost.checkLifeCost(ai, cost, sa.getHostCard(), ai.getStartingLife() * maxLife / 100, sa)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
double chance = .4; // 40 percent chance for instant speed
|
double chance = .4; // 40 percent chance for instant speed
|
||||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||||
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
|
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
|
||||||
@@ -94,6 +112,10 @@ public class SurveilAi extends SpellAbilityAi {
|
|||||||
randomReturn = true;
|
randomReturn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (randomReturn) {
|
||||||
|
AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||||
|
}
|
||||||
|
|
||||||
return randomReturn;
|
return randomReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user