414 lines
16 KiB
C++
414 lines
16 KiB
C++
#include <string>
|
|
#include <vector>
|
|
|
|
#include "cata_catch.h"
|
|
#include "cata_utility.h"
|
|
#include "coordinates.h"
|
|
#include "game.h"
|
|
#include "item.h"
|
|
#include "map_helpers.h"
|
|
#include "mongroup.h"
|
|
#include "mtype.h"
|
|
#include "options.h"
|
|
#include "options_helpers.h"
|
|
#include "player_helpers.h"
|
|
|
|
static const mongroup_id GROUP_PETS( "GROUP_PETS" );
|
|
static const mongroup_id GROUP_PET_DOGS( "GROUP_PET_DOGS" );
|
|
|
|
static const mtype_id mon_null( "mon_null" );
|
|
static const mtype_id mon_test_CBM( "mon_test_CBM" );
|
|
static const mtype_id mon_test_bovine( "mon_test_bovine" );
|
|
static const mtype_id mon_test_non_shearable( "mon_test_non_shearable" );
|
|
static const mtype_id mon_test_shearable( "mon_test_shearable" );
|
|
static const mtype_id mon_test_speed_desc_base( "mon_test_speed_desc_base" );
|
|
static const mtype_id mon_test_speed_desc_base_immobile( "mon_test_speed_desc_base_immobile" );
|
|
static const mtype_id mon_test_zombie_cop( "mon_test_zombie_cop" );
|
|
|
|
static const int max_iters = 10000;
|
|
|
|
static void spawn_x_monsters( int x, const mongroup_id &grp, const std::vector<mtype_id> &yesspawn,
|
|
const std::vector<mtype_id> &nospawn )
|
|
{
|
|
std::set<mtype_id> rand_gets;
|
|
std::set<mtype_id> rand_results;
|
|
calendar::turn = time_point( 1 );
|
|
for( int i = 0; i < x; i++ ) {
|
|
mtype_id tmp_get = MonsterGroupManager::GetRandomMonsterFromGroup( grp );
|
|
if( !tmp_get.is_null() ) {
|
|
rand_gets.emplace( tmp_get );
|
|
}
|
|
|
|
mtype_id tmp_res = MonsterGroupManager::GetResultFromGroup( grp ).front().name;
|
|
if( !tmp_res.is_null() ) {
|
|
rand_results.emplace( tmp_res );
|
|
}
|
|
}
|
|
if( nospawn.empty() ) {
|
|
CHECK( !rand_gets.empty() );
|
|
CHECK( !rand_results.empty() );
|
|
}
|
|
if( yesspawn.empty() ) {
|
|
CHECK( rand_gets.empty() );
|
|
CHECK( rand_results.empty() );
|
|
}
|
|
for( const mtype_id &m : nospawn ) {
|
|
CHECK( rand_gets.find( m ) == rand_gets.end() );
|
|
CHECK( rand_results.find( m ) == rand_results.end() );
|
|
}
|
|
for( const mtype_id &m : yesspawn ) {
|
|
CHECK( rand_gets.find( m ) != rand_gets.end() );
|
|
CHECK( rand_results.find( m ) != rand_results.end() );
|
|
}
|
|
}
|
|
|
|
TEST_CASE( "Event-based monster spawns do not spawn outside event", "[monster][mongroup]" )
|
|
{
|
|
override_option ev_spawn_opt( "EVENT_SPAWNS", "monsters" );
|
|
REQUIRE( get_option<std::string>( "EVENT_SPAWNS" ) == "monsters" );
|
|
|
|
GIVEN( "a monster group with entries from different events" ) {
|
|
mongroup_id test_group( "test_event_mongroup" );
|
|
REQUIRE( test_group->monsters.size() == 6 );
|
|
|
|
WHEN( "current holiday == none" ) {
|
|
holiday cur_event = get_holiday_from_time( 1614574800L, true );
|
|
REQUIRE( cur_event == holiday::none );
|
|
THEN( "only spawn non-event monsters" ) {
|
|
std::vector<mtype_id> mons = MonsterGroupManager::GetMonstersFromGroup( test_group, true );
|
|
CHECK( mons.size() == 1 );
|
|
CHECK( mons[0] == mon_test_speed_desc_base_immobile );
|
|
|
|
const std::vector<mtype_id> ylist = { mon_test_speed_desc_base_immobile };
|
|
const std::vector<mtype_id> nlist = { mon_test_speed_desc_base, mon_test_shearable, mon_test_non_shearable, mon_test_bovine, mon_test_CBM };
|
|
spawn_x_monsters( max_iters, test_group, ylist, nlist );
|
|
}
|
|
}
|
|
|
|
WHEN( "current holiday == christmas" ) {
|
|
holiday cur_event = get_holiday_from_time( 1640408400L, true );
|
|
REQUIRE( cur_event == holiday::christmas );
|
|
THEN( "only spawn christmas and non-event monsters" ) {
|
|
std::vector<mtype_id> mons = MonsterGroupManager::GetMonstersFromGroup( test_group, true );
|
|
CHECK( mons.size() == 4 );
|
|
|
|
const std::vector<mtype_id> ylist = { mon_test_speed_desc_base_immobile, mon_test_non_shearable, mon_test_shearable, mon_test_bovine };
|
|
const std::vector<mtype_id> nlist = { mon_test_speed_desc_base, mon_test_CBM };
|
|
spawn_x_monsters( max_iters, test_group, ylist, nlist );
|
|
}
|
|
}
|
|
|
|
WHEN( "current holiday == halloween" ) {
|
|
holiday cur_event = get_holiday_from_time( 1635652800L, true );
|
|
REQUIRE( cur_event == holiday::halloween );
|
|
THEN( "only spawn halloween and non-event monsters" ) {
|
|
std::vector<mtype_id> mons = MonsterGroupManager::GetMonstersFromGroup( test_group, true );
|
|
CHECK( mons.size() == 3 );
|
|
|
|
const std::vector<mtype_id> ylist = { mon_test_speed_desc_base_immobile, mon_test_CBM, mon_test_speed_desc_base };
|
|
const std::vector<mtype_id> nlist = { mon_test_shearable, mon_test_non_shearable, mon_test_bovine };
|
|
spawn_x_monsters( max_iters, test_group, ylist, nlist );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE( "Event-based monsters from an event-only mongroup", "[monster][mongroup]" )
|
|
{
|
|
override_option ev_spawn_opt( "EVENT_SPAWNS", "monsters" );
|
|
REQUIRE( get_option<std::string>( "EVENT_SPAWNS" ) == "monsters" );
|
|
|
|
GIVEN( "a monster group with entries from a single event" ) {
|
|
mongroup_id test_group( "test_event_only" );
|
|
REQUIRE( test_group->monsters.size() == 3 );
|
|
|
|
WHEN( "current holiday == none" ) {
|
|
holiday cur_event = get_holiday_from_time( 1614574800L, true );
|
|
REQUIRE( cur_event == holiday::none );
|
|
THEN( "don't spawn any monsters" ) {
|
|
std::vector<mtype_id> mons = MonsterGroupManager::GetMonstersFromGroup( test_group, true );
|
|
CHECK( mons.empty() );
|
|
|
|
const std::vector<mtype_id> ylist = {};
|
|
const std::vector<mtype_id> nlist = { mon_test_non_shearable, mon_test_shearable, mon_test_bovine };
|
|
spawn_x_monsters( max_iters, test_group, ylist, nlist );
|
|
}
|
|
}
|
|
|
|
WHEN( "current holiday == christmas" ) {
|
|
holiday cur_event = get_holiday_from_time( 1640408400L, true );
|
|
REQUIRE( cur_event == holiday::christmas );
|
|
THEN( "spawn all monsters" ) {
|
|
std::vector<mtype_id> mons = MonsterGroupManager::GetMonstersFromGroup( test_group, true );
|
|
CHECK( mons.size() == 3 );
|
|
|
|
const std::vector<mtype_id> ylist = { mon_test_non_shearable, mon_test_shearable, mon_test_bovine };
|
|
const std::vector<mtype_id> nlist = {};
|
|
spawn_x_monsters( max_iters, test_group, ylist, nlist );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE( "Using mon_null as mongroup default monster", "[mongroup]" )
|
|
{
|
|
mongroup_id test_group1( "test_default_monster" );
|
|
mongroup_id test_group2( "test_group_default_monster" );
|
|
mongroup_id test_group3( "test_event_only" );
|
|
mongroup_id test_group4( "test_event_mongroup" );
|
|
CHECK( test_group1->defaultMonster == mon_null );
|
|
CHECK( test_group2->defaultMonster == mon_null );
|
|
CHECK( test_group3->defaultMonster == mon_null );
|
|
CHECK( test_group4->defaultMonster != mon_null );
|
|
}
|
|
|
|
TEST_CASE( "Nested monster groups spawn chance", "[mongroup]" )
|
|
{
|
|
mongroup_id mg( "test_top_level_mongroup" );
|
|
|
|
const int iters = 10000;
|
|
|
|
// < result, < layer, expected prob, count > >
|
|
std::map<mtype_id, std::tuple<int, float, int>> results {
|
|
// top layer - 33.3%
|
|
{ mon_test_CBM, { 0, 1.f / 3.f, 0 } },
|
|
{ mon_test_bovine, { 0, 1.f / 3.f, 0 } },
|
|
// nested layer 1 - 15.9% (47.6%)
|
|
{ mon_test_shearable, { 1, ( 1.f / 3.f ) *( 50.f / 105.f ), 0 } },
|
|
{ mon_test_non_shearable, { 1, ( 1.f / 3.f ) *( 50.f / 105.f ), 0 } },
|
|
// nested layer 2 - 0.8% (2.4% (50%))
|
|
{ mon_test_speed_desc_base, { 2, ( ( 1.f / 3.f ) * ( 5.f / 105.f ) ) * 0.5f, 0 } },
|
|
{ mon_test_speed_desc_base_immobile, { 2, ( ( 1.f / 3.f ) * ( 5.f / 105.f ) ) * 0.5f, 0 } }
|
|
};
|
|
|
|
// < layer, < expected prob, count > >
|
|
std::map<int, std::pair<float, int>> layers {
|
|
// top layer - 66.7%
|
|
{ 0, { 2.f / 3.f, 0 } },
|
|
// nested layer 1 - 31.7% (95.2%)
|
|
{ 1, { ( 1.f / 3.f ) *( 100.f / 105.f ), 0 } },
|
|
// nested layer 2 - 1.6% (4.8% (100%))
|
|
{ 2, { ( 1.f / 3.f ) *( 5.f / 105.f ), 0 } }
|
|
};
|
|
|
|
calendar::turn += 1_turns;
|
|
|
|
for( int i = 0; i < iters; i++ ) {
|
|
MonsterGroupResult res = MonsterGroupManager::GetResultFromGroup( mg ).front();
|
|
auto iter = results.find( res.name );
|
|
CAPTURE( res.name.c_str() );
|
|
REQUIRE( iter != results.end() );
|
|
if( iter != results.end() ) {
|
|
layers[std::get<0>( iter->second )].second++;
|
|
std::get<2>( results[res.name] )++;
|
|
}
|
|
}
|
|
|
|
for( const auto &lyr : layers ) {
|
|
INFO( string_format( "layer %d - expected vs. actual", lyr.first ) );
|
|
CHECK( lyr.second.first ==
|
|
Approx( static_cast<float>( lyr.second.second ) / iters ).epsilon( 0.5 ) );
|
|
}
|
|
|
|
for( const auto &res : results ) {
|
|
INFO( string_format( "monster %s - expected vs. actual", res.first.c_str() ) );
|
|
CHECK( std::get<1>( res.second ) ==
|
|
Approx( static_cast<float>( std::get<2>( res.second ) ) / iters ).epsilon( 0.5 ) );
|
|
}
|
|
}
|
|
|
|
TEST_CASE( "Nested monster group pack size", "[mongroup]" )
|
|
{
|
|
const int iters = 100;
|
|
calendar::turn += 1_turns;
|
|
|
|
SECTION( "Nested group pack size used as-is" ) {
|
|
mongroup_id mg( "test_top_level_no_packsize" );
|
|
for( int i = 0; i < iters; i++ ) {
|
|
bool found = false;
|
|
std::vector<MonsterGroupResult> res =
|
|
MonsterGroupManager::GetResultFromGroup( mg, nullptr, &found );
|
|
REQUIRE( found );
|
|
|
|
// pack_size == [2, 4] * 1
|
|
CHECK( res.size() == 1 );
|
|
int total = 0;
|
|
for( const MonsterGroupResult &mgr : res ) {
|
|
total += mgr.pack_size;
|
|
}
|
|
CHECK( total == Approx( 3 ).margin( 1 ) );
|
|
}
|
|
}
|
|
|
|
SECTION( "Nested group pack size multiplied by top level pack size" ) {
|
|
mongroup_id mg( "test_top_level_packsize" );
|
|
for( int i = 0; i < iters; i++ ) {
|
|
bool found = false;
|
|
std::vector<MonsterGroupResult> res =
|
|
MonsterGroupManager::GetResultFromGroup( mg, nullptr, &found );
|
|
REQUIRE( found );
|
|
|
|
// pack_size == [2, 4] * [4, 6]
|
|
CHECK( res.size() == Approx( 5 ).margin( 1 ) );
|
|
int total = 0;
|
|
for( const MonsterGroupResult &mgr : res ) {
|
|
total += mgr.pack_size;
|
|
}
|
|
CHECK( total == Approx( 16 ).margin( 8 ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE( "mongroup_sets_quantity_correctly", "[mongroup]" )
|
|
{
|
|
mongroup_id mg = GENERATE( GROUP_PET_DOGS, GROUP_PETS );
|
|
CAPTURE( mg );
|
|
|
|
int quantity = 10;
|
|
bool found = false;
|
|
std::vector<MonsterGroupResult> res =
|
|
MonsterGroupManager::GetResultFromGroup( mg, &quantity, &found );
|
|
CHECK( found );
|
|
CHECK( 10 - quantity == static_cast<int>( res.size() ) );
|
|
}
|
|
|
|
static void test_multi_spawn( const mtype_id &old_mon, int range, int min, int max,
|
|
const time_point &start, const std::set<mtype_id> &new_mons,
|
|
std::map<mtype_id, int> &new_count )
|
|
{
|
|
const int upgrade_attempts = 100;
|
|
clear_avatar();
|
|
|
|
for( int i = 0; i < upgrade_attempts; i++ ) {
|
|
clear_map();
|
|
map &m = get_map();
|
|
calendar::turn = start;
|
|
const tripoint ground_zero = get_player_character().pos() - tripoint( 5, 5, 0 );
|
|
|
|
monster *orig = g->place_critter_at( old_mon, ground_zero );
|
|
REQUIRE( orig );
|
|
REQUIRE( orig->type->id == old_mon );
|
|
REQUIRE( orig->pos() == ground_zero );
|
|
REQUIRE( orig->can_upgrade() );
|
|
|
|
// monster::next_upgrade_time has a ~3% chance to outright fail
|
|
// so keep trying until we succeed
|
|
orig->try_upgrade( false );
|
|
if( orig->type->id == old_mon ) {
|
|
continue;
|
|
}
|
|
|
|
REQUIRE( new_mons.count( orig->type->id ) > 0 );
|
|
int total_spawns = 0;
|
|
for( const Creature *c : m.get_creatures_in_radius( ground_zero, 10 ) ) {
|
|
if( !c->is_monster() || new_mons.count( c->as_monster()->type->id ) == 0 ) {
|
|
continue;
|
|
}
|
|
total_spawns++;
|
|
CHECK( std::abs( c->pos().x - ground_zero.x ) <= range );
|
|
CHECK( std::abs( c->pos().y - ground_zero.y ) <= range );
|
|
if( new_count.count( c->as_monster()->type->id ) == 0 ) {
|
|
new_count[c->as_monster()->type->id] = 0;
|
|
}
|
|
new_count[c->as_monster()->type->id]++;
|
|
}
|
|
CHECK( total_spawns == Approx( ( max + min ) / 2 ).margin( ( max - min ) / 2 ) );
|
|
}
|
|
}
|
|
|
|
TEST_CASE( "mongroup_multi_spawn_upgrades", "[mongroup]" )
|
|
{
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_non_shearable, 5, 4, 6,
|
|
calendar::turn_zero + 1_days, { mon_test_shearable }, counts );
|
|
|
|
CHECK( counts.size() == 1 );
|
|
CHECK( counts.count( mon_test_shearable ) > 0 );
|
|
}
|
|
|
|
TEST_CASE( "mongroup_multi_spawn_restrictions", "[mongroup]" )
|
|
{
|
|
const std::set<mtype_id> all_types { mon_test_shearable, mon_test_CBM, mon_test_zombie_cop };
|
|
|
|
SECTION( "no valid spawns" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_bovine, 5, 4, 6, calendar::turn_zero + 4 * 3_days,
|
|
all_types, counts );
|
|
|
|
CHECK( counts.empty() );
|
|
}
|
|
|
|
SECTION( "1 valid spawn, with DAY" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_bovine, 5, 4, 6, calendar::turn_zero + 4 * 3_days + 12_hours,
|
|
all_types, counts );
|
|
|
|
CHECK( counts.size() == 1 );
|
|
CHECK( counts.count( mon_test_zombie_cop ) > 0 );
|
|
}
|
|
|
|
SECTION( "1 valid spawn, with ends" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_bovine, 5, 4, 6, calendar::turn_zero + 1_days,
|
|
all_types, counts );
|
|
|
|
CHECK( counts.size() == 1 );
|
|
CHECK( counts.count( mon_test_CBM ) > 0 );
|
|
}
|
|
|
|
SECTION( "2 valid spawn, with ends and DAY" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_bovine, 5, 4, 6, calendar::turn_zero + 1_days + 12_hours,
|
|
all_types, counts );
|
|
|
|
CHECK( counts.size() == 2 );
|
|
CHECK( counts.count( mon_test_CBM ) > 0 );
|
|
CHECK( counts.count( mon_test_zombie_cop ) > 0 );
|
|
}
|
|
|
|
SECTION( "1 valid spawn, with starts" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_bovine, 5, 4, 6, calendar::turn_zero + 4 * 8_days,
|
|
all_types, counts );
|
|
|
|
CHECK( counts.size() == 1 );
|
|
CHECK( counts.count( mon_test_shearable ) > 0 );
|
|
}
|
|
|
|
SECTION( "2 valid spawn, with starts and DAY" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_bovine, 5, 4, 6, calendar::turn_zero + 4 * 8_days + 12_hours,
|
|
all_types, counts );
|
|
|
|
CHECK( counts.size() == 2 );
|
|
CHECK( counts.count( mon_test_shearable ) > 0 );
|
|
CHECK( counts.count( mon_test_zombie_cop ) > 0 );
|
|
}
|
|
|
|
SECTION( "no valid spawns, with mon_null" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_CBM, 5, 4, 6, calendar::turn_zero + 1_days,
|
|
{ mon_null, mon_test_CBM, mon_test_zombie_cop }, counts );
|
|
|
|
CHECK( counts.empty() );
|
|
}
|
|
|
|
SECTION( "1 valid spawn, with mon_null" ) {
|
|
std::map<mtype_id, int> counts;
|
|
|
|
test_multi_spawn( mon_test_CBM, 5, 4, 6, calendar::turn_zero + 4 * 3_days + 12_hours,
|
|
{ mon_null, mon_test_CBM, mon_test_zombie_cop }, counts );
|
|
|
|
CHECK( counts.size() == 1 );
|
|
CHECK( counts.count( mon_test_zombie_cop ) > 0 );
|
|
}
|
|
}
|