archive by month
Skip to content

about Steamhammer 1.0

Change of plans. Today and tomorrow I’ll download my thoughts about Steamhammer 1.0 before I dive into 1.1 and start to forget which feature came when. After that I’ll return to tournament coverage. Keeping up with real time? I may have heard of that.

Steamhammer’s web page is updated. Get the 1.0 source and binary releases there. Downloading from SSCAIT is another way to get the binary. Don’t miss the config file, the bot needs it to run.

new skills

The configuration file lets you specify more possibilities as part of the opening build orders. One of my goals is to be able to play every opening, and I’m making progress.

  • scout on command: always, or only if necessary, or only to find the enemy
  • build a macro hatchery, or mineral only base, or a gas base, or a “hidden” base
  • stop and restart gas gathering, or gather a specified amount of gas then stop
  • build static defense in the main or natural

For example, you can code “go scout if needed” to send out a worker scout, right then, only if the enemy base location is not already known. Or “go gas until 100” to get 100 gas for zergling speed and then put the drones back on minerals. Or “hatchery @ min only” to start a new base that does not need to have gas. These are items that can go into the production queue to be executed when they come up; the strategy boss uses the same underlying code to communicate its dynamic plans to the production manager.

New openings use all these features. The old openings in Steamhammer have also been reworked extensively. Even when it looks like it’s doing the same thing as the previous version, there are differences. Some openings have fancy optimizations, like stopping and restarting gas more than once to smooth production. It’s easy to edit the config file, so it’s no special effort to try out tricks like that.

The strategy boss copes with emergencies like having no drones and has some basic reactive skills, like queueing up scourge when corsairs are sighted. Overall Steamhammer is much more robust when things go wrong; there are still bugs that can freeze it, but only on cool days.

The strategy boss also improves macro. Steamhammer still sometimes runs its minerals up into the thousands, but it recovers reasonably soon, and in many cases macro is fine from the beginning of the game to the end. Far from perfect, still much better.

random openings

Steamhammer 0.2’s ZvT configuration looks like this. It plays one fixed opening.

		"ZvT" : "ZvT_12Pool",

Steamhammer 1.0’s looks like this.

		"ZvT" :
			{ "StrategyMix" : [
				{ "Weight" :  3, "Strategy" : "4PoolHard" },
				{ "Weight" :  2, "Strategy" : "4PoolSoft" },
				{ "Weight" :  5, "Strategy" : "5Pool" },
				{ "Weight" : 10, "Strategy" : "9PoolSpeed" },
				{ "Weight" : 25, "Strategy" : "ZvT_12Pool" },
				{ "Weight" : 35, "Strategy" : "ZvT_13Pool" },
				{ "Weight" : 10, "Strategy" : "ZvT_2HatchMuta" },
				{ "Weight" : 10, "Strategy" : "ZvT_3HatchMuta" }
			]},

The bot chooses randomly among 8 openings, 4 zergling openings and 4 mutalisk openings. I haven’t worked seriously on lurker skills yet. The weights can be any positive integers, but I chose weights that add up to 100 so that I can interpret them as percentages. The 4 and 5 pool openings are slight variations of the same thing and happen 10% of the time in total. (With better scouting skills and followup, Steamhammer plays these fast rushes more strongly than UAlbertaBot.) The 9 pool overruns surprisingly many terrans and happens another 10% of the time. The 12 pool and 13 pool are similar muta rushes and together happen 60% of the time. Most people won’t notice the difference between the similar openings, but many details vary. The 2 hatch and 3 hatch muta openings are actual genuine honest-to-Betsy true-blue manufacturer-guaranteed mainstream builds with stronger economy, but Steamhammer is not yet adaptive enough to play them safely, so together they happen only 20% of the time. I’m curious to see how well they do in practice—hmm, I predict results will vary!

Steamhammer also chooses randomly in the other matchups, including versus random. You have to scout it to know what it is doing.

other stuff

The configuration file includes the version number: Steamhammer_1.0.json. That way I can conveniently run more than one version from the same directory.

I blogged about the fixes for original UAlbertaBot bugs. There were 10 in total, and only a couple were already in version 0.2. Stream viewers may not notice, but Steamhammer makes slightly fewer absurd blunders.

There are many small improvements to tactics and targeting, which together make a noticeable difference. The bunker obsession is reduced but still claims its toll. The bot is less eager to send out drones for emergency defense, but doesn’t run them away when they are under attack (“keep working, you cowards!”). And Steamhammer now has basic overlord vision skills thanks to hints from AIL, so it makes more informed engagement decisions and can fight cloaked units like dark templar.

And that’s about it. All other changes are minor.

future plans

Alongside further work on adaptivity in the strategy boss, next I want to improve the defense against worker harassment. Aggressive harassers like Iron and Bereaver often kill 2 or more drones, a grave setback (very like being set back in the grave). After that, I want to finally fix the mutalisk tactics and put those terrans in their place. When the mutas meet a strongpoint they should not call a dance party, they should turn aside (“you’re no fun!”) and fly around it. After comparing ideas and thinking ahead to how I want to integrate it someday with machine learning, I’m leaning toward Berkeley Overmind-style convex hull analysis rather than Overkill-style pathing analysis or ZerGreenBot-style reactive movement. But we’ll see.

One thing at a time. Mutalisk skills first, then lurker skills, then drop skills. Once I have all those, opponents will need real smarts to foresee what is going to happen to them.

Tomorrow: Some of Steamhammer’s opponent-specific openings.

Steamhammer 1.0 uploaded

Steamhammer 1.0 is uploaded. I had 2 essential bugs to fix; I fixed 1 and the other turned out to be transient or intermittent so it was not essential after all. After that, testing turned up 3 more bugs in the new adaptation code, and they affected how openings played out so they were essential too. Whew, all fixed, it took until now! The web page is not updated yet. I’ll also write it up here. I just... need some... time.

By the way, 3 brand new bots have also appeared, and old standards have been updated. This is a fun time.

Update: In a comment, AIL offers a video of his new bot AILien playing 3 games.

SSCAIT 2016 round of 8 - second half

Here is the second half of the SCCAIT round of 8. Today I’ll go over the later 2 matches of the 4, from the second video.

Bereaver vs XIMP

The newcomer protoss Bereaver versus the old school carrier bot XIMP.

XIMP always cannons itself in and goes carriers. The strategy seems easy to counter, and yet somehow it is not so easy; XIMP is still highly successful. I think protoss has the most difficult time countering the carriers. Terran can make tanks and blast down the cannon wall, and zerg can stop the carriers and prevent all expansions with hydralisks. Protoss has counters too, but they are not as simple to execute.

In game 1, Bereaver opened with two gates. On seeing the cannons it had the sense to immediately start its own natural and soon take its 3rd as well, pulling ahead in economy. Bereaver added some scattered cannons of its own, which seems strange and inefficient to me, but also got templar tech and started storm research, which is good play.

When the carriers arrived, high templar apparently did not yet have storm energy. Bereaver caught XIMP’s third starting and stopped it, which was necessary, but had made too many zealots and cannons and not enough dragoons, which was not promising against carriers. If Bereaver had mineral excess and gas shortage, then it probably should have taken a 4th base sooner.

But Bereaver did get a 4th soon enough, built more goons, and with help from some poor tactical decisions by XIMP, defeated the carriers using dragoons and storm. Bereaver had held off the carriers and stopped expansions, so it won.

Game 2 followed a similar course, but XIMP’s carriers took the long way around the map and XIMP successfully started a 3rd base (on its second attempt), while Bereaver saw nothing and opted to attack the cannon wall, setting itself back. Bereaver ignored the 3rd too long and XIMP got cannons up there, while Bereaver didn’t spend its extra money on a new 4th base after losing its first attempt to the carriers. Bereaver’s game plan failed and XIMP won.

I noticed movement on the minimap which looked like a failed reaver drop in XIMP’s main, but we didn’t get to see it in the video.

In Game 3 Bereaver got confused by the map, Heartbreak Ridge: It sent a probe to take a 3rd but was unable to navigate the mineral block. The probe wandered aimlessly. “This way! No, it’s closed off. This way! No. This way!” Bereaver went down without much fight.

XIMP, by the way, understands mineral blocks and is able to mine them out. A bot has to do a lot right to play well, and XIMP does a lot right. Bereaver is strong but doesn’t quite have the same robustness.

Krasi0 vs Tscmoo protoss

Krasi0’s game plan is to expand and build up while holding off any attacks with efficient terran defense, then move out with a large force that is difficult for any enemy to oppose directly. Tscmoo protoss is unpredictable, but it has played many games lately with ceaseless pinprick harassment. The clash of styles between two top bots promises to be fun!

Game 1. Tscmoo opened with scouts, expensive units with powerful air attack and puny ground attack. On the one hand, objectively scouts are poor at harassment. On the other hand, opponents have proven weak against widespread harassment and protoss has few other options for it. Tscmoo has won games this way, including against Krasi0.

As the game went, the scouts stopped mining at Krasi0’s natural for too long; air defense took a while to kick in. Tscmoo quickly went up to 5 bases with a fleet beacon to upgrade the scouts and shuttles with reavers to add firepower to the harassment. Krasi0 was satisfied with 3 bases for the time being, which seems fine to me.

Well, Krasi0 built many turrets and had good enough positioning, and Tscmoo’s harassment achieved nothing. Krasi0’s first push won outright. Oh well.

Game 2. This time Tscmoo went dark templar. Krasi0 the strong defender was of course prepared, and the dark templar never got close.

Tscmoo followed up with arbiters and researched recall. Arbiters started flitting irregularly across the map looking for openings, but Krasi0 had again built many turrets in its main and there were few openings to find. An arbiter finally recalled the 3rd. It stopped mining for a time but was not fully successful because the zealots in the recall did not feel like giving their lives for Aiur. Units with no escape route need the all-in mentality: “Die I must. Let me sell my life dearly.”

Krasi0 pushed out and started taking down bases, and Tscmoo never made a strong move to defend itself. Arbiters flew around stasising random units but not firing. Krasi0 built an absurd number of turrets but was too far ahead to suffer from the needless expense. Tscmoo's play was not focussed enough to make progress; it came across as scattershot and ineffective. Of course, that is largely because Krasi0 defended with cautious thoroughness.

Next I’ll cover the round of 4, likely tomorrow on the same day that the finals are broadcast. I didn’t catch up with real time after all.

SSCAIT 2016 round of 8 - first half

The SCCAIT round of 8 was played last week as 4 best-of-3 matches. The loser of each match is out and the winner moves on. Today I’ll go over the first 2 matches of the 4, from the first video.

LetaBot vs WuliBot

This match was easy to call and I don’t have much to say about it. The forecast: LetaBot will wall in and zealot-heavy Wuli will be unable to cope.

As it turned out the first game was even less interesting—Wuli froze up and died with one pylon to its name. The second game went to script. Wuli’s zealot rush is strong but risks being hard countered, and that’s what happened.

ZZZKBot vs Iron

ZZZKBot’s 4 pool also risks a hard counter. And Iron knows a counter, but it is not as hard as it could be. When Iron sees the danger it stops any tech beyond barracks (often canceling gas) and pulls SCVs to block its entrance. As soon it can it builds a bunker behind the SCVs, and if it succeeds in getting marines in, it is usually safe.

As always, Iron knows some excellent micro tricks. When it has enough SCVs blocking, it will sometimes mineral walk damaged front SCVs back through its own blockade to the mineral line. That means that the SCV right-clicks minerals so that it passes through any intervening units, allowing Iron to rescue damaged units in front without opening its blockade.

Game 1 was on the level-ground 2-player map Heartbreak Ridge, ideal for the 4 pool. Iron saw it coming in time, got the bunker up, and got a marine into it, pulling nearly all SCVs to block. It was close, but Iron held and had more income. When it was safe, Iron switched back into its usual aggressive strategy and won easily.

Game 2 was on Icarus, a 4-player map with ramps which is not as good for the 4 pool. But the bases turned out to be close together, which is favorable for the rush. Both bots scouted each other in time. This time ZZZKBot broke the ramp and won—Iron got a bunker up but could not get a marine into it. ZZZKBot showed impressively smart targeting with its lings, switching smoothly between hitting the empty bunker and chasing away any terran units that came close. At one point zerg split its lings into 2 groups, one to chase the last marine and one to disrupt mining.

The deciding game 3 was on Empire of the Sun, a 4-player map but without ramps and with a wider entrance to defend than Heartbreak Ridge. ZZZKBot sent its overlord scout the right direction and did not need to make an extra drone to scout, which strengthens the rush slightly because 2 more zerglings fit under the supply limit. 3 drones mining are enough to keep up constant zergling production; more drones produce extra resources that are only useful if the rush fails, when the rusher is generally lost anyway.

But in any case, this time Iron narrowly held and won. Apparently the result depends more on random factors than on favorable or unfavorable conditions!

With good worker micro, it is more efficient to hold the rush in the mineral line, rather than at the entrance. Tscmoo knows how to do it. In pro games, attacking zerglings have to be cautious around workers and only pick off stragglers, because the workers fight so effectively.

Tomorrow: The second half of the round of 8. Maybe I’ll catch up with real time by the time the finals broadcast on Saturday!

UAlbertaBot fixes #9 and #10

And these are the last of the batch.

#9 tracking mineral patches—or not

WorkerData has two maps of worker unit -> mineral patch.

  std::map<BWAPI::Unit, enum WorkerJob>         workerJobMap;
  std::map<BWAPI::Unit, BWAPI::Unit>  workerMineralMap;
  std::map<BWAPI::Unit, BWAPI::Unit>  workerDepotMap;
  std::map<BWAPI::Unit, BWAPI::Unit>  workerRefineryMap;
  std::map<BWAPI::Unit, BWAPI::Unit>  workerRepairMap;
  std::map<BWAPI::Unit, WorkerMoveData>         workerMoveMap;
  std::map<BWAPI::Unit, BWAPI::UnitType>        workerBuildingTypeMap;

  std::map<BWAPI::Unit, int>                    depotWorkerCount;
  std::map<BWAPI::Unit, int>                    refineryWorkerCount;

  std::map<BWAPI::Unit, int>                    workersOnMineralPatch;
  std::map<BWAPI::Unit, BWAPI::Unit>  workerMineralAssignment;

They are workerMineralMap and workerMineralAssignment. workerMineralAssignment is the “real” one that is updated. workerMineralMap is not updated and is referred to only in WorkerData::getWorkerResource(), which in UAlbertaBot happens to be called only to get the refinery and never the mineral patch.

I deleted workerMineralMap and replaced its 2 occurrences with workerMineralAssignment. The bug has no practical effect without other code changes, but it was lying in wait.

#10 no overlords in combat

This one is not mine--AIL reported it in a blog comment. If a zerg bot includes overlords in a combat squad as detectors (which you have to write code to do), then the bot does not pass the overlords in to SparCraft and ends up fleeing from dark templar without trying to fight because there is “no detection”.

I rewrote InformationManager::isCombatUnit() like this. The original has a redundant check to exclude lurkers and doesn’t include overlords.

bool InformationManager::isCombatUnit(BWAPI::UnitType type) const
{
	return
		type.canAttack() ||
		type == BWAPI::UnitTypes::Terran_Medic ||
		type == BWAPI::UnitTypes::Terran_Bunker ||
		type == BWAPI::UnitTypes::Protoss_Observer ||
		type == BWAPI::UnitTypes::Zerg_Overlord;
}

Thanks, AIL!

UAlbertaBot fixes #6 through #8

Thanks to AIL I have a 10th fix, so I decided to cover 3 today.

#6 a morphing hive is not a resource depot

WorkerManager::getClosestDepot() figures out what command center/nexus/hatchery to send a worker to.

  if (unit->getType().isResourceDepot() &&
    (unit->isCompleted() || unit->getType() == BWAPI::UnitTypes::Zerg_Lair) &&
    !workerData.depotIsFull(unit))

The unit->isCompleted() part handles a corner case. If a command center, nexus, or hatchery is uncompleted, it can’t accept resources. If a lair or a hive is still morphing, it can. The bug is that the check omits hive.

  if (unit->getType().isResourceDepot() &&
    (unit->isCompleted() || unit->getType() == BWAPI::UnitTypes::Zerg_Lair || unit->getType() == BWAPI::UnitTypes::Zerg_Hive) &&
    !workerData.depotIsFull(unit))

#7 my base is your base

This is the one I wrote up as Steamhammer’s funniest bug. InformationManager::updateBaseLocationInfo() implements (among other things) a heuristic to guess where the enemy base is: If an enemy building is seen in the same region as a starting location, it assumes that the enemy base is at that starting location. Obviously it can be fooled if the enemy builds in another main base.

  if (isEnemyBuildingInRegion(BWTA::getRegion(startLocation->getTilePosition())))
  {
    if (Config::Debug::DrawScoutInfo)
    {
      BWAPI::Broodwar->printf("Enemy base found by seeing it");
    }

    baseFound = true;
    _mainBaseLocations[_enemy] = startLocation;
    updateOccupiedRegions(BWTA::getRegion(startLocation->getTilePosition()), BWAPI::Broodwar->enemy());
  }
...

For example, if the enemy proxies in your base, UAlbertaBot believes that your base is the enemy base, with hilarious results. I fixed it by checking against our own base location. That removes this way for the heuristic to fail, but it adds another one—if you ever play on a map in which your base and the enemy base are in the same region, the bot cannot find the enemy base this way. That’s life when you accept heuristics.

  if (isEnemyBuildingInRegion(BWTA::getRegion(startLocation->getTilePosition()))) 
  {
    updateOccupiedRegions(BWTA::getRegion(startLocation->getTilePosition()), BWAPI::Broodwar->enemy());

    // On a competition map, our base and the enemy base will never be in the same region.
    // If we find an enemy building in our region, it's a proxy.
    if (startLocation != _mainBaseLocations[_self]))
    {
      if (Config::Debug::DrawScoutInfo)
      {
        BWAPI::Broodwar->printf("Enemy base found by seeing it");
      }

      baseFound = true;
      _mainBaseLocations[_enemy] = startLocation;
    }
...

#8 assigning workers to buildings

BuildingManager::assignWorkersToUnassignedBuildings() is responsible for choosing what worker builds each building. It calls on WorkerManager to find a worker which is close to the building’s final position, which is stored in b.finalPosition. I read this code quite a few times before the bug hit me.

  // grab a worker unit from WorkerManager which is closest to this final position                                         
  BWAPI::Unit workerToAssign = WorkerManager::Instance().getBuilder(b);

  if (workerToAssign)
  {
    //BWAPI::Broodwar->printf("VALID WORKER BEING ASSIGNED: %d", workerToAssign->getID());                               

    // TODO: special case of terran building whose worker died mid construction                                          
    //       send the right click command to the buildingUnit to resume construction                                     
    //           skip the buildingsAssigned step and push it back into buildingsUnderConstruction                        

    b.builderUnit = workerToAssign;

    BWAPI::TilePosition testLocation = getBuildingLocation(b);
    if (!testLocation.isValid())
    {
      continue;
    }

    b.finalPosition = testLocation;

    // reserve this building's space                                                                                     
    BuildingPlacer::Instance().reserveTiles(b.finalPosition,b.type.tileWidth(),b.type.tileHeight());

    b.status = BuildingStatus::Assigned;
  }

It is assigning “the closest worker” before the position it is to be close to has been computed. In practice, I found it was assigning the free worker closest to (0,0). If the bot has several bases, it may send a distant worker on a long trek. Since UAlbertaBot normally plays rushes, the buggy behavior is not easy to notice in its play, but Steamhammer likes to take the map.

  BWAPI::TilePosition testLocation = getBuildingLocation(b);
  if (!testLocation.isValid())
  {
    continue;
  }

  b.finalPosition = testLocation;

  // grab the worker unit from WorkerManager which is closest to this final position
  b.builderUnit = WorkerManager::Instance().getBuilder(b);
  if (!b.builderUnit)
  {
    continue;
  }

  // reserve this building's space
  BuildingPlacer::Instance().reserveTiles(b.finalPosition,b.type.tileWidth(),b.type.tileHeight());

  b.status = BuildingStatus::Assigned;

UAlbertaBot fixes #4 and #5

#4 where are you? where are you?

CombatCommander::getMainAttackLocation() figures out where the “main attack” squad should aim to attack. It thinks the enemy main base is the top possibility. Here enemyBaseLocation is the location of the enemy main base (the comment is a little off), null if the enemy base location is not known.

    // First choice: Attack an enemy region if we can see units inside it                                                        
    if (enemyBaseLocation)
    {
        BWAPI::Position enemyBasePosition = enemyBaseLocation->getPosition();

        // get all known enemy units in the area                                                                                 
        BWAPI::Unitset enemyUnitsInArea;
        MapGrid::Instance().GetUnits(enemyUnitsInArea, enemyBasePosition, 800, false, true);

        bool onlyOverlords = true;
	for (auto & unit : enemyUnitsInArea)
        {
            if (unit->getType() != BWAPI::UnitTypes::Zerg_Overlord)
            {
                onlyOverlords = false;
            }
        }

        if (!BWAPI::Broodwar->isExplored(BWAPI::TilePosition(enemyBasePosition)) || !enemyUnitsInArea.empty())
        {
            if (!onlyOverlords)
            {
                return enemyBaseLocation->getPosition();
            }
        }
    }

The isExplored() check is supposed to account for the case where the location of the enemy base has been inferred although the base has not been seen. (UAlbertaBot infers the location of the enemy base when it has seen all but 1 starting location and they’re all bare.) But the check is not correct, because of how onlyOverlords and isExplored() are combined.

The effect is that, if the enemy base location has been inferred, the first combat units do not go toward it but believe that the enemy base is empty. If no other enemy buildings have been seen, the squad ends up exploring the map trying to find the enemy whose location is already known. This happened in one game of Steamhammer 0.2 versus PeregrineBot, and it was the main reason that PeregrineBot won. I’ve seen it in other games too.

The exploration check can be made correct by separating it from the onlyOverlords check.

	if (enemyBaseLocation)
	{
		BWAPI::Position enemyBasePosition = enemyBaseLocation->getPosition();

		// If the enemy base hasn't been seen yet, go there.
		if (!BWAPI::Broodwar->isExplored(BWAPI::TilePosition(enemyBasePosition)))
		{
			return enemyBasePosition;
		}

		// get all known enemy units in the area
		BWAPI::Unitset enemyUnitsInArea;
		MapGrid::Instance().GetUnits(enemyUnitsInArea, enemyBasePosition, 800, false, true);

		for (auto & unit : enemyUnitsInArea)
		{
			if (unit->getType() != BWAPI::UnitTypes::Zerg_Overlord)
			{
				// Enemy base is not empty: It's not only overlords in the enemy base area.
				return enemyBasePosition;
			}
		}
	}

#5 there is always a building rush

Here is how UAlbertaBot checks whether it is getting proxied.

bool CombatCommander::beingBuildingRushed()
{
    int concernRadius = 1200;
    BWAPI::Position ourBasePosition = BWAPI::Position(BWAPI::Broodwar->self()->getStartLocation());

    // check to see if the enemy has zerglings as the only attackers in our base                                                 
    for (auto & unit : BWAPI::Broodwar->enemy()->getUnits())
    {
        if (unit->getType().isBuilding())
	{
            return true;
        }
    }

    return false;
}

Well, there is an incorrect comment, but that’s not a bug. The routine omits one little condition.

        if (unit->getType().isBuilding() && unit->getDistance(myBasePosition) < 1200)

UAlbertaBot thinks it is being proxied whenever it knows the location of any enemy building anywhere on the map. The bug causes workers to be pulled for emergency defense more often than originally intended. See CombatCommander::findClosestDefender. Here’s the rather confusing line you may want to rewrite to change when workers are pulled for defense:

        if (!Config::Micro::WorkersDefendRush || (unit->getType().isWorker() && !zerglingRush && !beingBuildingRushed()))

UAlbertaBot fix #3 - the double overlord bug

ProductionManager::detectBuildOrderDeadlock() figures out when the bot is supply blocked so that it can order up supply if needed—for zerg, it orders an overlord. To tell whether the supply block is real, it needs to know not only current supply but pending supply—if supply is already being built, you’re not blocked. Here’s the critical code in UAlbertaBot:

  // are any supply providers being built currently                                                                        
  bool supplyInProgress = BuildingManager::Instance().isBeingBuilt(BWAPI::Broodwar->self()->getRace().getSupplyProvider());

  for (auto & unit : BWAPI::Broodwar->self()->getUnits())
  {
    if (unit->getType() == BWAPI::UnitTypes::Zerg_Egg)
    {
      if (unit->getBuildType() == BWAPI::UnitTypes::Zerg_Overlord)
      {
        supplyInProgress = true;
        break;
      }
    }
  }

Oh, but there is a tricky case for zerg. When an overlord is just hatched from its egg, it has unit type overlord but does not provide supply for a short time. The pending supply is unnoticed in the above code, the routine detects a supply block when there is none, and the bot orders 2 overlords in a row. The first overlord spawned turns into 2 overlords in around half of games. That is why Steamhammer 0.2 often finds itself with 3 overlords (providing 25 supply total, including the hatchery) when it is using 10 supply. The wasted minerals and larva ruin any opening that depends on precise timing, which is most zerg openings.

I solved it by counting supply by hand, including pending supply. Remember that BWAPI counts supply in units double what the game reports in the user interface.

	// If supply is being built now, there's no block. Return right away.
	// Terran and protoss calculation:
	if (BuildingManager::Instance().isBeingBuilt(BWAPI::Broodwar->self()->getRace().getSupplyProvider()))
	{
		return false;
	}

	// Terran and protoss calculation:
	int supplyAvailable = BWAPI::Broodwar->self()->supplyTotal() - BWAPI::Broodwar->self()->supplyUsed();

	// Zerg calculation:
	// Zerg can create an overlord that doesn't count toward supply until the next check.
	// To work around it, add up the supply by hand, including hatcheries.
	if (BWAPI::Broodwar->self()->getRace() == BWAPI::Races::Zerg) {
		supplyAvailable = -BWAPI::Broodwar->self()->supplyUsed();
		for (auto & unit : BWAPI::Broodwar->self()->getUnits())
		{
			if (unit->getType() == BWAPI::UnitTypes::Zerg_Overlord)
			{
				supplyAvailable += 16;
			}
			else if (unit->getType() == BWAPI::UnitTypes::Zerg_Egg &&
					 unit->getBuildType() == BWAPI::UnitTypes::Zerg_Overlord)
			{
				return false;    // supply is building, return immediately
				// supplyAvailable += 16;
			}
			else if ((unit->getType() == BWAPI::UnitTypes::Zerg_Hatchery && unit->isCompleted()) ||
					 unit->getType() == BWAPI::UnitTypes::Zerg_Lair || 
					 unit->getType() == BWAPI::UnitTypes::Zerg_Hive)
			{
				supplyAvailable += 2;
			}
		}
	}

I haven’t tried it to make sure, but I think another way to fix it would be to look for overlords for which unit->getOrder() returns BWAPI::Orders::ZergBirth and count them as pending supply.

I sent Dave Churchill 9 fixes (so far!). I’m planning to blog the rest 2 at a time, so I should be able to write up the remaining 6 over the next 3 days. Tomorrow: #4 has to do with squad orders and #5 with detecting building rushes.

UAlbertaBot fixes #1 and #2

I heard news from Dave Churchill about UAlbertaBot.

1. He has been working on SparCraft. He has changes that are not yet pushed to github.

2. He is willing to take my bug fixes and may merge them into UAlbertaBot as he has time (which I take to mean don’t hold your breath, he’s busy). He prefers bug writeups to pull requests, so I guess the code changes to UAlbertaBot are extensive.

I’ve already written up 2 bug fixes and sent them off. I’ll also post them here, because if you’re actively working on a UAlbertaBot fork then you don’t want to wait.

If you have your own bug fixes to UAlbertaBot, one way to get them into the pipeline is to send them to me. If you want to do it that way, post a comment here or e-mail me, and be sure to include enough details. I will post them for the community and send them on to Dave Churchill as appropriate, with credit to you, of course.

#1 null pointer bug

From CombatCommander::updateScoutDefenseSquad():

  // get the region that our base is located in                                                                                
  BWTA::Region * myRegion = BWTA::getRegion(BWAPI::Broodwar->self()->getStartLocation());
  if (!myRegion && myRegion->getCenter().isValid())
  {
    return;
  }

Oops, if the pointer is null it goes down on its face. It’s a simple slip. I haven’t found a map that tickles this bug, and as far as I know the pointer might never be null (if so, the whole check can be dropped). But in the spirit of defensive programming I changed the if condition:

  if (!myRegion || !myRegion->getCenter().isValid())

#2 building zerg static defense

From ProductionManager::create(BWAPI::Unit producer, BuildOrderItem & item):

  if (t.isUnit() && t.getUnitType().isBuilding()
    && t.getUnitType() != BWAPI::UnitTypes::Zerg_Lair
    && t.getUnitType() != BWAPI::UnitTypes::Zerg_Hive
    && t.getUnitType() != BWAPI::UnitTypes::Zerg_Greater_Spire
    && !t.getUnitType().isAddon())
  {
...

This omits 2 morphed unit types. The error prevents a zerg bot from morphing any static defense.

        && t.getUnitType() != BWAPI::UnitTypes::Zerg_Sunken_Colony
        && t.getUnitType() != BWAPI::UnitTypes::Zerg_Spore_Colony

By the way, there is a similar omission in BOSS. From ActionTypeData::ActionTypeData(BWAPI::UnitType t, const ActionID id):

   if (t == BWAPI::UnitTypes::Zerg_Lair ||
    t == BWAPI::UnitTypes::Zerg_Hive ||
    t == BWAPI::UnitTypes::Zerg_Greater_Spire ||
    t == BWAPI::UnitTypes::Zerg_Lurker ||
    t == BWAPI::UnitTypes::Zerg_Guardian ||
    t == BWAPI::UnitTypes::Zerg_Sunken_Colony ||
    t == BWAPI::UnitTypes::Zerg_Spore_Colony)
  {
    morphed = true;
  }

It omits Zerg_Devourer. I haven’t tried to make a devourer via a BOSS production plan, so I don’t know what effects it has to leave them out. But it sure looks like a bug.

Tomorrow: The double overlord bug.

Steamhammer’s greatest misses

All bots have bugs, even strong bots. Watching a game Krasi0 vs Iron today, I saw that Iron had built many academies, some in rows like subsidized housing. Sadly, its devotion to education did not help it win. Krasi0, by the way, has a similar bug with turrets. It’s a kind of symmetry.

Here are some of Steamhammer 0.2’s most dramatic bugs.

drone defense aka suicide

Steamhammer loves to defend with drones. You can almost hear it jumping up and down, “Can I send out the drones yet? Can I send out the drones?” When it works, it looks brilliant—how did it kill all those zerglings and only lose 1 drone? When it fails, it looks ridiculous. Here the drones are blocking the bot’s own zerglings and the game is about to be a massacre.

I sent out the drones!

suicide aka drone non-defense

Despite its love of drone defense, Steamhammer sucks at defending its drones from early harassment. Its system of chasing the enemy scout with one drone is adequate against weaker bots, but often loses 1 or more drones against strong opponents. Here Bereaver’s probe already has 2 kills, and the newly-hatched zerglings are speeding past ignoring it. Not their job.

probe victorious

gas > life

Many bots share this play bug. Steamhammer 0.2 believes that vespine gas is more precious than life itself.

If you kill a gas drone, the bot will replace it. If you kill all but a few drones, it will put the survivors on gas and mine no minerals. If you kill a hatchery and leave the extractor, it will send drones to long-distance mine the extractor, and if you kill those, it will send more. Leave a few units in the destroyed base and Steamhammer 0.2 will ship over its entire mineral-mining population 3 at a time to be exterminated. Gas is more precious than life.

This bug is related to strategy, so it is mostly fixed. Steamhammer dev version has skills to start and stop collecting gas at sensible times. Tonight I’ll fix a bug in the interaction between automatically turning off excess gas collection and gas limits specified in the opening book. Later I’ll fix the one last corner case that I know of: If you kill an expansion hatchery while its extractor is still morphing, the bot will still do the extermination train when the extractor finishes.

expand to the enemy base

Some have wondered why, in longer games, Steamhammer sometimes sends a drone or two to the enemy base to die. That’s simple. It wants to expand there. What’s to stop it?

this looks like a good place to build

UAlbertaBot’s native “where’s the next expansion?” routine does not take into account where the enemy is. It only checks whether the spot is blocked by a known building, and I suspect that the check is not correct. The dev version tries to expand away from the enemy, only sets its sights on untaken bases, and is reluctant to take a base in the same region as any known enemy building (but it still will try, in a pinch).

scouting by mistake

But expanding to an enemy base is not so bad if you didn’t realize. After the early game, Steamhammer does not scout again until all known enemy buildings are destroyed. If you build where Steamhammer does not want to attack, there’s a good chance that you won’t be discovered until late game. It usually finds enemy expansions eventually, though, because it likes to expand across the map.

Scouting is high on my list, which means I should get to it sometime in February.

detection? what’s that?

Who would even try a straight dark templar rush against zerg, with no attempt to chase away overlords? Well, MegaBot would. It worked, too. Here the drones are trying to help out in a fight against invisible assassins that can kill them in one blow. Sure, you can send in the drones, stop jumping.

dark templar destroy a base

I’ll take this base twice—no, three times!

If you watch Steamhammer games, sooner or later the camera will move to a new base with a hatchery, one or two idle drones, and nothing else. The nothing else is because the bot does not try to balance its drones among bases; it only transfers as old bases saturate or mine out. The idle drones are because Steamhammer tried to take the same base more than once at the same time. The production queue had 2 or 3 hatcheries in a row, and the building placer decided to put them in the same spot. Often 2 drones arrive simultaneously and repeatedly prevent each other from starting the building—no need for an enemy, Steamhammer can harass itself. Eventually one succeeds, and then the other drones are despondent and go idle.

Nice one, right? It turns out that the building placer was missing a check, “don’t place a hatchery in a spot already reserved for another.” I added the check, but it still behaves the same; either the check is wrong (which I suspect), or else there’s another mistake. Steamhammer dev version at least puts the idle drones back to work.

So little to do, so much time! How will I alleviate the boredom?

Steamhammer vs LetaBot, SSCAIT round of 16

Steamhammer’s round of 16 game in the SSCAIT was entertaining. It was played on Moon Glaive against Martin Rooijackers, the latest LetaBot. Steamhammer has played a bunch of games against this version of LetaBot, so I can roughly estimate by eye (without counting the games to be sure) that Steamhammer wins maybe 1/4 of the time or so. LetaBot is clearly stronger, but there is a chance of an upset. The round of 16 is single elimination, so the winner moves on and the loser is out.

Steamhammer 0.2 opened with its 2 hatch muta rush and LetaBot went rax-expand. The video doesn’t show it, but Steamhammer’s first 4 zerglings got 3 SCV kills between them. They sniped the SCV building the natural command center and 2 more that came to replace it before marines chased them off. That set terran back, though not decisively. Here the zerglings are escaping from marines that you can see on the minimap, and LetaBot is about to resume construction.

the 4 zerglings escape

LetaBot built 2 bunkers in front of its natural for safety. Unfortunately for Steamhammer, the map positions meant that the bunkers were on the straight line flying path between bases, so Steamhammer’s bunker OCD meant that zerg could not harass. The mutalisks stopped and stared at the bunkers in a standoff. Nepeta in the video politely called it “containing” the terran, but since zerg played a rush opening and terran played an economic opening, zerg was falling behind with every passing second. The rush must do damage to win.

mutalisks stare at the bunkers

LetaBot already had a winning game when terran moved out. If the terran ball had stayed compact, the terran forces would have punched through all opposition and won on the spot. But LetaBot let its forces string out, and the mutalisks were—just barely—able to defeat the army in detail. (Watch Carsten Nielsen play. The protoss bot may be the champion of maintaining good formation with its zealots while attacking. Few bots can do it at all.)

LetaBot lets its forces string out

I selected a mutalisk so you can see the zerg resources and supply. Steamhammer 0.2’s poor macro is showing; it expands too late and does not know how to build macro hatcheries, and it did not have enough hatcheries to spend all its income. Terran was economically far ahead and zerg was failing to keep up production, so it hardly needs saying that the next attack brought victory to LetaBot. (Macro is much improved in the current development version, though still not good enough. The current version would have crushed the attack with forces to spare, and would have still been in the game.)

An entertaining game, though more one-sided than it may look!

backing up is hard to do

If you want to beat Steamhammer without really trying, I have a suggestion. All you have to do is maneuver your army to the other side of Steamhammer’s and put yourself on the path between Steamhammer and its base. You leave your base wide open, but you’ll probably win. It works versus ground units and versus air units.

At some point Steamhammer will want to retreat. Even when winning, it sometimes retreats for no apparent reason (I haven’t looked into why yet). It pays no attention to its surroundings when it decides to retreat, so if you are on the path between Steamhammer and its base, it will retreat through your army. And Steamhammer does not fight when it retreats—the units are on move command.

Here Steamhammer forced its way into Carsten Nielsen’s base, started in on the probes, then got scared for no good reason and left, giving the zealots free kills. If more zealots had been near the ramp, all the zerglings could have been caught.

Steamhammer loses zerglings

The next one is even more infuriating. Sometimes, Steamhammer retreats from a threat but does not retreat out of weapons range. It seems unpredictable when. It’s most glaring with mutalisks and goliaths: Mutalisks approach, take hits, retreat without firing, turn around before escaping goliath range and approach again, continue to take hits, retreat without firing... it doesn’t go on long, soon they’re all dead with nothing accomplished.

Here Steamhammer’s mutalisks are moving back and forth in goliath range, offering free lunch. The mutas did eventually escape, only to get caught again.

Steamhammer loses mutalisks

It also happens sometimes with mutalisks and bunkers, zerglings and bunkers, and zerglings and tanks. And probably other unit combinations. But it doesn’t happen every time, only sometimes.

I don’t have immediate plans to fix either of these weaknesses.

Later: I’ll have at least one more post about bugs and weaknesses in Steamhammer 0.2.

Steamhammer’s bunker obsession

The #2 most frustrating weakness in the play of Steamhammer 0.2 is its insane obsession with bunkers.

Here’s the kind of strongpoint Icebot builds when it sees Steamhammer’s mutalisks coming: A bunker, surrounded by turrets, with SCVs to repair, placed out in front where it can’t be overlooked. Objectively, it’s a stupid move, because the rest of the terran base is barely defended. Against an opponent that knows how to bypass strongpoints, like Overkill, Icebot collapses.

Icebot's strongpoint

But against Steamhammer, Icebot holds and usually wins. Steamhammer sees that bunker and can’t take its eyes away. Steamhammer measures the air defense and realizes it can’t attack, so it pulls back a little, waits, collects some more mutalisks, pokes in to take another look, still doesn’t have enough, waits some more... soon Icebot has goliaths and valkyries and it is too late. Steamhammer’s opening is a low-econ rush, and the last thing it can afford is to wait.

It’s frustrating for two reasons. One, it’s intolerably foolish and must be fixed. And two, it’s not a simple bug that can be set right with an hour of coding. It’s an ingrained behavior with both tactical and micro aspects—it’s the result of a design attitude, and it has more than one cause.

I already fixed one cause in moving from Steamhammer 0.1 to the tournament 0.2 version. In 0.1, the mutalisks were grouped with zerglings in the same squad and followed the same path, so the mutas were constrained to ground paths and could not avoid front bunkers. In 0.2 the mutas are in their own squad and can fly as they will, so in some cases they fly a straight line path far from any bunker and ravage the terran. It helps noticeably, but not enough.

Another cause is targeting priorities. Steamhammer 0.2 believes that a bunker is more important to attack than a marine or a turret. When it collects enough mutalisks to fight, it should pick off turrets until the bunker is laid bare—the turrets are easier to destroy. Instead it ignores the turrets and goes straight for the bunker, which of course terran repairs. The development version of Steamhammer gets the priorities mostly right, after about 3 rounds of tinkering, so this cause is fixed too (well, fixed enough for now). New Steamhammer plays clearly better around bunkers.

But improved Steamhammer still shows obsessive tendencies. Given an unguarded command center and a bunker, instead of winning the game without risk it will too often divert to fight the bunker, depending on what happens to catch its attention.

The most fundamental cause is that the tactical system is not conceived with guerilla warfare in mind. I think many bots share this weakness, maybe most bots. Steamhammer, like its parent UAlbertaBot, draws a line from its base to the enemy’s and wants to progress down the line: Defeat the units in front of me, move forward, defeat the units in front of me, etc. It’s a super-simplified tactical framework. And for the guerilla warfare of air units, it is exactly backwards. The guerilla wants to bypass the units in front and hit wherever the defenders aren’t.

The simplification shows in how the combat simulator SparCraft works. SparCraft wargames out a battle under the assumption that both sides stand and fight, and predicts who will win: The side that is not annihilated. But in the fight of mutalisks versus terran base, only the terrans want to stand and fight. The mutalisks are happy to hit and run, as long as they don’t suffer too much. The mutalisks would prefer to ask SparCraft the question, “can I kill something—anything—and get away without any mutas dying?”

Anyway, I’m not going to fix this fundamental cause soon. Someday I hope to work on SparCraft and teach it to answer more questions. But that is at the tactics level, and I’m doing strategy first.

Tomorrow: The #1 most frustrating weakness: Retreating is hard to do.

picking the opening randomly

Why does random UAlbertaBot score so well? Its 5 pool rush is easy to counter. Its marine rush is weak and easy to counter. Its zealot rush is strong, but it can be stopped too. I think the secret is: What opening counters all 3? Well, overpool works, but you have to play it very differently depending on what race UAlbertaBot turns out to be. Bots struggle to adapt. Randomness has value in itself, because it forces the opponent first, to choose openings carefully, and second, to scout and adapt.

Steamhammer’s new strategy boss is not far along; it can only respond to a few basic emergencies, like having no drones. (Steamhammer 0.2 freezes; the development version orders up a drone and can play on if it has 50 minerals.) But yesterday I decided it would be more fun to switch the order of improvements around and do randomized openings before getting the strategy basics down.

I was right, the variety is a lot more fun. I don’t know in advance what Steamhammer is going to do; I have to watch and see.

Today I played a long ZvZ match versus Zia to make sure that the random openings don’t hurt performance. Steamhammer played 5 different openings. One of the openings dominates Zia, but Zia has answers to the other 4—if it can find the answers with its opening learning. Does mixing in the weaker openings give Zia a chance to find the answers, or does the randomness wipe out any learning signal and make life even harder for Zia?

Answer: After 45 games, giving Zia time to learn, Steamhammer scored 40-5. Zia won 4 games by accidentally hitting on a counter and 1 by playing better tactically. At worst, the weaker openings do not hurt. They were chosen to have different counters, and Zia’s implicit assumption that there should be One Best Counter is wrong.

Lesson: Opening learning is not enough, at least not this kind. As difficult as it may be, you have to recognize what the opponent is doing and adapt to it.

On the downside, to earn the full advantage from random openings I have to implement more openings and make sure that Steamhammer plays all of them well enough. At the moment it only has enough variety in ZvZ. But someday, with both random openings and opening learning, Steamhammer will turn fearsome.

Update: I should have added a couple points. 1. Random openings make it hard for other bot authors to hand-code counters to your strategy. This change to Steamhammer should defeat Chris Coxe’s plan of countering specific opponents. 2. Random openings are more fun for humans to play against.

Tomorrow: The curséd bunker obsession. After that: Retreating is hard to do.

Steamhammer’s funniest bug

Here is Steamhammer’s most hilarious bug. When facing Jakub Trancik’s cannon bot, Steamhammer opens with 9 pool, a sunken, and then an expansion. The idea is that 9 pool counters the cannon rush; zerglings should be out before any cannons finish. If that somehow fails, the sunken prevents cannons from completely destroying the base, and the expansion gives zerg a foothold on the other side of the cannons; Steamhammer should be able to go destroy the protoss main. I hadn’t tested it, but with 3 separate counters to proxy cannons I felt confident. I was wrong, though. As it turned out, Steamhammer went 1-1 versus Jakub Trancik in the round robin phase of SSCAIT 2016.

In the losing game, here’s the probe trying to cannon up the zerg base. The probe took 1 sunken hit, escaped and started a pylon, then the probe and pylon immediately died. The defensive plan worked exactly as intended.

Jakub Trancik’s probe about to die

So how did Steamhammer lose? In the minimap, you can see Steamhammer’s overlord headed toward the northwest and drone scouting the southwest. The protoss main hadn’t been seen yet. When it saw a pylon warping in its base, Steamhammer applied a heuristic: I see an enemy building in this base location, so—this must be the enemy base! The scouting probe immediately returned home and starting running loops around the home base, because it was the enemy base too, and the scout never saw the enemy’s real base. It’s not entirely foolish behavior if your enemy likes to proxy! Zerglings explored around and found “another” enemy base filling up with cannons, and started to build up numbers in preparation for an eventual attack. The mistake didn’t look too bad so far.

Then Steamhammer built an extractor on its gas. Since the extractor was in enemy territory, it was obviously a gas steal, so the bot did not collect any gas. The drones will die in the enemy base, right? The zerg build was not able to progress, Steamhammer stopped building anything, and Jakub Trancik eventually did its zealot switch and won, gg.

Here is the unused “gas steal” extractor. That drone moving up the right is the scouting drone, still circling the “enemy” base. The minimap shows zerglings out exploring, looking for the “last remnants” of the protoss. You can see 2 zerg expansions; Steamhammer 0.2 is slow at it, but it set about methodically building up.

Steamhammer is about to freeze up

All those consequences from 1 flaky heuristic! I’m still laughing over it—I get to laugh because the fix is a 1-liner and I already made it in December. The heuristic and the bug are inherited from UAlbertaBot, but I have never seen the bug in UAlbertaBot’s play. I’ve also never seen the heuristic fail any other way, although it can. On the one hand, it’s a critical bug that can cause losses; on the other hand, it’s not that important to fix because it’s rare. Bugs are weird that way.

Next: More bugs and weaknesses (they won’t all get such a thorough writeup, though).

Steamhammer versus Zia for the SSCAIT round of 16

Steamhammer and Zia tied for 16-17th place in the round robin phase of SSCAIT 2016, and played a best-of-3 tiebreaker match to decide who would move on to the round of 16. The video is out with commentary, and here are the games. They’re all somewhat entertaining, if you can tolerate weak bot ZvZ. The caster seemed unfamiliar with bot play, so I’ll explain the reasons behind some of the events.

Game 1 was on Destination. Zia had learned from past games that its most successful opening was an aggressive 9 pool, and that’s how Zia opened. This version of Steamhammer follows a fixed 12 pool build, which is riskier on this 2 player map. When Zia’s initial 6 zerglings arrived at its base, Steamhammer was just about to produce its first 4 zerglings, so Steamhammer pulled drones to defend. With drones and 4 zerglings, Steamhammer should have defended safely. But Steamhammer tends to be overeager with its drone defense and pulls its drones too far out; it should defend in the mineral line, or at least on the creep, but it pulled drones most of the way to the ramp. (This is one of its most severe weaknesses, one step behind the absolutely-must-fix problems.) When Steamhammer’s zerglings followed the drones moments later, the drones got in their way, and too many drones were lost. Zia was far ahead, cautiously built up, and won easily.

Game 2 was on Circuit Breaker, a 4-player map where the aggressive 9 pool is less likely to work. Also, the players spawned in cross positions, so the rush distance was longer. Nevertheless, Zia had learned that it worked, so Zia played the aggressive opening. And sure enough, Zia arrived with more zerglings and Steamhammer again defended poorly, frittering away its lings against a larger force instead of massing for a short time before engaging. Zia was tearing down the natural hatchery while safely expanding itself, and the game seemed to be decided. Then Zia’s remaining zerglings settled on the wrong targets, one attacking an egg and another a larva. The hatchery was deep in the red and would have died if attacked by those 2 lings. Instead Steamhammer’s next wave cleared the invaders and the hatchery survived with 34 hit points, a sliver of red on its health bar. Then Steamhammer had both economic and tech advantages and went on to win. Steamhammer does not make the same targeting mistake, so you could say it was a deserved win, but it was no less a lucky win.

Game 3 was on Fighting Spirit. Zia’s learning algorithm said “uh oh, that didn’t work, better try another opening” and picked a defensive 9 pool instead. Zia massed its zerglings but held them on its ramp for safety. Steamhammer’s opening gave it more larvae, a stronger economy, and a faster tech path, all at the cost of fewer and later zerglings, so Zia’s choice to stay home was poor. Then Zia made another mistake when it wanted to expand to its natural. It should have swept its ling force out to secure the natural, but instead pulled back to the main hatchery to let the expansion drone out. Only after Steamhammer sniped a few expansion drones did Zia change its mind and attack across the map. Steamhammer defended with its characteristic incompetence, overeagerly pulling all drones and losing every one, but mutalisks were out and cleaned up.

Zia has a sneaky fallback when it repeatedly fails to take its natural: It takes a hidden base instead. This time it took the center base on Fighting Spirit. Zia is capable of rebuilding from a hidden base and the cash for a drone or two, but of course it takes a long time. Steamhammer’s mutalisks abolished Zia’s main and then set out across the map to find remaining buildings. Steamhammer’s search behavior (inherited from UAlbertaBot) is inefficient and looks strange, and the caster was understandably confused about it. The algorithm is simple. Steamhammer keeps track of when it explores each square of its map grid, and picks one of the squares that it has least recently explored as the next destination. It’s good enough.

Steamhammer will play LetaBot in the round of 16. The pairings are 1-16, 2-15 and so on, the arrangement with the least likelihood of upsets for the top finishers. Nevertheless, Steamhammer does defeat LetaBot occasionally and has a small chance to move on to the round of 8. If Steamhammer pulls off the upset, it will almost surely lose in the round of 8—it loses 100% against Wuli’s 9-9 gate zealot rush, and nearly as often against Overkill by Sijia Xu. I’ll borrow the bracket image.

SSCAIT 2016 finals bracket