archive by month
Skip to content

Steamhammer at 78%

I looked up Steamhammer’s standing on SSCAIT today and was startled to see it at 78% win rate, equal with Bereaver.

standings of Bereaver and Steamhammer

The winning percentages are volatile and I don’t believe for a minute that Steamhammer is as good as Bereaver. But I have never seen it fluctuate up that high, either. Apparently the 1.0 version is substantially stronger. It is definitely better than 0.2, but how much?

Update: I documented Steamhammer 1.0’s configuration file. Because of the new features I added, somebody already expressed interest in forking from Steamhammer rather than directly from UAlbertaBot. Go for it!

Steamhammer and opponent-specific strategies

Some successful bots play in a way that seems to me so unlike sound strategy that I feel morally obligated to beat them—or at least to try. Soundness must be upheld! I do it at the expense of teaching Steamhammer to play well in the general case, but it still feels worthwhile. It’s a quirk of mine, I guess.

As Steamhammer becomes more reactive I’ll remove some of these handmade counters, and when opening learning is in place I’ll drop all of them. But for now I think this is the way.

versus XIMP

The carriers must be crushed! For Steamhammer 0.2 I came up with a three hatch before pool build that puts a lot of hydralisks on the field, in the hope that they would tear down the cannon wall and win, or failing that, would hold off the carriers and bleed XIMP’s money away. I wrote up the first game. It didn’t quite work out; Steamhammer tore down the cannon wall, but with its other weaknesses did not have enough left over to win convincingly.

In later games, I found that Steamhammer 0.2 wins occasionally, but usually loses. It depends partly on the angle at which the hydras approach the cannons. Steamhammer doesn’t know the first thing about good formation, and when the hydras approach around a corner or otherwise in poor formation, they can’t take the cannons. The bot needs to know how to form up.

And it needs other improvements. It can eventually deal with either the cannons or the carriers, but when it tries to deal with both it loses. The tactical decision making is not there. Plus Steamhammer 0.2 has weak macro and doesn’t make as much stuff as it should.

Steamhammer 1.0 has adequate macro. It sometimes lets its minerals run up, but adds enough hatcheries to recover. Its hydra build is improved. The tactical decisions and formation are not improved, so I’m not sure how games will go.

For fun, I added a second three hatch before pool opening that goes mutas. We’ll see whether Steamhammer’s air power can prevail with adequate muta micro and disorganized scourge usage. XIMP goes corsair when it meets air, so I timed the build to produce the first 9 mutas simultaneously and hit hard before XIMP can react.

Versus XIMP, Steamhammer chooses hydra and muta each 50% of the time. Steamhammer already played 1 game versus XIMP with the air build, but it was on Benzene and Steamhammer’s third hatchery drone got lost due to the back door block—the same problem that Bereaver had against XIMP on Heartbreak Ridge, with the same sad result.

versus Iron

The vultures must be repelled! They don’t make sense as a mainstay against zerg. I tried an opening tailored and timed to beat the vultures without having to equal Iron’s superb micro. When I ran my specialized strat against an early December version of Iron, Steamhammer won 13-2. Iron is updated frequently, so as the tournament deadline approached I tested again against the 14 December version of Iron, and Steamhammer scored 8-7; that’s why I said that it had about equal chances. I messed up and my strat didn’t run during the tournament. But later I tried it against the tournament version of Iron, only a few days newer, and Iron won overwhelmingly! All respect to Igor Dimitrijevic, Iron improves at an incredible rate.

For Steamhammer 1.0 I put in a cheesy exploit strat, not a sound counter strategy. Against the tournament Iron it wins 13-2, losing only on 2-player maps. Iron improves so fast that I’m not sure it will work against the January Iron—and if it does, probably not for long! I have 2 different sound counter strategies in hand, but Steamhammer needs new skills before it will be able to pull them off. Eventually I hope to force Iron to play soundly, and then its amazing ability will be hard to touch.

versus Jakub Trancik

The cannon proxy scares me. If you see the cannons in time, they are no trouble. But what if you don’t see them? Steamhammer is none too sharp at scouting for surprises. In practice Steamhammer 0.2’s 9 pool is successful in stopping the cannons when no bug intervenes.

For Jakub Trancik I went with Steamhammer 1.0’s new “hidden base” skill in a 9 pool expo opening. This is Steamhammer’s only use of a hidden base in a far corner of the map. If the cannons are seen, lings will tear them down. If they are not seen, the zerg main will probably fall sooner or later, but the hidden base will live on. Steamhammer 1.0 is not fully robust, but it can often recover from losing its tech.

Steamhammer 1.0 also plays the 9 pool expo 20% of the time versus random opponents, because why not?

versus GarmBot

Against Aurelien Lermant’s GarmBot, Steamhammer plays its 13 pool opening—an opening that is specialized for play versus terran. By sheer coincidence it is also a strong counter to zerg GarmBot!

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.

the end of the rushbots: rushes and how to defeat them

Today I have a guest post by none other than Martin Rooijackers, on fast rush strategies and how to beat them. The rest of this post is written by Martin Rooijackers, author of terran bot LetaBot.


This Saturday (28 January) will be the last time that my bot will go for a rush strategy. The reason for this is simple, rush strategies will stop working against almost all the top bots. Already most top bots can hold just about anything you can throw at them off 1 base. There are a few holes in their play that I managed to exploit this SSCAI, but after those are patched, any bot will be forced to expand in order to win a game versus a top bot. In the case of my own bot: even with the suboptimal building placement, it can already hold zergling rushes more than 50% of the time. With that improved, Zerg bots will (almost) always lose if they open spawning pool first.

The only use that rush strategies would have after that would be to defeat a bot that rushes to a resource depot (14 CC, 14 Nexus and 3 hatch before pool come to mind). But you can play slightly from behind and still win with better tactics. So I prefer to go that route.

Anyway, here are the rush builds that are still effective, along with some information as to how to stop them

Worker rush

Bots that used it: Stone (predecessor to Iron bot), LetaBot. Any bot that lets its scouting unit attack the enemy workers.

The earliest rush you can do. You take your worker units and let them attack the enemy base. In the early stages of Brood War AI competitions, it sometimes happened that the scouting worker would attack the workers at the mineral line. Since some bots didn’t have any worker defence code, the scouting worker unit would simply destroy the entire work force of the opponent.

After a while, most bots would pull a worker off the mineral line and send it to attack the scouting worker. The problem with that solution was that the opponent could send more than just the scouting worker to attack the opponent.

An example of this happening in a human vs human game is Idra vs AllAboutYou (in SC2).

The first bot to specialize in the worker rush was Stone by Igor Dimitrijevic. Besides the usual attack move and retreat to repair that LetaBot was capable of before in its worker rush, Stone also prioritized SCVs that were building buildings. It got 3rd place in the SSCAI 2015. However it did get defeated by LetaBot (who had a worker rush strategy in it since its very first tournament in CIG 2014).

In 2016, LetaBot used the same strategy to defeat several bots (BeeBot, XelnagaII, Iron Bot, Krasi0). Krasi0 has already improved its worker defence in its latest version.

There are many ways to stop it. The main key is to keep on building worker units no matter what. After all, with this your production of worker units will be at the same rate as your opponent, ensuring that you will always have numerical superiority with which you should be able to hold easily. Don’t rush for a tier 1 combat unit, that only plays into the hand of the one that worker rushes you.

Terran: Watch the LetaBot vs Stone video above. You simply pull back workers that are on low hp. Just follow the regular build order (9 supply, 11 barracks) and pump marines. You will hold it easily

Protoss: Just like terran, pull back worker units that are damaged. Build a pylon when you have 100 minerals, and a gateway when you have 150. You should have a zealot out before the opponent has a critical mass of worker units.

Zerg: Go for a spawning pool when you have 200 minerals, and build a zergling when you have 50 minerals. If you went for a 4 pool, cancel the pool and build 3 worker units immediately.

Bunker Rush:

Bots that used this strategy: LetaBot

The favorite strategy of Boxer.

LetaBot is known for doing this rush back in 2014. But in the case of LetaBot, it was the proxy 5 rax variant instead of the usual marine rush:

Proxy 5 Rax from Liquipedia

Krasi0 vs LetaBot (2014).

To stop it, you shouldn’t go for a fast expansion, since the bunker rush hard counters such a build order. Pull workers to destroy the enemy bunker. More specifically

Terran: Pull SCVs to destroy the enemy bunker and get a bunker of your own on your ramp.

Protoss: Photon cannons out-range bunkers, so get them up asap in case the opponent managed to finish its bunkers. See Bunker Rush (vs. Protoss) from Liquipedia.

Zerg: Zerg Counter to Bunker Rush from Liquipedia.

Barrack Barracks Supply (BBS)

Also known as a marine rush.

Bots that used this strategy: LetaBot, Tyr

Example, LetaBot vs IceBot in SSCAI 2014.

Liquipedia has more information on this one. See Barracks Barracks Supply.

How to counter it:

Terran: Pull SCVs to force the marines back and build a bunker on the ramp. Terran Counter to Barracks Barracks Supply on Liquipedia.

Protoss: Protoss Counter to Barracks Barracks Supply on Liquipedia.

Zerg: Zerg Counter to BBS on Liquipedia.

4 pool

Bots that use it: ZZZKbot, way too many other ones to list here.

A popular strategy where you try to get zerglings as fast as possible. Could be considered the “Hello World” of Brood War Bot programming.

Example, Iron bot vs ZZZKbot in the SSCAI 2016.

counters:

Terran: Terran Counter to 4 Pool from Liquipedia.

Protoss: either 2 gate zealot, or Forge Fast Expand with 2 cannons and probes to block the opening.

Zerg: open with 9 pool and build a sunken colony if needed.

Cannon rush

Bots that use this: Aiur, Jakub Trancik

Example game AIUR vs Bakuryu (human player).

How to stop:

Terran: Make sure to chase the scouting probe to catch any pylon building in the main base. If outside the main base, build a wall and repair it till tanks with siege mode are out.

Protoss: go for a zealot rush.

Zerg: Go for a spawning pool. Then sunken colony or mass zerglings.

Zealot rush

Bots that use it: WuliBot, Dave Churchill, several other protoss bots

Example game: LetaBot vs WuliBot (Ro8 game 2 SSCAI 2016).

counters:

Terran: Terran Counter to Early Protoss Pressure on Liquipedia.

Protoss: 3 Gate Speedzeal (vs. Protoss) on Liquipedia.

Zerg: An overpool is a safe zerg build vs protoss. See Overpool (vs. Protoss) on Liquipedia.

safe build orders

So, with proper scouting, a bot should be able to stop any rush build if it starts out with a safe build order. There are many choices, but my suggestions for each matchup:

TvT: 2 factory build
TvP: Siege expand with a wall
TvZ: 1 rax FE. Build barracks at 10 supply.

PvP: 2 gate. start out with a zealot before dragoon
PvT: 1 gate Core
PvZ: Forge Fast Expand with 2 cannons, or 2 gate zealot

ZvT: It is usually best to go 12 hatch, but if you expect a rush go for 9 pool and put on pressure to come back from behind if terran plays standard.
ZvP: Overpool.
ZvZ: 9 pool into 1 hatch spire.

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.