Skip to content

Commit

Permalink
item: improve the effective dps display
Browse files Browse the repository at this point in the history
Don't calculate dps for monsters that aren't going to be displayed.

Adjust the unit tests based on relative values (epsilon) instead of
fixed values (margin).
  • Loading branch information
mlangsdorf committed Apr 14, 2020
1 parent c5cd16e commit 7b9d959
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 92 deletions.
137 changes: 88 additions & 49 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1265,20 +1265,20 @@ static void insert_separation_line( std::vector<iteminfo> &info )
}

/*
* 0 based lookup table of accuracy - monster defense converted into number of hits per 1000
* 0 based lookup table of accuracy - monster defense converted into number of hits per 10000
* attacks
* data painstakingly looked up at http://onlinestatbook.com/2/calculators/normal_dist.html
*/
static const double hits_by_accuracy[41] = {
0.0, 0.1, 0.2, 0.3, 0.7, // -20 to -16
1.3, 2.6, 4.7, 8.2, 13.9, // -15 to -11
22.8, 35.9, 54.8, 80.8, 115.1, // -10 to -6
158.7, 211.9, 274.3, 344.6, 420.7, // -5 to -1
500, // 0
579.3, 655.4, 725.7, 788.1, 841.3, // 1 to 5
884.9, 919.2, 945.2, 964.1, 977.2, // 6 to 10
986.1, 991.8, 995.3, 997.4, 998.7, // 11 to 15
999.3, 999.7, 999.8, 999.9, 1000.0 // 16 to 20
0, 1, 2, 3, 7, // -20 to -16
13, 26, 47, 82, 139, // -15 to -11
228, 359, 548, 808, 1151, // -10 to -6
1587, 2119, 2743, 3446, 4207, // -5 to -1
5000, // 0
5793, 6554, 7257, 7881, 8413, // 1 to 5
8849, 9192, 9452, 9641, 9772, // 6 to 10
9861, 9918, 9953, 9974, 9987, // 11 to 15
9993, 9997, 9998, 9999, 10000 // 16 to 20
};

double item::effective_dps( const player &guy, monster &mon ) const
Expand All @@ -1287,62 +1287,87 @@ double item::effective_dps( const player &guy, monster &mon ) const
float base_hit = guy.get_dex() / 4.0f + guy.get_hit_weapon( *this );
base_hit *= std::max( 0.25f, 1.0f - guy.encumb( bp_torso ) / 100.0f );
float mon_defense = mon_dodge + mon.size_melee_penalty() / 5.0;
constexpr double hit_trials = 1000.0;
// a hit occurs when a normal distribution with a mean value of base_hit * 5 and a
// standard deviation of 25, minus 5 * monster defense, is greater than 0
// to shorten the lookup table, divide everything by 5 and simplify
// hit occurs when normal( acc - monster_defense, 5 ) > 0
constexpr double hit_trials = 10000.0;
const int rng_mean = std::max( std::min( static_cast<int>( base_hit - mon_defense ), 20 ),
-20 ) + 20;
double num_hits = hits_by_accuracy[ rng_mean ];
double num_all_hits = hits_by_accuracy[ rng_mean ];
/* critical hits have two chances to occur: triple critical hits happen much less frequently,
* and double critical hits can only occur if a hit roll is more than 1.5 * monster dodge.
* Not the hit roll used to determine the attack, another one.
* the way the math works, some percentage of the total hits are eligible to be double
* critical hits, and the rest are eligible to be triple critical hits, but in each case,
* only some small percent of them actually become critical hits.
*/
const int rng_high_mean = std::max( std::min( static_cast<int>( base_hit - 1.5 * mon_dodge ),
20 ), -20 ) + 20;
double num_high_hits = hits_by_accuracy[ rng_high_mean ] * num_all_hits / hit_trials;
double double_crit_chance = guy.crit_chance( 4, 0, *this );
double crit_chance = guy.crit_chance( 0, 0, *this );
double num_low_hits = std::max( 0.0, num_all_hits - num_high_hits );

double moves_per_attack = guy.attack_speed( *this );
// attacks that miss do no damage but take time
double total_moves = ( hit_trials - num_hits ) * moves_per_attack;
double total_moves = ( hit_trials - num_all_hits ) * moves_per_attack;
double total_damage = 0.0;
double num_crits = std::min( hit_trials * guy.crit_chance( base_hit * 5.0,
mon_dodge * 5.0, *this ), num_hits );
double num_crits = std::min( num_low_hits * crit_chance + num_high_hits * double_crit_chance,
num_all_hits );
// critical hits are counted separately
num_hits -= num_crits;

double num_hits = num_all_hits - num_crits;
// sum average damage past armor and return the number of moves required to achieve
// that damage
const auto calc_effective_damage = [ &, moves_per_attack]( int num_hits, bool crit,
const player & guy, monster & mon ) {
const auto calc_effective_damage = [ &, moves_per_attack]( const double num_strikes,
const bool crit, const player & guy, monster & mon ) {
monster temp_mon = mon;
double subtotal_damage = 0;
damage_instance base_damage;
guy.roll_all_damage( crit, base_damage, true, *this );
damage_instance dealt_damage = base_damage;
mon.absorb_hit( bp_torso, dealt_damage );
double damage_per_hit = 0;
temp_mon.absorb_hit( bp_torso, dealt_damage );
dealt_damage_instance dealt_dams;
for( const damage_unit &dmg_unit : dealt_damage.damage_units ) {
damage_per_hit += dmg_unit.amount + dmg_unit.damage_multiplier;
int cur_damage = 0;
int total_pain = 0;
temp_mon.deal_damage_handle_type( dmg_unit, bp_torso, cur_damage, total_pain );
if( cur_damage > 0 ) {
dealt_dams.dealt_dams[ dmg_unit.type ] += cur_damage;
}
}
double subtotal_damage = damage_per_hit * num_hits;
double subtotal_moves = moves_per_attack * num_hits;
double damage_per_hit = dealt_dams.total_damage();
subtotal_damage = damage_per_hit * num_strikes;
double subtotal_moves = moves_per_attack * num_strikes;

if( has_technique( rapid_strike ) ) {
damage_instance dealt_rs_damage = base_damage;
monster temp_rs_mon = mon;
damage_instance rs_base_damage;
guy.roll_all_damage( crit, rs_base_damage, true, *this );
damage_instance dealt_rs_damage = rs_base_damage;
for( damage_unit &dmg_unit : dealt_rs_damage.damage_units ) {
dmg_unit.damage_multiplier *= 0.66;
}
mon.absorb_hit( bp_torso, dealt_rs_damage );
double rs_damage_per_hit = 0;
temp_rs_mon.absorb_hit( bp_torso, dealt_rs_damage );
dealt_damage_instance rs_dealt_dams;
for( const damage_unit &dmg_unit : dealt_rs_damage.damage_units ) {
rs_damage_per_hit += dmg_unit.amount + dmg_unit.damage_multiplier;
int cur_damage = 0;
int total_pain = 0;
temp_rs_mon.deal_damage_handle_type( dmg_unit, bp_torso, cur_damage, total_pain );
if( cur_damage > 0 ) {
rs_dealt_dams.dealt_dams[ dmg_unit.type ] += cur_damage;
}
}
// assume half of hits turn into rapid strikes
double rs_damage_per_hit = rs_dealt_dams.total_damage();
subtotal_moves *= 0.5;
subtotal_damage *= 0.5;
subtotal_moves += moves_per_attack * num_hits * 0.33;
subtotal_damage += rs_damage_per_hit * num_hits * 0.5;
subtotal_moves += moves_per_attack * num_strikes * 0.33;
subtotal_damage += rs_damage_per_hit * num_strikes * 0.5;
}
return std::make_pair( subtotal_moves, subtotal_damage );
};
std::pair<double, double> crit_summary = calc_effective_damage( num_crits, true, guy, mon );
total_moves += crit_summary.first;
total_damage += crit_summary.second;
std::pair<double, double> summary = calc_effective_damage( num_hits, false, guy, mon );
total_moves += summary.first;
total_damage += summary.second;
summary = calc_effective_damage( num_crits, true, guy, mon );
total_moves += summary.first;
total_damage += summary.second;
return total_damage * to_moves<double>( 1_seconds ) / total_moves;
}

Expand All @@ -1359,19 +1384,34 @@ static const std::vector<std::pair<translation, dps_comp_data>> dps_comp_monster
{ to_translation( "Vs. Mixed" ), { mtype_id( "mon_zombie_survivor" ), false, true } },
};

std::map<std::string, double> item::dps( const player &guy ) const
std::map<std::string, double> item::dps( const bool for_display, const bool for_calc,
const player &guy ) const
{
std::map<std::string, double> results;
for( const std::pair<translation, dps_comp_data> &comp_mon : dps_comp_monsters ) {
if( ( comp_mon.second.display != for_display ) &&
( comp_mon.second.evaluate != for_calc ) ) {
continue;
}
monster test_mon = monster( comp_mon.second.mon_id );
results[ comp_mon.first.translated() ] = effective_dps( guy, test_mon );
}
return results;
}

std::map<std::string, double> item::dps() const
std::map<std::string, double> item::dps( const bool for_display, const bool for_calc ) const
{
return dps( for_display, for_calc, g->u );
}

double item::average_dps( const player &guy ) const
{
return dps( g->u );
double dmg_count = 0.0;
const std::map<std::string, double> &dps_data = dps( false, true, guy );
for( const std::pair<std::string, double> &dps_entry : dps_data ) {
dmg_count += dps_entry.second;
}
return dmg_count / dps_data.size();
}

void item::basic_info( std::vector<iteminfo> &info, const iteminfo_query *parts, int batch,
Expand Down Expand Up @@ -3167,16 +3207,15 @@ void item::combat_info( std::vector<iteminfo> &info, const iteminfo_query *parts
info.push_back( iteminfo( "BASE", _( "Moves per attack: " ), "",
iteminfo::lower_is_better, attack_time() ) );
info.emplace_back( "BASE", _( "Typical damage per second:" ), "" );
const std::map<std::string, double> &dps_data = dps();
for( const std::pair<translation, dps_comp_data> &ref_data : dps_comp_monsters ) {
const auto dps_entry = dps_data.find( ref_data.first.translated() );
if( ( dps_entry == dps_data.end() ) || !ref_data.second.display ) {
continue;
}
info.emplace_back( "BASE", space + dps_entry->first + ": ", "",
const std::map<std::string, double> &dps_data = dps( true, false );
std::string sep;
for( const std::pair<std::string, double> &dps_entry : dps_data ) {
info.emplace_back( "BASE", sep + dps_entry.first + ": ", "",
iteminfo::no_newline | iteminfo::is_decimal,
dps_entry->second );
dps_entry.second );
sep = space;
}
info.emplace_back( "BASE", "" );
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/item.h
Original file line number Diff line number Diff line change
Expand Up @@ -568,9 +568,15 @@ class item : public visitable<item>
/**
* calculate effective dps against a stock set of monsters. by default, assume g->u
* is wielding
*/
std::map<std::string, double> dps( const player &guy ) const;
std::map<std::string, double> dps() const;
* for_display - include monsters intended for display purposes
* for_calc - include monsters intended for evaluation purposes
* for_display and for_calc are inclusive
*/
std::map<std::string, double> dps( bool for_display, bool for_calc, const player &guy ) const;
std::map<std::string, double> dps( bool for_display, bool for_calc ) const;
/** return the average dps of the weapon against evaluation monsters */
double average_dps( const player &guy ) const;

/**
* Whether the character needs both hands to wield this item.
*/
Expand Down
71 changes: 40 additions & 31 deletions tests/effective_dps_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ static void check_actual_dps( avatar &attacker, monster &defender, item &weapon
clear_character( attacker );
double expect_dps = weapon.effective_dps( attacker, defender );
double actual_dps = weapon_dps_trials( attacker, defender, weapon );
CHECK( actual_dps == Approx( expect_dps ).margin( 0.01f ) );
CHECK( actual_dps == Approx( expect_dps ).epsilon( 0.35f ) );
}

static void check_accuracy_dps( avatar &attacker, monster &defender, item &wpn1, item &wpn2,
Expand Down Expand Up @@ -97,30 +97,30 @@ TEST_CASE( "effective damage per second", "[effective][dps]" )
avatar &dummy = g->u;
clear_character( dummy );

item rock( "test_rock" );
item plank( "test_2x4" );
item knife( "knife_trench" );
item clumsy_pipe( "test_clumsy_pipe" );
item medium_pipe( "test_medium_pipe" );
item sharp_pipe( "test_sharp_pipe" );

SECTION( "against a debug monster with no armor or dodge" ) {
monster mummy( mtype_id( "debug_mon" ) );

CHECK( plank.effective_dps( dummy, mummy ) == Approx( 4.79f ).margin( 0.01f ) );
CHECK( knife.effective_dps( dummy, mummy ) == Approx( 20.16f ).margin( 0.01f ) );
CHECK( clumsy_pipe.effective_dps( dummy, mummy ) == Approx( 7.28f ).epsilon( 0.05f ) );
CHECK( sharp_pipe.effective_dps( dummy, mummy ) == Approx( 22.51f ).epsilon( 0.05f ) );
}

SECTION( "against an agile target" ) {
monster smoker( mtype_id( "mon_zombie_smoker" ) );
REQUIRE( smoker.get_dodge() >= 4 );

CHECK( plank.effective_dps( dummy, smoker ) == Approx( 2.51f ).margin( 0.01f ) );
CHECK( knife.effective_dps( dummy, smoker ) == Approx( 10.57f ).margin( 0.01f ) );
CHECK( clumsy_pipe.effective_dps( dummy, smoker ) == Approx( 3.04f ).epsilon( 0.05f ) );
CHECK( sharp_pipe.effective_dps( dummy, smoker ) == Approx( 14.02f ).epsilon( 0.05f ) );
}

SECTION( "against an armored target" ) {
monster soldier( mtype_id( "mon_zombie_soldier" ) );

CHECK( plank.effective_dps( dummy, soldier ) == Approx( 1.11f ).margin( 0.01f ) );
CHECK( knife.effective_dps( dummy, soldier ) == Approx( 2.22f ).margin( 0.01f ) );
CHECK( clumsy_pipe.effective_dps( dummy, soldier ) == Approx( 2.19f ).epsilon( 0.05f ) );
CHECK( sharp_pipe.effective_dps( dummy, soldier ) == Approx( 5.70f ).epsilon( 0.05f ) );
}

SECTION( "effect of STR and DEX on damage per second" ) {
Expand All @@ -130,27 +130,27 @@ TEST_CASE( "effective damage per second", "[effective][dps]" )
dummy.str_max = 6;
dummy.dex_max = 6;

CHECK( rock.effective_dps( dummy, mummy ) == Approx( 6.55f ).margin( 0.01f ) );
CHECK( plank.effective_dps( dummy, mummy ) == Approx( 3.60f ).margin( 0.01f ) );
CHECK( knife.effective_dps( dummy, mummy ) == Approx( 16.66f ).margin( 0.01f ) );
CHECK( clumsy_pipe.effective_dps( dummy, mummy ) == Approx( 6.73f ).epsilon( 0.05f ) );
CHECK( medium_pipe.effective_dps( dummy, mummy ) == Approx( 10.93f ).epsilon( 0.05f ) );
CHECK( sharp_pipe.effective_dps( dummy, mummy ) == Approx( 19.00f ).epsilon( 0.05f ) );
}

SECTION( "STR 8, DEX 8" ) {
SECTION( "STR 8, DEX 10" ) {
dummy.str_max = 8;
dummy.dex_max = 8;
dummy.dex_max = 10;

CHECK( rock.effective_dps( dummy, mummy ) == Approx( 8.84f ).margin( 0.01f ) );
CHECK( plank.effective_dps( dummy, mummy ) == Approx( 4.79f ).margin( 0.01f ) );
CHECK( knife.effective_dps( dummy, mummy ) == Approx( 20.16f ).margin( 0.01f ) );
CHECK( clumsy_pipe.effective_dps( dummy, mummy ) == Approx( 7.0f ).epsilon( 0.05f ) );
CHECK( medium_pipe.effective_dps( dummy, mummy ) == Approx( 13.15f ).epsilon( 0.05f ) );
CHECK( sharp_pipe.effective_dps( dummy, mummy ) == Approx( 22.51f ).epsilon( 0.05f ) );
}

SECTION( "STR 10, DEX 10" ) {
dummy.str_max = 10;
dummy.dex_max = 10;

CHECK( rock.effective_dps( dummy, mummy ) == Approx( 10.42f ).margin( 0.01f ) );
CHECK( plank.effective_dps( dummy, mummy ) == Approx( 5.55f ).margin( 0.01f ) );
CHECK( knife.effective_dps( dummy, mummy ) == Approx( 21.61f ).margin( 0.01f ) );
CHECK( clumsy_pipe.effective_dps( dummy, mummy ) == Approx( 7.66f ).epsilon( 0.05f ) );
CHECK( medium_pipe.effective_dps( dummy, mummy ) == Approx( 14.30f ).epsilon( 0.05f ) );
CHECK( sharp_pipe.effective_dps( dummy, mummy ) == Approx( 24.54f ).epsilon( 0.05f ) );
}
}
}
Expand All @@ -160,23 +160,32 @@ TEST_CASE( "effective vs actual damage per second", "[actual][dps][!mayfail]" )
avatar &dummy = g->u;
clear_character( dummy );

monster mummy( mtype_id( "debug_mon" ) );
monster soldier( mtype_id( "mon_zombie_soldier" ) );
monster smoker( mtype_id( "mon_zombie_smoker" ) );
monster survivor( mtype_id( "mon_zombie_survivor" ) );

item rock( "test_rock" );
item plank( "test_2x4" );
item knife( "knife_trench" );
item halligan( "test_halligan" );
item clumsy_pipe( "test_clumsy_pipe" );
item medium_pipe( "test_medium_pipe" );
item sharp_pipe( "test_sharp_pipe" );

SECTION( "soldier zombie" ) {
check_actual_dps( dummy, soldier, clumsy_pipe );
check_actual_dps( dummy, soldier, medium_pipe );
check_actual_dps( dummy, soldier, sharp_pipe );
}

SECTION( "debug monster" ) {
check_actual_dps( dummy, mummy, rock );
check_actual_dps( dummy, mummy, plank );
check_actual_dps( dummy, mummy, knife );
check_actual_dps( dummy, mummy, halligan );
SECTION( "smoker zombie" ) {
check_actual_dps( dummy, smoker, clumsy_pipe );
check_actual_dps( dummy, smoker, medium_pipe );
check_actual_dps( dummy, smoker, sharp_pipe );
}

SECTION( "survivor zombie" ) {
check_actual_dps( dummy, survivor, clumsy_pipe );
check_actual_dps( dummy, survivor, medium_pipe );
check_actual_dps( dummy, survivor, sharp_pipe );
}
}

TEST_CASE( "accuracy increases success", "[accuracy][dps]" )
{
Expand Down
Loading

0 comments on commit 7b9d959

Please sign in to comment.