Optimizing crop planting with mixed integer linear programming in Stardew Valley

post by hapanin · 2022-04-05T18:42:01.887Z · LW · GW · 4 comments

Contents

  Abstract
  Motivation
  Background
  Methods
  Results
  Discussion
  Footnotes
None
4 comments

Abstract

TL;DR: plant your crops like this and you will make the most money possible

Motivation

If you haven’t yet tried, consider escaping 2020’s cyberpunk hellscape into the 2D world of Stardew Valley, an open-world farming RPG where almost all actions lead to positive outcomes. The game is as non-punishing as they come, making it a superb unwinding after a long day or recurring date with your long-distance quarantined non-gaming significant other. Whether you spend your time watering blueberries, reeling in trout, or romancing the local townsfolk, most would say there is no wrong way to play as long as you’re having fun.

I disagree. A farm is a business and businesses should make as much money as possible by any means necessary[1]. Since farming provides the majority of a Stardew farmer’s income, it is therefore necessary to determine the rotation of crops which maximizes profit within the constraints of energy and available capital. If your fun is not optimized, you’re wasting valuable time.

Background

You arrive on your late grandfather’s farm with little more than a hoe[2] and a watering can. The mayor welcomes you with 15 parsnip seeds and you inherit 500 gold pieces. To plant a seed, you must first till the ground with your hoe, expending 2 of your 270 daily energy. For another 2 energy you then water the seeds that and every subsequent days until they are harvested. Harvesting takes no energy, so you should quickly pick your produce, sell it over at Pierre’s General Store and immediately buy more seeds, rinse, and repeat. Each season consists of 28 days, after which all crops die and cannot be harvested.

Additionally, you can forage materials at no monetary cost by cutting down trees and selling the wood and byproducts to Robin, the carpenter. Trees on average contain about 50g worth of materials and take 30 energy to harvest. Unlike crops there is no growing period and we will assume an infinite number of available trees[3].

In order to maximize profit, in general you should plant a lot of seeds, but not so many seeds that you cannot water them all in a single day, and also spend your excess energy foraging. One such algorithm could be:

Assuming we only plant parsnips, our farming and foraging strategy should look like this:

Benchmark farming and foraging strategy with number of crops presently in the ground shown on the Y axis.

As you can see, at first, crop planting is capital-limited. However, after one planting cycle, it becomes energy-limited at 67 parsnips, which requires just under 270 energy. In order to compensate for the planting energy, every harvest day foraging drops to 0.

Though straightforward to execute, this strategy suffers from several inefficiencies:

Benchmark planting algorithm profit & loss

It appears that an industrious but simple farmer can achieve on the order of 9735g in the first season. For the record, this would be enough gold to buy you about 2.5 goats.

Methods

But what would the mathematically inclined farmer do? As it turns out, this problem can be rearranged in to a mixed integer linear program (MIP), a type of linear program where some (in this case all) of the variables are integers. MIPs are NP-hard, but but we can still achieve optimal or good enough results with off-the-shelf solvers like CBC[4].

In order to solve a linear program, we must format our problem standard form, defining our constraints and cost function. In plain English, we want to:

Let us define our selection matrix Where is the number of crop planted on day . Each crop has several attributes associated with it, namely the price at which we buy seeds is The price at which we sell crops is The number of days a crop must grow before harvesting The energy used in planting a crop The energy used in watering a crop The number of days it takes a crop to regrow once harvested Note that only green beans regrow, so Furthermore, foraging must be treated as a crop which is harvested instantly but requires 30 energy to plant Given these parameters, we are trying to optimize profit, However, in order to avoid counting crops that would reach maturity after the season ends we must multiply our result element-wise by the timing relevancy matrix where Applying this to the profit expression, we have We must also derive our constraints. Firstly, we cannot conjure seeds and sell them back to Pierre Second, we cannot spend more than 270 energy in a single day. Since the daily energy expenditure is a function of both the crops planted that day and all crops still growing from previous days, we have our planting relevancy matrices Where And our watering relevancy matrices Where Which is to say that the the crop requires water on day only if the crop was planted before day and if either it still has not been harvested or it regrows.

Collectively, this gives us the constraints Where takes the sum of all elements in the matrix</p>

Third, we must construct our buying and selling relevancy matrices, respectively, since the available budget is the sum of all previous purchases, expenses, and the starting capital. Since there is a lag between buying seeds and selling mature produce, these matrices are in effect time shifted from each other according to the vector. The buying relevancy matrices are Where And our selling relevancy matrices Where Where indicates floor division. This is because recurring crops get sold an additional time every days after day . We can use the buying and selling relevance matrices to construct our final constraints: But wait! We forgot about Mayor Lewis’s fifteen parsnip seeds! We must edit our initial constraint To be Where And we adjust our budget constraint to reflect the additional 300g worth of parsnip seeds we have to “buy”. Therefore, our final problem in standard form is Subject to I coded this all up in Python using the CVXPY library and the CBC solver. You can check it out on github.

Results

When you run all the numbers, you end up with this:

Optimal planting strategy Optimal profit & loss

Discussion

The results agree with conventional Stardew wisdom. We begin by planting our 15 turnips and doing a lot of foraging while still cash-limited. This gives us the necessary cashflow to plant our first round of green beans as soon possible, since their regrowth property makes them the most profitable crop in the long run, assuming they are planted at the start of the season. We also plant a few cauliflower. Around day 4 the strategy gets interesting. At day 4 we plant a batch of garlic, followed immediately by a batch of kale as soon as the garlic matures. Once done planting the kale we switch back to garlic. The logic to this madness is to maximize the number of cauliflower planted on day 14, just in time to be harvestable on day 27. The late-spring cauliflower bonanza somewhat simplifies the second half of the season, which resembles the first half in that it consists of one batch of garlic, followed by a larger batch of kale overlapping a second batch of garlic. Finally at the end of the season with no time to harvest anything we briefly return to foraging as a somewhat profitable energy sink. Now that’s what I call crop rotation!

Unlike in the benchmarking case, we almost never have cash on hand until the end of the season. All profits are immediately reinvested in crops. The algorithm’s obsession with cauliflower like some sort of vegan baker makes sense given that cauliflower has by far the greatest sell:buy ratio at 2.19, while jazz flowers have a ratio of 1.6, and so aren’t planted at all. Parsnips also never get planted once our initial gift runs out, which makes sense given their low expense ratio of 1.75. Thanks for nothing Mayor Lewis.

Our net profit in the optimized case is 20,865g, a full 2.14x the baseline case! Now you too can work your farmer to the bone on an industrial cauliflower plantation. You might not have made any friends in town while you slaved away in the field, but you’ve probably earned the respect of Jojomart’s shareholders.

These results were solved to within 2% of optimal becuase I don't have the time nor budget for anything better than that. Trying to run the solver with tighter tolerance simply would not converge in a reasonable time frame. If you want to donate some server time to this noble science, suggest a similar project, or hire me, send me an email.

Footnotes


  1. I don’t actually believe this ↩︎

  2. Not your significant other ↩︎

  3. Time is also an important resource in Stardew Valley, but optimal pathing can typically eliminate the importance of this constraint. It is generally possible, though stressful, to cut down a number of trees, harvest all your crops, sell the wood to Robin, sell the crops to Pierre, buy your next round of seeds, till, plant, and water, in that order, before any shops close or the sun goes down. ↩︎

  4. I could not get the default CVXPY MIP solver, GLPK_MI to solve this problem. ↩︎

4 comments

Comments sorted by top scores.

comment by Vaniver · 2022-04-06T19:12:10.867Z · LW(p) · GW(p)

Note that conventional wisdom also has you push on your energy budget by eating some things instead of selling them.

comment by Starcher (stupidestanimations) · 2024-09-30T20:21:08.574Z · LW(p) · GW(p)

I know this article is two years old, but I think you swapped the indices in the bit of text after your first defined the selection matrix by mistake. That is, you wrote that j must be in the set of crops, and i must be in the set of days, when I’m pretty sure right before you said that i is the crop planted and j is the day.