Skip to content

Commit

Permalink
Auto-eat sorts by spoilage (#2402)
Browse files Browse the repository at this point in the history
* Auto-eat sorts by spoilage

* Update activity_item_handling.cpp

* Auto-eat sorts by spoilage

* Update activity_item_handling.cpp

* refactor: extract to `ok_to_consume`

* perf: find shortest shelf life item without allocating vectors

* docs: add docstring for `find_auto_consume`

* refactor: use enum class as option for clarity

also conform to astyle

* test: find_auto_consume

---------

Co-authored-by: scarf <greenscarf005@gmail.com>
  • Loading branch information
chaosvolt and scarf005 authored Jun 5, 2023
1 parent cfd2c06 commit 6585139
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 60 deletions.
12 changes: 11 additions & 1 deletion src/activity_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,17 @@ void activity_on_turn_move_loot( player_activity &act, player &p );
bool generic_multi_activity_handler( player_activity &act, player &p, bool check_only = false );
void activity_on_turn_fetch( player_activity &, player *p );
void activity_on_turn_wear( player_activity &act, player &p );
bool find_auto_consume( player &p, bool food );

enum class consume_type : bool { FOOD, DRINK };

/**
* @brief Find an item to consume automatically
*
* @param consume_type type of item to consume
* @return true player ate food or was nauseous
* @return false player did not find anything suitable or is a npc
*/
bool find_auto_consume( player &p, const consume_type type );
void try_fuel_fire( player_activity &act, player &p, bool starting_fire = false );

enum class item_drop_reason {
Expand Down
152 changes: 95 additions & 57 deletions src/activity_item_handling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3133,7 +3133,7 @@ static std::optional<tripoint> find_refuel_spot_trap( const std::vector<tripoint
return {};
}

bool find_auto_consume( player &p, const bool food )
bool find_auto_consume( player &p, const consume_type type )
{
// return false if there is no point searching again while the activity is still happening.
if( p.is_npc() ) {
Expand All @@ -3145,12 +3145,7 @@ bool find_auto_consume( player &p, const bool food )
const tripoint pos = p.pos();
map &here = get_map();
zone_manager &mgr = zone_manager::get_manager();
zone_type_id consume_type_zone( "" );
if( food ) {
consume_type_zone = zone_type_id( "AUTO_EAT" );
} else {
consume_type_zone = zone_type_id( "AUTO_DRINK" );
}
const zone_type_id consume_type_zone( type == consume_type::FOOD ? "AUTO_EAT" : "AUTO_DRINK" );
if( here.check_vehicle_zones( g->get_levz() ) ) {
mgr.cache_vzones();
}
Expand All @@ -3159,73 +3154,116 @@ bool find_auto_consume( player &p, const bool food )
if( dest_set.empty() ) {
return false;
}

const auto ok_to_consume = [&p, type]( item & it ) -> bool {
item &comest = p.get_consumable_from( it );
/* not food. */
if( comest.is_null() || comest.is_craft() || !comest.is_food() )
{
return false;
}
/* not enjoyable. */
if( p.fun_for( comest ).first < -5 )
{
return false;
}
/* cannot consume. */
if( !p.can_consume( comest ) )
{
return false;
}
/* wont eat, e.g cannibal */
if( !p.will_eat( comest, false ).success() )
{
return false;
}
/* not ours */
if( !it.is_owned_by( p, true ) )
{
return false;
}
/* not quenching enough */
if( type == consume_type::DRINK && comest.get_comestible()->quench < 15 )
{
return false;
}
/* Unsafe to drink or eat */
if( comest.has_flag( flag_UNSAFE_CONSUME ) )
{
return false;
}
return true;
};

struct {
item *min_shelf_life = nullptr;
tripoint loc = tripoint_min;
item_location item_loc = item_location::nowhere;

bool longer_life_than( item &it ) {
return !min_shelf_life || it.spoilage_sort_order() < min_shelf_life->spoilage_sort_order();
};
} current;

const auto should_skip = [&]( item & it ) {
return !ok_to_consume( it ) || !current.longer_life_than( it );
};

for( const tripoint loc : dest_set ) {
if( loc.z != p.pos().z ) {
continue;
}

const optional_vpart_position vp = here.veh_at( g->m.getlocal( loc ) );
std::vector<item *> items_here;
if( vp ) {
vehicle &veh = vp->vehicle();
int index = veh.part_with_feature( vp->part_index(), "CARGO", false );
if( index >= 0 ) {
vehicle_stack vehitems = veh.get_items( index );
for( item &it : vehitems ) {
items_here.push_back( &it );
const int index = veh.part_with_feature( vp->part_index(), "CARGO", false );
if( index < 0 ) {
continue;
}
/**
* TODO: when we get to use ranges library, current should be replaced with:
*
* const auto shortest = vehitems | filter_view(ok_to_consume) | max_element(spoilage_sort_order)
*
* rationale:
* 1. much more readable (mandatory FP shilling)
* 2. filter_view does not create a new container (it's a view), so it's performant
*
* @see https://en.cppreference.com/w/cpp/ranges/filter_view
* @see https://en.cppreference.com/w/cpp/algorithm/ranges/max_element
*/
vehicle_stack vehitems = veh.get_items( index );
for( item &it : vehitems ) {
if( should_skip( it ) ) {
continue;
}
current = { &it, loc, item_location( vehicle_cursor( vp->vehicle(), vp->part_index() ), &p.get_consumable_from( it ) ) };
}
} else {
map_stack mapitems = here.i_at( here.getlocal( loc ) );
for( item &it : mapitems ) {
items_here.push_back( &it );
if( should_skip( it ) ) {
continue;
}
current = { &it, loc, item_location( map_cursor( here.getlocal( loc ) ), &p.get_consumable_from( it ) ) };
}
}
}
if( !current.min_shelf_life ) {
return false;
}

for( item *it : items_here ) {
item &comest = p.get_consumable_from( *it );
if( comest.is_null() || comest.is_craft() || !comest.is_food() ||
p.fun_for( comest ).first < -5 ) {
// not good eatings.
continue;
}
if( !p.can_consume( *it ) ) {
continue;
}
if( !p.will_eat( comest, false ).success() ) {
// wont like it, cannibal meat etc
continue;
}
if( !it->is_owned_by( p, true ) ) {
// it aint ours.
continue;
}
if( !food && comest.get_comestible()->quench < 15 ) {
// not quenching enough
continue;
}
if( comest.has_flag( flag_UNSAFE_CONSUME ) ) {
// Unsafe to drink or eat
continue;
}
// actually eat
const auto cost = pickup::cost_to_move_item( p, *current.min_shelf_life );
const auto dist = std::max( rl_dist( p.pos(), here.getlocal( current.loc ) ), 1 );
p.mod_moves( -cost * dist );

p.mod_moves( -pickup::cost_to_move_item( p, *it ) * std::max( rl_dist( p.pos(),
here.getlocal( loc ) ), 1 ) );
item_location item_loc;
if( vp ) {
item_loc = item_location( vehicle_cursor( vp->vehicle(), vp->part_index() ), &comest );
} else {
item_loc = item_location( map_cursor( here.getlocal( loc ) ), &comest );
}
avatar_action::eat( g->u, item_loc );
// eat() may have removed the item, so check its still there.
if( item_loc.get_item() && item_loc->is_container() ) {
item_loc->on_contents_changed();
}
return true;
}
avatar_action::eat( g->u, current.item_loc );
// eat() may have removed the item, so check its still there.
if( current.item_loc.get_item() && current.item_loc->is_container() ) {
current.item_loc->on_contents_changed();
}
return false;
return true;
}

void try_fuel_fire( player_activity &act, player &p, const bool starting_fire )
Expand Down
4 changes: 2 additions & 2 deletions src/player_activity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,12 @@ void player_activity::do_turn( player &p )
}
if( *this && !p.is_npc() && type->valid_auto_needs() && !no_food_nearby_for_auto_consume ) {
if( p.get_kcal_percent() < 0.95f ) {
if( !find_auto_consume( p, true ) ) {
if( !find_auto_consume( p, consume_type::FOOD ) ) {
no_food_nearby_for_auto_consume = true;
}
}
if( p.get_thirst() > thirst_levels::thirsty && !no_drink_nearby_for_auto_consume ) {
if( !find_auto_consume( p, false ) ) {
if( !find_auto_consume( p, consume_type::DRINK ) ) {
no_drink_nearby_for_auto_consume = true;
}
}
Expand Down
171 changes: 171 additions & 0 deletions tests/find_auto_consume_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#include "avatar.h"
#include "character.h"
#include "clzones.h"
#include "itype.h"
#include "player.h"
#include "map.h"
#include "item.h"
#include "activity_handlers.h"
#include "avatar_action.h"
#include "item_location.h"
#include "pickup.h"
#include "calendar.h"
#include "player_helpers.h"
#include "state_helpers.h"
#include "vehicle.h"
#include "type_id.h"

#include "catch/catch.hpp"

/** food items are counted by charges */
static auto get_single_food_item( const tripoint &pos ) -> const item &
{
map &here = get_map();
const auto &items = here.i_at( pos );
CHECK( items.size() == 1 );

return *items.begin();
}

TEST_CASE( "auto_consume_priority", "[auto_consume][food][zone]" )
{
clear_all_state();

map &here = get_map();
auto &zmgr = zone_manager::get_manager();

constexpr auto zone_origin = tripoint{ 60, 60, 0 };
tripoint zone_origin_absolute = here.getabs( zone_origin );
constexpr auto zone_size = tripoint{ 6, 6, 0 };

avatar &you = get_avatar();
you.setpos( zone_origin );

static auto create_zone = [&]( const std::string & name ) -> void {
zmgr.add( name, zone_type_id( name ),
faction_id( "your_followers" ), false, true,
zone_origin_absolute - zone_size,
zone_origin_absolute + zone_size );
};

static auto place_items = [&]( const std::vector<std::pair<item, tripoint>> &item_pairs ) -> void {
for( const auto &[ item, pos ] : item_pairs )
{
here.add_item_or_charges( pos, item );
}
};

static const auto auto_consume = [&]( consume_type consume ) {
return [&you, consume]( int count ) -> bool {
bool ok = true;
for( int i = 0; i < count; i++ )
{
ok &= find_auto_consume( you, consume );
}
return ok;
};
};

using PosCounts = std::vector<std::pair<tripoint, int>>;

SECTION( "auto_eat" ) {
static const auto check_item_count =
[&]( const PosCounts & expected ) -> void {
for( const auto&[ pos, count ] : expected )
{
if( count == 0 ) {
INFO( "expected empty at " << pos );
CHECK( here.i_at( pos ).empty() );
} else {
INFO( "expected " << count << " at " << pos );
CHECK( get_single_food_item( pos ).count() == count );
}
}
};

static const auto auto_eat = auto_consume( consume_type::FOOD );

clear_avatar();
you.set_stored_kcal( 1000 );

create_zone( "AUTO_EAT" );

auto meat = item{ "meat_cooked", calendar::turn, 5 }; // shelf life: 2 days
auto meat_pos = zone_origin;
auto nuts = item{ "pine_nuts", calendar::turn, 5 }; // shelf life: 3 seasons
auto nuts_pos = zone_origin + tripoint( 1, 0, 0 );
auto hardtack = item{ "hardtack", calendar::turn, 5 }; // shelf life: 6 years
auto hardtack_pos = zone_origin + tripoint( 2, 0, 0 );

place_items( { { meat, meat_pos }, { nuts, nuts_pos }, { hardtack, hardtack_pos } } );

CHECK( auto_eat( 5 ) );
check_item_count( { { meat_pos, 0 }, { nuts_pos, 5 }, { hardtack_pos, 5 } } );
CHECK( auto_eat( 5 ) );
check_item_count( { { meat_pos, 0 }, { nuts_pos, 0 }, { hardtack_pos, 5 } } );
CHECK( auto_eat( 5 ) );
check_item_count( { { meat_pos, 0 }, { nuts_pos, 0 }, { hardtack_pos, 0 } } );

// check that the player has consumed the food
CHECK( you.stomach.get_calories() > 1000 );
}

SECTION( "auto_drink" ) {
static const auto check_drink_amount =
[&]( const PosCounts & expected ) -> void {
for( const auto&[ pos, count ] : expected )
{
auto jar = get_single_food_item( pos );
auto &contained = jar.get_contained();
INFO( contained.tname() << " has " << contained.count() << " charges" );
if( count == 0 ) {
CHECK( contained.is_null() );
} else {
CHECK( contained.count() == count );
}
}
};

static const auto auto_drink = auto_consume( consume_type::DRINK );

create_zone( "AUTO_DRINK" );

auto jar = itype_id{"jar_3l_glass"};
auto water = item( "water_clean" );
auto water_bottle = water.in_container( jar );
auto water_pos = zone_origin;
auto orange = item( "oj" ); // 5 days
auto orange_bottle = orange.in_container( jar );
auto orange_pos = zone_origin + tripoint( 1, 0, 0 );
auto cocoa = item( "hot_chocolate" ); // 1 day
auto cocoa_bottle = cocoa.in_container( jar );
auto cocoa_pos = zone_origin + tripoint( 2, 0, 0 );

place_items( { { water_bottle, water_pos }, { orange_bottle, orange_pos }, { cocoa_bottle, cocoa_pos } } );

SECTION( "full character won't drink drink with calories" ) {
clear_avatar();
you.set_stored_kcal( you.max_stored_kcal() );
you.set_thirst( 700 );

check_drink_amount( { { water_pos, 12 }, { orange_pos, 12 }, { cocoa_pos, 12 } } );
CHECK( auto_drink( 6 ) );
check_drink_amount( { { water_pos, 6 }, { orange_pos, 12 }, { cocoa_pos, 12 } } );

CHECK( you.get_thirst() < 700 );
}

SECTION( "hungry character will drink drink with calories" ) {
clear_avatar();
you.set_thirst( 700 );
you.set_stored_kcal( 1000 );

CHECK( auto_drink( 12 ) );
INFO( "only cocoa should be consumed" );
// FIXME: can't figure out why water is replenished, but it is
check_drink_amount( { { water_pos, 12 }, { orange_pos, 12 }, { cocoa_pos, 0 } } );

CHECK( you.get_thirst() < 700 );
}
}
}

0 comments on commit 6585139

Please sign in to comment.