archive by month
Skip to content

Slater’s progress

I’m pleased with how Slater is coming along. The 2 March update fixed combat problems so that its units fight much more effectively. As I hoped, fixing the biggest weaknesses let it jump up from the bottom ranks.

Slater is still low-ranked, but as it climbs it adds to its roster of upsets.Locutus-Slater on Destination is a deserved win for Slater. Slater denied scouting, leading Locutus to choose a passive build that exposed a weak side to Slater’s strong side. As the chess saying goes, it is not enough to be a good player, you must also play well.

I think it is intentional that Slater likes to hold its army back a little from the front, even when an attack would be safe. A containment strategy puts less immediate pressure on the enemy, but not less pressure overall. If Slater learns to expand beyond 2 bases, the plan can become very strong.

an advantage for playing random on BASIL

And another new bot, the strange random KangarooBot whose builds are... unconventional. Read the github, it’s funny. But that’s not what this post is about.

Here are graphs of Steamhammer’s win rates by matchup followed by Randomhammer’s for the same matchups. Randomhammer is Steamhammer, I upload the same file for both. Any differences in play are due to the learning data—or to the opponent.

Steamhammer win rates Randomhammer win rates

The software has helpfully given the graphs different scales, so here’s the key data in a table.

matchupSteamRandom
ZvZ73%80%
ZvT73%75%
ZvP60%64%

On BASIL, when you play random, the server chooses your race and so both players know it from the beginning. The idea is to negate any advantage from playing random. And yet Randomhammer performs better than Steamhammer in every zerg matchup. Why is that?

Maybe the issue is that other bots do not learn by matchup, but pool all their learning data for each opponent. (Steamhammer learns each matchup independently.) It’s a reasonable thing to do for a traditional random opponent, in which the game chooses the opponent’s race and you don’t know it at first. It amounts to pretending that you don’t know the opponent’s race for learning purposes, so the opponent appears more unpredictable than it should.

Any other theories?

PurpleWave and PurpleDestiny show the same effect for protoss, by the way. MadMix is not as simple. But Randomhammer is the only bot that I’m certain has no configuration differences when playing random.

irradiate trivia question

If you irradiate a unit and the unit then enters a bunker or a transport such as a dropship, then the irradiation continues to function: The unit takes irradiate damage, and the damage splashes to other units in the bunker or transport. (It’s a way to save ground units from splashing their friends with radiation: Pick them up in an empty transport.)

If you irradiate a worker and the worker enters a refinery building, the worker takes no damage while inside. At least that’s what Liquipedia says. Apparently it gets a radiation rest break.

What about a queen infesting a command center? Does it continue to take irradiate damage while inside the command center? Can it die inside without finishing the infestation? Can it successfully infest the command center and not reappear because it died?

It should go without saying that this is critical knowledge that affects the course of every zerg game. It should be on page 1 of all player guides.

new openings in Steamhammer 3.0

Part of my regular change list for each new version is a list of openings I’ve added, so here’s another preview. The list is long this time, but easy to write up. Only a few of these openings are configured for regular play, but all will show up now and again.

Steamhammer’s new proxy skills enable a huge range of cheese builds. I wrote 2 or 3 for each race, sort of as samples.

terran

• In base proxy openings ProxyBBS and ProxyFactory. Ecgberht occasionally pulls out a similar proxy factory, which has scored wins over Steamhammer. These builds annihilate unready opponents but show execution weaknesses if the enemy is able to defend at all. BunkerNatural makes barracks in the center and bunker at the enemy natural, and scouts at a timing so that it just barely works reliably on a 4-player map.

10-10-10Vultures gets vultures earlier than the standard factory timing. I wrote this to test zerg reactions to fast vultures. Might as well keep it.

protoss

Proxy2Gate in a corner of the enemy base, which I haven’t seen another bot try, and CannonRushNatural with dragoon followup, also new to bot play.

zerg

Burrow versions of existing openings, zergling openings with different timings. It is part of my followup on the burrow skill. As burrow gains more uses in the future, these openings should show up more often. 6PoolBurrow 6PoolSpeedBurrow 8Hatch7PoolBurrow 8Hatch7PoolBurrowB 9PoolBurrowB 9PoolSunkBurrow OverpoolBurrow Overpool_3HatchBurrow 9Hatch8PoolBurrow 10HatchBurrow Over10HatchBurrow 3HatchLingBurrow 5HatchPoolLingBurrow.

Proxy openings: Proxy8HatchNatural makes a quick hatchery in the enemy natural and sunkens it up. AntiTyr and AntiTyrLurker are the same idea, but timed much later to counter the Tyr protoss build where it makes cannons in its base and builds up for a timing attack. AntiTyr serenely swallows the attack and wins. Steamhammer still needs a couple more skills before it can do Fried Liverpool.

AntiFactory2 is a slightly faster version of Steamhammer’s longstanding anti-factory opening, with hydralisks first. The original opening was timed to stop a 3-vulture runby, but lately a couple of terrans have been winning with a 2-vulture runby which is earlier and sneaks in before the defenses are ready. AntiFactory2 closes the gap. I configured it as a top answer to factory openings, so it should be fairly frequent.

11Pool is slightly faster than 12 pool for ZvZ, filling a small gap.

10HatchLing and 10HatchLing2 are specialized versions of 10 hatchery 9 pool which go all-in on zerglings.

• Experimental openings: Phlegethon I timed and showed to be inferior. The alternative 2x10Hatch (10 hatchery, 10 hatchery, 9 spawning pool with repeated use of the extractor trick) I have not timed, so in my ignorance I allowed myself to write variants 2x10HatchSlow 2x10HatchAllIn and 2x10HatchBurrow. The less I know, the more fun stuff I get, there’s a lesson!

Over10Hatch11Pool starts with the same extractor trick-overlord-hatchery sequence as a number of common Steamhammer builds, but inserts a couple extra drones before the spawning pool to build up the economy.

• Going the opposite direction, 3 hatcheries before pool but cutting drones to get the hatcheries faster than standard, are 12-11Hatch, 12-12Hatch, and 12-13Hatch. I was pleased with these and added 12-11HatchLing as a zergling all-in version.

2HatchLurkerPure is a variant 2 hatch lurker build which includes minimal zerglings, making drones instead, so that there’s a little more economy. I figure that if opponents lose because they are unprepared with detection, then the zerglings may not help much.

QueenRush and GuardianRush are not really useful except for testing (the only reason I wrote the queen rush in the first place). But then, I used to think the same about the hive rush build, until it scored wins over BananaBrain. At least they’ll be entertaining if they ever show up. The queen rush makes a half dozen queens with broodling nice and early in the game.

AIST S3 pairings

The AIST S3 pairings are out. Normally that would not be worth a post, it’s only first-round pairings. But it immediately struck me that there are 3 mirror matchups out of 4 pairings. It’s the maximum possible; there are 3 terrans and 3 protoss, so not all can be paired with the same race. If that happened to me I would have to step away from the computer, I would be tempted to rerun the pairings to get something “more random”—there’s an irrational reaction!

PurpleWave BananaBrain
McRave zerg Microwave
WillyT Locutus
Dragon LetaBot

How unlikely is it? Doing the combinatorics, I get 8! / (24 * 4!) = 105 possible pairings, of which 9 have the maximum-mirror property—3 ways to choose the protoss player, 3 ways to choose the terran, and after that choice everything else is fixed. That’s about 8.6%, not all that rare. It seemed more surprising than it should have.

Starting with mostly mirrors doesn’t affect the tournament much. We should have slightly fewer than average mirrors in the next round in both the winner’s and loser’s bracket; it will even out over the tournament.

I judge that BananaBrain, McRave, and WillyT have little chance to upset their stronger opponents. I’m excited to see skilled defender LetaBot play against the dangerous harasser Dragon. Is this LetaBot version much improved?

Steamhammer 3.0 game record format

Steamhammer 3.0 is nearly ready, but I’m immoderately busy and don’t know how long it will take for the last adjustments. I can at least slip in short posts to show off some of what’s coming.

Steamhammer 3.0 changes the format of game records in the learning files of the opponent model. The original version 1.4 game record format has been in use since early 2018. The learning file for an opponent is simply a list of game records. Here is the new 3.0 game record format.

meaningitem
game record version3.0
matchupZvP
map(3)Longinus_200.scx
base ID of Steamhammer's start5
base ID of enemy start, 0 if unknown at end of game12
openingOver10Hatch2SunkHard
predicted enemy planHeavy rush
recognized enemy planUnknown
0 for loss, 1 for win0
frame of our first combat unit4382
frame we first gathered gas6814
frame the enemy scouted our base3134
frame the enemy got a combat unit3838
frame the enemy first used gas5162
frame the enemy got an air unit5182
frame the enemy got static anti-air (0 means never)0
frame the enemy got mobile anti-air5182
frame the enemy got a cloaked unit5182
frame the enemy got static detection0
frame the enemy got mobile detection18686
frame the game ended28553
skill kit data (2 skills)gas steal: 0 0 0
unit timings: 60 6161 61 9565 64 2435 65 3815 66 13383 84 18696 154 3240 156 3240 157 3217 160 3263 163 5172 164 3194 165 6575 167 5195
end of the recordEND GAME

As before, the frame number of an event is the frame when Steamhammer first noticed it, not when it happened. They are sometimes very different.

The game record version number can be “1.4” for old records or “3.0” for new records. Steamhammer can read them both and use the data; changing the format doesn’t mean I need to clear out existing learning files. Of course fresh records are written in 3.0 format.

Including the base IDs of the starting positions of the 2 sides means that Steamhammer can pay attention to starting positions. You have to know the map to interpret what the ID numbers mean. The information about enemy gas usage is new and helps with gas steal decisions. The gas steal items from version 1.4 records are moved into the gas steal skill.

There was no gas steal in the example game, so the gas steal recorded 0 0 0 for its data. The skill kit can record data for any number of skills; it is extensible. Each skill’s data is [name of skill]: [data for skill] on one line (the line can be arbitrarily long). The skill needs to know how to write one line of its own data and how to read it back, and that’s all; the central skill kit code takes care of everything else, including rewriting old records without change.

new bot Slater

We got a new bot! We got a new bot! Protoss Slater is not very strong yet, but it has some nice initial skills and I expect it could jump up from the bottom of the ranking with fixes for a few of the bigger weaknesses.

Java bot Slater seems to play a fixed build: One zealot, cybernetics core, unit mix of dragoons and zealots, adding in reavers with shuttles. It’s not an aggressive build, but one that builds up strength over time. The build seems well-executed for a beginner, and Slater is good at keeping its money down. It gets upgrades like dragoon range and shuttle speed. Like most new bots, it is weak in combat skills.

This reaver drop was almost devastating. The reavers are in range, it’s too bad that scarabs can’t reach down the cliff.

reavers cannot attack

Slater pulls nearly all probes to chase the enemy scouting worker in the early game, losing far too much mining time at a critical phase. That might be the biggest easy-to-fix weakness. I recommend ignoring the enemy scout unless it attacks something, at least until you have a unit that can catch it like a dragoon.

Update: Here is Slater’s best game so far, Slater-ZZZKBot by Chris Coxe on Tau Cross, played on BASIL. Slater’s build is adaptive; it played differently against the zergling rush.

SCHNAIL closed beta

For anyone who hasn’t noticed, SCHNAIL is in closed beta as of Friday, and all bot authors are invited. The fronting web site is unfinished, but that should not be an issue for early testers.

I’ll have more to say after I’ve tried it out myself.

Update: When will the mascot character get a proper radula? Mollusk pride!

preview of Steamhammer’s proxy skills

Here is a first look at Steamhammer’s upcoming basic proxy skills. These are features that you can use to write a wide variety of proxy openings. I still have work to do before I can release Steamhammer 3.0, and I’m working slowly so it will be a while. I’m making infrastructure changes that tend to introduce bugs. But I’m looking forward.

New macro locations @ enemy main, @ enemy natural, and @ proxy. Buildings for the enemy main or natural are placed in plain sight—MadMix has played that style of proxy with success against bots, and bunkering the enemy natural is standard in some situations. Buildings placed @ proxy go into a distant corner where they have a chance of remaining unseen. I also fixed a bug so that the existing macro location @ center works reliably.

To build in enemy territory, you need to know where your enemy is. Proxy locations interact with early game scouting. On a 2 player map you can always build at a proxy location. On a larger map, you’ll need to scout early enough to find the enemy first. I recommend go scout location with an early worker; on a 2 player map, the location is known so the scout is not sent. If the enemy is not found by the time building construction is to start, Steamhammer will fall back to building in the center of the map instead.

Post workers to a macro location. You can command a worker to take up a post at a given macro location with, for example, go post worker @ proxy. The posted worker can build at that location (barracks @ proxy) and will remain there for further orders (bunker @ proxy). Release posted workers from a given location with go unpost workers @ proxy or from all locations with go unpost workers. A posted worker is different from a worker that was pulled and assigned to a combat squad; a posted worker expects to build stuff and will not try to fight.

The feature bypasses weaknesses in Steamhammer’s assignment of construction workers. A worker sent a long distance tends to arrive late; by posting the worker in advance you can avoid the loss of time. Also, Steamhammer likes to send one worker per building, so if you ask for 3 forward cannons next to your forward pylon, it will send 3 probes (after all, you need 1 worker per building for Steamhammer’s main race zerg). If you post a probe there instead, special case code makes sure that the one posted probe makes the 3 cannons one after another.

Macro locations work for all actions that they make sense for. It’s essential so that you can control where production occurs: If you have a proxy gate and a gateway in your main, you can get your next zealot from the one you choose with zealot @ main or zealot @ proxy. Or similarly, if you have a forge in an exposed blocking position (which you’ll have to write code to place; Steamhammer doesn’t support that out of the box) and a forge in your main, you can specify ground weapons @ main to get your most important upgrade in a safer place. If you don’t specify, either building might be chosen.

The feature is a little fragile. The priority is to start the requested action as soon as possible, not to start it in the location requested. If you ask for a zealot at your proxy gate and that gate is already busy, the zealot will start in your main instead if that gate is free. That can be inconvenient in a hand-written build order, though it should be little trouble for a build order generated programmatically on the fly.

Proxy locations can block bases. A minor tweak to building placement: Normally Steamhammer places its buildings so that they don’t block any future base you may want to take. If you’re building in enemy territory, you don’t care about that. bunker @ enemy natural will commonly be placed to block the base location.

No maps are special cases in the code; all placement decisions are made by analyzing the map on the fly. I think the decisions are good for a first cut, but there is plenty of room to improve them. I did find one map where a mystery bug causes a worker posted in the center to fail to build there (so that Steamhammer sends a fresh worker from its base), but that’s the only bug I know of. It’s complicated, though, and there are probably a lot of misbehaviors I have yet to see.

The features are enough to support a good range of proxy openings. I wrote two or three for each race, for testing and for the fun of having them. In Steamhammer 3.0 none of them are configured to be played frequently, but they should pop up now and again.

The skill kit will be the other major feature in this release. It may sound independent of proxy skills, but in my mind there is a close connection: I want to write skills to make dynamic decisions about whether and how to proxy. It’s possible, and it doesn’t seem too difficult, to do things like cannon the enemy base differently each game based on analysis of the map and the base layout. Many bots place buildings the same way every game, and that can be exploited too.

Stand by for Steamhammer’s next step up in versatility.

looking ahead to AIST S3

AIST S3 is coming up fast, with play to start on 1 March. As I expected, I did not register Steamhammer (the proxy skills are looking fun, though). Events coincided to leave me extremely busy over the last ten days or so.

Here are the registered players. I sorted them by BASIL elo, so we can take the table as a first guess at the likely winners. Of course participants are likely to have special tournament updates, so it’s only a guess.

racebotelo
protossPurpleWave2878
protossLocutus2805
zergMicrowave2624
protossBananaBrain2610
terranDragon2483
terranWillyT2358
terranLetaBot2255
zergMcRave2153

McRave chooses to participate as zerg, which is interesting—first, that he chose to play, and second, that he chose to play offrace. The sensational news is that LetaBot signed up! That implies that LetaBot is updated to BWAPI 4.4.0, and suggests that the long-awaited bug fixing work may be complete. If so, this version of LetaBot may be much stronger than the BASIL elo of the old version indicates. I’m looking forward!

As usual in recent years, protoss is on top, terran struggles, and zerg is scattered around. We’ll see whether the tournament results agree with that.

8 is a power of 2, so 8 players are a good number for an elimination tournament. AIST S1 had 5 players and S2 had 10, so byes had to be inserted into the pairings. None of that should be needed this time.

The maps for S3 are (2)Overwatch, (2)Tres Pass, (3)Power Bond, (4)Circuit Breaker, (4)Fighting Spirit, and (4)Gladiator. I linked the unfamiliar maps; bots have played on the others before and should be ready. None of the maps has difficult features that call for special-case coding, which must be a relief to participants. The strangest feature is that Tres Pass has a short air distance between the 2 bases and a long ground distance, which is not so strange at all. Power Bond does have a neutral command center on the center platform, which zerg could purloin with a queen (and land elsewhere to make infested terrans), but I doubt either zerg has the skills.

Since Steamhammer is not playing, may the second best bot win!

how to defile

We have the next round of SSCAIT results. I was hoping for Killerbot by Marian Devecka over ZNZZBot, because Steamhammer is a favorite over Killerbot. But ZNZZBot squeaked a win by crash. Steamhammer will face ZNZZ in loser’s round 3 and almost certainly be eliminated. Oh well.

one little plague

A highlight from Steamhammer-Icebot on Fighting Spirit. In old days, Steamhammer would have finished a winning game like this by brute force, with mass ultralisks or mass guardians. The actual ending did feature brute force, but more elegantly applied.

a plague on all your tanks!

Dark swarm ensured that zerglings would break the front. Rubble of bunkers and turrets lies everywhere. Tanks rain splash damage from above, but as zerg reduced the natural, the defiler (selected) threw a plague over every tank. When the mutalisks visited them, the tanks popped like so many bubbles.

Very satisfying. All that work on defilers was worth it.

Steamhammer 3.0 status

Steamhammer 3.0 will be coming out “real soon now,” as we used to say, meaning “later than I ever thought,” thanks to my ambition to throw too many features into it. I originally wanted Steamhammer to participate in AIST S3, but I think I will probably skip it. I don’t expect Steamhammer to be upgraded to BWAPI 4.4.0 and working reliably by then. The last day to register is 15 February, so I have time to change my mind. But I want to concentrate on new features rather than on tightening the bolts for tournament play.

Here’s the main new stuff. I chose to delay work on pathfinding, overlord control, and scouting for now, though they are high priority features for good play, and are on my must-complete list for AIIDE 2020. (No plan survives contact with the next minute.) I suppose they’ll go into versions 3.1 and following. I was more interested in the skill kit and other infrastructure additions; they hardly help Steamhammer in the short run, but lay a foundation for faster improvement in the future. That’s what seems important now, foundation laying.

The skill kit to make the opponent model extensible.
• Basic proxy skills, including new macro locations like @ enemy natural.
Resource tracking to remember minerals and gas on the map.
• Followup on burrow, with many new burrow openings and tweaks to the Watch squad skill.
• Followup on queens, with slightly improved behavior and support for larger queen numbers.

The skill kit feels like my best new idea in a long time, so I was excited to do it right away. The initial skills will be gas steal (replacing the current opponent model code with something a little smarter) and a unit timing skill which doesn’t control anything in itself but only records information for the rest of the program to refer to. They both fit into the framework. I expect the skill kit to speed up strategy work and to help make Steamhammer more and more adaptive over time.

Proxy openings are amazingly complex to execute well. A bot needs building placement skills, the right worker control, and special unit production and tactical skills. Steamhammer 3.0 will have only some of the skills it needs, but it should at least be able to pull off a simple plan like building a barracks in the center and then bunkering the enemy natural. Even that takes infrastructure work that I haven’t finished yet (partly because I changed plans in the middle). The work will bring extra flexibility that will be useful in other situations too. For complex proxies that can’t be written in data as opening build orders but require code—write a skill for the skill kit, that is one of its purposes.

There are also, of course, the usual new openings and small fixes and tweaks.

the story of a trapped drone

In a game Steamhammer - McRaveZ on La Mancha, McRave pulled drones to try to survive.

unsupported drone

But what happened to that one drone that seems to be leaning over the chasm? It collided with other units and was pushed across unwalkable terrain to a new position where it was trapped.

zerglings can’t reach the drone

Steamhammer’s zerglings desperately want to get at that drone, but they can’t... quite... reach it. The zerglings were nearly useless for the rest of the game, distracted by the stuck drone every time they reached the enemy base. Steamhammer floundered until it finally remembered that mutalisks are also a unit it can make.

Here is the walkability map. Steamhammer actually checks that it can reach the drone target, but the check is incorrect in this case: There is a narrow corridor, one walk tile or 8 pixels across, leading to the little platform where the drone waits. No unit is narrow enough to walk there (even a ghost is 15 pixels wide), but Steamhammer doesn’t know that.

8x8 walk tiles

The imprisoned drone illustrates a flaw in the map design: Accidents should not push units into places where they will be permanently trapped. Freakling would never allow such a blunder! Of course it also illustrates a bug in Steamhammer. The bug makes a regular appearance on a few maps, like Fortress, but this is the first time I’ve seen it affect play on a regular SSCAIT map.

Next: Steamhammer status. Soon: Some of Steamhammer’s best recent games.

the strange question of reaching a refinery

You’ve located an enemy refinery building and you want to know whether your ground units can walk there to attack it. Is it on ground that they can reach, or on an island? For other buildings, Steamhammer solves the reachability problem with a partition map: Areas reachable by ground from each other are in the same partition, and 2 simple lookups tell it whether one point is reachable from another.

Refineries are trickier. First of all, a geyser is not walkable ground, so properly speaking it doesn’t belong to any partition. The same for a refinery built on it; every other building is built on ground that can be walked over after the building is destroyed. This actually causes a rare bug in Steamhammer, where the ground squad understands that it cannot attack an island—except for any refinery that may be on the island, it may attempt to attack that! It knows that a refinery building is a special case, but it doesn’t understand the special case correctly.

Of course, the problem can be solved by filling in the partition map for the refinery with the partition that the geyser is in. “Of course”? No, actually it can’t: The geyser itself might be part of the unwalkable terrain between partitions. Imagine a wall on the left and a wall on the right, and a geyser plugging the gap between them. North of the geyser is one partition, south might be a different partition, and there is no way to walk from one to the other. The geyser, and any refinery you build on it, is adjacent to 2 partitions but doesn’t belong to either.

This actually happens in an even more complicated way on maps like Gold Rush and a number of others, where two assimilators form a gate. An assimilator is smaller than a geyser (protoss space warp tech, no doubt), so if you destroy the eggs between the assimilators, units can pass through. But if you destroy the assimilators the gap disappears and the bare geysers block passage. A bot needs sophisticated knowledge of the game to understand the effects.

assimilator gate

I haven’t chosen a way to fix it yet. But it’s clear that geysers and refineries have to be treated with special care.

resource tracking code for everybody

Steamhammer’s new resource tracking code is short and largely independent of the rest of the program, so I decided to release it for anybody to borrow. If your bot is C++, you should be able to drop this in with little effort (using the results is up to you). If your bot is written in NeverHeardOfItScript, you can at least see how to do it. If you want, you may be able to get resource tracking into your bot before the next Steamhammer release.

ResourceInfo.zip includes ResourceInfo.cpp and its header ResourceInfo.h. The 2 files add up to 163 lines and have no dependencies beyond BWAPI (well, they are in namespace UAlbertaBot, but you can strip that out). One instance of ResourceInfo tracks the last known resource amount of one mineral patch or one geyser (for whatever reason, I chose to implement them both in the same class). I believe it handles all cases correctly. When a mineral patch mines out, its associated mineral patch unit disappears, and the code recognizes the missing patch and sets the amount to 0. A mineral patch that starts with 0 minerals causes no confusion. A geyser changes its unit type when a refinery is built on it, and then the associated unit changes entirely if the refinery is destroyed. The code understands all that. It correctly considers the gas amount inaccessible if there is an enemy refinery (I found a cute way to code it so that undoing the workaround is not a trivial one-line change).

There are different ways to integrate ResourceInfo into a program. In Steamhammer, I put it into the information manager. Here’s the declaration of the data structure to hold all the ResourceInfo instances:

// Track a resource container (mineral patch or geyser) by its initial static unit.
std::map<BWAPI::Unit, ResourceInfo> _resources;

It is a map from the static unit of each mineral patch or geyser to the corresponding ResourceInfo instance. It is initialized once on startup like this:

void InformationManager::initializeResources()
{
    for (BWAPI::Unit patch : BWAPI::Broodwar->getStaticMinerals())
    {
        _resources.insert(std::pair<BWAPI::Unit, ResourceInfo>(patch, ResourceInfo(patch)));
    }
    for (BWAPI::Unit geyser : BWAPI::Broodwar->getStaticGeysers())
    {
        _resources.insert(std::pair<BWAPI::Unit, ResourceInfo>(geyser, ResourceInfo(geyser)));
    }
}

Keeping separate data structures for minerals and gas would make as much sense, maybe more. Or you could associate each resource container with the base it belongs to, or however you want to organize it. For example, if all you want to know is how many minerals and gas were last known to exist at a given base, then you could give each base a vector of mineral ResourceInfo instances and another vector for gas, with no need for a map to look up individual patches and geysers. Steamhammer’s data structure is essentially global to allow central updating and general-purpose lookup.

Every frame, you have to update the ResourceInfo instances. It’s fast. With separate mineral and gas data structures, this step would be simpler.

// Update any visible mineral patches or vespene geysers with their remaining amounts.
void InformationManager::updateResources()
{
    for (BWAPI::Unit patch : BWAPI::Broodwar->getStaticMinerals())
    {
        auto it = _resources.find(patch);
        it->second.updateMinerals();
    }
    for (BWAPI::Unit geyser : BWAPI::Broodwar->getStaticGeysers())
    {
        auto it = _resources.find(geyser);
        it->second.updateGas();
    }
}

With Steamhammer’s _resources map, you look up a resource amount by its initial static unit. If you want error checking, throw instead of returning 0 on error.

// Return the last seen resource amount of a mineral patch or vespene geyser.
// NOTE Pass in the static unit of the resource container, or it won't work.
int InformationManager::getResourceAmount(BWAPI::Unit resource) const
{
    auto it = _resources.find(resource);
    if (it == _resources.end())
    {
        return 0;
    }
    return it->second.getAmount();
}

Each Base remembers its own mineral patches and geysers, and it can add up the values for you. No need to repeat that code. The only other piece is the debug drawing, so you can see what the subsystem knows. Might as well throw that in so you don’t have to write your own. It draws a currently visible resource’s amount in white, and an out-of-view resource’s last known amount in blue (mineral) or green (gas) with the last frame that the resource amount was updated.

void InformationManager::drawResourceAmounts() const
{
    const BWAPI::Position offset(-20, -16);
    const BWAPI::Position nextLineOffset(-24, -6);

    for (const std::pair<BWAPI::Unit, ResourceInfo> & r : _resources)
    {
        BWAPI::Position xy = r.first->getInitialPosition();
        if (r.second.isAccessible())
        {
            BWAPI::Broodwar->drawTextMap(xy + offset, "%c%d", white, r.second.getAmount());
        }
        else
        {
            char color = r.second.isMineralPatch() ? cyan : green;
            BWAPI::Broodwar->drawTextMap(xy + offset, "%c%d", color, r.second.getAmount());
            BWAPI::Broodwar->drawTextMap(xy + nextLineOffset, "%c@ %d", color, r.second.getFrame());
        }
    }
}