Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some tests for NPC rules #78524

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions data/mods/TEST_DATA/npc_behavior_arenas/arena_maps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
[
{
"type": "palette",
"id": "npc_behavior_test_palette",
"furniture": { "a": "f_chair" },
"terrain": {
".": "t_thconc_floor",
"X": "t_concrete_wall",
"C": "t_door_c",
"O": "t_door_o",
"u": "t_door_frame",
"W": "t_door_locked"
}
},
{
"type": "mapgen",
"method": "json",
"update_mapgen_id": "debug_npc_rules_test_avoid_doors",
"object": {
"rows": [
"XXXXXXXXXXXXXXXXXXXXXXXX",
"X......................X",
"X......................X",
"X......................X",
"X..........a...........X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"XC.CCCCCCCCCCCCCCCCCCCCX",
"X......................X",
"X......................X",
"XOOOOOOOOOOOOOOOOOOOO.OX",
"X......................X",
"X......................X",
"XXXXXXXXXXXuXXXXXXXXXXXX",
"X......................X",
"X......................X",
"XXXXXXXXXXX.XXXXXXXXXXXX",
"........................",
"........................"
],
"flags": [ "ERASE_ALL_BEFORE_PLACING_TERRAIN" ],
"palettes": [ "npc_behavior_test_palette" ]
}
},
{
"type": "mapgen",
"method": "json",
"update_mapgen_id": "debug_npc_rules_test_close_doors",
"object": {
"rows": [
"XXXXXXXXXXXXXXXXXXXXXXXX",
"X......................X",
"X......................X",
"X......................X",
"X..........a...........X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"X......................X",
"XXXXXXXXXXXOXXXXXXXXXXXX",
"X......................X",
"X......................X",
"XXXXXXXXXXXCXXXXXXXXXXXX",
"X......................X",
"X......................X",
"XXXXXXXXXXXWXXXXXXXXXXXX",
"X......................X",
"X......................X",
"XXXXXXXXXXX.XXXXXXXXXXXX",
"........................",
"........................"
],
"flags": [ "ERASE_ALL_BEFORE_PLACING_TERRAIN" ],
"palettes": [ "npc_behavior_test_palette" ]
}
},
{
"id": "locked_as_hell_car",
"type": "vehicle",
"name": "DEBUG locked car DEBUG",
"blueprint": [
[ "BBDBB" ],
[ "B===B" ],
[ "B===B" ],
[ "B===B" ],
[ "BBBBB" ]
],
"parts": [
{ "x": -1, "y": -1, "parts": [ "frame", "aisle", "roof" ] },
{ "x": 0, "y": -1, "parts": [ "frame", "aisle", "roof" ] },
{ "x": 1, "y": -1, "parts": [ "frame", "aisle", "roof" ] },
{ "x": -1, "y": 0, "parts": [ "frame", "aisle", "roof" ] },
{ "x": 0, "y": 0, "parts": [ "frame", "aisle", "roof" ] },
{ "x": 1, "y": 0, "parts": [ "frame", "aisle", "roof" ] },
{ "x": -1, "y": 1, "parts": [ "frame", "aisle", "roof" ] },
{ "x": 0, "y": 1, "parts": [ "frame", "aisle", "roof" ] },
{ "x": 1, "y": 1, "parts": [ "frame", "aisle", "roof" ] },
{ "x": -1, "y": 2, "parts": [ "frame", "board", "roof" ] },
{ "x": 0, "y": 2, "parts": [ "frame", "board", "roof" ] },
{ "x": 1, "y": 2, "parts": [ "frame", "board", "roof" ] },
{ "x": 2, "y": 2, "parts": [ "frame", "board", "roof" ] },
{ "x": 0, "y": -2, "parts": [ "frame", "door", "roof" ] },
{ "x": 1, "y": -2, "parts": [ "frame", "board", "roof" ] },
{ "x": 2, "y": -1, "parts": [ "frame", "board", "roof" ] },
{ "x": 2, "y": 0, "parts": [ "frame", "board", "roof" ] },
{ "x": 2, "y": 1, "parts": [ "frame", "board", "roof" ] },
{ "x": -2, "y": -1, "parts": [ "frame", "board", "roof" ] },
{ "x": -2, "y": 0, "parts": [ "frame", "board", "roof" ] },
{ "x": -2, "y": -2, "parts": [ "frame", "board", "roof" ] },
{ "x": -1, "y": -2, "parts": [ "frame", "board", "roof" ] },
{ "x": 2, "y": -2, "parts": [ "frame", "board", "roof" ] },
{ "x": -2, "y": 2, "parts": [ "frame", "board", "roof" ] },
{ "x": -2, "y": 1, "parts": [ "frame", "board", "roof" ] }
]
}
]
248 changes: 248 additions & 0 deletions tests/npc_behavior_rules_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

#include "calendar.h"
#include "cata_catch.h"
#include "character.h"
#include "common_types.h"
#include "creature_tracker.h"
#include "faction.h"
#include "field.h"
#include "field_type.h"
#include "game.h"
#include "gates.h"
#include "line.h"
#include "map.h"
#include "map_helpers.h"
#include "mapgen_helpers.h"
#include "memory_fast.h"
#include "npc.h"
#include "npctalk.h"
#include "overmapbuffer.h"
#include "pathfinding.h"
#include "pimpl.h"
#include "player_helpers.h"
#include "point.h"
#include "test_data.h"
#include "text_snippets.h"
#include "type_id.h"
#include "units.h"
#include "veh_type.h"
#include "vehicle.h"
#include "vpart_position.h"

class Creature;

static const update_mapgen_id
update_mapgen_debug_npc_rules_test_avoid_doors( "debug_npc_rules_test_avoid_doors" );

static const update_mapgen_id
update_mapgen_debug_npc_rules_test_close_doors( "debug_npc_rules_test_close_doors" );

static const furn_str_id furn_f_chair( "f_chair" );
static const ter_str_id ter_t_door_c( "t_door_c" );
static const ter_str_id ter_t_door_locked( "t_door_locked" );
static const ter_str_id ter_t_door_o( "t_door_o" );

static const vproto_id veh_locked_as_hell_car( "locked_as_hell_car" );

static shared_ptr_fast<npc> setup_generic_rules_test( ally_rule rule_to_test,
update_mapgen_id update_mapgen_id_to_apply )
{
clear_map();
clear_avatar();
Character &player = get_player_character();
tripoint_bub_ms next_to = player.pos_bub() + point::north;
REQUIRE( next_to != player.pos_bub() );
shared_ptr_fast<npc> guy = make_shared_fast<npc>();
clear_character( *guy );
guy->setpos( next_to );
talk_function::follow( *guy );
// rules don't work unless they're an ally.
REQUIRE( guy->is_player_ally() );
npc_follower_rules &tester_rules = guy->rules;
tester_rules = npc_follower_rules(); // just to be sure
tester_rules.clear_overrides(); // just to be sure
tester_rules.set_flag( rule_to_test );
REQUIRE( tester_rules.has_flag( rule_to_test ) );
const tripoint_abs_omt test_omt_pos = guy->global_omt_location() + point::north;
manual_mapgen( test_omt_pos, manual_update_mapgen, update_mapgen_id_to_apply );
return guy;
}

TEST_CASE( "NPC rules (avoid doors)", "[npc_rules]" )
{
/* Avoid doors rule
* Allows: Door frame, Open doors(??? pre-existing behavior)
* DOES NOT ALLOW: closed door (unlocked)
* Target is a chair in a room fully enclosed by concrete walls
* The straight-line path would take them through a door frame, closed door, and open door. This would fail.
* The legal path winds back and forth through the corridors (snake pattern) but only crosses a door frame,
* already opened doors, and concrete floors.
*/
const ally_rule rule_to_test = ally_rule::avoid_doors;
const shared_ptr_fast<npc> &test_subject = setup_generic_rules_test( rule_to_test,
update_mapgen_debug_npc_rules_test_avoid_doors );
map &here = get_map();
tripoint_bub_ms chair_target = test_subject->pos_bub();
for( const tripoint_bub_ms &furn_loc : here.points_in_radius( test_subject->pos_bub(), 60 ) ) {
if( here.furn( furn_loc ) == furn_f_chair ) {
chair_target = furn_loc;
break;
}
}
// if this fails, we somehow didn't find the destination chair (???)
REQUIRE( test_subject->pos_bub() != chair_target );
test_subject->update_path( chair_target, true, true );
// if this fails, we somehow didn't find a path
REQUIRE( !test_subject->path.empty() );
int path_position = 0;
for( tripoint_bub_ms &loc : test_subject->path ) {
std::string debug_log_msg = string_format( "Terrain at path position %d was %s",
path_position, here.ter( loc ).id().c_str() );
path_position++;
CAPTURE( debug_log_msg );
CHECK( here.ter( loc ).id() != ter_t_door_c );
}
}

TEST_CASE( "NPC rules (close doors)", "[npc_rules]" )
{
/* Close doors rule
* Target is a chair in a room fully enclosed by concrete walls
* We have a straight line path to the target, but in the way are several vexing trials!
* An open door, a closed door, and a locked door (unlockable from adjacent INDOORS tile).
* We must open them all, *and* close them behind us!
*/
const ally_rule rule_to_test = ally_rule::close_doors;
const shared_ptr_fast<npc> &test_subject = setup_generic_rules_test( rule_to_test,
update_mapgen_debug_npc_rules_test_close_doors );
// Some sanity checking to make sure we can even do this test
REQUIRE( !test_subject->rules.has_flag( ally_rule::avoid_doors ) );
REQUIRE( !test_subject->rules.has_flag( ally_rule::avoid_locks ) );
map &here = get_map();


tripoint_bub_ms door_unlock_position;
for( const tripoint_bub_ms &ter_loc : here.points_in_radius( test_subject->pos_bub(), 60 ) ) {
if( here.ter( ter_loc ) == ter_t_door_locked ) {
door_unlock_position = ter_loc + point::south;
break;
}
}
here.set_outside_cache_dirty( door_unlock_position.z() );
here.build_outside_cache( door_unlock_position.z() );
REQUIRE( !here.is_outside( door_unlock_position ) );

tripoint_bub_ms chair_target = test_subject->pos_bub();
for( const tripoint_bub_ms &furn_loc : here.points_in_radius( test_subject->pos_bub(), 60 ) ) {
if( here.furn( furn_loc ) == furn_f_chair ) {
chair_target = furn_loc;
break;
}
}
// if this fails, we somehow didn't find the destination chair (???)
REQUIRE( test_subject->pos_bub() != chair_target );
test_subject->update_path( chair_target, true, true );
// if this fails, we somehow didn't find a path
REQUIRE( !test_subject->path.empty() );

// we must force them to actually walk the path for this test
test_subject->goto_to_this_pos = here.getglobal( test_subject->path.back() );

// copy our path before we lose it
std::vector<tripoint_bub_ms> path_taken = test_subject->path;
int turns_taken = 0;
while( turns_taken++ < 100 && test_subject->pos_bub() != chair_target ) {
test_subject->set_moves( 100 );
test_subject->move();
}

// if one of these fails we didn't make it to the chair, somehow!
CHECK( turns_taken < 100 );
CHECK( test_subject->pos_bub() == chair_target );

for( tripoint_bub_ms &loc : path_taken ) {
// any other terrain on the way is valid, as long as it's not an open door.
// (since test area is spawned *nearby* they might walk over some random dirt, grass, w/e on the way to the test area)
CHECK( here.ter( loc ).id() != ter_t_door_o );
}

}

TEST_CASE( "NPC rules (avoid locks)", "[npc_rules]" )
{
/* Avoid locked doors rule
* Target is a the north side of a locked door (otherwise inaccessible room)
* We can open the door, but our rules forbid it
* Test succeeds if NPC fails to path to other side of door
*
* For the second part, we repeat the test with a vehicle locked door.
* The NPC is placed inside the vehicle for this test.
* In this case, our target is the north side of a locked door (otherwise inaccessible vehicle)
* We can unlock and open the door, but our rules forbid it.
* Test succeeds if NPC fails to path to outside the vehicle.
*/
const ally_rule rule_to_test = ally_rule::avoid_locks;
const shared_ptr_fast<npc> &test_subject = setup_generic_rules_test( rule_to_test,
update_mapgen_debug_npc_rules_test_close_doors );
// Some sanity checking to make sure we can even do this test
REQUIRE( !test_subject->rules.has_flag( ally_rule::avoid_doors ) );
map &here = get_map();


tripoint_bub_ms door_position;
for( const tripoint_bub_ms &ter_loc : here.points_in_radius( test_subject->pos_bub(), 60 ) ) {
if( here.ter( ter_loc ) == ter_t_door_locked ) {
door_position = ter_loc;
break;
}
}
tripoint_bub_ms door_unlock_position = door_position + point::south;
tripoint_bub_ms past_the_door = door_position + point::north;
here.set_outside_cache_dirty( door_position.z() );
here.build_outside_cache( door_position.z() );
REQUIRE( !here.is_outside( door_unlock_position ) );

test_subject->update_path( door_unlock_position, true, true );
REQUIRE( !test_subject->path.empty() ); // we can reach the unlock position
test_subject->path.clear();
test_subject->update_path( past_the_door, true, true );
// FIXME: NPC rules do not consider locked terrain doors, only vehicles
// CHECK( test_subject->path.empty() );
test_subject->path.clear();

const tripoint_bub_ms car_center_pos = test_subject->pos_bub() + tripoint_rel_ms{0, 10, 0};
const tripoint_bub_ms car_door_pos = car_center_pos + point_rel_ms{0, -2};
const tripoint_bub_ms car_door_unlock_pos = car_door_pos + point::south;
const tripoint_bub_ms outside_car_door_pos = car_door_pos + point::north;


// all sides of the vehicle are locked doors
vehicle *test_vehicle = here.add_vehicle( veh_locked_as_hell_car,
car_center_pos, 0_degrees, 0, 0 );

// vehicle is a 5x5 grid, car_door_pos is the only door/exit
std::vector<vehicle_part *> parts_at_target = test_vehicle->get_parts_at(
car_door_pos, "LOCKABLE_DOOR", part_status_flag::available );
REQUIRE( !parts_at_target.empty() );
vehicle_part *door = parts_at_target.front();
door->locked = true;
REQUIRE( ( door->is_available() && door->locked ) );

test_subject->setpos( car_door_unlock_pos );
here.board_vehicle( car_door_unlock_pos, &*test_subject );

CHECK( doors::can_unlock_door( here, *test_subject, car_door_pos ) );

Check failure on line 242 in tests/npc_behavior_rules_test.cpp

View workflow job for this annotation

GitHub Actions / Basic Build and Test (Clang 10, Ubuntu, Curses)

false

test_subject->update_path( outside_car_door_pos, true, true );
CAPTURE( test_subject->path.size() );
CHECK( test_subject->path.empty() );

Check failure on line 246 in tests/npc_behavior_rules_test.cpp

View workflow job for this annotation

GitHub Actions / Basic Build and Test (Clang 10, Ubuntu, Curses)

false

}
Loading