Food optimization algorithm (urban survival)

Hello,

Let’s I have a set of characters in an urban survival game, each have a consumption rate per day based on age, weight, size and activity profile, express in Calorie.

Let’s say I have a set of food, express as Calorie, but having component of nutrient such has carb, protein and fat, express as normalized unit, and serving ratio that express the minimal size of a portion relative to that unit. And each food as a cost per serving, which can change base on brand or packing size.

Given a balance of nutrient, such as 50%carb, 20% protein, 30% fat, per character. What’s the best algorithm I could use, that select food according to their minimal serving, fill the character need in Calorie, and achieve the best balance of nutrient for the lowest price?

There’re a few components to this. Obviously your own architecture takes preference over how I might imagine it, but let’s say you have a class something like

class FoodPortion
{
    public float carbCalories;
    public float proteinCalories;
    public float fatPerc;
    public float cost;
}

And as above, you’ve got a few needs for the character. Let’s say they look like this:

const float CALORIES_REQD_PER_DAY = 2000;
const float CARB_PERC = 0.5f;
const float PROTEIN_PERC = 0.2f;
const float FAT_PERC = 0.3f;

float myCurrentDailyCarbCount;
float myCurrentDailyProteinCount;
float myCurrentDailyFatCount;

We’ll also assume in this instance that the EatFood function looks something like this:

void EatFood(FoodPortion food)
{
    myCurrentDailyCarbCount += food.carbCalories;
    myCurrentDailyProteinCount += food.proteinCalories;
    myCurrentDailyFatCount += food.fatPerc;
}

What you want is to be able to evaluate based on different factors, and weight according to those. Stuff like the nutritional value to the character, the cost, as well other factors like the distance to the food (we don’t want the character walking all the way across town because something was marginally cheaper than something right next to them. Or do we? That’s where the weighting comes in).

For the nutrition value, the characters will want to keep track of what they need so that we can compare the benefits of a food portion.

// 0 - 1 values
float myCarbNeedPerc;
float myProteinNeedPerc;
float myFatNeedPerc;

void EvaluateMyFoodNeeds()
{
    //Will be zero if we're full on carbs, or 1 if we have none
    myCarbNeedPerc = CALORIES_REQD_PER_DAY * CARB_PERC - myCurrentDailyCarbCount;
    myProteinNeedPerc = CALORIES_REQD_PER_DAY * PROTEIN_PERC - myCurrentDailyProteinCount;
    myFatNeedPerc = CALORIES_REQD_PER_DAY * FAT_PERC - myCurrentDailyFatCount;
}

Then, we can take a hypothetical food portion and output a float based on our requirements.

float EvaluateFoodBasedOnNeed(FoodPortion food)
{
    EvaluateMyFoodNeeds();
    //Will return a smaller number if we don't need this or a larger number if we do
    float nutritionDesire = (food.carbCalories * myCarbNeedPerc + food.proteinCalories * myProteinNeedPerc + food.fatPerc * myFatNeedPerc);
    return nutritionDesire;
}

We can evaluate based on other factors too, like the aforementioned cost

float EvaluateFoodBasedOnCost(FoodPortion food)
{
    //Will return a smaller number for a higher cost or a larger number for a lower cost
    float costDesire = (1f / Mathf.Min(food.cost, 1f));
    return costDesire;
}

We’ll also want some weights for these evaluations based on how you want the behaviour to manifest itself. Do characters prefer cheaper, or more nutritious food? Will they travel for miles to eat a snack if it means they’ll gain a better nutritional benefit? These can be easily tweaked, or even dynamic based on the characters resources. Ultimately you’ll be adding up all these factors and simply choosing the best one for the character.

void ChooseFood(FoodPortion[] allAvailableFoods)
{
    int bestFoodIndex = -1;
    float bestFoodValue = 0f;
    for (int i = 0; i < allAvailableFoods.Length; i++)
    {
        float foodValue =
            EvaluateFoodBasedOnCost(allAvailableFoods[i]) * COST_WEIGHTING
            + EvaluateFoodBasedOnNeed(allAvailableFoods[i]) * NUTRITION_WEIGHTING
            + EvaluateFoodBasedOnDistance(allAvailableFoods[i]) * DISTANCE_WEIGHTING;
        if (foodValue > bestFoodValue)
        {
            bestFoodValue = foodValue;
            bestFoodIndex = i;
        }
    }
    FoodPortion chosenFood = allAvailableFoods[bestFoodIndex];
}

TL;DR: It depends on the needs of your game. Usually you’ll want some weighting to whatever factors you need that can be easily tweaked to affect behaviour. The choice algorithm just takes all these weights and decides which one is best overall.

Oh well, thanks for the long explanation, I got that part down, but you pointed to a weakness in my explanation.

Basically I want to find, not the best current food, but the “meal course” composed of a set of food portion such as it satisfies all the constrain. I’m not even sure how to actually express that lol.

Character aren’t autonomous, they are handled by a manager, and it automatically create a diet for the day for the character. Cost are actual money too and distance or access are not factored (it’s filtered by the set of food available), so it’s more of a management survival game.

Ah right, so if I understand correctly, you want the manager to ensure everyone gets a full balance of nutrients and calories, but from multiple sources of food portions throughout the day, and optimized for cost?

Is food infinite, or is it a finite resource in the world per day?

The pool doesn’t matter now, but I had somebody telling me it’s call the Stigler diet problem, which is exactly what I want but the problem is that I only found math notation and not pure code, especially not c# lol

Is this for a game or are you trying to optimize your personal food intake?

I’ve read a lot of articles on nutrition, both pseudo-science and science based, and it’s a really complicated topic. If you want to make a game around it, I think you need to greatly simplify and focus on the kinds of decisions the player will be making and evaluate if those will be fun. I have a hunch you are headed towards an overly complex simulation that will be very hard to understand and make good decisions on for players.

Edit: I added a Score function to the Food class which determines the value of the food based on 3 things:
How closely the macronutrients align with the desired ratios
How many calories the food has
How much the food costs
e.g. milk might satisfy all the macronutrient ratios, but not provide enough calories for the cost
The downfall of this method vs the random method is that you wont get any variety - some combination of the two methods might be a bit more interesting.
This could be made better by scoring the food based on the remaining fat/pro/carb calories rather than the total - might add this later on for fun :smile:

So I got a bit carried away and wrote a little console application to test out my theory which goes like this:

Take an array of food objects which have a name, protein per gram, fat per gram and carb per gram.

  1. get all the foods from this array which meet the dietary requirements using LINQ
  2. pick the food with the best score and add it to the meal plan
  3. keep a count of the calories, fat, protein and carb added
  4. repeat 1-3 until no foods meet the requirement

I ended up with this (I only have 5 foods defined):

Notice that it added a lot of lettuce at the end because the fat calories were fully allocated.
Also notice that the calories aren’t quite the at the max which I defined as 1800 - this is because 1 macro has reached its limit and there are no foods in the array with zero protein/fat.
My code says to stop adding food if it will exceed the limits for a certain macronutrient - you could modify this to allow some leeway.
Given a larger variety of foods, I think this would be a pretty decent solution for a game.

Please note I just made up the nutrition values for the food.

Anyway, here’s the code if you want to take a look. Let me know if you have any questions.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

/// <summary>
/// Defines a food and it's nutrients
/// </summary>
class Food
{
    public Food(string name, float grams, float carbsPerGram, float proteinPerGram, float fatPerGram, decimal price)
    {
        Name = name;
        CarbsPerGram = carbsPerGram;
        ProteinPerGram = proteinPerGram;
        FatPerGram = fatPerGram;
        Grams = grams;
        Price = price;
    }
    public string Name { get; private set; }
    public float CarbsPerGram { get; private set; }
    public float ProteinPerGram { get; private set; }
    public float FatPerGram { get; private set; }
    public decimal Price { get; private set; }

    public float Grams { get; private set; }

    public float GetCalories()
    {
        return CarbCalories() + ProteinCalories() + FatCalories();
    }

    public float CarbCalories() { return CarbsPerGram * 4 * Grams; }
    public float ProteinCalories() { return ProteinPerGram * 4 * Grams; }
    public float FatCalories() { return FatPerGram * 9 * Grams; }

    public float Score(float carbPercent, float fatPercent, float proteinPercent)
    {
        //calculate the food balance by checking the difference between the actual macro calories and the wanted percentage
        float nutrientBalance = GetCalories();
        nutrientBalance -= Math.Abs(CarbCalories() - GetCalories() * carbPercent);
        nutrientBalance -= Math.Abs(FatCalories() - GetCalories() * fatPercent);
        nutrientBalance -= Math.Abs(ProteinCalories() - GetCalories() * proteinPercent);
        return GetCalories() + nutrientBalance / (float)Price;
    }

}
class Program
{

    static void Main(string[] args)
    {
        //All available food
        var foods = new Food[]
        {
                new Food("Apple", 100, 0.1f, 0.05f, 0f, price: 1M),
                new Food("Porkchop", 150, 0.01f, 0.2f, 0.15f, price: 4M),
                new Food("Egg", 50, 0f, 0.05f, 0.2f, price: 0.5M),
                new Food("Lettuce", 80, 0.01f, 0f, 0f, price: 2M),
                new Food("Bread", 30, 0.33f, 0.05f, 0.05f, price: .2M)
        };

        float proteinRatio = 0.3f;
        float fatRatio = 0.2f;
        float carbRatio = 0.5f;

        //Variables to keep track of the allocated macronutrients
        float maxCalories = 1800;
        float calorieCount = 0;
        float maxProtein = maxCalories * proteinRatio; //0.5f = carb ratio
        float proteinCount = 0;
        float maxCarb = maxCalories * carbRatio; //0.5f = carb ratio
        float carbCount = 0;
        float maxFat = maxCalories * fatRatio; //0.5f = carb ratio
        float fatCount = 0;

        var mealPlan = new List<Food>();

        var rand = new Random();
        //Breaks when no more food can be allocated
        while (true)
        {
            //calculate the remaining macronutrients
            float remainingCalories = maxCalories - calorieCount;
            float remainingProtein = maxProtein - proteinCount;
            float remainingCarb = maxCarb - carbCount;
            float remainingFat = maxFat - fatCount;

            //Select foods that fit with our current diet
            var allowedFood = from food in foods
                              where food.GetCalories() <= remainingCalories
                                    && food.CarbCalories() <= remainingCarb
                                    && food.ProteinCalories() <= remainingProtein
                                    && food.FatCalories() <= remainingFat
                              select food;

            if (allowedFood.Any())
            {
                //var chosenFood = allowedFood.ToArray()[rand.Next(allowedFood.Count())];
                var chosenFood = allowedFood.OrderByDescending(x => x.Score(carbRatio, fatRatio, proteinRatio)).FirstOrDefault();
                mealPlan.Add(chosenFood);
                calorieCount += chosenFood.GetCalories();
                proteinCount += chosenFood.ProteinCalories();
                carbCount += chosenFood.CarbCalories();
                fatCount += chosenFood.FatCalories();
            }
            else
            {
                //No food matches requirements so DONT EAT ANYMORE
                break;
            }

        }

        Console.WriteLine($"Meal Plan ***************");
        Console.WriteLine($"Cost: ${mealPlan.Sum(x => x.Price)}");
        Console.WriteLine($"Calories: {calorieCount}");
        Console.WriteLine($"Calories from protein: {proteinCount}");
        Console.WriteLine($"Calories from carbs: {carbCount}");
        Console.WriteLine($"Calories from fat: {fatCount}\n");
        Console.WriteLine("Foods:\n");
        foreach (var food in mealPlan)
        {
            Console.WriteLine($"Food: {food.Name} \t Calories: {food.GetCalories()} \t Price {food.Price} \t Score: {food.Score(carbRatio, fatRatio, proteinRatio)}");
        }


    }
}
1 Like