Creating a Starcraft AI – Part 4: Finishing up, and a big decision

First part of this series | Previous | Next | Index

We didn’t quite get to it last time, but maybe today the Extractor trick will be finally implemented? Last time there were only a few missing steps. What’s missing is checking PlannedItems in the MORPHING state in our Lööp. Also, let’s try out adding the required items.

	} else if (pi.getStatus() == PlannedItemStatus.MORPHING) {
				if (pi.getDoCancel()) {
					Boolean cancelPrereqsOk = true;
					for (PlannedItemPrereq pip : pi.getCancelPrereqList()) {
						if (pip.isMorphing()) {
							if (!(unitsInProduction.getOrDefault(pip.getUnitType(), 0) >= pip.getAmount())) {
								cancelPrereqsOk = false;
								break;
							}
//To the onStart()
									plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1));
		plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1));
		PlannedItemPrereq pip1 = new PlannedItemPrereq(UnitType.Zerg_Drone, 9, false);
		PlannedItemPrereq cancelReq = new PlannedItemPrereq(UnitType.Zerg_Drone, 1, true);
		PlannedItem trickExt = new PlannedItem(PlannedItemType.BUILDING, UnitType.Zerg_Extractor, 16, 1 );
		
		PlannedItem overDrone = new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 16, 1);
		PlannedItemPrereq pipEx = new PlannedItemPrereq(UnitType.Zerg_Extractor, 1, true);
		overDrone.getPrereqList().add(pipEx);
		
		trickExt.getPrereqList().add(pip1);
		trickExt.setDoCancel(true);
		trickExt.getCancelPrereqList().add(cancelReq);
		plannedItems.add(trickExt);
		plannedItems.add(overDrone);
							
						} else if (!(unitCounts.getOrDefault(pip.getUnitType(), 0) >= pip.getAmount())) {
							cancelPrereqsOk = false;
							break;
						}
					}
					if (cancelPrereqsOk) {
						pi.setStatus(PlannedItemStatus.CANCEL);
					}
				}

I tried this out with great hopes, and of course it crashed miserably with a NullPointerException.

One crying session debugging session later, i found out that the problem is that Extractors are handled differently, because of course it is.  I needed to extend the onUnitRenegade() method.

//...
		if (unit.getPlayer() == self) {
			unitsById.put(unit.getID(), unit);
//...Other than that, i added this to the PlannedItem class, so no NPE on missing value
private Boolean doCancel = false;
		

And behold, one properly canceled Extractor later…

Omg hax

That is finally settled. On to our next improvement of many, which is crab torture cages better unit management. We already have two instances, where we loop through all our units, just to select one drone or larva. How about a convenient map for units, mapped by type? And since we have the unitsById value already, we just need to store the IDs. On any type of unit creation (onUnitComplete()onUnitRenegade(), etc) we want to put our newly created creature’s ID into this. But wait, we need to update the larva id as well. Fortunately, there is a handy method for keeping track what type of unit morphs into what – UnitType.buildsWhat().

Some juicy debug lines, to illustrate

As you can see, basically when morphing, we need to add the new unit’s id into our collection, and delete the previous unit’s one with the precursor unit’s key.

//Dis. HashSet because we don't want duplicate IDs
    HashMap<UnitType, HashSet<Integer>> unitIdsByType  = new HashMap<UnitType, HashSet<Integer>>();
//To onUnitCreate, onUnitComplete, and onUnitRenegade
if (unit.getPlayer() == self) {
			unitIdsByType.getOrDefault(unit.getType(), new HashSet<Integer>()).add(unit.getID());
			unitIdsByType.get(unit.getType()).add(unit.getID());
//To onUnitDestroy
unitIdsByType.get(unit.getType()).remove(unit.getID());
//And to onUnitMorph:
			UnitType precursor = unit.getType().whatBuilds().first;
			unitIdsByType.get(precursor).remove(unit.getID());
			unitIdsByType.putIfAbsent(unit.getType(), new HashSet<Integer>());
			unitIdsByType.get(unit.getType()).add(unit.getID());

This seems to cover all cases, but of course, this is BWAPI, and we can’t have nice things, so upon debugging the unitIdsByType…

Képtalálat a következőre: „this is why can't have nice things”

{Zerg_Hatchery=[1, 18, 4, 6, 23],
Zerg_Lurker=[128],
Zerg_Hydralisk=[],
Zerg_Larva=[129, 130, 131, 132, 133, 134, 126],
Zerg_Egg=[128, 3, 23, 24, 123, 125],
Zerg_Lurker_Egg=[128],
Zerg_Overlord=[19],
Zerg_Drone=[16, 3, 24, 123, 125]}

There are some intermediate biomass that is created, and whatBuilds() does not notify us of this. So, as always, let’s just handle these edge cases as well. Oh, and also there are units with UnitType.None precursor, so trying to get that key might throw a NullPointerException. So, fixed logic below.

UnitType precursor = unit.getType().whatBuilds().first;
			if (unit.getType() == UnitType.Zerg_Lurker) {
				unitIdsByType.get(UnitType.Zerg_Lurker_Egg).remove(unit.getID());
			} else if (unit.getType() == UnitType.Zerg_Guardian || unit.getType() == UnitType.Zerg_Devourer) {
				unitIdsByType.get(UnitType.Zerg_Cocoon).remove(unit.getID());
			} else {
				if (unitIdsByType.containsKey(UnitType.Zerg_Egg)) {
					unitIdsByType.get(UnitType.Zerg_Egg).remove(unit.getID());
				}
			}

So now, we have a collection of Unit IDs, nicely maintained. So if we need a larva, or a worker, we just select one.. oh no, workers have roles, remember? So, crabbos need special treatment. For research, generally the first available building is sufficient for our purposes. For larvae, it might be important where they are. But for now, let’s keep things simple, and just select the first available stuff. For drones, get a mineral-mining one, generally they are the most numerous. 

I added a convenience method for getting a worker, and reworked the Lööp. 

//Convenience method, yay! 
    public Unit getWorkerFromRole(WorkerRole role) { 
    	if (workerIdsByRole.get(role).isEmpty()) {
    		return null;
    	} else {
    		Integer size = workerIdsByRole.get(role).size();
    		return unitsById.get(workerIdsByRole.get(role).get(rand.nextInt(size)));
    	}
    }
//Lööp:
if (pi.plannedItemType == PlannedItemType.BUILDING) {
  Unit builder = getWorkerFromRole(WorkerRole.MINERAL);
  	if (builder != null) {
// (... )

Also, there is a bug: if there is no builder available, the resources still get reserved every time. I added yet another convenience method – because i like to treat myself like the nice princess I am – to reserve resources.

It’s all and well that I decided to extend the PlannedItem class,
but there are some things I didn’t really think through. For units, if there is a larva available, i reserve, then free the resources in the same frame. But if there is no larva, when should the reservation happen? 

After some crying in the corner thinking, i decided extending the concept, and adding another state (RESOURCES_RESERVED) to the PlannedItemStatus, since i kind of did this implicitly.

Like i wrote before, planning is important, but failing and reworking everything repeatedly is importanter.

The complete, reworked Lööp, which is surely the final one ever. I won’t copy everything here, you can check it out the complete code on GitHub.

//
f (pi.getImportance() >= lastImportance) {
						if (availableMinerals >= pi.getUnitType().mineralPrice()
								&& availableGas >= pi.getUnitType().gasPrice() && supplyUsedActual >= pi.getSupply()
								&& prereqsOk) {
							reserveResources(pi.getUnitType());
							pi.setStatus(PlannedItemStatus.RESOURCES_RESERVED);
									
//And the previous logic from the PLANNED state moved here
			} else if (pi.getStatus() == PlannedItemStatus.RESOURCES_RESERVED) {
				if (pi.plannedItemType == PlannedItemType.BUILDING) {
					Unit builder = getWorkerFromRole(WorkerRole.MINERAL);
					if (builder != null) {
						TilePosition plannedPosition = getBuildTile(builder, pi.getUnitType(), self.getStartLocation());
						builder.build(pi.getUnitType(), plannedPosition);
						pi.setPlannedPosition(plannedPosition);
						pi.setBuilderId(builder.getID());
						pi.setStatus(PlannedItemStatus.BUILDER_ASSIGNED);
					}
					break;
				} else if (pi.plannedItemType == PlannedItemType.UNIT) {
					if (unitIdsByType.get(UnitType.Zerg_Larva) != null
							&& !unitIdsByType.get(UnitType.Zerg_Larva).isEmpty()) {
						Integer larvaId = unitIdsByType.get(UnitType.Zerg_Larva).iterator().next();
						Unit larva = unitsById.get(larvaId);
						pi.setUnitId(larvaId);
						pi.setStatus(PlannedItemStatus.BUILDER_ASSIGNED);
						System.out.println("larva: " + larvaId + " assigned to morph into a beautiful "+ pi.getUnitType());
						larva.morph(pi.getUnitType());
						break;
					}
				

Okay, larva and worker management is somewhat decent (If you don’t really have high standards), now it’s time to actually stop and think about the next steps.

As i described in Part 2, crabbos can be utilized in multiple ways. One would think, that mining minerals is quite simple, but it can be optimized further. I plan to implement, and if possible, improve the concepts outlined in this article. 

One thing I’m 100% sure I will do, is to make Zerglings, another is expanding. Short term goals are those, then.

First of all, can I make a 4 Pool build with the tools I already have? I think so, let’s specify that for Mr. Computer, too.

    plannedItems.add(new PlannedItem(PlannedItemType.BUILDING, UnitType.Zerg_Spawning_Pool, 0, 1));
    PlannedItem dr1 = new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Drone, 0, 1);
    PlannedItemPrereq pip1 = new PlannedItemPrereq(UnitType.Zerg_Spawning_Pool, 1, true);
    dr1.getPrereqList().add(pip1);
    plannedItems.add(dr1);

    plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Zergling, 8, 1));
    plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Zergling, 10, 1));
    plannedItems.add(new PlannedItem(PlannedItemType.UNIT, UnitType.Zerg_Zergling, 12, 1));

Upon running it, i have discovered yet another problem. I don’t actually check if a larva is able to spawn a zergling. It cannot do so without a Spawning Pool present. 

There are two ways I can check that, and of course neither of them are working. One is the self.isUnitAvailable(), which return true for zerglings when the game begins, and self.hasUnitTypeRequirement(), which returns false for Spawning Pool availability. Other than that, there is  the Unit.canMorph() method, but for that to check I have to have an existing unit of the builder type. At this point, I decided to contact some nice people in the SCAI Discord, and complain loudly politely inquire if perhaps someone else have this probem.

Accurate representation of events

Upon some discussion, it turned out, that BWMirror not really actively developed at the moment. Instead, there is the BWAPI4J project, which is more or less constantly worked on. Of course, it does not have the methods I just mentioned implemented. 

So now what? I think there is only one way forward, and that is to participate in BWAPI4J. I thought about that, and made a big decision to switch over to that. This blog will also pivot a little, and the next few posts will be about BWAPI4J.

With that, i’ll cut this post short. The existing code will remain in GitHub for clarity, and historical reasons, but next time, it will be in the new era. 

Regardless, the blog will be still updated frequently, just maybe not with developing JumpyDoggoBot at the moment.

Thanks for reading!

Leave a Reply