Creating a Starcraft AI – Part 1: (Almost) building two buildings

First part of this series | Previous | Next | Index

So, in the last post, I explained some basic concepts at a glance, and poorly. I intend to continue this ungentlemanly behavior in this article as well.

First, let’s open our trusty IDE, and take a look at what we want to achieve. I modified a few things in the example bot, like setting the starting race to Zerg, and adding some bookkeeping. There is a very good saying for this, with which I tend to agree: “What gets measured, gets improved” 1

public static int frameCount = 0;
public HashMap<UnitType, Integer> unitCounts = new HashMap<UnitType,Integer>();
public HashMap<UnitType, Integer> unitsInProduction = new HashMap<UnitType, Integer>();
public int supplyUsedActual;

The framecount is – surprise – for counting frames. Frames are the heartbeat of the game, on Fastest speed, there are 23.81 frames per second. During bot development, this is the de facto measurement of time, as in testing, and replays, you’ll probably speed up the game a lot. It is also a finer metric.

The unitCounts variable is  for tracking all our units. Useful for adjusting unit compositions, evaluating worker count, and such accounting activites. Credit to Simon Prins’ Tyr bot for the code. HashMap is not only convenient for this, but provides O(1) access for us, so it’s efficient. We don’t want to be wasteful with our resources, otherwise we will anger the Computer Science Wizard. (Or worse, we make kittens cry)

Don’t mess with him

The unitsInProduction variable does the same for units not yet complete. (What a surprise). We want to count those if we plan to train X units.

Counting units is relatively simple, counting ones in production is a little trickier. For other races i’d count the production queues of buildings – well, for Terran I did anyway. In this code (which was for my bachelor’s thesis, and it’s kind of rush job) I check if the unit’s type is a building, when i’m looping through my units anyway, and do things with the training queue. Horrible things. I had my reasons for this, but this is not about that bot, so let’s leave the topic. For Zerg, it’s different, since units don’t come from buildings.

Here, let’s clear up some terminology. I use the term unit as used commonly: workers, and attacking units (And dropships and such, but you know what I mean). In BWAPI, basically every object on the map that you can interact with, is in the Unit class. This includes minerals, geysers, neutral buildings, and even critters. Buildings are units too. One might argue that they are absolute units.

The supplyUsedActual value is a workaround. The game.getSupplyUsed() method is only reliable without latency compensation enabled (game.setLatcom()). Since this is enabled by default, and generally a good thing to have, we must do this manual bookkeeping. It’s not really hard, just sum up the unit.getType.supplyRequired() values, and for eggs, the same value for the getBuildType(). Also, these values are doubled compared to the numbers shown in the top right corner.

Since we are efficient, this can be done by looping through all my units once. Here is our glorious counting code:

	public void countAllUnits() {
		unitCounts = new HashMap<UnitType, Integer>();
		unitsInProduction = new HashMap<UnitType, Integer>();
		supplyUsedActual = 0;
		for (Unit myUnit : self.getUnits()) {
			supplyUsedActual += myUnit.getType().supplyRequired();
			unitCounts.put(myUnit.getType(), unitCounts.getOrDefault(myUnit.getType(), 1));
			if (myUnit.getType() == UnitType.Zerg_Egg) {
				supplyUsedActual += myUnit.getBuildType().supplyRequired();
				unitsInProduction.put(myUnit.getType(), unitsInProduction.getOrDefault(myUnit.getType(), 1));
			}
		}
	}

But wait, isn’t it more efficient if i do this exactly once when the game begins, the only modify when an unit gets created, or destroyed? – asks the CS Wizard. First of all, shut up, nerd, second, yes, it probably is, but i’m deliberately not doing this at this point. I will rewrite and test this later, when i’m talking about those methods. For now, we have to waste the precious lives of our CPU cycles.

After our bookkeeping code is done, let’s talk about constructing buildings. A common – cheap – tactic is 4 Pool. Don’t train any drones, just get money for a Spawning Pool, make as many zerglings as you can, and rush the enemy. Okay, this is easy, if have 200 minerals, build pool. Just select a worker, and…

Oh. How do I select a worker? At this point, I know how many I have, but nothing else about them. No problem, just loop through all my units, and if it’s drone, just build a Spawning Pool.

for (Unit unit : self.getUnits()) {
	if (unit.getType() == UnitType.Zerg_Drone && self.minerals() >= 200) {
		unit.build(UnitType.Zerg_Spawning_Pool, getBuildTile(unit, UnitType.Zerg_Spawning_Pool, self.getStartLocation()));
	}

To shorten the code, and make your life easier, you can add the static import of the bwapi.UnitType.*, but for clarity, i’ll avoid it. Static imports can be handy, if you don’t overuse them (Although this is a whole debate in itself).

Back to the building. Okay, we got a worker to do that, but this loop runs on every frame. Also, we don’t really exit the loop, so this will achieve that all 4 of our workers will rush to build a Spawning Pool, then block each other from doing so, and they end up screwing around like the dumb hovercrabs they are. Eventually, they manage to place one, but one drone would have been enough for this.

Just look at those idiots.

Worse, they will repeat this once I have 200 minerals the next time. So the Spawning Pool has to have at least two variables tracking it’s state. Do I have one already? Is some dude going to build it?

boolean doIHaveASpawningPoolAlready;
boolean isSomeDudeGoingToBuildASpawningPool;
//Okay, stop, no.

While this is a deliberately bad example, in essence, this is what we need to do. I think we might possibly want to build other buildings in the future, so we need to generalize this. Let’s have some object which tracks these shenanigans. As we saw in the previous article, and in the link above, we might as well add a supply count for these. We will modify this class quite a lot in the future.

public class PlannedItem {
//Getters, setters, and constructor not shown here.	
	private UnitType unitType;
	private Integer supply = 0; //If we don't set this, it means "Build it at your earliest convenience, Mr. Computer."
	private boolean isInProgress = false;

}

//Add this to our main class
ArrayList<PlannedItem> plannedItems = new ArrayList<PlannedItem>();

But wait, we are not checking if we already have one! Yes, we should, but not here.

Let’s add the Spawning Pool to our planned items list, then in the onFrame(), check the prerequisites for it. Actually, since we are likely to have less items in this list than units (sometimes zero), just loop through the planned items, and check there.

//Add this to onStart()
plannedItems.add(new PlannedItem(UnitType.Zerg_Spawning_Pool, 0, false));

//Check a lot of things
		for (PlannedItem pi : plannedItems) {
			if (!pi.isInProgress() && self.minerals() >= pi.getUnitType().mineralPrice()
					&& self.gas() >= pi.getUnitType().gasPrice() && supplyUsedActual >= pi.getSupply()) {
				for (Unit unit : self.getUnits()) {
					if (unit.getType() == UnitType.Zerg_Drone) {
						unit.build(pi.getUnitType(), getBuildTile(unit, pi.getUnitType(), self.getStartLocation()));
						pi.setInProgress(true); //Dude got this.
						break; // This is important, one dude is enough!
					}
				}
			}
		}

If we don’t have a Pool yet, when a Drone starts to build one, the inProgress variable will be true, and we don’t check this again and again.

Let’s see how this works out.

Aw yes, good crabbos.

We built a Pool, while the other drones are still mining! This is basically victory! There are some dark clouds on the horizon, though. What if due some dumb hovercrabbery, like having the audacity to die from some marines, the Spawning Pool is not built, then no new attempts will be made to build it.

Second problem, if we have two buildings with the same supply, the cheaper one will always be built first, as our mineral supply is steadily going up. This is not always beneficial, we might want to have a hatchery, then build 6 sunken colonies, for example.

Let’s have just another property for our items, let’s call it importance. Our main thought is, build the more important stuff first, and if other stuff is less important, then just save up for that. I’d say, let’s start at 0 for the least important, and just increase that stat when necessary. This also creates a natural ordering, where we just have to check from top to bottom. We do that every frame, but inserting stuff there is relatively rare. We shall ask the Computer Science Wizard, if he has an efficient data structure for us.

The answer is a priority queue. Let’s set it up so the peek() method gets the element with the highest importance.

public class PlannedItemComparator implements Comparator<PlannedItem> {
	@Override
	public int compare(PlannedItem x, PlannedItem y) {
		if (x.getImportance() >= y.getImportance()) {
			return -1;
		}
		else if (x.getImportance() < y.getImportance()) {
			return 1;
		}
		return 0;
	}
}

//And change our plannedItems to this
    PriorityQueue<PlannedItem> plannedItems = new PriorityQueue<>(new PlannedItemComparator());

Nice! We need a little modification to our building logic also

		for (PlannedItem pi : plannedItems) {
			if (!pi.isInProgress()) {
				if (self.minerals() >= pi.getUnitType().mineralPrice() && self.gas() >= pi.getUnitType().gasPrice()
						&& supplyUsedActual >= pi.getSupply()) {
					for (Unit unit : self.getUnits()) {
						if (unit.getType() == UnitType.Zerg_Drone && !unit.isMorphing()) {
							unit.build(pi.getUnitType(), getBuildTile(unit, pi.getUnitType(), self.getStartLocation()));
							pi.setInProgress(true); // Dude got this
							break; // This is important, one dude is enough!
						}
					}
				} else {
					break;
				}
			}
		}

If we try this out, we will find, that a Pool gets built, then a whole lot of nothing happens. That seems like a step backward, isn’t it? Upon debugging, we find out, that as soon as we have 200 minerals, a drone is selected, then immediately the same drone gets selected, because we have 75 minerals as well. We know that those are the same minerals, and we can’t spend them twice, the computer isn’t, yet. The self.getUnits() collection is ordered some way, that might change, but generally, we get the units in the same order. We could pick one at random, but doesn’t solve our problem either. Let’s make a note of this, and leave it for later.

The problem’s core is that the minerals don’t get spent until the worker arrives, and begins morphing. Until that, we should reserve the resources for it. We could do this locally, in this loop, but it’s feasible that minerals get spent some other way, so i’ll use a variable in our main class (Actually, four).

Also, we need to no longer reserve those amounts, as the morphing commences. For this, we need to do a verification in the onUnitMorph() method.

New, better version:

 public static int reservedMinerals;
 public static int reservedGas;
 public static int availableMinerals;
 public static int availableGas;
//Add this to onFrame()
availableMinerals = self.minerals()-reservedMinerals;
availableGas = self.gas() - reservedGas;
//Modify our logic to compare these amounts, and to reserve stuff for us

//Finally, override the onUnitMorph method
	@Override
	public void onUnitMorph(Unit unit) {
		for (PlannedItem pi : plannedItems) {
			if (pi.getUnitType() == unit.getType()  && pi.isInProgress()) {
				reservedMinerals -= pi.getUnitType().mineralPrice();
			}
		}
	}

And if we try out, we find that in 9 out of 10 cases, a Spawning Pool, then a creep colony gets built. The remaining one is due serious hovercrabbery, but that’s a topic for the next part of this series, where we explore why this is still not good enough.

Thanks for reading!

ps.: I uploaded the completed example to GitHub

  1. Insert obvious dick joke here

Leave a Reply