React to removal of Spike Weaver by using the effect if it's already planning to do later

Don't leave blockers if planning to use fog effect, except the card that will do so. (might need to tap to use it, or might get killed if attacking)
This commit is contained in:
Rob Schnautz
2019-03-24 17:04:53 +00:00
parent fb8bfdc449
commit 6ca034eab9
10956 changed files with 71565 additions and 26003 deletions

View File

@@ -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>

3
.gitignore vendored
View File

@@ -231,3 +231,6 @@ forge-gui/tools/oracleScript.log
/release.properties
/target
/test-output
.settings
.classpath
.project

View 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

View 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

View File

@@ -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>

View File

@@ -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

View File

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

View File

@@ -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

View File

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

230
README.md Normal file
View 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
TBD
# 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.

21
checkstyle.xml Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
<!--
Checkstyle is very configurable.
http://checkstyle.sf.net (or in your downloaded distribution).
-->
<module name="Checker">
<module name="TreeWalker">
<module name="RedundantImport"/>
<module name="UnusedImports">
<!-- <property name="processJavadoc" value="false"/> -->
</module>
</module>
</module>

View File

@@ -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>

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.10-SNAPSHOT</version>
<version>1.6.23-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>
@@ -29,4 +29,5 @@
<version>3.6.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -30,6 +30,7 @@ import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.combat.GlobalAttackRestrictions;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -38,11 +39,11 @@ import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
@@ -60,9 +61,6 @@ public class AiAttackController {
private final List<Card> attackers;
private final List<Card> blockers;
private final static Random random = new Random();
private final static int randomInt = random.nextInt();
private List<Card> oppList; // holds human player creatures
private List<Card> myList; // holds computer creatures
@@ -105,7 +103,7 @@ public class AiAttackController {
} // overloaded constructor to evaluate single specified attacker
public static List<Card> getOpponentCreatures(final Player defender) {
List<Card> defenders = Lists.newArrayList();
List<Card> defenders = new ArrayList<Card>();
defenders.addAll(defender.getCreaturesInPlay());
Predicate<Card> canAnimate = new Predicate<Card>() {
@Override
@@ -204,8 +202,11 @@ public class AiAttackController {
if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) {
return true;
}
if (this.attackers.size() == 1 && attacker.hasKeyword("Exalted")
&& ComputerUtilCombat.predictDamageTo(opp, 1, attacker, true) > 0) {
// TODO check if that makes sense
int exalted = ai.countExaltedBonus();
if (this.attackers.size() == 1 && exalted > 0
&& ComputerUtilCombat.predictDamageTo(opp, exalted, attacker, true) > 0) {
return true;
}
@@ -267,6 +268,40 @@ public class AiAttackController {
if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) {
return attackers;
}
// no need to block (already holding mana to cast fog next turn)
if (!AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) {
// Don't send the card that'll do the fog effect to attack, it's unsafe!
List<Card> toRemove = Lists.newArrayList();
for(Card c : attackers) {
if (AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) {
toRemove.add(c);
}
}
attackers.removeAll(toRemove);
return attackers;
}
// no need to block if an effect is in play which untaps all creatures (pseudo-Vigilance akin to
// Awakening or Prophet of Kruphix)
for (Card card : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
boolean untapsEachTurn = card.hasSVar("UntapsEachTurn");
boolean untapsEachOtherTurn = card.hasSVar("UntapsEachOtherPlayerTurn");
if (untapsEachTurn || untapsEachOtherTurn) {
String affected = untapsEachTurn ? card.getSVar("UntapsEachTurn")
: card.getSVar("UntapsEachOtherPlayerTurn");
for (String aff : TextUtil.split(affected, ',')) {
if (aff.equals("Creature")
&& (untapsEachTurn || (untapsEachOtherTurn && ai.equals(card.getController())))) {
return attackers;
}
}
}
}
List<Card> opponentsAttackers = new ArrayList<Card>(oppList);
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() {
@Override
@@ -285,7 +320,7 @@ public class AiAttackController {
}
continue;
}
if (c.hasKeyword("Vigilance")) {
if (c.hasKeyword(Keyword.VIGILANCE)) {
vigilantes.add(c);
notNeededAsBlockers.remove(c); // they will be re-added later
if (canBlockAnAttacker(c, opponentsAttackers, false)) {
@@ -327,7 +362,7 @@ public class AiAttackController {
// In addition, if the computer guesses it needs no blockers, make sure
// that
// it won't be surprised by Exalted
final int humanExaltedBonus = countExaltedBonus(opp);
final int humanExaltedBonus = opp.countExaltedBonus();
if (humanExaltedBonus > 0) {
final boolean finestHour = opp.isCardInPlay("Finest Hour");
@@ -512,7 +547,7 @@ public class AiAttackController {
int trampleDamage = 0;
for (Card attacker : blockedAttackers) {
if (attacker.hasKeyword("Trample")) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
int damage = ComputerUtilCombat.getAttack(attacker);
for (Card blocker : this.blockers) {
if (CombatUtil.canBlock(attacker, blocker)) {
@@ -586,13 +621,6 @@ public class AiAttackController {
* @return a {@link forge.game.combat.Combat} object.
*/
public final void declareAttackers(final Combat combat) {
// if this method is called multiple times during a turn,
// it will always return the same value
// randomInt is used so that the computer doesn't always
// do the same thing on turn 3 if he had the same creatures in play
// I know this is a little confusing
random.setSeed(ai.getGame().getPhaseHandler().getTurn() + AiAttackController.randomInt);
if (this.attackers.isEmpty()) {
return;
@@ -705,8 +733,9 @@ public class AiAttackController {
// Exalted
if (combat.getAttackers().isEmpty()) {
boolean exalted = false;
int exaltedCount = 0;
boolean exalted = ai.countExaltedBonus() > 2;
if (!exalted) {
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
if (c.getName().equals("Rafiq of the Many") || c.getName().equals("Battlegrace Angel")) {
exalted = true;
@@ -716,12 +745,6 @@ public class AiAttackController {
exalted = true;
break;
}
if (c.hasKeyword("Exalted")) {
exaltedCount++;
if (exaltedCount > 2) {
exalted = true;
break;
}
}
}
if (exalted) {
@@ -1025,24 +1048,6 @@ public class AiAttackController {
}
} // getAttackers()
/**
* <p>
* countExaltedBonus.
* </p>
*
* @param player
* a {@link forge.game.player.Player} object.
* @return a int.
*/
public final static int countExaltedBonus(final Player player) {
int bonus = 0;
for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
bonus += c.getAmountOfKeyword("Exalted");
}
return bonus;
}
/**
* <p>
* getAttack.
@@ -1055,7 +1060,7 @@ public class AiAttackController {
public final static int getAttack(final Card c) {
int n = c.getNetCombatDamage();
if (c.hasKeyword("Double Strike")) {
if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
n *= 2;
}
@@ -1086,7 +1091,7 @@ public class AiAttackController {
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
if ((attacker.hasSVar("NonCombatPriority"))
&& (!attacker.hasKeyword("Vigilance"))) {
&& (!attacker.hasKeyword(Keyword.VIGILANCE))) {
// For each level of priority, enemy has to have life as much as the creature's power
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
// the lower the priroity, the less willing the AI is to use the creature for attacking.
@@ -1138,7 +1143,7 @@ public class AiAttackController {
&& CombatUtil.canBlock(attacker, defender)) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
&& !(attacker.hasKeyword("Undying") && attacker.getCounters(CounterType.P1P1) == 0)) {
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
@@ -1154,15 +1159,12 @@ public class AiAttackController {
if (defender.getSVar("HasCombatEffect").equals("TRUE") || defender.getSVar("HasBlockEffect").equals("TRUE")) {
canKillAllDangerous = false;
} else {
for (KeywordInterface inst : defender.getKeywords()) {
String keyword = inst.getOriginal();
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
if (defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT)
|| defender.hasKeyword(Keyword.LIFELINK)) {
canKillAllDangerous = false;
break;
// there is a creature that can survive an attack from this creature
// and combat will have negative effects
}
}
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
@@ -1185,7 +1187,7 @@ public class AiAttackController {
}
}
if (!attacker.hasKeyword("Vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
@@ -1271,7 +1273,7 @@ public class AiAttackController {
// creature would leave the battlefield
// no pain in exerting it
shouldExert = true;
} else if (c.hasKeyword("Vigilance")) {
} else if (c.hasKeyword(Keyword.VIGILANCE)) {
// Free exert - why not?
shouldExert = true;
}
@@ -1334,7 +1336,7 @@ public class AiAttackController {
}
}
if (!shouldExert && random.nextBoolean()) {
if (!shouldExert && MyRandom.getRandom().nextBoolean()) {
// TODO Improve when the AI wants to use Exert powers
shouldExert = true;
}

View File

@@ -22,9 +22,11 @@ import com.google.common.base.Predicates;
import forge.card.CardStateName;
import forge.game.CardTraitBase;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -180,7 +182,7 @@ public class AiBlockController {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword("Menace")) {
|| attacker.hasKeyword(Keyword.MENACE)) {
continue;
}
@@ -204,11 +206,11 @@ public class AiBlockController {
&& !ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
blocker = ComputerUtilCard.getWorstCreatureAI(safeBlockers);
// check whether it's better to block a creature without trample to absorb more damage
if (attacker.hasKeyword("Trample")) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
boolean doNotBlock = false;
for (Card other : attackersLeft) {
if (other.equals(attacker) || !CombatUtil.canBlock(other, blocker)
|| other.hasKeyword("Trample")
|| other.hasKeyword(Keyword.TRAMPLE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
|| ComputerUtilCombat.canDestroyBlocker(ai, blocker, other, combat, false)
|| other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
@@ -231,10 +233,9 @@ public class AiBlockController {
// 3.Blockers that can destroy the attacker and have an upside when dying
killingBlockers = getKillingBlockers(combat, attacker, blockers);
for (Card b : killingBlockers) {
if ((b.hasKeyword("Undying") && b.getCounters(CounterType.P1P1) == 0)
|| b.hasSVar("SacMe")
|| (b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1)
|| (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0)
if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe")
|| (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|| b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
break;
@@ -293,8 +294,7 @@ public class AiBlockController {
// 6. Blockers that don't survive until the next turn anyway
for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("Menace")
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
@@ -304,8 +304,8 @@ public class AiBlockController {
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
for (Card b : blockers) {
if ((b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1)
|| (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0)
if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|| b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
@@ -531,7 +531,7 @@ public class AiBlockController {
// Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) {
if (!attacker.hasKeyword("Menace") && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
if (!attacker.hasKeyword(Keyword.MENACE) && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
continue;
}
@@ -590,7 +590,7 @@ public class AiBlockController {
for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("Menace")
|| attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
@@ -645,7 +645,7 @@ public class AiBlockController {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword("Menace")
|| attacker.hasKeyword(Keyword.MENACE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0);
makeChumpBlocks(combat, attackers);
@@ -657,7 +657,7 @@ public class AiBlockController {
final Card blocker = ComputerUtilCard.getWorstCreatureAI(chumpBlockers);
// check if it's better to block a creature with lower power and without trample
if (attacker.hasKeyword("Trample")) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
final int damageAbsorbed = blocker.getLethalDamage();
if (attacker.getNetCombatDamage() > damageAbsorbed) {
for (Card other : attackers) {
@@ -665,7 +665,7 @@ public class AiBlockController {
continue;
}
if (other.getNetCombatDamage() >= damageAbsorbed
&& !other.hasKeyword("Trample")
&& !other.hasKeyword(Keyword.TRAMPLE)
&& !other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
&& !ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
&& CombatUtil.canBlock(other, blocker, combat)) {
@@ -696,7 +696,7 @@ public class AiBlockController {
for (final Card attacker : currentAttackers) {
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
&& !attacker.hasKeyword("Menace")
&& !attacker.hasKeyword(Keyword.MENACE)
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
@@ -729,7 +729,7 @@ public class AiBlockController {
List<Card> chumpBlockers;
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, "Trample");
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
// TODO - should check here for a "rampage-like" trigger that replaced
@@ -738,7 +738,7 @@ public class AiBlockController {
for (final Card attacker : tramplingAttackers) {
if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("Menace")) && !combat.isBlocked(attacker))
if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) && !combat.isBlocked(attacker))
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
@@ -772,6 +772,16 @@ public class AiBlockController {
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
blockers.removeAll(combat.getBlockers(attacker));
// Don't add any blockers that won't kill the attacker because the damage would be prevented by a static effect
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
blockers = CardLists.filter(blockers, new Predicate<Card>() {
@Override
public boolean apply(Card blocker) {
return !ComputerUtilCombat.isCombatDamagePrevented(blocker, attacker, blocker.getNetCombatDamage());
}
});
}
// Try to use safe blockers first
if (blockers.size() > 0) {
safeBlockers = getSafeBlockers(combat, attacker, blockers);
@@ -789,15 +799,15 @@ public class AiBlockController {
}
}
// don't try to kill what can't be killed
if (attacker.hasKeyword("indestructible") || ComputerUtil.canRegenerate(ai, attacker)) {
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, attacker)) {
continue;
}
// Try to add blockers that could be destroyed, but are worth less than the attacker
// Don't use blockers without First Strike or Double Strike if attacker has it
if (ComputerUtilCombat.dealsFirstStrikeDamage(attacker, false, combat)) {
safeBlockers = CardLists.getKeyword(blockers, "First Strike");
safeBlockers.addAll(CardLists.getKeyword(blockers, "Double Strike"));
safeBlockers = CardLists.getKeyword(blockers, Keyword.FIRST_STRIKE);
safeBlockers.addAll(CardLists.getKeyword(blockers, Keyword.DOUBLE_STRIKE));
} else {
safeBlockers = new ArrayList<>(blockers);
}
@@ -870,7 +880,7 @@ public class AiBlockController {
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains((Card) def)) {
if (attacker.hasKeyword("Trample")) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
// don't bother trying to chump a trampling creature
continue;
}
@@ -1025,22 +1035,26 @@ public class AiBlockController {
lifeInDanger = false;
}
// if life is still in danger
// Reinforce blockers blocking attackers with trample if life is still
// Reinforce blockers blocking attackers with trample if life is
// still
// in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat);
} else {
lifeInDanger = false;
}
// Support blockers not destroying the attacker with more blockers to
// Support blockers not destroying the attacker with more blockers
// to
// try to kill the attacker
if (!lifeInDanger) {
reinforceBlockersToKill(combat);
}
// == 2. If the AI life would still be in danger make a safer approach ==
// == 2. If the AI life would still be in danger make a safer
// approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); // reset every block assignment
clearBlockers(combat, possibleBlockers); // reset every block
// assignment
makeTradeBlocks(combat); // choose necessary trade blocks
// if life is in danger
makeGoodBlocks(combat);
@@ -1061,9 +1075,11 @@ public class AiBlockController {
reinforceBlockersToKill(combat);
}
// == 3. If the AI life would be in serious danger make an even safer approach ==
// == 3. If the AI life would be in serious danger make an even
// safer approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); // reset every block assignment
clearBlockers(combat, possibleBlockers); // reset every block
// assignment
makeChumpBlocks(combat); // choose chump blocks
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeTradeBlocks(combat); // choose necessary trade
@@ -1078,7 +1094,8 @@ public class AiBlockController {
reinforceBlockersAgainstTrample(combat);
}
makeGangBlocks(combat);
// Support blockers not destroying the attacker with more blockers
// Support blockers not destroying the attacker with more
// blockers
// to try to kill the attacker
reinforceBlockersToKill(combat);
}
@@ -1167,7 +1184,8 @@ public class AiBlockController {
* Orders a blocker that put onto the battlefield blocking. Depends heavily
* on the implementation of orderBlockers().
*/
public static CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) {
public static CardCollection orderBlocker(final Card attacker, final Card blocker,
final CardCollection oldBlockers) {
// add blocker to existing ordering
// sort by evaluate, then insert it appropriately
// relies on current implementation of orderBlockers()
@@ -1182,17 +1200,21 @@ public class AiBlockController {
boolean newBlockerIsAdded = false;
// The new blocker comes right after this one
final Card newBlockerRightAfter = (newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1));
if (newBlockerRightAfter == null && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
if (newBlockerRightAfter == null
&& damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
result.add(blocker);
newBlockerIsAdded = true;
}
// Don't bother to keep damage up-to-date after the new blocker is added, as we can't modify the order of the other cards anyway
// Don't bother to keep damage up-to-date after the new blocker is
// added, as we can't modify the order of the other cards anyway
for (final Card c : oldBlockers) {
final int lethal = ComputerUtilCombat.getEnoughDamageToKill(c, damage, attacker, true);
damage -= lethal;
result.add(c);
if (!newBlockerIsAdded && c == newBlockerRightAfter && damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
// If blocker is right after this card in priority and we have sufficient damage to kill it, add it here
if (!newBlockerIsAdded && c == newBlockerRightAfter
&& damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
// If blocker is right after this card in priority and we have
// sufficient damage to kill it, add it here
result.add(blocker);
newBlockerIsAdded = true;
}

View File

@@ -40,43 +40,55 @@ import java.util.Set;
*/
public class AiCardMemory {
private final Set<Card> memMandatoryAttackers;
private final Set<Card> memTrickAttackers;
private final Set<Card> memHeldManaSources;
private final Set<Card> memHeldManaSourcesForCombat;
private final Set<Card> memAttachedThisTurn;
private final Set<Card> memAnimatedThisTurn;
private final Set<Card> memBouncedThisTurn;
private final Set<Card> memActivatedThisTurn;
public AiCardMemory() {
this.memMandatoryAttackers = new HashSet<>();
this.memHeldManaSources = new HashSet<>();
this.memHeldManaSourcesForCombat = new HashSet<>();
this.memAttachedThisTurn = new HashSet<>();
this.memAnimatedThisTurn = new HashSet<>();
this.memBouncedThisTurn = new HashSet<>();
this.memActivatedThisTurn = new HashSet<>();
this.memTrickAttackers = 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,
ATTACHED_THIS_TURN,
ANIMATED_THIS_TURN,
BOUNCED_THIS_TURN,
ACTIVATED_THIS_TURN,
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> memTrickAttackers;
private final Set<Card> memHeldManaSources;
private final Set<Card> memHeldManaSourcesForCombat;
private final Set<Card> memHeldManaSourcesForEnemyCombat;
private final Set<Card> memHeldManaSourcesForNextSpell;
private final Set<Card> memAttachedThisTurn;
private final Set<Card> memAnimatedThisTurn;
private final Set<Card> memBouncedThisTurn;
private final Set<Card> memActivatedThisTurn;
private final Set<Card> memChosenFogEffect;
private final Set<Card> memMarkedToAvoidReentry;
public AiCardMemory() {
this.memMandatoryAttackers = new HashSet<>();
this.memHeldManaSources = new HashSet<>();
this.memHeldManaSourcesForCombat = new HashSet<>();
this.memHeldManaSourcesForEnemyCombat = new HashSet<>();
this.memAttachedThisTurn = new HashSet<>();
this.memAnimatedThisTurn = new HashSet<>();
this.memBouncedThisTurn = new HashSet<>();
this.memActivatedThisTurn = new HashSet<>();
this.memTrickAttackers = new HashSet<>();
this.memChosenFogEffect = new HashSet<>();
this.memMarkedToAvoidReentry = new HashSet<>();
this.memHeldManaSourcesForNextSpell = new HashSet<>();
}
private Set<Card> getMemorySet(MemorySet set) {
switch (set) {
case MANDATORY_ATTACKERS:
@@ -87,6 +99,10 @@ public class AiCardMemory {
return memHeldManaSources;
case HELD_MANA_SOURCES_FOR_DECLBLK:
return memHeldManaSourcesForCombat;
case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
return memHeldManaSourcesForEnemyCombat;
case HELD_MANA_SOURCES_FOR_NEXT_SPELL:
return memHeldManaSourcesForNextSpell;
case ATTACHED_THIS_TURN:
return memAttachedThisTurn;
case ANIMATED_THIS_TURN:
@@ -95,6 +111,10 @@ public class AiCardMemory {
return memBouncedThisTurn;
case ACTIVATED_THIS_TURN:
return memActivatedThisTurn;
case CHOSEN_FOG_EFFECT:
return memChosenFogEffect;
case MARKED_TO_AVOID_REENTRY:
return memMarkedToAvoidReentry;
//case REVEALED_CARDS:
// return memRevealedCards;
default:
@@ -263,33 +283,66 @@ public class AiCardMemory {
* Clears all memory sets stored in this card memory for the given player.
*/
public void clearAllRemembered() {
clearMemorySet(MemorySet.MANDATORY_ATTACKERS);
clearMemorySet(MemorySet.TRICK_ATTACKERS);
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
clearMemorySet(MemorySet.ATTACHED_THIS_TURN);
clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
clearMemorySet(MemorySet.BOUNCED_THIS_TURN);
clearMemorySet(MemorySet.ACTIVATED_THIS_TURN);
for (MemorySet memSet : MemorySet.values()) {
clearMemorySet(memSet);
}
}
// Static functions to simplify access to AI card memory of a given AI player.
public static void rememberCard(Player ai, Card c, MemorySet set) {
if (!ai.getController().isAI()) {
return;
}
((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) {
if (!ai.getController().isAI()) {
return;
}
((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) {
if (!ai.getController().isAI()) {
return false;
}
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) {
if (!ai.getController().isAI()) {
return false;
}
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) {
if (!ai.getController().isAI()) {
return;
}
((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) {
if (!ai.getController().isAI()) {
return false;
}
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isMemorySetEmpty(set);
}
public static boolean isMemorySetEmpty(AiController aic, MemorySet set) {
return aic.getCardMemory().isMemorySetEmpty(set);
}
}

View File

@@ -38,10 +38,12 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased;
import forge.game.card.*;
import forge.game.card.CardPredicates.Accessors;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -49,6 +51,7 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplaceMoved;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
@@ -58,6 +61,8 @@ import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder;
import java.security.InvalidParameterException;
import java.util.*;
@@ -88,6 +93,9 @@ public class AiController {
this.cheatShuffle = canCheatShuffle;
}
public boolean usesSimulation() {
return this.useSimulation;
}
public void setUseSimulation(boolean value) {
this.useSimulation = value;
}
@@ -126,10 +134,12 @@ public class AiController {
private List<SpellAbility> getPossibleETBCounters() {
CardCollection all = new CardCollection(player.getCardsIn(ZoneType.Hand));
CardCollectionView ccvPlayerLibrary = player.getCardsIn(ZoneType.Library);
all.addAll(player.getCardsIn(ZoneType.Exile));
all.addAll(player.getCardsIn(ZoneType.Graveyard));
if (!player.getCardsIn(ZoneType.Library).isEmpty()) {
all.add(player.getCardsIn(ZoneType.Library).get(0));
if (!ccvPlayerLibrary.isEmpty()) {
all.add(ccvPlayerLibrary.get(0));
}
for (final Player opp : player.getOpponents()) {
@@ -152,45 +162,50 @@ public class AiController {
// look for cards on the battlefield that should prevent the AI from using that spellability
private boolean checkCurseEffects(final SpellAbility sa) {
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
CardCollectionView ccvGameBattlefield = game.getCardsIn(ZoneType.Battlefield);
for (final Card c : ccvGameBattlefield) {
if (c.hasSVar("AICurseEffect")) {
final String curse = c.getSVar("AICurseEffect");
final Card host = sa.getHostCard();
if ("NonActive".equals(curse) && !player.equals(game.getPhaseHandler().getPlayerTurn())) {
return true;
} else if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
&& !sa.getHostCard().hasKeyword("Indestructible")) {
} else {
final Card host = sa.getHostCard();
if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
&& !host.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return true;
} else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
&& !sa.getHostCard().hasKeyword("CARDNAME can't be countered.")) {
&& CardFactoryUtil.isCounterable(host)) {
return true;
} else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.")
} else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)
&& host.getCMC() == c.getCounters(CounterType.CHARGE)) {
return true;
} else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.")) {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
if (!card.isToken() && card.getName().equals(host.getName())) {
} else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) {
String hostName = host.getName();
for (Card card : ccvGameBattlefield) {
if (!card.isToken() && card.getName().equals(hostName)) {
return true;
}
}
for (Card card : game.getCardsIn(ZoneType.Graveyard)) {
if (card.getName().equals(host.getName())) {
if (card.getName().equals(hostName)) {
return true;
}
}
}
}
}
}
return false;
}
public boolean checkETBEffects(final Card card, final SpellAbility sa, final ApiType api) {
boolean rightapi = false;
if (card.isCreature()
&& game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureETBTriggers)) {
return api == null;
}
boolean rightapi = false;
String battlefield = ZoneType.Battlefield.toString();
Player activatingPlayer = sa.getActivatingPlayer();
// Trigger play improvements
for (final Trigger tr : card.getTriggers()) {
@@ -201,21 +216,22 @@ public class AiController {
continue;
}
if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (params.containsKey("ValidCard")) {
if (!params.get("ValidCard").contains("Self")) {
String validCard = params.get("ValidCard");
if (!validCard.contains("Self")) {
continue;
}
if (params.get("ValidCard").contains("notkicked")) {
if (validCard.contains("notkicked")) {
if (sa.isKicked()) {
continue;
}
} else if (params.get("ValidCard").contains("kicked")) {
if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
String s = params.get("ValidCard").split("kicked ")[1];
} else if (validCard.contains("kicked")) {
if (validCard.contains("kicked ")) { // want a specific kicker
String s = validCard.split("kicked ")[1];
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
} else if (!sa.isKicked()) {
@@ -258,7 +274,7 @@ public class AiController {
}
if (sa != null) {
exSA.setActivatingPlayer(sa.getActivatingPlayer());
exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
@@ -266,15 +282,13 @@ public class AiController {
exSA.setTrigger(true);
// for trigger test, need to ignore the conditions
if (exSA.getConditions() != null) {
SpellAbilityCondition cons = exSA.getConditions();
if (cons.getIsPresent() != null) {
if (cons != null) {
String pres = cons.getIsPresent();
if ("Card.Self".equals(pres) || "Card.StrictlySelf".equals(pres)) {
if (pres != null && pres.matches("Card\\.(Strictly)?Self")) {
cons.setIsPresent(null);
}
}
}
// Run non-mandatory trigger.
// These checks only work if the Executing SpellAbility is an Ability_Sub.
@@ -296,21 +310,22 @@ public class AiController {
continue;
}
if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (params.containsKey("ValidCard")) {
if (!params.get("ValidCard").contains("Self")) {
String validCard = params.get("ValidCard");
if (!validCard.contains("Self")) {
continue;
}
if (params.get("ValidCard").contains("notkicked")) {
if (validCard.contains("notkicked")) {
if (sa.isKicked()) {
continue;
}
} else if (params.get("ValidCard").contains("kicked")) {
if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
String s = params.get("ValidCard").split("kicked ")[1];
} else if (validCard.contains("kicked")) {
if (validCard.contains("kicked ")) { // want a specific kicker
String s = validCard.split("kicked ")[1];
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
} else if (!sa.isKicked()) { // otherwise just any must be present
@@ -326,7 +341,7 @@ public class AiController {
if (exSA != null) {
if (sa != null) {
exSA.setActivatingPlayer(sa.getActivatingPlayer());
exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
@@ -374,8 +389,9 @@ public class AiController {
if (landsInPlay.size() + landList.size() > max) {
for (Card c : allCards) {
for (SpellAbility sa : c.getSpellAbilities()) {
if (sa.getPayCosts() != null) {
for (CostPart part : sa.getPayCosts().getCostParts()) {
Cost payCosts = sa.getPayCosts();
if (payCosts != null) {
for (CostPart part : payCosts.getCostParts()) {
if (part instanceof CostDiscard) {
return null;
}
@@ -389,10 +405,11 @@ public class AiController {
landList = CardLists.filter(landList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
canPlaySpellBasic(c, null);
if (c.getType().isLegendary() && !c.getName().equals("Flagstones of Trokair")) {
final CardCollectionView list = player.getCardsIn(ZoneType.Battlefield);
if (Iterables.any(list, CardPredicates.nameEquals(c.getName()))) {
String name = c.getName();
if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) {
if (Iterables.any(battlefield, CardPredicates.nameEquals(name))) {
return false;
}
}
@@ -401,7 +418,7 @@ public class AiController {
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
CardCollection lands = new CardCollection(player.getCardsIn(ZoneType.Battlefield));
CardCollection lands = new CardCollection(battlefield);
lands.addAll(hand);
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
@@ -412,7 +429,8 @@ public class AiController {
}
}
}
return true;
return player.canPlayLand(c);
}
});
return landList;
@@ -422,6 +440,38 @@ public class AiController {
if (landList.isEmpty()) {
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.
CardCollection unreflectedLands = new CardCollection(landList);
for (Card l : landList) {
@@ -433,7 +483,6 @@ public class AiController {
landList = unreflectedLands;
}
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
//try to skip lands that enter the battlefield tapped
if (!nonLandsInHand.isEmpty()) {
@@ -564,14 +613,17 @@ public class AiController {
Collections.sort(all, saComparator); // put best spells first
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
if (sa.getApi() == ApiType.Counter || sa.getApi() == exceptSA) {
ApiType saApi = sa.getApi();
if (saApi == ApiType.Counter || saApi == exceptSA) {
continue;
}
sa.setActivatingPlayer(player);
// TODO: this currently only works as a limited prediction of permanent spells.
// Ideally this should cast canPlaySa to determine that the AI is truly able/willing to cast a spell,
// but that is currently difficult to implement due to various side effects leading to stack overflow.
if (!ComputerUtil.castPermanentInMain1(player, sa) && sa.getHostCard() != null && !sa.getHostCard().isLand() && ComputerUtilCost.canPayCost(sa, player)) {
Card host = sa.getHostCard();
if (!ComputerUtil.castPermanentInMain1(player, sa) && host != null && !host.isLand() && ComputerUtilCost.canPayCost(sa, player)) {
if (sa instanceof SpellPermanent) {
return sa;
}
@@ -580,22 +632,42 @@ public class AiController {
return null;
}
public void reserveManaSources(SpellAbility sa) {
reserveManaSources(sa, PhaseType.MAIN2);
public boolean reserveManaSources(SpellAbility sa) {
return reserveManaSources(sa, PhaseType.MAIN2, false, false, null);
}
public void reserveManaSources(SpellAbility sa, PhaseType phaseType) {
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);
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) {
case MAIN2:
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
break;
case COMBAT_DECLARE_BLOCKERS:
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
memSet = enemy ? AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK
: AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
break;
default:
System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
@@ -603,10 +675,18 @@ public class AiController {
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
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) {
AiCardMemory.rememberCard(player, c, memSet);
}
return true;
}
return false;
}
// This is for playing spells regularly (no Cascade/Ripple etc.)
@@ -615,11 +695,27 @@ public class AiController {
return AiPlayDecision.CantPlaySa;
}
AiPlayDecision op = canPlaySa(sa);
if (op != AiPlayDecision.WillPlay) {
return op;
boolean xCost = sa.getPayCosts().hasXInAnyCostPart();
if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) {
// 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) {
@@ -630,8 +726,37 @@ public class AiController {
if (sa instanceof WrappedAbility) {
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) {
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);
// remove added extra
Sentry.getContext().removeExtra("Card");
Sentry.getContext().removeExtra("SA");
if (!canPlay) {
return AiPlayDecision.CantPlayAi;
}
@@ -686,7 +811,7 @@ public class AiController {
// will need actual logic that determines if the enchantment is able
// to disable the permanent or it's still functional and a duplicate is unneeded.
boolean disabledByEnemy = false;
for (Card card2 : card.getEnchantedBy(false)) {
for (Card card2 : card.getEnchantedBy()) {
if (card2.getOwner() != player) {
disabledByEnemy = true;
}
@@ -703,10 +828,71 @@ public class AiController {
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
}
// add any other necessary logic to play a basic spell here
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?
private final static Comparator<SpellAbility> saComparator = new Comparator<SpellAbility>() {
@Override
@@ -716,6 +902,13 @@ public class AiController {
int a1 = a.getPayCosts() == null ? 0 : a.getPayCosts().getTotalMana().getCMC();
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
// deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs
if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) {
return 1;
} else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) {
return -1;
}
// 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.*")) {
return 1;
@@ -755,6 +948,20 @@ public class AiController {
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);
b1 += getSpellAbilityPriority(b);
@@ -776,6 +983,9 @@ public class AiController {
if (source.isCreature()) {
p += 1;
}
if (source.hasSVar("AIPriorityModifier")) {
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
}
// don't play equipments before having any creatures
if (source.isEquipment() && noCreatures) {
p -= 9;
@@ -792,14 +1002,13 @@ public class AiController {
}
}
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) {
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
}
}
// use Surge and Prowl costs when able to
if (sa.isSurged() ||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
if (sa.isSurged() || sa.isProwl()) {
p += 9;
}
// sort planeswalker abilities with most costly first
@@ -942,10 +1151,14 @@ public class AiController {
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 freeCastAllowed = ComputerUtilCost.isFreeCastAllowedByPermanent(player, null);
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));
validCards.remove(validCards.get(j));
discardedUnplayable = true;
@@ -998,7 +1211,7 @@ public class AiController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
ApiType api = sa.getApi();
// Abilities without api may also use this routine, However they should provide a unique mode value
// Abilities without api may also use this routine, However they should provide a unique mode value ?? How could this work?
if (api == null) {
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
mode);
@@ -1140,6 +1353,9 @@ public class AiController {
// re-created if needed and used for any AI logic that needs it.
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) {
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
}
@@ -1161,7 +1377,7 @@ public class AiController {
if (landsWannaPlay != null) {
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
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?
Card land = chooseBestLandToPlay(landsWannaPlay);
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
@@ -1303,12 +1519,28 @@ public class AiController {
}
private final SpellAbility getSpellAbilityToPlay() {
// if top of stack is owned by me
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
// probably should let my stuff resolve
final CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
List<SpellAbility> saList = Lists.newArrayList();
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;
}
final CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
}
}
if (!game.getStack().isEmpty()) {
SpellAbility counter = chooseCounterSpell(getPlayableCounters(cards));
@@ -1319,7 +1551,13 @@ public class AiController {
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) {
@@ -1334,7 +1572,7 @@ public class AiController {
continue;
}
if (sa.getHostCard().hasKeyword("Storm")
if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.hasKeyword("Storm")))).size() > 0) {
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
@@ -1471,22 +1709,24 @@ public class AiController {
boolean hasLeyline1 = false;
SpellAbility saGemstones = null;
for(int i = 0; i < result.size(); i++) {
SpellAbility sa = result.get(i);
List<SpellAbility> toRemove = Lists.newArrayList();
for(SpellAbility sa : result) {
String srcName = sa.getHostCard().getName();
if ("Gemstone Caverns".equals(srcName)) {
if (saGemstones == null)
saGemstones = sa;
else
result.remove(i--);
toRemove.add(sa);
} else if ("Leyline of Singularity".equals(srcName)) {
if (!hasLeyline1)
hasLeyline1 = true;
else
result.remove(i--);
toRemove.add(sa);
}
}
for(SpellAbility sa : toRemove) {
result.remove(sa);
}
// Play them last
if (saGemstones != null) {
@@ -1562,11 +1802,11 @@ public class AiController {
// 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("Vanishing") && !"Chronozoa".equals(crd.getName())) {
if (crd.hasKeyword(Keyword.VANISHING) && !"Chronozoa".equals(crd.getName())) {
if (crd.getCounters(CounterType.TIME) == 1) {
return CounterType.TIME;
}
} else if (crd.hasKeyword("Fading")) {
} else if (crd.hasKeyword(Keyword.FADING)) {
if (crd.getCounters(CounterType.FADE) == 1) {
return CounterType.FADE;
}
@@ -1788,6 +2028,12 @@ public class AiController {
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();
}

View File

@@ -459,6 +459,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
String type = cost.getType();
boolean isVehicle = type.contains("+withTotalPowerGE");
CardCollection exclude = new CardCollection();
exclude.addAll(tapped);
if (c == null) {
final String sVar = ability.getSVar(amount);
if (sVar.equals("XChoice")) {
@@ -467,6 +471,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
ability.getActivatingPlayer(), ability.getHostCard(), ability);
typeList = CardLists.filter(typeList, Presets.UNTAPPED);
c = typeList.size();
// account for the fact that the activated card may be tapped in the process
if (ability.getPayCosts().hasTapCost() && typeList.contains(ability.getHostCard())) {
c--;
}
source.setSVar("ChosenX", "Number$" + Integer.toString(c));
} else {
if (!isVehicle) {
@@ -478,14 +486,32 @@ public class AiCostDecision extends CostDecisionMakerBase {
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 = "";
CardCollectionView totap;
if (isVehicle) {
totalP = type.split("withTotalPowerGE")[1];
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 {
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, exclude);
}
if (totap == null) {
@@ -510,20 +536,27 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
if (c == null) {
if (ability.getSVar(cost.getAmount()).equals("XChoice")) {
if ("SacToReduceCost".equals(ability.getParam("AILogic"))) {
String logic = ability.getParamOrDefault("AILogic", "");
if ("SacToReduceCost".equals(logic)) {
// e.g. Torgaar, Famine Incarnate
// TODO: currently returns an empty list, so the AI doesn't sacrifice anything. Trying to make
// the AI decide on creatures to sac makes the AI sacrifice them, but the cost is not reduced and the
// AI pays the full mana cost anyway (despite sacrificing creatures).
return PaymentDecision.card(new CardCollection());
}
// Other cards are assumed to be flagged RemAIDeck for now
} else if (!logic.isEmpty() && !logic.equals("Never")) {
// If at least some other AI logic is specified, assume that the AI for that API knows how
// to define ChosenX and thus honor that value.
// Cards which have no special logic for this yet but which do work in a simple/suboptimal way
// are currently conventionally flagged with AILogic$ DoSacrifice.
c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null);
} else {
// Other cards are assumed to be flagged AI:RemoveDeck:All for now
return null;
}
} else {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
}
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);
return PaymentDecision.card(list);
@@ -776,8 +809,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
final String sVar = ability.getSVar(amount);
if (sVar.equals("XChoice")) {
c = AbilityUtils.calculateAmount(source, "ChosenX", ability);
source.setSVar("ChosenX", "Number$" + String.valueOf(c));
} else if (amount.equals("All")) {
c = source.getCounters(cost.counter);
} else if (sVar.equals("Targeted$CardManaCost")) {
c = 0;
if (ability.getTargets().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 {
c = AbilityUtils.calculateAmount(source, amount, ability);
}

View File

@@ -58,6 +58,7 @@ public enum AiProps { /** */
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
TRY_TO_PRESERVE_BUYBACK_SPELLS ("true"), /** */
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
@@ -69,11 +70,18 @@ public enum AiProps { /** */
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
ALWAYS_COUNTER_AURAS ("true"), /** */
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_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
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"), /** */
USE_BERSERK_AGGRESSIVELY ("false"), /** */
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
@@ -93,6 +101,8 @@ public enum AiProps { /** */
SCRY_EVALTHR_CMC_THRESHOLD ("3"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL ("10"), /** */
SURVEIL_LIFEPERC_AFTER_PAYING_LIFE ("75"), /** */
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */
@@ -104,13 +114,24 @@ public enum AiProps { /** */
INTUITION_ALTERNATIVE_LOGIC ("false"), /** */
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE("2"), /** */
MOMIR_BASIC_LAND_STRATEGY("default"), /** */
MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA("5"), /** */
MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR ("50"), /** */
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"); /** */
// Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
// <-- There are no experimental options here -->
AI_IN_DANGER_THRESHOLD("4"),
AI_IN_DANGER_MAX_THRESHOLD("4");
private final String strDefaultVal;

View File

@@ -20,14 +20,14 @@ package forge.ai;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
import forge.ai.ability.ChooseGenericEffectAi;
import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi;
import forge.card.CardType;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCostShard;
import forge.game.CardTraitPredicates;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.*;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -37,6 +37,7 @@ import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -72,6 +73,7 @@ public class ComputerUtil {
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game, Runnable chooseTargets) {
game.getStack().freezeStack();
final Card source = sa.getHostCard();
source.setSplitStateToPlayAbility(sa);
if (sa.isSpell() && !source.isCopiedSpell()) {
if (source.getType().hasStringType("Arcane")) {
@@ -94,7 +96,9 @@ public class ComputerUtil {
sa.setHostCard(game.getAction().moveToStack(source, sa));
}
if (sa.isCopied()) {
sa.resetPaidHash();
}
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa);
@@ -259,6 +263,10 @@ public class ComputerUtil {
sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard());
newSA.setHostCard(game.getAction().moveToStack(source, sa));
if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) {
CharmEffect.makeChoices(newSA);
}
}
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
@@ -401,6 +409,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
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
@@ -789,7 +826,7 @@ public class ComputerUtil {
}
if (destroy) {
final CardCollection indestructibles = CardLists.getKeyword(remaining, "Indestructible");
final CardCollection indestructibles = CardLists.getKeyword(remaining, Keyword.INDESTRUCTIBLE);
if (!indestructibles.isEmpty()) {
return indestructibles.get(0);
}
@@ -816,7 +853,7 @@ public class ComputerUtil {
if (c != null && c.isEnchanted()) {
// 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)) {
return aura;
}
@@ -826,7 +863,7 @@ public class ComputerUtil {
}
public static boolean canRegenerate(Player ai, final Card card) {
if (card.hasKeyword("CARDNAME can't be regenerated.")) {
if (!card.canBeShielded()) {
return false;
}
@@ -939,7 +976,7 @@ public class ComputerUtil {
}
// try not to cast Raid creatures in main 1 if an attack is likely
if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword("Haste")) {
if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword(Keyword.HASTE)) {
for (Card potentialAtkr: ai.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
return false;
@@ -951,12 +988,33 @@ public class ComputerUtil {
return true;
}
if (card.isCreature() && !card.hasKeyword("Defender")
&& (card.hasKeyword("Haste") || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) {
if (card.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) {
// Planning to choose Haste for Riot, so do this in Main 1
return true;
}
if (card.hasKeyword("Exalted")) {
// 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)
&& (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) {
return true;
}
if (card.hasKeyword(Keyword.EXALTED)) {
return true;
}
@@ -968,7 +1026,7 @@ public class ComputerUtil {
playNow = false;
break;
}
if (!playNow && c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c) && c.canBeEquippedBy(card)) {
if (!playNow && c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c) && c.canBeAttached(card)) {
playNow = true;
}
}
@@ -991,16 +1049,16 @@ public class ComputerUtil {
return true;
}
if (card.isCreature()) {
if (buffedcard.hasKeyword("Soulbond") && !buffedcard.isPaired()) {
if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) {
return true;
}
if (buffedcard.hasKeyword("Evolve")) {
if (buffedcard.hasKeyword(Keyword.EVOLVE)) {
if (buffedcard.getNetPower() < card.getNetPower() || buffedcard.getNetToughness() < card.getNetToughness()) {
return true;
}
}
}
if (card.hasKeyword("Soulbond") && buffedcard.isCreature() && !buffedcard.isPaired()) {
if (card.hasKeyword(Keyword.SOULBOND) && buffedcard.isCreature() && !buffedcard.isPaired()) {
return true;
}
@@ -1018,6 +1076,20 @@ public class ComputerUtil {
}
} // 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");
if (!vengevines.isEmpty()) {
final CardCollectionView creatures = ai.getCardsIn(ZoneType.Hand);
@@ -1248,7 +1320,7 @@ public class ComputerUtil {
// Special for Odric
if (ai.isCardInPlay("Odric, Lunarch Marshal")
&& !CardLists.getKeyword(all, "Haste").isEmpty()) {
&& !CardLists.getKeyword(all, Keyword.HASTE).isEmpty()) {
return true;
}
@@ -1317,7 +1389,7 @@ public class ComputerUtil {
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
final ArrayList affected = Lists.newArrayList(params.get("Affected").split(","));
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(","));
if (affected.contains("Creature")) {
return true;
}
@@ -1340,6 +1412,16 @@ public class ComputerUtil {
if (sa.getApi() != ApiType.Fog) {
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)) {
continue;
}
@@ -1376,6 +1458,9 @@ public class ComputerUtil {
if (!ComputerUtilCost.canPayCost(sa, ai)) {
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;
}
@@ -1547,7 +1632,7 @@ public class ComputerUtil {
final Card c = (Card) o;
// indestructible
if (c.hasKeyword("Indestructible")) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1594,7 +1679,7 @@ public class ComputerUtil {
} else if (o instanceof Player) {
final Player p = (Player) o;
if (source.hasKeyword("Infect")) {
if (source.hasKeyword(Keyword.INFECT)) {
if (ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= p.getPoisonCounters()) {
threatened.add(p);
}
@@ -1615,14 +1700,14 @@ public class ComputerUtil {
if (o instanceof Card) {
final Card c = (Card) o;
final boolean canRemove = (c.getNetToughness() <= dmg)
|| (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c)));
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c)));
if (!canRemove) {
continue;
}
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
final boolean cantSave = c.getNetToughness() + toughness <= dmg
|| (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && !grantIndestructible
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && !grantIndestructible
&& (dmg >= toughness + ComputerUtilCombat.getDamageToKill(c)));
if (cantSave && (tgt == null || !grantShroud)) {
continue;
@@ -1662,7 +1747,7 @@ public class ComputerUtil {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
if (c.hasKeyword("Indestructible")) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1754,6 +1839,68 @@ public class ComputerUtil {
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) {
final Card source = sa.getHostCard();
final Zone zone = source.getZone();
@@ -2629,7 +2776,7 @@ public class ComputerUtil {
|| (type == CounterType.TIME && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|| type == CounterType.GOLD || type == CounterType.MUSIC || type == CounterType.PUPA
|| type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
|| type == CounterType.SLEIGHT || type == CounterType.WAGE;
|| type == CounterType.SLUMBER || type == CounterType.SLEIGHT || type == CounterType.WAGE;
}
// this countertypes has no effect
@@ -2701,7 +2848,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
return false;
@@ -2732,7 +2879,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
// no life gain is not negative
@@ -2762,6 +2909,10 @@ public class ComputerUtil {
if (ab.getApi() == null) {
// only API-based SAs are supported, other things may lead to a NPE (e.g. Ancestral Vision Suspend SA)
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();
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
@@ -2841,7 +2992,7 @@ public class ComputerUtil {
if (sa.getParam("AITgts").equals("BetterThanSource")) {
int value = ComputerUtilCard.evaluateCreature(source);
if (source.isEnchanted()) {
for (Card enc : source.getEnchantedBy(false)) {
for (Card enc : source.getEnchantedBy()) {
if (enc.getController().equals(ai)) {
value += 100; // is 100 per AI's own aura enough?
}

View File

@@ -1,11 +1,7 @@
package forge.ai;
import java.util.Iterator;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GameActionUtil;
@@ -16,10 +12,14 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets;
import forge.game.player.Player;
import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
import java.util.Iterator;
import java.util.List;
public class ComputerUtilAbility {
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
@@ -95,18 +95,58 @@ public class ComputerUtilAbility {
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
final List<SpellAbility> newAbilities = Lists.newArrayList();
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
for (SpellAbility sa : originList) {
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
sa.setActivatingPlayer(player);
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
}
for (SpellAbility sa : originListWithAddCosts) {
// determine which alternative costs are cheaper than the original and prioritize them
List<SpellAbility> saAltCosts = GameActionUtil.getAlternativeCosts(sa, player);
List<SpellAbility> priorityAltSa = Lists.newArrayList();
List<SpellAbility> otherAltSa = Lists.newArrayList();
for (SpellAbility altSa : saAltCosts) {
if (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.addAll(GameActionUtil.getAlternativeCosts(sa, player));
newAbilities.addAll(otherAltSa);
}
final List<SpellAbility> result = Lists.newArrayList();
for (SpellAbility sa : newAbilities) {
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;
}
@@ -128,6 +168,17 @@ public class ComputerUtilAbility {
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) {
return sa.getOriginalHost() != null ? sa.getOriginalHost() : sa.getHostCard();
}

View File

@@ -21,6 +21,7 @@ import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection;
@@ -585,7 +586,7 @@ public class ComputerUtilCard {
// Add all cost of all auras with the same controller
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();
}
@@ -833,7 +834,7 @@ public class ComputerUtilCard {
int score = tmp.isTapped() ? 2 : 0;
score += tmp.isBasicLand() ? 1 : 0;
score -= tmp.isCreature() ? 4 : 0;
for (Card aura : tmp.getEnchantedBy(false)) {
for (Card aura : tmp.getEnchantedBy()) {
if (aura.getController().isOpponentOf(tmp.getController())) {
score += 5;
} else {
@@ -857,7 +858,7 @@ public class ComputerUtilCard {
int score = tmp.isTapped() ? 0 : 2;
score += tmp.isBasicLand() ? 2 : 0;
score -= tmp.isCreature() ? 4 : 0;
score -= 5 * tmp.getEnchantedBy(false).size();
score -= 5 * tmp.getEnchantedBy().size();
if (score >= maxScore) {
land = tmp;
@@ -1033,7 +1034,7 @@ public class ComputerUtilCard {
// interrupt 3: two for one = good
if (c.isEnchanted()) {
boolean myEnchants = false;
for (Card enc : c.getEnchantedBy(false)) {
for (Card enc : c.getEnchantedBy()) {
if (enc.getOwner().equals(ai)) {
myEnchants = true;
break;
@@ -1081,7 +1082,7 @@ public class ComputerUtilCard {
valueTempo *= 2; //deal with annoying things
}
if (!destination.equals(ZoneType.Graveyard) && //TODO:boat-load of "when blah dies" triggers
c.hasKeyword("Persist") || c.hasKeyword("Undying") || c.hasKeyword("Modular")) {
c.hasKeyword(Keyword.PERSIST) || c.hasKeyword(Keyword.UNDYING) || c.hasKeyword(Keyword.MODULAR)) {
valueTempo *= 2;
}
if (destination.equals(ZoneType.Hand) && !c.isToken()) {
@@ -1311,11 +1312,23 @@ public class ComputerUtilCard {
//2. grant haste
if (keywords.contains("Haste") && c.hasSickness() && !c.isTapped()) {
chance += 0.5f;
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
double nonCombatChance = 0.0f;
double combatChance = 0.0f;
// 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
if (!CardLists.filter(oppCreatures, CardPredicates.possibleBlockers(c)).isEmpty()) {
@@ -1358,8 +1371,9 @@ public class ComputerUtilCard {
//1. save combatant
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && !pumpedWillDie
&& !c.hasKeyword("Indestructible")) { // hack because attackerWouldBeDestroyed() does not
// check for Indestructible when computing lethal damage
&& !c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
// hack because attackerWouldBeDestroyed()
// does not check for Indestructible when computing lethal damage
return true;
}
@@ -1396,17 +1410,17 @@ public class ComputerUtilCard {
int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
// predict Infect
if (pumpedDmg == 0 && c.hasKeyword("Infect")) {
if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) {
if (poisonPumped > poisonOrig) {
pumpedDmg = poisonPumped;
}
}
if (combat.isBlocked(c)) {
if (!c.hasKeyword("Trample")) {
if (!c.hasKeyword(Keyword.TRAMPLE)) {
dmg = 0;
}
if (c.hasKeyword("Trample") || keywords.contains("Trample")) {
if (c.hasKeyword(Keyword.TRAMPLE) || keywords.contains("Trample")) {
for (Card b : combat.getBlockers(c)) {
pumpedDmg -= ComputerUtilCombat.getDamageToKill(b);
}
@@ -1415,8 +1429,8 @@ public class ComputerUtilCard {
}
}
if (pumpedDmg > dmg) {
if ((!c.hasKeyword("Infect") && pumpedDmg >= opp.getLife())
|| (c.hasKeyword("Infect") && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
if ((!c.hasKeyword(Keyword.INFECT) && pumpedDmg >= opp.getLife())
|| (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
return true;
}
}
@@ -1425,7 +1439,7 @@ public class ComputerUtilCard {
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) {
int totalPowerUnblocked = 0;
for (Card atk : combat.getAttackers()) {
if (combat.isBlocked(atk) && !atk.hasKeyword("Trample")) {
if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) {
continue;
}
if (atk == c) {
@@ -1458,7 +1472,7 @@ public class ComputerUtilCard {
}
//4. lifelink
if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword("Lifelink") && keywords.contains("Lifelink")
if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword(Keyword.LIFELINK) && keywords.contains("Lifelink")
&& (combat.isAttacking(c) || combat.isBlocking(c))) {
int dmg = pumped.getNetCombatDamage();
//The actual dmg inflicted should be the sum of ComputerUtilCombat.predictDamageTo() for opposing creature
@@ -1471,7 +1485,7 @@ public class ComputerUtilCard {
List<Card> blockedBy = combat.getAttackersBlockedBy(c);
boolean attackerHasTrample = false;
for (Card b : blockedBy) {
attackerHasTrample |= b.hasKeyword("Trample");
attackerHasTrample |= b.hasKeyword(Keyword.TRAMPLE);
}
if (attackerHasTrample && (sa.isAbility() || ComputerUtilCombat.lifeInDanger(ai, combat))) {
return true;
@@ -1479,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 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)
@@ -1491,7 +1513,7 @@ public class ComputerUtilCard {
}
}
boolean wantToHoldTrick = holdCombatTricks;
boolean wantToHoldTrick = holdCombatTricks && !ai.getCardsIn(ZoneType.Hand).isEmpty();
if (chanceToHoldCombatTricks >= 0) {
// Obey the chance specified in the AI profile for holding combat tricks
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
@@ -1507,14 +1529,18 @@ public class ComputerUtilCard {
// 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
// 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
// the combat trick
boolean reserved = false;
if (ai.getController().isAI()) {
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS);
}
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;
}
}
} else {
// Don't try to mix "lure" and "precast" paradigms for combat tricks, since that creates issues with
// the AI overextending the attack
@@ -1572,10 +1598,10 @@ public class ComputerUtilCard {
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness);
pumped.addChangedCardKeywords(kws, new ArrayList<String>(), false, timestamp);
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
Set<CounterType> types = c.getCounters().keySet();
for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), c, true);
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true);
}
//Copies tap-state and extra keywords (auras, equipment, etc.)
if (c.isTapped()) {
@@ -1595,7 +1621,7 @@ public class ComputerUtilCard {
}
}
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
pumped.addChangedCardKeywordsInternal(toCopy, Lists.<KeywordInterface>newArrayList(), false, timestamp2, true);
pumped.addChangedCardKeywordsInternal(toCopy, null, false, false, timestamp2, true);
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
return pumped;
}
@@ -1615,6 +1641,7 @@ public class ComputerUtilCard {
if (exclude != null) {
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 StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
@@ -1624,6 +1651,9 @@ public class ComputerUtilCard {
if (!params.containsKey("Affected")) {
continue;
}
if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) {
continue;
}
final String valid = params.get("Affected");
if (!vCard.isValid(valid, c.getController(), c, null)) {
continue;
@@ -1713,10 +1743,10 @@ public class ComputerUtilCard {
}
public static boolean hasActiveUndyingOrPersist(final Card c) {
if (c.hasKeyword("Undying") && c.getCounters(CounterType.P1P1) == 0) {
if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterType.P1P1) == 0) {
return true;
}
if (c.hasKeyword("Persist") && c.getCounters(CounterType.M1M1) == 0) {
if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterType.M1M1) == 0) {
return true;
}
return false;
@@ -1781,18 +1811,23 @@ public class ComputerUtilCard {
CardCollection priorityCards = new CardCollection();
for (Card atk : oppCards) {
boolean canBeBlocked = false;
if (isUselessCreature(atk.getController(), atk)) {
continue;
}
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;
if (!priorityRemovalOnlyInDanger || threat) {
priorityCards.add(atk);
}
}
}
}
if (!priorityCards.isEmpty() && priorityCards.size() <= priorityRemovalThreshold) {
return priorityCards;
@@ -1840,4 +1875,13 @@ public class ComputerUtilCard {
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();
}
}

View File

@@ -36,6 +36,7 @@ import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.Untap;
import forge.game.player.Player;
@@ -201,10 +202,10 @@ public class ComputerUtilCombat {
}
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
if (!attacker.hasKeyword("Infect")) {
if (!attacker.hasKeyword(Keyword.INFECT)) {
sum = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
if (attacker.hasKeyword("Double Strike")) {
sum += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
sum *= 2;
}
}
return sum;
@@ -226,14 +227,15 @@ public class ComputerUtilCombat {
int damage = attacker.getNetCombatDamage();
int poison = 0;
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, null, false);
if (attacker.hasKeyword("Infect")) {
poison += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
if (attacker.hasKeyword("Double Strike")) {
poison += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
if (attacker.hasKeyword(Keyword.INFECT)) {
int pd = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
poison += pd;
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
poison += pd;
}
}
if (attacker.hasKeyword("Poisonous") && (damage > 0)) {
poison += attacker.getKeywordMagnitude("Poisonous");
if (attacker.hasKeyword(Keyword.POISONOUS) && (damage > 0)) {
poison += attacker.getKeywordMagnitude(Keyword.POISONOUS);
}
return poison;
}
@@ -301,9 +303,9 @@ public class ComputerUtilCombat {
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage "
+ "as though it weren't blocked.")) {
unblocked.add(attacker);
} else if (attacker.hasKeyword("Trample")
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
if (!attacker.hasKeyword("Infect")) {
if (!attacker.hasKeyword(Keyword.INFECT)) {
damage += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
}
}
@@ -330,6 +332,11 @@ public class ComputerUtilCombat {
*/
public static int resultingPoison(final Player ai, final Combat combat) {
// ai can't get poision counters, so the value can't change
if (!ai.canReceiveCounters(CounterType.POISON)) {
return ai.getPoisonCounters();
}
int poison = 0;
final List<Card> attackers = combat.getAttackersOf(ai);
@@ -343,13 +350,13 @@ public class ComputerUtilCombat {
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage"
+ " as though it weren't blocked.")) {
unblocked.add(attacker);
} else if (attacker.hasKeyword("Trample")
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
if (attacker.hasKeyword("Infect")) {
if (attacker.hasKeyword(Keyword.INFECT)) {
poison += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
}
if (attacker.hasKeyword("Poisonous")) {
poison += attacker.getKeywordMagnitude("Poisonous");
if (attacker.hasKeyword(Keyword.POISONOUS)) {
poison += attacker.getKeywordMagnitude(Keyword.POISONOUS);
}
}
}
@@ -576,7 +583,7 @@ public class ComputerUtilCombat {
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
if (defender.hasKeyword("Double Strike")) {
if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true);
}
@@ -590,25 +597,26 @@ public class ComputerUtilCombat {
* @return
*/
private static int predictDamageByBlockerWithoutDoubleStrike(final Card attacker, final Card defender) {
if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword("Indestructible")) {
if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
int flankingMagnitude = 0;
if (attacker.hasKeyword("Flanking") && !defender.hasKeyword("Flanking")) {
if (attacker.hasKeyword(Keyword.FLANKING) && !defender.hasKeyword(Keyword.FLANKING)) {
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= defender.getNetToughness()) {
return 0;
}
if ((flankingMagnitude >= (defender.getNetToughness() - defender.getDamage()))
&& !defender.hasKeyword("Indestructible")) {
&& !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
} // flanking
if (attacker.hasKeyword("Indestructible") && !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE)
&& !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) {
return 0;
}
@@ -667,21 +675,21 @@ public class ComputerUtilCombat {
}
int flankingMagnitude = 0;
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= blocker.getNetToughness()) {
return 0;
}
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
&& !blocker.hasKeyword("Indestructible")) {
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
} // flanking
final int defBushidoMagnitude = blocker.getKeywordMagnitude("Bushido");
final int defBushidoMagnitude = blocker.getKeywordMagnitude(Keyword.BUSHIDO);
final int defenderDefense = (blocker.getLethalDamage() - flankingMagnitude) + defBushidoMagnitude;
@@ -727,16 +735,16 @@ public class ComputerUtilCombat {
for (final Card defender : blockers) {
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true)
&& !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
&& !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) {
return true;
}
if (defender.hasKeyword("First Strike") || defender.hasKeyword("Double Strike")) {
if (defender.hasKeyword(Keyword.FIRST_STRIKE) || defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
firstStrikeBlockerDmg += defender.getNetCombatDamage();
}
}
// Consider first strike and double strike
if (attacker.hasKeyword("First Strike") || attacker.hasKeyword("Double Strike")) {
if (attacker.hasKeyword(Keyword.FIRST_STRIKE) || attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
return firstStrikeBlockerDmg >= ComputerUtilCombat.getDamageToKill(attacker);
}
@@ -920,9 +928,9 @@ public class ComputerUtilCombat {
// if the attacker has first strike and wither the blocker will deal
// less damage than expected
if (dealsFirstStrikeDamage(attacker, withoutAbilities, null)
&& (attacker.hasKeyword("Wither") || attacker.hasKeyword("Infect"))
&& (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, null)
&& !blocker.hasKeyword("CARDNAME can't have counters put on it.")) {
&& !blocker.canReceiveCounters(CounterType.M1M1)) {
power -= attacker.getNetCombatDamage();
}
@@ -967,12 +975,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
continue;
}
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump")) {
continue;
}
@@ -1041,6 +1057,10 @@ public class ComputerUtilCombat {
continue;
}
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (pBonus > 0) {
@@ -1069,8 +1089,8 @@ public class ComputerUtilCombat {
public static int predictToughnessBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
int toughness = 0;
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
toughness -= attacker.getAmountOfKeyword("Flanking");
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
toughness -= attacker.getAmountOfKeyword(Keyword.FLANKING);
}
if (blocker.getName().equals("Shape Stealer")) {
@@ -1090,12 +1110,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
continue;
}
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
String abType = "";
if (abilityParams.containsKey("AB")) {
abType = abilityParams.get("AB");
@@ -1205,6 +1233,10 @@ public class ComputerUtilCombat {
continue;
}
if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (tBonus > 0) {
@@ -1239,9 +1271,7 @@ public class ComputerUtilCombat {
//check Exalted only for the first attacker
if (combat != null && combat.getAttackers().isEmpty()) {
for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
power += card.getAmountOfKeyword("Exalted");
}
power += attacker.getController().countExaltedBonus();
}
// Serene Master switches power with attacker
@@ -1263,9 +1293,9 @@ public class ComputerUtilCombat {
// less damage than expected
if (null != blocker) {
if (ComputerUtilCombat.dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
&& (blocker.hasKeyword("Wither") || blocker.hasKeyword("Infect"))
&& (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))
&& !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
&& !attacker.hasKeyword("CARDNAME can't have counters put on it.")) {
&& !attacker.canReceiveCounters(CounterType.M1M1)) {
power -= blocker.getNetCombatDamage();
}
theTriggers.addAll(blocker.getTriggers());
@@ -1305,12 +1335,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
continue;
}
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
continue; // targeted pumping not supported
}
@@ -1324,7 +1362,14 @@ public class ComputerUtilCombat {
}
if (abilityParams.containsKey("Cost")) {
final SpellAbility sa = AbilityFactory.getAbility(ability, source);
SpellAbility sa = null;
if (trigger.getOverridingAbility() != null) {
sa = trigger.getOverridingAbility();
} else {
final String ability = source.getSVar(trigParams.get("Execute"));
sa = AbilityFactory.getAbility(ability, source);
}
sa.setActivatingPlayer(source.getController());
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
@@ -1410,6 +1455,10 @@ public class ComputerUtilCombat {
continue;
}
if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (pBonus > 0) {
@@ -1446,9 +1495,7 @@ public class ComputerUtilCombat {
//check Exalted only for the first attacker
if (combat != null && combat.getAttackers().isEmpty()) {
for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
toughness += card.getAmountOfKeyword("Exalted");
}
toughness += attacker.getController().countExaltedBonus();
}
if (blocker != null && attacker.getName().equals("Shape Stealer")) {
@@ -1494,7 +1541,7 @@ public class ComputerUtilCombat {
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature");
if (!attacker.isValid(valid, card.getController(), card, null)
|| attacker.hasKeyword("Vigilance")) {
|| attacker.hasKeyword(Keyword.VIGILANCE)) {
continue;
}
// remove the bonus, because it will no longer be granted
@@ -1510,12 +1557,20 @@ public class ComputerUtilCombat {
final Map<String, String> trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
|| !trigParams.containsKey("Execute")) {
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
continue;
}
Map<String, String> abilityParams = null;
if (trigger.getOverridingAbility() != null) {
abilityParams = trigger.getOverridingAbility().getMapParams();
} else if (trigParams.containsKey("Execute")) {
final String ability = source.getSVar(trigParams.get("Execute"));
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
abilityParams = AbilityFactory.getMapParams(ability);
} else {
continue;
}
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
continue; // targeted pumping not supported
}
@@ -1548,7 +1603,14 @@ public class ComputerUtilCombat {
}
if (abilityParams.containsKey("Cost")) {
final SpellAbility sa = AbilityFactory.getAbility(ability, source);
SpellAbility sa = null;
if (trigger.getOverridingAbility() != null) {
sa = trigger.getOverridingAbility();
} else {
final String ability = source.getSVar(trigParams.get("Execute"));
sa = AbilityFactory.getAbility(ability, source);
}
sa.setActivatingPlayer(source.getController());
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
@@ -1630,6 +1692,10 @@ public class ComputerUtilCombat {
continue;
}
if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (tBonus > 0) {
@@ -1647,7 +1713,7 @@ public class ComputerUtilCombat {
if (blocker.isEquippedBy("Godsend")) {
return true;
}
if (attacker.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(attacker.getController(), attacker)) {
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(attacker.getController(), attacker)) {
return false;
}
@@ -1712,12 +1778,12 @@ public class ComputerUtilCombat {
*/
public static boolean attackerCantBeDestroyedInCombat(Player ai, final Card attacker) {
// attacker is either indestructible or may regenerate
if (attacker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, attacker))) {
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker))) {
return true;
}
// attacker will regenerate
if (attacker.getShieldCount() > 0 && !attacker.hasKeyword("CARDNAME can't be regenerated.")) {
if (attacker.getShieldCount() > 0 && attacker.canBeShielded()) {
return true;
}
@@ -1766,24 +1832,24 @@ public class ComputerUtilCombat {
}
int flankingMagnitude = 0;
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= blocker.getNetToughness()) {
return false;
}
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
&& !blocker.hasKeyword("Indestructible")) {
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return false;
}
} // flanking
if (((attacker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities))
&& !(blocker.hasKeyword("Wither") || blocker.hasKeyword("Infect")))
|| (attacker.hasKeyword("Persist") && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker
if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities))
&& !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)))
|| (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker
.getCounters(CounterType.M1M1) == 0))
|| (attacker.hasKeyword("Undying") && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker
|| (attacker.hasKeyword(Keyword.UNDYING) && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker
.getCounters(CounterType.P1P1) == 0))) {
return false;
}
@@ -1830,7 +1896,7 @@ public class ComputerUtilCombat {
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
if (blocker.hasKeyword("Double Strike")) {
if (blocker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true;
}
@@ -1840,7 +1906,8 @@ public class ComputerUtilCombat {
// Attacker may kill the blocker before he can deal normal
// (secondary) damage
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat) && !blocker.hasKeyword("Indestructible")) {
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
if (attackerDamage >= defenderLife) {
return false;
}
@@ -1856,7 +1923,7 @@ public class ComputerUtilCombat {
else { // no double strike for defender
// Attacker may kill the blocker before he can deal any damage
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
&& !blocker.hasKeyword("Indestructible")
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, combat)) {
if (attackerDamage >= defenderLife) {
@@ -1895,7 +1962,7 @@ public class ComputerUtilCombat {
for (Card attacker : attackers) {
if (ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, true)
&& !(attacker.hasKeyword("Wither") || attacker.hasKeyword("Infect"))) {
&& !(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) {
return true;
}
}
@@ -1913,19 +1980,21 @@ public class ComputerUtilCombat {
}
int flankingMagnitude = 0;
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= blocker.getNetToughness()) {
return true;
}
if ((flankingMagnitude >= ComputerUtilCombat.getDamageToKill(blocker)) && !blocker.hasKeyword("Indestructible")) {
if ((flankingMagnitude >= ComputerUtilCombat.getDamageToKill(blocker))
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return true;
}
} // flanking
if (blocker.hasKeyword("Indestructible") || dontTestRegen || ComputerUtil.canRegenerate(blocker.getController(), blocker)) {
if (blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || dontTestRegen
|| ComputerUtil.canRegenerate(blocker.getController(), blocker)) {
return false;
}
@@ -2010,11 +2079,11 @@ public class ComputerUtilCombat {
return true;
}
if (((blocker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker
.hasKeyword("Wither") || attacker.hasKeyword("Infect")))
|| (blocker.hasKeyword("Persist") && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker
if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker
.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)))
|| (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker
.getCounters(CounterType.M1M1) == 0))
|| (blocker.hasKeyword("Undying") && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker
|| (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker
.getCounters(CounterType.P1P1) == 0))) {
return false;
}
@@ -2051,6 +2120,16 @@ public class ComputerUtilCombat {
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, 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) {
for (Card atkr : combat.getAttackersBlockedBy(blocker)) {
if (!atkr.equals(attacker)) {
@@ -2064,7 +2143,7 @@ public class ComputerUtilCombat {
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
if (attacker.hasKeyword("Double Strike")) {
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
return true;
}
@@ -2074,7 +2153,8 @@ public class ComputerUtilCombat {
// Attacker may kill the blocker before he can deal normal
// (secondary) damage
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible")) {
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
&& !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
if (defenderDamage >= attackerLife) {
return false;
}
@@ -2089,7 +2169,8 @@ public class ComputerUtilCombat {
else { // no double strike for attacker
// Defender may kill the attacker before he can deal any damage
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible")
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
&& !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)
&& !dealsFirstStrikeDamage(attacker, withoutAbilities, combat)) {
if (defenderDamage >= attackerLife) {
@@ -2136,7 +2217,7 @@ public class ComputerUtilCombat {
return damageMap;
}
final boolean hasTrample = attacker.hasKeyword("Trample");
final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE);
if (block.size() == 1) {
final Card blocker = block.getFirst();
@@ -2238,11 +2319,11 @@ public class ComputerUtilCombat {
final boolean noPrevention) {
final int killDamage = c.isPlaneswalker() ? c.getCurrentLoyalty() : ComputerUtilCombat.getDamageToKill(c);
if (c.hasKeyword("Indestructible") || c.getShieldCount() > 0) {
if (!(source.hasKeyword("Wither") || source.hasKeyword("Infect"))) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getShieldCount() > 0) {
if (!(source.hasKeyword(Keyword.WITHER) || source.hasKeyword(Keyword.INFECT))) {
return maxDamage + 1;
}
} else if (source.hasKeyword("Deathtouch")) {
} else if (source.hasKeyword(Keyword.DEATHTOUCH)) {
for (int i = 1; i <= maxDamage; i++) {
if (noPrevention) {
if (c.staticReplaceDamage(i, source, isCombat) > 0) {
@@ -2405,7 +2486,7 @@ public class ComputerUtilCombat {
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
if (combatant.hasKeyword("Double Strike") || combatant.hasKeyword("First Strike")) {
if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) {
return true;
}
@@ -2495,7 +2576,7 @@ public class ComputerUtilCombat {
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();
// first try to replace the damage
@@ -2509,14 +2590,14 @@ public class ComputerUtilCombat {
// repParams.put("PreventMap", preventMap);
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
return !list.isEmpty();
}
public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) {
// TODO: expand this to account for more complex situations like the Wildfire Eternal unblocked trigger
int afflictDmg = attacker.getKeywordMagnitude("Afflict");
int afflictDmg = attacker.getKeywordMagnitude(Keyword.AFFLICT);
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
}
@@ -2541,18 +2622,10 @@ public class ComputerUtilCombat {
CardCollection withoutEvasion = new CardCollection();
for (Card atk : attackers) {
boolean hasProtection = false;
for (KeywordInterface inst : atk.getKeywords()) {
String kw = inst.getOriginal();
if (kw.startsWith("Protection")) {
hasProtection = true;
break;
}
}
if (atk.hasKeyword("Flying") || atk.hasKeyword("Shadow")
|| atk.hasKeyword("Horsemanship") || (atk.hasKeyword("Fear")
|| atk.hasKeyword("Intimidate") || atk.hasKeyword("Skulk") || hasProtection)) {
if (atk.hasKeyword(Keyword.FLYING) || atk.hasKeyword(Keyword.SHADOW)
|| atk.hasKeyword(Keyword.HORSEMANSHIP) || (atk.hasKeyword(Keyword.FEAR)
|| atk.hasKeyword(Keyword.INTIMIDATE) || atk.hasKeyword(Keyword.SKULK)
|| atk.hasKeyword(Keyword.PROTECTION))) {
withEvasion.add(atk);
} else {
withoutEvasion.add(atk);
@@ -2608,6 +2681,30 @@ public class ComputerUtilCombat {
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;
}
}

View File

@@ -5,15 +5,13 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.game.GameActionUtil;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
@@ -77,7 +75,7 @@ public class ComputerUtilCost {
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
if (type.name().equals("P1P1")) {
if (CounterType.P1P1.equals(type)) {
return false;
}
continue;
@@ -92,7 +90,7 @@ public class ComputerUtilCost {
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
final String sVar = sa.getSVar(remCounter.getAmount());
if (sVar.equals("XChoice")) {
if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) {
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
}
}
@@ -108,7 +106,8 @@ public class ComputerUtilCost {
}
//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;
}
}
@@ -145,6 +144,7 @@ public class ComputerUtilCost {
continue;
}
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
if (pref == null) {
@@ -300,7 +300,7 @@ public class ComputerUtilCost {
if (!important) {
return false;
}
if (!CardLists.filterControlledBy(source.getEnchantedBy(false), source.getController()).isEmpty()) {
if (!CardLists.filterControlledBy(source.getEnchantedBy(), source.getController()).isEmpty()) {
return false;
}
continue;
@@ -446,7 +446,7 @@ public class ComputerUtilCost {
// Check for stuff like Nether Void
int extraManaNeeded = 0;
if (sa instanceof Spell) {
final boolean cannotBeCountered = sa.getHostCard().hasKeyword("CARDNAME can't be countered.");
final boolean cannotBeCountered = !CardFactoryUtil.isCounterable(sa.getHostCard());
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) {
@@ -495,6 +495,7 @@ public class ComputerUtilCost {
}
}
}
// KLD vehicle
if (sa.hasParam("Crew")) { // put under checkTapTypeCost?
for (final CostPart part : sa.getPayCosts().getCostParts()) {
@@ -504,6 +505,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)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
} // canPayCost()
@@ -607,7 +638,6 @@ public class ComputerUtilCost {
Set<String> colorsAvailable = Sets.newHashSet();
if (additionalLands != null) {
GameActionUtil.grantBasicLandsManaAbilities(additionalLands);
cardsToConsider.addAll(additionalLands);
}
@@ -621,4 +651,15 @@ public class ComputerUtilCost {
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;
}
}

View File

@@ -16,11 +16,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostPayment;
import forge.game.cost.*;
import forge.game.mana.Mana;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.mana.ManaPool;
@@ -860,14 +856,22 @@ public class ComputerUtilMana {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
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();
// For combat tricks, always obey mana reservation
if (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
}
else {
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) {
} else if (!(ai.getGame().getPhaseHandler().isPlayerTurn(ai)) && (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP)) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
} else {
if ((AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) ||
(AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK))) {
// This mana source is held elsewhere for a combat trick.
return true;
}
@@ -910,6 +914,10 @@ public class ComputerUtilMana {
// Make mana needed to avoid negative effect a mandatory cost for the AI
for (String manaPart : card.getSVar("ManaNeededToAvoidNegativeEffect").split(",")) {
// convert long color strings to short color strings
if (manaPart.isEmpty()) {
continue;
}
byte mask = ManaAtom.fromName(manaPart);
// make mana mandatory for AI
@@ -1506,7 +1514,7 @@ public class ComputerUtilMana {
final Card offering = sa.getSacrificedAsOffering();
offering.setUsedToPay(false);
if (costIsPaid && !test) {
sa.getHostCard().getController().getGame().getAction().sacrifice(offering, sa);
sa.getHostCard().getGame().getAction().sacrifice(offering, sa, null);
}
sa.resetSacrificedAsOffering();
}
@@ -1514,7 +1522,7 @@ public class ComputerUtilMana {
final Card emerge = sa.getSacrificedAsEmerge();
emerge.setUsedToPay(false);
if (costIsPaid && !test) {
sa.getHostCard().getController().getGame().getAction().sacrifice(emerge, sa);
sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, null);
}
sa.resetSacrificedAsEmerge();
}

View File

@@ -7,6 +7,7 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterType;
import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.spellability.SpellAbility;
@@ -53,10 +54,10 @@ public class CreatureEvaluator implements Function<Card, Integer> {
}
// Evasion keywords
if (c.hasKeyword("Flying")) {
if (c.hasKeyword(Keyword.FLYING)) {
value += addValue(power * 10, "flying");
}
if (c.hasKeyword("Horsemanship")) {
if (c.hasKeyword(Keyword.HORSEMANSHIP)) {
value += addValue(power * 10, "horses");
}
if (c.hasKeyword("Unblockable")) {
@@ -65,13 +66,13 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
value += addValue(power * 6, "thorns");
}
if (c.hasKeyword("Fear")) {
if (c.hasKeyword(Keyword.FEAR)) {
value += addValue(power * 6, "fear");
}
if (c.hasKeyword("Intimidate")) {
if (c.hasKeyword(Keyword.INTIMIDATE)) {
value += addValue(power * 6, "intimidate");
}
if (c.hasStartOfKeyword("Menace")) {
if (c.hasKeyword(Keyword.MENACE)) {
value += addValue(power * 4, "menace");
}
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
@@ -81,49 +82,49 @@ public class CreatureEvaluator implements Function<Card, Integer> {
// Other good keywords
if (power > 0) {
if (c.hasKeyword("Double Strike")) {
if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
value += addValue(10 + (power * 15), "ds");
} else if (c.hasKeyword("First Strike")) {
} else if (c.hasKeyword(Keyword.FIRST_STRIKE)) {
value += addValue(10 + (power * 5), "fs");
}
if (c.hasKeyword("Deathtouch")) {
if (c.hasKeyword(Keyword.DEATHTOUCH)) {
value += addValue(25, "dt");
}
if (c.hasKeyword("Lifelink")) {
if (c.hasKeyword(Keyword.LIFELINK)) {
value += addValue(power * 10, "lifelink");
}
if (power > 1 && c.hasKeyword("Trample")) {
if (power > 1 && c.hasKeyword(Keyword.TRAMPLE)) {
value += addValue((power - 1) * 5, "trample");
}
if (c.hasKeyword("Vigilance")) {
if (c.hasKeyword(Keyword.VIGILANCE)) {
value += addValue((power * 5) + (toughness * 5), "vigilance");
}
if (c.hasKeyword("Wither")) {
if (c.hasKeyword(Keyword.WITHER)) {
value += addValue(power * 10, "Wither");
}
if (c.hasKeyword("Infect")) {
if (c.hasKeyword(Keyword.INFECT)) {
value += addValue(power * 15, "infect");
}
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
value += addValue(c.getKeywordMagnitude("Afflict") * 5, "afflict");
value += addValue(c.getKeywordMagnitude(Keyword.RAMPAGE), "rampage");
value += addValue(c.getKeywordMagnitude(Keyword.AFFLICT) * 5, "afflict");
}
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
value += addValue(c.getAmountOfKeyword("Flanking") * 15, "flanking");
value += addValue(c.getAmountOfKeyword("Exalted") * 15, "exalted");
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido");
value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking");
value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted");
value += addValue(c.getKeywordMagnitude(Keyword.ANNIHILATOR) * 50, "eldrazi");
value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb");
// Keywords that may produce temporary or permanent buffs over time
if (c.hasKeyword("Prowess")) {
if (c.hasKeyword(Keyword.PROWESS)) {
value += addValue(5, "prowess");
}
if (c.hasKeyword("Outlast")) {
if (c.hasKeyword(Keyword.OUTLAST)) {
value += addValue(10, "outlast");
}
// Defensive Keywords
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) {
value += addValue(5, "reach");
}
if (c.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
@@ -131,7 +132,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
}
// Protection
if (c.hasKeyword("Indestructible")) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
value += addValue(70, "darksteel");
}
if (c.hasKeyword("Prevent all damage that would be dealt to CARDNAME.")) {
@@ -139,20 +140,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else if (c.hasKeyword("Prevent all combat damage that would be dealt to CARDNAME.")) {
value += addValue(50, "fogbank");
}
if (c.hasKeyword("Hexproof")) {
if (c.hasKeyword(Keyword.HEXPROOF)) {
value += addValue(35, "hexproof");
} else if (c.hasKeyword("Shroud")) {
} else if (c.hasKeyword(Keyword.SHROUD)) {
value += addValue(30, "shroud");
}
if (c.hasStartOfKeyword("Protection")) {
if (c.hasKeyword(Keyword.PROTECTION)) {
value += addValue(20, "protection");
}
if (c.hasStartOfKeyword("PreventAllDamageBy")) {
value += addValue(10, "prevent-dmg");
}
// Bad keywords
if (c.hasKeyword("Defender") || c.hasKeyword("CARDNAME can't attack.")) {
if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) {
value -= subValue((power * 9) + 40, "defender");
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
value -= subValue(40, "sac-end");
@@ -188,17 +186,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value -= subValue(30, "cupkeep");
} else if (c.hasStartOfKeyword("UpkeepCost")) {
value -= subValue(20, "sac-unless");
} else if (c.hasStartOfKeyword("Echo") && c.cameUnderControlSinceLastUpkeep()) {
} else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
value -= subValue(10, "echo-unpaid");
}
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
value -= subValue(20, "upkeep-dmg");
}
if (c.hasStartOfKeyword("Fading")) {
if (c.hasKeyword(Keyword.FADING)) {
value -= subValue(20, "fading");
}
if (c.hasStartOfKeyword("Vanishing")) {
if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20, "vanishing");
}
if (c.getSVar("Targeting").equals("Dies")) {

View File

@@ -6,6 +6,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
@@ -19,8 +20,10 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.event.GameEventAttackersDeclared;
import forge.game.event.GameEventCombatChanged;
import forge.game.mana.ManaPool;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
@@ -52,6 +55,14 @@ public abstract class GameState {
private int computerLife = -1;
private String humanCounters = "";
private String computerCounters = "";
private String humanManaPool = "";
private String computerManaPool = "";
private String humanPersistentMana = "";
private String computerPersistentMana = "";
private int humanLandsPlayed = 0;
private int computerLandsPlayed = 0;
private int humanLandsPlayedLastTurn = 0;
private int computerLandsPlayedLastTurn = 0;
private boolean puzzleCreatorState = false;
@@ -60,8 +71,10 @@ public abstract class GameState {
private final Map<Integer, Card> idToCard = new HashMap<>();
private final Map<Card, Integer> cardToAttachId = new HashMap<>();
private final Map<Card, Integer> cardToEnchantPlayerId = new HashMap<>();
private final Map<Card, Integer> markedDamage = new HashMap<>();
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>();
private final Map<Card, String> cardToChosenType = new HashMap<>();
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
@@ -79,11 +92,15 @@ public abstract class GameState {
private String tChangePlayer = "NONE";
private String tChangePhase = "NONE";
private String tAdvancePhase = "NONE";
private String precastHuman = null;
private String precastAI = null;
private int turn = 1;
private boolean removeSummoningSickness = false;
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
private final int TARGET_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
private final int TARGET_HUMAN = -2;
@@ -112,6 +129,10 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("humanlife=", String.valueOf(humanLife), "\n"));
sb.append(TextUtil.concatNoSpace("ailife=", String.valueOf(computerLife), "\n"));
sb.append(TextUtil.concatNoSpace("humanlandsplayed=", String.valueOf(humanLandsPlayed), "\n"));
sb.append(TextUtil.concatNoSpace("ailandsplayed=", String.valueOf(computerLandsPlayed), "\n"));
sb.append(TextUtil.concatNoSpace("humanlandsplayedlastturn=", String.valueOf(humanLandsPlayedLastTurn), "\n"));
sb.append(TextUtil.concatNoSpace("ailandsplayedlastturn=", String.valueOf(computerLandsPlayedLastTurn), "\n"));
sb.append(TextUtil.concatNoSpace("turn=", String.valueOf(turn), "\n"));
if (!humanCounters.isEmpty()) {
@@ -121,6 +142,13 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("aicounters=", computerCounters, "\n"));
}
if (!humanManaPool.isEmpty()) {
sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n"));
}
if (!computerManaPool.isEmpty()) {
sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n"));
}
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
sb.append(TextUtil.concatNoSpace("activephase=", tChangePhase, "\n"));
appendCards(humanCardTexts, "human", sb);
@@ -147,8 +175,14 @@ public abstract class GameState {
}
humanLife = human.getLife();
computerLife = ai.getLife();
humanLandsPlayed = human.getLandsPlayedThisTurn();
computerLandsPlayed = ai.getLandsPlayedThisTurn();
humanLandsPlayedLastTurn = human.getLandsPlayedLastTurn();
computerLandsPlayedLastTurn = ai.getLandsPlayedLastTurn();
humanCounters = countersToString(human.getCounters());
computerCounters = countersToString(ai.getCounters());
humanManaPool = processManaPool(human.getManaPool());
computerManaPool = processManaPool(ai.getManaPool());
tChangePlayer = game.getPhaseHandler().getPlayerTurn() == ai ? "ai" : "human";
tChangePhase = game.getPhaseHandler().getPhase().toString();
@@ -165,9 +199,7 @@ public abstract class GameState {
cardsReferencedByID.add(card.getExiledWith());
}
if (zone == ZoneType.Battlefield) {
if (!card.getEnchantedBy(false).isEmpty()
|| !card.getEquippedBy(false).isEmpty()
|| !card.getFortifiedBy(false).isEmpty()) {
if (!card.getAttachedCards().isEmpty()) {
// Remember the ID of cards that have attachments
cardsReferencedByID.add(card);
}
@@ -182,6 +214,10 @@ public abstract class GameState {
// Remember the IDs of imprinted cards
cardsReferencedByID.add(i);
}
for (Card i : card.getChosenCards()) {
// Remember the IDs of chosen cards
cardsReferencedByID.add(i);
}
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
// Remember the IDs of attacked planeswalkers
GameEntity def = game.getCombat().getDefenderByAttacker(card);
@@ -241,8 +277,7 @@ public abstract class GameState {
newText.append("|Renowned");
}
if (c.isMonstrous()) {
newText.append("|Monstrous:");
newText.append(c.getMonstrosityNum());
newText.append("|Monstrous");
}
if (c.isPhasedOut()) {
newText.append("|PhasedOut");
@@ -260,12 +295,14 @@ public abstract class GameState {
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
}
if (c.getEquipping() != null) {
newText.append("|Attaching:").append(c.getEquipping().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.isAttachedToEntity()) {
newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
}
if (c.getPlayerAttachedTo() != null) {
// TODO: improve this for game states with more than two players
newText.append("|EnchantingPlayer:");
Player p = c.getPlayerAttachedTo();
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
}
if (c.getDamage() > 0) {
@@ -282,6 +319,17 @@ public abstract class GameState {
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();
for (Object obj : c.getRemembered()) {
if (obj instanceof Card) {
@@ -389,8 +437,10 @@ public abstract class GameState {
if (categoryName.startsWith("active")) {
if (categoryName.endsWith("player"))
tChangePlayer = categoryValue.trim().toLowerCase();
if (categoryName.endsWith("phase"))
else if (categoryName.endsWith("phase"))
tChangePhase = categoryValue.trim().toUpperCase();
else if (categoryName.endsWith("phaseadvance"))
tAdvancePhase = categoryValue.trim().toUpperCase();
return;
}
@@ -400,6 +450,10 @@ public abstract class GameState {
turn = Integer.parseInt(categoryValue);
}
else if (categoryName.equals("removesummoningsickness")) {
removeSummoningSickness = categoryValue.equalsIgnoreCase("true");
}
else if (categoryName.endsWith("life")) {
if (isHuman)
humanLife = Integer.parseInt(categoryValue);
@@ -414,6 +468,20 @@ public abstract class GameState {
computerCounters = categoryValue;
}
else if (categoryName.endsWith("landsplayed")) {
if (isHuman)
humanLandsPlayed = Integer.parseInt(categoryValue);
else
computerLandsPlayed = Integer.parseInt(categoryValue);
}
else if (categoryName.endsWith("landsplayedlastturn")) {
if (isHuman)
humanLandsPlayedLastTurn = Integer.parseInt(categoryValue);
else
computerLandsPlayedLastTurn = Integer.parseInt(categoryValue);
}
else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) {
if (isHuman)
humanCardTexts.put(ZoneType.Battlefield, categoryValue);
@@ -466,6 +534,21 @@ public abstract class GameState {
else
precastAI = categoryValue;
}
else if (categoryName.endsWith("manapool")) {
if (isHuman)
humanManaPool = categoryValue;
else
computerManaPool = categoryValue;
}
else if (categoryName.endsWith("persistentmana")) {
if (isHuman)
humanPersistentMana = categoryValue;
else
computerPersistentMana = categoryValue;
}
else {
System.out.println("Unknown key: " + categoryName);
}
@@ -486,20 +569,28 @@ public abstract class GameState {
idToCard.clear();
cardToAttachId.clear();
cardToEnchantPlayerId.clear();
cardToRememberedId.clear();
cardToExiledWithId.clear();
markedDamage.clear();
cardToChosenClrs.clear();
cardToChosenCards.clear();
cardToChosenType.clear();
cardToScript.clear();
cardAttackMap.clear();
Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
Player newPlayerTurn = tChangePlayer.equalsIgnoreCase("human") ? human : tChangePlayer.equalsIgnoreCase("ai") ? ai : null;
PhaseType newPhase = tChangePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tChangePhase);
PhaseType advPhase = tAdvancePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tAdvancePhase);
// Set stack to resolving so things won't trigger/effects be checked right away
game.getStack().setResolving(true);
updateManaPool(human, humanManaPool, true, false);
updateManaPool(ai, computerManaPool, true, false);
updateManaPool(human, humanPersistentMana, false, true);
updateManaPool(ai, computerPersistentMana, false, true);
if (!humanCounters.isEmpty()) {
applyCountersToGameEntity(human, humanCounters);
}
@@ -511,8 +602,8 @@ public abstract class GameState {
game.getTriggerHandler().setSuppressAllTriggers(true);
setupPlayerState(humanLife, humanCardTexts, human);
setupPlayerState(computerLife, aiCardTexts, ai);
setupPlayerState(humanLife, humanCardTexts, human, humanLandsPlayed, humanLandsPlayedLastTurn);
setupPlayerState(computerLife, aiCardTexts, ai, computerLandsPlayed, computerLandsPlayedLastTurn);
handleCardAttachments();
handleChosenEntities();
@@ -532,9 +623,56 @@ public abstract class GameState {
game.getStack().setResolving(false);
// Advance to a certain phase, activating all triggered abilities
if (advPhase != null) {
game.getPhaseHandler().devAdvanceToPhase(advPhase);
}
if (removeSummoningSickness) {
for (Card card : game.getCardsInGame()) {
card.setSickness(false);
}
}
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
}
private String processManaPool(ManaPool manaPool) {
String mana = "";
for (final byte c : MagicColor.WUBRGC) {
int amount = manaPool.getAmountOfColor(c);
for (int i = 0; i < amount; i++) {
mana += MagicColor.toShortString(c) + " ";
}
}
return mana.trim();
}
private void updateManaPool(Player p, String manaDef, boolean clearPool, boolean persistent) {
Game game = p.getGame();
if (clearPool) {
p.getManaPool().clearPool(false);
}
if (!manaDef.isEmpty()) {
final Card dummy = new Card(-777777, game);
dummy.setOwner(p);
final Map<String, String> produced = Maps.newHashMap();
produced.put("Produced", manaDef);
if (persistent) {
produced.put("PersistentMana", "True");
}
final AbilityManaPart abMana = new AbilityManaPart(dummy, produced);
game.getAction().invoke(new Runnable() {
@Override
public void run() {
abMana.produceMana(null);
}
});
}
}
private void handleCombat(final Game game, final Player attackingPlayer, final Player defendingPlayer, final boolean toDeclareBlockers) {
// First we need to ensure that all attackers are declared in the Declare Attackers step,
// even if proceeding straight to Declare Blockers
@@ -838,33 +976,39 @@ public abstract class GameState {
Card c = entry.getKey();
c.setNamedCard(entry.getValue());
}
// Chosen cards
for (Entry<Card, CardCollection> entry : cardToChosenCards.entrySet()) {
Card c = entry.getKey();
c.setChosenCards(entry.getValue());
}
}
private void handleCardAttachments() {
// Unattach all permanents first
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
attachedTo.unEnchantAllCards();
attachedTo.unEquipAllCards();
for (Card c : attachedTo.getFortifiedBy(true)) {
attachedTo.unFortifyCard(c);
}
attachedTo.unAttachAllCards();
}
// Attach permanents by ID
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
Card attacher = entry.getKey();
if (attacher.isEquipment()) {
attacher.equipCard(attachedTo);
} else if (attacher.isAura()) {
attacher.enchantEntity(attachedTo);
} else if (attacher.isFortified()) {
attacher.fortifyCard(attachedTo);
if (attacher.isAttachment()) {
attacher.attachToEntity(attachedTo);
}
}
// Enchant players by ID
for(Entry<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
// TODO: improve this for game states with more than two players
Card attacher = entry.getKey();
Game game = attacher.getGame();
Player attachedTo = entry.getValue() == TARGET_AI ? game.getPlayers().get(1) : game.getPlayers().get(0);
attacher.attachToEntity(attachedTo);
}
}
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
@@ -876,9 +1020,15 @@ public abstract class GameState {
}
}
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
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
// 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);
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
String value = kv.getValue();
@@ -886,6 +1036,9 @@ public abstract class GameState {
}
if (life >= 0) p.setLife(life, null);
p.setLandsPlayedThisTurn(landsPlayed);
p.setLandsPlayedLastTurn(landsPlayedLastTurn);
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) {
@@ -909,7 +1062,9 @@ public abstract class GameState {
if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter)
c.setEnchanting(c);
//FIXME it shouldn't be able to attach itself
c.setEntityAttachedTo(c);
}
if (cardsWithoutETBTrigs.contains(c)) {
@@ -927,7 +1082,6 @@ public abstract class GameState {
zone.setCards(kv.getValue());
}
}
}
/**
@@ -979,9 +1133,8 @@ public abstract class GameState {
c.tap();
} else if (info.startsWith("Renowned")) {
c.setRenowned(true);
} else if (info.startsWith("Monstrous:")) {
} else if (info.startsWith("Monstrous")) {
c.setMonstrous(true);
c.setMonstrosityNum(Integer.parseInt(info.substring((info.indexOf(':') + 1))));
} else if (info.startsWith("PhasedOut")) {
c.setPhasedOut(true);
} else if (info.startsWith("Counters:")) {
@@ -1007,9 +1160,13 @@ public abstract class GameState {
} else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3));
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));
cardToAttachId.put(c, id);
} else if (info.startsWith("EnchantingPlayer:")) {
// TODO: improve this for game states with more than two players
String tgt = info.substring(info.indexOf(':') + 1);
cardToEnchantPlayerId.put(c, tgt.equalsIgnoreCase("AI") ? TARGET_AI : TARGET_HUMAN);
} else if (info.startsWith("Ability:")) {
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
@@ -1020,6 +1177,13 @@ public abstract class GameState {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) {
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("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:")) {
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ExecuteScript:")) {

View File

@@ -26,6 +26,7 @@ import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.cost.*;
import forge.game.mana.Mana;
import forge.game.mana.ManaConversionMatrix;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -161,19 +162,46 @@ public class PlayerControllerAi extends PlayerController {
@Override
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) {
// this isn't used
return null;
if (delayedReveal != 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 SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title) {
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
public SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells);
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params);
}
@Override
@@ -287,21 +315,54 @@ public class PlayerControllerAi extends PlayerController {
}
// put the rest on top in random order
Collections.shuffle(toTop);
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toBottom);
}
/* (non-Javadoc)
* @see forge.game.player.PlayerController#arrangeForSurveil(forge.game.card.CardCollection)
*/
@Override
public ImmutablePair<CardCollection, CardCollection> arrangeForSurveil(CardCollection topN) {
CardCollection toGraveyard = new CardCollection();
CardCollection toTop = new CardCollection();
// 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.).
if (getPlayer().getCardsIn(ZoneType.Library).size() <= getAi().getIntProperty(AiProps.SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL)) {
toTop.addAll(topN);
} else {
for (Card c : topN) {
if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c)) {
toGraveyard.add(c);
} else {
toTop.add(c);
}
}
}
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toGraveyard);
}
@Override
public boolean willPutCardOnTop(Card c) {
return true; // AI does not know what will happen next (another clash or that would become his topdeck)
// This is used for Clash. Currently uses Scry logic to determine whether the card should be put on top.
// Note that the AI does not know what will happen next (another clash or that would become his topdeck)
return !ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c);
}
@Override
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
//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) {
// 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>() {
@Override
public boolean apply(Card card) {
@@ -331,6 +392,61 @@ public class PlayerControllerAi extends PlayerController {
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
@@ -891,7 +1007,7 @@ public class PlayerControllerAi extends PlayerController {
}
@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
ManaCostBeingPaid cost = isActivatedSa ? ComputerUtilMana.calculateManaCost(sa, false, 0) : new ManaCostBeingPaid(toPay);
return ComputerUtilMana.payManaCost(cost, sa, player);
@@ -1000,7 +1116,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
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) {
// this isn't used
return null;
@@ -1057,9 +1173,27 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility choosen,
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen,
List<OptionalCostValue> optionalCostValues) {
// TODO Auto-generated method stub
return null;
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
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);
if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
chosenOptCosts.add(opt);
costSoFar.add(opt.getCost());
}
}
return chosenOptCosts;
}
@Override
public boolean confirmMulliganScry(Player p) {
// Always true?
return true;
}
}

View File

@@ -33,6 +33,7 @@ import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPart;
import forge.game.keyword.Keyword;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -206,7 +207,9 @@ public class SpecialCardAi {
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"))));
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(
CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH))));
boolean hasUsefulBlocker = false;
for (Card c : flyingCreatures) {
@@ -329,7 +332,7 @@ public class SpecialCardAi {
boolean oppHasFirstStrike = false;
boolean oppCantDie = true;
boolean unblocked = opposition.isEmpty();
boolean canTrample = source.hasKeyword("Trample");
boolean canTrample = source.hasKeyword(Keyword.TRAMPLE);
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = ((Card)combat.getDefenderByAttacker(source)).getCounters(CounterType.LOYALTY);
@@ -351,7 +354,7 @@ public class SpecialCardAi {
}
for (Card c : opposition) {
if (c.hasKeyword("First Strike") || c.hasKeyword("Double Strike")) {
if (c.hasKeyword(Keyword.FIRST_STRIKE) || c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
oppHasFirstStrike = true;
}
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) {
@@ -382,8 +385,8 @@ public class SpecialCardAi {
// Already enough to kill the blockers and survive, don't overpump
return false;
}
if (oppCantDie && !source.hasKeyword("Trample") && !source.hasKeyword("Wither")
&& !source.hasKeyword("Infect") && predictedPT.getLeft() <= oppT) {
if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.hasKeyword(Keyword.WITHER)
&& !source.hasKeyword(Keyword.INFECT) && predictedPT.getLeft() <= oppT) {
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
return false;
}
@@ -400,7 +403,7 @@ public class SpecialCardAi {
CardCollection potentialBlockers = new CardCollection();
for (Card b : oppInPlay) {
if (CombatUtil.canBlock(sa.getHostCard(), b)) {
if (CombatUtil.canBlock(source, b)) {
potentialBlockers.add(b);
}
}
@@ -408,7 +411,7 @@ public class SpecialCardAi {
Pair<Integer, Integer> predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness());
int oppT = Aggregates.sum(potentialBlockers, CardPredicates.Accessors.fnGetNetToughness);
if (potentialBlockers.isEmpty() || (sa.getHostCard().hasKeyword("Trample") && predictedPT.getLeft() - oppT >= oppLife)) {
if (potentialBlockers.isEmpty() || (source.hasKeyword(Keyword.TRAMPLE) && predictedPT.getLeft() - oppT >= oppLife)) {
return true;
}
@@ -674,6 +677,14 @@ public class SpecialCardAi {
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
public static class LivingDeath {
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 threshold = 320; // approximately a 4/4 Flying creature worth of extra value
@@ -818,6 +829,10 @@ public class SpecialCardAi {
int computerHandSize = ai.getZone(ZoneType.Hand).size();
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()) {
// Prefer Yawgmoth's Bargain because AI is generally better with it
@@ -1334,6 +1349,10 @@ public class SpecialCardAi {
Game game = ai.getGame();
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 maxHandSize = ai.getMaxHandSize();

View File

@@ -76,6 +76,13 @@ public abstract class SpellAbilityAi {
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)) {
return false;
}
@@ -169,7 +176,7 @@ public abstract class SpellAbilityAi {
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;
}
final AbilitySub subAb = sa.getSubAbility();
@@ -238,8 +245,8 @@ public abstract class SpellAbilityAi {
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa) {
return (sa.isSpell() && sa.getHostCard().isSorcery())
|| (sa.isAbility() && sa.getRestrictions().isSorcerySpeed())
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.getRestrictions().isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
}
@@ -324,7 +331,7 @@ public abstract class SpellAbilityAi {
return null;
}
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleSpellAbility is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return spells.get(0);
}

View File

@@ -25,6 +25,7 @@ public enum SpellApiToAi {
.put(ApiType.AnimateAll, AnimateAllAi.class)
.put(ApiType.Attach, AttachAi.class)
.put(ApiType.Ascend, AlwaysPlayAi.class)
.put(ApiType.AssignGroup, AssignGroupAi.class)
.put(ApiType.Balance, BalanceAi.class)
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
.put(ApiType.BecomesBlocked, BecomesBlockedAi.class)
@@ -78,12 +79,14 @@ public enum SpellApiToAi {
.put(ApiType.FlipACoin, FlipACoinAi.class)
.put(ApiType.Fog, FogAi.class)
.put(ApiType.GainControl, ControlGainAi.class)
.put(ApiType.GainControlVariant, AlwaysPlayAi.class)
.put(ApiType.GainLife, LifeGainAi.class)
.put(ApiType.GainOwnership, CannotPlayAi.class)
.put(ApiType.GameDrawn, CannotPlayAi.class)
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
.put(ApiType.Goad, GoadAi.class)
.put(ApiType.Haunt, HauntAi.class)
.put(ApiType.ImmediateTrigger, AlwaysPlayAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class)
.put(ApiType.LosesGame, GameLossAi.class)
.put(ApiType.Mana, ManaEffectAi.class)
@@ -144,6 +147,7 @@ public enum SpellApiToAi {
.put(ApiType.SkipTurn, SkipTurnAi.class)
.put(ApiType.StoreMap, StoreMapAi.class)
.put(ApiType.StoreSVar, StoreSVarAi.class)
.put(ApiType.Surveil, SurveilAi.class)
.put(ApiType.Tap, TapAi.class)
.put(ApiType.TapAll, TapAllAi.class)
.put(ApiType.TapOrUntap, TapOrUntapAi.class)

View File

@@ -12,6 +12,7 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@@ -93,7 +94,8 @@ public class ActivateAbilityAi extends SpellAbilityAi {
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
return spells.get(0);
}
}

View File

@@ -23,12 +23,12 @@ import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import forge.game.ability.effects.AnimateEffectBase;
/**
* <p>
@@ -149,15 +149,15 @@ public class AnimateAi extends SpellAbilityAi {
if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
if (!source.hasKeyword(keyword)) {
if (!c.hasKeyword(keyword)) {
bFlag = true;
}
}
@@ -188,7 +188,7 @@ public class AnimateAi extends SpellAbilityAi {
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
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;
}
}
@@ -342,7 +342,7 @@ public class AnimateAi extends SpellAbilityAi {
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that animate a target. Those can just use SVar:RemAIDeck:True until
// that animate a target. Those can just use AI:RemoveDeck:All until
// this can do a reasonably
// good job of picking a good target
return false;
@@ -363,11 +363,11 @@ public class AnimateAi extends SpellAbilityAi {
card.setSickness(hasOriginalCardSickness);
// AF specific sa
int power = -1;
Integer power = null;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
}
int toughness = -1;
Integer toughness = null;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
}
@@ -453,59 +453,7 @@ public class AnimateAi extends SpellAbilityAi {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
}
// duplicating AnimateEffectBase.doAnimate
boolean removeSuperTypes = false;
boolean removeCardTypes = false;
boolean removeSubTypes = false;
boolean removeCreatureTypes = false;
if (sa.hasParam("OverwriteTypes")) {
removeSuperTypes = true;
removeCardTypes = true;
removeSubTypes = true;
removeCreatureTypes = true;
}
if (sa.hasParam("KeepSupertypes")) {
removeSuperTypes = false;
}
if (sa.hasParam("KeepCardTypes")) {
removeCardTypes = false;
}
if (sa.hasParam("RemoveSuperTypes")) {
removeSuperTypes = true;
}
if (sa.hasParam("RemoveCardTypes")) {
removeCardTypes = true;
}
if (sa.hasParam("RemoveSubTypes")) {
removeSubTypes = true;
}
if (sa.hasParam("RemoveCreatureTypes")) {
removeCreatureTypes = true;
}
if ((power != -1) || (toughness != -1)) {
card.addNewPT(power, toughness, timestamp);
}
if (!types.isEmpty() || !removeTypes.isEmpty() || removeCreatureTypes) {
card.addChangedCardTypes(types, removeTypes, removeSuperTypes, removeCardTypes, removeSubTypes,
removeCreatureTypes, timestamp);
}
card.addChangedCardKeywords(keywords, removeKeywords, sa.hasParam("RemoveAllAbilities"), timestamp);
for (final String k : hiddenKeywords) {
card.addHiddenExtrinsicKeyword(k);
}
card.addColor(finalDesc, !sa.hasParam("OverwriteColors"), timestamp);
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, keywords, removeKeywords, hiddenKeywords, timestamp);
// back to duplicating AnimateEffect.resolve
// TODO will all these abilities/triggers/replacements/etc. lead to
@@ -515,10 +463,14 @@ public class AnimateAi extends SpellAbilityAi {
boolean clearAbilities = sa.hasParam("OverwriteAbilities");
boolean clearSpells = sa.hasParam("OverwriteSpells");
boolean removeAll = sa.hasParam("RemoveAllAbilities");
boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
if (clearAbilities || clearSpells || removeAll) {
for (final SpellAbility ab : card.getSpellAbilities()) {
if (removeAll || (ab.isAbility() && clearAbilities) || (ab.isSpell() && clearSpells)) {
if (removeAll
|| (ab.isIntrinsic() && removeIntrinsic && !ab.isBasicLandAbility())
|| (ab.isAbility() && clearAbilities)
|| (ab.isSpell() && clearSpells)) {
card.removeSpellAbility(ab);
removedAbilities.add(ab);
}
@@ -559,9 +511,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress triggers from the animated card
final List<Trigger> removedTriggers = Lists.newArrayList();
if (sa.hasParam("OverwriteTriggers") || removeAll) {
final FCollectionView<Trigger> triggersToRemove = card.getTriggers();
for (final Trigger trigger : triggersToRemove) {
if (sa.hasParam("OverwriteTriggers") || removeAll || removeIntrinsic) {
for (final Trigger trigger : card.getTriggers()) {
if (removeIntrinsic && !trigger.isIntrinsic()) {
continue;
}
trigger.setSuppressed(true);
removedTriggers.add(trigger);
}
@@ -597,9 +551,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress static abilities from the animated card
final List<StaticAbility> removedStatics = Lists.newArrayList();
if (sa.hasParam("OverwriteStatics") || removeAll) {
final FCollectionView<StaticAbility> staticsToRemove = card.getStaticAbilities();
for (final StaticAbility stAb : staticsToRemove) {
if (sa.hasParam("OverwriteStatics") || removeAll || removeIntrinsic) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (removeIntrinsic && !stAb.isIntrinsic()) {
continue;
}
stAb.setTemporarilySuppressed(true);
removedStatics.add(stAb);
}
@@ -607,8 +563,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress static abilities from the animated card
final List<ReplacementEffect> removedReplacements = Lists.newArrayList();
if (sa.hasParam("OverwriteReplacements") || removeAll) {
if (sa.hasParam("OverwriteReplacements") || removeAll || removeIntrinsic) {
for (final ReplacementEffect re : card.getReplacementEffects()) {
if (removeIntrinsic && !re.isIntrinsic()) {
continue;
}
re.setTemporarilySuppressed(true);
removedReplacements.add(re);
}

View File

@@ -0,0 +1,33 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
public class AssignGroupAi extends SpellAbilityAi {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
// otherwise the AI considers the card playable.
return true;
}
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
final String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("FriendOrFoe")) {
if (params.containsKey("Affected") && spells.size() >= 2) {
Player t = (Player) params.get("Affected");
return spells.get(player.isOpponentOf(t) ? 1 : 0);
}
}
return Iterables.getFirst(spells, null);
}
}

View File

@@ -2,8 +2,12 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.*;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -13,16 +17,19 @@ import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.ArrayList;
import java.util.Iterator;
@@ -39,6 +46,12 @@ public class AttachAi extends SpellAbilityAi {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
// TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures
// and gaining card advantage
if (source.hasKeyword("MayFlashSac") && !ai.couldCastSorcery(sa)) {
return false;
}
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
@@ -49,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)
&& !"Curse".equals(sa.getParam("AILogic"))) {
return false;
@@ -68,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")) {
// Set PayX here to maximum value. (Endless Scream and Venarian
// Gold)
@@ -88,7 +120,7 @@ public class AttachAi extends SpellAbilityAi {
final CardCollection targets = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof"));
return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
}
});
if (targets.isEmpty()) {
@@ -99,6 +131,122 @@ public class AttachAi extends SpellAbilityAi {
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.
*
@@ -249,7 +397,7 @@ public class AttachAi extends SpellAbilityAi {
@Override
public boolean apply(final Card c) {
// Don't do Untapped Vigilance cards
if (c.isCreature() && c.hasKeyword("Vigilance") && c.isUntapped()) {
if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
return false;
}
@@ -269,7 +417,7 @@ public class AttachAi extends SpellAbilityAi {
return true;
}
final Iterable<Card> auras = c.getEnchantedBy(false);
final Iterable<Card> auras = c.getEnchantedBy();
final Iterator<Card> itr = auras.iterator();
while (itr.hasNext()) {
final Card aura = itr.next();
@@ -391,7 +539,7 @@ public class AttachAi extends SpellAbilityAi {
List<Card> evenBetterList = CardLists.filter(betterList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.hasKeyword("Indestructible") || c.hasKeyword("Hexproof");
return c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF);
}
});
if (!evenBetterList.isEmpty()) {
@@ -458,8 +606,40 @@ public class AttachAi extends SpellAbilityAi {
private static Card attachAIReanimatePreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
final Card attachSource) {
// AI For choosing a Card to Animate.
// TODO Add some more restrictions for Reanimation Auras
final Card c = ComputerUtilCard.getBestCreatureAI(list);
final Player ai = sa.getActivatingPlayer();
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
// choose something
@@ -478,23 +658,23 @@ public class AttachAi extends SpellAbilityAi {
for (Card card : list) {
int cardPriority = 0;
// Prefer Evasion
if (card.hasKeyword("Trample")) {
if (card.hasKeyword(Keyword.TRAMPLE)) {
cardPriority += 10;
}
if (card.hasKeyword("Menace")) {
if (card.hasKeyword(Keyword.MENACE)) {
cardPriority += 10;
}
// Avoid this for Sleepers Robe?
if (card.hasKeyword("Fear")) {
if (card.hasKeyword(Keyword.FEAR)) {
cardPriority += 15;
}
if (card.hasKeyword("Flying")) {
if (card.hasKeyword(Keyword.FLYING)) {
cardPriority += 20;
}
if (card.hasKeyword("Shadow")) {
if (card.hasKeyword(Keyword.SHADOW)) {
cardPriority += 30;
}
if (card.hasKeyword("Horsemanship")) {
if (card.hasKeyword(Keyword.HORSEMANSHIP)) {
cardPriority += 40;
}
if (card.hasKeyword("Unblockable")) {
@@ -503,7 +683,7 @@ public class AttachAi extends SpellAbilityAi {
// Prefer "tap to deal damage"
// TODO : Skip this one if triggers on combat damage only?
for (SpellAbility sa2 : card.getSpellAbilities()) {
if ((sa2.getApi().equals(ApiType.DealDamage))
if (ApiType.DealDamage.equals(sa2.getApi())
&& (sa2.getTargetRestrictions().canTgtPlayer())) {
cardPriority += 300;
}
@@ -514,10 +694,10 @@ public class AttachAi extends SpellAbilityAi {
if (card.getCurrentPower() <= 0) {
cardPriority = -100;
}
if (card.hasKeyword("Defender")) {
if (card.hasKeyword(Keyword.DEFENDER)) {
cardPriority = -100;
}
if (card.hasKeyword("Indestructible")) {
if (card.hasKeyword(Keyword.INDESTRUCTIBLE)) {
cardPriority += 15;
}
if (cardPriority > priority) {
@@ -719,7 +899,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (!c.hasKeyword("Indestructible") && (c.getLethalDamage() <= Math.abs(tgh))) {
if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && (c.getLethalDamage() <= Math.abs(tgh))) {
return true;
}
@@ -898,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) {
// Look for Heroic triggers
@@ -964,8 +1172,8 @@ public class AttachAi extends SpellAbilityAi {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility");
@@ -985,6 +1193,10 @@ public class AttachAi extends SpellAbilityAi {
}
CardCollection prefList = new CardCollection(list);
// Filter AI-specific targets if provided
prefList = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
if (totToughness < 0) {
// Don't kill my own stuff with Negative toughness Auras
final int tgh = totToughness;
@@ -1006,7 +1218,7 @@ public class AttachAi extends SpellAbilityAi {
if (isUsefulAttachKeyword(keyword, c, sa, pow)) {
return true;
}
if (c.hasKeyword("Infect") && pow >= 2) {
if (c.hasKeyword(Keyword.INFECT) && pow >= 2) {
// consider +2 power a significant bonus on Infect creatures
return true;
}
@@ -1034,7 +1246,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !c.isEnchanted() || c.hasKeyword("Hexproof");
return !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF);
}
});
}
@@ -1104,8 +1316,15 @@ public class AttachAi extends SpellAbilityAi {
if (attachSource.hasSVar("DontEquip")) {
return null;
}
// is no attachment so no using attach
if (!attachSource.isAttachment()) {
return null;
}
// 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;
}
@@ -1115,11 +1334,7 @@ public class AttachAi extends SpellAbilityAi {
} else {
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
if (attachSource.isAura()) {
list = CardLists.filter(list, CardPredicates.canBeEnchantedBy(attachSource));
} else if (attachSource.isEquipment()) {
list = CardLists.filter(list, CardPredicates.canBeEquippedBy(attachSource));
}
list = CardLists.filter(list, CardPredicates.canBeAttached(attachSource));
// TODO If Attaching without casting, don't need to actually target.
// I believe this is the only case where mandatory will be true, so just
@@ -1311,6 +1526,22 @@ public class AttachAi extends SpellAbilityAi {
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
return false;
}
// Don't play if would choose a color the target is already protected from
if (sa.getHostCard().hasSVar("ChosenProtection")) {
CardCollectionView oppAllCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
String cc = ComputerUtilCard.getMostProminentColor(oppAllCards);
if (card.hasKeyword("Protection from " + cc.toLowerCase())) {
return false;
}
// Also don't play if it would destroy own Aura
for (Card c : card.getEnchantedBy()) {
if ((c.getController().equals(ai)) && (c.isOfColor(cc))) {
return false;
}
}
}
final boolean evasive = (keyword.equals("Unblockable") || keyword.equals("Fear")
|| keyword.equals("Intimidate") || keyword.equals("Shadow")
|| keyword.equals("Flying") || keyword.equals("Horsemanship")
@@ -1353,7 +1584,7 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("First Strike")) {
if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword("Double Strike")
if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword(Keyword.DOUBLE_STRIKE)
|| (!ComputerUtilCombat.canAttackNextTurn(card) && !CombatUtil.canBlock(card, true))) {
return false;
}
@@ -1386,7 +1617,7 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("Reach")) {
if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) {
if (card.hasKeyword(Keyword.FLYING) || !CombatUtil.canBlock(card, true)) {
return false;
}
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
@@ -1395,11 +1626,11 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {
if (!card.hasKeyword("Defender") || card.getNetCombatDamage() + powerBonus <= 0) {
if (!card.hasKeyword(Keyword.DEFENDER) || card.getNetCombatDamage() + powerBonus <= 0) {
return false;
}
} else if (keyword.equals("Shroud") || keyword.equals("Hexproof")) {
if (card.hasKeyword("Shroud") || card.hasKeyword("Hexproof")) {
if (card.hasKeyword(Keyword.SHROUD) || card.hasKeyword(Keyword.HEXPROOF)) {
return false;
}
} else if (keyword.equals("Defender")) {

View File

@@ -7,6 +7,7 @@ import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -30,7 +31,7 @@ public class BecomesBlockedAi extends SpellAbilityAi {
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
list = CardLists.getTargetableCards(list, sa);
list = CardLists.getNotKeyword(list, "Trample");
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) {
Card choice = null;

View File

@@ -6,6 +6,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.List;
import java.util.Map;
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
@@ -37,7 +38,8 @@ public class CanPlayAsDrawbackAi extends SpellAbilityAi {
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
// This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
return spells.get(0);
}

View File

@@ -152,12 +152,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
return doReturnCommanderLogic(sa, aiPlayer);
}
if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
} else if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
return true; // debuffed by opponent's auras to the level that it becomes useless
}
int delta = 0;
for (Card enc : sa.getHostCard().getEnchantedBy(false)) {
for (Card enc : sa.getHostCard().getEnchantedBy()) {
if (enc.getController().isOpponentOf(aiPlayer)) {
delta--;
} else {
@@ -249,6 +251,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
return false;
}
if (ai.getGame().getCombat() == null) {
return false;
}
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
boolean lowerCMC = false;
for (Card attacker : attackers) {
@@ -955,7 +961,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
for (Card aura : c.getEnchantedBy(false)) {
for (Card aura : c.getEnchantedBy()) {
if (aura.getController().isOpponentOf(ai)) {
return true;
} else {
@@ -1048,7 +1054,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
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)) {
return false;
}
@@ -1457,7 +1463,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
fetchList = CardLists.filter(fetchList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (c.hasSVar("RemAIDeck") || c.hasSVar("RemRandomDeck")) {
if (ComputerUtilCard.isCardRemAIDeck(c) || ComputerUtilCard.isCardRemRandomDeck(c)) {
return false;
}
return true;
@@ -1728,6 +1734,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
SpellAbility causeSa = (SpellAbility)originalParams.get("Cause");
SpellAbility causeSub = null;
// Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
if (sa.getHostCard().getName().contains("Squee, the Immortal")) {
return false;
}
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
ApiType subApi = causeSub.getApi();

View File

@@ -114,7 +114,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// spBounceAll has some AI we can compare to.
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
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;
} else {
// search targetable Opponents
@@ -220,6 +220,9 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} else {
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)) {
String logic = sa.getParam("AILogic");
@@ -344,8 +347,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
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
// Profaner from exile without paying its mana cost. Otherwise the card is marked RemAIDeck and there is no
// specific AI to support playing it in a smarter way. Feel free to expand.
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
// there is no specific AI to support playing it in a smarter way. Feel free to expand.
return !CardLists.filter(ai.getOpponents().getCardsIn(origin), CardPredicates.Presets.CREATURES).isEmpty();
}

View File

@@ -4,6 +4,7 @@ import java.util.Collections;
import java.util.List;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -22,6 +23,7 @@ import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
@@ -126,8 +128,8 @@ public class ChooseCardAi extends SpellAbilityAi {
} else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
// Use it as a wrath, when the human creatures threat the ai's life
if (aiCreatures.isEmpty() && ComputerUtilCombat.sumDamageIfUnblocked(oppCreatures, ai) >= ai.getLife()) {
@@ -261,7 +263,7 @@ public class ChooseCardAi extends SpellAbilityAi {
}
} else if (logic.equals("Duneblast")) {
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
if (aiCreatures.isEmpty()) {
return null;
@@ -273,6 +275,22 @@ public class ChooseCardAi extends SpellAbilityAi {
if (ai.equals(sa.getActivatingPlayer())) {
choice = ComputerUtilCard.getBestAI(options);
} // 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 {
choice = ComputerUtilCard.getBestAI(options);
}

View File

@@ -1,8 +1,16 @@
package forge.ai.ability;
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.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
public class ChooseDirectionAi extends SpellAbilityAi {
@@ -12,10 +20,23 @@ public class ChooseDirectionAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final String logic = sa.getParam("AILogic");
final Game game = sa.getActivatingPlayer().getGame();
if (logic == null) {
return false;
} 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;
}

View File

@@ -1,10 +1,13 @@
package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
@@ -23,6 +26,8 @@ import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -37,7 +42,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) {
return true;
} else if (aiLogic.startsWith("Fabricate")) {
} else if (aiLogic.startsWith("Fabricate") || "Riot".equals(aiLogic)) {
return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
@@ -79,7 +84,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
Card host = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Game game = host.getGame();
@@ -342,7 +348,56 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
if (!filtered.isEmpty()) {
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
}
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;
}
}

View File

@@ -142,14 +142,17 @@ public class CloneAi extends SpellAbilityAi {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
} else if ("CloneBestCreature".equals(sa.getParam("AILogic"))) {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
}
// Default:
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that clone a target. Those can just use SVar:RemAIDeck:True until
// this can do a reasonably
// good job of picking a good target
// two are the only things that clone a target. Those can just use
// AI:RemoveDeck:All until this can do a reasonably good job of picking
// a good target
return false;
}
@@ -158,7 +161,18 @@ public class CloneAi extends SpellAbilityAi {
*/
@Override
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;
}

View File

@@ -36,7 +36,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !c.hasSVar("RemAIDeck") && c.canBeTargetedBy(sa);
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa);
}
});
object1 = ComputerUtilCard.getBestAI(list);

View File

@@ -171,7 +171,7 @@ public class ControlGainAi extends SpellAbilityAi {
}
// do not take control on something it doesn't know how to use
return !c.hasSVar("RemAIDeck");
return !ComputerUtilCard.isCardRemAIDeck(c);
}
});

View File

@@ -83,7 +83,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
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
if (list.isEmpty()) {
return false;

View File

@@ -1,42 +1,133 @@
package forge.ai.ability;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.ApiType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilityActivated;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
return sa.isMandatory() || "Always".equals(sa.getParam("AILogic"));
Game game = aiPlayer.getGame();
int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK);
int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF);
String logic = sa.getParamOrDefault("AILogic", "");
if (game.getStack().isEmpty()) {
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
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// 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
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"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
return super.chkAIDrawback(sa, aiPlayer);
return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer));
}
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
Map<String, Object> params) {
return spells.get(0);
}

View File

@@ -105,7 +105,7 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}

View File

@@ -27,6 +27,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CounterType;
import forge.game.keyword.Keyword;
import forge.util.Aggregates;
@@ -59,7 +60,7 @@ public abstract class CountersAi {
if (type.equals("M1M1")) {
// try to kill the best killable creature, or reduce the best one
// but try not to target a Undying Creature
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), "Undying");
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING);
if (killable.size() > 0) {
choice = ComputerUtilCard.getBestCreatureAI(killable);
} else {

View File

@@ -8,6 +8,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -191,7 +192,7 @@ public class CountersMoveAi extends SpellAbilityAi {
}
// 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) {
return false;
}
@@ -286,7 +287,7 @@ public class CountersMoveAi extends SpellAbilityAi {
// do not steal a P1P1 from Undying if it would die
// this way
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying") || card.isToken()) {
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken()) {
return true;
}
return false;
@@ -332,11 +333,12 @@ public class CountersMoveAi extends SpellAbilityAi {
// try to remove P1P1 from undying or evolve
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;
}
}
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return true;
}
@@ -391,10 +393,10 @@ public class CountersMoveAi extends SpellAbilityAi {
}
if (cType != null) {
if (CounterType.P1P1.equals(cType) && card.hasKeyword("Undying")) {
if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) {
return false;
}
if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return false;
}

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
@@ -10,11 +11,10 @@ import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -44,17 +44,19 @@ public class CountersPutAi extends SpellAbilityAi {
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
final String type = sa.getParam("CounterType");
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// TODO Auto-generated method stub
if (!super.willPayCosts(ai, sa, cost, source)) {
return false;
}
// disable moving counters
// disable moving counters (unless a specialized AI logic supports it)
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType counterType = remCounter.counter;
if (counterType.name().equals(type)) {
if (counterType.name().equals(type) && !aiLogic.startsWith("MoveCounter")) {
return false;
}
if (!part.payCostFromSource()) {
@@ -100,7 +102,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (sa.hasParam("LevelUp")) {
// creatures enchanted by curse auras have low priority
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
for (Card aura : source.getEnchantedBy(false)) {
for (Card aura : source.getEnchantedBy()) {
if (aura.getController().isOpponentOf(ai)) {
return false;
}
@@ -123,8 +125,10 @@ public class CountersPutAi extends SpellAbilityAi {
CardCollection list;
Card choice = null;
final String type = sa.getParam("CounterType");
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final String logic = sa.getParamOrDefault("AILogic", "");
PhaseHandler ph = ai.getGame().getPhaseHandler();
final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined"))
&& "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X"))
@@ -154,7 +158,7 @@ public class CountersPutAi extends SpellAbilityAi {
// receive counters, execpt it has undying
CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1));
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, "Undying");
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate<Card>() {
@Override
@@ -214,15 +218,11 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
if ("Never".equals(sa.getParam("AILogic"))) {
if ("Never".equals(logic)) {
return false;
}
if ("PayEnergy".equals(sa.getParam("AILogic"))) {
} else if ("PayEnergy".equals(logic)) {
return true;
}
if ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) {
} else if ("PayEnergyConservatively".equals(logic)) {
boolean onlyInCombat = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
boolean onlyDefensive = ai.getController().isAI()
@@ -262,6 +262,28 @@ public class CountersPutAi extends SpellAbilityAi {
return true;
}
}
} else if (logic.equals("MarkOppCreature")) {
if (!ph.is(PhaseType.END_OF_TURN)) {
return false;
}
CardCollection oppCreats = CardLists.filter(ai.getOpponents().getCreaturesInPlay(),
Predicates.and(Predicates.not(CardPredicates.hasCounter(CounterType.getType(type))),
CardPredicates.isTargetableBy(sa)));
if (!oppCreats.isEmpty()) {
Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
sa.resetTargets();
sa.getTargets().add(bestCreat);
return true;
}
} else if (logic.equals("CheckDFC")) {
// for cards like Ludevic's Test Subject
if (!source.canTransform()) {
return false;
}
} else if (logic.startsWith("MoveCounter")) {
return doMoveCounterLogic(ai, sa, ph);
}
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
@@ -292,7 +314,18 @@ public class CountersPutAi extends SpellAbilityAi {
// TODO handle proper calculation of X values based on Cost
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
if ("Fight".equals(sa.getParam("AILogic"))) {
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)) {
int nPump = 0;
if (type.equals("P1P1")) {
nPump = amount;
@@ -323,7 +356,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
source.setSVar("PayX", Integer.toString(amount));
} else if ("ExiledCreatureFromGraveCMC".equals(sa.getParam("AILogic"))) {
} else if ("ExiledCreatureFromGraveCMC".equals(logic)) {
// e.g. Necropolis
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc);
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
@@ -337,16 +370,10 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
if ("Polukranos".equals(sa.getParam("AILogic"))) {
if ("Polukranos".equals(logic)) {
CardCollection humCreatures = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
CardCollection targets = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
final CardCollection targets = CardLists.filter(humCreatures, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof"));
}
});
if (!targets.isEmpty()){
boolean canSurvive = false;
for (Card humanCreature : targets) {
@@ -360,9 +387,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
}
PhaseHandler ph = ai.getGame().getPhaseHandler();
if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) {
if ("AlwaysAtOppEOT".equals(logic)) {
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
return true;
}
@@ -421,6 +446,18 @@ public class CountersPutAi extends SpellAbilityAi {
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")) {
final TargetRestrictions abTgt = sa.getTargetRestrictions();
// specific AI for instant with distribute two +1/+1 counters
@@ -566,8 +603,9 @@ public class CountersPutAi extends SpellAbilityAi {
final Game game = ai.getGame();
Card choice = null;
final String type = sa.getParam("CounterType");
final String logic = sa.getParamOrDefault("AILogic", "");
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
@@ -611,7 +649,7 @@ public class CountersPutAi extends SpellAbilityAi {
SpellAbility animate = sa.findSubAbilityByType(ApiType.Animate);
if (!lands.isEmpty() && animate != null) {
choice = ComputerUtilCard.getWorstLand(lands);
} else if ("BoonCounterOnOppCreature".equals(sa.getParam("AILogic"))) {
} else if ("BoonCounterOnOppCreature".equals(logic)) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = CountersAi.chooseBoonTarget(list, type);
@@ -647,7 +685,7 @@ public class CountersPutAi extends SpellAbilityAi {
boolean preferred = true;
CardCollection list;
final String type = sa.getParam("CounterType");
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
int left = amount;
@@ -702,6 +740,7 @@ public class CountersPutAi extends SpellAbilityAi {
int totalTargets = list.size();
sa.resetTargets();
while (sa.canAddMoreTarget()) {
if (mandatory) {
// When things are mandatory, gotta handle a little differently
@@ -790,14 +829,15 @@ public class CountersPutAi extends SpellAbilityAi {
if (mode == PlayerActionConfirmMode.Tribute) {
// add counter if that opponent has a giant creature
final List<Card> creats = player.getCreaturesInPlay();
final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa);
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final boolean isHaste = source.hasKeyword("Haste");
final boolean isHaste = source.hasKeyword(Keyword.HASTE);
List<Card> threatening = CardLists.filter(creats, new Predicate<Card>() {
@Override
public boolean apply(Card c) {
return CombatUtil.canBlock(source, c, !isHaste)
&& (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword("DeathTouch"));
&& (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword(Keyword.DEATHTOUCH));
}
});
if (!threatening.isEmpty()) {
@@ -814,7 +854,7 @@ public class CountersPutAi extends SpellAbilityAi {
List<Card> canBlock = CardLists.filter(creats, new Predicate<Card>() {
@Override
public boolean apply(Card c) {
return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword("DeathTouch"));
return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword(Keyword.DEATHTOUCH));
}
});
if (!canBlock.isEmpty()) {
@@ -849,7 +889,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
final CounterType type = CounterType.valueOf(sa.getParam("CounterType"));
final String amountStr = sa.getParam("CounterNum");
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final boolean isCurse = sa.isCurse();
@@ -907,7 +947,7 @@ public class CountersPutAi extends SpellAbilityAi {
final CardCollection persist = CardLists.filter(filtered, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
if (!input.hasKeyword("Persist"))
if (!input.hasKeyword(Keyword.PERSIST))
return false;
return input.getCounters(CounterType.M1M1) <= amount;
}
@@ -920,7 +960,7 @@ public class CountersPutAi extends SpellAbilityAi {
final CardCollection undying = CardLists.filter(filtered, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
if (!input.hasKeyword("Undying"))
if (!input.hasKeyword(Keyword.UNDYING))
return false;
return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount;
}
@@ -945,7 +985,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (e instanceof Card) {
Card c = (Card) e;
if (c.getController().isOpponentOf(ai)) {
if (options.contains(CounterType.M1M1) && !c.hasKeyword("Undying")) {
if (options.contains(CounterType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.M1M1;
}
for (CounterType type : options) {
@@ -975,4 +1015,80 @@ public class CountersPutAi extends SpellAbilityAi {
}
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;
}
}

View File

@@ -23,6 +23,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.card.*;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.SpellAbility;
@@ -56,7 +57,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final Game game = ai.getGame();
final int amount = Integer.valueOf(sa.getParam("CounterNum"));
@@ -71,7 +71,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
list = ComputerUtil.filterAITgts(sa, ai, list, false);
if (sa.hasParam("CounterType")) {
// currently only Jhoira's Timebug
@@ -125,7 +125,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist");
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
if (!aiPersistList.isEmpty()) {
aiM1M1List = aiPersistList;
}
@@ -137,7 +137,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
// do as P1P1 part
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying");
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
if (!aiUndyingList.isEmpty()) {
aiP1P1List = aiUndyingList;
@@ -226,9 +226,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
return CounterType.ICE;
}
} else if (tgt.hasKeyword("Undying") && options.contains(CounterType.P1P1)) {
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.P1P1)) {
return CounterType.P1P1;
} else if (tgt.hasKeyword("Persist") && options.contains(CounterType.M1M1)) {
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.M1M1)) {
return CounterType.M1M1;
}
@@ -272,9 +272,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
return false;
}
} else if (type.equals(CounterType.M1M1) && tgt.hasKeyword("Persist")) {
} else if (type.equals(CounterType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {
return false;
} else if (type.equals(CounterType.P1P1) && tgt.hasKeyword("Undying")) {
} else if (type.equals(CounterType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) {
return false;
}

View File

@@ -9,6 +9,7 @@ import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -188,7 +189,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist");
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
if (!aiPersistList.isEmpty()) {
aiM1M1List = aiPersistList;
}
@@ -200,7 +201,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// do as P1P1 part
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying");
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
if (!aiUndyingList.isEmpty()) {
aiP1P1List = aiUndyingList;
@@ -230,7 +231,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection aiList = CardLists.filterControlledBy(list, ai);
aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount));
CardCollection aiPersist = CardLists.getKeyword(aiList, "Persist");
CardCollection aiPersist = CardLists.getKeyword(aiList, Keyword.PERSIST);
if (!aiPersist.isEmpty()) {
aiList = aiPersist;
}
@@ -253,7 +254,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// targeting ai creatures too
CardCollection aiList = CardLists.filterControlledBy(list, ai);
if (!aiList.isEmpty()) {
CardCollection aiListUndying = CardLists.getKeyword(aiList, "Undying");
CardCollection aiListUndying = CardLists.getKeyword(aiList, Keyword.UNDYING);
if (!aiListUndying.isEmpty()) {
aiList = aiListUndying;
}
@@ -266,7 +267,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// need to target opponent creatures
CardCollection oppList = CardLists.filterControlledBy(list, ai.getOpponents());
if (!oppList.isEmpty()) {
CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, "Undying");
CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, Keyword.UNDYING);
if (!oppListNotUndying.isEmpty()) {
oppList = oppListNotUndying;
}
@@ -307,6 +308,30 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
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));
return true;
}
@@ -358,9 +383,9 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
} else {
if (options.contains(CounterType.M1M1) && target.hasKeyword("Persist")) {
if (options.contains(CounterType.M1M1) && target.hasKeyword(Keyword.PERSIST)) {
return CounterType.M1M1;
} else if (options.contains(CounterType.P1P1) && target.hasKeyword("Undying")) {
} else if (options.contains(CounterType.P1P1) && target.hasKeyword(Keyword.UNDYING)) {
return CounterType.M1M1;
}
for (CounterType type : options) {

View File

@@ -8,6 +8,7 @@ import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -24,8 +25,9 @@ public abstract class DamageAiBase extends SpellAbilityAi {
// Do not target a player if they aren't below 75% of our health.
// Unless Lifelink will cancel the damage to us
Card hostcard = sa.getHostCard();
boolean lifelink = hostcard.hasKeyword("Lifelink");
for (Card ench : hostcard.getEnchantedBy(false)) {
boolean lifelink = hostcard.hasKeyword(Keyword.LIFELINK);
if (!lifelink) {
for (Card ench : hostcard.getEnchantedBy()) {
// Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink.
if (ench.hasSVar("LikeLifeLink")) {
if ("True".equals(ench.getSVar("LikeLifeLink"))) {
@@ -33,6 +35,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
}
}
}
}
if ("SelfDamage".equals(sa.getParam("AILogic"))) {
if (comp.getLife() * 0.75 < enemy.getLife()) {
if (!lifelink) {

View File

@@ -8,6 +8,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -268,7 +269,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
};
list = CardLists.getNotKeyword(list, "Indestructible");
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
list = CardLists.filter(list, filterKillable);
return list;

View File

@@ -4,12 +4,17 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -20,7 +25,11 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -89,11 +98,26 @@ public class DamageDealAi extends DamageAiBase {
if (damage.equals("X")) {
if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
// Set PayX here to maximum value.
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));
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).contains("InYourHand") && source.getZone().is(ZoneType.Hand)) {
dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
if (sa.getTargetRestrictions().canTgtPlayer()) {
@@ -215,9 +239,34 @@ public class DamageDealAi extends DamageAiBase {
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)) {
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")) ||
sourceName.equals("Crater's Claws")){
@@ -237,6 +286,19 @@ public class DamageDealAi extends DamageAiBase {
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;
}
}
}
return true;
}
@@ -274,7 +336,7 @@ public class DamageDealAi extends DamageAiBase {
final Game game = source.getGame();
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
public boolean apply(final Card c) {
return c.getSVar("Targeting").equals("Dies")
@@ -285,7 +347,10 @@ public class DamageDealAi extends DamageAiBase {
});
// 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;
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
@@ -480,10 +545,11 @@ public class DamageDealAi extends DamageAiBase {
final PhaseHandler phase = game.getPhaseHandler();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
final String logic = sa.getParamOrDefault("AILogic", "");
Player enemy = ComputerUtil.getOpponentFor(ai);
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
if ("PowerDmg".equals(logic)) {
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
if (tgt.canTgtCreatureAndPlayer() && this.shouldTgtP(ai, sa, dmg, noPrevention)){
sa.resetTargets();
@@ -492,7 +558,9 @@ public class DamageDealAi extends DamageAiBase {
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;
}
@@ -503,11 +571,11 @@ public class DamageDealAi extends DamageAiBase {
TargetChoices tcs = sa.getTargets();
// Do not use if would kill self
if (("SelfDamage".equals(sa.getParam("AILogic"))) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
if (("SelfDamage".equals(logic)) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
return false;
}
if ("ChoiceBurn".equals(sa.getParam("AILogic"))) {
if ("ChoiceBurn".equals(logic)) {
// do not waste burns on player if other choices are present
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
tcs.add(enemy);
@@ -516,7 +584,7 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
}
if ("Polukranos".equals(sa.getParam("AILogic"))) {
if ("Polukranos".equals(logic)) {
int dmgTaken = 0;
CardCollection humCreatures = enemy.getCreaturesInPlay();
Card lastTgt = null;
@@ -558,7 +626,15 @@ public class DamageDealAi extends DamageAiBase {
return true;
}
}
int totalTargetedSoFar = -1;
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()) {
// canPlayAI (sa activated by ai)
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
@@ -572,10 +648,9 @@ public class DamageDealAi extends DamageAiBase {
if (c != null && !this.shouldTgtP(ai, sa, dmg, noPrevention, true)) {
tcs.add(c);
if (divided) {
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
if (assignedDamage <= dmg) {
int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
assignedDamage = Math.min(dmg, assignedDamage);
tgt.addDividedAllocation(c, assignedDamage);
}
dmg = dmg - assignedDamage;
if (dmg <= 0) {
break;
@@ -680,7 +755,7 @@ public class DamageDealAi extends DamageAiBase {
}
continue;
}
} else if ("OppAtTenLife".equals(sa.getParam("AILogic"))) {
} else if ("OppAtTenLife".equals(logic)) {
for (final Player p : ai.getOpponents()) {
if (sa.canTarget(p) && p.getLife() == 10 && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
tcs.add(p);
@@ -689,9 +764,10 @@ public class DamageDealAi extends DamageAiBase {
}
// TODO: Improve Damage, we shouldn't just target the player just
// because we can
else if (sa.canTarget(enemy)) {
if (sa.canTarget(enemy) && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
|| ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai))
|| sa.getPayCosts() == null || immediately
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) &&
(!avoidTargetP(ai, sa))) {
@@ -742,7 +818,7 @@ public class DamageDealAi extends DamageAiBase {
if (o instanceof Card) {
Card c = (Card) o;
final int restDamage = ComputerUtilCombat.predictDamageTo(c, dmg, saMe.getHostCard(), false);
if (!c.hasKeyword("Indestructible") && ComputerUtilCombat.getDamageToKill(c) <= restDamage) {
if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && ComputerUtilCombat.getDamageToKill(c) <= restDamage) {
if (c.getController().equals(ai)) {
return false;
} else {
@@ -942,7 +1018,7 @@ public class DamageDealAi extends DamageAiBase {
for (Card c : creatures) {
int power = c.getNetPower();
int toughness = c.getNetToughness();
boolean canDie = !(c.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(c.getController(), c));
boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c));
// Currently will target creatures with toughness 3+ (or power 5+)
// and only if the creature can actually die, do not "underdrain"
@@ -959,4 +1035,90 @@ public class DamageDealAi extends DamageAiBase {
source.setSVar("PayX", Integer.toString(dmg));
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;
}
}

View File

@@ -8,6 +8,7 @@ import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -109,7 +110,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
for (Card c : list) {
if (c.hasKeyword("Indestructible")) {
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
sa.getTargets().add(c);
return true;
}
@@ -133,7 +134,7 @@ public class DestroyAi extends SpellAbilityAi {
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
list = CardLists.getNotKeyword(list, "Indestructible");
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
if (CardLists.getNotType(list, "Creature").isEmpty()) {
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
}
@@ -160,7 +161,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
//Check for undying
return (!c.hasKeyword("Undying") || c.getCounters(CounterType.P1P1) > 0);
return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterType.P1P1) > 0);
}
});
}
@@ -177,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()) {
return false;
}
@@ -270,7 +273,7 @@ public class DestroyAi extends SpellAbilityAi {
} else {
// Don't destroy stolen permanents when the stealing aura can be destroyed
if (choice.getOwner() == ai) {
for (Card aura : choice.getEnchantedBy(false)) {
for (Card aura : choice.getEnchantedBy()) {
SpellAbility sp = aura.getFirstSpellAbility();
if (sp != null && "GainControl".equals(sp.getParam("AILogic"))
&& aura.getController() != ai && sa.canTarget(aura)) {
@@ -294,7 +297,7 @@ public class DestroyAi extends SpellAbilityAi {
if (list.isEmpty()
|| !CardLists.filterControlledBy(list, ai).isEmpty()
|| CardLists.getNotKeyword(list, "Indestructible").isEmpty()) {
|| CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
return false;
}
}
@@ -312,11 +315,14 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), 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)) {
return false;
}
CardCollection preferred = CardLists.getNotKeyword(list, "Indestructible");
CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);

View File

@@ -7,6 +7,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -18,7 +19,7 @@ public class DestroyAllAi extends SpellAbilityAi {
private static final Predicate<Card> predicate = new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !(c.hasKeyword("Indestructible") || c.getSVar("SacMe").length() > 0);
return !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getSVar("SacMe").length() > 0);
}
};
@@ -64,10 +65,15 @@ public class DestroyAllAi extends SpellAbilityAi {
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
Player opponent = ComputerUtil.getOpponentFor(ai); // TODO: how should this AI logic work for multiplayer and getOpponents()?
final int CREATURE_EVAL_THRESHOLD = 200;
if (logic.equals("Always")) {
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
}
String valid = "";
if (sa.hasParam("ValidCards")) {
valid = sa.getParam("ValidCards");

View File

@@ -26,10 +26,7 @@ import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.PaymentDecision;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -134,6 +131,8 @@ public class DrawAi extends SpellAbilityAi {
return true;
} else if (logic.equals("AlwaysAtOppEOT")) {
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
@@ -252,19 +251,34 @@ public class DrawAi extends SpellAbilityAi {
}
if (num != null && num.equals("ChosenX")) {
// Necrologia, Pay X Life : Draw X Cards
if (sa.getSVar("X").equals("XChoice")) {
// Draw up to max hand size but leave at least 3 in library
numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
// But no more than what's "safe" and doesn't risk a near death experience
if (sa.getPayCosts() != null) {
if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
// [Necrologia, Pay X Life : Draw X Cards]
// Don't draw more than what's "safe" and don't risk a near death experience
// Maybe would be better to check for "serious danger" and take more risk?
while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
numCards--;
}
} else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
// [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards]
// TODO: Add special logic to limit/otherwise modify the ChosenX value here
// Skip this ability if nothing is to be chosen for sacrifice
if (numCards <= 0) {
return false;
}
}
}
sa.setSVar("ChosenX", Integer.toString(numCards));
source.setSVar("ChosenX", Integer.toString(numCards));
}
}
// Logic for cards that require special handling
if ("YawgmothsBargain".equals(logic)) {
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
@@ -351,6 +365,10 @@ public class DrawAi extends SpellAbilityAi {
if (numCards >= computerLibrarySize) {
if (xPaid) {
numCards = computerLibrarySize - 1;
if (numCards <= 0 && !mandatory) {
// not drawing anything, so don't do it
return false;
}
} else if (!ai.isCardInPlay("Laboratory Maniac")) {
aiTarget = false;
}
@@ -384,6 +402,9 @@ public class DrawAi extends SpellAbilityAi {
if (computerHandSize + numCards > computerMaxHandSize && game.getPhaseHandler().isPlayerTurn(ai)) {
if (xPaid) {
numCards = computerMaxHandSize - computerHandSize;
if (sa.getHostCard().getZone().is(ZoneType.Hand)) {
numCards++; // the card will be spent
}
source.setSVar("PayX", Integer.toString(numCards));
} else {
// Don't draw too many cards and then risk discarding

View File

@@ -6,17 +6,15 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.ai.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -115,10 +113,39 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("SpellCopy")) {
// fetch Instant or Sorcery and AI has reason to play this turn
// does not try to get itself
final ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST;
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
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;
}
});
@@ -138,7 +165,26 @@ public class EffectAi extends SpellAbilityAi {
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return (c.isInstant() || c.isSorcery()) && !c.hasKeyword("Rebound") && ComputerUtil.hasReasonToPlayCardThisTurn(ai, c);
if (!(c.isInstant() || c.isSorcery()) || c.hasKeyword(Keyword.REBOUND)) {
return false;
}
for (SpellAbility ab : c.getSpellAbilities()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab))
|| ab.hasParam("AINoRecursiveCheck")) {
// prevent infinitely recursing mana ritual and other abilities with reentry
continue;
}
if (!ab.canPlay()) {
continue;
}
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab);
if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) {
if (ComputerUtilMana.canPayManaCost(ab, ai, 0)) {
return true;
}
}
}
return false;
}
});
@@ -149,6 +195,11 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true;
} else if (logic.equals("Always")) {
randomReturn = true;
} else if (logic.equals("Main1")) {
if (phase.getPhase().isBefore(PhaseType.MAIN1)) {
return false;
}
randomReturn = true;
} else if (logic.equals("Main2")) {
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
return false;

View File

@@ -7,6 +7,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -32,6 +33,12 @@ public class FightAi extends SpellAbilityAi {
sa.resetTargets();
final Card source = sa.getHostCard();
// everything is defined or targeted above, can't do anything there?
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
// TODO extend Logic for cards like Arena or Grothama
return true;
}
// Get creature lists
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
aiCreatures = CardLists.getTargetableCards(aiCreatures, sa);
@@ -246,15 +253,17 @@ public class FightAi extends SpellAbilityAi {
}
return false;
}
public static boolean canKill(Card fighter, Card opponent, int pumpAttack) {
if (opponent.getSVar("Targeting").equals("Dies")) {
return true;
}
if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed()
|| opponent.getShieldCount() > 0 || ComputerUtil.canRegenerate(opponent.getController(), opponent)) {
if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed() || opponent.getShieldCount() > 0
|| ComputerUtil.canRegenerate(opponent.getController(), opponent)) {
return false;
}
if (fighter.hasKeyword("Deathtouch") || ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) {
if (fighter.hasKeyword(Keyword.DEATHTOUCH)
|| ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) {
return true;
}
return false;

View File

@@ -1,17 +1,19 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.card.Card;
import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import java.util.List;
public class FogAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -20,6 +22,35 @@ public class FogAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
final Card hostCard = sa.getHostCard();
// Don't cast it, if the effect is already in place
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
return false;
}
// if card would be destroyed, react and use immediately if it's not own turn
if ((AiCardMemory.isRememberedCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
&& (!game.getStack().isEmpty())
&& (!game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()))) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, null);
if (objects.contains(hostCard)) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
return true;
}
}
// Reserve mana to cast this card if it will be likely needed
if (((game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()))
|| (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)))
&& (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
&& (ComputerUtil.aiLifeInDanger(ai, false, 0))) {
boolean reserved = ((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, true);
if (reserved) {
AiCardMemory.rememberCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
}
}
// AI should only activate this during Human's Declare Blockers phase
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return false;
@@ -33,17 +64,12 @@ public class FogAi extends SpellAbilityAi {
return false;
}
// Don't cast it, if the effect is already in place
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
return false;
}
if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) {
int dmg = 0;
for (Card atk : game.getCombat().getAttackersOf(ai)) {
if (game.getCombat().isUnblocked(atk)) {
dmg += atk.getNetCombatDamage();
} else if (atk.hasKeyword("Trample")) {
} else if (atk.hasKeyword(Keyword.TRAMPLE)) {
dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness);
}
}

View File

@@ -1,14 +1,15 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.MagicStack;
import forge.util.MyRandom;
public class LifeExchangeVariantAi extends SpellAbilityAi {
@@ -83,7 +84,48 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
return shouldDo;
}
else if ("Evra, Halcyon Witness".equals(sourceName)) {
// TODO add logic
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())
return false;
if (ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
return true;
}
// check the top of stack
MagicStack stack = game.getStack();
if (!stack.isEmpty()) {
SpellAbility saTop = stack.peekAbility();
if (ComputerUtil.predictDamageFromSpell(saTop, ai) >= aiLife) {
return true;
}
}
}
}
return false;

View File

@@ -51,6 +51,13 @@ public class LifeLoseAi extends SpellAbilityAi {
if (tgtPlayers.contains(ai) && amount > 0 && amount + 3 > ai.getLife()) {
return false;
}
if (sa.usesTargeting()) {
if (!doTgt(ai, sa, false)) {
return false;
}
}
return true;
}

View File

@@ -9,11 +9,13 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.Arrays;
import java.util.List;
@@ -28,8 +30,10 @@ public class ManaEffectAi extends SpellAbilityAi {
*/
@Override
protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) {
if ("ManaRitual".equals(aiLogic)) {
if (aiLogic.startsWith("ManaRitual")) {
return doManaRitualLogic(ai, sa);
} else if ("Always".equals(aiLogic)) {
return true;
}
return super.checkAiLogic(ai, sa, aiLogic);
}
@@ -103,6 +107,7 @@ public class ManaEffectAi extends SpellAbilityAi {
// Dark Ritual and other similar instants/sorceries that add mana to mana pool
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
final Card host = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
int numManaSrcs = manaSources.size();
@@ -114,7 +119,9 @@ public class ManaEffectAi extends SpellAbilityAi {
String produced = sa.getParam("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)) {
CounterType ctrType = CounterType.KI; // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) {
@@ -123,7 +130,12 @@ public class ManaEffectAi extends SpellAbilityAi {
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;
@@ -170,7 +182,7 @@ public class ManaEffectAi extends SpellAbilityAi {
}
testSaNoCost.setActivatingPlayer(ai);
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSaNoCost) == AiPlayDecision.WillPlay) {
if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword("Haste")
if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword(Keyword.HASTE)
&& !ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
// AI will waste a ritual in Main 1 unless the casted permanent is a haste creature
continue;
@@ -193,6 +205,13 @@ public class ManaEffectAi extends SpellAbilityAi {
CardPredicates.lessCMC(searchCMC),
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.
return castableSpells.size() > 0;
}

View File

@@ -113,7 +113,7 @@ public class ManifestAi extends SpellAbilityAi {
repParams.put("Origin", ZoneType.Library);
repParams.put("Destination", ZoneType.Battlefield);
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()) {
return false;
}

View File

@@ -55,10 +55,12 @@ public class MillAi extends SpellAbilityAi {
} 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
}
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)
&& ph.getNextTurn().equals(ai))) {
return false; // only self-mill at opponent EOT
}
}
if (sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) {
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

View File

@@ -2,15 +2,17 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import com.google.common.collect.Lists;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -23,16 +25,47 @@ public class MustBlockAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// disabled for the AI until he/she can make decisions about who to make
// block
final Card source = sa.getHostCard();
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;
}
@Override
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;
}
// Otherwise it's a standard targeted "target creature blocks CARDNAME" ability, so use the main canPlayAI code path
return canPlayAI(aiPlayer, sa);
}
@Override
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
@@ -62,27 +95,7 @@ public class MustBlockAi extends SpellAbilityAi {
boolean chance = false;
if (abTgt != null) {
List<Card> list = CardLists.filter(ComputerUtil.getOpponentFor(ai).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();
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;
}
});
final List<Card> list = determineGoodBlockers(definedAttacker, ai, ComputerUtil.getOpponentFor(ai), sa, true,true);
if (list.isEmpty()) {
return false;
}
@@ -90,6 +103,20 @@ public class MustBlockAi extends SpellAbilityAi {
if (blocker == null) {
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);
chance = true;
} else {
@@ -98,4 +125,40 @@ public class MustBlockAi extends SpellAbilityAi {
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;
}
}

View File

@@ -35,7 +35,7 @@ public class PermanentAi extends SpellAbilityAi {
final Card card = sa.getHostCard();
if (card.hasStartOfKeyword("You may cast CARDNAME as though it had flash. If") && !ai.couldCastSorcery(sa)) {
if (card.hasKeyword("MayFlashSac") && !ai.couldCastSorcery(sa)) {
// AiPlayDecision.AnotherTime
return false;
}
@@ -61,8 +61,23 @@ public class PermanentAi extends SpellAbilityAi {
if (card.getType().isLegendary()
&& !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
if (ai.isCardInPlay(card.getName())) {
if (!card.hasSVar("AILegendaryException")) {
// AiPlayDecision.WouldDestroyLegend
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
// like a reasonable default for some cards).
for (Card c : ctrld) {
if (c.getEnchantedBy(false).isEmpty()) {
if (c.getEnchantedBy().isEmpty()) {
numControlled++;
} else {
for (Card att : c.getEnchantedBy(false)) {
for (Card att : c.getEnchantedBy()) {
if (!att.getController().isOpponentOf(ai)) {
numControlled++;
}

View File

@@ -1,17 +1,21 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import com.google.common.base.Predicate;
import forge.ai.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.combat.Combat;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
/**
* AbilityFactory for Creature Spells.
@@ -79,21 +83,122 @@ public class PermanentCreatureAi extends PermanentAi {
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
if (card.hasKeyword("Flash")
&& (ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
if ((ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
&& ai.getManaPool().totalMana() <= 0
&& (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)) {
// AiPlayDecision.AnotherTime;
return false;
}
}
}
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
protected boolean checkApiLogic(Player ai, SpellAbility sa) {

View File

@@ -7,14 +7,14 @@ import forge.card.CardTypeView;
import forge.game.Game;
import forge.game.GameType;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
@@ -70,11 +70,28 @@ public class PlayAi extends SpellAbilityAi {
}
}
if ("ReplaySpell".equals(logic)) {
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
// 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 (source != null && source.hasKeyword("Hideaway") && source.hasRemembered()) {
if ("ReplaySpell".equals(logic)) {
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()) {
// AI is not very good at playing non-permanent spells this way, at least yet
// (might be possible to enable it for Sorceries in Main1/Main2 if target is available,
// but definitely not for most Instants)
@@ -137,6 +154,15 @@ public class PlayAi extends SpellAbilityAi {
if (!s.getRestrictions().checkTimingRestrictions(c, s))
continue;
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();
} else if (sa.hasParam("PlayCost")) {
Cost abCost;
@@ -149,6 +175,13 @@ public class PlayAi extends SpellAbilityAi {
spell = (Spell) spell.copyWithDefinedCost(abCost);
}
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;
}
}

View File

@@ -17,7 +17,6 @@ import forge.util.MyRandom;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class PowerExchangeAi extends SpellAbilityAi {
@@ -38,8 +37,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
final Map<String, String> vars = c.getSVars();
return !vars.containsKey("RemAIDeck") && c.canBeTargetedBy(sa);
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa);
}
});
CardLists.sortByPowerAsc(list);

View File

@@ -175,6 +175,11 @@ public class ProtectAi extends SpellAbilityAi {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (cards.size() == 0) {
return false;
} else if (cards.size() == 1) {
// Affecting single card
if ((getProtectCreatures(ai, sa)).contains(cards.get(0))) {
return true;
}
}
/*
* when this happens we need to expand AI to consider if its ok

View File

@@ -14,6 +14,7 @@ import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostTapType;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -35,12 +36,7 @@ public class PumpAi extends PumpAiBase {
if (cost == null) {
return true;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostTapType) {
return true;
}
}
return false;
return cost.hasSpecificCostType(CostTapType.class);
}
@Override
@@ -190,7 +186,7 @@ public class PumpAi extends PumpAiBase {
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying")
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING)
|| card.isToken()) {
return true;
}
@@ -243,7 +239,7 @@ public class PumpAi extends PumpAiBase {
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying")
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING)
|| card.isToken()) {
return true;
}
@@ -320,6 +316,9 @@ public class PumpAi extends PumpAiBase {
}
} else {
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;
@@ -336,6 +335,9 @@ public class PumpAi extends PumpAiBase {
}
} else {
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)) {
@@ -485,6 +487,10 @@ public class PumpAi extends PumpAiBase {
} else if (sa.getParam("AILogic").equals("DonateTargetPerm")) {
// Donate step 2 - target a donatable permanent.
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) {
return FightAi.canFightAi(ai, sa, attack, defense);
@@ -775,7 +781,7 @@ public class PumpAi extends PumpAiBase {
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (source.isCreature()) {
if (!source.hasKeyword("Indestructible") && source.getNetToughness() + defense <= source.getDamage()) {
if (!source.hasKeyword(Keyword.INDESTRUCTIBLE) && source.getNetToughness() + defense <= source.getDamage()) {
return false;
}
if (source.getNetToughness() + defense <= 0) {
@@ -864,7 +870,7 @@ public class PumpAi extends PumpAiBase {
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
final boolean isInfect = source.hasKeyword("Infect"); // Flesh-Eater Imp
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
@@ -970,7 +976,7 @@ public class PumpAi extends PumpAiBase {
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
final boolean isInfect = source.hasKeyword("Infect");
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {

View File

@@ -12,6 +12,7 @@ import forge.game.Game;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
@@ -135,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|| card.getNetCombatDamage() <= 0
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| ph.getPhase().isBefore(PhaseType.MAIN1)
|| CardLists.getNotKeyword(ai.getCreaturesInPlay(), "Defender").isEmpty())) {
|| CardLists.getNotKeyword(ai.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty())) {
return false;
}
if (!ph.isPlayerTurn(ai) && (combat == null || !combat.isAttacking(card) || card.getNetCombatDamage() <= 0)) {
@@ -193,21 +194,26 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.endsWith("Flying")) {
CardCollectionView attackingFlyer = CardCollection.EMPTY;
if (combat != null) {
attackingFlyer = CardLists.getKeyword(combat.getAttackers(), Keyword.FLYING);
}
if (ph.isPlayerTurn(opp)
&& ph.getPhase() == PhaseType.COMBAT_DECLARE_ATTACKERS
&& !CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty()
&& !card.hasKeyword("Reach")
&& !attackingFlyer.isEmpty()
&& !card.hasKeyword(Keyword.REACH)
&& CombatUtil.canBlock(card)
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
return true;
}
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"));
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH));
if (ph.isPlayerTurn(opp) && combat != null
&& Iterables.any(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))
&& !attackingFlyer.isEmpty()
&& CombatUtil.canBlock(card)) {
// Use defensively to destroy the opposing Flying creature when possible, or to block with an indestructible
// creature buffed with Flying
for (Card c : CardLists.filter(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))) {
for (Card c : attackingFlyer) {
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)
&& (card.getNetPower() >= c.getNetToughness() && card.getNetToughness() > c.getNetPower()
|| ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, card))) {
@@ -225,7 +231,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
} else if (keyword.endsWith("Horsemanship")) {
if (ph.isPlayerTurn(opp)
&& ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !CardLists.getKeyword(game.getCombat().getAttackers(), "Horsemanship").isEmpty()
&& !CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.HORSEMANSHIP).isEmpty()
&& CombatUtil.canBlock(card)
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
return true;
@@ -234,7 +240,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| newPower <= 0
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
"Horsemanship").isEmpty()) {
Keyword.HORSEMANSHIP).isEmpty()) {
return false;
}
} else if (keyword.endsWith("Intimidate")) {
@@ -298,7 +304,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("First Strike")) {
if (card.hasKeyword("Double Strike")) {
if (card.hasKeyword(Keyword.DOUBLE_STRIKE)) {
return false;
}
if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) {
@@ -338,7 +344,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|| newPower <= 0
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
"Flanking").isEmpty()) {
Keyword.FLANKING).isEmpty()) {
return false;
}
} else if (keyword.startsWith("Trample")) {
@@ -353,7 +359,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
if (newPower <= 0) {
return false;
}
if (combat != null && combat.isBlocking(card) && !card.hasKeyword("Wither")) {
if (combat != null && combat.isBlocking(card) && !card.hasKeyword(Keyword.WITHER)) {
return true;
}
if ((ph.isPlayerTurn(opp))
@@ -362,7 +368,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.endsWith("Wither")) {
if (newPower <= 0 || card.hasKeyword("Infect")) {
if (newPower <= 0 || card.hasKeyword(Keyword.INFECT)) {
return false;
}
return combat != null && ( combat.isBlocking(card) || (combat.isAttacking(card) && combat.isBlocked(card)) );
@@ -375,14 +381,14 @@ public abstract class PumpAiBase extends SpellAbilityAi {
if (ph.isPlayerTurn(opp) || !CombatUtil.canAttack(card, opp)
|| newPower <= 0
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| CardLists.getNotKeyword(opp.getCreaturesInPlay(), "Defender").isEmpty()) {
|| CardLists.getNotKeyword(opp.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty()) {
return false;
}
} else if (keyword.equals("Reach")) {
if (ph.isPlayerTurn(ai)
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty()
|| card.hasKeyword("Flying")
|| CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.FLYING).isEmpty()
|| card.hasKeyword(Keyword.FLYING)
|| !CombatUtil.canBlock(card)) {
return false;
}
@@ -409,7 +415,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("Persist")) {
if (card.getBaseToughness() <= 1 || card.hasKeyword("Undying")) {
if (card.getBaseToughness() <= 1 || card.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (keyword.equals("Islandwalk")) {
@@ -445,11 +451,15 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.endsWith("CARDNAME can attack as though it didn't have defender.")) {
if (!ph.isPlayerTurn(ai) || !card.hasKeyword("Defender")
if (!ph.isPlayerTurn(ai) || !card.hasKeyword(Keyword.DEFENDER)
|| ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)
|| card.isTapped() || newPower <= 0) {
return false;
}
} else if (keyword.equals("Prevent all combat damage that would be dealt to CARDNAME.")) {
if (combat == null || !(combat.isBlocking(card) || combat.isBlocked(card))) {
return false;
}
}
return true;
}
@@ -506,7 +516,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
if (c.getSVar("Targeting").equals("Dies") || c.getNetToughness() <= -defense) {
return true; // can kill indestructible creatures
}
return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword("Indestructible"));
return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
}
}); // leaves all creatures that will be destroyed
} // -X/-X end

View File

@@ -14,6 +14,8 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -36,6 +38,15 @@ public class PumpAllAi extends PumpAiBase {
final Game game = ai.getGame();
final Combat combat = game.getCombat();
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 defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
@@ -85,7 +96,7 @@ public class PumpAllAi extends PumpAiBase {
if (c.getNetToughness() <= -defense) {
return true; // can kill indestructible creatures
}
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible"));
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
}
}); // leaves all creatures that will be destroyed
human = CardLists.filter(human, new Predicate<Card>() {
@@ -94,7 +105,7 @@ public class PumpAllAi extends PumpAiBase {
if (c.getNetToughness() <= -defense) {
return true; // can kill indestructible creatures
}
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible"));
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
}
}); // leaves all creatures that will be destroyed
} // -X/-X end

View File

@@ -1,11 +1,20 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
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.PlayerActionConfirmMode;
import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -13,14 +22,22 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
*/
@Override
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.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)
* @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) {
// Do it once per turn, generally (may be improved later)
if (AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -28,21 +45,78 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
// ability is targeted
sa.resetTargets();
Player opp = ComputerUtil.getOpponentFor(ai);
Player opp = ComputerUtil.getOpponentFor(aiPlayer);
final boolean canTgtAI = aiPlayer.canBeTargetedBy(sa);
final boolean canTgtHuman = opp.canBeTargetedBy(sa);
if (!canTgtHuman) {
return false;
} else {
if (canTgtHuman && canTgtAI) {
// TODO: maybe some other consideration rather than random?
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);
} 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 {
// 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.
// For now, "true" is returned (without any action) if the SA is mandatory in order
// not to miss triggers.
return sa.isMandatory();
return true;
}
/* (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 mandatory || canPlayAI(ai, sa);
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@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;
}
}

View File

@@ -4,10 +4,13 @@ import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
@@ -82,7 +85,7 @@ public class SacrificeAi extends SpellAbilityAi {
if (!destroy) {
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(sa));
} else {
if (!CardLists.getKeyword(list, "Indestructible").isEmpty()) {
if (!CardLists.getKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
// human can choose to destroy indestructibles
return false;
}
@@ -132,7 +135,7 @@ public class SacrificeAi extends SpellAbilityAi {
List<Card> humanList =
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
if (humanList.size() < amount) {
return false;
@@ -156,4 +159,41 @@ public class SacrificeAi extends SpellAbilityAi {
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;
}
}

View File

@@ -11,7 +11,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
@@ -22,9 +21,8 @@ public class ScryAi extends SpellAbilityAi {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { // It doesn't appear that Scry ever targets
if (sa.usesTargeting()) { // It doesn't appear that Scry ever targets
// ability is targeted
sa.resetTargets();

View File

@@ -3,7 +3,6 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GlobalRuleChange;
@@ -30,33 +29,10 @@ public class SetStateAi extends SpellAbilityAi {
}
// 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() + ".");
if (!isSafeToTransformIntoLegendary(aiPlayer, source)) {
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)) {
return true;
}
@@ -65,8 +41,6 @@ public class SetStateAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
final Card source = sa.getHostCard();
return super.checkAiLogic(aiPlayer, sa, aiLogic);
}
@@ -87,7 +61,7 @@ public class SetStateAi extends SpellAbilityAi {
if("Transform".equals(mode)) {
if (!sa.usesTargeting()) {
// no Transform with Defined which is not Self
if (source.hasKeyword("CARDNAME can't transform")) {
if (!source.canTransform()) {
return false;
}
return shouldTransformCard(source, ai, ph) || "Always".equals(logic);
@@ -96,15 +70,13 @@ public class SetStateAi extends SpellAbilityAi {
sa.resetTargets();
CardCollection list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
// select only cards with Transform as SplitType
// select only the ones that can transform
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(Card c) {
return c.hasAlternateState() && c.getRules().getSplitType() == CardSplitType.Transform;
return c.canTransform();
}
});
// select only the ones that can transform
list = CardLists.getNotKeyword(list, "CARDNAME can't transform");
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty()) {
@@ -260,8 +232,44 @@ public class SetStateAi extends SpellAbilityAi {
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) {
// 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;
}
}

View File

@@ -8,6 +8,12 @@ import forge.game.spellability.SpellAbility;
public class ShuffleAi extends SpellAbilityAi {
@Override
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;
}
// not really sure when the compy would use this; maybe only after a
// human
// deliberately put a card on top of their library

View File

@@ -0,0 +1,126 @@
package forge.ai.ability;
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.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
public class SurveilAi extends SpellAbilityAi {
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#doTriggerAINoCost(forge.game.player.Player, forge.game.spellability.SpellAbility, boolean)
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
sa.resetTargets();
sa.getTargets().add(ai);
}
return true;
}
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#chkAIDrawback(forge.game.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return doTriggerAINoCost(ai, sa, false);
}
/**
* Checks if the AI will play a SpellAbility based on its phase restrictions
*/
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
// if the Surveil ability requires tapping and has a mana cost, it's best done at the end of opponent's turn
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
// even if there's no mana cost.
if (sa.getPayCosts() != null) {
if (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
&& !SpellAbilityAi.isSorcerySpeed(sa)) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
}
// in the player's turn Surveil should only be done in Main1 or in Upkeep if able
if (ph.isPlayerTurn(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa)) {
return ph.is(PhaseType.MAIN1) || sa.hasParam("Planeswalker");
} else {
return ph.is(PhaseType.UPKEEP);
}
}
return true;
}
/**
* Checks if the AI will play a SpellAbility with the specified AiLogic
*/
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
final Card source = sa.getHostCard();
if ("Never".equals(aiLogic)) {
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
return true;
}
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
// Makes no sense to do Surveil when there's nothing in the library
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
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
if (SpellAbilityAi.isSorcerySpeed(sa)) {
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
if (SpellAbilityAi.playReusable(ai, sa)) {
randomReturn = true;
}
if (randomReturn) {
AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
return randomReturn;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
}

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CounterType;
@@ -26,12 +24,21 @@ public class TapAi extends TapAiBase {
if (turn.isOpponentOf(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
// Tap things down if it's Human's turn
} else if (turn == ai && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Tap creatures down if in combat -- handled in tapPrefTargeting().
} else if (SpellAbilityAi.isSorcerySpeed(sa)) {
} else if (turn.equals(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa) && phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
// Cast it if it's a sorcery.
} else if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Aggro Brains are willing to use TapEffects aggressively instead of defensively
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
if (!aic.getBooleanProperty(AiProps.PLAY_AGGRO)) {
return false;
}
} else {
// Don't tap down after blockers
return false;
}
} else if (!SpellAbilityAi.playReusable(ai, sa)){
// Generally don't want to tap things with an Instant during AI turn outside of combat
// Generally don't want to tap things with an Instant during Players turn outside of combat
return false;
}

View File

@@ -8,12 +8,16 @@ import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -41,14 +45,11 @@ import java.util.List;
* @version $Id: AbilityFactoryToken.java 17656 2012-10-22 19:32:56Z Max mtg $
*/
public class TokenAi extends SpellAbilityAi {
private String tokenAmount;
private String tokenName;
private String[] tokenTypes;
private String[] tokenKeywords;
private String tokenPower;
private String tokenToughness;
private Card actualToken;
/**
* <p>
* Constructor for AbilityFactory_Token.
@@ -57,23 +58,17 @@ public class TokenAi extends SpellAbilityAi {
* a {@link forge.game.ability.AbilityFactory} object.
*/
private void readParameters(final SpellAbility mapParams) {
String[] keywords;
if (mapParams.hasParam("TokenKeywords")) {
// TODO: Change this Split to a semicolon or something else
keywords = mapParams.getParam("TokenKeywords").split("<>");
} else {
keywords = new String[0];
}
this.tokenAmount = mapParams.getParamOrDefault("TokenAmount", "1");
this.actualToken = TokenInfo.getProtoType(mapParams.getParam("TokenScript"), mapParams);
if (actualToken == null) {
this.tokenPower = mapParams.getParam("TokenPower");
this.tokenToughness = mapParams.getParam("TokenToughness");
this.tokenName = mapParams.getParam("TokenName");
this.tokenTypes = mapParams.getParam("TokenTypes").split(",");
this.tokenKeywords = keywords;
} else {
this.tokenPower = actualToken.getBasePowerString();
this.tokenToughness = actualToken.getBaseToughnessString();
}
}
@Override
@@ -102,8 +97,11 @@ public class TokenAi extends SpellAbilityAi {
}
}
final Card token = spawnToken(ai, sa);
if (token == null) {
if (actualToken == null) {
actualToken = spawnToken(ai, sa);
}
if (actualToken == null) {
final AbilitySub sub = sa.getSubAbility();
if (pwPlus || (sub != null && SpellApiToAi.Converter.get(sub.getApi()).chkAIDrawback(sub, ai))) {
return true; // planeswalker plus ability or sub-ability is
@@ -129,24 +127,21 @@ public class TokenAi extends SpellAbilityAi {
}
}
if (canInterruptSacrifice(ai, sa, token)) {
if (canInterruptSacrifice(ai, sa, actualToken)) {
return true;
}
boolean haste = false;
boolean haste = this.actualToken.hasKeyword(Keyword.HASTE);
boolean oneShot = sa.getSubAbility() != null
&& sa.getSubAbility().getApi() == ApiType.DelayedTrigger;
for (final String kw : this.tokenKeywords) {
if (kw.equals("Haste")) {
haste = true;
}
}
boolean isCreature = this.actualToken.getType().isCreature();
// Don't generate tokens without haste before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && ph.isPlayerTurn(ai) && !haste && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
boolean buff = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
if ("Creature".equals(c.getSVar("BuffedBy"))) {
if (isCreature && "Creature".equals(c.getSVar("BuffedBy"))) {
buff = true;
}
}
@@ -179,13 +174,10 @@ public class TokenAi extends SpellAbilityAi {
}
// Don't kill AIs Legendary tokens
for (final String type : this.tokenTypes) {
if (type.equals("Legendary")) {
if (ai.isCardInPlay(this.tokenName)) {
if (this.actualToken.getType().isLegendary() && ai.isCardInPlay(this.actualToken.getName())) {
// TODO Check if Token is useless due to an aura or counters?
return false;
}
}
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
@@ -201,7 +193,8 @@ public class TokenAi extends SpellAbilityAi {
sa.getTargets().add(ai);
} else {
// Flash Foliage
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield),
ai.getOpponents());
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
list = CardLists.getTargetableCards(list, sa);
CardCollection betterList = CardLists.filter(list, new Predicate<Card>() {
@@ -213,7 +206,7 @@ public class TokenAi extends SpellAbilityAi {
if (!betterList.isEmpty()) {
list = betterList;
}
betterList = CardLists.getNotKeyword(list, "Trample");
betterList = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
if (!betterList.isEmpty()) {
list = betterList;
}
@@ -222,7 +215,6 @@ public class TokenAi extends SpellAbilityAi {
} else {
return false;
}
}
}
}
@@ -310,6 +302,18 @@ public class TokenAi extends SpellAbilityAi {
}
}
if (mandatory) {
// Necessary because the AI goes into this method twice, first to set up targets (with mandatory=true)
// and then the second time to confirm the trigger (where mandatory may be set to false).
return true;
}
if ("OnlyOnAlliedAttack".equals(sa.getParam("AILogic"))) {
Combat combat = ai.getGame().getCombat();
return combat != null && combat.getAttackingPlayer() != null
&& !combat.getAttackingPlayer().isOpponentOf(ai);
}
return true;
}
/* (non-Javadoc)
@@ -327,6 +331,7 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
// TODO: AILogic
readParameters(sa); // remember to call this somewhere!
Combat combat = ai.getGame().getCombat();
// TokenAttacking
if (combat != null && sa.hasParam("TokenAttacking")) {
@@ -346,6 +351,7 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable<GameEntity> options) {
// TODO: AILogic
readParameters(sa); // remember to call this somewhere!
Combat combat = ai.getGame().getCombat();
// TokenAttacking
if (combat != null && sa.hasParam("TokenAttacking")) {
@@ -376,6 +382,7 @@ public class TokenAi extends SpellAbilityAi {
* @param sa Token SpellAbility
* @return token creature created by ability
*/
@Deprecated
public static Card spawnToken(Player ai, SpellAbility sa) {
return spawnToken(ai, sa, false);
}
@@ -387,9 +394,18 @@ public class TokenAi extends SpellAbilityAi {
* @param notNull if the token would not survive, still return it
* @return token creature created by ability
*/
// TODO Is this just completely copied from TokenEffect? Let's just call that thing
@Deprecated
public static Card spawnToken(Player ai, SpellAbility sa, boolean notNull) {
final Card host = sa.getHostCard();
Card result = TokenInfo.getProtoType(sa.getParam("TokenScript"), sa);
if (result != null) {
result.setController(ai, 0);
return result;
}
String[] tokenKeywords = sa.hasParam("TokenKeywords") ? sa.getParam("TokenKeywords").split("<>") : new String[0];
String tokenPower = sa.getParam("TokenPower");
String tokenToughness = sa.getParam("TokenToughness");
@@ -510,7 +526,7 @@ public class TokenAi extends SpellAbilityAi {
// Apply static abilities and prune dead tokens
final Game game = ai.getGame();
ComputerUtilCard.applyStaticContPT(game, token, null);
if (!notNull && token.getNetToughness() < 1) {
if (!notNull && token.isCreature() && token.getNetToughness() < 1) {
return null;
} else {
return token;

View File

@@ -5,6 +5,7 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
@@ -23,7 +24,7 @@ public class UntapAllAi extends SpellAbilityAi {
return false;
}
String valid = "";
CardCollectionView list = aiPlayer.getGame().getCardsIn(ZoneType.Battlefield);
CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED);
if (sa.hasParam("ValidCards")) {
valid = sa.getParam("ValidCards");
}
@@ -35,6 +36,15 @@ public class UntapAllAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
Card source = sa.getHostCard();
if (sa.hasParam("ValidCards")) {
String valid = sa.getParam("ValidCards");
CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED);
list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa);
return mandatory || !list.isEmpty();
}
return mandatory;
}
}

View File

@@ -39,7 +39,7 @@ public class ZoneExchangeAi extends SpellAbilityAi {
}
if (type.equals("Aura")) {
Card c = object1.getEnchantingCard();
if (!c.canBeEnchantedBy(object2)) {
if (!c.canBeAttached(object2)) {
return false;
}
}

View File

@@ -13,6 +13,7 @@ import forge.LobbyPlayer;
import forge.ai.LobbyPlayerAi;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.GameObjectMap;
import forge.game.GameRules;
@@ -204,19 +205,16 @@ public class GameCopier {
}
}
gameObjectMap = new CopiedGameObjectMap(newGame);
for (Card card : origGame.getCardsIn(ZoneType.Battlefield)) {
Card otherCard = cardMap.get(card);
otherCard.setTimestamp(card.getTimestamp());
otherCard.setSickness(card.hasSickness());
otherCard.setState(card.getCurrentStateName(), false);
if (card.isEnchanting()) {
otherCard.setEnchanting(gameObjectMap.map(card.getEnchanting()));
}
if (card.isEquipping()) {
otherCard.equipCard(cardMap.get(card.getEquipping()));
}
if (card.isFortifying()) {
otherCard.setFortifying(cardMap.get(card.getFortifying()));
if (card.isAttachedToEntity()) {
GameEntity ge = gameObjectMap.map(card.getEntityAttachedTo());
otherCard.setEntityAttachedTo(ge);
ge.addAttachedCard(otherCard);
}
if (card.getCloneOrigin() != null) {
otherCard.setCloneOrigin(cardMap.get(card.getCloneOrigin()));
@@ -312,13 +310,13 @@ public class GameCopier {
newCard.setManifested(true);
// TODO: Should be able to copy other abilities...
if (isCreature && hasManaCost) {
newCard.addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
newCard.getState(CardStateName.Original).addSpellAbility(
CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
}
}
}
if (c.isMonstrous()) {
newCard.setMonstrous(true);
newCard.setMonstrosityNum(c.getMonstrosityNum());
}
if (c.isRenowned()) {
newCard.setRenowned(true);

View File

@@ -47,7 +47,12 @@ public class GameStateEvaluator {
}
private Score getScoreForGameOver(Game game, Player aiPlayer) {
return game.getOutcome().getWinningTeam() == aiPlayer.getTeam() ? new Score(Integer.MAX_VALUE) : new Score(Integer.MIN_VALUE);
if (game.getOutcome().getWinningTeam() == aiPlayer.getTeam() ||
game.getOutcome().isWinner(aiPlayer.getRegisteredPlayer())) {
return new Score(Integer.MAX_VALUE);
}
return new Score(Integer.MIN_VALUE);
}
public Score getScoreForGameState(Game game, Player aiPlayer) {

View File

@@ -140,13 +140,13 @@ public class SpellAbilityPicker {
}
}
private static boolean isSorcerySpeed(SpellAbility sa) {
private static boolean isSorcerySpeed(SpellAbility sa, Player player) {
// TODO: Can we use the actual rules engine for this instead of trying to do the logic ourselves?
if (sa instanceof PlayLandAbility) {
return false;
}
if (sa.isSpell()) {
return !sa.getHostCard().isInstant() && !sa.getHostCard().hasKeyword("Flash");
return !sa.getHostCard().isInstant() && !sa.getHostCard().withFlash(player);
}
if (sa.getRestrictions().isPwAbility()) {
return !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed.");
@@ -167,7 +167,7 @@ public class SpellAbilityPicker {
if (currentPhase.isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
List<SpellAbility> candidateSAs2 = new ArrayList<SpellAbility>();
for (SpellAbility sa : candidateSAs) {
if (!isSorcerySpeed(sa)) {
if (!isSorcerySpeed(sa, player)) {
System.err.println("Not sorcery: " + sa);
candidateSAs2.add(sa);
}

View File

@@ -1,9 +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 kind="output" path="target/classes"/>
</classpath>

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.10-SNAPSHOT</version>
<version>1.6.23-SNAPSHOT</version>
</parent>
<artifactId>forge-core</artifactId>
@@ -16,7 +16,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<version>24.1-android</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@@ -24,4 +24,5 @@
<version>3.7</version>
</dependency>
</dependencies>
</project>

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