Creating a Starcraft AI – Part 2: Messing with crabbos

First part of this series | Previous | Next | Index

In the last part we examined that even placing buildings isn’t quite a simple task. Today, we will examine it harder, and also find out things harder, while also fighting against the ever-present hovercrabbery.

Image result for mess with crabo
Yeah, i’m not afraid of your dumb ass

We (hopefully) solved the resource allocation problem. For Zerg, there is no repairs, so we don’t have to keep an eye out for that slowly draining our bank. However, a simple passing unit at the allocated BuildingTile can deny our morphing. We should be ever-vigilant of errant crabs.

There are four stages that a planned building can be in. Planned, worker on the way to construct it, morphing, and done. The latter is important, because until the building is completed, we can’t train the relevant units.

Basically

I’ll represent these with an enum in the PlannedItem class, and get rid of the isInProgress variable. We need to rework – yet again – the assignment logic, and add some new ones, for the numbered transitions. I didn’t add to the diagram Step 0, when a PlannedItem gets added to the queue, and Step 4, when it is done. Step 0 is not important right now, however, and Step 4 is just housekeeping: After stuff is done, just throw it out from the queue.

First, we should rework the main loop, according to the item’s status. 

For PLANNED: There is another concern, which I didn’t talk about last time. What if two items have the same importance? Then we shouldn’t exit the loop after the first one’s creation didn’t succeed. Let’s keep in mind the last item’s importance, and if we can’t build something, then still check until we encounter a lower importance.

//Add before checking the plannedItems
Integer lastImportance = Integer.MIN_VALUE;
Boolean skip = false;
//Modify the loop
		if (pi.getStatus() == PlannedItemStatus.PLANNED) {
				if (!skip) {
					if (pi.getImportance() >= lastImportance) {
						if (availableMinerals >= pi.getUnitType().mineralPrice()
								&& availableGas >= pi.getUnitType().gasPrice() && supplyUsedActual >= pi.getSupply()) {
							reservedMinerals += pi.getUnitType().mineralPrice();
							reservedGas += pi.getUnitType().gasPrice();
							availableMinerals = self.minerals() - reservedMinerals;
							availableGas = self.gas() - reservedGas;
							for (Unit unit : self.getUnits()) {
								if (unit.getType() == UnitType.Zerg_Drone && !unit.isMorphing()) {
									unit.build(pi.getUnitType(),
											getBuildTile(unit, pi.getUnitType(), self.getStartLocation()));
									pi.setStatus(PlannedItemStatus.WORKER_ASSIGNED); // Dude got this
									break; // This is important, one dude is enough!
								}
							}
						} else {
							lastImportance = pi.getImportance();
						}
					} else {
						skip = true;
					}
				}
			}

There are quite some things to unpack here. As you might have noticed, I got rid of the break statement, in favor of boolean flags. The last importance value is set to Integer.MIN_VALUE at the beginning – a custom value which has the property that it is smaller than EVERY other Integer value, except itself. So if for some reason, the importance values get screwed (You add some runaway loop), it will still function as intended. Other than that, it’s just nested if statements achieving the goal stated above. We also implement Step 1 here, assigning the worker. An idle crab is a useless crab.

Step 2: We just change the onUnitMorph() method.

	@Override
	public void onUnitMorph(Unit unit) {
		for (PlannedItem pi : plannedItems) {
			if (pi.getUnitType() == unit.getType()  && pi.getStatus() == PlannedItemStatus.WORKER_ASSIGNED) {
				reservedMinerals -= pi.getUnitType().mineralPrice();
				pi.setStatus(PlannedItemStatus.MORPHING);
			}
		}
	}

Step 3: Profit When a morph is finished, it triggers the onUnitCreate() method. We add our logic to this. But wait, if there are two units planned with the same type, how do we know which one is completed? This is important, for example, it matters greatly where I place my Sunken Colonies, and i’d like to keep track of it.

Fortunately (kind of), buildings, except Extractors have the same ID as the drone who built them. So when a unit is created, check the constructing drone’s id. Which we don’t store. I wonder what could we do to change that.

//Add this to the PlannedItem class, with getter-setter pair. 
private Integer builderId;

Now we can keep an eye on our crabbo. 

Image result for i'm watching you
Community Hovercrabbery Watch needs you

A good moment to record it’s ID would be when he (she? it? what gender do complete floating bio-retards belong to?) gets assigned.

//In our PlannedItem loop, before dude got this
pi.setBuilderId(unit.getID());
//Add this to our main class
	@Override
	public void onUnitComplete(Unit unit) {
		if (unit.getPlayer() == self) {
			for (PlannedItem pi : plannedItems) {
				if (pi.getBuilderId() != null && pi.getBuilderId() == unit.getID()) {
					pi.setStatus(PlannedItemStatus.DONE);
				}
			}
		}
	}

Now, as I mentioned before, Extractors are a whole different business. When an extractor starts to build, the drone get destroyed (deserves it, no problem yet), and a new Unit with a random id gets created, as you see here (the text below is the console, I just logged some events)

Yes, very data.

So what parameters are available? We know it’s our unit, and… that’s about it. Wait, we know where we want to put it! Let’s store that in the PlannedItem class. This is also helpful if the drone gets destroyed because he is an idiot – more on that a little bit later (Not the idiotism, but handling the destroy event). Brood War is weird, on creating an Extractor, the onRenegade() method gets triggered. Since in theory, this can happen for multiple reasons, let’s check the Position of the our traitor Extractor.

//Add this to PlannedItem. You know the drill. Except i'd add another constructor with this parameter.
private TilePosition plannedPosition;
//Modify the onUnitMorphs, for normal units
		if (unit.getPlayer() == self) {
		for (PlannedItem pi : plannedItems) {
			if (pi.getUnitType() == unit.getType()  && pi.getStatus() == PlannedItemStatus.WORKER_ASSIGNED) {
				reservedMinerals -= pi.getUnitType().mineralPrice();
				pi.setStatus(PlannedItemStatus.MORPHING);
			}
		}
//Modify the onUnitRenegade, for pretty princess extractors
	
	@Override
	public void onUnitRenegade(Unit unit) {
		if (unit.getPlayer() == self) {
			if (unit.getType() == UnitType.Zerg_Extractor) {
				for (PlannedItem pi : plannedItems) {
					if (pi.getUnitType() == UnitType.Zerg_Extractor
							&& unit.getTilePosition().equals(pi.getPlannedPosition())) {
						reservedMinerals -= pi.getUnitType().mineralPrice();
						pi.setStatus(PlannedItemStatus.MORPHING);
					}
				}
			}
		}
	}
//Extractors, again, in the onUnitComplete()
				if (pi.getUnitType() == UnitType.Zerg_Extractor && unit.getTilePosition().equals(pi.getPlannedPosition())) {
					pi.setStatus(PlannedItemStatus.DONE);
				}			

You might notice that I added the pi.setBuilderId(unit.getID()) line. This ensures, that in the onUnitComplete(), we’ll track this as well.

While implementing this, I encountered the following error of the wtf kind. The first of many. Upon your journey into Starcraft AI development, you shall master vanquishing them.

High quality incident report

It turns out, that the method i’m using, the one from the tutorial (getBuildTile()) considers geysers that are not visible as well. To the very fair question, as to why i’m not using a better method for this (e.g. anything else), my answer is: I will, but not at this point. I want to make my development fully transparent, and documented here, so when i’m switching to a better system, there will be an article about it. Anyway, to fix this:

//Just add the n.isVisible() part to the code
			if ((n.getType() == UnitType.Resource_Vespene_Geyser) &&
   					( Math.abs(n.getTilePosition().getX() - aroundTile.getX()) < stopDist ) &&
   					( Math.abs(n.getTilePosition().getY() - aroundTile.getY()) < stopDist ) &&
   					n.isVisible()
   				) return n.getTilePosition();

The next task is to handle drone-dying situations. That will happen, like, a lot, because they are the opposite of smart. 

At this point, there are two cases we need to take a look at. First, when the drone dies on the way to creating a building. In those occasions, we should add a check to the onUnitDestroy() method.

The second case is when a morphing building gets destroyed. Not strictly drone-dying, but I still count it as hovercrabbery.

//Add this logic to the main class
	@Override
	public void onUnitDestroy(Unit unit) {
		if (unit.getPlayer() == self) {
			for (PlannedItem pi : plannedItems) {
				if (pi.getBuilderId() == unit.getID()) {
					pi.setStatus(PlannedItemStatus.PLANNED);
				}
			}
		}
	}

Of course, Extractors are an edge case here. For one frame, this erroneously sets the Extractor item’s status to PLANNED – but on the same frame, the onUnitRenegade() method triggers, and set things right. I don’t know at the moment, if it’s going to cause problems. (When saying things like this, it’s 99% that they will)

So if the unit is morphing, and gets destroyed, no problem, we just re-plan it. But what if it is done?

//For the onFrame plannedItems loop:			
} else if (pi.getStatus() == PlannedItemStatus.DONE) {
				plannedItems.remove(pi);
			}

Technically, items with the status DONE can trigger onUnitDestroy() in the same frame, and get reset to PLANNED. Since one frame ago it was still building, I don’t think there is much difference in the two cases, we are most likely want to rebuild that.

But we are still not done with this loop. Technically, if we get a lot of resources at the same time, a drone could get double-assigned to build 2 items at once – I used the /cheats console command in-game (for that, you need to enable cheats with the game.enableFlag(1) line in the onStart() method), but it can happen naturally, if multiple morphs get canceled at once. Since builder ids are tracked now, there is a way to accomplish this.

Actually, no amount of supervision and opression is enough when it comes to drones, let’s just track their every move. There are a few useful things they could be doing (and a lot of useless ones, but they don’t need code for that)

  • Mining minerals
  • Mining gas
  • Morping a building
  • Fighting (last resort)
  • Scouting

They can only be doing one at a time. Okay, what should we use to categorize creatures? Racism. Enums, and maps. We only store the IDs of the workers, and try to only manipulate those, because BWAPI calls are expensive, and we are poor boiz.

Image result for please sir i want some more
Please sir, I want more CPU cycles.

The downside is that we are still working with these fucking crabs this map needs to be maintained as well. Every time a drone is created, or destroyed, we have to update our map. The default activity for crabbos is mining dem sweet minerals. 

//Some more bookkeeping, yee-haw
    enum WorkerRole { MINERAL, GAS, MORPH, FIGHT, SCOUT}    
    HashMap<WorkerRole, ArrayList<Integer>> workerIdsByRole = new HashMap<WorkerRole, ArrayList<Integer>>();
//In the onStart(), we want to fill the map with empty collections
		for (WorkerRole wr : WorkerRole.values()) {
			workerIdsByRole.put(wr, new ArrayList<Integer>());
		}
//If a drone is born, he is now a mineral boi. Add this to onUnitCreate()
			if (unit.getType() == UnitType.Zerg_Drone) {
				workerIdsByRole.get(WorkerRole.MINERAL).add(unit.getID());
			}
//Finally if a drone iz ded, remove it's ID in onUnitMorph(), and onUnitDestroy()
//onUnitMorph() - when a drone becomes a building
		for (ArrayList<Integer> list : workerIdsByRole.values()) {
			if (list.contains(unit.getID())) {
				list.remove(list.indexOf(unit.getID()));
				break;
			}
		}
//onUnitDestroy() - we actually need to check if it's a drone
			if (unit.getType() == UnitType.Zerg_Drone) {
				for (ArrayList<Integer> list : workerIdsByRole.values()) {
					if (list.contains(unit.getID())) {
						list.remove(list.indexOf(unit.getID()));						
						break;
					}
				}
			}

Now after this extensive management, we can finally get back to our onFrame() plannedItems logic, and do the crab slavery properly. Except what if we want to cancel a building?

OR WHAT ABOUT THE Extractor trick?

Oh, do fuck off.

(Until the next part, that is)

Ps.: I added the code of this article to GitHub

Leave a Reply