Creating a Starcraft AI – Part 3: Extractor trick and The Lööp

First part of this series | Previous | Next | Index

Last time, we molested some crabs. This is the kind of activity that is never done, so let’s continue with it. The latest problem of ours is midlife crisis the state transition when a building is canceled.

As previously mentioned, when a building is morphed, it gets the builder drone’s ID. Canceling that building triggers another onMorph() event, this time the building morphs to a drone. Except for Extractors (of course), when the Extractor is destroyed, a new drone is created.

Since our idle drones automatically get assigned to gather minerals at the nearest mineral field, this does not concern us at this moment. In order to perform the Extractor trick, the following tools need to be available:

  1. The ability to cancel a building
  2. Recognizing that we have some amount of units in some state currently (One extractor morphing)
  3. Accessing those exact units.

First point: Let’s extend the PlannedItemStatus class with a CANCEL value. This means we don’t want that unit to exist, and after calling the unit.cancelMorph() it should be put on DONE. The general idea with the item planning is that, eventually, some real smart logic will manipulate it, and replace, delete, and request stuff – but that comes much later. Oh, but we also need to keep track of the unit’s actual ID. We only have the builderID in the PlannedItem class, and that’s no longer sufficient for our purposes.

You might have noticed that i didn’t name the PlannedItem PlannedBuilding – my plan is to have fighting units, and even useless crabbos stored in my main queue. 

Drone getting into the plannedItems priority queue, circa
2018 (colorized)

So let’s have an unitId in our PlannedItem as well, and set this when we set a unit to MORHPING status. This works well in most of the cases – except for zerglings, and scourge, where one larva morphs to egg, then ling/scourge, and another is just get created. Yeah, let’s just, like, not deal with this right now, man.

But to achieve that, we need some (efficient) way to get the unit. We store the builder id, but looping through all the units to get the one is cumbersome. Also, it is the kind of thing that angers the Computer Science Wizard. So we turn to our old friend, the HashMap. Yes, yet another collection we should maintain… Fortunately we only need to do that when a unit is created or destroyed. We don’t strictly need to even know the unit’s type – after all, that can change. In the future, if any interaction of a certain unit type is needed, only the IDs need to be kept in mind.

Képtalálat a következőre: „i am the lord of darkness”
He might be not the Computer Science Wizard himself, just one of his enforcers. Still, better watch out.

Just to be completely out of order, let’s talk about step 2: Units, and their states. I created a PlannedItemPrereq class, and added a list of that into the PlannedItem class. We have three fields, the unitType, the amount, and the morphing boolean value. We have a list of the prereqs, so we can specify that if we have 4 zerglings, 5 ultralisks, and 2 Sunken Colonies morphing and there is a full moon then get on with this item. 

All of the above, in code:

//More bookkeeping! I hope i excite some accountants today
HashMap<Integer, Unit> unitsById = new HashMap<Integer, Unit>();
//Only keep track our own units in this
//In onUnitCreate:
if (unit.getPlayer() == self) {
			unitsById.put(unit.getID(), unit);
}
//In onUnitDestroy:
	if (unit.getPlayer() == self) {
			unitsById.remove(unit.getID());		
	}
//Add this to PlannedItem, with getters and setters
private Integer unitId;
//This to onUnitRenegade, and onUnitMorph
pi.setUnitId(unit.getID());
//And for the cancel, this into our main plannedItems loop
//Add getters and setters, and a little salt
public class PlannedItemPrereq {
	private UnitType unitType;
	private Integer amount;
	private Boolean morphing;
//PlannedItem:
	private List<PlannedItemPrereq> prereqList = new ArrayList<PlannedItemPrereq>();

Okay, the validation logic seems simple (You know where this is going..). If we have X amount of stuff, build thing (9 drones, build an Extractor, got it). If we have Y amount of stuff, cancel thing (8 drones, 1 drone morphing). But wait, X!=X, what now? Yeah, we need another list of prerequisites for cancellation. Also, a boolean value to signal Mr. Computer to bother with cancellation. In our main plannedItems loop, let’s check stuff that are morphing (Can’t really cancel otherwise)

Fortunately, we already kind of keep track of the amount of our units, and how many of them are in production. Unfortunately, we need to rework that logic a bit. By a bit, I mean completely.

//This is better. Source: dude, trust me
	public void countAllUnits() {
		unitCounts = new HashMap<UnitType, Integer>();
		unitsInProduction = new HashMap<UnitType, Integer>();
		supplyUsedActual = 0;
		for (Unit myUnit : self.getUnits()) {
			if (myUnit.isMorphing()) {
				if (myUnit.getType() == UnitType.Zerg_Egg) {
					supplyUsedActual += myUnit.getBuildType().supplyRequired();
					unitsInProduction.put(myUnit.getBuildType(), unitsInProduction.getOrDefault(myUnit.getBuildType(), 0)+1);
				} else {
				unitsInProduction.put(myUnit.getType(), unitsInProduction.getOrDefault(myUnit.getType(), 0)+1);
				}
				
			} else {
			supplyUsedActual += myUnit.getType().supplyRequired();
			unitCounts.put(myUnit.getType(), unitCounts.getOrDefault(myUnit.getType(), 0)+1);
			}
		}
	}

//And FINALLY, extend our PlannedItem loop to check the prereqs. In the PLANNED case:
Boolean prereqsOk = true;
for (PlannedItemPrereq pip : pi.getPrereqList()) {
	if (pip.isMorphing()) {
		if (!(unitsInProduction.getOrDefault(pip.getUnitType(), 0) >= pip.getAmount()))) {
			prereqsOk = false;
			break;
		}
	} else if (!(unitCounts.getOrDefault(pip.getUnitType(), 0) >= pip.getAmount())) {
		prereqsOk = false;
		break;
	}
}
if (pi.getImportance() >= lastImportance) {
	if (availableMinerals >= pi.getUnitType().mineralPrice()
			&& availableGas >= pi.getUnitType().gasPrice() && supplyUsedActual >= pi.getSupply()
			&& prereqsOk) 

Oh, but this complicates our importance logic somewhat.
What happens, if for example, we have money, but the prereqs aren’t satisfied? Should we step down to the next highest importance? Well, this problem now goes on the shelf of broken dreams, and i’ll completely ignore it until the point when it will painfully manifest, and will be much harder to fix. Like any professional software developer.

Since units are now a plan of our build order, we need to manage our larvae to spawn them (Or is it larvas? Larvae sounds fancier, so I’m gonna use that). Hello darkness PlannedItem loop, my old friend.

Képtalálat a következőre: „bröther lööps”
Yes, the same fucking one every time though

The meaningful change here is that we need to check if the item is a building or a unit, and get a larva or a drone accordingly. That’s all and well, but if we generalize our PlannedItem, why not go all the way, and include upgrades as well?

Oh bother. Maybe I should plan this in full before dwelving into coding. Nah, fuck it, I’m sure it will be fine, and nothing like this will ever happen again.

In BWAPI, there are two objects for upgrades, TechType, and UpgradeType. Techs are stuff that gives you new abilites (Like Psionic Storm), Upgrades are stuff that kill the enemy more deader, or your units more aliver, like armor/gun upgrades – generally speaking. The line between those two are somewhat arbitrary in practice.

Field Manual for jumpy doggos

Below are the code modifications so far. I didn’t remove the juvenile comments – the joys of just coding for my enjoyment. (I also don’t get paid, so it’s fair)

//Modify the PlannedItem class this way. 
public enum PlannedItemType {
		BUILDING, UNIT, TECH, UPGRADE
	}
	public PlannedItemType plannedItemType;
	private UnitType unitType;
	private TechType techType;
//Technically, we can set these wrong, but we are very smart, and won't do that
//Öür lööp, revisited the nth time
	if (pi.plannedItemType == PlannedItemType.BUILDING) {
								for (Unit unit : self.getUnits()) {
									if (unit.getType() == UnitType.Zerg_Drone && !unit.isMorphing()) {
										TilePosition plannedPosition = getBuildTile(unit, pi.getUnitType(),
												self.getStartLocation());
										unit.build(pi.getUnitType(), plannedPosition);
										pi.setPlannedPosition(plannedPosition);
										pi.setBuilderId(unit.getID());
										pi.setStatus(PlannedItemStatus.WORKER_ASSIGNED); // Dude got this
										System.out.println(pi.getUnitType() + " : worker assigned" + pi.getStatus());
										break; // This is important, one dude is enough!

									}
								}
							} else if (pi.plannedItemType == PlannedItemType.UNIT ){
								System.out.println("ABSOLUTE UNIT");
								for (Unit unit : self.getUnits()) {
								if (unit.getType() == UnitType.Zerg_Larva) {
									pi.setUnitId(unit.getID());
									unit.morph(pi.getUnitType());
									System.out.println("Larva with id:" + unit.getID() + " selected to morph into " + pi.getUnitType());
									break;
								}
								}
//State changes need to be updated as well - the onUnitMorph, and the onUnitComplete
//		for (PlannedItem pi : plannedItems) {
			} else if (pi.plannedItemType == PlannedItemType.UNIT && pi.getUnitId() != null 
					&& unit.getID() == pi.getUnitId() && unit.getBuildType() == pi.getUnitType()) {
				reservedMinerals -= pi.getUnitType().mineralPrice();
				reservedGas -= pi.getUnitType().gasPrice();
				pi.setStatus(PlannedItemStatus.MORPHING);
				pi.setUnitId(unit.getID());
			}
//...onUnitComplete()
//		for (PlannedItem pi : plannedItems) {
				if (pi.getStatus() == PlannedItemStatus.MORPHING) {

					if (pi.plannedItemType == PlannedItemType.BUILDING) {
						if (pi.getBuilderId() != null && pi.getBuilderId() == unit.getID()) {
							pi.setStatus(PlannedItemStatus.DONE);
						}
						// For Extractors
						if (pi.getUnitType() == UnitType.Zerg_Extractor
								&& unit.getTilePosition().equals(pi.getPlannedPosition())) {
							pi.setStatus(PlannedItemStatus.DONE);
						}
					} else if (pi.plannedItemType == PlannedItemType.UNIT) {
						if (unit.getID() == pi.getUnitId()) {
							pi.setStatus(PlannedItemStatus.DONE);
							System.out.println("Setting unit: " + pi.getUnitType() + " to DONE");
						}
					}
				}
			}
//

Notice that we need to check both the unitId, and the buildType in the onUnitMorph()

Yee-haw, this almost seems like it could work maybe. Why not try it out with just 5 drones in our plan? Add the following to the onStart()

//TODO add a convenience method for this kind of stuff. Or three.
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1, PlannedItemStatus.PLANNED));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1, PlannedItemStatus.PLANNED));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1, PlannedItemStatus.PLANNED));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1, PlannedItemStatus.PLANNED));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1, PlannedItemStatus.PLANNED));

And run it!

Well, fück.

Upon some serious investigation (like, 2 minutes), the culprit is found out. In The Lööp, the elements that has the status DONE, are removed. BWAPI works in mysterious ways, and onUnitComplete() can (and will) fire when we are iterating through our PlannedItems. 

Just get our items that are to be removed into a collection, and get rid of them in the end. 

//Beföre The Lööp, in the onFrame()
List<PlannedItem> doneItems = new ArrayList<PlannedItem>();
//In The Lööp
} else  if (pi.getStatus() == PlannedItemStatus.DONE) {
			 	doneItems.add(pi);
		 }
//And after The Lööp
plannedItems.removeAll(doneItems)

…and we are probably done with the Extractor trick. Alsö, we överüsed a mëmë ënöügh.

In the next part, I hope to finalize, and verify our trick, and implement some more efficient unit handling. After all, you saw the bad boy and his cat above. 

Code in GitHub.

Leave a Reply