archive by month
Skip to content

separate, independent unit control for micro

I notice a lot of bot authors want to control each unit independently for micro decisions, to make it an agent with a mind of its own. There are good reasons to write a bot that way.

  • It’s simple. Fewer bugs and less implementation effort are strong reasons.
  • Being simple, it can run fast.
  • Frame-accurate control depends on each unit’s exact state at each frame. It seems much easier to handle unit by unit.
  • Units can sidestep the stupidest behaviors, like moving too much without shooting. Each unit can act efficiently under its local circumstances.
  • Cooperation: If a squad still gets orders from above, then the squad still acts together for higher-level goals.
  • Cooperation: Units can still work together on low-level goals like focus fire by simple means like preferring targets that are already hurt. Cooperation occurs as an emergent behavior.
  • Cooperation: At a cost in simplicity and speed, units can look at each other’s targeting and movement plans or expectations, to get more effective focus fire, to reduce overkill, to kite better, and so on.
  • And hey, you can call it an agent architecture, which sounds great.

It doesn’t make sense to me. My micro plans in Steamhammer call for reducing the independence of units, calculating more things centrally at the squad level and leaving less for individual units to decide. For focus fire and avoiding overkill, I want a data structure in the squad to keep track of assignments of shooters to targets. For kiting and fleeing, I want a near-term prediction of when each of my units is likely to die, based on the rate it is taking damage or on the approach of enemy melee units. A planning algorithm can look at all the data to decide, “You, shoot at that” and “You, pull back.” It’s not as simple, but I expect it will have a higher ceiling in practice, because it is easy to change out the planner. And I think that, done right, it can execute fast. Units act frame to frame, but the squad-level planner needs to execute only about once per cooldown period, and it can be out of synchrony with unit actions.

That’s general micro, but there are also important micro skills that bots don’t have and would benefit from. I’ve never seen a bot do any of these effectively.

Zergling surrounds. Watch a bot attack zealots with zerglings: Each zergling heads for where the zealot is now, and the lings pile up. With fast zerglings and slow zealots, if the zealots flee then the zerglings end up trailing behind and getting a hit in now and then, and mostly waste their time getting in each other’s way. Arrakhammer does modestly better by predicting the zealot’s future location, react to the future style. Then watch a human do it: The zerglings move into or around the formation of zealots, then attack all at once. If the zealots are fleeing, they get surrounded and have to find or make a way out before they can escape. Any intermediate zerg player knows this micro.

I’m seriously considering implementing zergling surrounds in Steamhammer with human-like control, at least as a first cut. It’s 2 commands total for small fights with up to one control group of zerglings: Command lings to move as a group, command lings to attack as a group. Choosing the destination for the move command is the tricky part.

Dragoon backstepping. Like most bots with a combat simulator, Steamhammer mostly attacks or retreats, and doesn’t know anything in between. But watch what happens in a human game when a strong terran early timing attack moves out and faces forward dragoons: The dragoons will lose if they stand and fight, they must step back... and back.... But if vultures pull too far in front of the tanks, or if any sloppy movement isolates a few terran units, the dragoons may suddenly stop and fire. The terran force must move slowly or be whittled down—usually some of both. Protoss gains time for more dragoons to take the field.

Related situations are common. FAP doesn’t understand vultures versus zealots, so if there are too many zealots, Steamhammer’s vultures will retreat in terror instead of pulling hit-and-run attacks. If an opportunity comes up, take it even if you weren’t intending to fight. I think the decision is best made at the squad level, so that the squad behavior stays coherent. I often see Tscmoo lose squad coherence and disintegrate; I want to avoid that.

Getting out of each other’s way. A large group of ranged units arrives to fight—hydralisks, say. The closest hydras get into range and stop to fire, and the ones behind maneuver around the sides to get into range (that is inefficient already). The ones behind those... push and shove, but can’t get into range and achieve nothing. Any human above beginner level will move the front hydras forward so that all can get into range, but bots don’t.

Climbing a ramp is similar. Bots see targets at the top of the ramp and stop to hit them, blocking the way so that the rest of the force is stuck below. Steamhammer is clever enough not to siege tanks on the ramp, but sieges them above the ramp and blocks the path anyway. Humans keep the front units moving until the whole force can get up. The difference in DPS is huge.

These decisions can be made by individual units, swarm intelligence style, but I find it hard to design good emergent behaviors. I think it is better to make the decisions at the squad level. It should be more efficient and more effective.

fixing a lurker micro bug

Change of plans. I solved a bug that others should know about, so Steamhammer-Tscmoo games will wait until next time.

Steamhammer has a bug that shows up when a lurker starts attacking a building: The lurker becomes unable to switch targets until the building is destroyed. So, for example, if a lurker doesn’t see any other immediate target, it will go after a supply depot. Once it has started in on the depot, marines can wander up behind the lurker in perfect safety, and perhaps scan and kill it. Until the depot is destroyed, all the marines have to do is stay out of the line of fire between the lurker and its target.

The bug is not in targeting. MicroLurkers correctly recognizes that the marines are higher priority targets and issues a command to target them. The bug is that Micro::SmartAttackUnit() does not act on the command. The code is inherited from UAlbertaBot, so probably many UAlbertaBot and Steamhammer forks have the same problem. Here is the inherited code:

// if we have issued a command to this unit already this frame, ignore this one
if (attacker->getLastCommandFrame() >= BWAPI::Broodwar->getFrameCount() || attacker->isAttackFrame())
{
    return;
}

I think the isAttackFrame() test may be a good idea if you are controlling, say, a dragoon. Retargeting a dragoon while it is in the middle of its animation will at best cause extra delay before the dragoon can fire again. But when I looked closely, it turned out that for a lurker attacking a fixed target every frame is an attack frame. Steamhammer drops the command because the lurker is constantly attacking.

Instead, it should accept the slight delay of retargeting lurkers. I rewrote it like this:

// Do nothing if we've already issued a command this frame, or the unit is busy attacking.
// NOTE A lurker attacking a fixed target is ALWAYS on an attack frame.
if (attacker->getLastCommandFrame() >= BWAPI::Broodwar->getFrameCount() ||
    (attacker->isAttackFrame() && attacker->getType() != BWAPI::UnitTypes::Zerg_Lurker))
{
    return;
}

Lurkers behave better, and other unit types are not affected. I see improved lurker micro in test games.

In Starcraft, every unit type behaves differently. UAlbertaBot, and therefore Steamhammer, tries to treat different unit types the same, which is sure to be causing other weaknesses. At some point I will analyze micro thoroughly and make sure that every unit type realizes its potential. For now, I’m happy that lurkers are a little smarter.

Skynet - avoiding splash damage

Skynet tries to avoid some area-of-effect spells and splash damage from some attacks. This is the fanciest skill that I’m writing up.

You can only avoid stuff that you can see coming. Skynet tracks threats in class AOEThreatTracker (where AOE stands for Area Of Effect), which creates and stores AOEThreat objects. Here is the key code from AOEThreatTracker::update(), which is called once per frame from the main onFrame() in Skynet.cpp:

	for each(Unit unit in UnitTracker::Instance().selectAllEnemy())
	{
		const BWAPI::UnitType &type = unit->getType();
		if((type == BWAPI::UnitTypes::Protoss_Scarab || type == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine) && mUnitThreats.count(unit) == 0)
		{
			AOEThreat newThreat = AOEThreat(new AOEThreatClass(unit));
			mAllThreats.insert(newThreat);
			mUnitThreats[unit] = newThreat;
		}
	}

	for each(BWAPI::Bullet* bullet in BWAPI::Broodwar->getBullets())
	{
		const BWAPI::BulletType &type = bullet->getType();
		if((type == BWAPI::BulletTypes::Psionic_Storm || type == BWAPI::BulletTypes::EMP_Missile) && mBulletThreats.count(bullet) == 0)
		{
			AOEThreat newThreat = AOEThreat(new AOEThreatClass(bullet));
			mAllThreats.insert(newThreat);
			mBulletThreats[bullet] = newThreat;
		}
	}

Skynet recognizes enemy reaver scarabs and spider mines as splash damage threats. (A bot that might lay mines of its own should also recognize its own spider mines as threats. Skynet does not use mind control.) It also recognizes psionic storm and EMP as area effect spells to try to dodge.

What does it do with the information it’s tracking? Here’s the public interface from AOEThreatTracker.h:

	void update();

	AOEThreat getClosestGroundThreat(const Position &pos) const;
	AOEThreat getClosestAirThreat(const Position &pos) const;
	AOEThreat getClosestEnergyThreat(const Position &pos) const;
	AOEThreat getClosestThreat(Unit unit) const;

	bool isTargetOfThreat(Unit unit) const;

Psionic storm is both an air threat and a ground threat. Scarabs and mines are ground threats. EMP is the only energy threat. But of the 5 informational methods, only getClosestThreat() and isTargetOfThreat() are used in the rest of the code; the other 3 get methods serve no purpose. getClosestThreat() takes a unit and considers whether it is an air or ground unit to find the closest threat. It counts EMP as a threat to spellcasters only, even though all protoss units will lose their shields to it. It’s a little inconsistent; corsairs count as spellcasters, but Skynet never researches disruption web.

The results of all this tracking work are used in only one place, in BasicUnitAction::update(), which is a micro action like the others we’ve seen in the last days. The BasicUnitAction of course has very low priority among micro actions, so if the unit under consideration is already doing something else (like dragging the mine that is the threat), then none of this happens.

	const bool isTargetOfThreat = AOEThreatTracker::Instance().isTargetOfThreat(mUnit);
	if(!isTargetOfThreat)
	{
		const AOEThreat &closestThreat = AOEThreatTracker::Instance().getClosestThreat(mUnit);
		if(closestThreat)
		{
			const int distanceToThreat = mUnit->getDistance(closestThreat->getPosition());
			if(distanceToThreat < closestThreat->getRadius()+32)
			{
				stayAtRange(mUnit, closestThreat->getPosition(), closestThreat->getRadius() + 64, distanceToThreat);
				return true;
			}
		}
	}

First it remembers whether the current unit mUnit is the target of the threat, for later use. The explicit target does not dodge the threat (sometimes it can’t). If the current unit is not the target, but it is within range of at least one threat, then it finds the closest threat and tries to get out of its range. stayAtRange() calculates a direction and moves that way. There’s no attempt to avoid multiple threats, which are likely for spider mines; Skynet is happy to flee one and run into another. Also, I notice that AOEThreatTracker keeps careful track of each threat radius, but this code ignores it and flees to a fixed radius. I assume that’s good enough? Anyway, this limitation may explain why Skynet does not try to dodge lurker spines, which affect a long narrow area.

If the current unit is the target, the unit stays put so that the other units around it know what to do to avoid the threat. currentTargetUnit is whatever the current unit is already shooting at. This is not best if under attack by a reaver scarab, but it does help nearby units know which way to flee.

	// If the target of a threat, dont do anything to cause it to move
	if(isTargetOfThreat)
	{
		if(currentTargetUnit)
			mUnit->attack(currentTargetUnit);
		else
			mUnit->stop();
		return true;
	}

Skynet has other interesting skills. See BlockedPathManager and MineBlockingMineralTask, for example. But that’s enough for now.

Tomorrow: Wrapup and lessons learned.

Skynet - arbiter control

Arbiters are tough to use well. You want to maneuver your arbiter to cloak as many of your units as possible while staying safe. You want to stasis or recall at critical times and places. Occasionally you want to do damage with the pea-shooter. The goals pull in different directions, and you have to decide what’s best.

Here’s how Skynet decides. Skynet has two arbiter skills: It knows how to cloak units efficiently and how to stasis. Skynet does not use recall, and it (understandably) doesn’t put any emphasis on targeting enemies with the puny gun.

Arbiter control is in ArbiterAction::update(), which is like the other micro actions, set up by Behaviour::createDefaultActions(). First it considers stasis:

	if(mUnit->getEnergy() >= 100 && BWAPI::Broodwar->self()->hasResearched(BWAPI::TechTypes::Stasis_Field))
	{
		const int stasisSize = 48;

		bool stasisUrgently = false;
		if(UnitInformation::Instance().getUnitsTargetting(mUnit).size() >= 6 || (UnitInformation::Instance().getUnitsTargetting(mUnit).size() >= 1 && mUnit->totalHitPointFraction() < 0.3))
			stasisUrgently = true;

First, if stasis is possible and the arbiter is in danger of being lost, Skynet sets stasisUrgently to true.

		UnitGroup stasisChoices;
		for each(Unit enemy in UnitTracker::Instance().selectAllEnemy())
		{
			if(!UnitHelper::isArmyUnit(enemy->getType()))
				continue;

			if(enemy->isUnderStorm() || enemy->isStasised())
				continue;

			const int distance = mUnit->getDistance(enemy);
			if(distance > 250 || distance < stasisSize)
				continue;

			stasisChoices.insert(enemy);
		}

Then it comes up with a list of candidate enemies to stasis. Enemies under psionic storm are not candidates—let them suffer!

		if(stasisChoices.size() > 4 || (!stasisChoices.empty() && stasisUrgently))
		{
			UnitGroup stasisTargets = stasisChoices.getBestFittingToCircle(stasisSize);

			if(stasisTargets.size() > 4 || (!stasisTargets.empty() && stasisUrgently))
			{
				const Position &stasisLocation = stasisTargets.getCenter();
				if(mUnit->getDistance(stasisLocation) <= BWAPI::TechTypes::Stasis_Field.getWeapon().maxRange())
				{
					mUnit->useTech(BWAPI::TechTypes::Stasis_Field, stasisLocation);
					LatencyTracker::Instance().placingStasis(mUnit, stasisLocation);
					return true;
				}
				else
				{
					mUnit->move(stasisLocation);
					return true;
				}
			}
		}
	}

And finally Skynet decides whether and where to stasis. If it can stasis at least 5 enemy units, or at least 1 unit and stasisUrgently is set, then it does. UnitGroup::getBestFittingToCircle() finds a circle of the given size which covers as many units in the UnitGroup as possible, and returns the smaller UnitGroup which it covers.

It’s a simple heuristic. An arbiter will happily stasis 5 vultures uselessly when it reaches stasis energy after the army it was covering is destroyed by tanks and vultures. The “right” way to do this would be with a pay-me-now-or-pay-me-later tradeoff that compares the situation now with possible future situations, which would of course be far more complicated. And, since that’s not hard enough yet, it should also consider the effect on the battle: Stasis on a ramp to block reinforcements? Stasis the tanks that are doing the most damage, or the vessel that is detecting the cloaked army? And so on.

Next is maneuvering the arbiter to cloak units:

	UnitGroup unitsToCloak;
	for each(Unit unit in squadUnitGroup)
	{
		if(!UnitHelper::isArmyUnit(unit->getType()))
			continue;

		if(unit->getType() == BWAPI::UnitTypes::Protoss_Arbiter || unit->getType().isBuilding())
			continue;

		if(mUnit->getDistance(unit) > 250)
			continue;

		unitsToCloak.insert(unit);
	}

	if(!unitsToCloak.empty())
	{
		unitsToCloak = unitsToCloak.getBestFittingToCircle(136);
		if(!unitsToCloak.empty())
		{
			Position cloakLocation = unitsToCloak.getCenter();
			if(mUnit->getDistance(cloakLocation) > 110)
			{
				mUnit->move(cloakLocation);
				return true;
			}
		}
	}

Candidate units to cloak are basically military units in the squadUnitGroup which can be cloaked and are near enough. Skynet never tries to protect probes or observers with the cloaking field (UnitHelper::isArmyUnit() returns false for both), but it will try to cloak dark templar that are already cloaked (a simple omission from the above code and trivial to fix) and units that are already covered by another arbiter (only a little harder to fix). It does the getBestFittingToCircle() thing to find the largest circle of units that it can cloak, and moves toward the circle’s center if it’s not close enough. If it’s already close enough then the micro action does not occur, which I assume frees the arbiter to fire its gun when possible, flee danger within limits, and so on.

Tomorrow: Avoiding splash damage, finishing the series.

Skynet - mine dragging

Spider mines do a ton of splash damage when they explode, and they don’t discriminate. Everything in splash range suffers, enemy or friendly. So it’s popular to intentionally trigger enemy mines while running up to enemy units, dragging the mine into the enemy to cause damage. Mines can sometimes follow a unit for quite a distance before going off. Protoss usually drags mines with zealots, though dark templar are also good. Zerg usually uses speedy zerglings in small groups, because they’re cheap and fast. Terran can sometimes trigger enemy mines with drops, but doesn’t have an appropriate unit to drag mines otherwise.

Skynet knows how to drag mines with a zealot. It uses the same system of micro actions that I touched on yesterday. In Behaviour::createDefaultActions() it records a possible mine drag micro action for every zealot:

	if(unitType == BWAPI::UnitTypes::Protoss_Zealot)
		mMicroActions.push_back(MicroAction(new MineDragAction(mUnit)));

By the way, the micro actions are evaluated in Behaviour::update(), which checks them in order. Each unit can only execute one micro action at a time, and the first action to return true is what happens. Skynet’s only prioritization is to record the most important micro actions first in createDefaultActions(). Mine dragging is recorded near the end, as a low-priority action. Yesterday’s archon-zealot undetected unit attack is higher priority.

Here is the entirety of MineDragAction::update():

bool MineDragAction::update(const Goal &squadGoal, const UnitGroup &squadUnitGroup)
{
	for each(Unit unit in UnitInformation::Instance().getUnitsTargetting(mUnit))
	{
		if(unit->getType() == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine)
		{
			int distance = std::numeric_limits<int>::max();
			Unit closestUnit;

			for each(Unit enemyUnit in UnitTracker::Instance().selectAllEnemy())
			{
				if(enemyUnit->getType().isFlyer() || enemyUnit->isLifted() || enemyUnit->getType().isBuilding() || enemyUnit->getType() == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine)
					continue;

				int thisDistance = mUnit->getDistance(enemyUnit);
				if(thisDistance < distance)
				{
					distance = distance;
					closestUnit = enemyUnit;
				}
			}
			
			if(closestUnit && distance < 32*5)
			{
				mUnit->attack(closestUnit);
				return true;
			}
		}
	}

	return false;
}

This seems to only notice spider mines which have already popped up and are targeting the current zealot. If one is found, then the code checks for suitable enemy units (not stuff in the air, for example) that are close enough. If any is found, then the zealot targets the nearest for attack, end of story.

That’s basic mine dragging. A stronger version would also consider detected or remembered mines: If a mine in the ground is detected or remembered to be near valuable enemies, then check whether it can be activated and dragged into the enemies. You can remember a mine location if you detected it in the past, or if you saw it being laid or moving and reburrowing. That’s more complicated, but human players do it as a matter of course. Humans will even try to drag mines that they expect or hope are there, without being sure.

Tomorrow: Arbiter control.

Skynet - killing undetected units

Archons do splash damage that affects all enemy units, included undetected units. That means that you can use an archon to, for example, kill a burrowed lurker that you can’t detect, by attacking one of your own units. The usual way is to park a sacrificial zealot on top of the lurker and attack the zealot with the archon; the archon will do full damage to the zealot and splash damage to the lurker.

Archon splash affects both ground and air. An archon attacking a zealot will damage a cloaked wraith that is in splash range. An archon attacking a floating ebay should clear spider mines in a small area (I haven’t tested to make sure).

Here’s how Skynet does it. Behaviour::createDefaultActions() recognizes the unit types and records possible micro actions to test and execute later:

	if(unitType == BWAPI::UnitTypes::Protoss_Zealot || unitType == BWAPI::UnitTypes::Protoss_Archon)
	{
		firstTargets.insert(BWAPI::UnitTypes::Terran_Siege_Tank_Siege_Mode);
		mMicroActions.push_back(MicroAction(new ArconZealotKillUnDetected(mUnit)));
	}

When the micro action comes up for testing, it runs ArconZealotKillUnDetected::update(), which either fails the test and returns false or succeeds, executes the micro, and returns true. Given the original archon or zealot in mUnit, it looks for the closest enemy unit that meets the conditions:

	Unit lurker;
	int lurkerDistance = std::numeric_limits<int>::max();
	for each(Unit unit in UnitTracker::Instance().selectAllEnemy())
	{
		if(unit->exists() && !unit->isDetected() && (unit->isCloaked() || unit->getType().hasPermanentCloak() || (unit->getType() == BWAPI::UnitTypes::Zerg_Lurker && unit->isBurrowed())))
		{
			int thisDistance = mUnit->getDistance(unit);
			if(thisDistance < lurkerDistance)
			{
				lurkerDistance = thisDistance;
				lurker = unit;
			}
		}
	}

	if(!lurker || lurkerDistance > minDistance)
		return false;

The variable is called “lurker” but it looks for any unit that is undetected and is temporarily cloaked (like a wraith or a unit under an arbiter), permanently cloaked (like a dark templar), or is a burrowed lurker. I skipped over the next bit of code: It checks for the other unit (of type typeToFind) and if it’s close enough puts it in other; if it has a zealot it looks for an archon, or if an archon it looks for a zealot. If it finds the needed other unit, it executes the micro:

	if(typeToFind == BWAPI::UnitTypes::Protoss_Archon)
	{
		if(mUnit != UnitTracker::Instance().selectAllUnits(BWAPI::UnitTypes::Protoss_Zealot).getClosestUnit(lurker))
			return false;

		mUnit->move(lurker->getPosition());
		return true;
	}
	else
	{
		if(otherDistance <= 14)
		{
			mUnit->attack(other);
			return true;
		}
		else
		{
			mUnit->move(other->getPosition());
			return true;
		}
	}

If the unit is a zealot (because typeToFind is archon) and it’s not the closest zealot, it bails so that the target doesn’t get swarmed by zealots—one volunteer at a time, please! The zealot moves to the position of the target. If the unit is the archon, it moves toward the zealot (not toward the target; this corrects for miscoordination that could occur if there’s more than one target around) or, if the zealot is within splash range of the target, attacks the zealot.

The bottom line is that Skynet has the basic skill, but its implementation is not sophisticated. It knows all the possible targets, but can only use a zealot as the sacrifice—pros in a pinch will use anything that works as the sacrifice. It doesn’t know that it could use a reaver instead of an archon. It doesn’t consider whether the sacrifice is worth the damage to the target (you’ll hurt your zealot and the cloaked wraith will probably escape). It does no preparation (keep the archon and zealot near each other if they may need to work together) and minimal coordination (if there’s an observer just out of range and approaching, you shouldn’t have bothered; if you have high templar around maybe you should storm it). If the target evades, Skynet may keep microing both units, leaving them out of the fight. But of course most bots don’t have even the basic skill.

Reminder: Other units that do splash can also kill undetected enemies this way. Reavers, sieged tanks, and lurkers are the prime splash-dealing candidates. Corsairs, firebats, valkyries, and infested terrans also have occasional uses. (Mutalisk bounces are technically not splash and do not hit undetected enemies.) And, of course, spell effects like nuke, irradiate, psionic storm, ensnare, and plague affect units whether they are detected or not. Ensnare and plague are particularly useful because they make cloaked units visible.

Near the end of this 2008 game, Stork fires on his own carriers with corsairs to hold off cloaked wraiths.

Tomorrow: Mine dragging.

the maximum shooting distance

I like Johan Hagelbäck’s writings (see this post), but one point bugged me. He kept repeating the idea that units should engage at their maximum shooting distance. Well, he talks about various kinds of unit micro that you can achieve with potential fields, and most of it is simplified from the ideal, but this one simplification bugged me.

Keeping near your maximum shooting distance is often a good idea. You’re close enough to harm the enemy, but being at the maximum range increases your options (you could run away or kite out of range during cooldown) and decreases the enemy’s (maybe they have to approach to hit you at all). If you have only one behavior, then staying near maximum shooting distance is a good one.

But look at the giant exceptions:

• If you have a group of ranged units, you want as many as possible to fire. If there are too many to fit into a single arc (you have a large number, or you’re fighting in a narrow space), then some of them must come closer. If you’re going to stand and fight, then bring the most possible firepower to bear. When your reinforcements join the battle, they have to be able to reach the front line—it’s a kind of coordination and hard to implement, but it’s critical to good micro. The front line can make gaps to allow reinforcements through (a skill also useful for reaver scarabs), or can move forward to let reinforcements into range.

• Sieged tanks can shoot farther than they can see, so you have to consider vision range too.

• Sieged tanks have a minimum range, so if you’re attacking them you may want to snuggle up. Also, if the enemy has sieged tanks on the field, any enemy unit you snuggle up to is at risk of taking splash damage from its own side. Few bots seem to know.

• If you’re blasting down static defense (cannons, say) with siege tanks, don’t siege just inside tank range like most bots, siege just outside cannon range. You’ll probably be able to destroy more cannons before you have to unsiege and move, so you’ll clear the cannons faster and be more likely to win overall (carriers may be coming, yes?). This advice will usually be right when the enemy front line is static defense, but might be wrong if the enemy has other units in wait. Humans commonly siege tanks at different ranges, partly to save micro but also because it’s more robust against surprises from different directions.

• If you have ranged units and there’s an obstacle between the armies, then your only reason to keep your distance is so that fewer enemies can shoot at you. Dragoons on a cliff can take potshots all day at zerglings down below, and the same for guardians uphill from marines.

• If you’re going to win the battle, then staying at maximum shooting distance gives the enemy better chances to escape. If you can’t surround, then keep close so you can pick off more escapees.

In full generality, the distance at which one unit should seek to engage another (given that you want to) depends on the range, damage, cooldown, speed, and acceleration of both units, plus the terrain, with consideration of special abilities (psionic storm, mine laying, medic healing...), and if you want to be utterly thorough, the full game situation too. For most unit pairs the basic calculation is easy enough. For example, if you have more speed and more range (speed vulture versus slow zealot, ranged dragoon versus unupgraded marine), who cares how close you get, as long as you don’t get hurt?

For army on army, the formation and distance you want depend on the sizes and unit mixes of both armies, plus the terrain, plus the game situation. The maximum shooting distance is a good base to start at, but you’ll need to expand beyond to win.

Zia and mutalisk micro

Zia’s mutalisk cloud is scary when it gets big. Eventually the mutas not only one-shot the units that they target, but their bounces instantly kill nearby units. The mutalisks sweep a path of destruction. But think about it—is that efficient? If mutalisk bounces at 1/3 power kill instantly, then the main attack must usually be gross overkill. Most of the firepower is wasted.

The idea of individual mutalisk control, as introduced by the Berkeley Overmind and copied by other zergs since, is to waste no firepower. Each flier independently dances in and out for safety and ideally attacks at near its maximum rate. But watch how Tscmoo zerg implements this: Its mutalisk cloud is also scary when it gets large, but usually not as scary as it could be, because it spreads out too much. Sometimes half the mutas are posing for pictures with the ground army while half are on the job. And the attackers often pick some targets over here, some over there, and don’t kill either as fast as they should. Tscmoo doesn’t focus its fire enough; it’s the opposite mistake from Zia.

Causing damage does not win games. Maximizing your damage output is not the winning move. You want to balance between killing the most important enemies and staying alive.

Try to imagine PerfectBot’s muta micro. Even PerfectBot can’t truly play perfectly, because calculating optimal micro is infeasible. But surely PerfectBot focuses fire efficiently, switching mutas fluidly between targets, taking into account importance and time to kill based on distance and damage rate and expected losses, to reduce overkill to near zero and spend less time flying between targets and strike a good balance between killing the most important stuff fast and staying alive. “This takes 5 more shots to kill, 12 are shooting, might lose 1, so switch 6 to new targets.” Zia and Tscmoo zerg are no competition for Jaedong, but I think Jaedong would boggle at PerfectBot’s mutalisks.

How close can we get to PerfectBot micro today? 1. Given a set of targets in priority order, calculating how to focus them down efficiently with minimal waste seems intricate but ultimately not that hard. 2. Folding in a desire to also minimize losses makes optimal decisions computationally infeasible. Even approximations seem tough. 3. Prioritizing the targets depends on the total game situation and will have to be done heuristically. For now I guess we’ll have to settle for a simplified algorithm.

Watching Zia last week, I thought it picked targets usually one at a time (simple 3) and once the target was chosen ignored damage taken while chasing it down (very simple 2), so the intricate-but-not-hard efficient killing calculation by itself should be a big improvement. Zia-this-week has been updated and has fancier micro than Zia-last-week, so I’m already behind the times! I got the impression that Zia-this-week is better about picking targets and switching targets and avoiding damage, but that it still wastes shots with too much overkill.

oversiege

I promised strategy search today, but it’s taking longer than planned. Instead, here’s a rant about indecisiveness.

Terran bots that make siege tanks then go on to siege and unsiege the tanks too often. Knowledgable ICEbot does it, micro-aware LetaBot and WOPR do it. Iron does it less often, because it likes to stay in tank mode as much as possible. The tanks can’t fire while sieging and unsieging and end up shooting less than they should—and tanks are the backbone, with a weak backbone the army falls. I keep seeing tanks siege up, fire a shot, unsiege, then without moving a pixel siege up to fire a second shot. Even politicians don’t flip-flop that often.

No taunt intended. It’s tough to know when to siege. But sieging up 6 tanks to annihilate a single probe that wanders into view—OK, now I’m taunting. Bots can definitely do better than that.

I’m going to throw out a few easy ideas. I can almost promise that all of these have been tried, and I can promise for sure that none of them solves the problem. But maybe it will help people improve?

If you decide to siege up based on a condition, the condition probably should not be “a target is in sight.” Otherwise GarmBot will stall your army in the middle of the map with one zergling at a time. Try a tighter condition, at least “a target the army can’t destroy in one volley is in sight.”

Or you could try putting a time delay on unsieging. Siege up immediately, but when you want to unsiege, instead set a flag that says “unsiege after n frames.” Clear the flag if the siege condition is met in the meantime. Then if another target appears a moment later, which I see a lot, you’ll be ready.

If you siege based on a measurement, a “need to siege,” you could put hysteresis on it. If you siege when needToSiege reaches 10, maybe you shouldn’t unsiege until needToSiege falls below 5, or falls to 0, or whatever.

If you siege a tank based on orders to its squad, have it follow orders. If the squad is on the move, it’s “go! go! go!” until battle is joined (after that it gets complicated). If the squad’s orders are to stand guard at the choke, tanks should find good positions and stay sieged until their orders or the situation changes (“oops, a dark templar reached me”). Or something like that; guarding against drops is trickier.

If you decide by cost-benefit analysis, be sure to include a fat term for the cost of sieging or unsieging. The cost is not only the time spent immobile and unfiring, it is the risk accepted that the best decision will change suddenly.

Sieging has two advantages, higher range and higher damage rate (including higher damage from splash). Whatever decision method you use, maybe it makes sense to separate the two as different concerns. If you’re sieging for the range, stay sieged as long as the goal remains that you need the range for: If you’re destroying static defense from a safe distance, until the target is destroyed; if you’re defending an approach, until you are ordered to do something else. If you’re sieging for the damage rate, you want to try to stay sieged long enough to make up for the shots you would have fired in tank mode. Well, the considerations are complicated, but that’s a start on them.

So much for easy ideas. Maybe the only good solutions are hard solutions. But there is definitely a problem to solve.

And y’know, this is top secret and maybe I shouldn’t say, but the indecision between tank mode and siege mode is only one example. Bots suffer a lot of indecisiveness. Should the lurkers burrow, should I attack or run away (nah, I’ll just move back and forth under fire until I die), where does this army want to go (this way, that way, this way, why go anywhere?).... Any agent making a big decision based on small considerations is at risk of vacillating.