Skip to content

Commit

Permalink
Merge pull request #36172 from jbytheway/nutrient_predictions
Browse files Browse the repository at this point in the history
Crafting menu shows range of possible nutrients for food crafts
  • Loading branch information
kevingranade authored Dec 23, 2019
2 parents 1c67f59 + 065d355 commit 8d59a49
Show file tree
Hide file tree
Showing 17 changed files with 529 additions and 41 deletions.
84 changes: 82 additions & 2 deletions src/cata_algo.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
#define CATA_ALGO_H

#include <algorithm>
#include <cassert>
#include <stack>
#include <unordered_map>
#include <unordered_set>
#include <vector>

namespace algo
namespace cata
{

/**
Expand Down Expand Up @@ -41,6 +45,82 @@ void sort_by_rating( Iterator begin, Iterator end, RatingFunction rating_func )
} );
}

} // namespace algo
// Implementation detail of below find_cycles
// This explores one branch of the given graph depth-first
template<typename T>
void find_cycles_impl(
const std::unordered_map<T, std::vector<T>> &edges,
const T &v,
std::unordered_set<T> &visited,
std::unordered_map<T, T> &on_current_branch,
std::vector<std::vector<T>> &result )
{
bool new_vertex = visited.insert( v ).second;

if( !new_vertex ) {
return;
}
auto it = edges.find( v );
if( it == edges.end() ) {
return;
}

for( const T &next_v : it->second ) {
if( next_v == v ) {
// Trivial self-loop
result.push_back( { v } );
continue;
}
auto previous_match = on_current_branch.find( next_v );
if( previous_match != on_current_branch.end() ) {
// We have looped back to somewhere along the branch we took to
// reach this vertex, so reconstruct the loop and save it.
std::vector<T> loop;
T on_path = v;
while( true ) {
loop.push_back( on_path );
if( on_path == next_v ) {
break;
}
on_path = on_current_branch[on_path];
}
std::reverse( loop.begin(), loop.end() );
result.push_back( loop );
} else {
on_current_branch.emplace( next_v, v );
find_cycles_impl( edges, next_v, visited, on_current_branch, result );
on_current_branch.erase( next_v );
}
}
}

// Find and return a list of all cycles in a directed graph.
// Each T defines a vertex.
// For a vertex a, edges[a] is a list of all the vertices connected by edges
// from a.
// It is acceptable for some vertex keys to be missing from the edges map, if
// those vertices have no out-edges.
// Complexity should be O(V+E)
// Based on https://www.geeksforgeeks.org/detect-cycle-in-a-graph/
template<typename T>
std::vector<std::vector<T>> find_cycles( const std::unordered_map<T, std::vector<T>> &edges )
{
std::unordered_set<T> visited;
std::unordered_map<T, T> on_current_branch;
std::vector<std::vector<T>> result;

for( const auto &p : edges ) {
const T &root = p.first;

on_current_branch.emplace( root, root );
find_cycles_impl( edges, root, visited, on_current_branch, result );
on_current_branch.erase( root );
assert( on_current_branch.empty() );
}

return result;
}

} // namespace cata

#endif // CATA_ALGO_H
128 changes: 122 additions & 6 deletions src/consumption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
#include "morale_types.h"
#include "mutation.h"
#include "options.h"
#include "recipe.h"
#include "recipe_dictionary.h"
#include "stomach.h"
#include "string_formatter.h"
#include "translations.h"
Expand Down Expand Up @@ -96,20 +98,24 @@ int player::stomach_capacity() const
}

// TODO: Move pizza scraping here.
static int compute_default_effective_kcal( const item &comest, const player &p )
static int compute_default_effective_kcal( const item &comest, const player &p,
const cata::flat_set<std::string> &extra_flags = {} )
{
static const trait_id trait_CARNIVORE( "CARNIVORE" );
static const trait_id trait_GIZZARD( "GIZZARD" );
static const trait_id trait_SAPROPHAGE( "SAPROPHAGE" );
static const std::string flag_CARNIVORE_OK( "CARNIVORE_OK" );

assert( comest.get_comestible() );
if( !comest.get_comestible() ) {
return 0;
}

// As float to avoid rounding too many times
float kcal = comest.get_comestible()->default_nutrition.kcal;

// Many raw foods give less calories, as your body has expends more energy digesting them.
if( comest.has_flag( "RAW" ) && !comest.has_flag( "COOKED" ) ) {
bool cooked = comest.has_flag( "COOKED" ) || extra_flags.count( "COOKED" );
if( comest.has_flag( "RAW" ) && !cooked ) {
kcal *= 0.75f;
}

Expand Down Expand Up @@ -145,7 +151,9 @@ static int compute_default_effective_kcal( const item &comest, const player &p )
static std::map<vitamin_id, int> compute_default_effective_vitamins(
const item &it, const player &p )
{
assert( it.get_comestible() );
if( !it.get_comestible() ) {
return {};
}

std::map<vitamin_id, int> res = it.get_comestible()->default_nutrition.vitamins;

Expand Down Expand Up @@ -173,9 +181,9 @@ static std::map<vitamin_id, int> compute_default_effective_vitamins(
// Calculate the effective nutrients for a given item, taking
// into account player traits but not item components.
static nutrients compute_default_effective_nutrients( const item &comest,
const player &p )
const player &p, const cata::flat_set<std::string> &extra_flags = {} )
{
return { compute_default_effective_kcal( comest, p ),
return { compute_default_effective_kcal( comest, p, extra_flags ),
compute_default_effective_vitamins( comest, p ) };
}

Expand Down Expand Up @@ -207,6 +215,114 @@ nutrients player::compute_effective_nutrients( const item &comest ) const
}
}

// Calculate range of nutrients obtainable for a given item when crafted via
// the given recipe
std::pair<nutrients, nutrients> player::compute_nutrient_range(
const item &comest, const recipe_id &recipe_i,
const cata::flat_set<std::string> &extra_flags ) const
{
if( !comest.is_comestible() ) {
return {};
}

// if item has components, will derive calories from that instead.
if( comest.has_flag( "NUTRIENT_OVERRIDE" ) ) {
nutrients result = compute_default_effective_nutrients( comest, *this );
return { result, result };
}

nutrients tally_min;
nutrients tally_max;

const recipe &rec = *recipe_i;

cata::flat_set<std::string> our_extra_flags = extra_flags;

if( rec.hot_result() ) {
our_extra_flags.insert( "COOKED" );
}

const requirement_data requirements = rec.requirements();
const requirement_data::alter_item_comp_vector &component_requirements =
requirements.get_components();

for( const std::vector<item_comp> &component_options : component_requirements ) {
nutrients this_min;
nutrients this_max;
bool first = true;
for( const item_comp &component_option : component_options ) {
std::pair<nutrients, nutrients> component_option_range =
compute_nutrient_range( component_option.type, our_extra_flags );
component_option_range.first *= component_option.count;
component_option_range.second *= component_option.count;

if( first ) {
std::tie( this_min, this_max ) = component_option_range;
first = false;
} else {
this_min.min_in_place( component_option_range.first );
this_max.max_in_place( component_option_range.second );
}
}
tally_min += this_min;
tally_max += this_max;
}

for( const std::pair<itype_id, int> &byproduct : rec.byproducts ) {
item byproduct_it( byproduct.first, calendar::turn, byproduct.second );
nutrients byproduct_nutr = compute_default_effective_nutrients( byproduct_it, *this );
tally_min -= byproduct_nutr;
tally_max -= byproduct_nutr;
}

int charges = comest.count();
return { tally_min / charges, tally_max / charges };
}

// Calculate the range of nturients possible for a given item across all
// possible recipes
std::pair<nutrients, nutrients> player::compute_nutrient_range(
const itype_id &comest_id, const cata::flat_set<std::string> &extra_flags ) const
{
const itype *comest = item::find_type( comest_id );
if( !comest->comestible ) {
return {};
}

item comest_it( comest, calendar::turn, 1 );
// The default nutrients are always a possibility
nutrients min_nutr = compute_default_effective_nutrients( comest_it, *this, extra_flags );

if( comest->item_tags.count( "NUTRIENT_OVERRIDE" ) ||
recipe_dict.is_item_on_loop( comest->get_id() ) ) {
return { min_nutr, min_nutr };
}

nutrients max_nutr = min_nutr;

for( const recipe_id &rec : comest->recipes ) {
nutrients this_min;
nutrients this_max;

item result_it = rec->create_result();
if( result_it.contents.size() == 1 ) {
const item alt_result = result_it.contents.front();
if( alt_result.typeId() == comest_it.typeId() ) {
result_it = alt_result;
}
}
if( result_it.typeId() != comest_it.typeId() ) {
debugmsg( "When creating recipe result expected %s, got %s\n",
comest_it.typeId(), result_it.typeId() );
}
std::tie( this_min, this_max ) = compute_nutrient_range( result_it, rec, extra_flags );
min_nutr.min_in_place( this_min );
max_nutr.max_in_place( this_max );
}

return { min_nutr, max_nutr };
}

int player::nutrition_for( const item &comest ) const
{
return compute_effective_nutrients( comest ).kcal / islot_comestible::kcal_per_nutr;
Expand Down
9 changes: 1 addition & 8 deletions src/crafting_gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -607,17 +607,10 @@ const recipe *select_crafting_recipe( int &batch_size )
if( last_recipe != current[line] ) {
last_recipe = current[line];
tmp = current[line]->create_result();
tmp.set_var( "recipe_exemplar", last_recipe->ident().str() );
}
tmp.info( true, thisItem, count );

// If it's food that can have variable nutrition, add disclaimer.
// Hidden if the user is attempting to page through components.
if( ( tmp.is_food_container() || tmp.is_food() ) && !tmp.has_flag( "NUTRIENT_OVERRIDE" ) &&
display_mode == 0 ) {
ypos += fold_and_print( w_data, point( xpos + 2, ypos ), pane - 2, c_light_gray,
_( "Shown nutrition is <color_cyan>estimated</color>, varying with <color_cyan>chosen ingredients</color>." ) );
}

//color needs to be preserved in case part of the previous page was cut off
nc_color stored_color = col;
if( display_mode > 1 ) {
Expand Down
76 changes: 53 additions & 23 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1322,12 +1322,38 @@ void item::med_info( const item *med_item, std::vector<iteminfo> &info, const it
void item::food_info( const item *food_item, std::vector<iteminfo> &info,
const iteminfo_query *parts, int batch, bool debug ) const
{
const nutrients nutr = g->u.compute_effective_nutrients( *food_item );
nutrients min_nutr;
nutrients max_nutr;

std::string recipe_exemplar = get_var( "recipe_exemplar", "" );
if( recipe_exemplar.empty() ) {
min_nutr = max_nutr = g->u.compute_effective_nutrients( *food_item );
} else {
std::tie( min_nutr, max_nutr ) =
g->u.compute_nutrient_range( *food_item, recipe_id( recipe_exemplar ) );
}

bool show_nutr = parts->test( iteminfo_parts::FOOD_NUTRITION ) ||
parts->test( iteminfo_parts::FOOD_VITAMINS );
if( min_nutr != max_nutr && show_nutr ) {
info.emplace_back(
"FOOD", _( "Nutrition will <color_cyan>vary with chosen ingredients</color>." ) );
if( recipe_dict.is_item_on_loop( food_item->typeId() ) ) {
info.emplace_back(
"FOOD", _( "Nutrition range cannot be calculated accurately due to "
"<color_red>recipe loops</color>." ) );
}
}

const std::string space = " ";
if( nutr.kcal != 0 || food_item->get_comestible()->quench != 0 ) {
if( max_nutr.kcal != 0 || food_item->get_comestible()->quench != 0 ) {
if( parts->test( iteminfo_parts::FOOD_NUTRITION ) ) {
info.push_back( iteminfo( "FOOD", _( "<bold>Calories (kcal)</bold>: " ),
"", iteminfo::no_newline, nutr.kcal ) );
"", iteminfo::no_newline, min_nutr.kcal ) );
if( max_nutr.kcal != min_nutr.kcal ) {
info.push_back( iteminfo( "FOOD", _( "-" ),
"", iteminfo::no_newline, max_nutr.kcal ) );
}
}
if( parts->test( iteminfo_parts::FOOD_QUENCH ) ) {
info.push_back( iteminfo( "FOOD", space + _( "Quench: " ),
Expand All @@ -1351,30 +1377,34 @@ void item::food_info( const item *food_item, std::vector<iteminfo> &info,
info.push_back( iteminfo( "FOOD", _( "Smells like: " ) + food_item->corpse->nname() ) );
}

const std::string required_vits = enumerate_as_string(
nutr.vitamins.begin(),
nutr.vitamins.end(),
[]( const std::pair<vitamin_id, int> &v ) {
auto format_vitamin = [&]( const std::pair<vitamin_id, int> &v, bool display_vitamins ) {
const bool is_vitamin = v.first->type() == vitamin_type::VITAMIN;
// only display vitamins that we actually require
return ( g->u.vitamin_rate( v.first ) > 0_turns && v.second != 0 &&
v.first->type() == vitamin_type::VITAMIN && !v.first->has_flag( "NO_DISPLAY" ) ) ?
string_format( "%s (%i%%)", v.first.obj().name(),
static_cast<int>( v.second * g->u.vitamin_rate( v.first ) /
1_days * 100 ) ) :
std::string();
if( g->u.vitamin_rate( v.first ) == 0_turns || v.second == 0 ||
display_vitamins != is_vitamin || v.first->has_flag( "NO_DISPLAY" ) ) {
return std::string();
}
const double multiplier = g->u.vitamin_rate( v.first ) / 1_days * 100;
const int min_value = min_nutr.get_vitamin( v.first );
const int max_value = v.second;
const int min_rda = lround( min_value * multiplier );
const int max_rda = lround( max_value * multiplier );
const std::string format = min_rda == max_rda ? "%s (%i%%)" : "%s (%i-%i%%)";
return string_format( format, v.first->name(), min_value, max_value );
};

const std::string required_vits = enumerate_as_string(
max_nutr.vitamins.begin(),
max_nutr.vitamins.end(),
[&]( const std::pair<vitamin_id, int> &v ) {
return format_vitamin( v, true );
} );

const std::string effect_vits = enumerate_as_string(
nutr.vitamins.begin(),
nutr.vitamins.end(),
[]( const std::pair<vitamin_id, int> &v ) {
// only display vitamins that we actually require
return ( g->u.vitamin_rate( v.first ) > 0_turns && v.second != 0 &&
v.first->type() != vitamin_type::VITAMIN && !v.first->has_flag( "NO_DISPLAY" ) ) ?
string_format( "%s (%i%%)", v.first.obj().name(),
static_cast<int>( v.second * g->u.vitamin_rate( v.first ) /
1_days * 100 ) ) :
std::string();
max_nutr.vitamins.begin(),
max_nutr.vitamins.end(),
[&]( const std::pair<vitamin_id, int> &v ) {
return format_vitamin( v, false );
} );

if( !required_vits.empty() && parts->test( iteminfo_parts::FOOD_VITAMINS ) ) {
Expand Down
Loading

0 comments on commit 8d59a49

Please sign in to comment.