archive by month
Skip to content

CherryPi - Stardust game

As you might guess from the CoG results, Stardust has gained strong new skills since last year. Against zerg in particular, one skill is that it uses corsairs heavily. Another is that it learned the forge expand opening. And yet it can still be defeated.

CherryPi is from 2017, but it shows how. The SSCAIT game Stardust v CherryPi is instructive (I kept the replay for when SSCAIT recycles it).

The recipe is:

1. Facing forge expand, play a greedy opening. CherryPi opened pool first (with a rare 13 pool) so that it could make zerglings if it needed to, but cannons were not going to walk across the map to attack, so it followed up with drones and hatcheries.

2. Fight efficiently in the middle game. CherryPi could not take any one-sided victories against such a tough opponent, but it traded well and cut the protoss army down to a safe size, where zerg could spawn enough defensive units in the time the protoss would take to cross the map.

3. With that breathing room, zerg could safely pull ahead in workers. Then it was just mass and smash.

See how easy it is? It’s a simple matter of being good at everything!

Of course it was only possible because Stardust showed weaknesses. One is that it was cautious and clumsy with its corsairs, and put on less counter-air pressure than it could have. First it kept them with the army, then it flew them over hydras.

AIIDE 2022 - what Stardust learned

I had forgotten that Stardust had learning in last year’s AIIDE, even though I wrote a post about it which explains how it works and wrote code to summarize its learning results.

I’m short of time, so I haven’t checked whether the recording and use of the data is unchanged. But the data format is unchanged, and my code still works. For each of the three specific events, n is the number of games the event occurred and the times are when it occurred in those games.

firstDarkTemplarCompleted pylonInOurMain firstMutaliskCompleted
opponent games n min median max n min median max n min median max
bananabrain 208 124 5:22 7:37 19:07 0 - - - 0 - - -
dragon 222 0 - - - 0 - - - 0 - - -
steamhammer 207 0 - - - 0 - - - 85 5:00 5:46 8:24
purplewave 214 188 4:55 5:43 17:08 0 - - - 0 - - -
mcrave 210 0 - - - 0 - - - 210 5:50 6:16 10:09
microwave 212 0 - - - 0 - - - 52 4:59 5:19 19:36
ualbertabot 325 13 4:28 4:32 4:43 0 - - - 0 - - -
pylonpuller 211 96 5:11 6:04 8:18 0 - - - 0 - - -
styx 207 0 - - - 0 - - - 0 - - -
cunybot 215 0 - - - 0 - - - 8 5:54 6:26 9:21

Every protoss (including random UAlbertaBot) made dark templar sometimes. The median time is near the minimum time, which suggests that the DTs were often rushed. For BananaBrain and PurpleWave, DTs sometimes came quite late. No protoss proxied in Stardust’s base—it would have been a surprise with any of these opponents. Mutalisks were also often rushed. McRave made mutalisks every game. Styx never made a mutalisk, at least not one that Stardust recorded.

Steamhammer-Stardust game

In this game, Steamhammer hit on a surprising plan that exploited a weakness in Stardust. Even so, Stardust had tricks up its sleeve.

A zerg hatchery in Stardust’s natural, mining away with no defense.

It is strategically correct for the stronger bot to play conservatively, taking no risks. Steamhammer happened across an opening which Stardust answers too conservatively: It proxied in Stardust’s natural. It started the hatchery almost immediately after scouting the location of the protoss base. Correct play is for Stardust to smash the proxy before it can be defended. Stardust scouted the proxy and immediately assumed without evidence that it was contained, so it played conservatively and did not try to break out. The yellow dot in Steamhammer’s natural is the scouting probe: Stardust had all the information it needed to conclude that the proxy was indefensible, but assumed that it was too risky to attack.

The production queues tell each bot’s strategy. Stardust made a robo to escape the containment by air. Also notice the Citadel of Adun. The two cannons next to the nexus are unnecessary; zerg does not have a lair, and protoss could have scouted it but did not bother to. Steamhammer’s opening build assumes blindly that the opponent will not attack—it is a build specialized for defeating one-base protoss players who build up and attack late. Steamhammer is making drones now in preparation for sunkens at the proxy and then a large army.

Speed zealots airlifted out of the protoss base attack the zerg natural.

Both sides have attack +1 already. The citadel was for zealot speed. A shuttle can carry four zealots but only two dragoons. Stardust cleverly elevatored zealots from its main to the north of its base where they would not be seen, and ran them across the map. Steamhammer was ready anyway. Zerglings are about to hatch, and once they joined the hydras the zealots were afraid to engage and ran away. The zealots tried to retreat to the protoss main through the proxy, where sunkens and the zerg army slaughtered them.

Zerg breaks into the protoss base.

After that zerg was in charge. Steamhammer immediately invaded the protoss base, while Stardust airlifted a probe out for a distant hidden base. There was more fighting, but Steamhammer is reliable about winning when this far ahead.

On Jade with its low main, it’s important to defend above the ramp if you can. Otherwise you don’t see what’s coming and you have to fight uphill. But as far as I can see, it didn’t affect this game. Stardust scouted the proxy and did not try to defend its ramp, except for one cannon.

Update: Steamhammer played a second game against Stardust, on the map Python. It went very much the same way. I was right that Jade’s low-ground main did not matter.

Second update, 10 July: Steamhammer has since won a bunch of games that went the same way. Here’s the first game that went differently, on Andromeda. It shows both the strength and the fragility of a strong proxy position.

AIIDE 2021 - Stardust table in minutes and seconds

It occurred to me a little late that many people would find the Stardust data table easier to understand if the frame times were converted to minutes and seconds. So here’s that version. See the previous post from today.

firstDarkTemplarCompleted pylonInOurMain firstMutaliskCompleted
opponent games n min median max n min median max n min median max
bananabrain 155 20 5:15 5:29 16:11 0 - - - 0 - - -
dragon 156 0 - - - 0 - - - 0 - - -
steamhammer 158 0 - - - 0 - - - 17 4:59 5:43 7:11
mcrave 157 0 - - - 0 - - - 124 6:17 7:35 11:12
willyt 157 0 - - - 0 - - - 0 - - -
microwave 157 0 - - - 0 - - - 17 5:07 5:55 7:54
daqin 156 126 5:13 5:29 12:36 2 1:53 1:54 1:55 0 - - -
freshmeat 157 0 - - - 0 - - - 1 11:40 11:40 11:40
ualbertabot 157 17 4:19 4:29 4:36 0 - - - 0 - - -

AIIDE 2021 - Stardust’s learning

I investigated how Stardust’s learning works, and what it learned. It’s unusual, so it was worth a close look.

In its learning file of game records for each opponent, Stardust records values for 3 keys for each game, firstDarkTemplarCompleted, pylonInOurMain, and firstMutaliskCompleted. If the event occurs in the game, the value is the frame time of the event; otherwise the value is 2147483647 (INT_MAX, the largest int value, in this C++ implementation). It also records whether the game was a win or a loss. It records the hash of the map, too, but that doesn’t seem to be used again.

summarizing the data

The class Opponent is responsible for providing the learned information to the rest of the bot. It summarizes the game records via two routines.

  int minValueInPreviousGames(const std::string &key, int defaultNoData, int maxCount = INT_MAX, int minCount = 0);

If there are at least minCount games, then look through the game records, most recent first, for up to maxCount games. Look up the key for each game and return its minimum value, or the default value if there are none. This amounts to finding the earliest frame at which the event happened, or the default if it did not happen in the specified number of games.

   double winLossRatio(double defaultValue, int maxCount = INT_MAX);

Look through the game records, most recent first, for up to maxCount games and return the winning ratio, or the default value if there are no games yet.

using the summarized data

Each of the 3 keys is used in exactly one place in the code. Here is where firstDarkTemplarCompleted is looked up in the PvP strategy code:

    if (Opponent::winLossRatio(0.0, 200) < 0.99)
    {
        expectedCompletionFrame = Opponent::minValueInPreviousGames("firstDarkTemplarCompleted", 7300, 15, 10);
    }

This means “If we’re rolling you absolutely flat (at least 99% wins in the last 200 games), then it doesn’t matter. Otherwise there’s some risk. In the most recent 15 games, find the earliest frame that the first enemy dark templar was (estimated to be) completed, or return frame 7300 if none.” The default frame 7300 is not the earliest a DT can emerge; they can be on the map over a thousand frames earlier. So it is not a worst-case assumption. Further code overrides the frame number if there is scouting information related to dark templar production. It attempts to build a defensive photon cannon just in time for the enemy DT’s arrival, and sometimes to get an observer.

The key pylonInOurMain is part of cannon rush defense. Stardust again checks the win ratio and again looks back 15 games with a minimum game count of 10, this time with a default of 0 if there are not enough games. It starts scouting its base 500 frames (about 21 seconds) ahead of the earliest seen enemy pylon appearing in its base, which may be never. The idea is that Stardust doesn’t waste time scouting its own base if it hasn’t seen you proxy a pylon in the last 15 games, and delays the scout if the pylon is proxied late.

The key firstMutaliskCompleted is used very similarly, to decide whether and when to defend each nexus with cannons. The goal is to get cannons in time in case mutalisks arrive without being scouted. There are simple rules to decide how many cannons at each nexus:

    // Main and natural are special cases, we only get cannons there to defend against air threats
    if (base == Map::getMyMain() || base == Map::getMyNatural())
    {
        if (enemyAirUnits > 6) return 4;
        if (enemyAirThreat) return 3;
        if (enemyDropThreat && BWAPI::Broodwar->getFrameCount() > 8000) return 1;
        return 0;
    }

    // At expansions we get cannons if the enemy is not contained or has an air threat
    if (!Strategist::isEnemyContained() || enemyAirUnits > 0) return 2;
    if (enemyAirThreat || enemyDropThreat) return 1;
    return 0;

If the firstMutaliskCompleted check says that it’s time, it sets enemyAirThreat to true and makes 3 cannons each at main and natural, and at least 1 at each other base.

the data itself

Here’s my summary of the data in Stardust’s files. The files include prepared data. I left the prepared data out; this covers only what was recorded during the tournament. The tournament was run for 157 rounds, although the official results are given after round 150. The table here is data for all 157 rounds. I don’t have a way to tell which unrecorded games were from rounds 1-150 and which were from 151-157... though I think I could guess.

n is the number of games for which a value (other than 2147483647) was recorded for the key. The values are frame numbers.

firstDarkTemplarCompleted pylonInOurMain firstMutaliskCompleted
opponent games n min median max n min median max n min median max
bananabrain 155 20 7579 7897.5 23319 0 - - - 0 - - -
dragon 156 0 - - - 0 - - - 0 - - -
steamhammer 158 0 - - - 0 - - - 17 7188 8241 10355
mcrave 157 0 - - - 0 - - - 124 9070 10939 16146
willyt 157 0 - - - 0 - - - 0 - - -
microwave 157 0 - - - 0 - - - 17 7371 8534 11397
daqin 156 126 7533 7912.5 18154 2 2721 2743.5 2766 0 - - -
freshmeat 157 0 - - - 0 - - - 1 16801 16801 16801
ualbertabot 157 17 6230 6477 6627 0 - - - 0 - - -

As you might expect after deep contemplation of the nature of reality, only protoss makes dark templar or proxy pylons, and only zerg makes mutalisks. Nothing interesting was recorded for the terran opponents.

Notice that UAlbertaBot sometimes makes dark templar much earlier than the no-data 7300 frame default time; the others do not. DaQin is recorded as twice placing a proxy pylon in Stardust’s main. I didn’t think it ever did that. I guess it’s a holdover from the Locutus proxy pylon play, to trick opponents into overreacting? DaQin made DTs in most games, and McRave went mutalisks in most games. FreshMeat is recorded as having made a mutalisk (or more than one) in exactly one game, which seems unusual.

Steamhammer > Stardust

Today on BASIL, Steamhammer scored its first win over Stardust in months (no exaggeration). Steamhammer-Stardust on Destination is a zergling rush and not that interesting, other than in being a rare win.

The build is the 6 pool speed opening that I wrote up in 2018. It is a little slower than 5 pool, in exchange for getting zergling speed to make the lings more dangerous. I suspect the build may have winning chances only on 2-player maps, because Steamhammer has to drone scout on bigger maps to find the enemy, and sending 1 drone out of only 6 total gives up a lot.

Did Stardust misdiagnose the opening because it saw the gas being mined? If so, I don’t think it was critical. After zerglings broke in, the probes felt unsafe and stopped mining; that was the real mistake. I think it’s a good guess that the overreaction has been fixed in the CoG 2021 version.

Stardust-BetaStar game

Steamhammer continues its tradition of starting strongly: It has scored 7-0 in its first games. That is better than Stardust at 6-1. The winning streak will, I’m sure everyone agrees, unquestionably continue for the rest of the tournament, unless of course Steamhammer grows bored with winning. I imagine that many people have seen the Stardust loss, because it is interesting for more than one reason. To me, the key point is that Stardust took island bases.

The game is Stardust-BetaStar on Andromeda. Stardust started a shuttle immediately after its observatory, and the shuttle picked up a probe and headed out immediately, at about 8:10 into the game. By this time, Stardust had already fallen behind in worker count and army size, because its build was not as efficient—which is interesting too, since BetaStar was derived from Locutus. The shuttle headed straight back home to pick up 8 more probes, setting Stardust further behind in economy; after the transfer its main was not mining at full strength, and the timing was too early because the probes arrived at the island base before it was finished and couldn’t mine there at first either.

As soon as the island base finished and the 9 probes there had turned in their first mineral cargoes, the shuttle picked up one of them and carried it due north to the other Andromeda island base. That base too started as soon as minerals allowed, and the shuttle returned—not to the main, but to the south island base. It picked up a load of probes there, leaving the south island severely undersaturated, and flew north straight into BetaStar’s moving dragoons, being shot down with no attempt to evade. Ouch.

Meanwhile BetaStar was in Stardust’s main, starting to take things apart. Given that Stardust intended to take the second island, transferring probes from the main would have made more sense. The rest of the game was boring. BetaStar had only dragoons and never found the island bases, so they were invulnerable. Stardust mined its islands and watched dragoons move around the map with its observer, but never attempted to rebuild. At the end of the game, neither island had as much as a pylon on it. And, curiously, neither island mined gas. Overall, taking the islands contributed to Stardust’s loss, but it didn’t seem like an entirely bad idea because the islands were untouchable. In a longer game, the extra bases would have paid off.

That tells us something about the author of Stardust and Locutus. Bruce Nielsen is like me in one way: We are both willing to enable cool but half-baked features in serious games.

Here is a BASIL game where Stardust started to take an island, though the game ends before the nexus is placed: Stardust-XIAOYICOG2019 on Python. The SSCAIT maps include 3 maps (of 14 used) with islands: Andromeda, Empire of the Sun, and Python.