Creating a Starcraft AI – Part 5: A Few Good Boys

First part of this series | Previous | Next | Index

So, I’ve switched to BWAPI4J. Currently I’m using the raw branch at the moment, but it’s features will be integrated into the master, when it’s done. I’ve doing a little work on BWAPI4J, but just very minor stuff. Now it’s somewhat usable, at least to the degree that I could adapt my bot with minimal changes. The code is 99% the same, as it was the goal – We don’t want to create too much work for existing Java bots – or more precisely, their authors. 

Képtalálat a következőre: „technically correct the best kind of correct”

Let’s get into the details.

The first thing we need to do is connect to the game. Chaoslauncher works perfectly for testing this. The code is simple:

private BW bw;
Player self;

public void run() {
	this.bw = new BW(this);
	this.bw.startGame();
}

public static void main(String[] args) {
	Main main = new Main();
	main.run();
}

@Override
public void onStart() {
	self = bw.getInteractionHandler().self();
	bw.getInteractionHandler().setLocalSpeed(30);
	bw.getInteractionHandler().enableUserInput();
}

It’s important to not initialize the Player object (self) earlier than onStart, otherwise it will be null.

Last time I was trying to check if I have the prerequisites for a particular unit. Neither of the methods I mentioned were actually the one I was looking for – in particular, hasUnitRequirements() is checking if I already have the unit type, not if I can build it. Also, canMake() is nice, but it checks my resources also, and I want to reserve those before I actually have the required amount. But the UnitType.requiredUnits() lists all the requirements, and amounts of them. I can just check if I have enough. 

	public boolean hasRequirements(UnitType unitType) {
		for (UnitType req : unitType.requiredUnits().keySet()) {
			if (unitCounts.getOrDefault(req, 0) < unitType.requiredUnits().get(req)) {
				return false;
			}
		}
		return true;
	}
//Add this check to the Lööp's validation (...)
	if (pi.getImportance() >= lastImportance) {
						if (availableMinerals >= pi.getUnitType().mineralPrice()
								&& availableGas >= pi.getUnitType().gasPrice() && supplyUsedActual >= pi.getSupply()
								&& prereqsOk
								&& hasRequirements(pi.getUnitType())
//(...)

This worked well enough. I added this to my items to build.

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

But only four zerglings got built. 

Those four assholes.

You might have guessed, it’s the larva selecting logic. In the long run, larvae management is a very important part of Zerg Strategy, so let’s devise something overly complicated at this early point. I did some research on these jolly good worm things. 

For the most part, larvae are kinda in the same place, tied to their Hatchery. In the future, when I’ll have multiple bases, their position might be important, I’d probably like to reinforce my army from the base closest to the front. 

I can have 3 larvae per Hatchery maximum, if I morph one, another will get created some time after. Larva spawn time is an interesting topic, with a strange answer. Quote from the forum post: 
“tl;dr: Average larva spawn time 342 frames, best ~318, worst ~366.” 

In the future, these times will become very important. At this stage, what we need to take away from this is larvae are just a resource, that should be managed accordingly. I sometimes saw people and bots just make a sort of random unit, when there were enough larva – if you have a lot of money, another overlord, drone, whatever couldn’t hurt, and you really shouldn’t do it when your army just got wasted.

Bearing all this in mind, I’ll just have a Set of ids with the available larvae. I’ll ad the ids when a larva gets created, and remove them on morph.

HashSet<Integer> availableLarvaIds = new HashSet<>();
//OnUnitCreate
if (unit.getPlayer() == self) {
//		(..)
			if (unit.getType() == UnitType.Zerg_Larva) {
				availableLarvaIds.add(unit.getID());
			}
		}
//
//In the loop, on the RESOURCES_RESERVED case:
				} else if (pi.plannedItemType == PlannedItemType.UNIT) {
					if (!availableLarvaIds.isEmpty())
					{
						Integer larvaId = availableLarvaIds.iterator().next();
						Unit larva = unitsById.get(larvaId);
						
						pi.setUnitId(larvaId);
						pi.setStatus(PlannedItemStatus.CREATOR_ASSIGNED);
						larva.morph(pi.getUnitType());
					}
				}
//And in onUnitMorph
if (precursor.equals(UnitType.Zerg_Larva)) {
	availableLarvaIds.remove(unit.getID());
}

I also renamed the BUILDER_ASSIGNED state to CREATOR_ASSIGNED, to better reflect the intended purpose.

Will this work? Experts say lol nope, but I tried out, and the desired amount of zerglings gets created. (Not strictly true, as you always want more zerglings, because they are just so cute, but you get the point)

During this, I have spotted something unsettling: I never change worker roles, there is just MINERAL, forever. So let’s change that, and when a builder gets assigned, manage it. I added a separate method for this. Also changed the workerIdsByRole to contain a HashSet – that can’t contain duplicates, and that’s kind of what we want. The Computer Science Wizard is pleased.

//
	HashMap<WorkerRole, HashSet<Integer>> workerIdsByRole = new HashMap<>();
//
	public void changeWorkerRole(Integer id, WorkerRole prev, WorkerRole next) {
		workerIdsByRole.get(prev).remove(id);
		workerIdsByRole.get(next).add(id);
	}

With that detour, the next step is to make our lil jumpy doggos do something worthwile. (I know they are doing their best, I really do. They are good boys). 

The key part of the 4 Pool strategy is to attack before the enemy can even have an unit. For that, we need to know where to do that. For that, we would like to have some terrain analyzer. Oh boy, I wish BWAPIJ would contain something like that! Unfortuantely, at the writing of this article, the implementation is not finished yet. 

(BWEB is originally a C++ library, BWAP4J contains the Java implementation for it.) 

Basically at this point, we need the starting locations, and to order our zerglings to attack them. That part IS done in BWEB. So as soon a zergling is born, it should attack. But there are multiple positions, and I need to keep track of the visited ones. Otherwise my zerglings might just end up standing around on an empty base.

Like this.

I could just queue up the attac(k) commands, and try that out. That results in two things. 

  • Only half of my zerglings attack.
  • They can’t find some buildings, because they just check the starting location
  • Losing the game if they don’t find the location first.

So, 50% efficiency at not accomplishing anything worthwile. Good news, there is room to improve! The second and third points should be solved with some kind of scouting algorithm, the first is just a quirk on how BWAPI assigns unit IDs. 

When a larva morphs into zerglings, the ID of the larva gets carried over to the egg, then one unit gets morphed from the egg, and another is created. I added my attack command to the onUnitCreate() methods, so half of the good bois were just chilling on the comfy purple rug at home. (Also known as creep) I don’t really blame them, but their participation would greatly enhance my chances of success.

I didn’t include my code here, because it’s easy to figure out, and also, you generally should not do this. And please, especially don’t submit a primitive 4 Pool bot to the SSCAIT ladder, there are more than enough of them – unless you do something really well, that others don’t.

Also, there is more to a zergling’s life than being a good boi attacking right after being born. Without delving into the endless options, a general concept needs to be made. A very basic way to manage units is to iterate through self.getUnits() every frame. But that’s obviously very inefficient. First of all, there are some information that should be stored about a unit, mainly, what should it do next. For this, I created an UnitManager class, which will be the parent class of every specific manager. Since we are working with zerglings mainly, I added another child class, 
BigOlJumpyPupperManager ZerglingManager as well.

Eventually, all the units will get managed by one of these classes. At first look, this might seem even more inefficient than just looping through them, but the experience (Well, mainly with BWMirror) is that usually BWAPI calls are slow, but doing things on the Java side are quicker, in fact, we should cache a lot of things. UnitManager has a general execute() class, which should be overridden, and that’s about it for now.

HashMap<Integer, UnitManager> unitManagerMap = new HashMap<>();

Ideally, I would like to know the location of the enemy base the moment the Zerglings are spawned. I could send a drone to scout, but my income is strained as it is. So let’s just send the lings in separate directions, and when a base is found, every ling should attack it. 

Except zerglings can’t attack flying units – one of the greatest injustices in Brood War

A memory of enemy buildings should be made – this will be useful for other purposes as well. A little background: When a unit is spotted for the first time, onUnitDiscover() gets executed. After that, if it’s seen, the onUnitShow() method gets triggered, and onUnitHide() when they are hidden in the fog of war. Except for neutral units, where onUnitDiscover() will be executed every time.

Bearing that in mind, we should put the building into our collection when it’s discovered, and only remove when it’s destroyed, right? 
Very wrong as always

First of all, the most important thing we should store is the position, and maybe the type. Both of these can change. Terran buildings can float away (sneaky), and Zerg buildings can morph into a different one. 

Also, while I’m at it, might as well store some info about the enemy units. So, I created two classes: EnemyBuildingInfo, and EnemyUnitInfo. Generally, the interactions for these two types tend to be different, and most of the cases, I should iterate over less elements. 

There are quite a few concepts outlined there – I will provide the implementation details in the next post. Thanks for reading – if you want to be notified of updates, just subscribe with the box on the right.

ps.: I’ll no longer make separate branches per article. I concluded that it’s not necessary, you can just view the commits, and I will just work on the master for now. 

Leave a Reply