Stardew Valley is a “small town farm life simulation” computer game made by Eric Barone (also known as ConcernedApe). It’s an open-ended game, allowing players to grow crops, raise livestock, fish, cook, mine, forage, and socialize with the townspeople. One of the most lucrative activities in Stardew Valley is growing and selling crops. It’s a relatively simple activity, but there are many variables that turns this into a really fun linear programming problem. Let’s take a look into the different crops of each season and write some different objective functions to minimize and maximize over different scenarios!

Introduction

We have a farm with a finite amount of tiles, a wallet with a finite amount of balance and a finite amount of days remaining in the current season. Our goal is, based on these and more variables, end the season with the largest profit possible (or some other arbitrary goal). There are four seasons: Spring, Summer, Fall, and Winter, where each season lasts 28 days, as well as each month.
Well, Wolfie, kinda. That’s a “yes” but only if your objective is to get the highest profit at the end of the season when you’re not considering plenty of variables:
◼
  • Time to harvest crop in days..
  • ◼
  • Time of crop regrowth in days..
  • ◼
  • Seed price..
  • ◼
  • Player balance..
  • ◼
  • Yield per crop..
  • As well as many other variables and scenarios, when your goal is not maximizing profit.
    For small problems, that is in fact feasible Wolfie. But for big enough problems, a global minimum/maximum solution can be unfeasible really fast! Let’s see if we could pull out a brute-force search in our current scenario, for the sake of simplicity, let’s choose only the crops that can grow in the summer..
    There are 13 possible crops. Let’s suppose that we have 50 tiles available in our farm, so we have all of the following search space, with no heuristics at all:
    13×13×...×13=
    50
    13
    Assuming that we can compute what we want to minimize/maximize every microsecond, this gives us..
    In[]:=
    UnitConvert[13.0^50*Quantity[1,"Microsecond"],"Hours"]
    Out[]=
    1.38314×
    46
    10
    h
    ≈1.3×
    46
    10
    hours! Yikes!!! A quick Wolfram|Alpha query can say some comparisons about this value.
    In[]:=
    WolframAlpha["1.3 * 10^43 hours","PodCells"][[6]]
    Out[]=
    ≈ 1.1×
    29
    10
    ×age of the universe (≈13.8​Gyr)
    Yeah, no chance. Using a method such as Linear Programming, we can employ some clever techniques to help us get the global optimal solution in a feasible amount of time..! So, without further ado, let’s model our problem!

    Storing and Retrieving Crop Data

    Before we start, here’s an Association containing information about pretty much every crop, so it’s not cluttered everywhere, and we have a single variable that stores prices and important information about all crops. I extracted all information from the Stardew Valley Wiki.
    Note: Some crops were removed because of their complexity and their extreme rarity, so the crops in CropData are easily accessible.
    In[]:=
    CropData =
    ;
    Here is a list of the crops in the game:
    In[]:=
    CropData[[All,"Icon"]]
    Out[]=
    Blue Jazz
    ,Carrot
    ,Cauliflower
    ,Coffee Bean
    ,Garlic
    ,Green Bean
    ,Kale
    ,Parsnip
    ,Potato
    ,Rhubarb
    ,Strawberry
    ,Tulip
    ,Blueberry
    ,Corn
    ,Hops
    ,Hot Pepper
    ,Melon
    ,Poppy
    ,Red Cabbage
    ,Starfruit
    ,Summer Spangle
    ,Summer Squash
    ,Sunflower
    ,Tomato
    ,Wheat
    ,Amaranth
    ,Artichoke
    ,Beet
    ,Boy Chok
    ,Brocolli
    ,Cranberries
    ,Eggplant
    ,Fairy Rose
    ,Grape
    ,Pumpkin
    
    So we can easily obtain information about every crop within a single variable. For instance, here’s some information about Corn.
    In[]:=
    CropData["Corn"]
    Out[]=
    Icon
    ,Price150,Yield3,Profit50,Harvest14,Regrowth4,Seasons{Summer,Fall},NameCorn,SymbolCorn
    All of the crops are structured with the following fields:
    ◼
  • Icon: The icon of the crop, just a visual aid.
  • ◼
  • Price: The amount of currency needed to buy a single seed of the crop (assuming the lowest price). When the Price is zero, it’s because the seeds cannot be bought, but rather only be found in the nature.
  • ◼
  • Yield: The yield of the crop, for instance, “Coffee” yields 4 beans per crop.
  • ◼
  • Profit: The base profit of the crop when selled.
  • ◼
  • Harvest: The amount of days it takes for the crop to be “harvestable”.
  • ◼
  • Regrowth (Optional): If the crop regrowths in a few days, this field will exist.
  • ◼
  • Seasons: A list of all seasons that this crop can grow.
  • Max Seasonal Harvest

    As we saw before, some crops can be harvested more than once, because they can regrow, in order to
    In[]:=
    ClearAll @ MaxSeasonalHarvest;​​MaxSeasonalHarvest[crop_] := Module{regrowth, harvest, yields},​​ ​​ (* Get the regrowth rate of the crop. *)​​ regrowth = Lookup[crop, "Regrowth", 0];​​ ​​ (* If the regrowth is zero, it's because the crop can't regrow, and the max harvest is one. *)​​ If[regrowth == 0, Return[1]];​​ ​​ (* Get the harvest rate of the crop. *)​​ harvest = Lookup[crop, "Harvest"];​​ ​​ (* Return how many times the crop can be regrown. *)​​ Ceiling
    28 - harvest
    regrowth
    ​​ ​​
    Testing for a few crops, we can see that carrots only yield a max harvest of 1.
    In[]:=
    MaxSeasonalHarvest[CropData["Carrot"]]
    Out[]=
    1
    But the crops that can be harvested more than once, do in fact yield higher max seasonal harvests.
    In[]:=
    MaxSeasonalHarvest[CropData["Green Bean"]]
    Out[]=
    6

    Universal Constraints

    In a farm, there’s a finite amount of crops that can be planted, and a finite amount of starting money and money between seasons. In pretty much all seasons there will be these constraints:
    ◼
  • Non-Negativity Constraint:
  • ◼
  • s
    1
    >=0∧
    s
    2
    >=0∧...∧
    s
    c
    >=0
    where
    s
    c
    is a seed packet of the crop
    c
    , since you can’t buy a negative amount of seeds.
  • ◼
  • Integer Constraint:
  • ◼
  • s
    1
    ∈∧
    s
    2
    ∈∧...∧
    s
    c
    ∈
    where
    s
    c
    is a seed packet of the crop
    c
    , since you can’t buy a fractionary amount of a seed.
  • ◼
  • Space Constraint: The sum of the amount of seeds bought must be less than or equal to the amount of “tiles” in the farm.
  • ◼
  • s
    1
    +
    s
    2
    +
    s
    3
    +...+
    s
    c
    <=t
    where
    t
    is the amount of tiles available and
    s
    c
    is a seed packet of the crop
    c
    .
  • ◼
  • Balance Constraint:
  • ◼
  • p
    1
    s
    1
    +
    p
    2
    s
    2
    +
    p
    3
    s
    3
    +...+
    p
    n
    s
    n
    <=b
    where
    b
    is the player balance available,
    s
    i
    is a seed packet of the crop
    c
    and
    p
    c
    is the seed packet price of the crop
    c
    .
  • Modeling Profit and Cost

    First, let’s model some functions that will be used to represent the profits and costs over a given season.
    In[]:=
    ClearAll @ SeasonalProfitFunction;​​SeasonalProfitFunction[season_] := Module{crops, names},​​ ​​ (* First, obtain all crops of the given season. *)​​ crops = Select[CropData, MemberQ[#"Seasons", season] &];​​ ​​ (* Select only crops that can be bought. *)​​ crops = Select[crops, #"Price" > 0 &];​​ ​​ (* Now, for each crop
    c
    n
    , do: (
    Max
    c
    *
    Y
    c
    *
    P
    c
    ) [max harvest times yield times profit]. *)​​ Total @ Table[(MaxSeasonalHarvest[c] * c[["Yield"]] * c[["Profit"]]) * c[["Symbol"]], {c, crops}]​​ ​​
    In[]:=
    ClearAll @ SeasonalCostFunction;​​SeasonalCostFunction[season_] := Module{crops, names},​​ ​​ (* First, obtain all crops of the given season. *)​​ crops = Select[CropData, MemberQ[#"Seasons", season] &];​​ ​​ (* Select only crops that can be bought. *)​​ crops = Select[crops, #"Price" > 0 &];​​ ​​ (* Now, for each crop
    c
    n
    , do: (
    Max
    c
    *
    Y
    c
    *
    P
    c
    ) [max harvest times yield times profit]. *)​​ Total @ Table[(c[["Price"]]) * c[["Symbol"]], {c, crops}]​​ ​​
    The objective function should be the profit minus the minus the cost.

    An Optimal Solution

    Now, let’s maximize the profit for all of the seasons!
    And there we have it! We can finally get optimal solutions with some values.

    A Naive Solution

    In order to compare how good the optimal solution really is, let’s do a naive strategy that buys the crop with the highest ratio possible.
    And in order to compare, graphically, the two solutions, let’s create a function called CompareSolutions.

    Comparing Naive vs. Optimal

    Now, let’s do a simple simulation: let’s compare the two solutions over the seasons, the naive strategy of planting and selling the only the most expensive crop, and the other one that uses LinearOptimization.
    Let’s assume that we have 50 tiles and 1000 starting gold, using CompareSolutions, let’s see what 10 seasons looks like:
    Let’s plot it visually so we can see the differences..
    In less than four seasons, the optimal solution was able to reach a profit of 25.6k, when the naive solution only made 8.5k, and that’s a..
    ..≈400% increase in profit from the naive solution to the optimal solution!
    Now, what if our farm keeps expanding? Let’s use the option “IncreasingTiles”, starting from 5 tiles and 1000 gold for 100 seasons.
    And that’s a..
    ..≈500% increase in profit from the naive solution to the optimal solution!

    Conclusion

    I really like optimization and linear programming, and this was a really fun scenario to apply it. There are some advanced mechanics such as Fertilizers, Irrigation and special skills such as “Agriculturist Profession” that my model does not consider, but I’ll leave the more complex implementation of these variables for a later post (or perhaps a collaboration). Thanks for reading!

    CITE THIS NOTEBOOK

    Maximizing profitability in stardew valley using linear programming​
    by Pedro Cabral​
    Wolfram Community, STAFF PICKS, March 31, 2024
    ​https://community.wolfram.com/groups/-/m/t/3150069