Skip to content

Commit

Permalink
Add cardio_test.cpp
Browse files Browse the repository at this point in the history
This test covers several aspects of cardio fitness, stamina, and running
distance, especially related to mutations affecting cardio and stamina,
and in particular the three CARDIO traits affected by #52980.

Some FIXMEs are noted, where trait buffs have changed significantly
since the introduction of cardio, and may need to be rebalanced.

Several stub test cases are included to invite future collaboration.
  • Loading branch information
wapcaplet committed Dec 31, 2021
1 parent 1ea2a93 commit 2d4a90c
Showing 1 changed file with 336 additions and 0 deletions.
336 changes: 336 additions & 0 deletions tests/cardio_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
#include "avatar.h"
#include "cata_catch.h"
#include "game.h"
#include "options.h"
#include "map.h"
#include "map_helpers.h"
#include "player_helpers.h"

// Cardio Fitness
// --------------
// The hidden "Cardio Fitness" stat returned by Character::get_cardiofit() is the main factor in
// calculations related to cardiovascular health, including weariness, stamina, and stamina regen,
// more or less as described in the original issue #44370 "Rule #1. CARDIO".
//
// For player characters, cardio fitness is derived from a combination of several attributes:
//
// (
// [ 1/2 BMR ] // Basal Metabolic Rate, varies with activity and body size
// + [ Health ] // Hidden health modifier, -200..+200
// + [ Proficiency modifiers ] // NOT IMPLEMENTED
// + [ Cardio_Accumulator ] // Adjustment based on daily activity, starting at 1/2 BMR
// + 10 * [ Athletics skill ] // Formerly swimming skill, now more versatile and valuable
// ) * [ Trait modifiers ] // Good/bad cardio mutations like Languorous and Hyperactive
//
// For NPCs (having no cardio), the formula is simply 2 * BMR.
//
// Important functions:
// - Character::get_stamina
// - Character::set_stamina
// - Character::get_stamina_max
// - Character::get_cardiofit
// - Character::get_cardio_acc
// - Character::set_cardio_acc
// - avatar::update_cardio_acc <-- Critical

static const efftype_id effect_winded( "winded" );

static const move_mode_id move_mode_run( "run" );

static const skill_id skill_swimming( "swimming" );

// Base BMR for default character
static const int base_bmr = 1738;
static const int base_cardio = base_bmr;
// Base stamina
// MAX_STAMINA_BASE + CARDIOFIT_STAMINA_SCALING * base_bmr == 3500 + 3*1738
static const int base_stamina = 8714;

// Ensure the configured options from game_balance.json are what the tests assume they are
static void verify_default_cardio_options()
{
const int max_stamina_base = get_option<int>( "PLAYER_MAX_STAMINA_BASE" );
const int cardiofit_stamina_scaling = get_option<int>( "PLAYER_CARDIOFIT_STAMINA_SCALING" );
REQUIRE( max_stamina_base == 3500 );
REQUIRE( cardiofit_stamina_scaling == 3 );
}

// Count the number of steps (tiles) until character runs out of stamina or becomes winded.
static int running_steps( Character &they, const ter_id &terrain = t_pavement )
{
map &here = get_map();
// Please take off your shoes when entering, and no NPCs allowed
REQUIRE_FALSE( they.is_wearing_shoes() );
REQUIRE_FALSE( they.is_npc() );
// You put your left foot in, you put your right foot in
const tripoint left = they.pos();
const tripoint right = left + tripoint_east;
// You ensure two tiles of terrain to hokey-pokey in
here.ter_set( left, terrain );
here.ter_set( right, terrain );
REQUIRE( here.ter( left ) == terrain );
REQUIRE( here.ter( right ) == terrain );
// Count how many steps (1-tile moves) it takes to become winded
int steps = 0;
const int STOP_STEPS = 1000; // Safe exit in case of Superman
// Track changes to moves and stamina
int last_moves = they.get_speed();
int last_stamina = they.get_stamina_max();

// Take a deep breath and start running
they.moves = last_moves;
they.set_stamina( last_stamina );
they.set_movement_mode( move_mode_run );
// Run until out of stamina or winded (should happen at the same time)
while( they.get_stamina() > 0 && !they.has_effect( effect_winded ) && steps < STOP_STEPS ) {
// Step right on even steps, left on odd steps
if( steps % 2 == 0 ) {
REQUIRE( they.pos() == left );
REQUIRE( g->walk_move( right, false, false ) );
} else {
REQUIRE( they.pos() == right );
REQUIRE( g->walk_move( left, false, false ) );
}
++steps;

// Ensure moves are decreasing, or else a turn will never pass
REQUIRE( they.moves < last_moves );
const int move_cost = last_moves - they.moves;
// When moves run out, one turn has passed
if( they.moves <= 0 ) {
// Get "speed" moves back each turn
they.moves += they.get_speed();
calendar::turn += 1_turns;
}
last_moves = they.moves;

// Update body for stamina regen
they.update_body();
// NOTE: Stamina cost is always 100, 101, 120, or 121 ??
const int stamina_cost = last_stamina - they.get_stamina();
// Total stamina must also be decreasing; if not, quit
CAPTURE( move_cost );
CAPTURE( stamina_cost );
REQUIRE( they.get_stamina() < last_stamina );
last_stamina = they.get_stamina();
}
// Reset to starting position
they.setpos( left );
return steps;
}

// Give character a trait, and verify their expected cardio, max stamina, and running distance.
static void check_trait_cardio_stamina_run( Character &they, std::string trait_name,
const int expect_cardio_fit, const int expect_stamina_max, const int expect_run_tiles )
{
clear_avatar();
if( trait_name.empty() ) {
trait_name = "NONE";
} else {
set_single_trait( they, trait_name );
}
GIVEN( "trait: " + trait_name ) {
CHECK( they.get_cardiofit() == Approx( expect_cardio_fit ).margin( 2 ) );
CHECK( they.get_stamina_max() == Approx( expect_stamina_max ).margin( 5 ) );
//CHECK( they.get_stamina_max() == Approx( 3500 + 3 * expect_cardio_fit ).margin( 2 ) );
CHECK( running_steps( they ) == Approx( expect_run_tiles ).margin( 3 ) );
}
}


// Trait Modifiers
// ---------------
// Mutations/traits can influence cardio fitness level in a few ways:
//
// - Mutation affecting total body size, which affects base BMR and thus cardio
// - Little / Tiny: Smaller body has lower BMR, less cardio, cannot run as far
// - Large / Huge: Larger body has higher BMR, more cardio, can run further
// - metabolism_modifier: Affects metabolic_rate_base
// - Heat Dependent / Cold Blooded: Decreased BMR and cardio, less running
// - Fast/Rapid/Extreme Metabolism: Increased BMR and cardio, more running
//
// Some traits affect cardio fitness directly:
//
// - cardio_multiplier: Multiplies maximum cardio
// - Languorous: Bad cardio, less total stamina
// - Indefatigable, Hyperactive: Good cardio, more total stamina
//
// Some traits affect stamina regen and total running distance without affecting cardio:
//
// - stamina_regen_modifier
// - Fast Metabolism, Persistence Hunter: Increased stamina regeneration
//
TEST_CASE( "cardio is affected by certain traits", "[cardio][traits]" )
{
verify_default_cardio_options();
Character &they = get_player_character();

clear_map();
clear_avatar();

// Ensure no initial effects that would affect cardio
REQUIRE( they.get_healthy() == 0 );
REQUIRE( they.get_skill_level( skill_swimming ) == 0 );
// Ensure base_bmr and starting cardio are what we expect
REQUIRE( they.base_bmr() == base_bmr );
REQUIRE( they.get_cardiofit() == base_cardio );
REQUIRE( 2 * they.get_cardio_acc() == base_bmr );

// Values trailing after check //123 are pre-Cardio in-game running distances, a high-level
// metric for balancing traits relative to how they were before cardio. Some traits were buffed
// by the change to cardio; the fast metabolism, persistence hunger, and cold-blooded traits got
// a 20-30% boost in total running distance. The old values are preserved here for possible
// future rebalancing and comparison.

SECTION( "Base character with no traits" ) {
// pre-Cardio, could run 96 steps
// post-Cardio, can run 84 steps in-game, test case reached 87
// correctly counting moves/steps instead of turns, test case reaches 83
check_trait_cardio_stamina_run( they, "", base_cardio, base_stamina, 83 ); //96
}

// Sprint distance for each body size is only slightly different than it was before cardio
SECTION( "Traits affecting body size" ) {
// Body size determines BMR, which affects base cardio fitness
// Pre-Cardio, body size did not affect how many steps you could run
check_trait_cardio_stamina_run( they, "SMALL2", 1088, 6764, 83 ); //97
// FIXME: But why the heck can SMALL2 run further than SMALL?
check_trait_cardio_stamina_run( they, "SMALL", 1376, 7628, 77 ); //97
check_trait_cardio_stamina_run( they, "LARGE", 2162, 9986, 92 ); //97
check_trait_cardio_stamina_run( they, "HUGE", 2663, 11489, 106 ); //97
}

SECTION( "Traits with cardio_multiplier" ) {
// These traits were formerly implemented by max_stamina_modifier, which multiplied
// maximum stamina. Now that cardio fitness is actually implemented, these traits
// directly affect total cardio fitness, and thus maximum stamina (and running distance).
// Languorous
check_trait_cardio_stamina_run( they, "BADCARDIO", 0.7 * base_cardio, 7148, 67 ); //70
// Indefatigable
check_trait_cardio_stamina_run( they, "GOODCARDIO", 1.3 * base_cardio, 10277, 103 ); //126
// Hyperactive
check_trait_cardio_stamina_run( they, "GOODCARDIO2", 1.6 * base_cardio, 11840, 125 ); //145
}

// FIXME: These traits need a significant nerf (-8 to -32) to reach their pre-Cardio balance
SECTION( "Traits with metabolism_modifier AND stamina_regen_modifier" ) {
// Fast Metabolism
check_trait_cardio_stamina_run( they, "HUNGER", 2173, 10019, 95 ); //76
// Very Fast Metabolism
check_trait_cardio_stamina_run( they, "HUNGER2", 2608, 11324, 108 ); //87
// Extreme Metabolism
check_trait_cardio_stamina_run( they, "HUNGER3", 3477, 13931, 132 ); //107
}

// FIXME: These traits need a significant nerf (-20) to reach their pre-Cardio balance
SECTION( "Traits with ONLY stamina_regen_modifier" ) {
check_trait_cardio_stamina_run( they, "PERSISTENCE_HUNTER", base_cardio, base_stamina, 85 ); //68
check_trait_cardio_stamina_run( they, "PERSISTENCE_HUNTER2", base_cardio, base_stamina, 86 ); //69
}

// FIXME: These traits need a significant nerf (-20) to reach their pre-Cardio balance
SECTION( "Traits with ONLY metabolism_modifier" ) {
check_trait_cardio_stamina_run( they, "COLDBLOOD", 1449, 7847, 78 ); //63
check_trait_cardio_stamina_run( they, "COLDBLOOD2", 1304, 7412, 77 ); //62
check_trait_cardio_stamina_run( they, "COLDBLOOD3", 1304, 7412, 77 ); //62
check_trait_cardio_stamina_run( they, "COLDBLOOD4", 1304, 7412, 77 ); //62
check_trait_cardio_stamina_run( they, "LIGHTEATER", 1449, 7847, 78 ); //63
check_trait_cardio_stamina_run( they, "MET_RAT", 2028, 9584, 90 ); //72
}
}

TEST_CASE( "cardio affects stamina regeneration", "[cardio][stamina]" )
{
// With baseline cardio, stamina regen is X
// With low cardio, stamina regen is X--
// With high cardio, stamina regen is X++
}

TEST_CASE( "cardio affects weariness", "[cardio][weariness]" )
{
// Weariness threshold is 1/2 cardio
}

TEST_CASE( "cardio is affected by activity level each day", "[cardio][activity]" )
{
// When activity increases, cardio goes up
// When activity decreases, cardio goes down
// When activity stays the same, cardio stays the same

// Given a starting character
// When they get no exercise for a week
// Then their cardio should decrease slightly
// When they get moderate exercise for a week
// Then their cardio should increase slightly
}

TEST_CASE( "cardio is affected by character height", "[cardio][height]" )
{
verify_default_cardio_options();
Character &they = get_player_character();
clear_avatar();

REQUIRE( they.size_class == creature_size::medium );

SECTION( "Within a size class, greater height means greater cardio" ) {
they.set_base_height( Character::default_height( they.size_class ) );
CHECK( they.get_cardiofit() == base_cardio ); //1739
they.set_base_height( Character::max_height( they.size_class ) );
CHECK( they.get_cardiofit() == Approx( base_cardio + 196 ).margin( 5 ) ); //1935
they.set_base_height( Character::min_height( they.size_class ) );
CHECK( they.get_cardiofit() == Approx( base_cardio - 214 ).margin( 5 ) ); //1525
}
}

TEST_CASE( "cardio is affected by character weight", "[cardio][weight]" )
{
// Underweight, overweight
}

TEST_CASE( "cardio is affected by character health", "[cardio][health]" )
{
verify_default_cardio_options();
Character &they = get_player_character();
clear_avatar();

SECTION( "Hidden health stat adds directly to cardio fitness" ) {
they.set_healthy( 0 );
CHECK( they.get_cardiofit() == base_cardio );
they.set_healthy( 200 );
CHECK( they.get_cardiofit() == base_cardio + 200 );
they.set_healthy( -200 );
CHECK( they.get_cardiofit() == base_cardio - 200 );
}
}

TEST_CASE( "cardio is affected by athletics skill", "[cardio][athletics]" )
{
verify_default_cardio_options();
Character &they = get_player_character();
clear_avatar();

SECTION( "Athletics skill adds 10 per level to cardio fitness" ) {
they.set_skill_level( skill_swimming, 0 );
CHECK( they.get_cardiofit() == base_cardio );
they.set_skill_level( skill_swimming, 1 );
CHECK( they.get_cardiofit() == base_cardio + 10 );
they.set_skill_level( skill_swimming, 2 );
CHECK( they.get_cardiofit() == base_cardio + 20 );
they.set_skill_level( skill_swimming, 3 );
CHECK( they.get_cardiofit() == base_cardio + 30 );
they.set_skill_level( skill_swimming, 4 );
CHECK( they.get_cardiofit() == base_cardio + 40 );
they.set_skill_level( skill_swimming, 5 );
CHECK( they.get_cardiofit() == base_cardio + 50 );
they.set_skill_level( skill_swimming, 6 );
CHECK( they.get_cardiofit() == base_cardio + 60 );
they.set_skill_level( skill_swimming, 7 );
CHECK( they.get_cardiofit() == base_cardio + 70 );
they.set_skill_level( skill_swimming, 8 );
CHECK( they.get_cardiofit() == base_cardio + 80 );
they.set_skill_level( skill_swimming, 9 );
CHECK( they.get_cardiofit() == base_cardio + 90 );
they.set_skill_level( skill_swimming, 10 );
CHECK( they.get_cardiofit() == base_cardio + 100 );
}
}

0 comments on commit 2d4a90c

Please sign in to comment.