The Life of a Programmer

Simulating the way to victory: Bloons TD Battles

Simulations can provide curious insights; in this case a distinct advantage to winning a game. I recently encountered a perfect opportunity to run a simulation on the game “Bloons TD Battles”. There is a “money” aspect and I wanted to know what strategy would be best. Since the rules are quite simple, at least part of them, it shouldn’t be too hard to code.

The full code can be found here. It’s written in C++ using a few C++11 features. I’ve also put it up at ideone for quick experimentation.

The Game

This tower defense game has a two-player “defensive” mode. You and another player attempt to hold off an onslaught of balloons. The person who lasts longer wins. You can either buy towers (monkeys in this game) or invest the money to increase your income. It’s a somewhat tricky balance: lots of things to buy, four investment plans to choose from.

Each investment plan has an upfront cost and a revenue increase amount: spend money now to gain back more over time. The lowest plan is $50 for $2 income: spend $50 and at each income interval you get an additional $2. The next plan is $300 for $15. At first it looks better: buying 6 of the lower plan would cost the same, but get you less return ($12 as opposed to $15). The trick is that you normally don’t have enough money to buy the higher plan and would be forced to wait. During that waiting time you could be compounding more money with the lower plans.

The Plans

That was my basic question: is it worth it to wait for the higher plans? This is what I addressed first in my simulation. My initial results seemed a bit off due to a missing variable. Each time a plan is purchased it has a cool-off time before it can be purchased again. Watching the game I noted the numbers and created a plan data structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct plan {
    //plan acquisition cost
    int cost;
    //resulting increase in revenue
    int increase;
    //timeout before repeat purchase
    int regen;
};
std::vector<plan> plans({
    { 50, 2, 4 },
    { 300, 15, 13 },
    { 1000, 70, 60 },
});

Money in the game is an integral value therefore all monetary values are using int. There is also a fourth plan, which costs a lot more, that I’ve omitted. In practice you never have enough money to buy it: I was unable to determine it’s regen timeout.

Game State

A simulation requires a state. This state is modified on each iteration of the game. Here I use a 1-second interval as it matches the precision needed for this game. My state structure looks like below, plus several member functions to manipulate it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct game_state {
    // how much money we have now
    int money;
    //income on each income interval
    int income;
    //total money we had/have
    int total;
    //money spent on upgrades
    int spent;
    //money invested for income
    int invested;
    //block purchase of the plan for regen time
    std::vector<int> plan_timeout;
    //total elapsed time
    int time;

A step function does the one-step iteration. It adds the income to the money and updates the tracking stats. Additionally it iterates over the plan_timeout to model the plan purchase lockout time. My state object thus also models the core mechanics of the game. Often one finds the state as independent data and a separate driver class modifies it. If the rules of the simulation are complex that would be required, but here the rules are simple enough, and static, to be situated in the state class.

Strategy

With the basic modelling complete a strategy is now required. A strategy models the player’s behaviour in the game. At each step in the simulation the strategy is called and allowed to perform its actions. This is the perfect scenario for polymorphism with inheritance and a virtual function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct strategy {
    void step(int spend) {
        /*common behaviour*/
        .
        .
        .
        state->step();
        step_impl();
    }

    virtual void step_impl() = 0;
};

The approach I’ve taken is to implete a step function as well as a virtual step_impl. This allows me to implement common behaviour across all strategies in the base class and specialized behaviour in derived classes. I’ve also also chosen to store the state directly in each strategy; the strategy step function is responsible for calling step on the state. I don’t believe in overdoing the design and this setup is sufficient for this simulation.

Baseline Strategy

A couple of basic strategies establish the baseline for our simulation. The first is the buy_none strategy which never invests in any plan. It sticks with the starting income level for the duration of the game. The second is buy_all which buys every plan it can whenever it can.

Before I show any results I must explain the stats I track first. The total money in the game is the total amount of money the player ever had (starting money plus all income). It’s actually not an interesting number. The spent money is more interesting. It tracks how much money was actually spent on building defenses — without these you lose quite quickly. In my initial version no money was “spent” though, so this number is just how much money was left over at the end. It is essentially the disposable income, and it is the interesting number.

For the first round the buy_none strategy ends up with only 3950 spent, and the buy_all has 9060. Clearly investing looks better.

Spending

Investment is obviously not the goal of the game. You need to build defense to block those pesky balloons from getting past. This means a strategy that doesn’t consider spending may not be valid. The ideal way to spend the money is not easy to simulate as there are a lot of factors involved. However, I can estimate roughly what the rate of spending must be.

I created a very simple formula of multiplying the elapsed time by a constant factor. At each interval the baseline strategy ensures it has spent at least this much money. If not enough money is available it will check again next interval.

Adjusting the value does change the results. Lower values tend to improve the value of investing. That is of course at the expense of possibly letting balloons through and loosing.

Fixed plans and stopping

I added a few other simple strategies that purchase only one particular plan. Here it came out that buy_all and buy_plan_1 looked to be quite comparable in spent money. I realized though that it doesn’t make sense to invest right up to the end of the game: a certain period of time is needed to recoup the investment. I needed a strategy that stopped investing after some point.

This is a modification to all existing strategies. Rather than complicate the hiearchy I just added a stop_when variable to the base strategy and introduced a should_buy_plan function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct strategy {
    //stop investing at this time, if -1 then never stop
    int when;
    .
    .
    .
    virtual bool should_buy_plan( int i ) {
        if( stop_invest_when >= 0 && state->time >= stop_invest_when ) {
            return false;
        }
        return state->can_buy_plan(i);
    }
};

When to stop depends highly on how long the game actually lasts. So far my games have been topping out at seven minutes. I run the simulation with a variety of stopping times. Stopping at 240s is roughly ideal for the buy_all strategy, and at 300s for the buy_play_1 strategy. They both have roughly the same spent money. Interesting though is that stopping at 180s for buy_all is only 2% less money. Since the game is hectic it is helpful to stop earlier and focus on other things.

Conclusion

The results are somewhat unfortunate. Investing is clearly meant to play a significant role in the game, but the influence is quite limited. A good strategy is to buy whatever plan you can, whenever you can, and stopping around half-time. Changing many of the constants in the simulation change the total money spent, but the same strategy is usually one of the best. It is also has the great advantage of being very flexible: you are never waiting to accumulate money to invest.

Finding this desired strategy is of course the purpose of the simulation. I also just enjoy writing simulations; they are somewhat different from my typical daily programming tasks.

Please join me on Discord to discuss, or ping me on Mastadon.

Simulating the way to victory: Bloons TD Battles

A Harmony of People. Code That Runs the World. And the Individual Behind the Keyboard.

Mailing List

Signup to my mailing list to get notified of each article I publish.

Recent Posts