From 158c49371ec23889c382ea50017961229c4b646e Mon Sep 17 00:00:00 2001 From: Kevin Granade Date: Mon, 27 Apr 2020 06:37:07 +0000 Subject: [PATCH] Monster behavior tree (#39810) * Initial integration of behavior tree into monster AI. * Extract character oracle to its own module --- data/json/monsters/monster_goals.json | 27 ++++++ src/behavior_oracle.cpp | 124 ++------------------------ src/behavior_oracle.h | 23 ----- src/character_oracle.cpp | 116 ++++++++++++++++++++++++ src/character_oracle.h | 34 +++++++ src/monmove.cpp | 10 ++- src/monster.cpp | 13 +++ src/monster.h | 2 + src/monster_oracle.cpp | 33 +++++++ src/monster_oracle.h | 29 ++++++ src/monstergenerator.cpp | 9 ++ src/mtype.cpp | 16 ++++ src/mtype.h | 6 ++ src/panels.cpp | 1 + tests/behavior_test.cpp | 21 +++++ 15 files changed, 324 insertions(+), 140 deletions(-) create mode 100644 data/json/monsters/monster_goals.json create mode 100644 src/character_oracle.cpp create mode 100644 src/character_oracle.h create mode 100644 src/monster_oracle.cpp create mode 100644 src/monster_oracle.h diff --git a/data/json/monsters/monster_goals.json b/data/json/monsters/monster_goals.json new file mode 100644 index 0000000000000..43a5d05be11b8 --- /dev/null +++ b/data/json/monsters/monster_goals.json @@ -0,0 +1,27 @@ +[ + { + "type": "behavior", + "id": "monster_goals", + "strategy": "sequential_until_done", + "children": [ "absorb_items", "monster_special" ] + }, + { + "type": "behavior", + "id": "absorb_items", + "strategy": "sequential", + "predicate": "monster_not_hallucination", + "children": [ "do_absorb" ] + }, + { + "type": "behavior", + "id": "do_absorb", + "predicate": "monster_items_available", + "goal": "consume_items" + }, + { + "type": "behavior", + "id": "monster_special", + "predicate": "monster_has_special", + "goal": "do_special" + } +] diff --git a/src/behavior_oracle.cpp b/src/behavior_oracle.cpp index cd37a837a55b9..297e8d93e74f7 100644 --- a/src/behavior_oracle.cpp +++ b/src/behavior_oracle.cpp @@ -1,22 +1,12 @@ #include "behavior_oracle.h" -#include #include -#include -#include +#include +#include #include "behavior.h" -#include "bodypart.h" -#include "character.h" -#include "inventory.h" -#include "item.h" -#include "itype.h" -#include "player.h" -#include "ret_val.h" -#include "value_ptr.h" -#include "weather.h" - -static const std::string flag_FIRESTARTER( "FIRESTARTER" ); +#include "character_oracle.h" +#include "monster_oracle.h" namespace behavior { @@ -26,106 +16,6 @@ status_t return_running( const oracle_t * ) return running; } -// To avoid a local minima when the character has access to warmth in a shelter but gets cold -// when they go outside, this method needs to only alert when travel time to known shelter -// approaches time to freeze. -status_t character_oracle_t::needs_warmth_badly() const -{ - const player *p = dynamic_cast( subject ); - // Use player::temp_conv to predict whether the Character is "in trouble". - for( const body_part bp : all_body_parts ) { - if( p->temp_conv[ bp ] <= BODYTEMP_VERY_COLD ) { - return running; - } - } - return success; -} - -status_t character_oracle_t::needs_water_badly() const -{ - // Check thirst threshold. - if( subject->get_thirst() > 520 ) { - return running; - } - return success; -} - -status_t character_oracle_t::needs_food_badly() const -{ - // Check hunger threshold. - if( subject->get_hunger() >= 300 && subject->get_starvation() > 2500 ) { - return running; - } - return success; -} - -status_t character_oracle_t::can_wear_warmer_clothes() const -{ - const player *p = dynamic_cast( subject ); - // Check inventory for wearable warmer clothes, greedily. - // Don't consider swapping clothes yet, just evaluate adding clothes. - for( const auto &i : subject->inv.const_slice() ) { - const item &candidate = i->front(); - if( candidate.get_warmth() > 0 || p->can_wear( candidate ).success() ) { - return running; - } - } - return failure; -} - -status_t character_oracle_t::can_make_fire() const -{ - // Check inventory for firemaking tools and fuel - bool tool = false; - bool fuel = false; - for( const auto &i : subject->inv.const_slice() ) { - const item &candidate = i->front(); - if( candidate.has_flag( flag_FIRESTARTER ) ) { - tool = true; - if( fuel ) { - return running; - } - } else if( candidate.flammable() ) { - fuel = true; - if( tool ) { - return running; - } - } - } - return success; -} - -status_t character_oracle_t::can_take_shelter() const -{ - // See if we know about some shelter - // Don't know how yet. - return failure; -} - -status_t character_oracle_t::has_water() const -{ - // Check if we know about water somewhere - bool found_water = subject->inv.has_item_with( []( const item & cand ) { - return cand.is_food() && cand.get_comestible()->quench > 0; - } ); - return found_water ? running : failure; -} - -status_t character_oracle_t::has_food() const -{ - // Check if we know about food somewhere - bool found_food = subject->inv.has_item_with( []( const item & cand ) { - return cand.is_food() && cand.get_comestible()->has_calories(); - } ); - return found_food ? running : failure; -} - -// predicate_map doesn't have to live here, but for the time being it's pretty pointless -// to break it out into it's own module. -// In principle this can be populated with any function that has a matching signature. -// In practice each element is a pointer-to-function to one of the above methods so that -// They can have provlidged access to the subject's internals. - // Just a little helper to make populating predicate_map slightly less gross. static std::function < status_t( const oracle_t * ) > make_function( status_t ( character_oracle_t::* fun )() const ) @@ -141,7 +31,11 @@ std::unordered_map> pre { "npc_can_make_fire", make_function( &character_oracle_t::can_make_fire ) }, { "npc_can_take_shelter", make_function( &character_oracle_t::can_take_shelter ) }, { "npc_has_water", make_function( &character_oracle_t::has_water ) }, - { "npc_has_food", make_function( &character_oracle_t::has_food ) } + { "npc_has_food", make_function( &character_oracle_t::has_food ) }, + { "monster_has_special", static_cast( &monster_oracle_t::has_special ) }, + { "monster_not_hallucination", static_cast( &monster_oracle_t::not_hallucination ) }, + { "monster_items_available", static_cast( &monster_oracle_t::items_available ) } } }; + } // namespace behavior diff --git a/src/behavior_oracle.h b/src/behavior_oracle.h index 8f35266a64fef..79171642bb53a 100644 --- a/src/behavior_oracle.h +++ b/src/behavior_oracle.h @@ -6,8 +6,6 @@ #include #include -class Character; - namespace behavior { enum status_t : char; @@ -25,27 +23,6 @@ class oracle_t status_t return_running( const oracle_t * ); -class character_oracle_t : public oracle_t -{ - public: - character_oracle_t( const Character *subject ) { - this->subject = subject; - } - /** - * Predicates used by AI to determine goals. - */ - status_t needs_warmth_badly() const; - status_t needs_water_badly() const; - status_t needs_food_badly() const; - status_t can_wear_warmer_clothes() const; - status_t can_make_fire() const; - status_t can_take_shelter() const; - status_t has_water() const; - status_t has_food() const; - private: - const Character *subject; -}; - extern std::unordered_map> predicate_map; } // namespace behavior diff --git a/src/character_oracle.cpp b/src/character_oracle.cpp new file mode 100644 index 0000000000000..aece481d2d430 --- /dev/null +++ b/src/character_oracle.cpp @@ -0,0 +1,116 @@ +#include +#include +#include + +#include "behavior.h" +#include "character_oracle.h" +#include "bodypart.h" +#include "character.h" +#include "inventory.h" +#include "item.h" +#include "itype.h" +#include "player.h" +#include "ret_val.h" +#include "value_ptr.h" +#include "weather.h" + +static const std::string flag_FIRESTARTER( "FIRESTARTER" ); + +namespace behavior +{ + +// To avoid a local minima when the character has access to warmth in a shelter but gets cold +// when they go outside, this method needs to only alert when travel time to known shelter +// approaches time to freeze. +status_t character_oracle_t::needs_warmth_badly() const +{ + const player *p = dynamic_cast( subject ); + // Use player::temp_conv to predict whether the Character is "in trouble". + for( const body_part bp : all_body_parts ) { + if( p->temp_conv[ bp ] <= BODYTEMP_VERY_COLD ) { + return running; + } + } + return success; +} + +status_t character_oracle_t::needs_water_badly() const +{ + // Check thirst threshold. + if( subject->get_thirst() > 520 ) { + return running; + } + return success; +} + +status_t character_oracle_t::needs_food_badly() const +{ + // Check hunger threshold. + if( subject->get_hunger() >= 300 && subject->get_starvation() > 2500 ) { + return running; + } + return success; +} + +status_t character_oracle_t::can_wear_warmer_clothes() const +{ + const player *p = dynamic_cast( subject ); + // Check inventory for wearable warmer clothes, greedily. + // Don't consider swapping clothes yet, just evaluate adding clothes. + for( const auto &i : subject->inv.const_slice() ) { + const item &candidate = i->front(); + if( candidate.get_warmth() > 0 || p->can_wear( candidate ).success() ) { + return running; + } + } + return failure; +} + +status_t character_oracle_t::can_make_fire() const +{ + // Check inventory for firemaking tools and fuel + bool tool = false; + bool fuel = false; + for( const auto &i : subject->inv.const_slice() ) { + const item &candidate = i->front(); + if( candidate.has_flag( flag_FIRESTARTER ) ) { + tool = true; + if( fuel ) { + return running; + } + } else if( candidate.flammable() ) { + fuel = true; + if( tool ) { + return running; + } + } + } + return success; +} + +status_t character_oracle_t::can_take_shelter() const +{ + // See if we know about some shelter + // Don't know how yet. + return failure; +} + +status_t character_oracle_t::has_water() const +{ + // Check if we know about water somewhere + bool found_water = subject->inv.has_item_with( []( const item & cand ) { + return cand.is_food() && cand.get_comestible()->quench > 0; + } ); + return found_water ? running : failure; +} + +status_t character_oracle_t::has_food() const +{ + // Check if we know about food somewhere + bool found_food = subject->inv.has_item_with( []( const item & cand ) { + return cand.is_food() && cand.get_comestible()->has_calories(); + } ); + return found_food ? running : failure; +} + +} // namespace behavior diff --git a/src/character_oracle.h b/src/character_oracle.h new file mode 100644 index 0000000000000..01781694f20a7 --- /dev/null +++ b/src/character_oracle.h @@ -0,0 +1,34 @@ +#pragma once +#ifndef CATA_SRC_CHARACTER_ORACLE_H +#define CATA_SRC_CHARACTER_ORACLE_H + +#include "behavior_oracle.h" + +class Character; + +namespace behavior +{ + +class character_oracle_t : public oracle_t +{ + public: + character_oracle_t( const Character *subject ) { + this->subject = subject; + } + /** + * Predicates used by AI to determine goals. + */ + status_t needs_warmth_badly() const; + status_t needs_water_badly() const; + status_t needs_food_badly() const; + status_t can_wear_warmer_clothes() const; + status_t can_make_fire() const; + status_t can_take_shelter() const; + status_t has_water() const; + status_t has_food() const; + private: + const Character *subject; +}; + +} //namespace behavior +#endif // CATA_SRC_CHARACTER_ORACLE_H diff --git a/src/monmove.cpp b/src/monmove.cpp index 07505cd7dc267..3bb39fdf6b66b 100644 --- a/src/monmove.cpp +++ b/src/monmove.cpp @@ -13,6 +13,7 @@ #include #include "avatar.h" +#include "behavior.h" #include "bionics.h" #include "cata_utility.h" #include "creature_tracker.h" @@ -30,6 +31,7 @@ #include "memory_fast.h" #include "messages.h" #include "monfaction.h" +#include "monster_oracle.h" #include "mtype.h" #include "npc.h" #include "pathfinding.h" @@ -629,10 +631,14 @@ void monster::move() return; } + behavior::monster_oracle_t oracle( this ); + behavior::tree goals; + goals.add( type->get_goals() ); + std::string action = goals.tick( &oracle ); //The monster can consume objects it stands on. Check if there are any. //If there are. Consume them. - if( !is_hallucination() && ( has_flag( MF_ABSORBS ) || has_flag( MF_ABSORBS_SPLITS ) ) && - !g->m.has_flag( TFLAG_SEALED, pos() ) && g->m.has_items( pos() ) ) { + // TODO: Stick this in a map and dispatch to it via the action string. + if( action == "consume_items" ) { if( g->u.sees( *this ) ) { add_msg( _( "The %s flows around the objects on the floor and they are quickly dissolved!" ), name() ); diff --git a/src/monster.cpp b/src/monster.cpp index 46b17bea90f5f..e8f40fb63b6bd 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -2014,6 +2014,19 @@ void monster::disable_special( const std::string &special_name ) special_attacks.at( special_name ).enabled = false; } +int monster::shortest_special_cooldown() const +{ + int countdown = std::numeric_limits::max(); + for( const std::pair &sp_type : special_attacks ) { + const mon_special_attack &local_attack_data = sp_type.second; + if( !local_attack_data.enabled ) { + continue; + } + countdown = std::min( countdown, local_attack_data.cooldown ); + } + return countdown; +} + void monster::normalize_ammo( const int old_ammo ) { int total_ammo = 0; diff --git a/src/monster.h b/src/monster.h index 455a6acee525e..9d2df09381110 100644 --- a/src/monster.h +++ b/src/monster.h @@ -390,6 +390,8 @@ class monster : public Creature void set_special( const std::string &special_name, int time ); /** Sets the enabled flag for the given special to false */ void disable_special( const std::string &special_name ); + /** Return the lowest cooldown for an enabled special */ + int shortest_special_cooldown() const; void process_turn() override; /** Resets the value of all bonus fields to 0, clears special effect flags. */ diff --git a/src/monster_oracle.cpp b/src/monster_oracle.cpp new file mode 100644 index 0000000000000..88ba91621bb38 --- /dev/null +++ b/src/monster_oracle.cpp @@ -0,0 +1,33 @@ +#include + +#include "behavior.h" +#include "game.h" +#include "map.h" +#include "monster.h" +#include "monster_oracle.h" + +namespace behavior +{ + +status_t monster_oracle_t::has_special() const +{ + if( subject->shortest_special_cooldown() == 0 ) { + return running; + } + return failure; +} + +status_t monster_oracle_t::not_hallucination() const +{ + return subject->is_hallucination() ? failure : running; +} + +status_t monster_oracle_t::items_available() const +{ + if( !g->m.has_flag( TFLAG_SEALED, subject->pos() ) && g->m.has_items( subject->pos() ) ) { + return running; + } + return failure; +} + +} // namespace behavior diff --git a/src/monster_oracle.h b/src/monster_oracle.h new file mode 100644 index 0000000000000..d4452d49e8205 --- /dev/null +++ b/src/monster_oracle.h @@ -0,0 +1,29 @@ +#pragma once +#ifndef CATA_SRC_MONSTER_ORACLE_H +#define CATA_SRC_MONSTER_ORACLE_H + +#include "behavior_oracle.h" + +class monster; + +namespace behavior +{ + +class monster_oracle_t : public oracle_t +{ + public: + monster_oracle_t( const monster *subject ) { + this->subject = subject; + } + /** + * Predicates used by AI to determine goals. + */ + status_t has_special() const; + status_t not_hallucination() const; + status_t items_available() const; + private: + const monster *subject; +}; + +} // namespace behavior +#endif // CATA_SRC_MONSTER_ORACLE_H diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 50fa7c11664ad..3331b85a98edc 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -328,6 +328,14 @@ void load_monster_adjustment( const JsonObject &jsobj ) adjustments.push_back( adj ); } +static void build_behavior_tree( mtype &type ) +{ + type.set_strategy(); + if( type.has_flag( MF_ABSORBS ) || type.has_flag( MF_ABSORBS_SPLITS ) ) { + type.add_goal( "absorb_items" ); + } +} + void MonsterGenerator::finalize_mtypes() { mon_templates->finalize(); @@ -368,6 +376,7 @@ void MonsterGenerator::finalize_mtypes() // Lower bound for hp scaling mon.hp = std::max( mon.hp, 1 ); + build_behavior_tree( mon ); finalize_pathfinding_settings( mon ); } diff --git a/src/mtype.cpp b/src/mtype.cpp index 452e348c8d9b7..b5f102d6f8fa2 100644 --- a/src/mtype.cpp +++ b/src/mtype.cpp @@ -3,6 +3,7 @@ #include #include +#include "behavior_strategy.h" #include "creature.h" #include "field_type.h" #include "item.h" @@ -235,3 +236,18 @@ std::string mtype::get_footsteps() const } return _( "footsteps." ); } + +void mtype::set_strategy() +{ + goals.set_strategy( behavior::strategy_map[ "sequential_until_done" ] ); +} + +void mtype::add_goal( const std::string &goal_id ) +{ + goals.add_child( &string_id( goal_id ).obj() ); +} + +const behavior::node_t *mtype::get_goals() const +{ + return &goals; +} diff --git a/src/mtype.h b/src/mtype.h index 3f1fad40b8031..ff41fad5143f1 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -7,6 +7,7 @@ #include #include +#include "behavior.h" #include "calendar.h" #include "color.h" #include "damage.h" @@ -213,6 +214,8 @@ struct mtype { enum_bitset fear; enum_bitset placate; + behavior::node_t goals; + void add_special_attacks( const JsonObject &jo, const std::string &member_name, const std::string &src ); void remove_special_attacks( const JsonObject &jo, const std::string &member_name, @@ -385,6 +388,9 @@ struct mtype { int get_meat_chunks_count() const; std::string get_description() const; std::string get_footsteps() const; + void set_strategy(); + void add_goal( const std::string &goal_id ); + const behavior::node_t *get_goals() const; // Historically located in monstergenerator.cpp void load( const JsonObject &jo, const std::string &src ); diff --git a/src/panels.cpp b/src/panels.cpp index bf455076ba0af..3e29eebf7bbb2 100644 --- a/src/panels.cpp +++ b/src/panels.cpp @@ -19,6 +19,7 @@ #include "cata_utility.h" #include "catacharset.h" #include "character.h" +#include "character_oracle.h" #include "character_martial_arts.h" #include "color.h" #include "compatibility.h" diff --git a/tests/behavior_test.cpp b/tests/behavior_test.cpp index 8aa66fd8f7175..a7be50e8f1e38 100644 --- a/tests/behavior_test.cpp +++ b/tests/behavior_test.cpp @@ -5,9 +5,12 @@ #include "behavior_oracle.h" #include "behavior_strategy.h" #include "catch/catch.hpp" +#include "character_oracle.h" #include "game.h" #include "item.h" #include "item_location.h" +#include "monster_oracle.h" +#include "mtype.h" #include "npc.h" #include "player.h" #include "map_helpers.h" @@ -176,3 +179,21 @@ TEST_CASE( "check_npc_behavior_tree", "[npc][behavior]" ) CHECK( npc_needs.tick( &oracle ) == "idle" ); } } + +TEST_CASE( "check_monster_behavior_tree", "[monster][behavior]" ) +{ + behavior::tree monster_goals; + monster_goals.add( &string_id( "monster_special" ).obj() ); + monster &test_monster = spawn_test_monster( "mon_zombie", { 5, 5, 0 } ); + for( const std::string &special_name : test_monster.type->special_attacks_names ) { + test_monster.reset_special( special_name ); + } + behavior::monster_oracle_t oracle( &test_monster ); + CHECK( monster_goals.tick( &oracle ) == "idle" ); + SECTION( "Special Attack" ) { + test_monster.set_special( "bite", 0 ); + CHECK( monster_goals.tick( &oracle ) == "do_special" ); + test_monster.set_special( "bite", 1 ); + CHECK( monster_goals.tick( &oracle ) == "idle" ); + } +}