Plague Game Part 3: The Tech
Imagine guessing with 200K on the line
Generally I like to keep the ‘tech’ posts in these series dry and informative, but quite a bit more story slipped into this one due to how things went down.
IMPORTANT: If you’re familiar with the game, imagine a hypothetical world where the minimum cure rate is 3% and start reading with this in mind. You’ll find out why later.
Thinking about strategy
When I saw people talking about Plague Game and saying stuff like ‘I think that/ it seems like/ it probably would be best to <xyz>’ it literally gave me hives.
There’s probably a pure math solution to this, but I’m not very good at math. So I did the obvious thing for a developer to do, and built a simulator for the game.
At the time I started there weren’t many concrete details about the game, so I just built a bunch of strategies for how infecting and curing doctors might work and started playing around.
The simple strategy
The obvious, most simple strategy is to just let all your doctors die except for your target amount, and then heal those doctors whenever they get sick. So that was the first one I implemented to test.
The epoch count here is really high because I was guessing about the rules at this point.
The original ruleset didn’t have much in the way of strategy and outputted some crazy numbers like 600 epochs to completion. My guesses for the new rulesets produced 100-150 epochs. After the new sicken rate and potion resistance mechanics were announced, my simulator started spitting out much more sane numbers around 30-40 epochs.
The introduction of more mechanics meant that there were more possible strategies to pursue.
My first idea to outperform the simple strategy was to test overhealing (keeping more doctors alive than our target amount), and abandoning them once they’ve dropped below 100% cure rate. My intuition was that this was probably the optimal way to take advantage of the first 3 heals not being penalized.
Surprisingly this strategy had basically zero effect over the simple strategy. I had implemented this by abandoning doctors once they fell below % cure rate, with that percentage set to 100%.
On a whim, I tried swapping that percentage around, and started getting some different (including better) numbers. Since I had two different levers to pull, it was time to build a grid display for the simulator.
Exploring for better parameters
It wasn’t too difficult to add another loop over the sim and dump things to a table, and it immediately made clear that the heal abandonment % was a key factor in improving outcomes.
Most of the options outperformed the simple strategy by 2-3 epochs, which is great.
Keep in mind that for 8 doctors once cure resistance hits maximum, 2-3 epochs is an enormous amount of potions. So squeezing out those additional epochs gives more breathing room, and saves us from potentially having to buy 1500 AVAX worth of potions.
Narrowing to optimal parameters
I spent a fair amount of time running these simulators with a high iteration count, narrowing down the range of overheal and max heal %. I used some extremely advanced tech to identify the ranges to focus down on.
I took a screenshot and drew on it with my finger. The yellow lines represent good mean values, and the red lines represent good min values. Then I drew green boxes around the areas of interest that I wanted to focus on.
Meanwhile, fucking Grandma is out there doing color plots and 3D surface areas representing the data. Making me look bad.
What a piece of shit this guy is.
Anyway, we eventually settled on healing <target> + 32 doctors (AKA OH32), until they dropped to a 25% cure rate. I had a suspicion that the 25% cure rate being the cutoff has some relationship to the 50% sickening rate, but it could be unrelated. Math is hard.
We had some obvious competitors that we knew the doctor count of. I went back and ran my grid simulator on the competitors to see where they would end up.
The main variable I had to fiddle around with was the target doctor count. I used 2/3 for Smol Joes, and 1 for some of the other smaller accounts that were hanging around. I realized a couple of interesting things.
RNG was a huge factor for smaller accounts. I guess this shouldn’t be a surprise, but the more doctors you had, the smaller the range between the computed min and max epochs. These people targeting 1 doctor with 125 doctors in inventory had enormous ranges.
We were basically winning from the start. It wasn’t clear what strategy our competition would pursue, but even the optimal play my model was putting out had them at a severe disadvantage.
The ‘overhealing’ strategy had the massive benefit of us being able to change our strategy. If you target 8 doctors and you have 8 doctors alive, you can’t really go up to 10 later. Also, since healing more doctors is optimal regardless, you can decide to abandon more doctors later with no penalty to strategy.
Game time - data fetching
I prepared basically nothing in advance, so I was left scrambling a bit when the game launched. It actually ended up being quite annoying to fetch the data we needed, which was the ownership of every doctor/potion, and the status/potion count for each doctor.
At first I was scraping ownership from the Joepegs API, but that was slow, buggy, and turnover was kind of fast, so the data ended up not being high quality.
I switched to pulling everything from the smart contract, but that was also slow as shit, making 7500*4 queries serially. I swapped it to make queries in parallel, but it was still taking 10-15 minutes. Eventually I figured out how to make Multicall work, and I got a data refresh down to 30 seconds.
With this data in hand, it was relatively easy to build some code to summarize and announce game state to Discord.
We had a lot of doctors, and we were planning on letting an enormous amount of them die. So brewing potions seemed like a good option for us.
But I wasn’t willing to click on the stupid brew button a zillion times, or waste the gas to do so. It actually took 750K gas to try (and fail) brewing potions for 5 doctors, which is just outrageous.
So as a workaround, I wrote a thing that would estimate the gas cost to brew potions for every 5 doctors, with the assumption that the gas consumed when a brew was successful would be sufficiently higher than when it failed. It worked, I used it for the first epoch.
But Smitty almost immediately worked out how to directly compute which doctors are brewable, and even sniped a listed doctor off the market.
We ended up only sniping 2 doctors total over the course of the game. I later found out that someone else was doing the same thing, but much faster than us, and sniped about 8 brewable docs.
Brewing potions seemed to be annoying and disappointing to most people. Over the first 7 epochs we brewed 95% of the potions that were brewed, over 35 potions in total. But this dramatically slowed down; Smitty noticed the Plague team had incorrectly computed the brew rate, it was actually only 20% of the expected value.
This basically meant that in later rounds, maximum 1-2 potions would be brewable per epoch.
Fortunately for us, the Plague team messed up basically the whole thing. The earlier rounds were dramatically under the documented rate, but the later rates were more accurate. We were still brewing 2-4 potions per round, until the apothecary was shut down.
Later on while investigating another competitor who was brewing potions, I stumbled across this contract.
Someone had written a contract to brew potions, and possibly to feed doctors as well? I’m not sure why either of these was necessary. Perhaps it saves some gas to feed multiple doctors simultaneously, but it ends up not being much faster due to waiting for Chainlink VRF to return.
Annoyingly, I used interactions with this contract to group some addresses together into a single unknown competitor, but it wasn’t till much later in the game that I found out this caused me to miscategorize them; the other person had also discovered the contract and attempted to call it.
Things get boring
At 8 epochs in, I had basically wrapped up all my dev work and settled into a pattern. Every time the epoch rolled over, I would:
Scrape the latest game state
Brew potions (if any)
Potion all the doctors that needed it (using a script)
Re-scrape the game state
Run a simulation to make sure things are on track
Post an automated status update to discord
The main annoyance at this point was that I was trying to keep our accounts from being associated with each other, so I had to delay between potioning different accounts. I was too paranoid to let this be automated, so I did it manually every once and a while. During the day this wasn’t too much of a problem, but it was kind of irritating squeezing it in before bed.
Last minute panic
After 6-7 epochs it was time to start potioning our doctors according to strategy. After a few epochs, our doctors started accruing potion resistance.
The first time I potioned a doctor from 80% down to 40% I had a little panic attack. Was this worth it? Was our strategy wrong? My thought was that this 40% doctor had 20+ epochs to go. What are the chances that he survives that period when he’s already so damaged?
I coded up an alternative strategy that kept our overheal rate at 32 and our minimum heal at 25%, but used a sliding scale of maximum heal rates and ‘threshold’ doctor amounts. The idea being that if you have a lot of doctors then you’re early in the game, and it might be worth it to abandon damaged doctors.
I didn’t see a significant difference using this new strategy, and I had less confidence in the implementation, so I elected to continue using the original strategy. If I had more time to work on it and convince me of the legitimacy, I might have gone with it instead.
A minor glitch
One of the last things I did (stupidly) was write code to validate that the simulation values I had hardcoded in (cure rate by potion and sicken rate by epoch) matched what was in the smart contract.
There was an unexpected (and incorrect, according to the medium) tier for healing rate. By this point there wasn’t much we could do about our overheal rate, since our excess doctors had all died.
A MAJOR glitch
Here’s where things get awful and depressing. I wrote the code to validate the documented cure rates, plus 1 to ensure that they keep going.
Sounds great right? Unfortunately… there was an extra 95% cure rate in there. And I fixed my cure rates in code by copy pasting the rates out of the console. And then I reran my code and everything matched.
BUT I DID NOT INCREASE MY RANGE BY ONE
Therefore, I didn’t notice that the cure rate kept declining past 3%. In fact, I didn’t notice this until extremely late in the game, when we were down to 10 doctors.
Why this mattered
We had a model-produced optimal strategy, but of course I was interested in knowing why it was the right strategy. When I investigated, I determined that there basically three stages to the model’s play:
Let a ton of doctors die in the early game. The more doctors you have, the more that can get infected every round, and the more potions you might have to use.
Sacrifice the minimum amount of potions to keep the healthiest doctors alive for as long as possible in the mid game. The unluckiest doctors get abandoned along the way when they become too much dead weight.
Rely on the odds of curing doctors at 3% to drag the game out as long as possible in the end game.
With a 3% cure rate, a target of 8 doctors, and 50% infection rate, you can assume you will spend around 18 potions per round per doctor, so 150 potions per round. With the huge stockpile of potions we’d have remaining, that can last a very long time.
But that doesn’t work AT ALL with a steadily decreasing cure rate. Oops.
At this point, there wasn’t too much we could do in the way of strategy. We had already let way too many doctors die. The only things we could do were decide how many potions we were willing to dump into docs before abandoning, and take ‘loaner’ doctors from other players; basically ‘healing as a service’, we attempted to heal people’s doctors in exchange for a hypothetical percentage if they made it to the end.
Ultimately we still cleared 4 doctors, so I didn’t have to commit sudoku. But it was still an enormous letdown.
If I could do it again
I suspect the strategy I investigated halfway through the game (with the sliding scale of abandonment) would have significantly outperformed the one we ended up using.
But if I had enough time to prepare (and a significant chunk of compute) I would probably have trained a neural net to decide which doctors to heal and which to abandon. It seems like a problem very amenable to training with discrete inputs and outputs and a clear maximization function. I bet it would have taken ages to train though.
I have material for probably 1-2 additional posts on the Plague Game, but going from forecasting a crushing victory to barely eking out a mulligan really saps your motivation to talk about stuff. We’ll see what happens.
This was an enormously stressful week for me. I’m expecting my second child next month so I’ll probably be taking a hiatus from further shenanigans (and writing) for a while.
I hope you’ve enjoyed this series.